feat(web): vehicle detail polish — analytics, chatwoot cleanup, skeleton, heading

- Capture category_view_changed and catalog_search_opened so the funnel
  picks up category-view and cross-tree search activation (previously
  uninstrumented).
- Expose clearChatwootVehicle() and call it from the route's effect
  cleanup so a later support chat doesn't carry the previously-viewed
  car's VIN/brand.
- Match the loading skeleton shape to the active viewMode — render a
  card grid for grid mode (was always 8 row-bars).
- Switch the vehicle title to the display font (font-display, tracking-tight)
  for typography parity with the rest of the app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 21:45:04 +03:00
parent b9dd154bfd
commit a4597544b4
2 changed files with 35 additions and 8 deletions

View File

@@ -117,3 +117,13 @@ export function setChatwootVehicle(vehicle: {
window.$chatwoot?.setCustomAttributes(attrs);
});
}
/** Drop the vehicle context — call on unmount so a later support chat doesn't
* carry the previously-viewed car. */
export function clearChatwootVehicle(): void {
whenReady(() => {
window.$chatwoot?.deleteCustomAttribute("son_arac_id");
window.$chatwoot?.deleteCustomAttribute("son_arac_vin");
window.$chatwoot?.deleteCustomAttribute("son_arac");
});
}

View File

@@ -6,8 +6,9 @@ import { CategoryTree } from "@/components/categories/category-tree";
import { CategoryViewToggle } from "@/components/categories/category-view-toggle";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { ApiError, api } from "@/lib/api-client";
import { setChatwootVehicle } from "@/lib/chatwoot";
import { clearChatwootVehicle, setChatwootVehicle } from "@/lib/chatwoot";
import { useTranslation } from "@/lib/i18n";
import { capture } from "@/lib/posthog";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { cleanModelName } from "@/lib/vehicle";
import type { CategoryNode, Vehicle } from "@sase/shared";
@@ -46,6 +47,7 @@ function VehicleDetailPage() {
const changeViewMode = (mode: "grid" | "tree" | "columns") => {
setViewMode(mode);
setUserSetting("categoryViewMode", mode);
capture("category_view_changed", { mode, vehicle_id: id });
};
const router = useRouter();
@@ -97,6 +99,7 @@ function VehicleDetailPage() {
year: vehicle.year,
});
}
return () => clearChatwootVehicle();
}, [vehicle, id]);
if (vehicleLoading) {
@@ -211,7 +214,7 @@ function VehicleDetailPage() {
<CarBrandLogo brandName={vehicle.brandName} size={32} className="shrink-0" />
)}
<div className="min-w-0 flex-1">
<h2 className="break-words text-2xl font-bold leading-tight">
<h2 className="break-words font-[family-name:var(--font-display)] text-2xl font-bold leading-tight tracking-tight">
{vehicle?.brandName} {cleanModelName(vehicle?.model)}{" "}
{vehicle?.year && `(${vehicle.year})`}
</h2>
@@ -252,15 +255,29 @@ function VehicleDetailPage() {
{/* Search box keeps its own padding when the columns view zeroes out
CardContent padding. */}
<div className={viewMode === "columns" && !searchActive ? "p-6 pb-0" : undefined}>
<CatalogSearch vehicleId={id} onActiveChange={setSearchActive} />
<CatalogSearch
vehicleId={id}
onActiveChange={(active) => {
setSearchActive(active);
if (active) capture("catalog_search_opened", { vehicle_id: id });
}}
/>
</div>
{!searchActive &&
(categoriesLoading ? (
<div className="space-y-2">
{KEYS_8.map((__k) => (
<Skeleton key={__k} className="h-8 w-full" />
))}
</div>
viewMode === "grid" ? (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
{KEYS_8.map((__k) => (
<Skeleton key={__k} className="h-24 w-full rounded-lg" />
))}
</div>
) : (
<div className="space-y-2">
{KEYS_8.map((__k) => (
<Skeleton key={__k} className="h-8 w-full" />
))}
</div>
)
) : !categoryTree || categoryTree.length === 0 ? (
<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">