i18n(web): translate the vehicle catalog detail surface
Move every hard-coded Turkish string on /dashboard/vehicles/:id and the drill-down /categories/:categoryId page into a vehicle.* message block (tr + en) — page header, error/404 panels with retry, "Araç Bilgileri" accordion, "Yedek Parça Kategorileri" + the new empty state, equipment title and show-more/less toggle, summary labels — plus the leaf-category catalog-error panel and the drill-down's loading/retry copy. Also translate the CatalogSearch component (placeholder, arias, searching/treeIncomplete/noMatch hints, section headings, parts-count badge) under a new catalogSearch.* block, since it lives inside the same surface and the rest of the page already speaks two languages. PCAT_LABELS / EMEX_LABELS / VINAPI_FIELDS / LABEL_OVERRIDES are source-specific data tables, not UI chrome — left as-is. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { api } from "@/lib/api-client";
|
import { api } from "@/lib/api-client";
|
||||||
import { getCategoryIcon } from "@/lib/category-icons";
|
import { getCategoryIcon } from "@/lib/category-icons";
|
||||||
|
import { useTranslation } from "@/lib/i18n";
|
||||||
import { Badge, Input } from "@sase/ui";
|
import { Badge, Input } from "@sase/ui";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
@@ -71,6 +72,7 @@ export function CatalogSearch({
|
|||||||
vehicleId: string;
|
vehicleId: string;
|
||||||
onActiveChange?: (active: boolean) => void;
|
onActiveChange?: (active: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const debounced = useDebounced(input.trim(), 300);
|
const debounced = useDebounced(input.trim(), 300);
|
||||||
@@ -107,8 +109,8 @@ export function CatalogSearch({
|
|||||||
type="search"
|
type="search"
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
placeholder="Tüm katalogda ara — ör. fren balatası veya OEM kodu"
|
placeholder={t("catalogSearch.placeholder")}
|
||||||
aria-label="Tüm katalogda ara"
|
aria-label={t("catalogSearch.ariaLabel")}
|
||||||
className="pl-9 pr-9"
|
className="pl-9 pr-9"
|
||||||
/>
|
/>
|
||||||
{active && isFetching ? (
|
{active && isFetching ? (
|
||||||
@@ -117,7 +119,7 @@ export function CatalogSearch({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setInput("")}
|
onClick={() => setInput("")}
|
||||||
aria-label="Aramayı temizle"
|
aria-label={t("catalogSearch.clearAria")}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
@@ -129,33 +131,33 @@ export function CatalogSearch({
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center gap-2 py-8 text-sm text-muted-foreground">
|
<div className="flex items-center justify-center gap-2 py-8 text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" /> Aranıyor…
|
<Loader2 className="h-4 w-4 animate-spin" /> {t("catalogSearch.searching")}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{data?.treeIncomplete && (
|
{data?.treeIncomplete && (
|
||||||
<div className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-xs text-muted-foreground">
|
<div className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-xs text-muted-foreground">
|
||||||
<Info className="mt-0.5 h-4 w-4 flex-shrink-0 text-amber-500" />
|
<Info className="mt-0.5 h-4 w-4 flex-shrink-0 text-amber-500" />
|
||||||
<span>
|
<span>{t("catalogSearch.treeIncomplete")}</span>
|
||||||
Bu aracın kataloğu henüz tam indirilmemiş. Arama yalnızca şu ana kadar açılmış
|
|
||||||
kategorileri kapsar — kategorileri gezdikçe kapsam genişler.
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{nothing ? (
|
{nothing ? (
|
||||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||||
"{debounced}" ile eşleşen kategori veya parça bulunamadı.
|
{t("catalogSearch.noMatch", { query: debounced })}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Section 1 — leaf categories */}
|
{/* Section 1 — leaf categories */}
|
||||||
<section>
|
<section>
|
||||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
Kategoriler {categories.length > 0 && `(${categories.length})`}
|
{t("catalogSearch.categoriesHeading")}{" "}
|
||||||
|
{categories.length > 0 && `(${categories.length})`}
|
||||||
</h3>
|
</h3>
|
||||||
{categories.length === 0 ? (
|
{categories.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">Eşleşen kategori yok.</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("catalogSearch.noCategoryMatch")}
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-1.5">
|
<ul className="space-y-1.5">
|
||||||
{categories.map((c) => {
|
{categories.map((c) => {
|
||||||
@@ -179,7 +181,9 @@ export function CatalogSearch({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="flex-shrink-0 text-[11px]"
|
className="flex-shrink-0 text-[11px]"
|
||||||
>
|
>
|
||||||
{c.matchingPartCount} parça
|
{t("catalogSearch.partsCount", {
|
||||||
|
count: c.matchingPartCount,
|
||||||
|
})}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -197,11 +201,11 @@ export function CatalogSearch({
|
|||||||
{/* Section 2 — parts (OEM) */}
|
{/* Section 2 — parts (OEM) */}
|
||||||
<section>
|
<section>
|
||||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
Parçalar (OEM) {parts.length > 0 && `(${parts.length})`}
|
{t("catalogSearch.partsHeading")} {parts.length > 0 && `(${parts.length})`}
|
||||||
</h3>
|
</h3>
|
||||||
{parts.length === 0 ? (
|
{parts.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Eşleşen parça yok. (Yalnızca açılmış kategorilerin parçaları aranır.)
|
{t("catalogSearch.noPartMatch")}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-1.5">
|
<ul className="space-y-1.5">
|
||||||
|
|||||||
@@ -582,5 +582,44 @@
|
|||||||
"pasteInvalidChars": "VIN cannot contain I, O, Q",
|
"pasteInvalidChars": "VIN cannot contain I, O, Q",
|
||||||
"candidateSelectFailed": "Vehicle selection failed",
|
"candidateSelectFailed": "Vehicle selection failed",
|
||||||
"autoCorrected": "Auto-corrected: {list}"
|
"autoCorrected": "Auto-corrected: {list}"
|
||||||
|
},
|
||||||
|
"vehicle": {
|
||||||
|
"title": "Vehicle",
|
||||||
|
"back": "Back",
|
||||||
|
"notFoundTitle": "Vehicle not found",
|
||||||
|
"errorTitle": "Couldn't load vehicle",
|
||||||
|
"notFoundMsg": "This vehicle record was not found",
|
||||||
|
"errorMsg": "Couldn't load vehicle details",
|
||||||
|
"notFoundHint": "The link may be invalid or the record removed.",
|
||||||
|
"errorHint": "An error occurred loading the data.",
|
||||||
|
"retry": "Try again",
|
||||||
|
"info": "Vehicle details",
|
||||||
|
"categories": "Spare parts categories",
|
||||||
|
"noCategoriesTitle": "No categories for this vehicle",
|
||||||
|
"noCategoriesHint": "The vehicle was decoded but the catalog may not be ready yet. Reach out to support to expedite it.",
|
||||||
|
"noAttrs": "No detailed info available for this vehicle.",
|
||||||
|
"labelModel": "Model",
|
||||||
|
"labelYear": "Model year",
|
||||||
|
"labelEngineCode": "Engine code",
|
||||||
|
"equipmentTitle": "Equipment codes ({count})",
|
||||||
|
"showAll": "Show all ({count})",
|
||||||
|
"showLess": "Show less",
|
||||||
|
"categoryDetail": "Category detail",
|
||||||
|
"categoryNotLoadedTitle": "Couldn't load category",
|
||||||
|
"catalogUnavailableTitle": "Catalog isn't available right now",
|
||||||
|
"catalogUnavailableHint": "Couldn't fetch this category from the supplier catalog. Please try again shortly."
|
||||||
|
},
|
||||||
|
"catalogSearch": {
|
||||||
|
"placeholder": "Search the whole catalog — e.g. brake pads or OEM code",
|
||||||
|
"ariaLabel": "Search the whole catalog",
|
||||||
|
"clearAria": "Clear search",
|
||||||
|
"searching": "Searching…",
|
||||||
|
"treeIncomplete": "This vehicle's catalog hasn't been fully indexed yet. Search covers only categories opened so far — browsing more categories expands the scope.",
|
||||||
|
"noMatch": "No categories or parts match \"{query}\".",
|
||||||
|
"categoriesHeading": "Categories",
|
||||||
|
"noCategoryMatch": "No matching categories.",
|
||||||
|
"partsHeading": "Parts (OEM)",
|
||||||
|
"noPartMatch": "No matching parts. (Only parts in opened categories are searched.)",
|
||||||
|
"partsCount": "{count} part(s)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -582,5 +582,44 @@
|
|||||||
"pasteInvalidChars": "Şase numarasında I, O, Q harfleri kullanılamaz",
|
"pasteInvalidChars": "Şase numarasında I, O, Q harfleri kullanılamaz",
|
||||||
"candidateSelectFailed": "Araç seçimi başarısız",
|
"candidateSelectFailed": "Araç seçimi başarısız",
|
||||||
"autoCorrected": "Otomatik düzeltildi: {list}"
|
"autoCorrected": "Otomatik düzeltildi: {list}"
|
||||||
|
},
|
||||||
|
"vehicle": {
|
||||||
|
"title": "Araç",
|
||||||
|
"back": "Geri dön",
|
||||||
|
"notFoundTitle": "Araç bulunamadı",
|
||||||
|
"errorTitle": "Araç yüklenemedi",
|
||||||
|
"notFoundMsg": "Bu araç kaydı bulunamadı",
|
||||||
|
"errorMsg": "Araç bilgileri yüklenemedi",
|
||||||
|
"notFoundHint": "Bağlantı geçersiz olabilir ya da kayıt kaldırılmış olabilir.",
|
||||||
|
"errorHint": "Veriler yüklenirken bir hata oluştu.",
|
||||||
|
"retry": "Tekrar dene",
|
||||||
|
"info": "Araç Bilgileri",
|
||||||
|
"categories": "Yedek Parça Kategorileri",
|
||||||
|
"noCategoriesTitle": "Bu araç için kategori bulunamadı",
|
||||||
|
"noCategoriesHint": "Araç decode edildi ama parça kataloğu henüz hazırlanmamış olabilir. Destek ekibimize bildirirseniz katalog hızlandırılır.",
|
||||||
|
"noAttrs": "Bu araç için ayrıntı bilgisi bulunamadı.",
|
||||||
|
"labelModel": "Model",
|
||||||
|
"labelYear": "Model yılı",
|
||||||
|
"labelEngineCode": "Motor kodu",
|
||||||
|
"equipmentTitle": "Donanım kodları ({count})",
|
||||||
|
"showAll": "Tümünü göster ({count})",
|
||||||
|
"showLess": "Daha az göster",
|
||||||
|
"categoryDetail": "Kategori Detayı",
|
||||||
|
"categoryNotLoadedTitle": "Kategori yüklenemedi",
|
||||||
|
"catalogUnavailableTitle": "Katalog şu an yüklenemedi",
|
||||||
|
"catalogUnavailableHint": "Bu kategori tedarikçi katalogundan alınamadı. Lütfen birazdan tekrar deneyin."
|
||||||
|
},
|
||||||
|
"catalogSearch": {
|
||||||
|
"placeholder": "Tüm katalogda ara — ör. fren balatası veya OEM kodu",
|
||||||
|
"ariaLabel": "Tüm katalogda ara",
|
||||||
|
"clearAria": "Aramayı temizle",
|
||||||
|
"searching": "Aranıyor…",
|
||||||
|
"treeIncomplete": "Bu aracın kataloğu henüz tam indirilmemiş. Arama yalnızca şu ana kadar açılmış kategorileri kapsar — kategorileri gezdikçe kapsam genişler.",
|
||||||
|
"noMatch": "\"{query}\" ile eşleşen kategori veya parça bulunamadı.",
|
||||||
|
"categoriesHeading": "Kategoriler",
|
||||||
|
"noCategoryMatch": "Eşleşen kategori yok.",
|
||||||
|
"partsHeading": "Parçalar (OEM)",
|
||||||
|
"noPartMatch": "Eşleşen parça yok. (Yalnızca açılmış kategorilerin parçaları aranır.)",
|
||||||
|
"partsCount": "{count} parça"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { CategoryTree } from "@/components/categories/category-tree";
|
|||||||
import { CategoryViewToggle } from "@/components/categories/category-view-toggle";
|
import { CategoryViewToggle } from "@/components/categories/category-view-toggle";
|
||||||
import { useCategoryParts } from "@/hooks/use-parts";
|
import { useCategoryParts } from "@/hooks/use-parts";
|
||||||
import { api } from "@/lib/api-client";
|
import { api } from "@/lib/api-client";
|
||||||
|
import { useTranslation } from "@/lib/i18n";
|
||||||
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
|
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
|
||||||
import { cleanModelName } from "@/lib/vehicle";
|
import { cleanModelName } from "@/lib/vehicle";
|
||||||
import { Button, Skeleton } from "@sase/ui";
|
import { Button, Skeleton } from "@sase/ui";
|
||||||
@@ -51,6 +52,7 @@ export const Route = createFileRoute("/dashboard/vehicles_/$id/categories_/$cate
|
|||||||
});
|
});
|
||||||
|
|
||||||
function VehicleCategoryPage() {
|
function VehicleCategoryPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { id, categoryId } = Route.useParams();
|
const { id, categoryId } = Route.useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data, isLoading, error, refetch, isFetching } = useCategoryParts(id, categoryId);
|
const { data, isLoading, error, refetch, isFetching } = useCategoryParts(id, categoryId);
|
||||||
@@ -93,7 +95,7 @@ function VehicleCategoryPage() {
|
|||||||
const cleanModel = cleanModelName(vehicle?.model);
|
const cleanModel = cleanModelName(vehicle?.model);
|
||||||
const vehicleLabel = vehicle?.brandName
|
const vehicleLabel = vehicle?.brandName
|
||||||
? `${vehicle.brandName}${cleanModel ? ` ${cleanModel}` : ""}`
|
? `${vehicle.brandName}${cleanModel ? ` ${cleanModel}` : ""}`
|
||||||
: "Araç";
|
: t("vehicle.title");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -111,8 +113,8 @@ function VehicleCategoryPage() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
title="Geri dön"
|
title={t("vehicle.back")}
|
||||||
aria-label="Geri dön"
|
aria-label={t("vehicle.back")}
|
||||||
data-faro-user-action-name="category-back"
|
data-faro-user-action-name="category-back"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
@@ -123,7 +125,7 @@ function VehicleCategoryPage() {
|
|||||||
(isLoading ? (
|
(isLoading ? (
|
||||||
<span className="inline-block h-6 w-48 animate-pulse rounded-md bg-primary/10 align-middle" />
|
<span className="inline-block h-6 w-48 animate-pulse rounded-md bg-primary/10 align-middle" />
|
||||||
) : (
|
) : (
|
||||||
"Kategori Detayı"
|
t("vehicle.categoryDetail")
|
||||||
))}
|
))}
|
||||||
</h1>
|
</h1>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
@@ -141,9 +143,9 @@ function VehicleCategoryPage() {
|
|||||||
className="flex flex-col items-start gap-3 rounded-lg border border-destructive/40 bg-destructive/5 p-5 text-sm"
|
className="flex flex-col items-start gap-3 rounded-lg border border-destructive/40 bg-destructive/5 p-5 text-sm"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-destructive">Kategori yüklenemedi</p>
|
<p className="font-medium text-destructive">{t("vehicle.categoryNotLoadedTitle")}</p>
|
||||||
<p className="mt-1 text-muted-foreground">
|
<p className="mt-1 text-muted-foreground">
|
||||||
{error instanceof Error ? error.message : "Veriler yüklenirken bir hata oluştu."}
|
{error instanceof Error ? error.message : t("vehicle.errorHint")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -154,7 +156,7 @@ function VehicleCategoryPage() {
|
|||||||
disabled={isFetching}
|
disabled={isFetching}
|
||||||
data-faro-user-action-name="category-retry"
|
data-faro-user-action-name="category-retry"
|
||||||
>
|
>
|
||||||
{isFetching ? "Yükleniyor…" : "Tekrar dene"}
|
{isFetching ? t("common.loading") : t("vehicle.retry")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -183,10 +185,8 @@ function VehicleCategoryPage() {
|
|||||||
className="flex flex-col items-start gap-3 rounded-lg border border-destructive/40 bg-destructive/5 p-5 text-sm"
|
className="flex flex-col items-start gap-3 rounded-lg border border-destructive/40 bg-destructive/5 p-5 text-sm"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-destructive">Katalog şu an yüklenemedi</p>
|
<p className="font-medium text-destructive">{t("vehicle.catalogUnavailableTitle")}</p>
|
||||||
<p className="mt-1 text-muted-foreground">
|
<p className="mt-1 text-muted-foreground">{t("vehicle.catalogUnavailableHint")}</p>
|
||||||
Bu kategori tedarikçi katalogundan alınamadı. Lütfen birazdan tekrar deneyin.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -196,7 +196,7 @@ function VehicleCategoryPage() {
|
|||||||
disabled={isFetching}
|
disabled={isFetching}
|
||||||
data-faro-user-action-name="category-loaderror-retry"
|
data-faro-user-action-name="category-loaderror-retry"
|
||||||
>
|
>
|
||||||
{isFetching ? "Yükleniyor…" : "Tekrar dene"}
|
{isFetching ? t("common.loading") : t("vehicle.retry")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { CategoryViewToggle } from "@/components/categories/category-view-toggle
|
|||||||
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
|
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
|
||||||
import { ApiError, api } from "@/lib/api-client";
|
import { ApiError, api } from "@/lib/api-client";
|
||||||
import { setChatwootVehicle } from "@/lib/chatwoot";
|
import { setChatwootVehicle } from "@/lib/chatwoot";
|
||||||
|
import { useTranslation } from "@/lib/i18n";
|
||||||
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
|
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
|
||||||
import { cleanModelName } from "@/lib/vehicle";
|
import { cleanModelName } from "@/lib/vehicle";
|
||||||
import type { CategoryNode, Vehicle } from "@sase/shared";
|
import type { CategoryNode, Vehicle } from "@sase/shared";
|
||||||
@@ -33,6 +34,7 @@ export const Route = createFileRoute("/dashboard/vehicles_/$id/")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function VehicleDetailPage() {
|
function VehicleDetailPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { id } = Route.useParams();
|
const { id } = Route.useParams();
|
||||||
const [viewMode, setViewMode] = useState<"grid" | "tree" | "columns">(
|
const [viewMode, setViewMode] = useState<"grid" | "tree" | "columns">(
|
||||||
() => getUserSettings().categoryViewMode ?? "grid",
|
() => getUserSettings().categoryViewMode ?? "grid",
|
||||||
@@ -81,7 +83,7 @@ function VehicleDetailPage() {
|
|||||||
|
|
||||||
const vehicleLabel: string = vehicle?.brandName
|
const vehicleLabel: string = vehicle?.brandName
|
||||||
? `${vehicle.brandName}${cleanModelName(vehicle.model) ? ` ${cleanModelName(vehicle.model)}` : ""}`
|
? `${vehicle.brandName}${cleanModelName(vehicle.model) ? ` ${cleanModelName(vehicle.model)}` : ""}`
|
||||||
: "Araç";
|
: t("vehicle.title");
|
||||||
|
|
||||||
// Surface the viewed vehicle to the support chat widget (VIN/brand/model)
|
// Surface the viewed vehicle to the support chat widget (VIN/brand/model)
|
||||||
// so agents have the car context for part-compatibility questions.
|
// so agents have the car context for part-compatibility questions.
|
||||||
@@ -136,13 +138,15 @@ function VehicleDetailPage() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
title="Geri dön"
|
title={t("vehicle.back")}
|
||||||
aria-label="Geri dön"
|
aria-label={t("vehicle.back")}
|
||||||
data-faro-user-action-name="vehicle-back"
|
data-faro-user-action-name="vehicle-back"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<h2 className="text-xl font-bold">{is404 ? "Araç bulunamadı" : "Araç yüklenemedi"}</h2>
|
<h2 className="text-xl font-bold">
|
||||||
|
{is404 ? t("vehicle.notFoundTitle") : t("vehicle.errorTitle")}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@@ -150,14 +154,14 @@ function VehicleDetailPage() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-destructive">
|
<p className="font-medium text-destructive">
|
||||||
{is404 ? "Bu araç kaydı bulunamadı" : "Araç bilgileri yüklenemedi"}
|
{is404 ? t("vehicle.notFoundMsg") : t("vehicle.errorMsg")}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-muted-foreground">
|
<p className="mt-1 text-muted-foreground">
|
||||||
{is404
|
{is404
|
||||||
? "Bağlantı geçersiz olabilir ya da kayıt kaldırılmış olabilir."
|
? t("vehicle.notFoundHint")
|
||||||
: vehicleErrorObj instanceof Error
|
: vehicleErrorObj instanceof Error
|
||||||
? vehicleErrorObj.message
|
? vehicleErrorObj.message
|
||||||
: "Veriler yüklenirken bir hata oluştu."}
|
: t("vehicle.errorHint")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -170,11 +174,11 @@ function VehicleDetailPage() {
|
|||||||
disabled={vehicleFetching}
|
disabled={vehicleFetching}
|
||||||
data-faro-user-action-name="vehicle-retry"
|
data-faro-user-action-name="vehicle-retry"
|
||||||
>
|
>
|
||||||
{vehicleFetching ? "Yükleniyor…" : "Tekrar dene"}
|
{vehicleFetching ? t("common.loading") : t("vehicle.retry")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button type="button" size="sm" variant="ghost" onClick={handleBack}>
|
<Button type="button" size="sm" variant="ghost" onClick={handleBack}>
|
||||||
Geri dön
|
{t("vehicle.back")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,8 +200,8 @@ function VehicleDetailPage() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
title="Geri dön"
|
title={t("vehicle.back")}
|
||||||
aria-label="Geri dön"
|
aria-label={t("vehicle.back")}
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
data-faro-user-action-name="vehicle-back"
|
data-faro-user-action-name="vehicle-back"
|
||||||
>
|
>
|
||||||
@@ -222,7 +226,7 @@ function VehicleDetailPage() {
|
|||||||
<AccordionItem value="info" className="border-b-0">
|
<AccordionItem value="info" className="border-b-0">
|
||||||
<AccordionTrigger className="px-6 hover:no-underline">
|
<AccordionTrigger className="px-6 hover:no-underline">
|
||||||
<div className="flex flex-1 flex-col items-start gap-1.5 pr-3 text-left">
|
<div className="flex flex-1 flex-col items-start gap-1.5 pr-3 text-left">
|
||||||
<span className="text-base font-semibold">Araç Bilgileri</span>
|
<span className="text-base font-semibold">{t("vehicle.info")}</span>
|
||||||
<VehicleSummary vehicle={vehicle} />
|
<VehicleSummary vehicle={vehicle} />
|
||||||
</div>
|
</div>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
@@ -237,7 +241,7 @@ function VehicleDetailPage() {
|
|||||||
{/* Categories */}
|
{/* Categories */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<CardTitle className="text-base">Yedek Parça Kategorileri</CardTitle>
|
<CardTitle className="text-base">{t("vehicle.categories")}</CardTitle>
|
||||||
{!searchActive && <CategoryViewToggle value={viewMode} onChange={changeViewMode} />}
|
{!searchActive && <CategoryViewToggle value={viewMode} onChange={changeViewMode} />}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent
|
<CardContent
|
||||||
@@ -260,11 +264,10 @@ function VehicleDetailPage() {
|
|||||||
) : !categoryTree || categoryTree.length === 0 ? (
|
) : !categoryTree || categoryTree.length === 0 ? (
|
||||||
<div className="rounded-lg border border-dashed border-border bg-muted/30 px-6 py-10 text-center">
|
<div className="rounded-lg border border-dashed border-border bg-muted/30 px-6 py-10 text-center">
|
||||||
<p className="text-sm font-medium text-foreground">
|
<p className="text-sm font-medium text-foreground">
|
||||||
Bu araç için kategori bulunamadı
|
{t("vehicle.noCategoriesTitle")}
|
||||||
</p>
|
</p>
|
||||||
<p className="mx-auto mt-1 max-w-md text-xs text-muted-foreground">
|
<p className="mx-auto mt-1 max-w-md text-xs text-muted-foreground">
|
||||||
Araç decode edildi ama parça kataloğu henüz hazırlanmamış olabilir. Destek
|
{t("vehicle.noCategoriesHint")}
|
||||||
ekibimize bildirirseniz katalog hızlandırılır.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : viewMode === "grid" ? (
|
) : viewMode === "grid" ? (
|
||||||
@@ -495,12 +498,11 @@ function getDisplayAttributes(vehicle: Vehicle): Attr[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function VehicleAttributes({ vehicle }: { vehicle: Vehicle }) {
|
function VehicleAttributes({ vehicle }: { vehicle: Vehicle }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const attrs = getDisplayAttributes(vehicle);
|
const attrs = getDisplayAttributes(vehicle);
|
||||||
|
|
||||||
if (attrs.length === 0) {
|
if (attrs.length === 0) {
|
||||||
return (
|
return <p className="text-sm text-muted-foreground">{t("vehicle.noAttrs")}</p>;
|
||||||
<p className="text-sm text-muted-foreground">Bu araç için ayrıntı bilgisi bulunamadı.</p>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -536,10 +538,11 @@ function getEngineCode(vehicle: Vehicle): string | undefined {
|
|||||||
|
|
||||||
/** Compact at-a-glance fields shown on the collapsed Araç Bilgileri header. */
|
/** Compact at-a-glance fields shown on the collapsed Araç Bilgileri header. */
|
||||||
function VehicleSummary({ vehicle }: { vehicle: Vehicle }) {
|
function VehicleSummary({ vehicle }: { vehicle: Vehicle }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const fields = [
|
const fields = [
|
||||||
{ label: "Model", value: cleanModelName(vehicle.model) },
|
{ label: t("vehicle.labelModel"), value: cleanModelName(vehicle.model) },
|
||||||
{ label: "Model yılı", value: vehicle.year },
|
{ label: t("vehicle.labelYear"), value: vehicle.year },
|
||||||
{ label: "Motor kodu", value: getEngineCode(vehicle) },
|
{ label: t("vehicle.labelEngineCode"), value: getEngineCode(vehicle) },
|
||||||
].filter((f) => f.value != null && f.value !== "");
|
].filter((f) => f.value != null && f.value !== "");
|
||||||
|
|
||||||
if (fields.length === 0) return null;
|
if (fields.length === 0) return null;
|
||||||
@@ -585,6 +588,7 @@ function getEquipment(vehicle: Vehicle): Equipment[] {
|
|||||||
/** Full decoded equipment / PR-code list, shown inside the expanded card. */
|
/** Full decoded equipment / PR-code list, shown inside the expanded card. */
|
||||||
const EQUIPMENT_PREVIEW = 8;
|
const EQUIPMENT_PREVIEW = 8;
|
||||||
function VehicleEquipment({ vehicle }: { vehicle: Vehicle }) {
|
function VehicleEquipment({ vehicle }: { vehicle: Vehicle }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const items = getEquipment(vehicle);
|
const items = getEquipment(vehicle);
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
if (items.length === 0) return null;
|
if (items.length === 0) return null;
|
||||||
@@ -595,7 +599,7 @@ function VehicleEquipment({ vehicle }: { vehicle: Vehicle }) {
|
|||||||
return (
|
return (
|
||||||
<div className="mt-6 border-t border-border pt-4">
|
<div className="mt-6 border-t border-border pt-4">
|
||||||
<p className="mb-3 text-xs font-semibold text-muted-foreground">
|
<p className="mb-3 text-xs font-semibold text-muted-foreground">
|
||||||
Donanım kodları ({items.length})
|
{t("vehicle.equipmentTitle", { count: items.length })}
|
||||||
</p>
|
</p>
|
||||||
<ul className="grid grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-2">
|
<ul className="grid grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-2">
|
||||||
{visible.map((it, i) => (
|
{visible.map((it, i) => (
|
||||||
@@ -617,7 +621,7 @@ function VehicleEquipment({ vehicle }: { vehicle: Vehicle }) {
|
|||||||
onClick={() => setExpanded((v) => !v)}
|
onClick={() => setExpanded((v) => !v)}
|
||||||
className="mt-3 h-8 px-2 text-xs text-muted-foreground hover:text-foreground"
|
className="mt-3 h-8 px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
{expanded ? "Daha az göster" : `Tümünü göster (${items.length})`}
|
{expanded ? t("vehicle.showLess") : t("vehicle.showAll", { count: items.length })}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user