feat(web): show vehicle-shaped shimmer while decoding instead of a frozen button
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled

When Şase Çöz is clicked and the decode (reused pre-decode or fresh) is still in
flight, the search form is replaced by a vehicle-page-shaped skeleton with a
sweeping shimmer, so the wait reads as "the page is loading" rather than an
unresponsive button. On success we navigate; candidates/errors fall back to the
form. When the pre-decode already resolved, the skeleton is near-instant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 11:57:12 +03:00
parent e96f311651
commit 125c6fc431

View File

@@ -2,7 +2,7 @@ import { WelcomeOnboardingModal } from "@/components/onboarding/welcome-onboardi
import { VehicleSelectModal } from "@/components/vehicles/vehicle-select-modal";
import { ApiError, api } from "@/lib/api-client";
import { startAction } from "@/lib/faro";
import { KEYS_17 } from "@/lib/keys";
import { KEYS_6, KEYS_17 } from "@/lib/keys";
import { capture } from "@/lib/posthog";
import { toast } from "@/lib/toast";
import { Badge, Button, Input, Separator } from "@sase/ui";
@@ -39,6 +39,50 @@ function sanitizeVin(raw: string): { cleaned: string; corrections: string[] } {
return { cleaned, corrections };
}
/** A muted box with a sweeping light reflection (the shimmer keyframe in
* globals.css), used for the decode loading state. */
function ShimmerBox({ className }: { className?: string }) {
return (
<div className={`relative overflow-hidden rounded-md bg-muted ${className ?? ""}`}>
<div className="absolute inset-0 animate-[shimmer_1.5s_ease-in-out_infinite] bg-gradient-to-r from-transparent via-background/60 to-transparent" />
</div>
);
}
/** Shown the instant "Şase Çöz" is clicked while the decode is still in flight —
* a vehicle-page-shaped skeleton so the wait feels like the page is loading
* rather than an unresponsive button. */
function DecodingSkeleton() {
return (
<div className="space-y-6" aria-busy="true" aria-live="polite">
<span className="sr-only">Araç bilgileri yükleniyor</span>
{/* Header: back + brand logo + 2-line title */}
<div className="flex items-center gap-3">
<ShimmerBox className="h-9 w-9 rounded-md" />
<ShimmerBox className="h-8 w-8 rounded-md" />
<div className="flex flex-col gap-1.5">
<ShimmerBox className="h-6 w-56" />
<ShimmerBox className="h-4 w-40" />
</div>
</div>
{/* Vehicle info card (collapsed) */}
<div className="rounded-xl border border-border px-6 py-4">
<ShimmerBox className="mb-2 h-5 w-32" />
<ShimmerBox className="h-3 w-64" />
</div>
{/* Categories grid */}
<div className="rounded-xl border border-border p-6">
<ShimmerBox className="mb-4 h-5 w-44" />
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{KEYS_6.map((k) => (
<ShimmerBox key={k} className="h-20 w-full rounded-xl" />
))}
</div>
</div>
</div>
);
}
// ─── ROUTE ────────────────────────────────────────────────────────────────────
export const Route = createFileRoute("/dashboard/search")({
@@ -416,6 +460,18 @@ function SearchPage() {
inputRef.current?.focus();
}
// While a decode is in flight (button clicked, result not back yet), swap the
// form for a vehicle-page-shaped shimmer so the click feels like it's loading
// the page — not a frozen button. On resolve we navigate (success), or fall
// back to the form (candidates/error).
if (loading) {
return (
<div className="mx-auto max-w-3xl space-y-8">
<DecodingSkeleton />
</div>
);
}
return (
<div className="mx-auto max-w-3xl space-y-8">
{showWelcome && (