fix(catalog): kill "Modeller" empty-card on vehicle page + unify drill headers

The PL24 vehicle page rendered a Card titled `t("catalog.models")` ("Modeller")
whose content was the *selected vehicle's* specs (motor/kasa/vites/pazar). When
all four spec fields were null — common for sparse decodes — the user saw an
empty card with a bogus "Modeller" heading just below the model name.

Replace the misleading card with a flat spec-chip row (icon + label : value),
hidden entirely when no specs exist. Use the shared CatalogHeader so brand →
vehicle has a proper breadcrumb back to "/catalog/$brandName" and uses the same
back-button affordance as the rest of the catalog surface. Use the shared
ViewModeToggle so view-mode buttons stop having hardcoded TR title attrs
("Izgara", "Agac", "Sutun") and inherit the i18n + a11y from the component.

Same cleanup for pcat/$catalogId_/$modelId and emex/$catalogCode_/$vehicleId:
both rolled their own header — both now use CatalogHeader with full crumbs and
i18n'd search/count/empty-state strings (catalog.pcat.*, catalog.emex.*).

Why now: catalog flow audit caught the empty-card bug ("model seçilince
yukarıda boş bir alan kalıyor"), plus drift between drill levels (some pages
used CatalogHeader, vehicle/pcat/emex did not). One unified pattern across
brands → models → vehicle → categories.

i18n: +catalog.categoryCount, +catalog.vehicleSpecs.{engine,body,transmission,
market}, +catalog.pcat.{searchPlaceholder,vehicleCount,schemaCount},
+catalog.emex.{searchPlaceholder,optionCount,variantCount,noResults,
noPartsTitle,noPartsHint,loadError}.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 03:01:09 +03:00
parent 2040686e4d
commit 37aa4dfd02
6 changed files with 358 additions and 284 deletions

View File

