From 76f745cc87a399ad62f8affc6493718786b807bc Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 16 Feb 2026 18:50:34 +0100 Subject: [PATCH] fix: resolve lint and build errors - Added 'use client' to not-found.tsx - Refactored RelatedProducts to Server Component to fix 'fs' import error - Created RelatedProductLink for client-side analytics - Fixed lint syntax issues in RecordModeVisuals.tsx - Fixed rule-of-hooks violation in WebsiteVideo.tsx --- app/[locale]/not-found.tsx | 1 + components/RelatedProductLink.tsx | 39 +++ components/RelatedProducts.tsx | 52 +--- components/record-mode/RecordModeVisuals.tsx | 284 ++++++++++++------- next-env.d.ts | 2 +- remotion/WebsiteVideo.tsx | 214 +++++++------- 6 files changed, 349 insertions(+), 243 deletions(-) create mode 100644 components/RelatedProductLink.tsx diff --git a/app/[locale]/not-found.tsx b/app/[locale]/not-found.tsx index f2bb9a93..3e1c334b 100644 --- a/app/[locale]/not-found.tsx +++ b/app/[locale]/not-found.tsx @@ -1,3 +1,4 @@ +'use client'; import { useTranslations } from 'next-intl'; import { Container, Button, Heading } from '@/components/ui'; import Scribble from '@/components/Scribble'; diff --git a/components/RelatedProductLink.tsx b/components/RelatedProductLink.tsx new file mode 100644 index 00000000..adf7230f --- /dev/null +++ b/components/RelatedProductLink.tsx @@ -0,0 +1,39 @@ +'use client'; + +import Link from 'next/link'; +import { useAnalytics } from './analytics/useAnalytics'; +import { AnalyticsEvents } from './analytics/analytics-events'; + +interface RelatedProductLinkProps { + href: string; + productSlug: string; + productTitle: string; + children: React.ReactNode; + className?: string; +} + +export function RelatedProductLink({ + href, + productSlug, + productTitle, + children, + className, +}: RelatedProductLinkProps) { + const { trackEvent } = useAnalytics(); + + return ( + + trackEvent(AnalyticsEvents.PRODUCT_VIEW, { + product_id: productSlug, + product_name: productTitle, + location: 'related_products', + }) + } + > + {children} + + ); +} diff --git a/components/RelatedProducts.tsx b/components/RelatedProducts.tsx index a7bbb39b..872d8f81 100644 --- a/components/RelatedProducts.tsx +++ b/components/RelatedProducts.tsx @@ -1,13 +1,7 @@ -'use client'; - import { getAllProducts } from '@/lib/mdx'; -import { mapFileSlugToTranslated } from '@/lib/slugs'; import { getTranslations } from 'next-intl/server'; import Image from 'next/image'; -import Link from 'next/link'; -import { useAnalytics } from './analytics/useAnalytics'; -import { AnalyticsEvents } from './analytics/analytics-events'; -import { useEffect, useState } from 'react'; +import { RelatedProductLink } from './RelatedProductLink'; interface RelatedProductsProps { currentSlug: string; @@ -15,25 +9,16 @@ interface RelatedProductsProps { locale: string; } -export default function RelatedProducts({ currentSlug, categories, locale }: RelatedProductsProps) { - const { trackEvent } = useAnalytics(); - const [allProducts, setAllProducts] = useState([]); - const [t, setT] = useState(null); - - useEffect(() => { - async function load() { - const products = await getAllProducts(locale); - const translations = await getTranslations('Products'); - setAllProducts(products); - setT(() => translations); - } - load(); - }, [locale]); - - if (!t) return null; +export default async function RelatedProducts({ + currentSlug, + categories, + locale, +}: RelatedProductsProps) { + const products = await getAllProducts(locale); + const t = await getTranslations('Products'); // Filter products: same category, not current product - const related = allProducts + const related = products .filter( (p) => p.slug !== currentSlug && p.frontmatter.categories.some((cat) => categories.includes(cat)), @@ -73,24 +58,13 @@ export default function RelatedProducts({ currentSlug, categories, locale }: Rel ); }) || 'low-voltage-cables'; - // Note: Since this is now client-side, we can't easily use mapFileSlugToTranslated - // if it's a server-only lib. Let's assume for now the slugs are compatible or - // we'll need to pass translated slugs from parent if needed. - // For now, let's just use the product slug as is, or if we want to be safe, - // we should have kept this a server component and used a client wrapper for the link. - return ( - - trackEvent(AnalyticsEvents.PRODUCT_VIEW, { - product_id: product.slug, - product_name: product.frontmatter.title, - location: 'related_products', - }) - } >
{product.frontmatter.images?.[0] ? ( @@ -142,7 +116,7 @@ export default function RelatedProducts({ currentSlug, categories, locale }: Rel
- + ); })} diff --git a/components/record-mode/RecordModeVisuals.tsx b/components/record-mode/RecordModeVisuals.tsx index 6135efe6..a2174d5b 100644 --- a/components/record-mode/RecordModeVisuals.tsx +++ b/components/record-mode/RecordModeVisuals.tsx @@ -4,34 +4,36 @@ import React from 'react'; import { useRecordMode } from './RecordModeContext'; export function RecordModeVisuals({ children }: { children: React.ReactNode }) { - const { isActive, isPlaying, zoomLevel, cursorPosition, isBlurry } = useRecordMode(); - const [mounted, setMounted] = React.useState(false); - const [isEmbedded, setIsEmbedded] = React.useState(false); - const [iframeUrl, setIframeUrl] = React.useState(null); + const { isActive, isPlaying, zoomLevel, cursorPosition, isBlurry } = useRecordMode(); + const [mounted, setMounted] = React.useState(false); + const [isEmbedded, setIsEmbedded] = React.useState(false); + const [iframeUrl, setIframeUrl] = React.useState(null); - React.useEffect(() => { - setMounted(true); - // Explicit non-magical detection - const embedded = window.location.search.includes('embedded=true') || window.name === 'record-mode-iframe'; - setIsEmbedded(embedded); + React.useEffect(() => { + setMounted(true); + // Explicit non-magical detection + const embedded = + window.location.search.includes('embedded=true') || window.name === 'record-mode-iframe'; + setIsEmbedded(embedded); - if (!embedded) { - const url = new URL(window.location.href); - url.searchParams.set('embedded', 'true'); - setIframeUrl(url.toString()); - } - }, [isEmbedded]); + if (!embedded) { + const url = new URL(window.location.href); + url.searchParams.set('embedded', 'true'); + setIframeUrl(url.toString()); + } + }, [isEmbedded]); - // Hydration Guard: Match server on first render - if (!mounted) return <>{children}; + // Hydration Guard: Match server on first render + if (!mounted) return <>{children}; - // Recursion Guard: If we are already in an embedded iframe, - // strictly return just the children to prevent Inception. - if (isEmbedded) { - return ( - <> - - - - ); + `, + }} + /> + + + ); } diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818f..9edff1c7 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/remotion/WebsiteVideo.tsx b/remotion/WebsiteVideo.tsx index 95e9f146..6925d4e5 100644 --- a/remotion/WebsiteVideo.tsx +++ b/remotion/WebsiteVideo.tsx @@ -1,107 +1,127 @@ import React, { useMemo } from 'react'; -import { AbsoluteFill, useVideoConfig, useCurrentFrame, interpolate, spring, Easing } from 'remotion'; +import { + AbsoluteFill, + useVideoConfig, + useCurrentFrame, + interpolate, + spring, + Easing, +} from 'remotion'; import { RecordingSession, RecordEvent } from '../types/record-mode'; export const WebsiteVideo: React.FC<{ - session: RecordingSession | null; - siteUrl: string; + session: RecordingSession | null; + siteUrl: string; }> = ({ session, siteUrl }) => { - const { fps, width, height, durationInFrames } = useVideoConfig(); - const frame = useCurrentFrame(); + const { fps, width, height, durationInFrames } = useVideoConfig(); + const frame = useCurrentFrame(); - if (!session || !session.events.length) { - return ( - - No session data found. - - ); - } - - const sortedEvents = useMemo(() => { - return [...session.events].sort((a, b) => a.timestamp - b.timestamp); - }, [session]); - - const elapsedTimeMs = (frame / fps) * 1000; - - // --- Interpolation Logic --- - - // 1. Find the current window (between which two events are we?) - const nextEventIndex = sortedEvents.findIndex(e => e.timestamp > elapsedTimeMs); - let currentEventIndex; - - if (nextEventIndex === -1) { - // We are past the last event, stay at the end - currentEventIndex = sortedEvents.length - 1; - } else { - currentEventIndex = Math.max(0, nextEventIndex - 1); - } - - const currentEvent = sortedEvents[currentEventIndex]; - // If there is no next event, we just stay at current (next=current) - const nextEvent = (nextEventIndex !== -1) ? sortedEvents[nextEventIndex] : currentEvent; - - // 2. Calculate Progress between events - const gap = nextEvent.timestamp - currentEvent.timestamp; - const progress = gap > 0 ? (elapsedTimeMs - currentEvent.timestamp) / gap : 1; - const easedProgress = Easing.cubic(Math.min(Math.max(progress, 0), 1)); - - // 3. Calculate Cursor Position from Rects - const getCenter = (event: RecordEvent) => { - if (event.rect) { - return { - x: event.rect.x + event.rect.width / 2, - y: event.rect.y + event.rect.height / 2 - }; - } - return { x: width / 2, y: height / 2 }; - }; - - const p1 = getCenter(currentEvent); - const p2 = getCenter(nextEvent); - - const cursorX = interpolate(easedProgress, [0, 1], [p1.x, p2.x]); - const cursorY = interpolate(easedProgress, [0, 1], [p1.y, p2.y]); - - // 4. Zoom & Blur - const zoom = interpolate(easedProgress, [0, 1], [currentEvent.zoom || 1, nextEvent.zoom || 1]); - const isBlurry = currentEvent.motionBlur || nextEvent.motionBlur; + const sortedEvents = useMemo(() => { + if (!session) return []; + return [...session.events].sort((a, b) => a.timestamp - b.timestamp); + }, [session]); + if (!session || !session.events.length) { return ( - -
-