fix: resolve lint and build errors
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🏗️ Build (push) Failing after 1m0s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🏗️ Build (push) Failing after 1m0s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
- 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
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
'use client';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { Container, Button, Heading } from '@/components/ui';
|
import { Container, Button, Heading } from '@/components/ui';
|
||||||
import Scribble from '@/components/Scribble';
|
import Scribble from '@/components/Scribble';
|
||||||
|
|||||||
39
components/RelatedProductLink.tsx
Normal file
39
components/RelatedProductLink.tsx
Normal file
@@ -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 (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className={className}
|
||||||
|
onClick={() =>
|
||||||
|
trackEvent(AnalyticsEvents.PRODUCT_VIEW, {
|
||||||
|
product_id: productSlug,
|
||||||
|
product_name: productTitle,
|
||||||
|
location: 'related_products',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { getAllProducts } from '@/lib/mdx';
|
import { getAllProducts } from '@/lib/mdx';
|
||||||
import { mapFileSlugToTranslated } from '@/lib/slugs';
|
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import { RelatedProductLink } from './RelatedProductLink';
|
||||||
import { useAnalytics } from './analytics/useAnalytics';
|
|
||||||
import { AnalyticsEvents } from './analytics/analytics-events';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
interface RelatedProductsProps {
|
interface RelatedProductsProps {
|
||||||
currentSlug: string;
|
currentSlug: string;
|
||||||
@@ -15,25 +9,16 @@ interface RelatedProductsProps {
|
|||||||
locale: string;
|
locale: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RelatedProducts({ currentSlug, categories, locale }: RelatedProductsProps) {
|
export default async function RelatedProducts({
|
||||||
const { trackEvent } = useAnalytics();
|
currentSlug,
|
||||||
const [allProducts, setAllProducts] = useState<any[]>([]);
|
categories,
|
||||||
const [t, setT] = useState<any>(null);
|
locale,
|
||||||
|
}: RelatedProductsProps) {
|
||||||
useEffect(() => {
|
const products = await getAllProducts(locale);
|
||||||
async function load() {
|
const t = await getTranslations('Products');
|
||||||
const products = await getAllProducts(locale);
|
|
||||||
const translations = await getTranslations('Products');
|
|
||||||
setAllProducts(products);
|
|
||||||
setT(() => translations);
|
|
||||||
}
|
|
||||||
load();
|
|
||||||
}, [locale]);
|
|
||||||
|
|
||||||
if (!t) return null;
|
|
||||||
|
|
||||||
// Filter products: same category, not current product
|
// Filter products: same category, not current product
|
||||||
const related = allProducts
|
const related = products
|
||||||
.filter(
|
.filter(
|
||||||
(p) =>
|
(p) =>
|
||||||
p.slug !== currentSlug && p.frontmatter.categories.some((cat) => categories.includes(cat)),
|
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';
|
}) || '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 (
|
return (
|
||||||
<Link
|
<RelatedProductLink
|
||||||
key={product.slug}
|
key={product.slug}
|
||||||
href={`/${locale}/products/${catSlug}/${product.slug}`}
|
href={`/${locale}/products/${catSlug}/${product.slug}`}
|
||||||
|
productSlug={product.slug}
|
||||||
|
productTitle={product.frontmatter.title}
|
||||||
className="group block bg-white rounded-[32px] overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-500 border border-neutral-dark/5"
|
className="group block bg-white rounded-[32px] overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-500 border border-neutral-dark/5"
|
||||||
onClick={() =>
|
|
||||||
trackEvent(AnalyticsEvents.PRODUCT_VIEW, {
|
|
||||||
product_id: product.slug,
|
|
||||||
product_name: product.frontmatter.title,
|
|
||||||
location: 'related_products',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="aspect-[16/10] relative bg-neutral-light/30 p-8 overflow-hidden">
|
<div className="aspect-[16/10] relative bg-neutral-light/30 p-8 overflow-hidden">
|
||||||
{product.frontmatter.images?.[0] ? (
|
{product.frontmatter.images?.[0] ? (
|
||||||
@@ -142,7 +116,7 @@ export default function RelatedProducts({ currentSlug, categories, locale }: Rel
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</RelatedProductLink>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,34 +4,36 @@ import React from 'react';
|
|||||||
import { useRecordMode } from './RecordModeContext';
|
import { useRecordMode } from './RecordModeContext';
|
||||||
|
|
||||||
export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
|
export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
|
||||||
const { isActive, isPlaying, zoomLevel, cursorPosition, isBlurry } = useRecordMode();
|
const { isActive, isPlaying, zoomLevel, cursorPosition, isBlurry } = useRecordMode();
|
||||||
const [mounted, setMounted] = React.useState(false);
|
const [mounted, setMounted] = React.useState(false);
|
||||||
const [isEmbedded, setIsEmbedded] = React.useState(false);
|
const [isEmbedded, setIsEmbedded] = React.useState(false);
|
||||||
const [iframeUrl, setIframeUrl] = React.useState<string | null>(null);
|
const [iframeUrl, setIframeUrl] = React.useState<string | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
// Explicit non-magical detection
|
// Explicit non-magical detection
|
||||||
const embedded = window.location.search.includes('embedded=true') || window.name === 'record-mode-iframe';
|
const embedded =
|
||||||
setIsEmbedded(embedded);
|
window.location.search.includes('embedded=true') || window.name === 'record-mode-iframe';
|
||||||
|
setIsEmbedded(embedded);
|
||||||
|
|
||||||
if (!embedded) {
|
if (!embedded) {
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
url.searchParams.set('embedded', 'true');
|
url.searchParams.set('embedded', 'true');
|
||||||
setIframeUrl(url.toString());
|
setIframeUrl(url.toString());
|
||||||
}
|
}
|
||||||
}, [isEmbedded]);
|
}, [isEmbedded]);
|
||||||
|
|
||||||
// Hydration Guard: Match server on first render
|
// Hydration Guard: Match server on first render
|
||||||
if (!mounted) return <>{children}</>;
|
if (!mounted) return <>{children}</>;
|
||||||
|
|
||||||
// Recursion Guard: If we are already in an embedded iframe,
|
// Recursion Guard: If we are already in an embedded iframe,
|
||||||
// strictly return just the children to prevent Inception.
|
// strictly return just the children to prevent Inception.
|
||||||
if (isEmbedded) {
|
if (isEmbedded) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style dangerouslySetInnerHTML={{
|
<style
|
||||||
__html: `
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
/* Harder Isolation: Hide ALL potentially duplicate overlays and DEV TOOLS */
|
/* Harder Isolation: Hide ALL potentially duplicate overlays and DEV TOOLS */
|
||||||
#nextjs-portal,
|
#nextjs-portal,
|
||||||
#nextjs-portal-root,
|
#nextjs-portal-root,
|
||||||
@@ -49,7 +51,7 @@ export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
|
|||||||
[style*="z-index: 9999"],
|
[style*="z-index: 9999"],
|
||||||
[style*="z-index: 10000"],
|
[style*="z-index: 10000"],
|
||||||
.fixed.bottom-6.left-6,
|
.fixed.bottom-6.left-6,
|
||||||
.fixed.bottom-6.left-1\/2,
|
.fixed.bottom-6.left-1/2,
|
||||||
.feedback-ui-overlay,
|
.feedback-ui-overlay,
|
||||||
[id^="feedback-"],
|
[id^="feedback-"],
|
||||||
[class^="feedback-"] {
|
[class^="feedback-"] {
|
||||||
@@ -78,18 +80,21 @@ export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
|
|||||||
overflow-x: hidden !important;
|
overflow-x: hidden !important;
|
||||||
overflow-y: auto !important;
|
overflow-y: auto !important;
|
||||||
}
|
}
|
||||||
`}} />
|
`,
|
||||||
{children}
|
}}
|
||||||
</>
|
/>
|
||||||
);
|
{children}
|
||||||
}
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Global Style for Body Lock */}
|
{/* Global Style for Body Lock */}
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<style dangerouslySetInnerHTML={{
|
<style
|
||||||
__html: `
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
html, body {
|
html, body {
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
height: 100vh !important;
|
height: 100vh !important;
|
||||||
@@ -102,78 +107,143 @@ export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
|
|||||||
.nextjs-static-indicator {
|
.nextjs-static-indicator {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
`}} />
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`transition-all duration-1000 ${isActive ? 'fixed inset-0 z-[9997] bg-[#020202] flex items-center justify-center p-6 md:p-12 lg:p-20' : 'relative w-full'}`}
|
||||||
|
>
|
||||||
|
{/* Studio Background - Only visible when active */}
|
||||||
|
{isActive && (
|
||||||
|
<div className="absolute inset-0 z-0 pointer-events-none overflow-hidden">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-[#03110a] via-[#020202] to-[#030a11] animate-pulse duration-[10s]" />
|
||||||
|
<div
|
||||||
|
className="absolute -top-[60%] -left-[50%] w-[140%] h-[140%] rounded-full opacity-[0.7]"
|
||||||
|
style={{
|
||||||
|
background: 'radial-gradient(circle, #10b981 0%, transparent 70%)',
|
||||||
|
filter: 'blur(160px)',
|
||||||
|
animation: 'mesh-float-1 18s ease-in-out infinite',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute -bottom-[60%] -right-[50%] w-[130%] h-[130%] rounded-full opacity-[0.55]"
|
||||||
|
style={{
|
||||||
|
background: 'radial-gradient(circle, #06b6d4 0%, transparent 70%)',
|
||||||
|
filter: 'blur(150px)',
|
||||||
|
animation: 'mesh-float-2 22s ease-in-out infinite',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute -top-[30%] -right-[40%] w-[100%] h-[100%] rounded-full opacity-[0.5]"
|
||||||
|
style={{
|
||||||
|
background: 'radial-gradient(circle, #82ed20 0%, transparent 70%)',
|
||||||
|
filter: 'blur(130px)',
|
||||||
|
animation: 'mesh-float-3 14s ease-in-out infinite',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute -bottom-[50%] -left-[40%] w-[110%] h-[110%] rounded-full opacity-[0.45]"
|
||||||
|
style={{
|
||||||
|
background: 'radial-gradient(circle, #2563eb 0%, transparent 70%)',
|
||||||
|
filter: 'blur(140px)',
|
||||||
|
animation: 'mesh-float-4 20s ease-in-out infinite',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-[0.12] mix-blend-overlay"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E")`,
|
||||||
|
backgroundSize: '128px 128px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-[0.06]"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
'repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.03) 2px, rgba(255,255,255,0.03) 4px)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`transition-all duration-700 ease-in-out relative z-10 w-full ${isActive ? 'h-full max-h-[1000px] max-w-[1600px] drop-shadow-[0_60px_150px_rgba(0,0,0,1)] scale-in' : 'h-full'}`}
|
||||||
|
style={{
|
||||||
|
transform: isPlaying ? `scale(${zoomLevel})` : undefined,
|
||||||
|
transformOrigin: isPlaying ? `${cursorPosition.x}px ${cursorPosition.y}px` : 'center',
|
||||||
|
filter: isBlurry ? 'blur(4px)' : 'none',
|
||||||
|
willChange: 'transform, filter',
|
||||||
|
WebkitBackfaceVisibility: 'hidden',
|
||||||
|
backfaceVisibility: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isActive
|
||||||
|
? 'relative h-full w-full rounded-[3rem] overflow-hidden bg-[#050505] isolate'
|
||||||
|
: 'w-full h-full'
|
||||||
|
}
|
||||||
|
style={{ transform: isActive ? 'translateZ(0)' : 'none' }}
|
||||||
|
>
|
||||||
|
{isActive && (
|
||||||
|
<>
|
||||||
|
<div className="absolute inset-0 rounded-[3rem] border border-white/[0.08] pointer-events-none z-50" />
|
||||||
|
<div
|
||||||
|
className="absolute inset-[-2px] rounded-[3rem] pointer-events-none z-20"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(135deg, rgba(16,185,129,0.15), rgba(130,237,32,0.15))',
|
||||||
|
animation: 'pulse-ring 4s ease-in-out infinite',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-[#82ed20]/[0.05] to-transparent h-[15%] w-full top-[-15%] animate-scan-slow z-50 pointer-events-none opacity-20" />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`transition-all duration-1000 ${isActive ? 'fixed inset-0 z-[9997] bg-[#020202] flex items-center justify-center p-6 md:p-12 lg:p-20' : 'relative w-full'}`}>
|
<div
|
||||||
{/* Studio Background - Only visible when active */}
|
className={
|
||||||
{isActive && (
|
isActive
|
||||||
<div className="absolute inset-0 z-0 pointer-events-none overflow-hidden">
|
? 'w-full h-full rounded-[3rem] overflow-hidden relative'
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-[#03110a] via-[#020202] to-[#030a11] animate-pulse duration-[10s]" />
|
: 'w-full h-full relative'
|
||||||
<div className="absolute -top-[60%] -left-[50%] w-[140%] h-[140%] rounded-full opacity-[0.7]"
|
}
|
||||||
style={{ background: 'radial-gradient(circle, #10b981 0%, transparent 70%)', filter: 'blur(160px)', animation: 'mesh-float-1 18s ease-in-out infinite' }} />
|
style={{
|
||||||
<div className="absolute -bottom-[60%] -right-[50%] w-[130%] h-[130%] rounded-full opacity-[0.55]"
|
WebkitMaskImage: isActive ? '-webkit-radial-gradient(white, black)' : 'none',
|
||||||
style={{ background: 'radial-gradient(circle, #06b6d4 0%, transparent 70%)', filter: 'blur(150px)', animation: 'mesh-float-2 22s ease-in-out infinite' }} />
|
transform: isActive ? 'translateZ(0)' : 'none',
|
||||||
<div className="absolute -top-[30%] -right-[40%] w-[100%] h-[100%] rounded-full opacity-[0.5]"
|
}}
|
||||||
style={{ background: 'radial-gradient(circle, #82ed20 0%, transparent 70%)', filter: 'blur(130px)', animation: 'mesh-float-3 14s ease-in-out infinite' }} />
|
>
|
||||||
<div className="absolute -bottom-[50%] -left-[40%] w-[110%] h-[110%] rounded-full opacity-[0.45]"
|
{isActive && iframeUrl ? (
|
||||||
style={{ background: 'radial-gradient(circle, #2563eb 0%, transparent 70%)', filter: 'blur(140px)', animation: 'mesh-float-4 20s ease-in-out infinite' }} />
|
<iframe
|
||||||
<div className="absolute inset-0 opacity-[0.12] mix-blend-overlay" style={{ backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E")`, backgroundSize: '128px 128px' }} />
|
src={iframeUrl}
|
||||||
<div className="absolute inset-0 opacity-[0.06]" style={{ backgroundImage: 'repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.03) 2px, rgba(255,255,255,0.03) 4px)' }} />
|
name="record-mode-iframe"
|
||||||
</div>
|
className="w-full h-full border-0 block"
|
||||||
)}
|
style={{
|
||||||
|
backgroundColor: '#050505',
|
||||||
|
scrollbarWidth: 'none',
|
||||||
|
msOverflowStyle: 'none',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className={`transition-all duration-700 ease-in-out relative z-10 w-full ${isActive ? 'h-full max-h-[1000px] max-w-[1600px] drop-shadow-[0_60px_150px_rgba(0,0,0,1)] scale-in' : 'h-full'}`}
|
className={
|
||||||
style={{
|
isActive
|
||||||
transform: isPlaying ? `scale(${zoomLevel})` : undefined,
|
? 'blur-2xl opacity-20 pointer-events-none scale-95 transition-all duration-700'
|
||||||
transformOrigin: isPlaying ? `${cursorPosition.x}px ${cursorPosition.y}px` : 'center',
|
: 'transition-all duration-700'
|
||||||
filter: isBlurry ? 'blur(4px)' : 'none',
|
}
|
||||||
willChange: 'transform, filter',
|
|
||||||
WebkitBackfaceVisibility: 'hidden',
|
|
||||||
backfaceVisibility: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className={isActive ? 'relative h-full w-full rounded-[3rem] overflow-hidden bg-[#050505] isolate' : 'w-full h-full'}
|
{children}
|
||||||
style={{ transform: isActive ? 'translateZ(0)' : 'none' }}>
|
|
||||||
|
|
||||||
{isActive && (
|
|
||||||
<>
|
|
||||||
<div className="absolute inset-0 rounded-[3rem] border border-white/[0.08] pointer-events-none z-50" />
|
|
||||||
<div className="absolute inset-[-2px] rounded-[3rem] pointer-events-none z-20"
|
|
||||||
style={{ background: 'linear-gradient(135deg, rgba(16,185,129,0.15), rgba(130,237,32,0.15))', animation: 'pulse-ring 4s ease-in-out infinite' }} />
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-[#82ed20]/[0.05] to-transparent h-[15%] w-full top-[-15%] animate-scan-slow z-50 pointer-events-none opacity-20" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={isActive ? "w-full h-full rounded-[3rem] overflow-hidden relative" : "w-full h-full relative"}
|
|
||||||
style={{
|
|
||||||
WebkitMaskImage: isActive ? '-webkit-radial-gradient(white, black)' : 'none',
|
|
||||||
transform: isActive ? 'translateZ(0)' : 'none'
|
|
||||||
}}>
|
|
||||||
{isActive && iframeUrl ? (
|
|
||||||
<iframe
|
|
||||||
src={iframeUrl}
|
|
||||||
name="record-mode-iframe"
|
|
||||||
className="w-full h-full border-0 block"
|
|
||||||
style={{
|
|
||||||
backgroundColor: '#050505',
|
|
||||||
scrollbarWidth: 'none',
|
|
||||||
msOverflowStyle: 'none',
|
|
||||||
height: '100%',
|
|
||||||
width: '100%'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className={isActive ? 'blur-2xl opacity-20 pointer-events-none scale-95 transition-all duration-700' : 'transition-all duration-700'}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style jsx global>{`
|
<style
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
@keyframes mesh-float-1 { 0%, 100% { transform: translate(0, 0) scale(1) rotate(0deg); } 33% { transform: translate(15%, 10%) scale(1.1) rotate(5deg); } 66% { transform: translate(-10%, 20%) scale(0.9) rotate(-3deg); } }
|
@keyframes mesh-float-1 { 0%, 100% { transform: translate(0, 0) scale(1) rotate(0deg); } 33% { transform: translate(15%, 10%) scale(1.1) rotate(5deg); } 66% { transform: translate(-10%, 20%) scale(0.9) rotate(-3deg); } }
|
||||||
@keyframes mesh-float-2 { 0%, 100% { transform: translate(0, 0) scale(1) rotate(0deg); } 33% { transform: translate(-20%, -15%) scale(1.2) rotate(-8deg); } 66% { transform: translate(15%, -10%) scale(0.8) rotate(4deg); } }
|
@keyframes mesh-float-2 { 0%, 100% { transform: translate(0, 0) scale(1) rotate(0deg); } 33% { transform: translate(-20%, -15%) scale(1.2) rotate(-8deg); } 66% { transform: translate(15%, -10%) scale(0.8) rotate(4deg); } }
|
||||||
@keyframes mesh-float-3 { 0%, 100% { transform: translate(0, 0) scale(1.2); } 50% { transform: translate(20%, -25%) scale(0.7); } }
|
@keyframes mesh-float-3 { 0%, 100% { transform: translate(0, 0) scale(1.2); } 50% { transform: translate(20%, -25%) scale(0.7); } }
|
||||||
@@ -182,8 +252,10 @@ export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
|
|||||||
@keyframes scan-slow { 0% { transform: translateY(-100%); opacity: 0; } 5% { opacity: 0.2; } 95% { opacity: 0.2; } 100% { transform: translateY(800%); opacity: 0; } }
|
@keyframes scan-slow { 0% { transform: translateY(-100%); opacity: 0; } 5% { opacity: 0.2; } 95% { opacity: 0.2; } 100% { transform: translateY(800%); opacity: 0; } }
|
||||||
@keyframes scale-in { 0% { transform: scale(0.95); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
|
@keyframes scale-in { 0% { transform: scale(0.95); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
|
||||||
.scale-in { animation: scale-in 0.7s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
|
.scale-in { animation: scale-in 0.7s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
|
||||||
`}</style>
|
`,
|
||||||
</div>
|
}}
|
||||||
</>
|
/>
|
||||||
);
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,107 +1,127 @@
|
|||||||
import React, { useMemo } from 'react';
|
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';
|
import { RecordingSession, RecordEvent } from '../types/record-mode';
|
||||||
|
|
||||||
export const WebsiteVideo: React.FC<{
|
export const WebsiteVideo: React.FC<{
|
||||||
session: RecordingSession | null;
|
session: RecordingSession | null;
|
||||||
siteUrl: string;
|
siteUrl: string;
|
||||||
}> = ({ session, siteUrl }) => {
|
}> = ({ session, siteUrl }) => {
|
||||||
const { fps, width, height, durationInFrames } = useVideoConfig();
|
const { fps, width, height, durationInFrames } = useVideoConfig();
|
||||||
const frame = useCurrentFrame();
|
const frame = useCurrentFrame();
|
||||||
|
|
||||||
if (!session || !session.events.length) {
|
const sortedEvents = useMemo(() => {
|
||||||
return (
|
if (!session) return [];
|
||||||
<AbsoluteFill style={{ backgroundColor: 'black', color: 'white', justifyContent: 'center', alignItems: 'center' }}>
|
return [...session.events].sort((a, b) => a.timestamp - b.timestamp);
|
||||||
No session data found.
|
}, [session]);
|
||||||
</AbsoluteFill>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
|
if (!session || !session.events.length) {
|
||||||
return (
|
return (
|
||||||
<AbsoluteFill style={{ backgroundColor: '#000' }}>
|
<AbsoluteFill
|
||||||
<div style={{
|
style={{
|
||||||
width: '100%',
|
backgroundColor: 'black',
|
||||||
height: '100%',
|
color: 'white',
|
||||||
position: 'relative',
|
justifyContent: 'center',
|
||||||
transform: `scale(${zoom})`,
|
alignItems: 'center',
|
||||||
transformOrigin: `${cursorX}px ${cursorY}px`,
|
}}
|
||||||
filter: isBlurry ? 'blur(8px)' : 'none',
|
>
|
||||||
transition: 'filter 0.1s ease-out'
|
No session data found.
|
||||||
}}>
|
</AbsoluteFill>
|
||||||
<iframe
|
|
||||||
src={siteUrl}
|
|
||||||
style={{ width: '100%', height: '100%', border: 'none' }}
|
|
||||||
title="Website"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Visual Cursor */}
|
|
||||||
<div style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: cursorX,
|
|
||||||
top: cursorY,
|
|
||||||
width: 34, height: 34,
|
|
||||||
backgroundColor: 'white',
|
|
||||||
borderRadius: '50%',
|
|
||||||
border: '3px solid black',
|
|
||||||
boxShadow: '0 4px 15px rgba(0,0,0,0.4)',
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
zIndex: 100
|
|
||||||
}}>
|
|
||||||
<div style={{ width: 12, height: 12, backgroundColor: '#3b82f6', borderRadius: '50%' }} />
|
|
||||||
</div>
|
|
||||||
</AbsoluteFill>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<AbsoluteFill style={{ backgroundColor: '#000' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
transform: `scale(${zoom})`,
|
||||||
|
transformOrigin: `${cursorX}px ${cursorY}px`,
|
||||||
|
filter: isBlurry ? 'blur(8px)' : 'none',
|
||||||
|
transition: 'filter 0.1s ease-out',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
src={siteUrl}
|
||||||
|
style={{ width: '100%', height: '100%', border: 'none' }}
|
||||||
|
title="Website"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Visual Cursor */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: cursorX,
|
||||||
|
top: cursorY,
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: '50%',
|
||||||
|
border: '3px solid black',
|
||||||
|
boxShadow: '0 4px 15px rgba(0,0,0,0.4)',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 100,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ width: 12, height: 12, backgroundColor: '#3b82f6', borderRadius: '50%' }} />
|
||||||
|
</div>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user