feat(web): catalog sub-route normalize — CatalogHeader + PostHog on pcat & emex landings

Brings the secondary catalog entry points onto the shared header so
breadcrumbs and back behaviour stop drifting from /catalog/:brandName.

pcat/$catalogId
- Switches the inline "ArrowLeft + h1" combo to CatalogHeader with a
  Katalog / {CATALOG_ID} breadcrumb
- Replaces `Array.from({ length: 8 })` + numeric index keys with
  KEYS_10 (stable, no React-key warning risk)
- `{model.carsCount} araç` was hard-coded TR — now reuses
  `catalog.modelCount2` so EN-locale users see "N model(s)"
- Empty-state img placeholder swapped from arbitrary emerald to
  `bg-primary/10 text-primary` (token-driven, theme-correct)
- preload="intent" on model links

emex/$catalogCode
- CatalogHeader with breadcrumb; reset button moves into the actions slot
- Adds `catalog_emex_wizard_opened` view event for funnel parity with
  pl24/pcat — emex wizard is the highest-cognitive-load surface and we
  had zero data on its open-vs-abandon rate

Future passes will translate the remaining hard-coded TR phrases inside
the wizard body and the matched-vehicles list — kept out of this commit
to avoid sprawl.
This commit is contained in:
2026-06-01 00:28:12 +03:00
parent 141e90d176
commit d9b14af96d
2 changed files with 75 additions and 39 deletions

View File

@@ -1,10 +1,12 @@
import { CatalogHeader } from "@/components/catalog/catalog-header";
import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { capture } from "@/lib/posthog";
import { Button, Skeleton } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute } from "@tanstack/react-router";
import { AlertCircle, ArrowLeft, Car, ChevronRight, RotateCcw } from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { AlertCircle, Car, ChevronRight, RotateCcw } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
export const Route = createFileRoute("/dashboard/catalog_/emex/$catalogCode/")({
component: EmexVehicleListPage,
@@ -44,10 +46,19 @@ interface EmexVehicle {
function EmexVehicleListPage() {
const { t } = useTranslation();
const { catalogCode } = Route.useParams();
const navigate = useNavigate();
const viewedRef = useRef(false);
// Current SSD state for wizard navigation
const [ssd, setSsd] = useState("");
useEffect(() => {
if (!viewedRef.current) {
viewedRef.current = true;
capture("catalog_emex_wizard_opened", { catalog_code: catalogCode });
}
}, [catalogCode]);
// Fetch wizard data for current SSD
const {
data: wizardRows,
@@ -117,22 +128,22 @@ function EmexVehicleListPage() {
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center gap-3">
<Link to="/dashboard/catalog" search={{}}>
<Button variant="ghost" size="sm">
<ArrowLeft className="mr-1 size-4" />
{t("catalog.backToBrands")}
</Button>
</Link>
<h1 className="text-xl font-bold">{decodeURIComponent(catalogCode)}</h1>
{determined.length > 0 && (
<Button variant="ghost" size="sm" onClick={handleReset}>
<RotateCcw className="mr-1 size-3.5" />
{t("catalog.resetSelection")}
</Button>
)}
</div>
<CatalogHeader
crumbs={[
{ label: t("catalog.title"), to: "/dashboard/catalog" },
{ label: decodeURIComponent(catalogCode) },
]}
title={decodeURIComponent(catalogCode)}
onBack={() => navigate({ to: "/dashboard/catalog" })}
actions={
determined.length > 0 ? (
<Button variant="ghost" size="sm" onClick={handleReset}>
<RotateCcw className="mr-1 size-3.5" />
{t("catalog.resetSelection")}
</Button>
) : undefined
}
/>
{/* Determined params — shown as tags */}
{determined.length > 0 && (

View File

@@ -1,9 +1,13 @@
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_10 } from "@/lib/keys";
import { capture } from "@/lib/posthog";
import { Skeleton } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute } from "@tanstack/react-router";
import { ArrowLeft, Library } from "lucide-react";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { Library } from "lucide-react";
import { useEffect, useRef } from "react";
export const Route = createFileRoute("/dashboard/catalog_/pcat/$catalogId/")({
component: PcatModelsPage,
@@ -22,28 +26,39 @@ interface PcatModel {
function PcatModelsPage() {
const { t } = useTranslation();
const { catalogId } = Route.useParams();
const navigate = useNavigate();
const viewedRef = useRef(false);
const { data: models, isLoading } = useQuery({
queryKey: ["pcat-models", catalogId],
queryFn: () => api.get<PcatModel[]>(`/catalog/pcat/catalogs/${catalogId}/models`),
});
useEffect(() => {
if (models && !viewedRef.current) {
viewedRef.current = true;
capture("catalog_pcat_models_viewed", {
catalog_id: catalogId,
count: models.length,
});
}
}, [models, catalogId]);
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
<Link to="/dashboard/catalog" search={{}}>
<Button variant="ghost" size="sm">
<ArrowLeft className="mr-1 size-4" />
{t("catalog.backToBrands")}
</Button>
</Link>
<h1 className="text-xl font-bold">{catalogId.toUpperCase()}</h1>
</div>
<CatalogHeader
crumbs={[
{ label: t("catalog.title"), to: "/dashboard/catalog" },
{ label: catalogId.toUpperCase() },
]}
title={catalogId.toUpperCase()}
onBack={() => navigate({ to: "/dashboard/catalog" })}
/>
{isLoading ? (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton key={i} className="h-28 w-full rounded-xl" />
{KEYS_10.map((__k) => (
<Skeleton key={__k} className="h-28 w-full rounded-xl" />
))}
</div>
) : !models || models.length === 0 ? (
@@ -55,7 +70,15 @@ function PcatModelsPage() {
key={model.id}
to="/dashboard/catalog/pcat/$catalogId/$modelId"
params={{ catalogId, modelId: model.id }}
className="flex flex-col items-center justify-center rounded-xl border border-border bg-card p-4 text-center transition-colors hover:bg-accent hover:border-accent-foreground/20"
onClick={() =>
capture("catalog_pcat_model_clicked", {
catalog_id: catalogId,
model_id: model.id,
model_name: model.name,
})
}
preload="intent"
className="flex flex-col items-center justify-center rounded-xl border border-border bg-card p-4 text-center transition-colors hover:border-primary/30 hover:bg-accent/40"
>
{model.imgUrl ? (
<img
@@ -64,18 +87,20 @@ function PcatModelsPage() {
className="mb-2 h-16 w-auto object-contain"
/>
) : (
<div className="mb-2 flex size-10 items-center justify-center rounded-full bg-emerald-500/10">
<Library className="size-5 text-emerald-500" />
<div className="mb-2 flex size-10 items-center justify-center rounded-full bg-primary/10">
<Library className="size-5 text-primary" />
</div>
)}
<p className="text-sm font-semibold">{model.name}</p>
{(model.yearFrom || model.yearTo) && (
<p className="mt-0.5 text-xs text-muted-foreground">
{model.yearFrom || "?"} - {model.yearTo || "..."}
<p className="mt-0.5 text-xs text-muted-foreground tabular-nums">
{model.yearFrom || "?"} {model.yearTo || ""}
</p>
)}
{model.carsCount > 0 && (
<p className="mt-0.5 text-xs text-muted-foreground">{model.carsCount} araç</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{t("catalog.modelCount2", { count: model.carsCount })}
</p>
)}
</Link>
))}