- Sheet component: slide-in panel replacing Dialog for all forms - EventForm: structured recurrence picker, all-day end-date offset fix, LocationPicker with OSM search integration - CalendarPage: scope dialog for editing/deleting recurring events - TodoForm/ReminderForm/LocationForm: migrated to Sheet with 2-col layouts - LocationPicker: debounced search combining local DB + Nominatim results - Backend: /locations/search endpoint with OSM proxy - CSS: slimmer all-day event bars in calendar grid - Types: RecurrenceRule interface, extended CalendarEvent fields Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
import { useState, FormEvent } from 'react';
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { toast } from 'sonner';
|
|
import api, { getErrorMessage } from '@/lib/api';
|
|
import type { Reminder } from '@/types';
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
SheetFooter,
|
|
SheetClose,
|
|
} from '@/components/ui/sheet';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Select } from '@/components/ui/select';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
interface ReminderFormProps {
|
|
reminder: Reminder | null;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export default function ReminderForm({ reminder, onClose }: ReminderFormProps) {
|
|
const queryClient = useQueryClient();
|
|
const [formData, setFormData] = useState({
|
|
title: reminder?.title || '',
|
|
description: reminder?.description || '',
|
|
remind_at: reminder?.remind_at ? reminder.remind_at.slice(0, 16) : '',
|
|
recurrence_rule: reminder?.recurrence_rule || '',
|
|
});
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: async (data: typeof formData) => {
|
|
if (reminder) {
|
|
const response = await api.put(`/reminders/${reminder.id}`, data);
|
|
return response.data;
|
|
} else {
|
|
const response = await api.post('/reminders', data);
|
|
return response.data;
|
|
}
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['reminders'] });
|
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
|
|
queryClient.invalidateQueries({ queryKey: ['upcoming'] });
|
|
toast.success(reminder ? 'Reminder updated' : 'Reminder created');
|
|
onClose();
|
|
},
|
|
onError: (error) => {
|
|
toast.error(getErrorMessage(error, reminder ? 'Failed to update reminder' : 'Failed to create reminder'));
|
|
},
|
|
});
|
|
|
|
const handleSubmit = (e: FormEvent) => {
|
|
e.preventDefault();
|
|
mutation.mutate(formData);
|
|
};
|
|
|
|
return (
|
|
<Sheet open={true} onOpenChange={onClose}>
|
|
<SheetContent>
|
|
<SheetClose onClick={onClose} />
|
|
<SheetHeader>
|
|
<SheetTitle>{reminder ? 'Edit Reminder' : 'New Reminder'}</SheetTitle>
|
|
</SheetHeader>
|
|
<form onSubmit={handleSubmit} className="flex flex-col flex-1 overflow-y-auto">
|
|
<div className="px-6 py-5 space-y-4 flex-1">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="title">Title</Label>
|
|
<Input
|
|
id="title"
|
|
value={formData.title}
|
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="description">Description</Label>
|
|
<Textarea
|
|
id="description"
|
|
value={formData.description}
|
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
|
rows={4}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="remind_at">Remind At</Label>
|
|
<Input
|
|
id="remind_at"
|
|
type="datetime-local"
|
|
value={formData.remind_at}
|
|
onChange={(e) => setFormData({ ...formData, remind_at: e.target.value })}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="recurrence">Recurrence</Label>
|
|
<Select
|
|
id="recurrence"
|
|
value={formData.recurrence_rule}
|
|
onChange={(e) => setFormData({ ...formData, recurrence_rule: e.target.value })}
|
|
>
|
|
<option value="">None</option>
|
|
<option value="daily">Daily</option>
|
|
<option value="weekly">Weekly</option>
|
|
<option value="monthly">Monthly</option>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<SheetFooter>
|
|
<Button type="button" variant="outline" onClick={onClose}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={mutation.isPending}>
|
|
{mutation.isPending ? 'Saving...' : reminder ? 'Update' : 'Create'}
|
|
</Button>
|
|
</SheetFooter>
|
|
</form>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
}
|