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:
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user