diff --git a/package.json b/package.json index 71bc8ea..e18c3c4 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-hover-card": "^1.1.13", "@radix-ui/react-label": "^2.1.6", - "@radix-ui/react-scroll-area": "^1.2.8", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.4", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", @@ -48,13 +49,18 @@ "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", "lucide-react": "^0.515.0", "next": "15.3.4", "next-auth": "^5.0.0-beta.25", + "next-swagger-doc": "^0.4.1", "next-themes": "^0.4.6", "react": "^19.0.0", + "react-day-picker": "^9.7.0", "react-dom": "^19.0.0", "react-hook-form": "^7.56.4", + "sonner": "^2.0.5", "swagger-ui-react": "^5.24.1", "tailwind-merge": "^3.2.0", "zod": "^3.25.60" @@ -80,7 +86,7 @@ "ts-node": "10.9.2", "tsconfig-paths": "4.2.0", "tw-animate-css": "1.3.4", - "typescript": "5.8.3" + "typescript": "^5.8.3" }, "packageManager": "yarn@4.9.2" } diff --git a/src/app/(main)/events/[eventID]/page.tsx b/src/app/(main)/events/[eventID]/page.tsx new file mode 100644 index 0000000..bfc390d --- /dev/null +++ b/src/app/(main)/events/[eventID]/page.tsx @@ -0,0 +1,238 @@ +'use client'; + +import React, { useState } from 'react'; +import Logo from '@/components/misc/logo'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { + useDeleteApiEventEventID, + useGetApiEventEventID, +} from '@/generated/api/event/event'; +import { useGetApiUserMe } from '@/generated/api/user/user'; +import { RedirectButton } from '@/components/buttons/redirect-button'; +import { useSession } from 'next-auth/react'; +import ParticipantListEntry from '@/components/custom-ui/participant-list-entry'; +import { useParams, useRouter } from 'next/navigation'; +import { Button } from '@/components/ui/button'; +import { ToastInner } from '@/components/misc/toast-inner'; +import { toast } from 'sonner'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; + +export default function ShowEvent() { + const session = useSession(); + const router = useRouter(); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + + const { eventID: eventID } = useParams<{ eventID: string }>(); + + // Fetch event data + const { data: eventData, isLoading, error } = useGetApiEventEventID(eventID); + const { data: userData, isLoading: userLoading } = useGetApiUserMe(); + const deleteEvent = useDeleteApiEventEventID(); + + if (isLoading || userLoading) { + return ( +
+ Loading... +
+ ); + } + if (error || !eventData?.data?.event) { + return ( +
+ Error loading event. +
+ ); + } + + const event = eventData.data.event; + const organiserName = userData?.data.user?.name || 'Unknown User'; + + // Format dates & times for display + const formatDate = (isoString?: string) => { + if (!isoString) return '-'; + return new Date(isoString).toLocaleDateString(); + }; + const formatTime = (isoString?: string) => { + if (!isoString) return '-'; + return new Date(isoString).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }); + }; + + return ( +
+ + + + +
+
+
+
+ +
+
+

+ {event.title || 'Untitled Event'} +

