From e6430e855b52c9dfdb5d5a3f01b65e65e027e5f9 Mon Sep 17 00:00:00 2001 From: Denis Urs Rudolph Date: Fri, 3 Apr 2026 21:26:58 +0200 Subject: [PATCH] Add event detail/form components and page routes --- frontend/src/app/events/[eventId]/page.tsx | 19 +++ frontend/src/app/events/create/page.tsx | 12 ++ frontend/src/app/events/page.tsx | 23 +++ frontend/src/app/login/page.tsx | 9 + frontend/src/app/register/page.tsx | 9 + frontend/src/components/event-detail.tsx | 151 +++++++++++++++++ frontend/src/components/event-form.tsx | 187 +++++++++++++++++++++ 7 files changed, 410 insertions(+) create mode 100644 frontend/src/app/events/[eventId]/page.tsx create mode 100644 frontend/src/app/events/create/page.tsx create mode 100644 frontend/src/app/events/page.tsx create mode 100644 frontend/src/app/login/page.tsx create mode 100644 frontend/src/app/register/page.tsx create mode 100644 frontend/src/components/event-detail.tsx create mode 100644 frontend/src/components/event-form.tsx diff --git a/frontend/src/app/events/[eventId]/page.tsx b/frontend/src/app/events/[eventId]/page.tsx new file mode 100644 index 0000000..cfa0d0b --- /dev/null +++ b/frontend/src/app/events/[eventId]/page.tsx @@ -0,0 +1,19 @@ +import { EventDetail } from '@/components/event-detail'; + +interface EventPageProps { + params: Promise<{ + eventId: string; + }>; +} + +export default async function EventPage({ params }: EventPageProps) { + const { eventId } = await params; + + return ( +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/app/events/create/page.tsx b/frontend/src/app/events/create/page.tsx new file mode 100644 index 0000000..e4f30da --- /dev/null +++ b/frontend/src/app/events/create/page.tsx @@ -0,0 +1,12 @@ +import { EventForm } from '@/components/event-form'; + +export default function CreateEventPage() { + return ( +
+
+

Create Event

+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/app/events/page.tsx b/frontend/src/app/events/page.tsx new file mode 100644 index 0000000..3734980 --- /dev/null +++ b/frontend/src/app/events/page.tsx @@ -0,0 +1,23 @@ +import { EventList } from '@/components/event-list'; +import { Suspense } from 'react'; + +export default function EventsPage() { + return ( +
+
+
+

Events

+ + Create Event + +
+ Loading...
}> + + +
+ + ); +} \ No newline at end of file diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx new file mode 100644 index 0000000..9cf484a --- /dev/null +++ b/frontend/src/app/login/page.tsx @@ -0,0 +1,9 @@ +import { LoginForm } from '@/components/login-form'; + +export default function LoginPage() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/app/register/page.tsx b/frontend/src/app/register/page.tsx new file mode 100644 index 0000000..404d9bd --- /dev/null +++ b/frontend/src/app/register/page.tsx @@ -0,0 +1,9 @@ +import { RegisterForm } from '@/components/register-form'; + +export default function RegisterPage() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/event-detail.tsx b/frontend/src/components/event-detail.tsx new file mode 100644 index 0000000..29de50d --- /dev/null +++ b/frontend/src/components/event-detail.tsx @@ -0,0 +1,151 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { api, Event } from '@/lib/api'; +import { useAuth } from '@/lib/auth-context'; + +interface EventDetailProps { + eventId: string; +} + +export function EventDetail({ eventId }: EventDetailProps) { + const [event, setEvent] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { user } = useAuth(); + + useEffect(() => { + loadEvent(); + }, [eventId]); + + const loadEvent = async () => { + try { + setIsLoading(true); + const data = await api.getEvent(eventId); + setEvent(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load event'); + } finally { + setIsLoading(false); + } + }; + + if (isLoading) { + return
Loading...
; + } + + if (error) { + return ( +
+ {error} +
+ ); + } + + if (!event) { + return
Event not found
; + } + + const isOrganizer = user?.id === event.organizer.id; + const canEdit = isOrganizer || user?.role === 'Organizer'; + + return ( +
+
+
+
+ + {event.status} + +

{event.name}

+
+ {canEdit && ( + + Edit Event + + )} +
+ +
+
+
+ 📅 +
+

Date

+

{new Date(event.eventDate).toLocaleString()}

+
+
+
+ 📍 +
+

Location

+

{event.location}

+
+
+
+ 👥 +
+

Registrations

+

+ {event.currentRegistrations} registered + {event.maxParticipants && ` / ${event.maxParticipants} max`} +

+
+
+
+ +
+ {event.category && ( +
+ 🏷️ +
+

Category

+

{event.category}

+
+
+ )} +
+ 👤 +
+

Organizer

+

{event.organizer.name}

+
+
+ {event.tags.length > 0 && ( +
+ {event.tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+
+ +
+

Description

+

{event.description}

+
+ + {user?.role === 'Participant' && event.status === 'Published' && ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/event-form.tsx b/frontend/src/components/event-form.tsx new file mode 100644 index 0000000..30f76b3 --- /dev/null +++ b/frontend/src/components/event-form.tsx @@ -0,0 +1,187 @@ +'use client'; + +import { useState } from 'react'; +import { api, Event } from '@/lib/api'; +import { useRouter } from 'next/navigation'; + +interface EventFormProps { + event?: Event; +} + +export function EventForm({ event }: EventFormProps) { + const [name, setName] = useState(event?.name || ''); + const [description, setDescription] = useState(event?.description || ''); + const [eventDate, setEventDate] = useState(event ? new Date(event.eventDate).toISOString().slice(0, 16) : ''); + const [location, setLocation] = useState(event?.location || ''); + const [category, setCategory] = useState(event?.category || ''); + const [maxParticipants, setMaxParticipants] = useState(event?.maxParticipants?.toString() || ''); + const [tags, setTags] = useState(event?.tags.join(', ') || ''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const router = useRouter(); + + const categories = ['Running', 'Cycling', 'Triathlon', 'Trail', 'Road']; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setIsSubmitting(true); + + try { + const eventData = { + name, + description, + eventDate: new Date(eventDate).toISOString(), + location, + category: category || undefined, + maxParticipants: maxParticipants ? parseInt(maxParticipants) : undefined, + tags: tags.split(',').map(t => t.trim()).filter(t => t), + }; + + if (event) { + await api.updateEvent(event.id, eventData); + } else { + await api.createEvent(eventData); + } + + router.push('/events'); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to save event'); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+
+ + setName(e.target.value)} + required + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ +
+ +