feat: VIN textbox WMI marka ikonu + WMI haritasını shared'e taşı/genişlet

- WMI_BRAND_MAP + getBrandFromWmi packages/shared'e taşındı (tek kaynak);
  corgi.service artık buradan import ediyor (davranış aynı, testler geçiyor)
- VinBrandIcon: VIN'in WMI'ı bilinen markaya denk gelince büyüteç yerine
  marka logosu pop animasyonuyla görünür (landing + search VIN textbox)
- WMI haritasına prod DB'de decode edilmiş VIN'lerden 23 eksik WMI eklendi
  (Türkiye fabrikaları NM4/NMT/NLA/NLH/NMB dahil; tüm DB WMI'ları artık tanınıyor)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 23:51:40 +03:00
parent 96171c0726
commit 77de860211
7 changed files with 215 additions and 103 deletions

View File

@@ -0,0 +1,42 @@
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { getBrandFromWmi } from "@sase/shared";
import { cn } from "@sase/ui";
import { Search } from "lucide-react";
interface VinBrandIconProps {
/** Mevcut VIN değeri — ilk 3 karakter (WMI) bilinen bir markaya denk gelirse logo gösterilir. */
vin: string;
size?: number;
/** Konumlandırma için sarmalayıcıya verilen sınıf (örn. absolute left-4 ...). */
className?: string;
}
/**
* VIN textbox'ının solundaki ikon. WMI (ilk 3 karakter) bilinen bir markaya
* denk gelince büyüteç yerine ilgili marka logosu pop animasyonuyla görünür;
* WMI silinince tekrar büyüteç olur.
*/
export function VinBrandIcon({ vin, size = 20, className }: VinBrandIconProps) {
const wmi = vin.trim().toUpperCase().slice(0, 3);
const brand = wmi.length === 3 ? getBrandFromWmi(wmi) : null;
return (
<span className={cn("pointer-events-none flex items-center justify-center", className)}>
{brand ? (
<CarBrandLogo
// key: marka değiştiğinde yeniden mount edip pop animasyonunu tetikler
key={`brand:${brand}`}
brandName={brand}
size={size}
className="animate-icon-pop"
/>
) : (
<Search
key="search"
style={{ width: size, height: size }}
className="animate-icon-pop text-muted-foreground"
/>
)}
</span>
);
}

View File

@@ -168,6 +168,23 @@
}
}
@keyframes icon-pop {
0% {
opacity: 0;
transform: scale(0.4) rotate(-12deg);
filter: blur(2px);
}
60% {
opacity: 1;
transform: scale(1.12) rotate(0);
filter: blur(0);
}
100% {
opacity: 1;
transform: scale(1) rotate(0);
}
}
@keyframes accordion-down {
from {
height: 0;
@@ -201,6 +218,9 @@
.animate-char-in {
animation: char-in 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.animate-icon-pop {
animation: icon-pop 0.32s cubic-bezier(0.16, 1, 0.3, 1);
}
.carousel-track:hover .animate-scroll-left {
animation-play-state: paused;
}

View File

@@ -1,4 +1,5 @@
import { WelcomeOnboardingModal } from "@/components/onboarding/welcome-onboarding-modal";
import { VinBrandIcon } from "@/components/ui/vin-brand-icon";
import { VehicleSelectModal } from "@/components/vehicles/vehicle-select-modal";
import { ApiError, api } from "@/lib/api-client";
import { startAction } from "@/lib/faro";
@@ -500,7 +501,11 @@ function SearchPage() {
<form onSubmit={handleSearch} className="space-y-4">
{/* Input */}
<div className="relative">
<Search className="absolute left-4 top-1/2 size-5 -translate-y-1/2 text-muted-foreground" />
<VinBrandIcon
vin={vin}
size={22}
className="absolute left-4 top-1/2 size-6 -translate-y-1/2"
/>
<Input
ref={inputRef}
placeholder="Şase numarasını girin (17 karakter)"

View File

@@ -1,5 +1,6 @@
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { SmartAnimateText } from "@/components/ui/smart-animate-text";
import { VinBrandIcon } from "@/components/ui/vin-brand-icon";
import { KEYS_17, dynamicKeys } from "@/lib/keys";
import { Button, Input, Separator } from "@sase/ui";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
@@ -840,7 +841,11 @@ export function HomePage() {
{/* VIN Input */}
<div className="mx-auto mt-8 flex max-w-xl flex-col gap-3 sm:flex-row">
<div className="relative flex-1">
<Search className="absolute left-4 top-1/2 size-5 -translate-y-1/2 text-muted-foreground" />
<VinBrandIcon
vin={vin}
size={22}
className="absolute left-4 top-1/2 size-6 -translate-y-1/2"
/>
<Input
value={vin}
onChange={(e) => setVin(e.target.value.toUpperCase())}