Kyle Pope d811890509 Add Sheet forms, recurrence UI, all-day fix, LocationPicker
- 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>
2026-02-22 00:42:12 +08:00

135 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 { Location } 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';
import LocationPicker from '@/components/ui/location-picker';
interface LocationFormProps {
location: Location | null;
onClose: () => void;
}
export default function LocationForm({ location, onClose }: LocationFormProps) {
const queryClient = useQueryClient();
const [formData, setFormData] = useState({
name: location?.name || '',
address: location?.address || '',
category: location?.category || 'other',
notes: location?.notes || '',
});
const mutation = useMutation({
mutationFn: async (data: typeof formData) => {
if (location) {
const response = await api.put(`/locations/${location.id}`, data);
return response.data;
} else {
const response = await api.post('/locations', data);
return response.data;
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['locations'] });
toast.success(location ? 'Location updated' : 'Location created');
onClose();
},
onError: (error) => {
toast.error(getErrorMessage(error, location ? 'Failed to update location' : 'Failed to create location'));
},
});
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
mutation.mutate(formData);
};
return (
<Sheet open={true} onOpenChange={onClose}>
<SheetContent>
<SheetClose onClick={onClose} />
<SheetHeader>
<SheetTitle>{location ? 'Edit Location' : 'New Location'}</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="name">Name</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="address">Address</Label>
<LocationPicker
id="address"
value={formData.address}
onChange={(val) => setFormData({ ...formData, address: val })}
onSelect={(result) => {
setFormData({
...formData,
name: formData.name || result.name,
address: result.address,
});
}}
placeholder="Search or type an address..."
/>
</div>
<div className="space-y-2">
<Label htmlFor="category">Category</Label>
<Select
id="category"
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value as any })}
>
<option value="home">Home</option>
<option value="work">Work</option>
<option value="restaurant">Restaurant</option>
<option value="shop">Shop</option>
<option value="other">Other</option>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="notes">Notes</Label>
<Textarea
id="notes"
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
rows={4}
/>
</div>
</div>
<SheetFooter>
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Saving...' : location ? 'Update' : 'Create'}
</Button>
</SheetFooter>
</form>
</SheetContent>
</Sheet>
);
}