(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 (
-
-
-
-
-
- {/* Visual Cursor */}
-
-
+
+ No session data found.
+
);
+ }
+
+ 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;
+
+ return (
+
+
+
+
+
+ {/* Visual Cursor */}
+
+
+ );
};