This commit is contained in:
2026-03-29 21:57:23 -05:00
parent 35ed5223a0
commit 420321c9a9
12 changed files with 1620 additions and 3 deletions
@@ -0,0 +1,146 @@
import { useState, useEffect } from 'react';
import { Trash2 } from 'lucide-react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { api, type Chore } from '@/lib/api';
import { useMembers } from '@/hooks/useMembers';
import { Modal } from '@/components/ui/Modal';
import { Input } from '@/components/ui/Input';
import { Textarea } from '@/components/ui/Textarea';
import { Select } from '@/components/ui/Select';
import { Button } from '@/components/ui/Button';
const RECURRENCE = [
{ value: 'none', label: 'One-time' },
{ value: 'daily', label: 'Daily' },
{ value: 'weekly', label: 'Weekly' },
{ value: 'monthly', label: 'Monthly' },
];
interface Props {
open: boolean;
onClose: () => void;
chore?: Chore | null;
}
export function ChoreModal({ open, onClose, chore }: Props) {
const qc = useQueryClient();
const { data: members = [] } = useMembers();
const isEdit = !!chore;
const blank = () => ({ title: '', description: '', member_id: '', recurrence: 'none', due_date: '' });
const [form, setForm] = useState(blank);
const [error, setError] = useState('');
useEffect(() => {
if (!open) return;
if (chore) {
setForm({
title: chore.title,
description: chore.description ?? '',
member_id: chore.member_id ? String(chore.member_id) : '',
recurrence: chore.recurrence,
due_date: chore.due_date ?? '',
});
} else {
setForm(blank());
}
setError('');
}, [open, chore]);
const set = (k: keyof typeof form, v: string) => setForm((f) => ({ ...f, [k]: v }));
const saveMutation = useMutation({
mutationFn: (body: object) =>
isEdit
? api.put(`/chores/${chore!.id}`, body).then((r) => r.data)
: api.post('/chores', body).then((r) => r.data),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['chores'] }); onClose(); },
onError: () => setError('Failed to save. Please try again.'),
});
const deleteMutation = useMutation({
mutationFn: () => api.delete(`/chores/${chore!.id}`),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['chores'] }); onClose(); },
});
const handleSubmit = () => {
if (!form.title.trim()) { setError('Title is required'); return; }
saveMutation.mutate({
title: form.title.trim(),
description: form.description || null,
member_id: form.member_id ? Number(form.member_id) : null,
recurrence: form.recurrence,
due_date: form.due_date || null,
});
};
return (
<Modal open={open} onClose={onClose} title={isEdit ? 'Edit Chore' : 'New Chore'}>
<div className="space-y-4">
<Input
label="Title"
value={form.title}
onChange={(e) => set('title', e.target.value)}
placeholder="What needs to be done?"
autoFocus
/>
<div className="grid grid-cols-2 gap-4">
<Select
label="Assigned To"
value={form.member_id}
onChange={(e) => set('member_id', e.target.value)}
>
<option value="">Unassigned</option>
{members.map((m) => (
<option key={m.id} value={m.id}>{m.name}</option>
))}
</Select>
<Select
label="Repeats"
value={form.recurrence}
onChange={(e) => set('recurrence', e.target.value)}
>
{RECURRENCE.map((r) => (
<option key={r.value} value={r.value}>{r.label}</option>
))}
</Select>
</div>
<div>
<label className="text-sm font-medium text-secondary block mb-1.5">Due Date</label>
<input
type="date"
value={form.due_date}
onChange={(e) => set('due_date', e.target.value)}
className="w-full rounded-lg border border-theme bg-surface-raised px-3 py-2 text-sm text-primary focus:outline-none focus:ring-2 ring-accent transition-colors"
/>
</div>
<Textarea
label="Notes"
value={form.description}
onChange={(e) => set('description', e.target.value)}
placeholder="Optional details…"
rows={2}
/>
{error && <p className="text-sm text-red-500">{error}</p>}
<div className="flex items-center gap-2 pt-1">
{isEdit && (
<Button
variant="ghost"
onClick={() => deleteMutation.mutate()}
loading={deleteMutation.isPending}
className="text-red-500 hover:bg-red-50 dark:hover:bg-red-950"
>
<Trash2 size={16} />
</Button>
)}
<div className="flex gap-2 ml-auto">
<Button variant="secondary" onClick={onClose}>Cancel</Button>
<Button onClick={handleSubmit} loading={saveMutation.isPending}>
{isEdit ? 'Save Changes' : 'Add Chore'}
</Button>
</div>
</div>
</div>
</Modal>
);
}