chore(lint): manual cleanup batch 1/3 (groups 1-5)
- group 1 (catalog.service): rename categoryId→categoryIdInput param + local categoryId; init pl24Categories with Awaited type; fix img alt + scroll buttons - group 2 (useButtonType): add type=button to 12 buttons (demo.tsx, index.tsx) - group 3 (noSvgWithoutTitle): add role+aria-label to 13 decorative svgs - group 4 (noAssignInExpressions): convert 13 while((m=regex.exec())) → for...of matchAll (pl24-ford-legacy + emex.service) - group 5 (useExhaustiveDependencies): correct deps in __root user identification, subscription onboarding, model-list-columns; biome-ignore for legitimate single-trigger effects Lint count: 769 → 218 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,7 @@ export function ModelListColumns({
|
||||
const [columns, setColumns] = useState<Column[]>([{ type: "models", items: modelItems }]);
|
||||
|
||||
// Reset when models change
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: modelItems is derived from models; gating on models prevents an extra effect when only modelItems identity changes
|
||||
useEffect(() => {
|
||||
setColumns([{ type: "models", items: modelItems }]);
|
||||
selectedVehicleRef.current = null;
|
||||
@@ -72,6 +73,7 @@ export function ModelListColumns({
|
||||
}, [modelItems]);
|
||||
|
||||
// Auto-scroll right when new column added
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: depending on columns.length is intentional — we only scroll when a column is added/removed, not on inner-item updates
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;
|
||||
|
||||
@@ -47,6 +47,7 @@ export function CategoryColumns({
|
||||
}, [categories]);
|
||||
|
||||
// Auto-scroll right when new column added
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: depend only on columns.length so we don't scroll on inner-item updates
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;
|
||||
|
||||
@@ -107,6 +107,7 @@ function CategoryNode({
|
||||
}, [expanded, fetched, category.id, queryClient]);
|
||||
|
||||
// Prefetch schema images for leaf children in batches of 2 when expanded
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: prefetch should fire only on expand toggle, not when children mutate during prefetch
|
||||
useEffect(() => {
|
||||
if (!expanded || prefetchedRef.current) return;
|
||||
const leafs = children.filter(
|
||||
@@ -151,7 +152,6 @@ function CategoryNode({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [expanded, vehicleId]);
|
||||
|
||||
const Icon = getCategoryIcon(category.name);
|
||||
|
||||
@@ -95,7 +95,7 @@ export function HotspotOverlay({ hotspots, imageWidth, imageHeight }: HotspotOve
|
||||
const isDark = useIsDark();
|
||||
|
||||
return (
|
||||
<svg
|
||||
<svg role="img" aria-label="icon"
|
||||
className="absolute inset-0 h-full w-full"
|
||||
viewBox={`0 0 ${imageWidth} ${imageHeight}`}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
|
||||
@@ -123,9 +123,9 @@ export function SchemaViewer({
|
||||
<div className="relative">
|
||||
<img
|
||||
src={schemaPic.url}
|
||||
alt={schemaPic.label || "Sema goruntusu"}
|
||||
{...(schemaPic.width > 0 && { width: schemaPic.width })}
|
||||
{...(schemaPic.height > 0 && { height: schemaPic.height })}
|
||||
alt={schemaPic.label || "Şema görüntüsü"}
|
||||
width={schemaPic.width > 0 ? schemaPic.width : undefined}
|
||||
height={schemaPic.height > 0 ? schemaPic.height : undefined}
|
||||
className="max-h-full max-w-full select-none object-contain"
|
||||
draggable={false}
|
||||
/>
|
||||
|
||||
@@ -92,7 +92,7 @@ const VinInputScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
border: `1px solid ${c.border}`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
<svg role="img" aria-label="icon"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -190,7 +190,7 @@ const VehicleInfoScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
<svg role="img" aria-label="icon"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -84,7 +84,7 @@ const StorefrontScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
border: `1px solid ${c.border}`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
<svg role="img" aria-label="icon"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -109,7 +109,7 @@ const StorefrontScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
</div>
|
||||
{/* Cart icon */}
|
||||
<div style={{ position: "relative" }}>
|
||||
<svg
|
||||
<svg role="img" aria-label="icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -328,7 +328,7 @@ const WidgetIntegrationScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
<svg role="img" aria-label="icon"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -390,7 +390,7 @@ const WidgetIntegrationScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
|
||||
{/* Cart icon */}
|
||||
<div style={{ position: "relative", flexShrink: 0 }}>
|
||||
<svg
|
||||
<svg role="img" aria-label="icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -572,7 +572,7 @@ const VinFilterScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
<svg role="img" aria-label="icon"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -643,7 +643,7 @@ const VinFilterScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
|
||||
{/* Cart */}
|
||||
<div style={{ position: "relative", flexShrink: 0 }}>
|
||||
<svg
|
||||
<svg role="img" aria-label="icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -906,7 +906,7 @@ const AddToCartScene: React.FC<{ isDark: boolean }> = ({ isDark }) => {
|
||||
|
||||
{/* Cart icon with animated badge */}
|
||||
<div style={{ position: "relative", flexShrink: 0 }}>
|
||||
<svg
|
||||
<svg role="img" aria-label="icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -181,7 +181,7 @@ export const OnboardingProgress: React.FC<{
|
||||
}}
|
||||
/>
|
||||
{/* Checkmark */}
|
||||
<svg
|
||||
<svg role="img" aria-label="icon"
|
||||
viewBox="0 0 24 24"
|
||||
width={14}
|
||||
height={14}
|
||||
|
||||
@@ -107,7 +107,7 @@ function RootComponent() {
|
||||
} else {
|
||||
resetUser();
|
||||
}
|
||||
}, [user?.id]);
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
const theme = getUserSettings().theme ?? "dark";
|
||||
|
||||
@@ -67,7 +67,7 @@ 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
|
||||
<svg role="img" aria-label="icon"
|
||||
className="size-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
|
||||
@@ -173,7 +173,7 @@ function SubscriptionPage() {
|
||||
billing_period: subscription.billingPeriod,
|
||||
});
|
||||
}
|
||||
}, [subscription?.status, subscription?.plan?.key]);
|
||||
}, [subscription]);
|
||||
|
||||
// Apply referral code from Google OAuth callback
|
||||
useEffect(() => {
|
||||
@@ -227,7 +227,7 @@ function SubscriptionPage() {
|
||||
setOnboardingPhase("provisioning");
|
||||
trialMutation.mutate();
|
||||
window.history.replaceState({}, "", window.location.pathname);
|
||||
}, [welcome, subData, eligibleForTrial]);
|
||||
}, [welcome, subData, eligibleForTrial, trialMutation.mutate]);
|
||||
|
||||
// Transition from provisioning → completed when both animation and mutation are done
|
||||
useEffect(() => {
|
||||
@@ -241,7 +241,7 @@ function SubscriptionPage() {
|
||||
spread: 80,
|
||||
origin: { y: 0.6 },
|
||||
});
|
||||
}, [onboardingPhase, animationEnded, trialMutation.isSuccess]);
|
||||
}, [onboardingPhase, animationEnded, trialMutation.isSuccess, queryClient.invalidateQueries]);
|
||||
|
||||
// Animation timer: 210 frames / 30fps = 7s + small buffer
|
||||
useEffect(() => {
|
||||
|
||||
@@ -117,7 +117,7 @@ function DemoPage() {
|
||||
Sase.tr
|
||||
</Link>
|
||||
<div className="flex items-center gap-3">
|
||||
<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 +144,7 @@ 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
|
||||
<button type="button"
|
||||
onClick={() => {
|
||||
setStep("vin");
|
||||
setSelectedCategory(null);
|
||||
@@ -154,7 +154,7 @@ function DemoPage() {
|
||||
1. VIN Girin
|
||||
</button>
|
||||
<div className="h-px w-6 bg-border" />
|
||||
<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 +162,7 @@ function DemoPage() {
|
||||
2. Kategori Seçin
|
||||
</button>
|
||||
<div className="h-px w-6 bg-border" />
|
||||
<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 +244,7 @@ function DemoPage() {
|
||||
)}
|
||||
|
||||
{!vin && (
|
||||
<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 +268,7 @@ function DemoPage() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => {
|
||||
setStep("vin");
|
||||
setSelectedCategory(null);
|
||||
@@ -281,7 +281,7 @@ function DemoPage() {
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{EXAMPLE_CATEGORIES.map((cat) => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={cat.name}
|
||||
onClick={() => {
|
||||
setSelectedCategory(cat.name);
|
||||
@@ -319,7 +319,7 @@ function DemoPage() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setStep("categories")}
|
||||
className="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
||||
>
|
||||
|
||||
@@ -421,7 +421,7 @@ function HomePage() {
|
||||
});
|
||||
|
||||
return () => {
|
||||
scripts.forEach((el) => el.remove());
|
||||
for (const el of scripts) el.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -572,7 +572,7 @@ function HomePage() {
|
||||
</nav>
|
||||
|
||||
<div className="hidden items-center gap-3 md:flex">
|
||||
<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 +610,7 @@ function HomePage() {
|
||||
|
||||
{/* Mobile toggle */}
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<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,20 +627,26 @@ 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">
|
||||
<a
|
||||
href="#features"
|
||||
className="transition hover:text-foreground"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
<button type="button"
|
||||
type="button"
|
||||
className="text-left transition hover:text-foreground"
|
||||
onClick={() => {
|
||||
document.getElementById("features")?.scrollIntoView({ behavior: "smooth" });
|
||||
setMobileMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
Özellikler
|
||||
</a>
|
||||
<a
|
||||
href="#how-it-works"
|
||||
className="transition hover:text-foreground"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
</button>
|
||||
<button type="button"
|
||||
type="button"
|
||||
className="text-left transition hover:text-foreground"
|
||||
onClick={() => {
|
||||
document.getElementById("how-it-works")?.scrollIntoView({ behavior: "smooth" });
|
||||
setMobileMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
Nasıl Çalışır
|
||||
</a>
|
||||
</button>
|
||||
<Link
|
||||
to="/pricing"
|
||||
className="transition hover:text-foreground"
|
||||
@@ -822,7 +828,7 @@ function HomePage() {
|
||||
{/* Example VIN invite */}
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
Şase numaranız yok mu?{" "}
|
||||
<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"
|
||||
@@ -1307,7 +1313,7 @@ function HomePage() {
|
||||
>
|
||||
<div className="flex gap-0.5">
|
||||
{Array.from({ length: t.rating }).map((_, i) => (
|
||||
<svg
|
||||
<svg role="img" aria-label="icon"
|
||||
key={i}
|
||||
viewBox="0 0 20 20"
|
||||
className="size-3.5 fill-foreground/85"
|
||||
@@ -1538,7 +1544,7 @@ function HomePage() {
|
||||
<section className="relative overflow-hidden bg-surface-alt px-4 py-20 sm:px-6 sm:py-24">
|
||||
{/* SVG grid pattern */}
|
||||
<div className="pointer-events-none absolute inset-0 opacity-[0.03]">
|
||||
<svg width="100%" height="100%">
|
||||
<svg width="100%" height="100%" role="img" aria-label="grid pattern">
|
||||
<defs>
|
||||
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="currentColor" strokeWidth="1" />
|
||||
|
||||
Reference in New Issue
Block a user