From 77860eb6c4d24c484eb25c79ed18bafb2b4f9141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gjermund=20H=C3=B8s=C3=B8ien=20Wiggen?= Date: Sun, 7 Jun 2026 22:16:18 +0200 Subject: [PATCH] Redesign: Linear-inspired dark mode frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete rewrite of all pages: - layout.tsx: App shell with 240px sidebar (saved views, queue list, admin link) - app-shell.tsx: Client sidebar component with route highlighting + counts - page.tsx: Sleek ticket list with filter chips (All/Open/In progress/Resolved), search bar, status dots, assignee avatars, skeleton loading - tickets/[id]/page.tsx: Two-panel conversation layout — message thread (left) + properties sidebar (right) with status change, scrip preview, reply box - admin/page.tsx: Suspense-wrapped admin with tabs in sheet panels - command-palette.tsx: Cmd+K search with keyboard navigation Design tokens from Linear: - bg-[#08090a] canvas, bg-[#0f1011] panels, bg-[#191a1b] cards - text-[#f7f8f8] primary, text-[#d0d6e0] secondary, text-[#8a8f98] tertiary - borders: rgba(255,255,255,0.08) standard, rgba(255,255,255,0.05) subtle - accent: #5e6ad2 primary, #7170ff interactive - status colors: new=gray, open=indigo, in_progress=amber, resolved=green - Inter font, weights 400/510/590, no pure white Fixed: Suspense boundaries for useSearchParams in layout and admin pages Build: passes with zero errors --- web/src/app/admin/page-content.tsx | 745 +++++++++++++++++++++ web/src/app/admin/page.tsx | 747 +-------------------- web/src/app/layout.tsx | 47 +- web/src/app/page.tsx | 521 +++++++++------ web/src/app/tickets/[id]/page.tsx | 877 +++++++++++++++++-------- web/src/components/app-shell.tsx | 265 ++++++++ web/src/components/command-palette.tsx | 172 +++++ 7 files changed, 2118 insertions(+), 1256 deletions(-) create mode 100644 web/src/app/admin/page-content.tsx create mode 100644 web/src/components/app-shell.tsx create mode 100644 web/src/components/command-palette.tsx diff --git a/web/src/app/admin/page-content.tsx b/web/src/app/admin/page-content.tsx new file mode 100644 index 0000000..7cf1687 --- /dev/null +++ b/web/src/app/admin/page-content.tsx @@ -0,0 +1,745 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { Plus, RefreshCw } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableHeader, + TableBody, + TableRow, + TableHead, + TableCell, +} from "@/components/ui/table"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Tabs, + TabsList, + TabsTrigger, + TabsContent, +} from "@/components/ui/tabs"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { + getQueues, + createQueue, + getLifecycles, + createLifecycle, + getScrips, + createScrip, + getCustomFields, + createCustomField, +} from "@/lib/api"; +import type { Queue, Lifecycle, Scrip, CustomField } from "@/lib/types"; + +const LIFECYCLE_PLACEHOLDER = `{ + "statuses": { + "initial": ["new"], + "active": ["open", "in_progress"], + "inactive": ["resolved", "closed"] + }, + "transitions": { + "new": ["open"], + "open": ["in_progress", "resolved"], + "in_progress": ["resolved"], + "*": ["closed"] + } +}`; + +export default function AdminPage() { + return ( +
+

Admin

+ + + Queues + Lifecycles + Scrips + Custom Fields + + + + + + + + + + + + + + +
+ ); +} + +function QueuesTab() { + const [queues, setQueues] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [dialogOpen, setDialogOpen] = useState(false); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [saving, setSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + + const fetchQueues = useCallback(async () => { + setLoading(true); + setError(null); + const { data, error } = await getQueues(); + if (error) setError(error); + else setQueues(data ?? []); + setLoading(false); + }, []); + + useEffect(() => { + fetchQueues(); + }, [fetchQueues]); + + const handleCreate = async () => { + if (!name.trim()) return; + setSaving(true); + setSaveError(null); + const { error } = await createQueue({ name: name.trim(), description: description.trim() || undefined }); + setSaving(false); + if (error) { + setSaveError(error); + } else { + setDialogOpen(false); + setName(""); + setDescription(""); + fetchQueues(); + } + }; + + return ( +
+
+

Queues

+ +
+ + {error &&
{error}
} + {loading ? ( +
Loading...
+ ) : ( + + + + Name + Description + + + + {queues.length === 0 ? ( + + + No queues yet. + + + ) : ( + queues.map((q) => ( + + {q.name} + {q.description ?? "-"} + + )) + )} + +
+ )} + + + + + Add Queue + Create a new ticket queue. + +
+
+ + setName(e.target.value)} + /> +
+
+ +