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:
2026-03-05 11:25:02 -04:00
parent 64f4563bfa
commit 4989ff1061
49 changed files with 19996 additions and 12 deletions

124
web/src/api.ts Normal file
View 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;
}