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:
@@ -160,6 +160,27 @@
|
|||||||
"oldest": "Year (oldest first)",
|
"oldest": "Year (oldest first)",
|
||||||
"alphabetical": "Model (A→Z)"
|
"alphabetical": "Model (A→Z)"
|
||||||
},
|
},
|
||||||
|
"categoryCount": "{count} categories",
|
||||||
|
"vehicleSpecs": {
|
||||||
|
"engine": "Engine",
|
||||||
|
"body": "Body",
|
||||||
|
"transmission": "Transmission",
|
||||||
|
"market": "Market"
|
||||||
|
},
|
||||||
|
"pcat": {
|
||||||
|
"searchPlaceholder": "Search vehicle",
|
||||||
|
"vehicleCount": "{count} vehicles",
|
||||||
|
"schemaCount": "{count} schemas"
|
||||||
|
},
|
||||||
|
"emex": {
|
||||||
|
"searchPlaceholder": "Search",
|
||||||
|
"optionCount": "{count} options",
|
||||||
|
"variantCount": "{count} variants",
|
||||||
|
"noResults": "No results found",
|
||||||
|
"noPartsTitle": "No parts data yet for this configuration",
|
||||||
|
"noPartsHint": "Try a different model or variant.",
|
||||||
|
"loadError": "Could not load catalog data. Please try again."
|
||||||
|
},
|
||||||
"tabSasetr": "SASE",
|
"tabSasetr": "SASE",
|
||||||
"tabPl24": "Pl24",
|
"tabPl24": "Pl24",
|
||||||
"tabPcat": "Pcat",
|
"tabPcat": "Pcat",
|
||||||
@@ -1168,4 +1189,4 @@
|
|||||||
"decodeGeneric": "Something went wrong. Please try again."
|
"decodeGeneric": "Something went wrong. Please try again."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,27 @@
|
|||||||
"oldest": "Yıla göre (eskiden yeniye)",
|
"oldest": "Yıla göre (eskiden yeniye)",
|
||||||
"alphabetical": "Modele göre (A→Z)"
|
"alphabetical": "Modele göre (A→Z)"
|
||||||
},
|
},
|
||||||
|
"categoryCount": "{count} kategori",
|
||||||
|
"vehicleSpecs": {
|
||||||
|
"engine": "Motor",
|
||||||
|
"body": "Kasa",
|
||||||
|
"transmission": "Vites",
|
||||||
|
"market": "Pazar"
|
||||||
|
},
|
||||||
|
"pcat": {
|
||||||
|
"searchPlaceholder": "Araç ara",
|
||||||
|
"vehicleCount": "{count} araç",
|
||||||
|
"schemaCount": "{count} şema"
|
||||||
|
},
|
||||||
|
"emex": {
|
||||||
|
"searchPlaceholder": "Ara",
|
||||||
|
"optionCount": "{count} seçenek",
|
||||||
|
"variantCount": "{count} varyant",
|
||||||
|
"noResults": "Sonuç bulunamadı",
|
||||||
|
"noPartsTitle": "Bu araç konfigürasyonu için parça verisi henüz yok",
|
||||||
|
"noPartsHint": "Farklı bir model veya varyant seçmeyi deneyin.",
|
||||||
|
"loadError": "Katalog verileri yüklenemedi. Lütfen tekrar deneyin."
|
||||||
|
},
|
||||||
"tabSasetr": "SASE",
|
"tabSasetr": "SASE",
|
||||||
"tabPl24": "Pl24",
|
"tabPl24": "Pl24",
|
||||||
"tabPcat": "Pcat",
|
"tabPcat": "Pcat",
|
||||||
@@ -1168,4 +1189,4 @@
|
|||||||
"decodeGeneric": "Bir hata oluştu. Lütfen tekrar deneyin."
|
"decodeGeneric": "Bir hata oluştu. Lütfen tekrar deneyin."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
|
import { CatalogHeader } from "@/components/catalog/catalog-header";
|
||||||
import { FordVariantSelector } from "@/components/catalog/ford-variant-selector";
|
import { FordVariantSelector } from "@/components/catalog/ford-variant-selector";
|
||||||
import { P5RestrictionSelector } from "@/components/catalog/p5-restriction-selector";
|
import { P5RestrictionSelector } from "@/components/catalog/p5-restriction-selector";
|
||||||
import { PsaVariantSelector } from "@/components/catalog/psa-variant-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 { CategoryColumns } from "@/components/categories/category-columns";
|
||||||
import { CategoryGrid } from "@/components/categories/category-grid";
|
import { CategoryGrid } from "@/components/categories/category-grid";
|
||||||
import { CategoryTree } from "@/components/categories/category-tree";
|
import { CategoryTree } from "@/components/categories/category-tree";
|
||||||
import {
|
|
||||||
CardsViewIcon,
|
|
||||||
ListViewIcon,
|
|
||||||
TreeViewIcon,
|
|
||||||
} from "@/components/categories/view-toggle-icons";
|
|
||||||
import { api } from "@/lib/api-client";
|
import { api } from "@/lib/api-client";
|
||||||
import { useTranslation } from "@/lib/i18n";
|
import { useTranslation } from "@/lib/i18n";
|
||||||
|
import { KEYS_8 } from "@/lib/keys";
|
||||||
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
|
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 { useQuery } from "@tanstack/react-query";
|
||||||
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { Cog, Gauge, MapPin, Settings2 } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { KEYS_8 } from "@/lib/keys";
|
|
||||||
export const Route = createFileRoute("/dashboard/catalog_/$brandName_/$modelId/")({
|
export const Route = createFileRoute("/dashboard/catalog_/$brandName_/$modelId/")({
|
||||||
validateSearch: (search) => ({
|
validateSearch: (search) => ({
|
||||||
body: typeof search.body === "string" ? search.body : undefined,
|
body: typeof search.body === "string" ? search.body : undefined,
|
||||||
@@ -29,14 +26,7 @@ export const Route = createFileRoute("/dashboard/catalog_/$brandName_/$modelId/"
|
|||||||
component: CatalogVehiclePage,
|
component: CatalogVehiclePage,
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildVariantQuery(body?: string, engine?: string, gearbox?: string): string {
|
type ViewMode = "grid" | "tree" | "columns";
|
||||||
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}` : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function CatalogVehiclePage() {
|
function CatalogVehiclePage() {
|
||||||
const { brandName, modelId } = Route.useParams();
|
const { brandName, modelId } = Route.useParams();
|
||||||
@@ -50,13 +40,13 @@ function CatalogVehiclePage() {
|
|||||||
const mgp = search.mgp;
|
const mgp = search.mgp;
|
||||||
const hasVariant = !!(body || engine || gearbox || mgp);
|
const hasVariant = !!(body || engine || gearbox || mgp);
|
||||||
|
|
||||||
const [viewMode, setViewMode] = useState<"grid" | "tree" | "columns">(
|
const [viewMode, setViewMode] = useState<ViewMode>(
|
||||||
() => getUserSettings().categoryViewMode ?? "grid",
|
() => (getUserSettings().categoryViewMode as ViewMode | undefined) ?? "grid",
|
||||||
);
|
);
|
||||||
|
|
||||||
const decodedBrandName = decodeURIComponent(brandName);
|
const decodedBrandName = decodeURIComponent(brandName);
|
||||||
|
|
||||||
const changeViewMode = (mode: "grid" | "tree" | "columns") => {
|
const changeViewMode = (mode: ViewMode) => {
|
||||||
setViewMode(mode);
|
setViewMode(mode);
|
||||||
setUserSetting("categoryViewMode", mode);
|
setUserSetting("categoryViewMode", mode);
|
||||||
};
|
};
|
||||||
@@ -72,8 +62,6 @@ function CatalogVehiclePage() {
|
|||||||
const isP5WithRestrictions =
|
const isP5WithRestrictions =
|
||||||
vehicle?.architecture === "P5_MODERN" &&
|
vehicle?.architecture === "P5_MODERN" &&
|
||||||
!!vehicle?.catalogPath &&
|
!!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);
|
!/\/maingroup/i.test(vehicle.catalogPath);
|
||||||
const showPsaVariantSelector = isPsa && !hasVariant;
|
const showPsaVariantSelector = isPsa && !hasVariant;
|
||||||
const showFordVariantSelector = isP4Legacy && !hasVariant;
|
const showFordVariantSelector = isP4Legacy && !hasVariant;
|
||||||
@@ -115,9 +103,6 @@ function CatalogVehiclePage() {
|
|||||||
to: "/dashboard/catalog/$brandName/$modelId",
|
to: "/dashboard/catalog/$brandName/$modelId",
|
||||||
params: { brandName, modelId },
|
params: { brandName, modelId },
|
||||||
search: {
|
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),
|
body: norm(selectedBody) ?? (selectedBody === "_nor_" ? "_nor_" : undefined),
|
||||||
engine: norm(selectedEngine),
|
engine: norm(selectedEngine),
|
||||||
gearbox: norm(selectedGearbox),
|
gearbox: norm(selectedGearbox),
|
||||||
@@ -129,144 +114,54 @@ function CatalogVehiclePage() {
|
|||||||
if (vehicleLoading) {
|
if (vehicleLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<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" />
|
<Skeleton className="h-64 w-full" />
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header / Breadcrumb */}
|
<CatalogHeader
|
||||||
<div className="flex items-center justify-between">
|
crumbs={crumbs}
|
||||||
<div className="flex items-center gap-3">
|
title={headerTitle}
|
||||||
<Button
|
onBack={() =>
|
||||||
variant="ghost"
|
navigate({
|
||||||
size="icon"
|
to: "/dashboard/catalog/$brandName",
|
||||||
onClick={() =>
|
params: { brandName },
|
||||||
navigate({
|
search: { catalog: undefined },
|
||||||
to: "/dashboard/catalog/$brandName",
|
})
|
||||||
params: { brandName },
|
}
|
||||||
search: { catalog: undefined },
|
actions={
|
||||||
})
|
!showVariantSelector ? (
|
||||||
}
|
<ViewModeToggle
|
||||||
>
|
value={viewMode}
|
||||||
<ArrowLeft className="size-4" />
|
onChange={changeViewMode}
|
||||||
</Button>
|
groupLabelKey="catalog.view.groupLabel"
|
||||||
<div>
|
/>
|
||||||
<div className="text-xs text-muted-foreground">
|
) : undefined
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Vehicle Info */}
|
{/* Spec chips — flat row instead of a card. Only renders if at least one
|
||||||
{vehicle && (
|
spec is populated. Each chip carries an icon so the row reads as
|
||||||
<Card>
|
metadata, not a free-floating label list. */}
|
||||||
<CardHeader>
|
{vehicle && <VehicleSpecRow vehicle={vehicle} variantSearch={variantSearch} />}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Variant Selector OR Categories */}
|
{/* Variant Selector OR Categories */}
|
||||||
{showPsaVariantSelector ? (
|
{showPsaVariantSelector ? (
|
||||||
@@ -276,36 +171,41 @@ function CatalogVehiclePage() {
|
|||||||
) : showP5RestrictionSelector ? (
|
) : showP5RestrictionSelector ? (
|
||||||
<P5RestrictionSelector vehicleId={modelId} onComplete={handleP5RestrictionComplete} />
|
<P5RestrictionSelector vehicleId={modelId} onComplete={handleP5RestrictionComplete} />
|
||||||
) : (
|
) : (
|
||||||
<Card>
|
<section className="space-y-3">
|
||||||
<CardHeader>
|
<div className="flex items-baseline justify-between">
|
||||||
<CardTitle className="text-base">{t("catalog.categories")}</CardTitle>
|
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
</CardHeader>
|
{t("catalog.categories")}
|
||||||
<CardContent
|
</h2>
|
||||||
className={viewMode === "columns" ? "p-0 overflow-hidden rounded-b-lg" : undefined}
|
{categoryTree && categoryTree.length > 0 && (
|
||||||
>
|
<span className="text-xs tabular-nums text-muted-foreground">
|
||||||
{categoriesLoading ? (
|
{t("catalog.categoryCount", { count: categoryCount })}
|
||||||
<div className="space-y-2">
|
</span>
|
||||||
{KEYS_8.map((__k) => (
|
)}
|
||||||
<Skeleton key={__k} className="h-8 w-full" />
|
</div>
|
||||||
))}
|
{categoriesLoading ? (
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
) : viewMode === "grid" ? (
|
{KEYS_8.map((__k) => (
|
||||||
<CategoryGrid
|
<Skeleton key={__k} className="h-10 w-full rounded-md" />
|
||||||
categories={categoryTree || []}
|
))}
|
||||||
vehicleId={modelId}
|
</div>
|
||||||
catalogMode
|
) : viewMode === "grid" ? (
|
||||||
brandName={brandName}
|
<CategoryGrid
|
||||||
variantSearch={variantSearch}
|
categories={categoryTree || []}
|
||||||
/>
|
vehicleId={modelId}
|
||||||
) : viewMode === "tree" ? (
|
catalogMode
|
||||||
<CategoryTree
|
brandName={brandName}
|
||||||
categories={categoryTree || []}
|
variantSearch={variantSearch}
|
||||||
vehicleId={modelId}
|
/>
|
||||||
catalogMode
|
) : viewMode === "tree" ? (
|
||||||
brandName={brandName}
|
<CategoryTree
|
||||||
variantSearch={variantSearch}
|
categories={categoryTree || []}
|
||||||
/>
|
vehicleId={modelId}
|
||||||
) : (
|
catalogMode
|
||||||
|
brandName={brandName}
|
||||||
|
variantSearch={variantSearch}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-hidden rounded-lg border border-border">
|
||||||
<CategoryColumns
|
<CategoryColumns
|
||||||
categories={categoryTree || []}
|
categories={categoryTree || []}
|
||||||
vehicleId={modelId}
|
vehicleId={modelId}
|
||||||
@@ -313,10 +213,82 @@ function CatalogVehiclePage() {
|
|||||||
brandName={brandName}
|
brandName={brandName}
|
||||||
variantSearch={variantSearch}
|
variantSearch={variantSearch}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</CardContent>
|
)}
|
||||||
</Card>
|
</section>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ function EmexVehicleListPage() {
|
|||||||
{wizardError && (
|
{wizardError && (
|
||||||
<div className="flex items-center gap-2 rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive">
|
<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" />
|
<AlertCircle className="size-4 shrink-0" />
|
||||||
<p>Katalog verileri yüklenemedi. Lütfen tekrar deneyin.</p>
|
<p>{t("catalog.emex.loadError")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -192,7 +192,9 @@ function EmexVehicleListPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : matchedVehicles && matchedVehicles.length > 0 ? (
|
) : matchedVehicles && matchedVehicles.length > 0 ? (
|
||||||
<div className="space-y-1">
|
<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) => (
|
{matchedVehicles.map((v) => (
|
||||||
<VehicleRow key={v.id} vehicle={v} catalogCode={catalogCode} />
|
<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">
|
<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" />
|
<Car className="mx-auto mb-2 size-8 text-muted-foreground/50" />
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">{t("catalog.emex.noPartsTitle")}</p>
|
||||||
Bu araç konfigürasyonu için parça verisi henüz mevcut değil.
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground/70">
|
<p className="mt-1 text-xs text-muted-foreground/70">
|
||||||
Farklı bir model veya varyant seçmeyi deneyin.
|
{t("catalog.emex.noPartsHint")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -228,27 +228,31 @@ function WizardStep({
|
|||||||
row: WizardRow;
|
row: WizardRow;
|
||||||
onSelect: (rowName: string, option: WizardOption) => void;
|
onSelect: (rowName: string, option: WizardOption) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
if (!search) return row.options;
|
if (!search) return row.options;
|
||||||
const q = search.toLowerCase();
|
const q = search.toLocaleLowerCase("tr");
|
||||||
return row.options.filter((o) => o.value.toLowerCase().includes(q));
|
return row.options.filter((o) => o.value.toLocaleLowerCase("tr").includes(q));
|
||||||
}, [row.options, search]);
|
}, [row.options, search]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-sm font-medium">{row.name}</h2>
|
<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>
|
</div>
|
||||||
|
|
||||||
{row.options.length > 10 && (
|
{row.options.length > 10 && (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="search"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
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"
|
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>
|
</button>
|
||||||
))}
|
))}
|
||||||
{filtered.length === 0 && (
|
{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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { CatalogHeader } from "@/components/catalog/catalog-header";
|
||||||
import { api } from "@/lib/api-client";
|
import { api } from "@/lib/api-client";
|
||||||
import { EMEX_GROUP_HIERARCHY } from "@/lib/emex-group-hierarchy";
|
import { EMEX_GROUP_HIERARCHY } from "@/lib/emex-group-hierarchy";
|
||||||
import { useTranslation } from "@/lib/i18n";
|
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 { useQuery } from "@tanstack/react-query";
|
||||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
import { ArrowLeft, ChevronDown, ChevronRight, FolderOpen } from "lucide-react";
|
import { ChevronDown, ChevronRight, FolderOpen } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/catalog_/emex/$catalogCode_/$vehicleId/")({
|
export const Route = createFileRoute("/dashboard/catalog_/emex/$catalogCode_/$vehicleId/")({
|
||||||
@@ -60,6 +62,7 @@ function buildTree(groups: EmexGroup[]): TreeNode[] {
|
|||||||
function EmexGroupListPage() {
|
function EmexGroupListPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { catalogCode, vehicleId } = Route.useParams();
|
const { catalogCode, vehicleId } = Route.useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { data: groups, isLoading } = useQuery({
|
const { data: groups, isLoading } = useQuery({
|
||||||
queryKey: ["emex-groups", vehicleId],
|
queryKey: ["emex-groups", vehicleId],
|
||||||
@@ -69,23 +72,37 @@ function EmexGroupListPage() {
|
|||||||
const tree = groups ? buildTree(groups) : [];
|
const tree = groups ? buildTree(groups) : [];
|
||||||
const isFlat = tree.length === 0 && groups && groups.length > 0;
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-3">
|
<CatalogHeader
|
||||||
<Link to="/dashboard/catalog/emex/$catalogCode" params={{ catalogCode }}>
|
crumbs={crumbs}
|
||||||
<Button variant="ghost" size="sm">
|
title={t("catalog.categories")}
|
||||||
<ArrowLeft className="mr-1 size-4" />
|
onBack={() =>
|
||||||
{t("catalog.backToModels")}
|
navigate({ to: "/dashboard/catalog/emex/$catalogCode", params: { catalogCode } })
|
||||||
</Button>
|
}
|
||||||
</Link>
|
actions={
|
||||||
<h1 className="text-xl font-bold">{t("catalog.categories")}</h1>
|
groups && groups.length > 0 ? (
|
||||||
{groups && <span className="text-sm text-muted-foreground">({groups.length})</span>}
|
<span className="text-xs tabular-nums text-muted-foreground">
|
||||||
</div>
|
{t("catalog.categoryCount", { count: groups.length })}
|
||||||
|
</span>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Array.from({ length: 8 }).map((_, i) => (
|
{KEYS_8.map((__k) => (
|
||||||
<Skeleton key={i} className="h-10 w-full rounded-lg" />
|
<Skeleton key={__k} className="h-10 w-full rounded-lg" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : !groups || groups.length === 0 ? (
|
) : !groups || groups.length === 0 ? (
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { CatalogHeader } from "@/components/catalog/catalog-header";
|
||||||
import { api } from "@/lib/api-client";
|
import { api } from "@/lib/api-client";
|
||||||
import { useTranslation } from "@/lib/i18n";
|
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 { useQuery } from "@tanstack/react-query";
|
||||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
import { ArrowLeft, Car, ChevronRight } from "lucide-react";
|
import { Car, ChevronRight, Search, X } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/dashboard/catalog_/pcat/$catalogId_/$modelId/")({
|
export const Route = createFileRoute("/dashboard/catalog_/pcat/$catalogId_/$modelId/")({
|
||||||
@@ -29,6 +31,7 @@ interface PcatCar {
|
|||||||
function PcatCarsPage() {
|
function PcatCarsPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { catalogId, modelId } = Route.useParams();
|
const { catalogId, modelId } = Route.useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const { data: cars, isLoading } = useQuery({
|
const { data: cars, isLoading } = useQuery({
|
||||||
@@ -39,84 +42,118 @@ function PcatCarsPage() {
|
|||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
if (!cars) return [];
|
if (!cars) return [];
|
||||||
if (!search) return cars;
|
if (!search) return cars;
|
||||||
const q = search.toLowerCase();
|
const q = search.toLocaleLowerCase("tr");
|
||||||
return cars.filter(
|
return cars.filter(
|
||||||
(c) =>
|
(c) =>
|
||||||
c.name.toLowerCase().includes(q) ||
|
c.name.toLocaleLowerCase("tr").includes(q) ||
|
||||||
c.engine?.toLowerCase().includes(q) ||
|
c.engine?.toLocaleLowerCase("tr").includes(q) ||
|
||||||
c.bodyType?.toLowerCase().includes(q),
|
c.bodyType?.toLocaleLowerCase("tr").includes(q),
|
||||||
);
|
);
|
||||||
}, [cars, search]);
|
}, [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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-3">
|
<CatalogHeader
|
||||||
<Link to="/dashboard/catalog/pcat/$catalogId" params={{ catalogId }}>
|
crumbs={crumbs}
|
||||||
<Button variant="ghost" size="sm">
|
title={catalogId.toUpperCase()}
|
||||||
<ArrowLeft className="mr-1 size-4" />
|
onBack={() => navigate({ to: "/dashboard/catalog/pcat/$catalogId", params: { catalogId } })}
|
||||||
{t("catalog.backToModels")}
|
/>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<h1 className="text-xl font-bold">{catalogId.toUpperCase()}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Array.from({ length: 6 }).map((_, i) => (
|
{KEYS_8.map((__k) => (
|
||||||
<Skeleton key={i} className="h-14 w-full rounded-lg" />
|
<Skeleton key={__k} className="h-14 w-full rounded-lg" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : !cars || cars.length === 0 ? (
|
) : !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 && (
|
{cars.length > 10 && (
|
||||||
<input
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
type="text"
|
<div className="relative flex-1 sm:max-w-md">
|
||||||
value={search}
|
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
<Input
|
||||||
placeholder="Ara..."
|
type="search"
|
||||||
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"
|
value={search}
|
||||||
/>
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
)}
|
placeholder={t("catalog.pcat.searchPlaceholder")}
|
||||||
<div className="space-y-1">
|
aria-label={t("catalog.pcat.searchPlaceholder")}
|
||||||
<p className="text-xs text-muted-foreground">{filtered.length} araç</p>
|
className="pl-9"
|
||||||
{filtered.map((car) => (
|
/>
|
||||||
<Link
|
{search && (
|
||||||
key={car.id}
|
<button
|
||||||
to="/dashboard/catalog/pcat/$catalogId/$modelId/$carId"
|
type="button"
|
||||||
params={{ catalogId, modelId, carId: car.id }}
|
onClick={() => setSearch("")}
|
||||||
className="flex items-center gap-3 rounded-lg border border-border bg-card px-3 py-2.5 transition-colors hover:bg-accent"
|
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"
|
||||||
<Car className="size-4 shrink-0 text-muted-foreground" />
|
>
|
||||||
<div className="min-w-0 flex-1">
|
<X className="size-3.5" />
|
||||||
<p className="truncate text-sm font-medium">{car.name}</p>
|
</button>
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
<ChevronRight className="size-4 shrink-0 text-muted-foreground" />
|
</div>
|
||||||
</Link>
|
<span className="text-xs tabular-nums text-muted-foreground">
|
||||||
))}
|
{t("catalog.pcat.vehicleCount", { count: filtered.length })}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user