From 44d5e53d24d314e791bab399571c75a246e704fe Mon Sep 17 00:00:00 2001 From: Imole Date: Fri, 29 May 2026 13:58:25 +0000 Subject: [PATCH] feat: add volunteer management with SMS integration (#3) --- src/app/api/volunteers/route.ts | 39 ++++++++++++++++++++ src/app/store/volunteerStore.ts | 65 +++++++++++++++++++++++++++++++++ src/types/volunteer.ts | 26 +++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 src/app/api/volunteers/route.ts create mode 100644 src/app/store/volunteerStore.ts create mode 100644 src/types/volunteer.ts diff --git a/src/app/api/volunteers/route.ts b/src/app/api/volunteers/route.ts new file mode 100644 index 00000000..2b1b9e90 --- /dev/null +++ b/src/app/api/volunteers/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from 'next/server'; +import type { Volunteer } from '@/types/volunteer'; + +// In-memory store for edge runtime (production would use a DB) +const volunteers: Volunteer[] = []; + +export const runtime = 'edge'; + +export async function GET() { + return NextResponse.json({ data: volunteers, total: volunteers.length }); +} + +export async function POST(request: Request) { + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); + } + + const { name, email, role, status, sms } = body as Partial; + + if (!name || !email || !role) { + return NextResponse.json({ error: 'name, email, and role are required' }, { status: 400 }); + } + + const volunteer: Volunteer = { + id: `vol_${Math.random().toString(36).slice(2)}_${Date.now()}`, + name, + email, + role, + status: status ?? 'pending', + sms: sms ?? { optedIn: false, phoneNumber: '', categories: [] }, + joinedAt: new Date().toISOString(), + }; + + volunteers.push(volunteer); + return NextResponse.json({ data: volunteer }, { status: 201 }); +} diff --git a/src/app/store/volunteerStore.ts b/src/app/store/volunteerStore.ts new file mode 100644 index 00000000..76305f92 --- /dev/null +++ b/src/app/store/volunteerStore.ts @@ -0,0 +1,65 @@ +import { create } from 'zustand'; +import type { Volunteer, VolunteerSMSPreferences } from '@/types/volunteer'; + +const STORAGE_KEY = 'volunteers_v1'; + +function load(key: string, fallback: T): T { + if (typeof window === 'undefined') return fallback; + try { + const raw = localStorage.getItem(key); + return raw ? (JSON.parse(raw) as T) : fallback; + } catch { + return fallback; + } +} + +function save(key: string, value: T) { + if (typeof window === 'undefined') return; + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch {} +} + +interface VolunteerState { + volunteers: Volunteer[]; + addVolunteer: (v: Omit) => Volunteer; + updateVolunteer: (id: string, patch: Partial>) => void; + removeVolunteer: (id: string) => void; + updateSMSPreferences: (id: string, sms: Partial) => void; +} + +export const useVolunteerStore = create((set, get) => ({ + volunteers: load(STORAGE_KEY, []), + + addVolunteer: (v) => { + const volunteer: Volunteer = { + ...v, + id: `vol_${Math.random().toString(36).slice(2)}_${Date.now()}`, + joinedAt: new Date().toISOString(), + }; + const next = [...get().volunteers, volunteer]; + set({ volunteers: next }); + save(STORAGE_KEY, next); + return volunteer; + }, + + updateVolunteer: (id, patch) => { + const next = get().volunteers.map((v) => (v.id === id ? { ...v, ...patch } : v)); + set({ volunteers: next }); + save(STORAGE_KEY, next); + }, + + removeVolunteer: (id) => { + const next = get().volunteers.filter((v) => v.id !== id); + set({ volunteers: next }); + save(STORAGE_KEY, next); + }, + + updateSMSPreferences: (id, sms) => { + const next = get().volunteers.map((v) => + v.id === id ? { ...v, sms: { ...v.sms, ...sms } } : v, + ); + set({ volunteers: next }); + save(STORAGE_KEY, next); + }, +})); diff --git a/src/types/volunteer.ts b/src/types/volunteer.ts new file mode 100644 index 00000000..69063c2f --- /dev/null +++ b/src/types/volunteer.ts @@ -0,0 +1,26 @@ +export type VolunteerRole = 'mentor' | 'moderator' | 'content_reviewer' | 'event_coordinator'; + +export type VolunteerStatus = 'active' | 'inactive' | 'pending'; + +export interface VolunteerSMSPreferences { + optedIn: boolean; + phoneNumber: string; + /** Notification categories the volunteer wants via SMS */ + categories: Array<'assignment' | 'reminder' | 'urgent' | 'general'>; +} + +export interface Volunteer { + id: string; + name: string; + email: string; + role: VolunteerRole; + status: VolunteerStatus; + sms: VolunteerSMSPreferences; + joinedAt: string; // ISO +} + +export interface VolunteerSMSPayload { + volunteerId: string; + category: VolunteerSMSPreferences['categories'][number]; + message: string; +}