feat(web): clean repeated tokens from model names

Catalog sources double up tokens in model names — "Laguna (Laguna)",
"Golf 1,6 GOLF", "Golf TDI Variant GOLF". Add a shared cleanModelName helper
that drops a parenthetical repeating surrounding text and exact case-insensitive
duplicate tokens (order preserved, conservative on ambiguous cases). Applied to
the vehicle page h2, the collapsed summary, and the category breadcrumb. Unit
tested.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 03:55:56 +03:00
parent 0bd9165e66
commit 77947cd162
4 changed files with 68 additions and 11 deletions

View File

@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { cleanModelName } from "../vehicle";
describe("cleanModelName", () => {
it("drops a parenthetical that repeats the name", () => {
expect(cleanModelName("Laguna (Laguna)")).toBe("Laguna");
});
it("drops a trailing duplicate token regardless of case", () => {
expect(cleanModelName("Golf 1,6 GOLF")).toBe("Golf 1,6");
expect(cleanModelName("Golf TDI Variant GOLF")).toBe("Golf TDI Variant");
});
it("keeps non-redundant parentheticals and tokens", () => {
expect(cleanModelName("Laguna (Laguna II)")).toBe("Laguna (Laguna II)");
expect(cleanModelName("Astra Sports Tourer")).toBe("Astra Sports Tourer");
});
it("handles empty / nullish input", () => {
expect(cleanModelName("")).toBe("");
expect(cleanModelName(null)).toBe("");
expect(cleanModelName(undefined)).toBe("");
});
});

View File

@@ -0,0 +1,31 @@
/**
* Collapse repeated tokens that catalog sources leave in model names, e.g.
* "Laguna (Laguna)" → "Laguna", "Golf 1,6 GOLF" → "Golf 1,6". Conservative:
* only drops a parenthetical that repeats surrounding text, and exact
* (case-insensitive) duplicate whitespace tokens — order is preserved.
*/
export function cleanModelName(model?: string | null): string {
if (!model) return "";
let s = model.trim();
// 1) Drop a parenthetical group whose content already appears outside it.
s = s
.replace(/\(([^)]*)\)/g, (full, inner: string) => {
const innerKey = inner.trim().toLocaleLowerCase("tr");
const outside = s.replace(full, " ").toLocaleLowerCase("tr");
return innerKey && outside.includes(innerKey) ? " " : full;
})
.replace(/\s+/g, " ")
.trim();
// 2) Drop exact duplicate tokens (case-insensitive), keeping the first.
const seen = new Set<string>();
const out: string[] = [];
for (const token of s.split(" ")) {
const key = token.toLocaleLowerCase("tr");
if (key && seen.has(key)) continue;
if (key) seen.add(key);
out.push(token);
}
return out.join(" ");
}

View File

@@ -5,6 +5,7 @@ import { CategoryViewToggle } from "@/components/categories/category-view-toggle
import { useCategoryParts } from "@/hooks/use-parts";
import { api } from "@/lib/api-client";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { cleanModelName } from "@/lib/vehicle";
import { Button, Skeleton } from "@sase/ui";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
@@ -78,7 +79,8 @@ function VehicleCategoryPage() {
// Vehicle for breadcrumb root — cached if user arrived from /dashboard/vehicles/$id
const { data: vehicle } = useQuery({
queryKey: ["vehicle", id],
queryFn: () => api.get<{ brandName?: string; model?: string; year?: number }>(`/vehicles/${id}`),
queryFn: () =>
api.get<{ brandName?: string; model?: string; year?: number }>(`/vehicles/${id}`),
enabled: !!id,
});
@@ -122,8 +124,9 @@ function VehicleCategoryPage() {
navigate({ to: "/dashboard/vehicles/$id", params: { id } });
};
const cleanModel = cleanModelName(vehicle?.model);
const vehicleLabel = vehicle?.brandName
? `${vehicle.brandName}${vehicle.model ? ` ${vehicle.model}` : ""}`
? `${vehicle.brandName}${cleanModel ? ` ${cleanModel}` : ""}`
: "Araç";
// Breadcrumb segments excluding the current page (last item rendered as text below)
@@ -181,13 +184,12 @@ function VehicleCategoryPage() {
</Button>
<div>
<h1 className="text-xl font-bold">
{data?.name ?? (
isLoading ? (
{data?.name ??
(isLoading ? (
<span className="inline-block h-6 w-48 animate-pulse rounded-md bg-primary/10 align-middle" />
) : (
"Kategori Detayı"
)
)}
))}
</h1>
{data?.description && (
<p className="text-sm text-muted-foreground">{data.description}</p>
@@ -206,9 +208,7 @@ function VehicleCategoryPage() {
<div>
<p className="font-medium text-destructive">Kategori yüklenemedi</p>
<p className="mt-1 text-muted-foreground">
{error instanceof Error
? error.message
: "Veriler yüklenirken bir hata oluştu."}
{error instanceof Error ? error.message : "Veriler yüklenirken bir hata oluştu."}
</p>
</div>
<Button

View File

@@ -5,6 +5,7 @@ import { CategoryViewToggle } from "@/components/categories/category-view-toggle
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { ApiError, api } from "@/lib/api-client";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { cleanModelName } from "@/lib/vehicle";
import {
Accordion,
AccordionContent,
@@ -174,7 +175,8 @@ function VehicleDetailPage() {
)}
<div>
<h2 className="text-2xl font-bold">
{vehicle?.brandName} {vehicle?.model} {vehicle?.year && `(${vehicle.year})`}
{vehicle?.brandName} {cleanModelName(vehicle?.model)}{" "}
{vehicle?.year && `(${vehicle.year})`}
</h2>
<p className="font-mono text-sm text-muted-foreground">{vehicle?.vin}</p>
</div>
@@ -442,7 +444,7 @@ function getEngineCode(vehicle: any): string | undefined {
/** Compact at-a-glance fields shown on the collapsed Araç Bilgileri header. */
function VehicleSummary({ vehicle }: { vehicle: any }) {
const fields = [
{ label: "Model", value: vehicle?.model },
{ label: "Model", value: cleanModelName(vehicle?.model) },
{ label: "Model yılı", value: vehicle?.year },
{ label: "Motor kodu", value: getEngineCode(vehicle) },
].filter((f) => f.value != null && f.value !== "");