{error}
: null} diff --git a/src/index.css b/src/index.css index 168b306..61d2ade 100644 --- a/src/index.css +++ b/src/index.css @@ -292,4 +292,62 @@ body::before { .tag-tree-loading { @apply flex flex-col items-center justify-center gap-2 py-12 text-muted-foreground; } + + /* + * Native — Feld, WebKit-Segmente & Kalender-Icon. + * Das aufklappende Kalender-Popup gestaltet weiterhin der Browser. + */ + .date-field { + color-scheme: light; + } + + .dark .date-field { + color-scheme: dark; + } + + .date-field::-webkit-datetime-edit, + .date-field::-webkit-datetime-edit-fields-wrapper { + padding: 0.05rem 0; + } + + .date-field::-webkit-datetime-edit-text { + color: var(--muted-foreground); + padding: 0 0.1rem; + } + + .date-field::-webkit-datetime-edit-day-field, + .date-field::-webkit-datetime-edit-month-field, + .date-field::-webkit-datetime-edit-year-field { + padding: 0.1rem 0.2rem; + border-radius: 0.2rem; + } + + .date-field:focus::-webkit-datetime-edit-day-field, + .date-field:focus::-webkit-datetime-edit-month-field, + .date-field:focus::-webkit-datetime-edit-year-field, + .date-field:focus-visible::-webkit-datetime-edit-day-field, + .date-field:focus-visible::-webkit-datetime-edit-month-field, + .date-field:focus-visible::-webkit-datetime-edit-year-field { + background-color: color-mix(in oklch, var(--accent) 45%, transparent); + color: var(--accent-foreground); + } + + .date-field::-webkit-calendar-picker-indicator { + cursor: pointer; + opacity: 0.65; + border-radius: 0.25rem; + padding: 0.15rem 0.35rem; + margin-inline-start: 0.125rem; + transition: opacity 0.12s ease, background-color 0.12s ease; + } + + .date-field:hover::-webkit-calendar-picker-indicator { + opacity: 1; + background-color: color-mix(in oklch, var(--muted) 75%, transparent); + } + + .date-field:focus::-webkit-calendar-picker-indicator, + .date-field:focus-visible::-webkit-calendar-picker-indicator { + opacity: 1; + } } diff --git a/src/lib/snippetDeadline.ts b/src/lib/snippetDeadline.ts new file mode 100644 index 0000000..0fa6d35 --- /dev/null +++ b/src/lib/snippetDeadline.ts @@ -0,0 +1,55 @@ +/** Kalendertag lokal als `YYYY-MM-DD` (für Vergleich mit DB). */ +export function localDateKey(d: Date): string { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + return `${y}-${m}-${day}`; +} + +/** UI-Wert für `` aus API-Wert. */ +export function deadlineToDateInput(deadlineAt: string | null | undefined): string { + if (!deadlineAt) return ""; + const s = deadlineAt.trim(); + if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s; + const t = new Date(s); + if (Number.isNaN(t.getTime())) return ""; + return localDateKey(t); +} + +export type DeadlineTone = "none" | "upcoming" | "today" | "overdue"; + +/** Blau = künftig (nicht heute), Orange = heute fällig, Rot = überschritten. */ +export function deadlineTone(deadlineAt: string | null | undefined): DeadlineTone { + if (!deadlineAt) return "none"; + const key = deadlineToDateInput(deadlineAt); + if (!key) return "none"; + const today = localDateKey(new Date()); + if (key < today) return "overdue"; + if (key === today) return "today"; + return "upcoming"; +} + +export function deadlineBadgeClass(tone: DeadlineTone): string { + switch (tone) { + case "upcoming": + return "border-blue-500/70 bg-blue-500/10 text-blue-800 dark:text-blue-200"; + case "today": + return "border-orange-500/80 bg-orange-500/15 text-orange-900 dark:text-orange-100"; + case "overdue": + return "border-destructive/80 bg-destructive/10 text-destructive"; + default: + return ""; + } +} + +export function formatDeadlineLabel(deadlineAt: string): string { + const key = deadlineToDateInput(deadlineAt); + if (!key) return deadlineAt; + try { + const [y, m, d] = key.split("-").map(Number); + const dt = new Date(y, m - 1, d); + return dt.toLocaleDateString(undefined, { dateStyle: "medium" }); + } catch { + return key; + } +} diff --git a/src/server/db.ts b/src/server/db.ts index f874a3a..31a7a3f 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -44,6 +44,12 @@ function migrate(database: Database) { } database.run(`CREATE INDEX IF NOT EXISTS idx_tag_tree_parent ON tag_tree_nodes(parent_id);`); database.run(`CREATE INDEX IF NOT EXISTS idx_snippets_updated ON snippets(updated_at);`); + try { + database.run(`ALTER TABLE snippets ADD COLUMN deadline_at TEXT`); + } catch { + /* Spalte existiert bereits */ + } + database.run(`CREATE INDEX IF NOT EXISTS idx_snippets_deadline ON snippets(deadline_at);`); } export function getDb(): Database { diff --git a/src/server/routes/snippetsHandlers.ts b/src/server/routes/snippetsHandlers.ts index 24a9c08..850bfb8 100644 --- a/src/server/routes/snippetsHandlers.ts +++ b/src/server/routes/snippetsHandlers.ts @@ -1,5 +1,12 @@ import { readJson } from "@/server/http/readJson"; -import { createSnippet, deleteSnippet, getSnippetById, listSnippets, updateSnippet } from "@/server/snippets"; +import { + createSnippet, + deleteSnippet, + getSnippetById, + listSnippets, + normalizeDeadlineAtInput, + updateSnippet, +} from "@/server/snippets"; export const snippetRoutes = { "/api/snippets": { @@ -16,11 +23,21 @@ export const snippetRoutes = { } const o = body as Record