@@ -1,24 +1,21 @@
import { CatalogHeader } from "@/components/catalog/catalog-header";
import { FordVariantSelector } from "@/components/catalog/ford-variant-selector";
import { P5RestrictionSelector } from "@/components/catalog/p5-restriction-selector";
import { PsaVariantSelector } from "@/components/catalog/psa-variant-selector";
import { ViewModeToggle } from "@/components/catalog/view-mode-toggle";
import { CategoryColumns } from "@/components/categories/category-columns";
import { CategoryGrid } from "@/components/categories/category-grid";
import { CategoryTree } from "@/components/categories/category-tree";
import {
CardsViewIcon,
ListViewIcon,
TreeViewIcon,
} from "@/components/categories/view-toggle-icons";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { KEYS_8 } from "@/lib/keys";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@sase/ui";
import { Skeleton } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { ArrowLeft } from "lucide-react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { Cog, Gauge, MapPin, Settings2 } from "lucide-react";
import { useState } from "react";
import { KEYS_8 } from "@/lib/keys";
export const Route = createFileRoute("/dashboard/catalog_/$brandName_/$modelId/")({
validateSearch: (search) => ({
body: typeof search.body === "string" ? search.body : undefined,
@@ -29,14 +26,7 @@ export const Route = createFileRoute("/dashboard/catalog_/$brandName_/$modelId/"
component: CatalogVehiclePage,
});
function buildVariantQuery(body?: string, engine?: string, gearbox?: string): string {
const params = new URLSearchParams();
if (body) params.set("body", body);
if (engine) params.set("engine", engine);
if (gearbox) params.set("gearbox", gearbox);
const qs = params.toString();
return qs ? `?${qs}` : "";
}
type ViewMode = "grid" | "tree" | "columns";
function CatalogVehiclePage() {
const { brandName, modelId } = Route.useParams();
@@ -50,13 +40,13 @@ function CatalogVehiclePage() {
const mgp = search.mgp;
const hasVariant = !!(body || engine || gearbox || mgp);
const [viewMode, setViewMode] = useState<"grid" | "tree" | "columns">(
() => getUserSettings().categoryViewMode ?? "grid",
const [viewMode, setViewMode] = useState<ViewMode>(
() => (getUserSettings().categoryViewMode as ViewMode | undefined) ?? "grid",
);
const decodedBrandName = decodeURIComponent(brandName);
const changeViewMode = (mode: "grid" | "tree" | "columns") => {
const changeViewMode = (mode: ViewMode) => {
setViewMode(mode);
setUserSetting("categoryViewMode", mode);
};
@@ -72,8 +62,6 @@ function CatalogVehiclePage() {
const isP5WithRestrictions =
vehicle?.architecture === "P5_MODERN" &&
!!vehicle?.catalogPath &&
// Case-insensitive: Fiat's catalogPath is already a maingroups endpoint
// (/extern/groups/mdl/maingroups) → no restriction step, categories load directly.
!/\/maingroup/i.test(vehicle.catalogPath);
const showPsaVariantSelector = isPsa && !hasVariant;
const showFordVariantSelector = isP4Legacy && !hasVariant;
@@ -115,9 +103,6 @@ function CatalogVehiclePage() {
to: "/dashboard/catalog/$brandName/$modelId",
params: { brandName, modelId },
search: {
// For Ford (catCode) / Volvo (year) variants: keep the meaningful selection,
// strip _all_ / _nor_ (no-restriction) values to keep URL clean.
// Special case: if ALL are _nor_, pass body="_nor_" so hasVariant=true skips re-showing selector.
body: norm(selectedBody) ?? (selectedBody === "_nor_" ? "_nor_" : undefined),
engine: norm(selectedEngine),
gearbox: norm(selectedGearbox),
@@ -129,144 +114,54 @@ function CatalogVehiclePage() {
if (vehicleLoading) {
return (
<div className="space-y-6">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-9 w-72" />
<Skeleton className="h-6 w-96" />
<Skeleton className="h-64 w-full" />
</div>
);
}
const titleYear = vehicle?.year ? ` (${vehicle.year})` : "";
const headerTitle = `${vehicle?.model ?? decodedBrandName}${titleYear}`;
const categoryCount = categoryTree?.length ?? 0;
const crumbs = [
{ label: t("catalog.title"), to: "/dashboard/catalog" },
{
label: decodedBrandName,
to: "/dashboard/catalog/$brandName",
search: { catalog: undefined },
},
{ label: vehicle?.model ?? "" },
];
return (
<div className="space-y-6">
{/* Header / Breadcrumb */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onClick={() =>
navigate({
to: "/dashboard/catalog/$brandName",
params: { brandName },
search: { catalog: undefined },
})
}
>
<ArrowLeft className="size-4" />
</Button>
<div>
<div className="text-xs text-muted-foreground">
<Link to="/dashboard/catalog" className="hover:underline">
{t("catalog.title")}
</Link>
{" / "}
<Link
to="/dashboard/catalog/$brandName"
params={{ brandName }}
search={{ catalog: undefined }}
className="hover:underline"
>
{decodedBrandName}
</Link>
{" / "}
<span className="font-medium text-foreground">{vehicle?.model}</span>
{hasVariant && (
<>
{body && body !== "_all_" && (
<>
<span className="mx-1">/</span>
<span className="font-medium text-foreground">{body}</span>
</>
)}
{engine && engine !== "_all_" && (
<>
<span className="mx-1">/</span>
<span className="font-medium text-foreground">{engine}</span>
</>
)}
{gearbox && gearbox !== "_all_" && (
<>
<span className="mx-1">/</span>
<span className="font-medium text-foreground">{gearbox}</span>
</>
)}
</>
)}
</div>
<h1 className="text-xl font-bold">
{vehicle?.model}
{vehicle?.year && (
<span className="ml-2 text-base font-normal text-muted-foreground">
({vehicle.year})
</span>
)}
</h1>
</div>
</div>
{/* View toggle — always visible */}
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => changeViewMode("grid")}
className={`rounded p-1.5 ${viewMode === "grid" ? "bg-accent" : "text-muted-foreground hover:text-foreground"}`}
title="Izgara"
>
<CardsViewIcon isActive={viewMode === "grid"} />
</button>
<button
type="button"
onClick={() => changeViewMode("tree")}
className={`rounded p-1.5 ${viewMode === "tree" ? "bg-accent" : "text-muted-foreground hover:text-foreground"}`}
title="Agac"
>
<TreeViewIcon isActive={viewMode === "tree"} />
</button>
<button
type="button"
onClick={() => changeViewMode("columns")}
className={`rounded p-1.5 ${viewMode === "columns" ? "bg-accent" : "text-muted-foreground hover:text-foreground"}`}
title="Sutun"
>
<ListViewIcon isActive={viewMode === "columns"} />
</button>
</div>
</div>
<CatalogHeader
crumbs={crumbs}
title={headerTitle}
onBack={() =>
navigate({
to: "/dashboard/catalog/$brandName",
params: { brandName },
search: { catalog: undefined },
})
}
actions={
!showVariantSelector ? (
<ViewModeToggle
value={viewMode}
onChange={changeViewMode}
groupLabelKey="catalog.view.groupLabel"
/>
) : undefined
}
/>
{/* Vehicle Info */}
{vehicle && (
<Card>
<CardHeader>
<CardTitle className="text-base">{t("catalog.models")}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-3 text-sm sm:grid-cols-3">
{vehicle.engine && (
<div>
<span className="text-muted-foreground">Motor:</span>{" "}
<span className="font-medium">{vehicle.engine}</span>
</div>
)}
{vehicle.bodyType && (
<div>
<span className="text-muted-foreground">Kasa:</span>{" "}
<span className="font-medium">{vehicle.bodyType}</span>
</div>
)}
{vehicle.transmission && (
<div>
<span className="text-muted-foreground">Vites:</span>{" "}
<span className="font-medium">{vehicle.transmission}</span>
</div>
)}
{vehicle.market && (
<div>
<span className="text-muted-foreground">Pazar:</span>{" "}
<span className="font-medium">{vehicle.market}</span>
</div>
)}
</div>
</CardContent>
</Card>
)}
{/* Spec chips — flat row instead of a card. Only renders if at least one
spec is populated. Each chip carries an icon so the row reads as
metadata, not a free-floating label list. */}
{vehicle && <VehicleSpecRow vehicle={vehicle} variantSearch={variantSearch} />}
{/* Variant Selector OR Categories */}
{showPsaVariantSelector ? (
@@ -276,36 +171,41 @@ function CatalogVehiclePage() {
) : showP5RestrictionSelector ? (
<P5RestrictionSelector vehicleId={modelId} onComplete={handleP5RestrictionComplete} />
) : (
<Card>
<CardHeader>
<CardTitle className="text-base">{t("catalog.categories")}</CardTitle>
</CardHeader>
<CardContent
className={viewMode === "columns" ? "p-0 overflow-hidden rounded-b-lg" : undefined}
>
{categoriesLoading ? (
<div className="space-y-2">
{KEYS_8.map((__k) => (
<Skeleton key={__k} className="h-8 w-full" />
))}
</div>
) : viewMode === "grid" ? (
<CategoryGrid
categories={categoryTree || []}
vehicleId={modelId}
catalogMode
brandName={brandName}
variantSearch={variantSearch}
/>
) : viewMode === "tree" ? (
<CategoryTree
categories={categoryTree || []}
vehicleId={modelId}
catalogMode
brandName={brandName}
variantSearch={variantSearch}
/>
) : (
<section className="space-y-3">
<div className="flex items-baseline justify-between">
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
{t("catalog.categories")}
</h2>
{categoryTree && categoryTree.length > 0 && (
<span className="text-xs tabular-nums text-muted-foreground">
{t("catalog.categoryCount", { count: categoryCount })}
</span>
)}
</div>
{categoriesLoading ? (
<div className="space-y-2">
{KEYS_8.map((__k) => (
<Skeleton key={__k} className="h-10 w-full rounded-md" />
))}
</div>
) : viewMode === "grid" ? (
<CategoryGrid
categories={categoryTree || []}
vehicleId={modelId}
catalogMode
brandName={brandName}
variantSearch={variantSearch}
/>
) : viewMode === "tree" ? (
<CategoryTree
categories={categoryTree || []}
vehicleId={modelId}
catalogMode
brandName={brandName}
variantSearch={variantSearch}
/>
) : (
<div className="overflow-hidden rounded-lg border border-border">
<CategoryColumns
categories={categoryTree || []}
vehicleId={modelId}
@@ -313,10 +213,82 @@ function CatalogVehiclePage() {
brandName={brandName}
variantSearch={variantSearch}
/>
)}
</CardContent>
</Card>
</div>
)}
</section>
)}
</div>
);
}
function VehicleSpecRow({
vehicle,
variantSearch,
}: {
vehicle: {
engine?: string | null;
bodyType?: string | null;
transmission?: string | null;
market?: string | null;
};
variantSearch?: { body?: string; engine?: string; gearbox?: string; mgp?: string };
}) {
const { t } = useTranslation();
// Show the catalog-vehicle's intrinsic specs first; overlay the actively
// selected variant (PSA/Ford restrictions) when the user has drilled
// further. Skip non-meaningful sentinels.
const items: Array<{ key: string; label: string; value: string; Icon: typeof Gauge }> = [];
const skip = (v?: string | null) => !v || v === "_all_" || v === "_nor_";
const engineValue = !skip(variantSearch?.engine) ? variantSearch?.engine : vehicle.engine;
const bodyValue = !skip(variantSearch?.body) ? variantSearch?.body : vehicle.bodyType;
const gearboxValue = !skip(variantSearch?.gearbox)
? variantSearch?.gearbox
: vehicle.transmission;
if (engineValue)
items.push({
key: "engine",
label: t("catalog.vehicleSpecs.engine"),
value: engineValue,
Icon: Gauge,
});
if (bodyValue)
items.push({
key: "body",
label: t("catalog.vehicleSpecs.body"),
value: bodyValue,
Icon: Cog,
});
if (gearboxValue)
items.push({
key: "transmission",
label: t("catalog.vehicleSpecs.transmission"),
value: gearboxValue,
Icon: Settings2,
});
if (vehicle.market)
items.push({
key: "market",
label: t("catalog.vehicleSpecs.market"),
value: vehicle.market,
Icon: MapPin,
});
if (items.length === 0) return null;
return (
<div className="flex flex-wrap items-center gap-2">
{items.map(({ key, label, value, Icon }) => (
<span
key={key}
className="inline-flex items-center gap-2 rounded-full border border-border bg-muted/40 px-3 py-1 text-xs"
>
<Icon className="size-3.5 text-muted-foreground" aria-hidden="true" />
<span className="text-muted-foreground">{label}:</span>
<span className="font-medium text-foreground">{value}</span>
</span>
))}
</div>
);
}

View File

@@ -163,7 +163,7 @@ function EmexVehicleListPage() {
{wizardError && (
<div className="flex items-center gap-2 rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive">
<AlertCircle className="size-4 shrink-0" />
<p>Katalog verileri yüklenemedi. Lütfen tekrar deneyin.</p>
<p>{t("catalog.emex.loadError")}</p>
</div>
)}
@@ -192,7 +192,9 @@ function EmexVehicleListPage() {
</div>
) : matchedVehicles && matchedVehicles.length > 0 ? (
<div className="space-y-1">
<p className="text-xs text-muted-foreground">{matchedVehicles.length} varyant</p>
<p className="text-xs tabular-nums text-muted-foreground">
{t("catalog.emex.variantCount", { count: matchedVehicles.length })}
</p>
{matchedVehicles.map((v) => (
<VehicleRow key={v.id} vehicle={v} catalogCode={catalogCode} />
))}
@@ -200,11 +202,9 @@ function EmexVehicleListPage() {
) : (
<div className="rounded-lg border border-dashed border-border py-8 text-center">
<Car className="mx-auto mb-2 size-8 text-muted-foreground/50" />
<p className="text-sm text-muted-foreground">
Bu araç konfigürasyonu için parça verisi henüz mevcut değil.
</p>
<p className="text-sm text-muted-foreground">{t("catalog.emex.noPartsTitle")}</p>
<p className="mt-1 text-xs text-muted-foreground/70">
Farklı bir model veya varyant seçmeyi deneyin.
{t("catalog.emex.noPartsHint")}
</p>
</div>
)}
@@ -228,27 +228,31 @@ function WizardStep({
row: WizardRow;
onSelect: (rowName: string, option: WizardOption) => void;
}) {
const { t } = useTranslation();
const [search, setSearch] = useState("");
const filtered = useMemo(() => {
if (!search) return row.options;
const q = search.toLowerCase();
return row.options.filter((o) => o.value.toLowerCase().includes(q));
const q = search.toLocaleLowerCase("tr");
return row.options.filter((o) => o.value.toLocaleLowerCase("tr").includes(q));
}, [row.options, search]);
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h2 className="text-sm font-medium">{row.name}</h2>
<span className="text-xs text-muted-foreground">{row.options.length} seçenek</span>
<span className="text-xs tabular-nums text-muted-foreground">
{t("catalog.emex.optionCount", { count: row.options.length })}
</span>
</div>
{row.options.length > 10 && (
<input
type="text"
type="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Ara..."
placeholder={t("catalog.emex.searchPlaceholder")}
aria-label={t("catalog.emex.searchPlaceholder")}
className="h-8 w-full rounded-md border border-input bg-background px-3 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
/>
)}
@@ -266,7 +270,9 @@ function WizardStep({
</button>
))}
{filtered.length === 0 && (
<p className="py-4 text-center text-sm text-muted-foreground">Sonuç bulunamadı</p>
<p className="py-4 text-center text-sm text-muted-foreground">
{t("catalog.emex.noResults")}
</p>
)}
</div>
</div>

View File

@@ -1,10 +1,12 @@
import { CatalogHeader } from "@/components/catalog/catalog-header";
import { api } from "@/lib/api-client";
import { EMEX_GROUP_HIERARCHY } from "@/lib/emex-group-hierarchy";
import { useTranslation } from "@/lib/i18n";
import { Button, Skeleton } from "@sase/ui";
import { KEYS_8 } from "@/lib/keys";
import { Skeleton } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute } from "@tanstack/react-router";
import { ArrowLeft, ChevronDown, ChevronRight, FolderOpen } from "lucide-react";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { ChevronDown, ChevronRight, FolderOpen } from "lucide-react";
import { useState } from "react";
export const Route = createFileRoute("/dashboard/catalog_/emex/$catalogCode_/$vehicleId/")({
@@ -60,6 +62,7 @@ function buildTree(groups: EmexGroup[]): TreeNode[] {
function EmexGroupListPage() {
const { t } = useTranslation();
const { catalogCode, vehicleId } = Route.useParams();
const navigate = useNavigate();
const { data: groups, isLoading } = useQuery({
queryKey: ["emex-groups", vehicleId],
@@ -69,23 +72,37 @@ function EmexGroupListPage() {
const tree = groups ? buildTree(groups) : [];
const isFlat = tree.length === 0 && groups && groups.length > 0;
const crumbs = [
{ label: t("catalog.title"), to: "/dashboard/catalog" },
{
label: decodeURIComponent(catalogCode),
to: "/dashboard/catalog/emex/$catalogCode",
search: {},
},
{ label: t("catalog.categories") },
];
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
<Link to="/dashboard/catalog/emex/$catalogCode" params={{ catalogCode }}>
<Button variant="ghost" size="sm">
<ArrowLeft className="mr-1 size-4" />
{t("catalog.backToModels")}
</Button>
</Link>
<h1 className="text-xl font-bold">{t("catalog.categories")}</h1>
{groups && <span className="text-sm text-muted-foreground">({groups.length})</span>}
</div>
<div className="space-y-6">
<CatalogHeader
crumbs={crumbs}
title={t("catalog.categories")}
onBack={() =>
navigate({ to: "/dashboard/catalog/emex/$catalogCode", params: { catalogCode } })
}
actions={
groups && groups.length > 0 ? (
<span className="text-xs tabular-nums text-muted-foreground">
{t("catalog.categoryCount", { count: groups.length })}
</span>
) : undefined
}
/>
{isLoading ? (
<div className="space-y-2">
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton key={i} className="h-10 w-full rounded-lg" />
{KEYS_8.map((__k) => (
<Skeleton key={__k} className="h-10 w-full rounded-lg" />
))}
</div>
) : !groups || groups.length === 0 ? (

View File

@@ -1,9 +1,11 @@
import { CatalogHeader } from "@/components/catalog/catalog-header";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { Button, Skeleton } from "@sase/ui";
import { KEYS_8 } from "@/lib/keys";
import { Input, Skeleton } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute } from "@tanstack/react-router";
import { ArrowLeft, Car, ChevronRight } from "lucide-react";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { Car, ChevronRight, Search, X } from "lucide-react";
import { useMemo, useState } from "react";
export const Route = createFileRoute("/dashboard/catalog_/pcat/$catalogId_/$modelId/")({
@@ -29,6 +31,7 @@ interface PcatCar {
function PcatCarsPage() {
const { t } = useTranslation();
const { catalogId, modelId } = Route.useParams();
const navigate = useNavigate();
const [search, setSearch] = useState("");
const { data: cars, isLoading } = useQuery({
@@ -39,84 +42,118 @@ function PcatCarsPage() {
const filtered = useMemo(() => {
if (!cars) return [];
if (!search) return cars;
const q = search.toLowerCase();
const q = search.toLocaleLowerCase("tr");
return cars.filter(
(c) =>
c.name.toLowerCase().includes(q) ||
c.engine?.toLowerCase().includes(q) ||
c.bodyType?.toLowerCase().includes(q),
c.name.toLocaleLowerCase("tr").includes(q) ||
c.engine?.toLocaleLowerCase("tr").includes(q) ||
c.bodyType?.toLocaleLowerCase("tr").includes(q),
);
}, [cars, search]);
const crumbs = [
{ label: t("catalog.title"), to: "/dashboard/catalog" },
{
label: catalogId.toUpperCase(),
to: "/dashboard/catalog/pcat/$catalogId",
search: {},
},
{ label: t("catalog.models") },
];
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
<Link to="/dashboard/catalog/pcat/$catalogId" params={{ catalogId }}>
<Button variant="ghost" size="sm">
<ArrowLeft className="mr-1 size-4" />
{t("catalog.backToModels")}
</Button>
</Link>
<h1 className="text-xl font-bold">{catalogId.toUpperCase()}</h1>
</div>
<div className="space-y-6">
<CatalogHeader
crumbs={crumbs}
title={catalogId.toUpperCase()}
onBack={() => navigate({ to: "/dashboard/catalog/pcat/$catalogId", params: { catalogId } })}
/>
{isLoading ? (
<div className="space-y-2">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-14 w-full rounded-lg" />
{KEYS_8.map((__k) => (
<Skeleton key={__k} className="h-14 w-full rounded-lg" />
))}
</div>
) : !cars || cars.length === 0 ? (
<p className="py-8 text-center text-muted-foreground">{t("catalog.noModels")}</p>
<div className="flex flex-col items-center justify-center py-16 text-center">
<Car className="mb-4 size-12 text-muted-foreground/40" />
<p className="text-muted-foreground">{t("catalog.noModels")}</p>
</div>
) : (
<>
<div className="space-y-3">
{cars.length > 10 && (
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Ara..."
className="h-8 w-full rounded-md border border-input bg-background px-3 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
/>
)}
<div className="space-y-1">
<p className="text-xs text-muted-foreground">{filtered.length} araç</p>
{filtered.map((car) => (
<Link
key={car.id}
to="/dashboard/catalog/pcat/$catalogId/$modelId/$carId"
params={{ catalogId, modelId, carId: car.id }}
className="flex items-center gap-3 rounded-lg border border-border bg-card px-3 py-2.5 transition-colors hover:bg-accent"
>
<Car className="size-4 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{car.name}</p>
<p className="truncate text-xs text-muted-foreground">
{[
car.engine,
car.bodyType,
car.transmission,
car.fuelType,
car.yearFrom && car.yearTo
? `${car.yearFrom}-${car.yearTo}`
: car.yearFrom
? `${car.yearFrom}+`
: null,
]
.filter(Boolean)
.join(" · ")}
</p>
</div>
{car.schemasCount > 0 && (
<span className="shrink-0 text-xs text-muted-foreground">
{car.schemasCount} şema
</span>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="relative flex-1 sm:max-w-md">
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
type="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t("catalog.pcat.searchPlaceholder")}
aria-label={t("catalog.pcat.searchPlaceholder")}
className="pl-9"
/>
{search && (
<button
type="button"
onClick={() => setSearch("")}
aria-label={t("common.cancel")}
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-muted-foreground hover:text-foreground"
>
<X className="size-3.5" />
</button>
)}
<ChevronRight className="size-4 shrink-0 text-muted-foreground" />
</Link>
))}
</div>
</>
</div>
<span className="text-xs tabular-nums text-muted-foreground">
{t("catalog.pcat.vehicleCount", { count: filtered.length })}
</span>
</div>
)}
{filtered.length === 0 ? (
<div className="rounded-2xl border border-border bg-muted/10 p-8 text-center">
<p className="text-sm font-medium">{t("catalog.modelSearchNoMatch")}</p>
</div>
) : (
<div className="space-y-1.5">
{filtered.map((car) => (
<Link
key={car.id}
to="/dashboard/catalog/pcat/$catalogId/$modelId/$carId"
params={{ catalogId, modelId, carId: car.id }}
preload="intent"
className="flex items-center gap-3 rounded-lg border border-border bg-card px-3 py-3 transition-colors hover:border-primary/30 hover:bg-accent/40"
>
<Car className="size-4 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{car.name}</p>
<p className="truncate text-xs text-muted-foreground">
{[
car.engine,
car.bodyType,
car.transmission,
car.fuelType,
car.yearFrom && car.yearTo
? `${car.yearFrom}-${car.yearTo}`
: car.yearFrom
? `${car.yearFrom}+`
: null,
]
.filter(Boolean)
.join(" · ")}
</p>
</div>
{car.schemasCount > 0 && (
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">
{t("catalog.pcat.schemaCount", { count: car.schemasCount })}
</span>
)}
<ChevronRight className="size-4 shrink-0 text-muted-foreground" />
</Link>
))}
</div>
)}
</div>
)}
</div>
);