chore(lint): manual cleanup batch 2/3 (group 6 partial + format sweep)
- new helper: apps/web/src/lib/keys.ts — stable string-key arrays for fixed-length skeleton/decorative lists, plus dynamicKeys() for runtime-sized lists - group 6 (noArrayIndexKey): convert ~25 of 32 Array.from skeleton patterns to KEYS_N.map across 21 files (admin pages, catalog, dashboard, schema viewer, remotion demos) - DashboardDemo HOTSPOTS now carries explicit ids; OnboardingProgress uses step.label as key - biome --write format pass (24 files) Lint count: 218 → 164 (groups 6/7 still in progress: 9 noArrayIndexKey + 121 noExplicitAny + 42 noNonNullAssertion + a few small remaining). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -95,7 +95,9 @@ export function HotspotOverlay({ hotspots, imageWidth, imageHeight }: HotspotOve
|
||||
const isDark = useIsDark();
|
||||
|
||||
return (
|
||||
<svg role="img" aria-label="icon"
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="icon"
|
||||
className="absolute inset-0 h-full w-full"
|
||||
viewBox={`0 0 ${imageWidth} ${imageHeight}`}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { HotspotOverlay } from "./hotspot-overlay";
|
||||
import { PartsPanel } from "./parts-panel";
|
||||
import { SchemaToolbar } from "./schema-toolbar";
|
||||
|
||||
import { KEYS_8 } from "@/lib/keys";
|
||||
interface SchemaViewerProps {
|
||||
schemaPic: SchemaPic | null;
|
||||
hotspots: Hotspot[];
|
||||
@@ -76,8 +77,8 @@ export function SchemaViewer({
|
||||
</div>
|
||||
<div className="w-full space-y-3 p-4 md:w-[40%]">
|
||||
<Skeleton className="h-6 w-1/2" />
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-10 w-full" />
|
||||
{KEYS_8.map((__k) => (
|
||||
<Skeleton key={__k} className="h-10 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
97
apps/web/src/lib/keys.ts
Normal file
97
apps/web/src/lib/keys.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Stable React `key` arrays for fixed-length skeleton/decorative lists.
|
||||
*
|
||||
* Lets us replace `Array.from({ length: N }).map((_, i) => <… key={i} />)`
|
||||
* (which Biome flags via lint/suspicious/noArrayIndexKey) with
|
||||
* `KEYS_N.map((k) => <… key={k} />)` — the keys are module-level constants
|
||||
* so they don't change identity across renders.
|
||||
*
|
||||
* If you need a length not listed here, add a new export — they cost nothing.
|
||||
*/
|
||||
export const KEYS_4 = ["k0", "k1", "k2", "k3"];
|
||||
export const KEYS_5 = ["k0", "k1", "k2", "k3", "k4"];
|
||||
export const KEYS_6 = ["k0", "k1", "k2", "k3", "k4", "k5"];
|
||||
export const KEYS_8 = ["k0", "k1", "k2", "k3", "k4", "k5", "k6", "k7"];
|
||||
export const KEYS_9 = ["k0", "k1", "k2", "k3", "k4", "k5", "k6", "k7", "k8"];
|
||||
export const KEYS_10 = ["k0", "k1", "k2", "k3", "k4", "k5", "k6", "k7", "k8", "k9"];
|
||||
export const KEYS_16 = [
|
||||
"k0",
|
||||
"k1",
|
||||
"k2",
|
||||
"k3",
|
||||
"k4",
|
||||
"k5",
|
||||
"k6",
|
||||
"k7",
|
||||
"k8",
|
||||
"k9",
|
||||
"k10",
|
||||
"k11",
|
||||
"k12",
|
||||
"k13",
|
||||
"k14",
|
||||
"k15",
|
||||
];
|
||||
export const KEYS_17 = [
|
||||
"k0",
|
||||
"k1",
|
||||
"k2",
|
||||
"k3",
|
||||
"k4",
|
||||
"k5",
|
||||
"k6",
|
||||
"k7",
|
||||
"k8",
|
||||
"k9",
|
||||
"k10",
|
||||
"k11",
|
||||
"k12",
|
||||
"k13",
|
||||
"k14",
|
||||
"k15",
|
||||
"k16",
|
||||
];
|
||||
export const KEYS_25 = [
|
||||
"k0",
|
||||
"k1",
|
||||
"k2",
|
||||
"k3",
|
||||
"k4",
|
||||
"k5",
|
||||
"k6",
|
||||
"k7",
|
||||
"k8",
|
||||
"k9",
|
||||
"k10",
|
||||
"k11",
|
||||
"k12",
|
||||
"k13",
|
||||
"k14",
|
||||
"k15",
|
||||
"k16",
|
||||
"k17",
|
||||
"k18",
|
||||
"k19",
|
||||
"k20",
|
||||
"k21",
|
||||
"k22",
|
||||
"k23",
|
||||
"k24",
|
||||
];
|
||||
|
||||
/**
|
||||
* Get a stable key list of arbitrary length. Use this when the count is
|
||||
* dynamic (e.g. derived from props/state). The result is memoized per length.
|
||||
*/
|
||||
const dynamicCache = new Map<number, string[]>();
|
||||
export function dynamicKeys(length: number): string[] {
|
||||
let cached = dynamicCache.get(length);
|
||||
if (!cached) {
|
||||
cached = Array.from({ length }, (_, i) => `k${i}`);
|
||||
dynamicCache.set(length, cached);
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
/** Backwards-compat alias used by a few earlier call sites. */
|
||||
export const BRAND_SKELETON_KEYS = KEYS_8;
|
||||
@@ -92,7 +92,9 @@ const VinInputScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
border: `1px solid ${c.border}`,
|
||||
}}
|
||||
>
|
||||
<svg role="img" aria-label="icon"
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="icon"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -190,7 +192,9 @@ const VehicleInfoScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<svg role="img" aria-label="icon"
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="icon"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -366,10 +370,10 @@ const SchemaViewScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
const c = getColors(isDark);
|
||||
|
||||
const HOTSPOTS = [
|
||||
{ x: "30%", y: "35%" },
|
||||
{ x: "60%", y: "25%" },
|
||||
{ x: "45%", y: "60%" },
|
||||
{ x: "70%", y: "55%" },
|
||||
{ id: "hs-1", x: "30%", y: "35%" },
|
||||
{ id: "hs-2", x: "60%", y: "25%" },
|
||||
{ id: "hs-3", x: "45%", y: "60%" },
|
||||
{ id: "hs-4", x: "70%", y: "55%" },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -468,7 +472,7 @@ const SchemaViewScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
key={spot.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: spot.x,
|
||||
|
||||
@@ -84,7 +84,9 @@ const StorefrontScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
border: `1px solid ${c.border}`,
|
||||
}}
|
||||
>
|
||||
<svg role="img" aria-label="icon"
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="icon"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -109,7 +111,9 @@ const StorefrontScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
</div>
|
||||
{/* Cart icon */}
|
||||
<div style={{ position: "relative" }}>
|
||||
<svg role="img" aria-label="icon"
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -328,7 +332,9 @@ const WidgetIntegrationScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<svg role="img" aria-label="icon"
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="icon"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -390,7 +396,9 @@ const WidgetIntegrationScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
|
||||
{/* Cart icon */}
|
||||
<div style={{ position: "relative", flexShrink: 0 }}>
|
||||
<svg role="img" aria-label="icon"
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -572,7 +580,9 @@ const VinFilterScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<svg role="img" aria-label="icon"
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="icon"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -643,7 +653,9 @@ const VinFilterScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
|
||||
{/* Cart */}
|
||||
<div style={{ position: "relative", flexShrink: 0 }}>
|
||||
<svg role="img" aria-label="icon"
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -906,7 +918,9 @@ const AddToCartScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
|
||||
{/* Cart icon with animated badge */}
|
||||
<div style={{ position: "relative", flexShrink: 0 }}>
|
||||
<svg role="img" aria-label="icon"
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -81,7 +81,7 @@ export const OnboardingProgress: React.FC<{
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
key={label}
|
||||
style={{
|
||||
position: "absolute",
|
||||
opacity: labelOpacity,
|
||||
@@ -152,7 +152,7 @@ export const OnboardingProgress: React.FC<{
|
||||
const circleSize = 28;
|
||||
|
||||
return (
|
||||
<div key={i}>
|
||||
<div key={step.label}>
|
||||
{/* Outer circle */}
|
||||
<div
|
||||
style={{
|
||||
@@ -181,7 +181,9 @@ export const OnboardingProgress: React.FC<{
|
||||
}}
|
||||
/>
|
||||
{/* Checkmark */}
|
||||
<svg role="img" aria-label="icon"
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="icon"
|
||||
viewBox="0 0 24 24"
|
||||
width={14}
|
||||
height={14}
|
||||
|
||||
@@ -67,7 +67,9 @@ function AuthLayout() {
|
||||
1.2sn Sorgu
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full border border-border bg-surface/60 px-3 py-1.5 text-xs text-foreground/80 backdrop-blur-sm">
|
||||
<svg role="img" aria-label="icon"
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="icon"
|
||||
className="size-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { KEYS_5 } from "@/lib/keys";
|
||||
import { capture, resetUser } from "@/lib/posthog";
|
||||
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
|
||||
import { Button, Separator, Skeleton } from "@sase/ui";
|
||||
@@ -164,8 +165,8 @@ function DashboardLayout() {
|
||||
<div className="flex min-h-screen">
|
||||
<div className="hidden w-64 border-r border-border bg-background p-4 lg:block">
|
||||
<Skeleton className="mb-8 h-8 w-32" />
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Skeleton key={`nav-skel-${i}`} className="mb-3 h-10 w-full" />
|
||||
{KEYS_5.map((__k) => (
|
||||
<Skeleton key={__k} className="mb-3 h-10 w-full" />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 p-6">
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { Activity, CheckCircle, ChevronLeft, ChevronRight, Search, X, XCircle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { KEYS_10 } from "@/lib/keys";
|
||||
export const Route = createFileRoute("/dashboard/admin/analytics")({
|
||||
component: AdminAnalyticsPage,
|
||||
});
|
||||
@@ -126,8 +127,8 @@ function AdminAnalyticsPage() {
|
||||
{/* Table */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 10 }).map((_, i) => (
|
||||
<Skeleton key={`log-skeleton-${i}`} className="h-12 w-full" />
|
||||
{KEYS_10.map((__k) => (
|
||||
<Skeleton key={__k} className="h-12 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : !data || data.items.length === 0 ? (
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { ChevronLeft, ChevronRight, Copy, Search, TrendingUp, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { KEYS_10 } from "@/lib/keys";
|
||||
export const Route = createFileRoute("/dashboard/admin/copy-logs")({
|
||||
component: AdminCopyLogsPage,
|
||||
});
|
||||
@@ -152,8 +153,8 @@ function AdminCopyLogsPage() {
|
||||
{/* Table */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 10 }).map((_, i) => (
|
||||
<Skeleton key={`copy-skel-${i}`} className="h-12 w-full" />
|
||||
{KEYS_10.map((__k) => (
|
||||
<Skeleton key={__k} className="h-12 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : !data || data.items.length === 0 ? (
|
||||
@@ -250,8 +251,8 @@ function AdminCopyLogsPage() {
|
||||
{tab === "top" &&
|
||||
(topLoading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 10 }).map((_, i) => (
|
||||
<Skeleton key={`top-skel-${i}`} className="h-12 w-full" />
|
||||
{KEYS_10.map((__k) => (
|
||||
<Skeleton key={__k} className="h-12 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : !topCodes || topCodes.length === 0 ? (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { KEYS_6 } from "@/lib/keys";
|
||||
import { Card, CardContent } from "@sase/ui";
|
||||
import { Skeleton } from "@sase/ui";
|
||||
import { Badge } from "@sase/ui";
|
||||
@@ -75,8 +76,8 @@ function AdminDashboardPage() {
|
||||
<div className="space-y-6">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={`skeleton-${i}`} className="h-32" />
|
||||
{KEYS_6.map((__k) => (
|
||||
<Skeleton key={__k} className="h-32" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,8 +189,8 @@ function AdminDashboardPage() {
|
||||
{/* Stat Cards */}
|
||||
{statsLoading ? (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={`stat-skeleton-${i}`} className="h-32" />
|
||||
{KEYS_6.map((__k) => (
|
||||
<Skeleton key={__k} className="h-32" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -10,6 +10,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { AlertTriangle, CheckCircle, ExternalLink, Receipt, XCircle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { KEYS_5 } from "@/lib/keys";
|
||||
export const Route = createFileRoute("/dashboard/admin/payments")({
|
||||
component: AdminPaymentsPage,
|
||||
});
|
||||
@@ -106,8 +107,8 @@ function AdminPaymentsPage() {
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Skeleton key={`payment-skeleton-${i}`} className="h-32 w-full" />
|
||||
{KEYS_5.map((__k) => (
|
||||
<Skeleton key={__k} className="h-32 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : !payments || payments.length === 0 ? (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { KEYS_6 } from "@/lib/keys";
|
||||
import { Card, CardContent } from "@sase/ui";
|
||||
import { Button } from "@sase/ui";
|
||||
import { Input } from "@sase/ui";
|
||||
@@ -180,8 +181,8 @@ function AdminReferralsPage() {
|
||||
{/* Referrers List */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={`ref-skeleton-${i}`} className="h-20 w-full" />
|
||||
{KEYS_6.map((__k) => (
|
||||
<Skeleton key={__k} className="h-20 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : !data || data.items.length === 0 ? (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { KEYS_8 } from "@/lib/keys";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@sase/ui";
|
||||
import { Button } from "@sase/ui";
|
||||
@@ -431,8 +432,8 @@ function AdminUsersPage() {
|
||||
{/* Table */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={`row-skeleton-${i}`} className="h-14 w-full" />
|
||||
{KEYS_8.map((__k) => (
|
||||
<Skeleton key={__k} className="h-14 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : !data || data.items.length === 0 ? (
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { ChevronRight, Columns2, LayoutGrid, Library, List, Lock } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { KEYS_10 } from "@/lib/keys";
|
||||
export const Route = createFileRoute("/dashboard/catalog/")({
|
||||
component: CatalogBrandsPage,
|
||||
});
|
||||
@@ -77,8 +78,8 @@ function CatalogBrandsPage() {
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{Array.from({ length: 10 }).map((_, i) => (
|
||||
<Skeleton key={`brand-skel-${i}`} className="h-28 w-full rounded-xl" />
|
||||
{KEYS_10.map((__k) => (
|
||||
<Skeleton key={__k} className="h-28 w-full rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
) : !brands || brands.length === 0 ? (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ModelListColumns } from "@/components/catalog/model-list-columns";
|
||||
import { ModelListTree } from "@/components/catalog/model-list-tree";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { KEYS_4, KEYS_9 } from "@/lib/keys";
|
||||
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
|
||||
import { Button, Skeleton, cn } from "@sase/ui";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -129,8 +130,8 @@ function CatalogModelsPage() {
|
||||
|
||||
{catalogsLoading ? (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={`cat-skel-${i}`} className="h-24 w-full rounded-xl" />
|
||||
{KEYS_4.map((__k) => (
|
||||
<Skeleton key={__k} className="h-24 w-full rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
) : isMultiCatalog && !activeCatalog ? (
|
||||
@@ -143,8 +144,8 @@ function CatalogModelsPage() {
|
||||
{t("catalog.loadingModels")}
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 9 }).map((_, i) => (
|
||||
<Skeleton key={`model-skel-${i}`} className="h-24 w-full rounded-lg" />
|
||||
{KEYS_9.map((__k) => (
|
||||
<Skeleton key={__k} className="h-24 w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
|
||||
import { Suspense, lazy, useState } from "react";
|
||||
|
||||
import { KEYS_8 } from "@/lib/keys";
|
||||
const SchemaViewer = lazy(() =>
|
||||
import("@/components/schema/schema-viewer").then((mod) => ({
|
||||
default: mod.SchemaViewer,
|
||||
@@ -24,8 +25,8 @@ function SchemaViewerFallback() {
|
||||
</div>
|
||||
<div className="w-full space-y-3 p-4 md:w-[40%]">
|
||||
<Skeleton className="h-6 w-1/2" />
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={`schema-skel-${i}`} className="h-10 w-full" />
|
||||
{KEYS_8.map((__k) => (
|
||||
<Skeleton key={__k} className="h-10 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { KEYS_8 } from "@/lib/keys";
|
||||
export const Route = createFileRoute("/dashboard/catalog_/$brandName_/$modelId/")({
|
||||
validateSearch: (search) => ({
|
||||
body: typeof search.body === "string" ? search.body : undefined,
|
||||
@@ -277,8 +278,8 @@ function CatalogVehiclePage() {
|
||||
>
|
||||
{categoriesLoading ? (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={`cat-skel-${i}`} className="h-8 w-full" />
|
||||
{KEYS_8.map((__k) => (
|
||||
<Skeleton key={__k} className="h-8 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : viewMode === "grid" ? (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { KEYS_4 } from "@/lib/keys";
|
||||
import { Badge, Button, Separator, Skeleton } from "@sase/ui";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
||||
@@ -231,8 +232,8 @@ function DashboardHome() {
|
||||
{/* ─── STAT CARDS ─────────────────────────────────────────────── */}
|
||||
{isLoadingCards ? (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={`card-skel-${i}`} className="h-64 w-full rounded-2xl" />
|
||||
{KEYS_4.map((__k) => (
|
||||
<Skeleton key={__k} className="h-64 w-full rounded-2xl" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CarBrandLogo } from "@/components/ui/car-brand-logo";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { startAction } from "@/lib/faro";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { BRAND_SKELETON_KEYS } from "@/lib/keys";
|
||||
import { capture, setPeopleProperties } from "@/lib/posthog";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { getUserSettings } from "@/lib/user-settings";
|
||||
@@ -50,8 +51,8 @@ const LazyOnboardingProgress = lazy(() =>
|
||||
function BrandSelectorFallback() {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={`brand-skel-${i}`} className="h-24 w-full rounded-lg" />
|
||||
{BRAND_SKELETON_KEYS.map((k) => (
|
||||
<Skeleton key={k} className="h-24 w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
|
||||
import { Suspense, lazy, useState } from "react";
|
||||
|
||||
import { KEYS_6, KEYS_8 } from "@/lib/keys";
|
||||
const SchemaViewer = lazy(() =>
|
||||
import("@/components/schema/schema-viewer").then((mod) => ({
|
||||
default: mod.SchemaViewer,
|
||||
@@ -22,8 +23,8 @@ function SchemaViewerFallback() {
|
||||
</div>
|
||||
<div className="w-full space-y-3 p-4 md:w-[40%]">
|
||||
<Skeleton className="h-6 w-1/2" />
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={`schema-skel-${i}`} className="h-10 w-full" />
|
||||
{KEYS_8.map((__k) => (
|
||||
<Skeleton key={__k} className="h-10 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,8 +34,8 @@ function SchemaViewerFallback() {
|
||||
function CategoryGridFallback() {
|
||||
return (
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={`cat-grid-skel-${i}`} className="h-24 w-full rounded-lg" />
|
||||
{KEYS_6.map((__k) => (
|
||||
<Skeleton key={__k} className="h-24 w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ArrowLeft, Columns2, LayoutGrid, List } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { KEYS_8 } from "@/lib/keys";
|
||||
export const Route = createFileRoute("/dashboard/vehicles_/$id/")({
|
||||
component: VehicleDetailPage,
|
||||
});
|
||||
@@ -126,8 +127,8 @@ function VehicleDetailPage() {
|
||||
>
|
||||
{categoriesLoading ? (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={`cat-skel-${i}`} className="h-8 w-full" />
|
||||
{KEYS_8.map((__k) => (
|
||||
<Skeleton key={__k} className="h-8 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : viewMode === "grid" ? (
|
||||
|
||||
@@ -117,7 +117,8 @@ function DemoPage() {
|
||||
Sase.tr
|
||||
</Link>
|
||||
<div className="flex items-center gap-3">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
className="inline-flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
aria-label="Tema değiştir"
|
||||
@@ -144,7 +145,8 @@ function DemoPage() {
|
||||
<main id="main-content" className="mx-auto max-w-5xl px-4 py-12 sm:px-6">
|
||||
{/* Step indicator */}
|
||||
<div className="mb-8 flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setStep("vin");
|
||||
setSelectedCategory(null);
|
||||
@@ -154,7 +156,8 @@ function DemoPage() {
|
||||
1. VIN Girin
|
||||
</button>
|
||||
<div className="h-px w-6 bg-border" />
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => vinPreview && setStep("categories")}
|
||||
className={`rounded-full px-3 py-1 transition ${step === "categories" ? "bg-foreground text-background" : "bg-muted"} ${!vinPreview ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||
disabled={!vinPreview}
|
||||
@@ -162,7 +165,8 @@ function DemoPage() {
|
||||
2. Kategori Seçin
|
||||
</button>
|
||||
<div className="h-px w-6 bg-border" />
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectedCategory && setStep("schema")}
|
||||
className={`rounded-full px-3 py-1 transition ${step === "schema" ? "bg-foreground text-background" : "bg-muted"} ${!selectedCategory ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||
disabled={!selectedCategory}
|
||||
@@ -244,7 +248,8 @@ function DemoPage() {
|
||||
)}
|
||||
|
||||
{!vin && (
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVin("WVWZZZ1JZ3W597935")}
|
||||
className="mx-auto block text-sm text-muted-foreground underline underline-offset-4 transition hover:text-foreground"
|
||||
>
|
||||
@@ -268,7 +273,8 @@ function DemoPage() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setStep("vin");
|
||||
setSelectedCategory(null);
|
||||
@@ -281,7 +287,8 @@ function DemoPage() {
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{EXAMPLE_CATEGORIES.map((cat) => (
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
key={cat.name}
|
||||
onClick={() => {
|
||||
setSelectedCategory(cat.name);
|
||||
@@ -319,7 +326,8 @@ function DemoPage() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep("categories")}
|
||||
className="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
||||
>
|
||||
|
||||
@@ -572,7 +572,8 @@ function HomePage() {
|
||||
</nav>
|
||||
|
||||
<div className="hidden items-center gap-3 md:flex">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
className="inline-flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
aria-label="Tema değiştir"
|
||||
@@ -610,7 +611,8 @@ function HomePage() {
|
||||
|
||||
{/* Mobile toggle */}
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
className="inline-flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
aria-label="Tema değiştir"
|
||||
@@ -627,7 +629,7 @@ function HomePage() {
|
||||
{mobileMenuOpen && (
|
||||
<div className="border-t border-border px-4 py-4 md:hidden">
|
||||
<nav className="flex flex-col gap-3 text-sm text-muted-foreground">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
className="text-left transition hover:text-foreground"
|
||||
onClick={() => {
|
||||
@@ -637,7 +639,7 @@ function HomePage() {
|
||||
>
|
||||
Özellikler
|
||||
</button>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
className="text-left transition hover:text-foreground"
|
||||
onClick={() => {
|
||||
@@ -828,7 +830,8 @@ function HomePage() {
|
||||
{/* Example VIN invite */}
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
Şase numaranız yok mu?{" "}
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
onClick={fillExampleVin}
|
||||
data-faro-user-action-name="hero-free-trial"
|
||||
className="text-foreground underline underline-offset-4 transition hover:text-foreground/80"
|
||||
@@ -1313,7 +1316,9 @@ function HomePage() {
|
||||
>
|
||||
<div className="flex gap-0.5">
|
||||
{Array.from({ length: t.rating }).map((_, i) => (
|
||||
<svg role="img" aria-label="icon"
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="icon"
|
||||
key={i}
|
||||
viewBox="0 0 20 20"
|
||||
className="size-3.5 fill-foreground/85"
|
||||
|
||||
Reference in New Issue
Block a user