- Remove instant invalid:ring/border from Input component (was showing red outline on empty required fields before any interaction) - Add CSS rule: form[data-submitted] input:invalid shows red border - Add global submit listener in main.tsx that sets data-submitted on forms - Add required prop to Labels missing asterisks: PersonForm (First Name), LocationForm (Location Name), CalendarForm (Name), LockScreen (Username, Password, Confirm Password) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
146 lines
4.4 KiB
TypeScript
146 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 { Calendar } from '@/types';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
DialogClose,
|
|
} from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
interface CalendarFormProps {
|
|
calendar: Calendar | null;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const colorSwatches = [
|
|
'#3b82f6', // blue
|
|
'#ef4444', // red
|
|
'#f97316', // orange
|
|
'#eab308', // yellow
|
|
'#22c55e', // green
|
|
'#8b5cf6', // purple
|
|
'#ec4899', // pink
|
|
'#06b6d4', // cyan
|
|
];
|
|
|
|
export default function CalendarForm({ calendar, onClose }: CalendarFormProps) {
|
|
const queryClient = useQueryClient();
|
|
const [name, setName] = useState(calendar?.name || '');
|
|
const [color, setColor] = useState(calendar?.color || '#3b82f6');
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: async () => {
|
|
if (calendar) {
|
|
const { data } = await api.put(`/calendars/${calendar.id}`, { name, color });
|
|
return data;
|
|
} else {
|
|
const { data } = await api.post('/calendars', { name, color });
|
|
return data;
|
|
}
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['calendars'] });
|
|
queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
|
|
toast.success(calendar ? 'Calendar updated' : 'Calendar created');
|
|
onClose();
|
|
},
|
|
onError: (error) => {
|
|
toast.error(getErrorMessage(error, 'Failed to save calendar'));
|
|
},
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: async () => {
|
|
await api.delete(`/calendars/${calendar?.id}`);
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['calendars'] });
|
|
queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
|
|
toast.success('Calendar deleted');
|
|
onClose();
|
|
},
|
|
onError: (error) => {
|
|
toast.error(getErrorMessage(error, 'Failed to delete calendar'));
|
|
},
|
|
});
|
|
|
|
const handleSubmit = (e: FormEvent) => {
|
|
e.preventDefault();
|
|
if (!name.trim()) return;
|
|
mutation.mutate();
|
|
};
|
|
|
|
const canDelete = calendar && !calendar.is_default && !calendar.is_system;
|
|
|
|
return (
|
|
<Dialog open={true} onOpenChange={onClose}>
|
|
<DialogContent>
|
|
<DialogClose onClick={onClose} />
|
|
<DialogHeader>
|
|
<DialogTitle>{calendar ? 'Edit Calendar' : 'New Calendar'}</DialogTitle>
|
|
</DialogHeader>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="cal-name" required>Name</Label>
|
|
<Input
|
|
id="cal-name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Calendar name"
|
|
required
|
|
disabled={calendar?.is_system}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Color</Label>
|
|
<div className="flex gap-2">
|
|
{colorSwatches.map((c) => (
|
|
<button
|
|
key={c}
|
|
type="button"
|
|
onClick={() => setColor(c)}
|
|
className="h-8 w-8 rounded-full border-2 transition-all duration-150 hover:scale-110"
|
|
style={{
|
|
backgroundColor: c,
|
|
borderColor: color === c ? 'hsl(0 0% 98%)' : 'transparent',
|
|
boxShadow: color === c ? `0 0 0 2px ${c}40` : 'none',
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
{canDelete && (
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
onClick={() => deleteMutation.mutate()}
|
|
disabled={deleteMutation.isPending}
|
|
className="mr-auto"
|
|
>
|
|
Delete
|
|
</Button>
|
|
)}
|
|
<Button type="button" variant="outline" onClick={onClose}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={mutation.isPending}>
|
|
{mutation.isPending ? 'Saving...' : calendar ? 'Update' : 'Create'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|