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:
106
web/src/pages/Schedules.tsx
Normal file
106
web/src/pages/Schedules.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { useEffect, useState, FormEvent } from 'react';
|
||||
import { api, Schedule } from '../api';
|
||||
import { useAuth } from '../auth';
|
||||
|
||||
export default function Schedules() {
|
||||
const { role } = useAuth();
|
||||
const [schedules, setSchedules] = useState<Schedule[]>([]);
|
||||
const [error, setError] = useState('');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [form, setForm] = useState({ title: '', starts_at: '', ends_at: '', notes: '' });
|
||||
|
||||
useEffect(() => {
|
||||
api.listSchedules().then(setSchedules).catch(() => setError('Could not load schedules.'));
|
||||
}, []);
|
||||
|
||||
async function handleCreate(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
try {
|
||||
const sc = await api.createSchedule(form);
|
||||
setSchedules(prev => [...prev, sc]);
|
||||
setForm({ title: '', starts_at: '', ends_at: '', notes: '' });
|
||||
setShowForm(false);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
if (!window.confirm('Delete this schedule?')) return;
|
||||
try {
|
||||
await api.deleteSchedule(id);
|
||||
setSchedules(prev => prev.filter(s => s.id !== id));
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="page-header">
|
||||
<h2>Schedules</h2>
|
||||
{role === 'admin' && (
|
||||
<button onClick={() => setShowForm(v => !v)}>
|
||||
{showForm ? 'Cancel' : 'Add Shift'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
{showForm && (
|
||||
<form className="card" onSubmit={handleCreate}>
|
||||
<h3>New Shift</h3>
|
||||
<label>
|
||||
Title
|
||||
<input value={form.title} onChange={e => setForm(f => ({ ...f, title: e.target.value }))} required />
|
||||
</label>
|
||||
<label>
|
||||
Starts At
|
||||
<input type="datetime-local" value={form.starts_at} onChange={e => setForm(f => ({ ...f, starts_at: e.target.value }))} required />
|
||||
</label>
|
||||
<label>
|
||||
Ends At
|
||||
<input type="datetime-local" value={form.ends_at} onChange={e => setForm(f => ({ ...f, ends_at: e.target.value }))} required />
|
||||
</label>
|
||||
<label>
|
||||
Notes
|
||||
<textarea value={form.notes} onChange={e => setForm(f => ({ ...f, notes: e.target.value }))} />
|
||||
</label>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{schedules.length === 0 ? (
|
||||
<p>No schedules found.</p>
|
||||
) : (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Starts</th>
|
||||
<th>Ends</th>
|
||||
<th>Notes</th>
|
||||
{role === 'admin' && <th>Actions</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{schedules.map(s => (
|
||||
<tr key={s.id}>
|
||||
<td>{s.title}</td>
|
||||
<td>{new Date(s.starts_at).toLocaleString()}</td>
|
||||
<td>{new Date(s.ends_at).toLocaleString()}</td>
|
||||
<td>{s.notes ?? '—'}</td>
|
||||
{role === 'admin' && (
|
||||
<td>
|
||||
<button className="btn-danger btn-small" onClick={() => handleDelete(s.id)}>Delete</button>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user