diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e1a6cda..0031213 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -11,6 +11,7 @@ import type { DashboardMeta } from "@/app/d/actions"; import { getQuickAdds } from "@/modules/_core"; import { QuickAddProvider } from "@/components/quick-add-provider"; import { QuickAddSheet } from "@/components/quick-add-sheet"; +import { QuickAddCreateHost } from "@/components/quick-add/create-host"; import { CommandPalette } from "@/components/command-palette"; import { PwaRegister } from "@/components/pwa-register"; import { InstallPrompt } from "@/components/install-prompt"; @@ -167,6 +168,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo {children} + diff --git a/src/components/command-palette.tsx b/src/components/command-palette.tsx index 571a96a..098957d 100644 --- a/src/components/command-palette.tsx +++ b/src/components/command-palette.tsx @@ -6,7 +6,7 @@ import { useRouter } from "next/navigation"; import { useQuickAdd } from "./quick-add-provider"; export function CommandPalette() { - const { paletteOpen, closePalette, actions } = useQuickAdd(); + const { paletteOpen, closePalette, openCreate, actions } = useQuickAdd(); const router = useRouter(); const inputRef = useRef(null); @@ -19,9 +19,13 @@ export function CommandPalette() { if (!paletteOpen) return null; - function handleSelect(url: string) { + function handleSelect(action: import("@/modules/_core").SerializedQuickAddItem) { closePalette(); - router.push(url); + if (action.createKey) { + openCreate(action.createKey); + return; + } + router.push(action.url); } return ( @@ -64,7 +68,7 @@ export function CommandPalette() { handleSelect(action.url)} + onSelect={() => handleSelect(action)} className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-sm aria-selected:bg-accent aria-selected:text-accent-foreground" > {iconEmoji(action.icon)} @@ -113,6 +117,10 @@ function iconEmoji(icon?: string): string { "list-checks": "✅", "list-plus": "📋", "file-plus": "📝", + leaf: "🌿", + box: "📦", + droplets: "💧", + sparkles: "💥", }; return icon ? (map[icon] ?? "➕") : "➕"; } diff --git a/src/components/quick-add-provider.tsx b/src/components/quick-add-provider.tsx index 0fb08e0..a744cef 100644 --- a/src/components/quick-add-provider.tsx +++ b/src/components/quick-add-provider.tsx @@ -6,10 +6,13 @@ import type { SerializedQuickAddItem } from "@/modules/_core"; type QuickAddState = { sheetOpen: boolean; paletteOpen: boolean; + activeCreateKey: string | null; openSheet: () => void; closeSheet: () => void; openPalette: () => void; closePalette: () => void; + openCreate: (key: string) => void; + closeCreate: () => void; actions: SerializedQuickAddItem[]; }; @@ -30,11 +33,14 @@ export function QuickAddProvider({ }) { const [sheetOpen, setSheetOpen] = useState(false); const [paletteOpen, setPaletteOpen] = useState(false); + const [activeCreateKey, setActiveCreateKey] = useState(null); const openSheet = useCallback(() => setSheetOpen(true), []); const closeSheet = useCallback(() => setSheetOpen(false), []); const openPalette = useCallback(() => setPaletteOpen(true), []); const closePalette = useCallback(() => setPaletteOpen(false), []); + const openCreate = useCallback((key: string) => setActiveCreateKey(key), []); + const closeCreate = useCallback(() => setActiveCreateKey(null), []); useEffect(() => { function onKeyDown(e: KeyboardEvent) { @@ -49,7 +55,18 @@ export function QuickAddProvider({ return ( {children} diff --git a/src/components/quick-add-sheet.tsx b/src/components/quick-add-sheet.tsx index 37392de..1631199 100644 --- a/src/components/quick-add-sheet.tsx +++ b/src/components/quick-add-sheet.tsx @@ -27,6 +27,10 @@ const QUICK_ADD_ICON: Record = { "list-checks": "check-square", "list-plus": "list", "file-plus": "note", + leaf: "sprout", + box: "box", + droplets: "droplets", + sparkles: "sparkles", }; function useShortcutLabel() { @@ -35,7 +39,7 @@ function useShortcutLabel() { } export function QuickAddSheet() { - const { sheetOpen, closeSheet, actions } = useQuickAdd(); + const { sheetOpen, closeSheet, openCreate, actions } = useQuickAdd(); const shortcut = useShortcutLabel(); const router = useRouter(); const backdropRef = useRef(null); @@ -53,9 +57,13 @@ export function QuickAddSheet() { const groups = groupByModule(actions); - function handleAction(url: string) { + function handleAction(action: SerializedQuickAddItem) { closeSheet(); - router.push(url); + if (action.createKey) { + openCreate(action.createKey); + return; + } + router.push(action.url); } return ( @@ -129,7 +137,7 @@ export function QuickAddSheet() { {group.items.map((action) => ( + + + ); +} + +function todayValue(): string { + const d = new Date(); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; +} diff --git a/src/components/quick-add/dialogs/calendar-create-dialog.tsx b/src/components/quick-add/dialogs/calendar-create-dialog.tsx new file mode 100644 index 0000000..1bd4caf --- /dev/null +++ b/src/components/quick-add/dialogs/calendar-create-dialog.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { 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 { createCalendar } from "@/modules/calendar/server/actions"; + +const DEFAULT_COLOR = "#B85C3C"; +const VISIBILITY_ITEMS = [ + { label: "Household", value: "household" }, + { label: "Private", value: "private" }, +]; + +type Props = { + open: boolean; + onOpenChange: (open: boolean) => void; +}; + +export function CalendarCreateDialog({ open, onOpenChange }: Props) { + return ( + + + {open ? onOpenChange(false)} /> : null} + + + ); +} + +function CalendarCreateForm({ onDone }: { onDone: () => void }) { + const [name, setName] = useState(""); + const [color, setColor] = useState(DEFAULT_COLOR); + const [visibility, setVisibility] = useState<"private" | "household">("household"); + const [isPending, startTransition] = useTransition(); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!name.trim()) return; + startTransition(async () => { + await createCalendar({ name: name.trim(), color, visibility }); + onDone(); + }); + } + + return ( + <> + + New calendar + +
+
+ + setName(e.target.value)} + required + /> +
+
+ setColor(e.target.value)} + /> + +
+
+ + + + + ); +} diff --git a/src/components/quick-add/dialogs/calendar-event-create-dialog.tsx b/src/components/quick-add/dialogs/calendar-event-create-dialog.tsx new file mode 100644 index 0000000..0558a7e --- /dev/null +++ b/src/components/quick-add/dialogs/calendar-event-create-dialog.tsx @@ -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 ( + + + {open ? onOpenChange(false)} /> : null} + + + ); +} + +function CalendarEventCreateForm({ onDone }: { onDone: () => void }) { + const [calendars, setCalendars] = useState([]); + 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 ( + <> + + New event + +
+
+ + setTitle(e.target.value)} + required + /> +
+
+ + +
+
+
+ + setStartAt(e.target.value)} + required + /> +
+
+ + setEndAt(e.target.value)} + required + /> +
+
+
+ + setLocation(e.target.value)} + /> +
+
+ +