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