Scaffold full-stack volunteer scheduling application
Go backend with domain-based packages (volunteer, schedule, timeoff, checkin, notification), SQLite storage, JWT auth, and chi router. React TypeScript frontend with routing, auth context, and pages for all core features. Multi-stage Dockerfile and docker-compose included. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
124
web/src/api.ts
Normal file
124
web/src/api.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
const BASE = '/api/v1';
|
||||
|
||||
function getToken(): string | null {
|
||||
return localStorage.getItem('token');
|
||||
}
|
||||
|
||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
const token = getToken();
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (res.status === 204) return undefined as T;
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Request failed');
|
||||
return data as T;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
// Auth
|
||||
login: (email: string, password: string) =>
|
||||
request<{ token: string }>('POST', '/auth/login', { email, password }),
|
||||
register: (name: string, email: string, password: string, role = 'volunteer') =>
|
||||
request<Volunteer>('POST', '/auth/register', { name, email, password, role }),
|
||||
|
||||
// Volunteers
|
||||
listVolunteers: () => request<Volunteer[]>('GET', '/volunteers'),
|
||||
getVolunteer: (id: number) => request<Volunteer>('GET', `/volunteers/${id}`),
|
||||
updateVolunteer: (id: number, data: Partial<Volunteer>) =>
|
||||
request<Volunteer>('PUT', `/volunteers/${id}`, data),
|
||||
|
||||
// Schedules
|
||||
listSchedules: () => request<Schedule[]>('GET', '/schedules'),
|
||||
createSchedule: (data: CreateScheduleInput) => request<Schedule>('POST', '/schedules', data),
|
||||
updateSchedule: (id: number, data: Partial<CreateScheduleInput>) =>
|
||||
request<Schedule>('PUT', `/schedules/${id}`, data),
|
||||
deleteSchedule: (id: number) => request<void>('DELETE', `/schedules/${id}`),
|
||||
|
||||
// Time off
|
||||
listTimeOff: () => request<TimeOffRequest[]>('GET', '/timeoff'),
|
||||
createTimeOff: (data: CreateTimeOffInput) => request<TimeOffRequest>('POST', '/timeoff', data),
|
||||
reviewTimeOff: (id: number, status: 'approved' | 'rejected') =>
|
||||
request<TimeOffRequest>('PUT', `/timeoff/${id}/review`, { status }),
|
||||
|
||||
// Check-in / out
|
||||
checkIn: (schedule_id?: number, notes?: string) =>
|
||||
request<CheckIn>('POST', '/checkin', { schedule_id, notes }),
|
||||
checkOut: (notes?: string) => request<CheckIn>('POST', '/checkout', { notes }),
|
||||
getHistory: () => request<CheckIn[]>('GET', '/checkin/history'),
|
||||
|
||||
// Notifications
|
||||
listNotifications: () => request<Notification[]>('GET', '/notifications'),
|
||||
markRead: (id: number) => request<Notification>('PUT', `/notifications/${id}/read`, {}),
|
||||
};
|
||||
|
||||
export interface Volunteer {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
role: 'admin' | 'volunteer';
|
||||
active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
id: number;
|
||||
volunteer_id: number;
|
||||
title: string;
|
||||
starts_at: string;
|
||||
ends_at: string;
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateScheduleInput {
|
||||
volunteer_id?: number;
|
||||
title: string;
|
||||
starts_at: string;
|
||||
ends_at: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface TimeOffRequest {
|
||||
id: number;
|
||||
volunteer_id: number;
|
||||
starts_at: string;
|
||||
ends_at: string;
|
||||
reason?: string;
|
||||
status: 'pending' | 'approved' | 'rejected';
|
||||
reviewed_by?: number;
|
||||
reviewed_at?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateTimeOffInput {
|
||||
starts_at: string;
|
||||
ends_at: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface CheckIn {
|
||||
id: number;
|
||||
volunteer_id: number;
|
||||
schedule_id?: number;
|
||||
checked_in_at: string;
|
||||
checked_out_at?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
volunteer_id: number;
|
||||
message: string;
|
||||
read: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
Reference in New Issue
Block a user