diff --git a/frontend/src/components/event-list.tsx b/frontend/src/components/event-list.tsx new file mode 100644 index 0000000..3bd7385 --- /dev/null +++ b/frontend/src/components/event-list.tsx @@ -0,0 +1,134 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { api, Event } from '@/lib/api'; + +export function EventList() { + const [events, setEvents] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [category, setCategory] = useState(''); + const [status, setStatus] = useState(''); + + useEffect(() => { + loadEvents(); + }, [category, status]); + + const loadEvents = async () => { + try { + setIsLoading(true); + const data = await api.getEvents({ + category: category || undefined, + status: status || undefined, + }); + setEvents(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load events'); + } finally { + setIsLoading(false); + } + }; + + const categories = ['Running', 'Cycling', 'Triathlon', 'Trail', 'Road']; + + if (isLoading) { + return
Loading events...
; + } + + if (error) { + return ( +
+ {error} +
+ ); + } + + return ( +
+ {/* Filters */} +
+ + + +
+ + {/* Events Grid */} + {events.length === 0 ? ( +
No events found
+ ) : ( +
+ {events.map((event) => ( +
+
+ + {event.status} + + {event.category && ( + {event.category} + )} +
+ +

{event.name}

+

{event.description}

+ +
+
+ 📅 + {new Date(event.eventDate).toLocaleDateString()} +
+
+ 📍 + {event.location} +
+
+ 👥 + {event.currentRegistrations} + {event.maxParticipants && ` / ${event.maxParticipants}`} registered +
+
+ + {event.tags.length > 0 && ( +
+ {event.tags.map((tag) => ( + + {tag} + + ))} +
+ )} + + + View Details + +
+ ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/login-form.tsx b/frontend/src/components/login-form.tsx new file mode 100644 index 0000000..fd8b0bf --- /dev/null +++ b/frontend/src/components/login-form.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { useState } from 'react'; +import { useAuth } from '@/lib/auth-context'; +import { useRouter } from 'next/navigation'; + +export function LoginForm() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const { login, error } = useAuth(); + const router = useRouter(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + + try { + await login(email, password); + router.push('/dashboard'); + } catch (err) { + // Error is handled by auth context + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+

Login

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(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" + /> +
+ +
+ + setPassword(e.target.value)} + required + minLength={8} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + +
+ +

+ Don't have an account?{' '} + + Register + +

+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/register-form.tsx b/frontend/src/components/register-form.tsx new file mode 100644 index 0000000..ca24740 --- /dev/null +++ b/frontend/src/components/register-form.tsx @@ -0,0 +1,145 @@ +'use client'; + +import { useState } from 'react'; +import { useAuth } from '@/lib/auth-context'; +import { useRouter } from 'next/navigation'; + +export function RegisterForm() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [name, setName] = useState(''); + const [role, setRole] = useState<'Organizer' | 'Participant'>('Participant'); + const [isSubmitting, setIsSubmitting] = useState(false); + const [validationError, setValidationError] = useState(null); + const { register, error } = useAuth(); + const router = useRouter(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setValidationError(null); + + if (password !== confirmPassword) { + setValidationError('Passwords do not match'); + return; + } + + if (password.length < 8) { + setValidationError('Password must be at least 8 characters'); + return; + } + + setIsSubmitting(true); + + try { + await register(email, password, name, role); + router.push('/dashboard'); + } catch (err) { + // Error is handled by auth context + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+

Register

+ + {(error || validationError) && ( +
+ {error || validationError} +
+ )} + +
+
+ + 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" + /> +
+ +
+ + setEmail(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" + /> +
+ +
+ + setPassword(e.target.value)} + required + minLength={8} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + required + minLength={8} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ +
+ + +
+ + +
+ +

+ Already have an account?{' '} + + Login + +

+
+ ); +} \ No newline at end of file