fix: quick add opens create ui

Add createKey to quick-add manifests and client create dialog host.

FAB and command palette open in-place create UIs instead of navigating away.
This commit is contained in:
ginnoir
2026-07-04 11:47:35 -05:00
parent 76f68548b2
commit 68a573c4d6
24 changed files with 1017 additions and 20 deletions
@@ -0,0 +1,181 @@
"use client";
import { useEffect, useMemo, useState, useTransition } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { createEvent } from "@/modules/calendar/server/actions";
import { listCalendars, type CalendarDto } from "@/modules/calendar/server/queries";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export function CalendarEventCreateDialog({ open, onOpenChange }: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
{open ? <CalendarEventCreateForm onDone={() => onOpenChange(false)} /> : null}
</DialogContent>
</Dialog>
);
}
function CalendarEventCreateForm({ onDone }: { onDone: () => void }) {
const [calendars, setCalendars] = useState<CalendarDto[]>([]);
const [calendarId, setCalendarId] = useState("");
const [title, setTitle] = useState("");
const [startAt, setStartAt] = useState(() => toInputDateTime(new Date()));
const [endAt, setEndAt] = useState(() => toInputDateTime(new Date(Date.now() + 60 * 60 * 1000)));
const [location, setLocation] = useState("");
const [notes, setNotes] = useState("");
const [remind, setRemind] = useState(true);
const [isPending, startTransition] = useTransition();
useEffect(() => {
listCalendars().then((rows) => {
setCalendars(rows);
setCalendarId(rows[0]?.id ?? "");
});
}, []);
const calendarItems = useMemo(
() => calendars.map((c) => ({ label: c.name, value: c.id })),
[calendars],
);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!calendarId || !title.trim()) return;
startTransition(async () => {
await createEvent({
calendarId,
title: title.trim(),
startAt: new Date(startAt),
endAt: new Date(endAt),
allDay: false,
location: location || null,
notes: notes || null,
remindMinutesBefore: remind ? 30 : null,
});
onDone();
});
}
return (
<>
<DialogHeader>
<DialogTitle>New event</DialogTitle>
</DialogHeader>
<form id="quick-add-event-form" onSubmit={handleSubmit} className="grid gap-3">
<div className="space-y-1.5">
<Label htmlFor="qa-event-title">Title</Label>
<Input
id="qa-event-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="qa-event-calendar">Calendar</Label>
<Select
items={calendarItems}
value={calendarId}
onValueChange={(value) => setCalendarId(value ?? "")}
>
<SelectTrigger id="qa-event-calendar">
<SelectValue>
{calendars.find((c) => c.id === calendarId)?.name ?? "Select a calendar"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{calendarItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="qa-event-start">Start</Label>
<Input
id="qa-event-start"
type="datetime-local"
value={startAt}
onChange={(e) => setStartAt(e.target.value)}
required
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="qa-event-end">End</Label>
<Input
id="qa-event-end"
type="datetime-local"
value={endAt}
onChange={(e) => setEndAt(e.target.value)}
required
/>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="qa-event-location">Location</Label>
<Input
id="qa-event-location"
value={location}
onChange={(e) => setLocation(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="qa-event-notes">Notes</Label>
<textarea
id="qa-event-notes"
className="min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
value={notes}
onChange={(e) => setNotes(e.target.value)}
/>
</div>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
className="size-4 cursor-pointer"
checked={remind}
onChange={(e) => setRemind(e.target.checked)}
/>
Remind me 30 min before
</label>
</form>
<DialogFooter>
<Button type="submit" form="quick-add-event-form" disabled={!title.trim() || isPending}>
Save event
</Button>
</DialogFooter>
</>
);
}
function toInputDateTime(date: Date) {
const offset = date.getTimezoneOffset();
const local = new Date(date.getTime() - offset * 60 * 1000);
return local.toISOString().slice(0, 16);
}