Add frontend API client and authentication context
This commit is contained in:
@@ -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<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...((options.headers as Record<string, string>) || {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
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<Event[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEvent(id: string) {
|
||||||
|
return this.fetch(`/events/${id}`) as Promise<Event>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createEvent(event: Partial<Event>) {
|
||||||
|
return this.fetch('/events', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(event),
|
||||||
|
}) as Promise<Event>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEvent(id: string, event: Partial<Event>) {
|
||||||
|
return this.fetch(`/events/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(event),
|
||||||
|
}) as Promise<Event>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Registration>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMyRegistrations() {
|
||||||
|
return this.fetch('/registrations/my-registrations') as Promise<Registration[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventRegistrations(eventId: string) {
|
||||||
|
return this.fetch(`/registrations/event/${eventId}`) as Promise<Registration[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelRegistration(id: string) {
|
||||||
|
return this.fetch(`/registrations/${id}/cancel`, {
|
||||||
|
method: 'POST',
|
||||||
|
}) as Promise<Registration>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<PaymentReport>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Announcements
|
||||||
|
async getEventAnnouncements(eventId: string) {
|
||||||
|
return this.fetch(`/announcements/event/${eventId}`) as Promise<Announcement[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAnnouncement(eventId: string, title: string, content: string) {
|
||||||
|
return this.fetch('/announcements', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ eventId, title, content }),
|
||||||
|
}) as Promise<Announcement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
async getOrganizerDashboard() {
|
||||||
|
return this.fetch('/dashboard/organizer');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getParticipantDashboard() {
|
||||||
|
return this.fetch('/dashboard/participant');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = new ApiClient();
|
||||||
@@ -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<void>;
|
||||||
|
register: (email: string, password: string, name: string, role: 'Organizer' | 'Participant') => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(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 (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{
|
||||||
|
user,
|
||||||
|
isLoading,
|
||||||
|
isAuthenticated: !!user,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
error,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useAuth must be used within an AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user