Files
sase.tr/apps/web/src/routes/dashboard.tsx
Semih Yesilyurt 03353a703a
Some checks failed
CI / Lint, Typecheck, Test & Build (push) Has been cancelled
feat: add service test page for individual VIN decode testing
Admin-only page at /dashboard/service-test with dropdown to test
each VIN decode service (Corgi, PartsCatalogs, PL24, EMEX, VIN API)
individually or the full cascade. Results shown as JSON on page,
no DB writes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:12:50 +00:00

446 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createFileRoute, Outlet, Link, useNavigate } from "@tanstack/react-router";
import { useAuth } from "@/hooks/use-auth";
import { useTranslation } from "@/lib/i18n";
import { Button, Separator, Skeleton } from "@sase/ui";
import {
Search,
History,
CreditCard,
Receipt,
Settings,
Shield,
Users,
BarChart3,
DollarSign,
Share2,
Menu,
X,
LogOut,
PanelLeftClose,
PanelLeftOpen,
LayoutDashboard,
Bell,
ArrowRight,
Mail,
BookOpen,
Sun,
Moon,
Copy,
Library,
FlaskConical,
} from "lucide-react";
import { useState } from "react";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { capture, resetUser } from "@/lib/posthog";
export const Route = createFileRoute("/dashboard")({
component: DashboardLayout,
});
// ─── NAV SECTIONS ─────────────────────────────────────────────────────────────
const mainMenuItems = [
{ to: "/dashboard", label: "Gösterge Paneli", icon: LayoutDashboard, exact: true },
{ to: "/dashboard/search", label: "nav.search", translatable: true, icon: Search },
{ to: "/dashboard/catalog", label: "nav.catalog", translatable: true, icon: Library },
{ to: "/dashboard/history", label: "nav.history", translatable: true, icon: History },
] as const;
const accountItems = [
{ to: "/dashboard/subscription", label: "nav.subscription", translatable: true, icon: CreditCard },
{ to: "/dashboard/billing", label: "nav.billing", translatable: true, icon: Receipt },
{ to: "/dashboard/settings", label: "nav.settings", translatable: true, icon: Settings },
] as const;
const supportItems = [
{ to: "/contact", label: "İletişim", icon: Mail },
{ to: "/blog", label: "Blog", icon: BookOpen },
] as const;
const adminItems = [
{ to: "/dashboard/admin", label: "Yönetim Paneli", icon: Shield },
{ to: "/dashboard/admin/users", label: "Kullanıcılar", icon: Users },
{ to: "/dashboard/admin/payments", label: "Ödemeler", icon: DollarSign },
{ to: "/dashboard/admin/analytics", label: "Analitik", icon: BarChart3 },
{ to: "/dashboard/admin/copy-logs", label: "OEM Kopyalama", icon: Copy },
{ to: "/dashboard/admin/referrals", label: "Referanslar", icon: Share2 },
{ to: "/dashboard/service-test", label: "Servis Test", icon: FlaskConical },
] as const;
// ─── HELPERS ──────────────────────────────────────────────────────────────────
function NavSection({
title,
collapsed,
}: {
title: string;
collapsed: boolean;
}) {
if (collapsed) {
return <Separator className="my-2 bg-border/50" />;
}
return (
<p className="mb-1 mt-4 px-3 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
{title}
</p>
);
}
function NavLink({
to,
label,
icon: Icon,
collapsed,
onClick,
exact,
}: {
to: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
collapsed: boolean;
onClick?: () => void;
exact?: boolean;
}) {
return (
<Link
to={to}
title={collapsed ? label : undefined}
className={`flex items-center rounded-lg text-sm font-medium transition-colors hover:bg-accent [&.active]:bg-accent [&.active]:text-accent-foreground ${collapsed ? "justify-center p-2.5" : "gap-3 px-3 py-2"}`}
activeProps={{ className: "active" }}
activeOptions={exact ? { exact: true } : undefined}
onClick={onClick}
>
<Icon className="size-4 shrink-0" />
{!collapsed && <span>{label}</span>}
</Link>
);
}
// ─── LAYOUT ───────────────────────────────────────────────────────────────────
function DashboardLayout() {
const { t } = useTranslation();
const { user, isLoading, signOut, isAdmin } = useAuth();
const navigate = useNavigate();
const [mobileOpen, setMobileOpen] = useState(false);
const [collapsed, setCollapsed] = useState(
() => getUserSettings().sidebarCollapsed ?? false,
);
const [isDark, setIsDark] = useState(() => {
const theme = getUserSettings().theme ?? "dark";
if (theme === "system") {
return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
return theme === "dark";
});
const handleSignOut = () => {
capture("user_logged_out");
resetUser();
signOut();
};
const toggleTheme = () => {
const next = isDark ? "light" : "dark";
document.documentElement.classList.toggle("dark", next === "dark");
setUserSetting("theme", next);
setIsDark(next === "dark");
};
const toggleCollapsed = () => {
const next = !collapsed;
setCollapsed(next);
setUserSetting("sidebarCollapsed", next);
};
if (isLoading) {
return (
<div className="flex min-h-screen">
<div className="hidden w-64 border-r border-border bg-background p-4 lg:block">
<Skeleton className="mb-8 h-8 w-32" />
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={`nav-skel-${i}`} className="mb-3 h-10 w-full" />
))}
</div>
<div className="flex-1 p-6">
<Skeleton className="h-8 w-48" />
</div>
</div>
);
}
if (!user) {
navigate({ to: "/login" });
return null;
}
const initials = user.name
? user.name
.split(" ")
.map((w) => w[0])
.join("")
.toUpperCase()
.slice(0, 2)
: "?";
// Resolve labels (some need translation, some are static)
function resolveLabel(item: { label: string; translatable?: boolean }) {
return item.translatable ? t(item.label) : item.label;
}
// Sidebar content (shared between desktop & mobile)
function SidebarNav({ onNavigate }: { onNavigate?: () => void }) {
return (
<>
{/* ANA MENÜ */}
<NavSection title="Ana Menü" collapsed={collapsed && !onNavigate} />
{mainMenuItems.map((item) => (
<NavLink
key={item.to}
to={item.to}
label={resolveLabel(item)}
icon={item.icon}
collapsed={collapsed && !onNavigate}
onClick={onNavigate}
exact={(item as any).exact}
/>
))}
{/* DESTEK */}
<NavSection title="Destek" collapsed={collapsed && !onNavigate} />
{supportItems.map((item) => (
<NavLink
key={item.to}
to={item.to}
label={resolveLabel(item)}
icon={item.icon}
collapsed={collapsed && !onNavigate}
onClick={onNavigate}
/>
))}
{/* HESAP */}
<NavSection title="Hesap" collapsed={collapsed && !onNavigate} />
{accountItems.map((item) => (
<NavLink
key={item.to}
to={item.to}
label={resolveLabel(item)}
icon={item.icon}
collapsed={collapsed && !onNavigate}
onClick={onNavigate}
/>
))}
{/* ADMIN */}
{isAdmin && (
<>
<NavSection title="Yönetim" collapsed={collapsed && !onNavigate} />
{adminItems.map((item) => (
<NavLink
key={item.to}
to={item.to}
label={item.label}
icon={item.icon}
collapsed={collapsed && !onNavigate}
onClick={onNavigate}
/>
))}
</>
)}
</>
);
}
return (
<div className="flex min-h-screen bg-background">
{/* ─── Desktop Sidebar ──────────────────────────────────────────── */}
<aside
className={`hidden flex-shrink-0 border-r border-border transition-[width] duration-200 lg:flex lg:flex-col ${collapsed ? "w-[68px]" : "w-64"}`}
>
{/* Logo + Collapse */}
<div
className={`flex h-16 items-center border-b border-border ${collapsed ? "justify-center px-2" : "justify-between px-4"}`}
>
{!collapsed && (
<Link to="/" className="text-xl font-bold tracking-tight">
Sase.tr
</Link>
)}
<button
type="button"
onClick={toggleCollapsed}
className="flex size-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
{collapsed ? (
<PanelLeftOpen className="size-5" />
) : (
<PanelLeftClose className="size-5" />
)}
</button>
</div>
{/* Navigation */}
<nav
className={`flex-1 space-y-0.5 overflow-y-auto ${collapsed ? "p-2" : "px-3 py-2"}`}
>
<SidebarNav />
</nav>
{/* User Profile - Bottom */}
<div className={`border-t border-border ${collapsed ? "p-2" : "p-3"}`}>
<button
type="button"
onClick={handleSignOut}
title={collapsed ? user.name ?? ıkış" : undefined}
className={`flex w-full items-center rounded-lg text-left transition-colors hover:bg-accent ${collapsed ? "justify-center p-2.5" : "gap-3 px-3 py-2.5"}`}
>
<div className="flex size-9 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground">
{initials}
</div>
{!collapsed && (
<>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{user.name}</p>
<p className="truncate text-xs text-muted-foreground">
{user.email}
</p>
</div>
<LogOut className="size-4 shrink-0 text-muted-foreground" />
</>
)}
</button>
</div>
</aside>
{/* ─── Main Content Area ────────────────────────────────────────── */}
<div className="flex flex-1 flex-col">
{/* Top Header Bar */}
<header className="flex h-16 items-center justify-between border-b border-border px-4 sm:px-6">
{/* Left: Mobile menu + User welcome */}
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
className="lg:hidden"
onClick={() => setMobileOpen(true)}
>
<Menu className="size-5" />
</Button>
<div className="flex items-center gap-3">
<div className="hidden size-10 items-center justify-center rounded-full bg-muted text-sm font-semibold sm:flex">
{initials}
</div>
<div className="hidden sm:block">
<p className="text-sm font-semibold">{user.name}</p>
<p className="text-xs text-muted-foreground">
Sase.tr'ye hoş geldiniz 👋
</p>
</div>
</div>
</div>
{/* Right: Actions */}
<div className="flex items-center gap-2">
<Link to="/dashboard/subscription">
<Button
variant="outline"
size="sm"
className="hidden rounded-full border-border text-xs sm:inline-flex"
>
<CreditCard className="mr-1.5 size-3.5" />
Plan Yükselt
</Button>
</Link>
<button
type="button"
onClick={toggleTheme}
className="flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
aria-label="Tema değiştir"
>
{isDark ? <Sun className="size-4" /> : <Moon className="size-4" />}
</button>
<button
type="button"
className="flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
<Bell className="size-4" />
</button>
<button
type="button"
onClick={handleSignOut}
className="flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground lg:hidden"
>
<LogOut className="size-4" />
</button>
</div>
</header>
{/* Page Content */}
<main className="flex-1 overflow-auto bg-muted/30 p-4 sm:p-6">
<Outlet />
</main>
{/* Footer */}
<div className="border-t border-border px-6 py-3">
<p className="text-center text-xs text-muted-foreground/60">
&copy; {new Date().getFullYear()} Sase.tr | Gizlilik Politikası,
Kullanım Koşulları
</p>
</div>
</div>
{/* ─── Mobile Nav Overlay ───────────────────────────────────────── */}
{mobileOpen && (
<div className="fixed inset-0 z-50 lg:hidden">
<div
className="absolute inset-0 bg-black/50"
onClick={() => setMobileOpen(false)}
onKeyDown={() => {}}
role="button"
tabIndex={-1}
/>
<aside className="absolute left-0 top-0 flex h-full w-64 flex-col bg-background shadow-lg">
<div className="flex h-16 items-center justify-between border-b border-border px-4">
<span className="text-xl font-bold">Sase.tr</span>
<Button
variant="ghost"
size="icon"
onClick={() => setMobileOpen(false)}
>
<X className="size-4" />
</Button>
</div>
<nav className="flex-1 space-y-0.5 overflow-y-auto px-3 py-2">
<SidebarNav onNavigate={() => setMobileOpen(false)} />
</nav>
{/* User section - bottom */}
<div className="border-t border-border p-3">
<button
type="button"
onClick={() => {
handleSignOut();
setMobileOpen(false);
}}
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left transition-colors hover:bg-accent"
>
<div className="flex size-9 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground">
{initials}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{user.name}</p>
<p className="truncate text-xs text-muted-foreground">
{user.email}
</p>
</div>
<LogOut className="size-4 shrink-0 text-muted-foreground" />
</button>
</div>
</aside>
</div>
)}
</div>
);
}