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:
@@ -652,7 +652,7 @@ export class CatalogService {
|
||||
|
||||
if (dbCategories.length === 0 && effectiveCatalogPath) {
|
||||
try {
|
||||
let pl24Categories;
|
||||
let pl24Categories: Awaited<ReturnType<PL24Service["fetchMainGroups"]>>;
|
||||
|
||||
if (vehicle.architecture === "LEGACY_PSA") {
|
||||
// PSA vehicles: parse family/salesType from catalogPath, mode/upds from metadata
|
||||
@@ -742,7 +742,7 @@ export class CatalogService {
|
||||
*/
|
||||
async getCategoryWithParts(
|
||||
catalogVehicleId: string,
|
||||
categoryId: string,
|
||||
categoryIdInput: string,
|
||||
userId: string,
|
||||
body = "_all_",
|
||||
engine = "_all_",
|
||||
@@ -762,21 +762,27 @@ export class CatalogService {
|
||||
|
||||
// PSA variant trees return PL24 codes (e.g. "_FCT0100") as IDs instead of UUIDs.
|
||||
// Detect and resolve to DB UUID via externalId lookup.
|
||||
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(categoryId);
|
||||
const isUuid =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(categoryIdInput);
|
||||
let categoryRow: typeof categories.$inferSelect | undefined;
|
||||
|
||||
if (isUuid) {
|
||||
[categoryRow] = await this.db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(eq(categories.id, categoryId))
|
||||
.where(eq(categories.id, categoryIdInput))
|
||||
.limit(1);
|
||||
} else {
|
||||
// PSA external code — look up by externalId within this catalog vehicle
|
||||
[categoryRow] = await this.db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.externalId, categoryId), eq(categories.catalogVehicleId, catalogVehicleId)))
|
||||
.where(
|
||||
and(
|
||||
eq(categories.externalId, categoryIdInput),
|
||||
eq(categories.catalogVehicleId, catalogVehicleId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
}
|
||||
|
||||
@@ -784,7 +790,7 @@ export class CatalogService {
|
||||
|
||||
// Normalize to DB UUID for all downstream queries
|
||||
const category = categoryRow;
|
||||
categoryId = category.id;
|
||||
const categoryId = category.id;
|
||||
|
||||
const linkPath = category.linkPath;
|
||||
|
||||
|
||||
@@ -256,8 +256,7 @@ export class EmexService {
|
||||
const linkRx = /href="(Vehicle\.aspx\?[^"]+)">([^<]+)<\/a>/g;
|
||||
const seen = new Set<string>();
|
||||
const vehicles: EmexHttpVehicle[] = [];
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = linkRx.exec(html)) !== null) {
|
||||
for (const m of html.matchAll(linkRx)) {
|
||||
const href = m[1].replace(/&/g, "&");
|
||||
if (seen.has(href)) continue;
|
||||
seen.add(href);
|
||||
@@ -291,8 +290,7 @@ export class EmexService {
|
||||
const catRx = /href="(QuickDetails\.aspx\?[^"]+)">([^<]+)<\/a>/g;
|
||||
const seen = new Set<string>();
|
||||
const cats: EmexHttpCategory[] = [];
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = catRx.exec(html)) !== null) {
|
||||
for (const m of html.matchAll(catRx)) {
|
||||
const href = m[1].replace(/&/g, "&");
|
||||
const name = m[2].trim();
|
||||
if (name.length < 2 || seen.has(href)) continue;
|
||||
|
||||
@@ -2185,8 +2185,7 @@ export class PL24FordLegacyService {
|
||||
// Scope rows contain jsonUrl="json-main-groups.action?...&scope=_FCT0001&..." and a name cell
|
||||
const trRegex =
|
||||
/<tr[^>]+jsonUrl="(json-main-groups\.action[^"]+)"[^>]*>[\s\S]*?<td[^>]*>([^<]+)<\/td>/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = trRegex.exec(html)) !== null) {
|
||||
for (const m of html.matchAll(trRegex)) {
|
||||
const jsonUrl = m[1];
|
||||
const name = m[2].trim();
|
||||
const scopeMatch = jsonUrl.match(/[?&]scope=([^&]+)/);
|
||||
@@ -2843,8 +2842,7 @@ export class PL24FordLegacyService {
|
||||
// These are server-rendered (not JS-injected) — the most reliable source.
|
||||
const trRegex = /<tr[^>]*\bmodelFamily="([^"]+)"[^>]*\bmodelFamilyName="([^"]+)"/g;
|
||||
const fromHtmlAttrs: Array<{ id: string; name: string }> = [];
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = trRegex.exec(html)) !== null) {
|
||||
for (const m of html.matchAll(trRegex)) {
|
||||
fromHtmlAttrs.push({ id: m[1], name: m[2] });
|
||||
}
|
||||
if (fromHtmlAttrs.length > 0) {
|
||||
@@ -2875,7 +2873,7 @@ export class PL24FordLegacyService {
|
||||
if (selectMatch) {
|
||||
const optionRegex = /<option[^>]*value=["']([^"']+)["'][^>]*>([\s\S]*?)<\/option>/gi;
|
||||
const options: Array<{ id: string; name: string }> = [];
|
||||
while ((m = optionRegex.exec(selectMatch[1])) !== null) {
|
||||
for (const m of selectMatch[1].matchAll(optionRegex)) {
|
||||
const id = m[1].trim();
|
||||
const name = m[2].replace(/<[^>]+>/g, "").trim();
|
||||
if (id && id !== "" && id !== "0" && name) {
|
||||
@@ -2894,8 +2892,7 @@ export class PL24FordLegacyService {
|
||||
"gi",
|
||||
);
|
||||
const modes = new Map<string, string>();
|
||||
let modeMatch: RegExpExecArray | null;
|
||||
while ((modeMatch = modeRegex.exec(html)) !== null) {
|
||||
for (const modeMatch of html.matchAll(modeRegex)) {
|
||||
const mode = modeMatch[1];
|
||||
if (mode && !modes.has(mode)) {
|
||||
modes.set(mode, mode);
|
||||
@@ -3279,8 +3276,7 @@ export class PL24FordLegacyService {
|
||||
// Try to extract any text from <td> elements, including those with nested tags
|
||||
let tdName = "";
|
||||
const tdPattern = /<td[^>]*>([\s\S]*?)<\/td>/g;
|
||||
let tdMatch2: RegExpExecArray | null;
|
||||
while ((tdMatch2 = tdPattern.exec(segment)) !== null) {
|
||||
for (const tdMatch2 of segment.matchAll(tdPattern)) {
|
||||
const text = tdMatch2[1]
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/ /g, " ")
|
||||
@@ -3331,8 +3327,7 @@ export class PL24FordLegacyService {
|
||||
// Fallback: <a href="...group...action"> links
|
||||
if (groups.length === 0) {
|
||||
const linkRegex = /<a[^>]+href="([^"]*group[^"]*\.action[^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = linkRegex.exec(html)) !== null) {
|
||||
for (const match of html.matchAll(linkRegex)) {
|
||||
const url = match[1];
|
||||
const name = match[2].replace(/<[^>]+>/g, "").trim();
|
||||
if (!name || seen.has(url)) continue;
|
||||
@@ -3508,8 +3503,7 @@ export class PL24FordLegacyService {
|
||||
} else {
|
||||
// Extract text from first non-empty <td> (strip inner tags)
|
||||
const tdPattern = /<td[^>]*>([\s\S]*?)<\/td>/g;
|
||||
let tdMatch: RegExpExecArray | null;
|
||||
while ((tdMatch = tdPattern.exec(segment)) !== null) {
|
||||
for (const tdMatch of segment.matchAll(tdPattern)) {
|
||||
const text = tdMatch[1]
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/ /g, " ")
|
||||
@@ -3568,9 +3562,7 @@ export class PL24FordLegacyService {
|
||||
extractLinks(html: string, pattern: RegExp): { href: string; text: string }[] {
|
||||
const linkRegex = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
|
||||
const results: { href: string; text: string }[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = linkRegex.exec(html)) !== null) {
|
||||
for (const match of html.matchAll(linkRegex)) {
|
||||
const href = match[1];
|
||||
const text = match[2].replace(/<[^>]+>/g, "").trim();
|
||||
if (pattern.test(href) && text) {
|
||||
@@ -3596,22 +3588,19 @@ export class PL24FordLegacyService {
|
||||
// Extract header cells
|
||||
const headerRegex = /<th[^>]*>([\s\S]*?)<\/th>/gi;
|
||||
const headers: string[] = [];
|
||||
let hMatch: RegExpExecArray | null;
|
||||
while ((hMatch = headerRegex.exec(tableHtml)) !== null) {
|
||||
for (const hMatch of tableHtml.matchAll(headerRegex)) {
|
||||
headers.push(hMatch[1].replace(/<[^>]+>/g, "").trim());
|
||||
}
|
||||
|
||||
// Extract rows
|
||||
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
||||
const rows: Record<string, string>[] = [];
|
||||
let rMatch: RegExpExecArray | null;
|
||||
let rowIndex = 0;
|
||||
|
||||
while ((rMatch = rowRegex.exec(tableHtml)) !== null) {
|
||||
for (const rMatch of tableHtml.matchAll(rowRegex)) {
|
||||
const cellRegex = /<td[^>]*>([\s\S]*?)<\/td>/gi;
|
||||
const cells: string[] = [];
|
||||
let cMatch: RegExpExecArray | null;
|
||||
while ((cMatch = cellRegex.exec(rMatch[1])) !== null) {
|
||||
for (const cMatch of rMatch[1].matchAll(cellRegex)) {
|
||||
cells.push(cMatch[1].replace(/<[^>]+>/g, "").trim());
|
||||
}
|
||||
|
||||
|
||||
@@ -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