feat(web): add error/not-found state + safe back nav to vehicle page
The vehicle detail page had no error handling: a failed /vehicles/:id (404 or
network) rendered a broken header (" ()") with no retry, while only the
categories sub-page had a proper error block. Now a failed load shows a
not-found vs. generic-error alert with retry (mirroring the category page), and
404s skip retries so the state appears immediately. The back button now uses
the router history and falls back to /dashboard/search on deep links instead of
window.history.back() leaving the site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,13 +3,13 @@ import { CategoryGrid } from "@/components/categories/category-grid";
|
||||
import { CategoryTree } from "@/components/categories/category-tree";
|
||||
import { CategoryViewToggle } from "@/components/categories/category-view-toggle";
|
||||
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { ApiError, api } from "@/lib/api-client";
|
||||
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@sase/ui";
|
||||
import { Skeleton } from "@sase/ui";
|
||||
import { Button } from "@sase/ui";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute, useCanGoBack, useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -29,10 +29,31 @@ function VehicleDetailPage() {
|
||||
setUserSetting("categoryViewMode", mode);
|
||||
};
|
||||
|
||||
const { data: vehicle, isLoading: vehicleLoading } = useQuery({
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Go back within the app; if the page was opened from a deep link (no app
|
||||
// history), fall back to search instead of leaving the site.
|
||||
const handleBack = () => {
|
||||
if (canGoBack) router.history.back();
|
||||
else navigate({ to: "/dashboard/search" });
|
||||
};
|
||||
|
||||
const {
|
||||
data: vehicle,
|
||||
isLoading: vehicleLoading,
|
||||
isError: vehicleError,
|
||||
error: vehicleErrorObj,
|
||||
refetch: refetchVehicle,
|
||||
isFetching: vehicleFetching,
|
||||
} = useQuery({
|
||||
queryKey: ["vehicle", id],
|
||||
queryFn: () => api.get<any>(`/vehicles/${id}`),
|
||||
enabled: !!id,
|
||||
// A missing vehicle (404) is final — don't burn retries before showing the
|
||||
// not-found state. Transient errors still retry.
|
||||
retry: (count, err) => !(err instanceof ApiError && err.status === 404) && count < 2,
|
||||
});
|
||||
|
||||
const { data: categoryTree, isLoading: categoriesLoading } = useQuery({
|
||||
@@ -78,6 +99,60 @@ function VehicleDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (vehicleError) {
|
||||
const is404 = vehicleErrorObj instanceof ApiError && vehicleErrorObj.status === 404;
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleBack}
|
||||
title="Geri dön"
|
||||
data-faro-user-action-name="vehicle-back"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h2 className="text-xl font-bold">{is404 ? "Araç bulunamadı" : "Araç yüklenemedi"}</h2>
|
||||
</div>
|
||||
<div
|
||||
role="alert"
|
||||
className="flex flex-col items-start gap-3 rounded-lg border border-destructive/40 bg-destructive/5 p-5 text-sm"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-destructive">
|
||||
{is404 ? "Bu araç kaydı bulunamadı" : "Araç bilgileri yüklenemedi"}
|
||||
</p>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{is404
|
||||
? "Bağlantı geçersiz olabilir ya da kayıt kaldırılmış olabilir."
|
||||
: vehicleErrorObj instanceof Error
|
||||
? vehicleErrorObj.message
|
||||
: "Veriler yüklenirken bir hata oluştu."}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{!is404 && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => refetchVehicle()}
|
||||
disabled={vehicleFetching}
|
||||
data-faro-user-action-name="vehicle-retry"
|
||||
>
|
||||
{vehicleFetching ? "Yükleniyor…" : "Tekrar dene"}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" size="sm" variant="ghost" onClick={handleBack}>
|
||||
Geri dön
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@@ -85,7 +160,7 @@ function VehicleDetailPage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => window.history.back()}
|
||||
onClick={handleBack}
|
||||
title="Geri dön"
|
||||
data-faro-user-action-name="vehicle-back"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user