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:
@@ -2,10 +2,12 @@ const STORAGE_KEY = "userSettings";
|
||||
|
||||
interface UserSettings {
|
||||
categoryViewMode?: "grid" | "tree";
|
||||
sidebarCollapsed?: boolean;
|
||||
}
|
||||
|
||||
const defaults: UserSettings = {
|
||||
categoryViewMode: "grid",
|
||||
sidebarCollapsed: false,
|
||||
};
|
||||
|
||||
export function getUserSettings(): UserSettings {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user