diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..b01c360 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,245 @@ +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api'; + +// Types +export interface User { + id: string; + email: string; + name: string; + role: 'Organizer' | 'Participant'; +} + +export interface AuthResponse { + token: string; + user: User; +} + +export interface Event { + id: string; + name: string; + description: string; + eventDate: string; + location: string; + status: 'Draft' | 'Published' | 'Cancelled' | 'Completed'; + category?: string; + tags: string[]; + maxParticipants?: number; + currentRegistrations: number; + createdAt: string; + updatedAt: string; + organizer: User; +} + +export interface Registration { + id: string; + eventId: string; + eventName: string; + eventDate: string; + participantId: string; + participantName: string; + participantEmail: string; + status: string; + category?: string; + emergencyContact?: string; + createdAt: string; + updatedAt?: string; + totalPaid: number; + amountDue: number; +} + +export interface Announcement { + id: string; + eventId: string; + eventName: string; + title: string; + content: string; + authorId: string; + authorName: string; + createdAt: string; + updatedAt?: string; + isPublished: boolean; +} + +export interface PaymentReport { + eventId: string; + eventName: string; + totalCollected: number; + totalPending: number; + totalOutstanding: number; + totalRegistrations: number; + paidRegistrations: number; + partialRegistrations: number; + unpaidRegistrations: number; +} + +// API Client +class ApiClient { + private token: string | null = null; + + constructor() { + if (typeof window !== 'undefined') { + this.token = localStorage.getItem('token'); + } + } + + setToken(token: string) { + this.token = token; + if (typeof window !== 'undefined') { + localStorage.setItem('token', token); + } + } + + clearToken() { + this.token = null; + if (typeof window !== 'undefined') { + localStorage.removeItem('token'); + } + } + + getToken(): string | null { + return this.token; + } + + private async fetch(endpoint: string, options: RequestInit = {}) { + const url = `${API_URL}${endpoint}`; + const headers: Record = { + 'Content-Type': 'application/json', + ...((options.headers as Record) || {}), + }; + + if (this.token) { + headers['Authorization'] = `Bearer ${this.token}`; + } + + const response = await fetch(url, { + ...options, + headers, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ error: 'Unknown error' })); + throw new Error(error.error || `HTTP ${response.status}`); + } + + if (response.status === 204) { + return null; + } + + return response.json(); + } + + // Auth + async register(email: string, password: string, name: string, role: 'Organizer' | 'Participant' = 'Participant') { + const data = await this.fetch('/auth/register', { + method: 'POST', + body: JSON.stringify({ email, password, name, role }), + }); + this.setToken(data.token); + return data as AuthResponse; + } + + async login(email: string, password: string) { + const data = await this.fetch('/auth/login', { + method: 'POST', + body: JSON.stringify({ email, password }), + }); + this.setToken(data.token); + return data as AuthResponse; + } + + logout() { + this.clearToken(); + } + + // Events + async getEvents(filters?: { category?: string; status?: string; fromDate?: string; toDate?: string }) { + const params = new URLSearchParams(); + if (filters?.category) params.append('category', filters.category); + if (filters?.status) params.append('status', filters.status); + if (filters?.fromDate) params.append('fromDate', filters.fromDate); + if (filters?.toDate) params.append('toDate', filters.toDate); + + const query = params.toString(); + return this.fetch(`/events${query ? `?${query}` : ''}`) as Promise; + } + + async getEvent(id: string) { + return this.fetch(`/events/${id}`) as Promise; + } + + async createEvent(event: Partial) { + return this.fetch('/events', { + method: 'POST', + body: JSON.stringify(event), + }) as Promise; + } + + async updateEvent(id: string, event: Partial) { + return this.fetch(`/events/${id}`, { + method: 'PUT', + body: JSON.stringify(event), + }) as Promise; + } + + async deleteEvent(id: string) { + return this.fetch(`/events/${id}`, { + method: 'DELETE', + }); + } + + // Registrations + async createRegistration(eventId: string, category?: string, emergencyContact?: string) { + return this.fetch('/registrations', { + method: 'POST', + body: JSON.stringify({ eventId, category, emergencyContact }), + }) as Promise; + } + + async getMyRegistrations() { + return this.fetch('/registrations/my-registrations') as Promise; + } + + async getEventRegistrations(eventId: string) { + return this.fetch(`/registrations/event/${eventId}`) as Promise; + } + + async cancelRegistration(id: string) { + return this.fetch(`/registrations/${id}/cancel`, { + method: 'POST', + }) as Promise; + } + + // Payments + async recordPayment(registrationId: string, amount: number, method: string, transactionId?: string, notes?: string) { + return this.fetch('/payments', { + method: 'POST', + body: JSON.stringify({ registrationId, amount, method, transactionId, notes }), + }); + } + + async getPaymentReport(eventId: string) { + return this.fetch(`/payments/event/${eventId}/report`) as Promise; + } + + // Announcements + async getEventAnnouncements(eventId: string) { + return this.fetch(`/announcements/event/${eventId}`) as Promise; + } + + async createAnnouncement(eventId: string, title: string, content: string) { + return this.fetch('/announcements', { + method: 'POST', + body: JSON.stringify({ eventId, title, content }), + }) as Promise; + } + + // Dashboard + async getOrganizerDashboard() { + return this.fetch('/dashboard/organizer'); + } + + async getParticipantDashboard() { + return this.fetch('/dashboard/participant'); + } +} + +export const api = new ApiClient(); \ No newline at end of file diff --git a/frontend/src/lib/auth-context.tsx b/frontend/src/lib/auth-context.tsx new file mode 100644 index 0000000..0d9119e --- /dev/null +++ b/frontend/src/lib/auth-context.tsx @@ -0,0 +1,92 @@ +'use client'; + +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { api, User, AuthResponse } from './api'; + +interface AuthContextType { + user: User | null; + isLoading: boolean; + isAuthenticated: boolean; + login: (email: string, password: string) => Promise; + register: (email: string, password: string, name: string, role: 'Organizer' | 'Participant') => Promise; + logout: () => void; + error: string | null; +} + +const AuthContext = createContext(undefined); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + // Check for existing token on mount + const token = api.getToken(); + if (token) { + // Token exists, user is authenticated + // In a real app, you might want to validate the token + setIsLoading(false); + } else { + setIsLoading(false); + } + }, []); + + const login = async (email: string, password: string) => { + try { + setError(null); + setIsLoading(true); + const response = await api.login(email, password); + setUser(response.user); + } catch (err) { + setError(err instanceof Error ? err.message : 'Login failed'); + throw err; + } finally { + setIsLoading(false); + } + }; + + const register = async (email: string, password: string, name: string, role: 'Organizer' | 'Participant') => { + try { + setError(null); + setIsLoading(true); + const response = await api.register(email, password, name, role); + setUser(response.user); + } catch (err) { + setError(err instanceof Error ? err.message : 'Registration failed'); + throw err; + } finally { + setIsLoading(false); + } + }; + + const logout = () => { + api.logout(); + setUser(null); + setError(null); + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} \ No newline at end of file