feat: panel UI'ını Tailwind v4 + shadcn/ui ile yeniden tasarla (Faz 4)

Elle yazılmış tek-dosya CSS (sabit koyu tema, ham table/button, window.confirm
ile silme onayı) yerine tutarlı bir bileşen sistemi: Card/Table/Badge/Button/
Input/Switch/AlertDialog primitifleri, Hanken Grotesk + JetBrains Mono
tipografi çifti, "broadcast/monitör" temalı özel bir renk paleti (camgöbeği
accent, ok/warn/err/live durum rengi sözlüğü). Tüm sayfalar (dashboard, kanal
detay, segmentler, ayarlar, istatistik, kullanıcılar, login) aynı oturumda
taşındı — silme onayları artık gerçek AlertDialog, native confirm() değil.

Veri akışı/server action'lar değişmedi, tamamen görsel katman.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 14:33:24 +03:00
co-authored by Claude Sonnet 5
parent d045b24c85
commit 7d504d1938
30 changed files with 3436 additions and 801 deletions
+45 -12
View File
@@ -1,24 +1,57 @@
"use client";
import { useRef } from "react";
import { Button, type buttonVariants } from "@/components/ui/button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import type { VariantProps } from "class-variance-authority";
/** Submits the form this button lives in — used inside the (portaled) AlertDialogAction, which isn't a DOM descendant of that form, so `type="submit"` alone wouldn't reach it. */
export function ConfirmButton({
confirmText,
label,
variant = "err",
triggerLabel,
variant = "destructive",
}: {
confirmText: string;
label: string;
variant?: "err" | "warn" | "muted" | "ok";
triggerLabel?: string;
variant?: VariantProps<typeof buttonVariants>["variant"];
}) {
const triggerRef = useRef<HTMLButtonElement>(null);
return (
<button
type="submit"
className={`badge ${variant}`}
style={{ border: "none", cursor: "pointer" }}
onClick={(e) => {
if (!confirm(confirmText)) e.preventDefault();
}}
>
{label}
</button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button ref={triggerRef} type="button" variant={variant} size="sm">
{triggerLabel ?? label}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Emin misin?</AlertDialogTitle>
<AlertDialogDescription>{confirmText}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Vazgeç</AlertDialogCancel>
<AlertDialogAction
variant={variant}
onClick={() => triggerRef.current?.closest("form")?.requestSubmit()}
>
{label}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
@@ -1,16 +0,0 @@
"use client";
export function DeleteButton({ confirmText, label = "Sil" }: { confirmText: string; label?: string }) {
return (
<button
type="submit"
className="badge err"
style={{ border: "none", cursor: "pointer" }}
onClick={(e) => {
if (!confirm(confirmText)) e.preventDefault();
}}
>
{label}
</button>
);
}
+1 -12
View File
@@ -34,18 +34,7 @@ export function LiveLogViewer({ sessionId }: { sessionId: string }) {
return (
<pre
ref={boxRef}
className="mono"
style={{
maxHeight: "220px",
overflowY: "auto",
background: "var(--bg)",
border: "1px solid var(--border)",
borderRadius: "6px",
padding: "0.6rem 0.7rem",
fontSize: "0.7rem",
whiteSpace: "pre-wrap",
marginTop: "0.5rem",
}}
className="font-mono-num mt-3 max-h-56 overflow-y-auto rounded-md border border-border bg-background/60 p-3 text-[0.7rem] leading-relaxed whitespace-pre-wrap text-muted-foreground"
>
{lines.length > 0 ? lines.join("\n") : "Log bekleniyor…"}
</pre>
+24
View File
@@ -0,0 +1,24 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export function NavLink({ href, children }: { href: string; children: ReactNode }) {
const pathname = usePathname();
const active = href === "/" ? pathname === "/" : pathname.startsWith(href);
return (
<Link
href={href}
className={cn(
"text-sm font-medium transition-colors hover:text-foreground",
active ? "text-foreground" : "text-muted-foreground",
)}
>
{children}
</Link>
);
}
@@ -19,5 +19,5 @@ export function SessionTimer({ startedAt }: { startedAt: string }) {
return () => clearInterval(timer);
}, []);
return <span className="mono">{formatElapsed(now - new Date(startedAt).getTime())}</span>;
return <span className="font-mono-num">{formatElapsed(now - new Date(startedAt).getTime())}</span>;
}