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