UMBRA/frontend/src/components/calendar/CalendarForm.tsx
Kyle Pope f5265a589e Fix form validation: red outline only on submit, add required asterisks
- 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>
2026-02-25 17:53:15 +08:00

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>
);
}