feat: Ford legacy support, EMEX browser pooling, collapsible sidebar

- Add PL24 Ford legacy service for fordt_parts architecture
- Refactor EMEX to use persistent browser pool instead of per-call instances
- Make vehicle decode resilient: fallback to PL24 when Corgi doesn't recognize VIN
- Add collapsible sidebar with persistent user preference
- Improve brand access guard and categories service
- Add debug/test scripts for VIN e2e testing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sase Dev
2026-02-14 22:25:06 +00:00
parent 7b024df4d5
commit 4a06ba5fdb
53 changed files with 17053 additions and 682 deletions

View File

@@ -17,8 +17,11 @@ import {
Menu,
X,
LogOut,
PanelLeftClose,
PanelLeftOpen,
} from "lucide-react";
import { useState } from "react";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
export const Route = createFileRoute("/dashboard")({
component: DashboardLayout,
@@ -45,6 +48,13 @@ function DashboardLayout() {
const { user, isLoading, signOut, isAdmin } = useAuth();
const navigate = useNavigate();
const [mobileOpen, setMobileOpen] = useState(false);
const [collapsed, setCollapsed] = useState(() => getUserSettings().sidebarCollapsed ?? false);
const toggleCollapsed = () => {
const next = !collapsed;
setCollapsed(next);
setUserSetting("sidebarCollapsed", next);
};
if (isLoading) {
return (
@@ -70,25 +80,41 @@ function DashboardLayout() {
return (
<div className="flex min-h-screen">
{/* Desktop Sidebar */}
<aside className="hidden w-64 flex-shrink-0 border-r lg:block">
<aside
className={`hidden flex-shrink-0 border-r transition-[width] duration-200 lg:block ${collapsed ? "w-16" : "w-64"}`}
>
<div className="flex h-full flex-col">
<div className="flex h-16 items-center border-b px-6">
<Link to="/" className="text-xl font-bold">
Sase.tr
</Link>
<div className={`flex h-16 items-center border-b ${collapsed ? "justify-center px-2" : "justify-between px-4"}`}>
{!collapsed && (
<Link to="/" className="text-xl font-bold">
Sase.tr
</Link>
)}
<button
type="button"
onClick={toggleCollapsed}
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
{collapsed ? (
<PanelLeftOpen className="h-5 w-5" />
) : (
<PanelLeftClose className="h-5 w-5" />
)}
</button>
</div>
<nav className="flex-1 space-y-1 p-4">
<nav className={`flex-1 space-y-1 overflow-y-auto ${collapsed ? "p-2" : "p-4"}`}>
{navItems.map((item) => {
const Icon = item.icon;
return (
<Link
key={item.to}
to={item.to}
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors hover:bg-accent [&.active]:bg-accent [&.active]:text-accent-foreground"
title={collapsed ? t(item.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" : "gap-3 px-3 py-2"}`}
activeProps={{ className: "active" }}
>
<Icon className="h-4 w-4" />
{t(item.label)}
<Icon className="h-4 w-4 shrink-0" />
{!collapsed && <span>{t(item.label)}</span>}
</Link>
);
})}
@@ -99,37 +125,52 @@ function DashboardLayout() {
<Link
key={item.to}
to={item.to}
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors hover:bg-accent [&.active]:bg-accent [&.active]:text-accent-foreground"
title={collapsed ? item.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" : "gap-3 px-3 py-2"}`}
activeProps={{ className: "active" }}
>
<Icon className="h-4 w-4" />
{item.label}
<Icon className="h-4 w-4 shrink-0" />
{!collapsed && <span>{item.label}</span>}
</Link>
);
})}
</nav>
{/* User section - bottom */}
<div className={`border-t ${collapsed ? "p-2" : "p-3"}`}>
<button
type="button"
onClick={() => signOut()}
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" : "gap-3 px-3 py-2"}`}
>
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground">
{user.name?.charAt(0).toUpperCase() ?? "?"}
</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="h-4 w-4 shrink-0 text-muted-foreground" />
</>
)}
</button>
</div>
</div>
</aside>
{/* Main Content */}
<div className="flex flex-1 flex-col">
{/* Header */}
<header className="flex h-16 items-center justify-between border-b px-6">
<header className="flex h-16 items-center border-b px-6 lg:hidden">
<Button
variant="ghost"
size="icon"
className="lg:hidden"
onClick={() => setMobileOpen(true)}
>
<Menu className="h-5 w-5" />
</Button>
<div className="flex-1" />
<div className="flex items-center gap-3">
<span className="text-sm text-muted-foreground">{user.name}</span>
<Button variant="ghost" size="icon" onClick={() => signOut()}>
<LogOut className="h-4 w-4" />
</Button>
</div>
</header>
{/* Page Content */}
@@ -148,14 +189,14 @@ function DashboardLayout() {
role="button"
tabIndex={-1}
/>
<aside className="absolute left-0 top-0 h-full w-64 bg-background shadow-lg">
<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 px-6">
<span className="text-xl font-bold">Sase.tr</span>
<Button variant="ghost" size="icon" onClick={() => setMobileOpen(false)}>
<X className="h-4 w-4" />
</Button>
</div>
<nav className="space-y-1 p-4">
<nav className="flex-1 space-y-1 overflow-y-auto p-4">
{navItems.map((item) => {
const Icon = item.icon;
return (
@@ -188,6 +229,26 @@ function DashboardLayout() {
);
})}
</nav>
{/* User section - bottom */}
<div className="border-t p-3">
<button
type="button"
onClick={() => {
signOut();
setMobileOpen(false);
}}
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors hover:bg-accent"
>
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground">
{user.name?.charAt(0).toUpperCase() ?? "?"}
</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="h-4 w-4 shrink-0 text-muted-foreground" />
</button>
</div>
</aside>
</div>
)}

View File

@@ -1,5 +1,6 @@
import { useState } from "react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@sase/ui";
import { Input } from "@sase/ui";
import { Card, CardContent } from "@sase/ui";
@@ -22,6 +23,12 @@ function SearchPage() {
const [vin, setVin] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showHistory, setShowHistory] = useState(false);
const { data: history } = useQuery({
queryKey: ["vehicles", "history"],
queryFn: () => api.get<any[]>("/vehicles/history?limit=20"),
});
async function handleSearch(e: React.FormEvent) {
e.preventDefault();
@@ -61,16 +68,40 @@ function SearchPage() {
<Card>
<CardContent className="pt-6">
<form onSubmit={handleSearch} className="flex gap-3">
<Input
placeholder="VIN numarasını girin (17 karakter)"
value={vin}
onChange={(e) => {
setVin(e.target.value.toUpperCase());
setError(null);
}}
maxLength={17}
className="font-mono text-lg tracking-wider"
/>
<div className="relative flex-1">
<Input
placeholder="VIN numarasını girin (17 karakter)"
value={vin}
onChange={(e) => {
setVin(e.target.value.toUpperCase());
setError(null);
}}
onFocus={() => setShowHistory(true)}
onBlur={() => setShowHistory(false)}
maxLength={17}
className="font-mono text-lg tracking-wider"
/>
{showHistory && vin.length === 0 && history && history.length > 0 && (
<div className="absolute z-50 mt-1 max-h-80 w-full overflow-y-auto rounded-md border bg-popover shadow-md">
{history.map((v: any) => (
<div
key={v.id}
className="flex cursor-pointer items-center justify-between px-3 py-2 hover:bg-accent"
onMouseDown={(e) => {
e.preventDefault();
setVin(v.vin);
setShowHistory(false);
}}
>
<span className="font-mono text-sm">{v.vin}</span>
<span className="text-xs text-muted-foreground">
{v.brandName} {v.model} {v.year}
</span>
</div>
))}
</div>
)}
</div>
<Button type="submit" disabled={loading || vin.length !== 17}>
{loading ? (
<span className="animate-spin">...</span>