refactor(web): kill dashboard home, /dashboard now redirects to /search
The 460-line dashboard "home" was a low-value stat-and-profile screen: new users saw mostly zeros, the subscription card duplicated info already on /subscription and /billing, and the page sat between login and the product's actual value (VIN decoding). Login → 1 click → search added no information. Now /dashboard short-circuits via beforeLoad into /dashboard/search. Trial urgency and subscription status are already surfaced by the dashboard layout shell (TrialUrgencyBanner). The "Gösterge Paneli" sidebar entry is removed since it now duplicates "Arama". If we miss the at-a-glance overview later we'll bring it back as a real command center (VIN input front-and-center, recent searches prominent, plan mini-strip) — based on PostHog data, not on SaaS-template reflex.
This commit is contained in:
@@ -36,7 +36,6 @@ import {
|
||||
CreditCard,
|
||||
FlaskConical,
|
||||
History,
|
||||
LayoutDashboard,
|
||||
Library,
|
||||
LogOut,
|
||||
Mail,
|
||||
@@ -69,7 +68,6 @@ type NavItem = {
|
||||
};
|
||||
|
||||
const mainMenuItems: readonly NavItem[] = [
|
||||
{ to: "/dashboard", labelKey: "nav.dashboard", icon: LayoutDashboard, exact: true },
|
||||
{ to: "/dashboard/search", labelKey: "nav.search", icon: Search },
|
||||
{ to: "/dashboard/catalog", labelKey: "nav.catalog", icon: Library },
|
||||
{ to: "/dashboard/history", labelKey: "nav.history", icon: History },
|
||||
|
||||
@@ -1,460 +1,13 @@
|
||||
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { KEYS_4 } from "@/lib/keys";
|
||||
import { Badge, Button, Separator, Skeleton } from "@sase/ui";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
||||
import {
|
||||
ArrowRight,
|
||||
Calendar,
|
||||
Car,
|
||||
CheckCircle2,
|
||||
Crown,
|
||||
Database,
|
||||
Search,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
// The old /dashboard "home" page was a low-value stat-and-profile screen that
|
||||
// duplicated information already shown on /subscription and /billing, and for
|
||||
// new users showed mostly zeros. The product's core motion is VIN decoding,
|
||||
// so /dashboard now bounces straight to /dashboard/search. Trial urgency,
|
||||
// subscription status and recent searches are surfaced by the dashboard
|
||||
// layout shell + the search page itself.
|
||||
export const Route = createFileRoute("/dashboard/")({
|
||||
component: DashboardHome,
|
||||
beforeLoad: () => {
|
||||
throw redirect({ to: "/dashboard/search" });
|
||||
},
|
||||
});
|
||||
|
||||
// ─── TYPES ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface DashboardStats {
|
||||
totalSearches: number;
|
||||
monthlySearches: number;
|
||||
totalVehicles: number;
|
||||
recentVehicles: number;
|
||||
}
|
||||
|
||||
interface SubscriptionBrand {
|
||||
brandId: string;
|
||||
brandName: string;
|
||||
}
|
||||
|
||||
interface Subscription {
|
||||
status: string;
|
||||
plan?: { name: string; key: string };
|
||||
billingPeriod: string;
|
||||
brands?: SubscriptionBrand[];
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
// ─── STAT CARD ────────────────────────────────────────────────────────────────
|
||||
|
||||
function StatCard({
|
||||
icon: Icon,
|
||||
value,
|
||||
label,
|
||||
detail,
|
||||
detailValue,
|
||||
buttonLabel,
|
||||
buttonTo,
|
||||
progress,
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
value: string;
|
||||
label: string;
|
||||
detail?: string;
|
||||
detailValue?: string;
|
||||
buttonLabel?: string;
|
||||
buttonTo?: string;
|
||||
progress?: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col rounded-2xl border border-border bg-background p-5 sm:p-6">
|
||||
{/* Icon + Value + Label */}
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="inline-flex size-12 items-center justify-center rounded-xl bg-muted">
|
||||
<Icon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-[family-name:var(--font-display)] text-3xl font-bold tracking-tight">
|
||||
{value}
|
||||
</p>
|
||||
<p className="mt-0.5 text-sm font-medium text-muted-foreground">{label}</p>
|
||||
</div>
|
||||
{detail && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">{detail}</span>
|
||||
<span className="font-medium">{detailValue}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<Separator className="my-4 bg-border" />
|
||||
|
||||
{/* Progress bar or button */}
|
||||
{progress !== undefined ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Kullanım</span>
|
||||
<span className="font-semibold">{progress}%</span>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary transition-all"
|
||||
style={{ width: `${Math.min(progress, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : buttonLabel && buttonTo ? (
|
||||
<Link to={buttonTo}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-center rounded-lg border-border text-sm"
|
||||
>
|
||||
{buttonLabel}
|
||||
<ArrowRight className="ml-1.5 size-3.5" />
|
||||
</Button>
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── PROFILE CARD ─────────────────────────────────────────────────────────────
|
||||
|
||||
function ProfileCard({
|
||||
name,
|
||||
email,
|
||||
initials,
|
||||
}: {
|
||||
name: string;
|
||||
email: string;
|
||||
initials: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col rounded-2xl border border-border bg-background p-5 sm:p-6">
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="inline-flex size-12 items-center justify-center rounded-xl bg-muted">
|
||||
<User className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-[family-name:var(--font-display)] text-2xl font-bold tracking-tight">
|
||||
{name}
|
||||
</p>
|
||||
<p className="mt-0.5 text-sm text-muted-foreground">{email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4 bg-border" />
|
||||
|
||||
<Link to="/dashboard/settings">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-center rounded-lg border-border text-sm"
|
||||
>
|
||||
Profil Ayarları
|
||||
<ArrowRight className="ml-1.5 size-3.5" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── PAGE ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function DashboardHome() {
|
||||
const { user } = useAuth();
|
||||
|
||||
const { data: stats, isLoading: statsLoading } = useQuery({
|
||||
queryKey: ["dashboard", "stats"],
|
||||
queryFn: async () => {
|
||||
// Try to get stats from the API, fall back to defaults
|
||||
try {
|
||||
return await api.get<DashboardStats>("/dashboard/stats");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { data: subData, isLoading: subLoading } = useQuery({
|
||||
queryKey: ["subscription", "me"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await api.get<{ subscription: Subscription | null; eligibleForTrial: boolean }>(
|
||||
"/subscriptions/me",
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const subscription = subData?.subscription ?? null;
|
||||
|
||||
const { data: history } = useQuery({
|
||||
queryKey: ["vehicles", "history"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await api.get<any[]>("/vehicles/history?limit=5");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const initials = user?.name
|
||||
? user.name
|
||||
.split(" ")
|
||||
.map((w) => w[0])
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2)
|
||||
: "?";
|
||||
|
||||
const totalSearches = stats?.totalSearches ?? history?.length ?? 0;
|
||||
const monthlySearches = stats?.monthlySearches ?? 0;
|
||||
const totalVehicles = stats?.totalVehicles ?? history?.length ?? 0;
|
||||
const recentVehicles = stats?.recentVehicles ?? 0;
|
||||
const brandCount = subscription?.brands?.length ?? 0;
|
||||
const planKey: string | null = subscription?.plan?.name
|
||||
? ({
|
||||
"1 Marka": "brand1",
|
||||
"2 Marka": "brand2",
|
||||
"3 Marka": "brand3",
|
||||
"Full Paket": "full",
|
||||
}[subscription.plan.name] ?? null)
|
||||
: null;
|
||||
const maxBrands =
|
||||
planKey === "full"
|
||||
? 27
|
||||
: planKey === "brand3"
|
||||
? 3
|
||||
: planKey === "brand2"
|
||||
? 2
|
||||
: planKey === "brand1"
|
||||
? 1
|
||||
: 0;
|
||||
|
||||
const isLoadingCards = statsLoading || subLoading;
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl space-y-8">
|
||||
{/* ─── STAT CARDS ─────────────────────────────────────────────── */}
|
||||
{isLoadingCards ? (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{KEYS_4.map((__k) => (
|
||||
<Skeleton key={__k} className="h-64 w-full rounded-2xl" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{/* 1. Toplam Arama */}
|
||||
<StatCard
|
||||
icon={Search}
|
||||
value={String(totalSearches)}
|
||||
label="Toplam Arama"
|
||||
detail="Bu Ay"
|
||||
detailValue={String(monthlySearches)}
|
||||
buttonLabel="Şase Çöz"
|
||||
buttonTo="/dashboard/search"
|
||||
/>
|
||||
|
||||
{/* 2. Araç Kaydı */}
|
||||
<StatCard
|
||||
icon={Car}
|
||||
value={String(totalVehicles)}
|
||||
label="Araç Kaydı"
|
||||
detail="Son 30 Gün"
|
||||
detailValue={String(recentVehicles)}
|
||||
buttonLabel="Geçmişe Git"
|
||||
buttonTo="/dashboard/history"
|
||||
/>
|
||||
|
||||
{/* 3. Aktif Marka */}
|
||||
<StatCard
|
||||
icon={Database}
|
||||
value={maxBrands > 0 ? `${brandCount}/${maxBrands}` : "—"}
|
||||
label="Aktif Marka"
|
||||
detail="Erişilebilir Marka"
|
||||
detailValue={maxBrands > 0 ? String(maxBrands) : "—"}
|
||||
progress={maxBrands > 0 ? Math.round((brandCount / maxBrands) * 100) : undefined}
|
||||
buttonLabel={maxBrands === 0 ? "Plan Seçin" : undefined}
|
||||
buttonTo={maxBrands === 0 ? "/dashboard/subscription" : undefined}
|
||||
/>
|
||||
|
||||
{/* 4. Profil */}
|
||||
<ProfileCard name={user?.name ?? "—"} email={user?.email ?? "—"} initials={initials} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ─── ACTIVE SUBSCRIPTION ────────────────────────────────────── */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="font-[family-name:var(--font-display)] text-2xl font-bold tracking-tight">
|
||||
Aktif Aboneliğiniz
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Şase çözme ve parça kataloğuna tek tıkla erişin
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{subLoading ? (
|
||||
<Skeleton className="h-48 w-full rounded-2xl" />
|
||||
) : subscription &&
|
||||
(subscription.status === "active" ||
|
||||
(subscription.status === "trial" && !subData?.eligibleForTrial)) ? (
|
||||
<div className="rounded-2xl border border-border bg-background p-5 sm:p-6">
|
||||
<div className="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between">
|
||||
{/* Plan Info */}
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-xl bg-primary/10">
|
||||
<Crown className="size-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-bold">
|
||||
{subscription.plan?.name ?? "Aktif Plan"}
|
||||
</h3>
|
||||
<Badge
|
||||
variant="default"
|
||||
className="bg-brand text-xs text-brand-foreground hover:bg-brand/90"
|
||||
>
|
||||
{subscription.status === "trial" ? "Deneme" : "Aktif"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{subscription.billingPeriod === "yearly" ? "Yıllık" : "Aylık"} abonelik
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Brands */}
|
||||
{subscription.brands && subscription.brands.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{subscription.brands.map((b) => (
|
||||
<Badge
|
||||
key={b.brandId}
|
||||
variant="outline"
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
<CarBrandLogo brandName={b.brandName} size={16} className="shrink-0" />
|
||||
{b.brandName}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dates */}
|
||||
<div className="flex flex-wrap gap-6 text-sm">
|
||||
{subscription.startDate && (
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Calendar className="size-3.5" />
|
||||
Başlangıç:{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{new Date(subscription.startDate).toLocaleDateString("tr-TR")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{subscription.endDate && (
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Calendar className="size-3.5" />
|
||||
Bitiş:{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{new Date(subscription.endDate).toLocaleDateString("tr-TR")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
||||
{["Sınırsız şase arama", "Parça kataloğu", "İnteraktif şema"].map((f) => (
|
||||
<span
|
||||
key={f}
|
||||
className="flex items-center gap-1.5 text-sm text-muted-foreground"
|
||||
>
|
||||
<CheckCircle2 className="size-3.5 text-brand" />
|
||||
{f}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link to="/dashboard/search">
|
||||
<Button className="w-full rounded-lg sm:w-auto">
|
||||
Şase Ara
|
||||
<ArrowRight className="ml-1.5 size-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/dashboard/subscription">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full rounded-lg border-border text-sm sm:w-auto"
|
||||
>
|
||||
Plan Detayları
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* No subscription */
|
||||
<div className="rounded-2xl border border-dashed border-border bg-background p-8 text-center">
|
||||
<div className="mx-auto flex size-14 items-center justify-center rounded-2xl bg-muted">
|
||||
<Crown className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="mt-4 text-lg font-semibold">Henüz aktif planınız yok</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Şase çözme, parça kataloğu ve interaktif şemalara erişmek için bir plan seçin.
|
||||
</p>
|
||||
<Link to="/dashboard/subscription">
|
||||
<Button className="mt-4 rounded-lg">
|
||||
Plan Seçin
|
||||
<ArrowRight className="ml-1.5 size-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ─── SON ARAMALAR ───────────────────────────────────────────── */}
|
||||
{history && history.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="font-[family-name:var(--font-display)] text-2xl font-bold tracking-tight">
|
||||
Son Aramalar
|
||||
</h2>
|
||||
<Link
|
||||
to="/dashboard/history"
|
||||
className="text-sm text-muted-foreground transition hover:text-foreground"
|
||||
>
|
||||
Tümünü Gör →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{history.slice(0, 3).map((v: any) => (
|
||||
<Link key={v.id} to="/dashboard/vehicles/$id" params={{ id: v.id }}>
|
||||
<div className="group flex items-center gap-4 rounded-2xl border border-border bg-background p-4 transition-colors hover:bg-accent">
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-muted">
|
||||
<Car className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium">
|
||||
{v.brandName} {v.model}
|
||||
</p>
|
||||
<p className="truncate font-mono text-xs text-muted-foreground">{v.vin}</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="shrink-0">
|
||||
{v.year}
|
||||
</Badge>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user