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:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user