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
+94 -79
View File
@@ -2,6 +2,11 @@ import Link from "next/link";
import { prisma } from "@streamclipper/db";
import { addChannel, toggleChannelActive, toggleAllChannels, checkChannelNow } from "./actions";
import { fetchChannelStatuses } from "../lib/apiDaemon";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
export const dynamic = "force-dynamic";
@@ -21,89 +26,99 @@ export default async function DashboardPage() {
]);
return (
<>
<h1>Kanal Durumu</h1>
<div className="flex flex-col gap-6">
<h1 className="text-2xl font-semibold tracking-tight">Kanal Durumu</h1>
<form action={addChannel} className="card add-channel-form">
<input name="name" placeholder="Kanal adı" required />
<input name="youtubeHandle" placeholder="@handle" required />
<input name="channelId" placeholder="channel_id (UC...)" required />
<button type="submit">Kanal Ekle</button>
</form>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">Yeni Kanal Ekle</CardTitle>
</CardHeader>
<CardContent>
<form action={addChannel} className="flex flex-wrap gap-2">
<Input name="name" placeholder="Kanal adı" required className="flex-1 min-w-[140px]" />
<Input name="youtubeHandle" placeholder="@handle" required className="flex-1 min-w-[120px]" />
<Input name="channelId" placeholder="channel_id (UC...)" required className="flex-1 min-w-[180px]" />
<Button type="submit">Kanal Ekle</Button>
</form>
</CardContent>
</Card>
{channels.length === 0 ? (
<p className="empty">Henüz kanal eklenmedi.</p>
<p className="text-sm text-muted-foreground">Henüz kanal eklenmedi.</p>
) : (
<>
<div style={{ display: "flex", gap: "0.5rem", marginBottom: "0.6rem" }}>
<form action={toggleAllChannels.bind(null, true)}>
<button type="submit" className="badge ok" style={{ border: "none", cursor: "pointer" }}>
Tümünü Devam Ettir
</button>
</form>
<form action={toggleAllChannels.bind(null, false)}>
<button type="submit" className="badge muted" style={{ border: "none", cursor: "pointer" }}>
Tümünü Duraklat
</button>
</form>
</div>
<table>
<thead>
<tr>
<th>Kanal</th>
<th>Durum</th>
<th>Kayıt</th>
<th>Son Kontrol</th>
<th></th>
</tr>
</thead>
<tbody>
{channels.map((c) => {
const recording = c.sessions.length > 0;
const status = statuses.get(c.id);
return (
<tr key={c.id}>
<td>
<Link href={`/channels/${c.id}`}>{c.name}</Link>{" "}
<span className="mono">{c.youtubeHandle}</span>
</td>
<td>
<span className={`badge ${c.isActive ? "ok" : "muted"}`}>
{c.isActive ? "Aktif" : "Pasif"}
</span>
</td>
<td>
<span className={`badge ${recording ? "warn" : "muted"}`}>
{recording ? "🔴 Kayıtta" : "—"}
</span>
{status?.lastPollError && (
<span className="badge err" style={{ marginLeft: "0.4rem" }}>
hata
</span>
)}
</td>
<td className="mono">
{c.lastCheckedAt ? new Date(c.lastCheckedAt).toLocaleString("tr-TR") : "—"}
</td>
<td style={{ display: "flex", gap: "0.35rem" }}>
<form action={toggleChannelActive.bind(null, c.id, !c.isActive)}>
<button type="submit" className="badge muted" style={{ border: "none", cursor: "pointer" }}>
{c.isActive ? "Duraklat" : "Devam Ettir"}
</button>
</form>
<form action={checkChannelNow.bind(null, c.id)}>
<button type="submit" className="badge muted" style={{ border: "none", cursor: "pointer" }}>
Şimdi Kontrol Et
</button>
</form>
</td>
</tr>
);
})}
</tbody>
</table>
</>
<Card>
<CardHeader className="flex-row items-center justify-between">
<CardTitle className="text-sm font-medium text-muted-foreground">{channels.length} kanal</CardTitle>
<div className="flex gap-2">
<form action={toggleAllChannels.bind(null, true)}>
<Button type="submit" variant="outline" size="sm">
Tümünü Devam Ettir
</Button>
</form>
<form action={toggleAllChannels.bind(null, false)}>
<Button type="submit" variant="outline" size="sm">
Tümünü Duraklat
</Button>
</form>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Kanal</TableHead>
<TableHead>Durum</TableHead>
<TableHead>Kayıt</TableHead>
<TableHead>Son Kontrol</TableHead>
<TableHead className="text-right">Aksiyon</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{channels.map((c) => {
const recording = c.sessions.length > 0;
const status = statuses.get(c.id);
return (
<TableRow key={c.id}>
<TableCell>
<Link href={`/channels/${c.id}`} className="font-medium hover:text-primary hover:underline">
{c.name}
</Link>{" "}
<span className="font-mono-num text-xs text-muted-foreground">{c.youtubeHandle}</span>
</TableCell>
<TableCell>
<Badge variant={c.isActive ? "ok" : "muted"}>{c.isActive ? "Aktif" : "Pasif"}</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-1.5">
<Badge variant={recording ? "live" : "muted"}>{recording ? "🔴 Kayıtta" : "—"}</Badge>
{status?.lastPollError && <Badge variant="err">hata</Badge>}
</div>
</TableCell>
<TableCell className="font-mono-num text-xs text-muted-foreground">
{c.lastCheckedAt ? new Date(c.lastCheckedAt).toLocaleString("tr-TR") : "—"}
</TableCell>
<TableCell>
<div className="flex justify-end gap-1.5">
<form action={toggleChannelActive.bind(null, c.id, !c.isActive)}>
<Button type="submit" variant="outline" size="sm">
{c.isActive ? "Duraklat" : "Devam Ettir"}
</Button>
</form>
<form action={checkChannelNow.bind(null, c.id)}>
<Button type="submit" variant="ghost" size="sm">
Şimdi Kontrol Et
</Button>
</form>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</CardContent>
</Card>
)}
</>
</div>
);
}