+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+ + +
+
+
+ {' '} +
+ {event.participants?.map((user) => ( + + ))} +
+
+
+ +
+
+ {session.data?.user?.id === event.organizer.id ? ( + + + + + + + Delete Event + + Are you sure you want to delete the event “ + {event.title}”? This action cannot be undone. + + + + + + + + + ) : null} +
+
+ {session.data?.user?.id === event.organizer.id ? ( + + ) : null} +
+
+
+
+
+
+
+ ); +} diff --git a/src/app/(main)/events/edit/[eventID]/page.tsx b/src/app/(main)/events/edit/[eventID]/page.tsx new file mode 100644 index 0000000..42c6e8b --- /dev/null +++ b/src/app/(main)/events/edit/[eventID]/page.tsx @@ -0,0 +1,24 @@ +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import EventForm from '@/components/forms/event-form'; +import { Suspense } from 'react'; + +export default async function Page({ + params, +}: { + params: Promise<{ eventID: string }>; +}) { + const eventID = (await params).eventID; + return ( +
+ + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/events/new/page.tsx b/src/app/(main)/events/new/page.tsx new file mode 100644 index 0000000..2db7ae2 --- /dev/null +++ b/src/app/(main)/events/new/page.tsx @@ -0,0 +1,21 @@ +import { ThemePicker } from '@/components/misc/theme-picker'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import EventForm from '@/components/forms/event-form'; +import { Suspense } from 'react'; + +export default function NewEvent() { + return ( +
+
{}
+ + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/events/page.tsx b/src/app/(main)/events/page.tsx new file mode 100644 index 0000000..f0391dd --- /dev/null +++ b/src/app/(main)/events/page.tsx @@ -0,0 +1,54 @@ +'use client'; + +import { RedirectButton } from '@/components/buttons/redirect-button'; +import EventListEntry from '@/components/custom-ui/event-list-entry'; +import { Label } from '@/components/ui/label'; +import { useGetApiEvent } from '@/generated/api/event/event'; + +export default function Events() { + const { data: eventsData, isLoading, error } = useGetApiEvent(); + + if (isLoading) return
Loading...
; + if (error) + return ( +
Error loading events
+ ); + + const events = eventsData?.data?.events || []; + + return ( +
+ {/* Heading */} +

+ My Events +

+ + {/* Scrollable event list */} +
+
+ {events.length > 0 ? ( + events.map((event) => ( + + )) + ) : ( +
+ + +
+ )} +
+
+
+ ); +} diff --git a/src/app/(main)/home/page.tsx b/src/app/(main)/home/page.tsx index c381c03..66a97d8 100644 --- a/src/app/(main)/home/page.tsx +++ b/src/app/(main)/home/page.tsx @@ -15,6 +15,7 @@ export default function Home() { + ); diff --git a/src/app/globals.css b/src/app/globals.css index 93a24ce..bc18178 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -9,6 +9,7 @@ --font-heading: 'Comfortaa', sans-serif; --font-label: 'Varela Round', sans-serif; --font-button: 'Varela Round', sans-serif; + --font-sans: var(--font-label); --transparent: transparent; @@ -28,7 +29,7 @@ --background: var(--neutral-800); --background-reversed: var(--neutral-000); - --base: var(--neutral-800); + --basecl: var(--neutral-800); --text: var(--neutral-000); --text-alt: var(--neutral-900); --text-input: var(--text); @@ -49,11 +50,23 @@ --active-secondary: oklch(0.4254 0.133 272.15); --disabled-secondary: oklch(0.4937 0.1697 271.26 / 0.5); + --destructive: oklch(60.699% 0.20755 25.945); + --hover-destructive: oklch(60.699% 0.20755 25.945 / 0.8); + --active-destructive: oklch(50.329% 0.17084 25.842); + --disabled-destructive: oklch(60.699% 0.20755 25.945 / 0.4); + --muted: var(--color-neutral-700); --hover-muted: var(--color-neutral-600); --active-muted: var(--color-neutral-400); --disabled-muted: var(--color-neutral-400); + --toaster-default-bg: var(--color-neutral-150); + --toaster-success-bg: oklch(54.147% 0.09184 144.208); + --toaster-error-bg: oklch(52.841% 0.10236 27.274); + --toaster-info-bg: oklch(44.298% 0.05515 259.369); + --toaster-warning-bg: oklch(61.891% 0.07539 102.943); + --toaster-notification-bg: var(--color-neutral-150); + --card: var(--neutral-800); --sidebar-width-icon: 32px; @@ -80,8 +93,6 @@ --accent-foreground: oklch(0.21 0.034 264.665); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.928 0.006 264.531); --input: oklch(0.928 0.006 264.531); @@ -115,6 +126,62 @@ --sidebar-ring: oklch(0.707 0.022 261.325); } +h1 { + font-family: var(--font-heading); + font-size: 40px; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +h2 { + font-family: var(--font-heading); + font-size: 36px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +h3 { + font-family: var(--font-heading); + font-size: 32px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +h4 { + font-family: var(--font-heading); + font-size: 28px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +h5 { + font-family: var(--font-heading); + font-size: 26px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +h6 { + font-family: var(--font-heading); + font-size: 20px; + font-style: normal; + font-weight: 600; + line-height: normal; +} + +p { + font-family: var(--font-label); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; +} + @font-face { font-family: 'Comfortaa'; font-style: normal; @@ -153,7 +220,7 @@ --color-background: var(--neutral-750); --color-background-reversed: var(--background-reversed); - --color-base: var(--neutral-800); + --color-basecl: var(--neutral-800); --color-text: var(--text); --color-text-alt: var(--text-alt); --color-text-input: var(--text-input); @@ -175,11 +242,23 @@ --color-active-secondary: var(--active-secondary); --color-disabled-secondary: var(--disabled-secondary); + --color-destructive: var(--destructive); + --color-hover-destructive: var(--hover-destructive); + --color-active-destructive: var(--active-destructive); + --color-disabled-destructive: var(--disabled-destructive); + --color-muted: var(--muted); --color-hover-muted: var(--hover-muted); --color-active-muted: var(--active-muted); --color-disabled-muted: var(--disabled-muted); + --color-toaster-default-bg: var(--toaster-default-bg); + --color-toaster-success-bg: var(--toaster-success-bg); + --color-toaster-error-bg: var(--toaster-error-bg); + --color-toaster-info-bg: var(--toaster-info-bg); + --color-toaster-warning-bg: var(--toaster-warning-bg); + --color-toaster-notification-bg: var(--toaster-notification-bg); + /* Custom values */ --radius-sm: calc(var(--radius) - 4px); @@ -220,8 +299,6 @@ --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); --color-input: var(--input); @@ -277,7 +354,7 @@ --background: var(--neutral-750); --background-reversed: var(--neutral-000); - --base: var(--neutral-750); + --basecl: var(--neutral-750); --text: var(--neutral-000); --text-alt: var(--neutral-900); --text-input: var(--text); @@ -297,11 +374,23 @@ --active-secondary: oklch(0.4471 0.15 271.61); --disabled-secondary: oklch(0.6065 0.213 271.11 / 0.4); + --destructive: oklch(0.58 0.2149 27.13); + --hover-destructive: oklch(0.58 0.2149 27.13 / 0.8); + --active-destructive: oklch(45.872% 0.16648 26.855); + --disabled-destructive: oklch(0.58 0.2149 27.13 / 0.4); + --muted: var(--color-neutral-650); --hover-muted: var(--color-neutral-500); --active-muted: var(--color-neutral-400); --disabled-muted: var(--color-neutral-400); + --toaster-default-bg: var(--color-neutral-150); + --toaster-success-bg: var(--color-green-200); + --toaster-error-bg: var(--color-red-200); + --toaster-info-bg: var(--color-blue-200); + --toaster-warning-bg: var(--color-yellow-200); + --toaster-notification-bg: var(--color-neutral-150); + --card: var(--neutral-750); /* ------------------- */ @@ -326,8 +415,6 @@ --accent-foreground: oklch(0.985 0.002 247.839); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index af40867..47cec2d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,8 @@ import { ThemeProvider } from '@/components/wrappers/theme-provider'; import type { Metadata } from 'next'; import './globals.css'; import { QueryProvider } from '@/components/wrappers/query-provider'; +import { Toaster } from '@/components/ui/sonner'; +import { SessionProvider } from 'next-auth/react'; export const metadata: Metadata = { title: 'MeetUp', @@ -50,14 +52,17 @@ export default function RootLayout({ - - {children} - + + + {children} + + + ); diff --git a/src/assets/usericon/default/default-user-icon_dark.svg b/src/assets/usericon/default/default-user-icon_dark.svg new file mode 100644 index 0000000..b2a1cfb --- /dev/null +++ b/src/assets/usericon/default/default-user-icon_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/usericon/default/default-user-icon_light.svg b/src/assets/usericon/default/default-user-icon_light.svg new file mode 100644 index 0000000..60ba6d0 --- /dev/null +++ b/src/assets/usericon/default/default-user-icon_light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/usericon/default/defaultusericon-export.tsx b/src/assets/usericon/default/defaultusericon-export.tsx new file mode 100644 index 0000000..d1b482d --- /dev/null +++ b/src/assets/usericon/default/defaultusericon-export.tsx @@ -0,0 +1,2 @@ +export { default as user_default_dark } from '@/assets/usericon/default/default-user-icon_dark.svg'; +export { default as user_default_light } from '@/assets/usericon/default/default-user-icon_light.svg'; diff --git a/src/components/buttons/redirect-button.tsx b/src/components/buttons/redirect-button.tsx index c4bf997..e67acc1 100644 --- a/src/components/buttons/redirect-button.tsx +++ b/src/components/buttons/redirect-button.tsx @@ -1,16 +1,18 @@ -import { Button } from '../ui/button'; +import { Button } from '@/components/ui/button'; import Link from 'next/link'; export function RedirectButton({ redirectUrl, buttonText, + className, }: { redirectUrl: string; buttonText: string; + className?: string; }) { return ( - + ); } diff --git a/src/components/custom-ui/event-list-entry.tsx b/src/components/custom-ui/event-list-entry.tsx new file mode 100644 index 0000000..197649b --- /dev/null +++ b/src/components/custom-ui/event-list-entry.tsx @@ -0,0 +1,68 @@ +import { Card } from '@/components/ui/card'; +import Logo from '@/components/misc/logo'; +import { Label } from '@/components/ui/label'; +import Link from 'next/link'; +import zod from 'zod/v4'; +import { EventSchema } from '@/app/api/event/validation'; + +type EventListEntryProps = zod.output; + +export default function EventListEntry({ + title, + id, + start_time, + end_time, + location, +}: EventListEntryProps) { + const formatDate = (isoString?: string) => { + if (!isoString) return '-'; + return new Date(isoString).toLocaleDateString(); + }; + const formatTime = (isoString?: string) => { + if (!isoString) return '-'; + return new Date(isoString).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }); + }; + return ( + + +
+
+ +
+
+

{title}

+
+
+
+ + +
+
+ + +
+ {location && ( +
+ + +
+ )} +
+
+
+ + ); +} diff --git a/src/components/custom-ui/labeled-input.tsx b/src/components/custom-ui/labeled-input.tsx index ea26e51..4746a31 100644 --- a/src/components/custom-ui/labeled-input.tsx +++ b/src/components/custom-ui/labeled-input.tsx @@ -1,4 +1,4 @@ -import { Input } from '@/components/ui/input'; +import { Input, Textarea } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; export default function LabeledInput({ @@ -7,6 +7,7 @@ export default function LabeledInput({ placeholder, value, name, + variantSize = 'default', autocomplete, error, ...rest @@ -16,22 +17,37 @@ export default function LabeledInput({ placeholder?: string; value?: string; name?: string; + variantSize?: 'default' | 'big' | 'textarea'; autocomplete?: string; error?: string; } & React.InputHTMLAttributes) { return (
- - + {variantSize === 'textarea' ? ( +