fix(search): left-aligned header, drop the hero card, debounce auto-fix toast

Two unrelated polish items in the VIN search page bundled into one commit
because they touch the same file.

1. Hero card removed. The entire form lived inside a generic
   `rounded-2xl border bg-background p-6 sm:p-8` container on a page that had
   no other content competing for attention — the card added no hierarchy,
   only a frame. The header is also no longer centered: icon sits to the left
   of a left-aligned h2 + subtitle, breaking the AI-default centered hero.

2. I/O/Q auto-correction toast is now debounced. Each keystroke that produced
   a correction fired its own toast, so a user holding the I key or pasting
   "IIO" stacked three toasts on top of each other. Corrections now collect
   in a ref-backed Set and a single consolidated toast fires 400ms after the
   last edit, with cleanup on unmount.

Phase 5/8 of the UX audit follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 20:51:19 +03:00
parent 25300a339a
commit cfe090b45c

View File

@@ -8,7 +8,7 @@ import { Badge, Button, Input, Separator } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { AlertCircle, Car, Clock, Loader2, RotateCcw, Search, Send } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
// ─── HELPERS ──────────────────────────────────────────────────────────────────
@@ -240,6 +240,32 @@ function SearchPage() {
}
}
// ─── Debounced auto-correction toast ──────────────────────────────────────
// Character-by-character typing was firing one toast per keystroke for users
// who held a key or rapidly typed several invalid chars. Accumulate the set
// of corrections and emit a single consolidated toast after a quiet period.
const correctionsRef = useRef<Set<string>>(new Set());
const correctionTimerRef = useRef<number | null>(null);
const flushCorrectionToast = useCallback(() => {
correctionTimerRef.current = null;
if (correctionsRef.current.size === 0) return;
const list = [...correctionsRef.current];
correctionsRef.current = new Set();
toast.info(`Otomatik düzeltildi: ${list.join(", ")}`, {
description: "Şase numarasında I, O, Q harfleri kullanılamaz",
duration: 2500,
});
}, []);
useEffect(() => {
return () => {
if (correctionTimerRef.current !== null) {
window.clearTimeout(correctionTimerRef.current);
}
};
}, []);
function handleVinChange(raw: string) {
const upper = raw.toUpperCase();
const { cleaned, corrections } = sanitizeVin(upper);
@@ -247,11 +273,11 @@ function SearchPage() {
setError(null);
setReportSent(false);
if (corrections.length > 0) {
const unique = [...new Set(corrections)];
toast.info(`Otomatik düzeltildi: ${unique.join(", ")}`, {
description: "Şase numarasında I, O, Q harfleri kullanılamaz",
duration: 2500,
});
for (const c of corrections) correctionsRef.current.add(c);
if (correctionTimerRef.current !== null) {
window.clearTimeout(correctionTimerRef.current);
}
correctionTimerRef.current = window.setTimeout(flushCorrectionToast, 400);
}
}
@@ -306,24 +332,26 @@ function SearchPage() {
}
return (
<div className="mx-auto max-w-3xl space-y-6">
{/* ─── SECTION 1: Hero Input Card ─────────────────────────────────── */}
<div className="rounded-2xl border border-border bg-background p-6 sm:p-8">
{/* Header */}
<div className="flex flex-col items-center text-center">
<div className="inline-flex size-14 items-center justify-center rounded-2xl bg-muted">
<Search className="size-6 text-muted-foreground" />
</div>
<h2 className="mt-4 font-[family-name:var(--font-display)] text-2xl font-bold tracking-tight">
<div className="mx-auto max-w-3xl space-y-8">
{/* ─── SECTION 1: Header + Form (no card) ─────────────────────────── */}
<header className="flex items-start gap-4">
<div
aria-hidden
className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-muted"
>
<Search className="size-5 text-muted-foreground" />
</div>
<div className="min-w-0">
<h2 className="font-[family-name:var(--font-display)] text-3xl font-bold tracking-tight">
Şase Arama
</h2>
<p className="mt-1 text-sm text-muted-foreground">
Araç şase numarasını girerek yedek parça kataloğuna erişin
</p>
</div>
</header>
<Separator className="my-6 bg-border" />
<div>
{/* Form */}
<form onSubmit={handleSearch} className="space-y-4">
{/* Input */}