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:
@@ -1,4 +1,5 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { WMI_BRAND_MAP } from "@sase/shared";
|
||||
|
||||
interface CorgiDecodeResult {
|
||||
brandName: string;
|
||||
@@ -7,105 +8,6 @@ interface CorgiDecodeResult {
|
||||
isKnown: boolean;
|
||||
}
|
||||
|
||||
const WMI_DATABASE: Record<string, string> = {
|
||||
// BMW
|
||||
WBA: "BMW",
|
||||
WBS: "BMW",
|
||||
WBY: "BMW",
|
||||
"5UX": "BMW",
|
||||
// Mercedes-Benz
|
||||
WDB: "Mercedes-Benz",
|
||||
WDC: "Mercedes-Benz",
|
||||
WDD: "Mercedes-Benz",
|
||||
WDF: "Mercedes-Benz",
|
||||
// Audi
|
||||
WAU: "Audi",
|
||||
WUA: "Audi",
|
||||
// Volkswagen
|
||||
WVW: "Volkswagen",
|
||||
WVG: "Volkswagen",
|
||||
"3VW": "Volkswagen",
|
||||
WV1: "Volkswagen",
|
||||
WV2: "Volkswagen",
|
||||
WV3: "Volkswagen",
|
||||
// Toyota
|
||||
JTD: "Toyota",
|
||||
JTE: "Toyota",
|
||||
JTN: "Toyota",
|
||||
"2T1": "Toyota",
|
||||
"4T1": "Toyota",
|
||||
// Fiat
|
||||
ZFA: "Fiat",
|
||||
ZFC: "Fiat",
|
||||
// Renault
|
||||
VF1: "Renault",
|
||||
VF2: "Renault",
|
||||
// Peugeot
|
||||
VF3: "Peugeot",
|
||||
VR3: "Peugeot", // newer Stellantis-era WMI
|
||||
VR7: "Peugeot", // newer Stellantis-era WMI
|
||||
// Citroen
|
||||
VF7: "Citroen",
|
||||
VR1: "Citroen", // newer Citroën/DS WMI
|
||||
// Honda
|
||||
JHM: "Honda",
|
||||
SHH: "Honda",
|
||||
"1HG": "Honda",
|
||||
// Hyundai
|
||||
KMH: "Hyundai",
|
||||
"5NP": "Hyundai",
|
||||
// Kia
|
||||
KNA: "Kia",
|
||||
KND: "Kia",
|
||||
// Ford
|
||||
WF0: "Ford",
|
||||
NM0: "Ford",
|
||||
"1FA": "Ford",
|
||||
"3FA": "Ford",
|
||||
// Opel
|
||||
W0L: "Opel",
|
||||
W0V: "Opel", // newer Stellantis-era WMI
|
||||
// Skoda
|
||||
TMB: "Skoda",
|
||||
// Seat
|
||||
VSS: "Seat",
|
||||
// Volvo
|
||||
YV1: "Volvo",
|
||||
// Nissan
|
||||
JN1: "Nissan",
|
||||
"1N4": "Nissan",
|
||||
"3N1": "Nissan",
|
||||
// Mazda
|
||||
JMZ: "Mazda",
|
||||
JM1: "Mazda",
|
||||
JM3: "Mazda",
|
||||
// Porsche
|
||||
WP0: "Porsche",
|
||||
WP1: "Porsche",
|
||||
// Land Rover
|
||||
SAL: "Land Rover",
|
||||
// Jaguar
|
||||
SAJ: "Jaguar",
|
||||
// Mini
|
||||
WMW: "Mini",
|
||||
// Dacia
|
||||
UU1: "Dacia",
|
||||
// Subaru
|
||||
JF1: "Subaru",
|
||||
JF2: "Subaru",
|
||||
// Suzuki
|
||||
JS2: "Suzuki",
|
||||
JS3: "Suzuki",
|
||||
TSM: "Suzuki",
|
||||
MA3: "Suzuki",
|
||||
MBH: "Suzuki",
|
||||
// Mitsubishi
|
||||
JMB: "Mitsubishi",
|
||||
JMY: "Mitsubishi",
|
||||
MMB: "Mitsubishi",
|
||||
ML3: "Mitsubishi",
|
||||
};
|
||||
|
||||
const YEAR_MAP: Record<string, number> = {
|
||||
A: 2010,
|
||||
B: 2011,
|
||||
@@ -148,7 +50,7 @@ export class CorgiService {
|
||||
if (upper.length !== 17) return null;
|
||||
|
||||
const wmi = upper.substring(0, 3);
|
||||
const brandName = WMI_DATABASE[wmi];
|
||||
const brandName = WMI_BRAND_MAP[wmi];
|
||||
|
||||
if (!brandName) {
|
||||
this.logger.warn(`Unknown WMI: ${wmi}`);
|
||||
@@ -169,6 +71,6 @@ export class CorgiService {
|
||||
}
|
||||
|
||||
getBrandFromWmi(wmi: string): string | null {
|
||||
return WMI_DATABASE[wmi.toUpperCase()] ?? null;
|
||||
return WMI_BRAND_MAP[wmi.toUpperCase()] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
42
apps/web/src/components/ui/vin-brand-icon.tsx
Normal file
42
apps/web/src/components/ui/vin-brand-icon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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())}
|
||||
|
||||
@@ -61,6 +61,8 @@ export {
|
||||
validateVinCheckDigit,
|
||||
extractWmi,
|
||||
extractModelYear,
|
||||
getBrandFromWmi,
|
||||
WMI_BRAND_MAP,
|
||||
} from "./utils/vin-validator.js";
|
||||
export { formatTRY, kurusToLira, liraToKurus } from "./utils/currency.js";
|
||||
export {
|
||||
|
||||
@@ -55,6 +55,142 @@ export function extractWmi(vin: string): string {
|
||||
return vin.toUpperCase().slice(0, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* World Manufacturer Identifier (VIN'in ilk 3 karakteri) → marka adı.
|
||||
* Marka adlarının çoğu @sase/ui CarBrandLogo eşlemesiyle uyumludur
|
||||
* (Chevrolet/Lancia gibi birkaçında logo yok, harf fallback'i gösterilir).
|
||||
*/
|
||||
export const WMI_BRAND_MAP: Record<string, string> = {
|
||||
// BMW
|
||||
WBA: "BMW",
|
||||
WBS: "BMW",
|
||||
WBY: "BMW",
|
||||
"5UX": "BMW",
|
||||
// Mercedes-Benz
|
||||
WDB: "Mercedes-Benz",
|
||||
WDC: "Mercedes-Benz",
|
||||
WDD: "Mercedes-Benz",
|
||||
WDF: "Mercedes-Benz",
|
||||
// Audi
|
||||
WAU: "Audi",
|
||||
WUA: "Audi",
|
||||
// Volkswagen
|
||||
WVW: "Volkswagen",
|
||||
WVG: "Volkswagen",
|
||||
"3VW": "Volkswagen",
|
||||
WV1: "Volkswagen",
|
||||
WV2: "Volkswagen",
|
||||
WV3: "Volkswagen",
|
||||
// Toyota
|
||||
JTD: "Toyota",
|
||||
JTE: "Toyota",
|
||||
JTN: "Toyota",
|
||||
"2T1": "Toyota",
|
||||
"4T1": "Toyota",
|
||||
// Fiat
|
||||
ZFA: "Fiat",
|
||||
ZFC: "Fiat",
|
||||
// Renault
|
||||
VF1: "Renault",
|
||||
VF2: "Renault",
|
||||
// Peugeot
|
||||
VF3: "Peugeot",
|
||||
VR3: "Peugeot", // newer Stellantis-era WMI
|
||||
VR7: "Peugeot", // newer Stellantis-era WMI
|
||||
// Citroen
|
||||
VF7: "Citroen",
|
||||
VR1: "Citroen", // newer Citroën/DS WMI
|
||||
// Honda
|
||||
JHM: "Honda",
|
||||
SHH: "Honda",
|
||||
"1HG": "Honda",
|
||||
// Hyundai
|
||||
KMH: "Hyundai",
|
||||
"5NP": "Hyundai",
|
||||
// Kia
|
||||
KNA: "Kia",
|
||||
KND: "Kia",
|
||||
// Ford
|
||||
WF0: "Ford",
|
||||
NM0: "Ford",
|
||||
"1FA": "Ford",
|
||||
"3FA": "Ford",
|
||||
// Opel
|
||||
W0L: "Opel",
|
||||
W0V: "Opel", // newer Stellantis-era WMI
|
||||
// Skoda
|
||||
TMB: "Skoda",
|
||||
// Seat
|
||||
VSS: "Seat",
|
||||
// Volvo
|
||||
YV1: "Volvo",
|
||||
// Nissan
|
||||
JN1: "Nissan",
|
||||
"1N4": "Nissan",
|
||||
"3N1": "Nissan",
|
||||
// Mazda
|
||||
JMZ: "Mazda",
|
||||
JM1: "Mazda",
|
||||
JM3: "Mazda",
|
||||
// Porsche
|
||||
WP0: "Porsche",
|
||||
WP1: "Porsche",
|
||||
// Land Rover
|
||||
SAL: "Land Rover",
|
||||
// Jaguar
|
||||
SAJ: "Jaguar",
|
||||
// Mini
|
||||
WMW: "Mini",
|
||||
// Dacia
|
||||
UU1: "Dacia",
|
||||
// Subaru
|
||||
JF1: "Subaru",
|
||||
JF2: "Subaru",
|
||||
// Suzuki
|
||||
JS2: "Suzuki",
|
||||
JS3: "Suzuki",
|
||||
TSM: "Suzuki",
|
||||
MA3: "Suzuki",
|
||||
MBH: "Suzuki",
|
||||
// Mitsubishi
|
||||
JMB: "Mitsubishi",
|
||||
JMY: "Mitsubishi",
|
||||
MMB: "Mitsubishi",
|
||||
ML3: "Mitsubishi",
|
||||
|
||||
// ── Prod DB'de decode edilmiş VIN'lerden eklenen WMI'lar (2026-05-25) ──
|
||||
// Türkiye / ek pazar fabrikaları dahil. Marka = katalog decode sonucu.
|
||||
NM4: "Fiat", // Tofaş — Türkiye (Egea/Doblo/Fiorino)
|
||||
NMT: "Toyota", // Toyota — Sakarya, Türkiye (Corolla/C-HR/Yaris)
|
||||
NLA: "Honda", // Honda — Gebze, Türkiye (Civic)
|
||||
NLH: "Hyundai", // Hyundai Assan — İzmit, Türkiye (i10/i20)
|
||||
NLJ: "Hyundai", // Hyundai — H-1 / Grand Starex
|
||||
NMB: "Mercedes-Benz", // Mercedes-Benz — Türkiye
|
||||
TMA: "Hyundai", // Hyundai — Çek Cumhuriyeti (Nošovice)
|
||||
XW8: "Skoda", // VW Group — Skoda
|
||||
U5Y: "Kia", // Kia — Slovakya (Žilina)
|
||||
KNE: "Kia", // Kia (Spectra/Sephia/Shuma/Mentor)
|
||||
KL1: "Chevrolet", // GM Korea — Chevrolet/Daewoo
|
||||
SB1: "Toyota", // Toyota — Avrupa (Avensis/Auris)
|
||||
SJN: "Nissan", // Nissan — İngiltere (Sunderland)
|
||||
MDH: "Nissan", // Nissan — Hindistan (Micra K13)
|
||||
MR0: "Toyota", // Toyota — Tayland (Hilux)
|
||||
MAK: "Honda", // Honda — Hindistan (City)
|
||||
XMC: "Mitsubishi", // Mitsubishi — Tayland
|
||||
JSA: "Suzuki", // Suzuki (Swift)
|
||||
"2T2": "Lexus", // Toyota/Lexus — Kanada (RX)
|
||||
ZAR: "Alfa Romeo", // Alfa Romeo (145/146)
|
||||
ZLA: "Lancia", // Lancia (Kappa) — CarBrandLogo'da logo yok
|
||||
WMA: "MAN", // MAN — ticari araç/kamyon
|
||||
};
|
||||
|
||||
/**
|
||||
* WMI'dan marka adını döndürür. Bilinmeyen WMI için null.
|
||||
*/
|
||||
export function getBrandFromWmi(wmi: string): string | null {
|
||||
return WMI_BRAND_MAP[wmi.toUpperCase()] ?? null;
|
||||
}
|
||||
|
||||
export function extractModelYear(vin: string): number | null {
|
||||
const yearChar = vin.toUpperCase()[9];
|
||||
const yearMap: Record<string, number> = {
|
||||
|
||||
Reference in New Issue
Block a user