feature/new-raceplanner-app #1

Merged
MasterMito merged 12 commits from feature/new-raceplanner-app into main 2026-04-03 21:47:59 +02:00
2 changed files with 337 additions and 0 deletions
Showing only changes of commit b54e2265d9 - Show all commits
+245
View File
@@ -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();
+92
View File
@@ -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;
}