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
This commit is contained in:
2026-02-16 18:50:34 +01:00
parent a8b8d703c8
commit 4b41ba1c27
6 changed files with 349 additions and 243 deletions

View File

@@ -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';

View 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>
);
}

View File

@@ -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(() => {
async function load() {
const products = await getAllProducts(locale); const products = await getAllProducts(locale);
const translations = await getTranslations('Products'); const t = 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>

View File

@@ -12,7 +12,8 @@ export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
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 =
window.location.search.includes('embedded=true') || window.name === 'record-mode-iframe';
setIsEmbedded(embedded); setIsEmbedded(embedded);
if (!embedded) { if (!embedded) {
@@ -30,7 +31,8 @@ export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
if (isEmbedded) { if (isEmbedded) {
return ( return (
<> <>
<style dangerouslySetInnerHTML={{ <style
dangerouslySetInnerHTML={{
__html: ` __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,
@@ -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,7 +80,9 @@ 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}
</> </>
); );
@@ -88,7 +92,8 @@ export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
<> <>
{/* Global Style for Body Lock */} {/* Global Style for Body Lock */}
{isActive && ( {isActive && (
<style dangerouslySetInnerHTML={{ <style
dangerouslySetInnerHTML={{
__html: ` __html: `
html, body { html, body {
overflow: hidden !important; overflow: hidden !important;
@@ -102,24 +107,64 @@ 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'}`}> <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 */} {/* Studio Background - Only visible when active */}
{isActive && ( {isActive && (
<div className="absolute inset-0 z-0 pointer-events-none overflow-hidden"> <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 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]" <div
style={{ background: 'radial-gradient(circle, #10b981 0%, transparent 70%)', filter: 'blur(160px)', animation: 'mesh-float-1 18s ease-in-out infinite' }} /> className="absolute -top-[60%] -left-[50%] w-[140%] h-[140%] rounded-full opacity-[0.7]"
<div className="absolute -bottom-[60%] -right-[50%] w-[130%] h-[130%] rounded-full opacity-[0.55]" style={{
style={{ background: 'radial-gradient(circle, #06b6d4 0%, transparent 70%)', filter: 'blur(150px)', animation: 'mesh-float-2 22s ease-in-out infinite' }} /> background: 'radial-gradient(circle, #10b981 0%, transparent 70%)',
<div className="absolute -top-[30%] -right-[40%] w-[100%] h-[100%] rounded-full opacity-[0.5]" filter: 'blur(160px)',
style={{ background: 'radial-gradient(circle, #82ed20 0%, transparent 70%)', filter: 'blur(130px)', animation: 'mesh-float-3 14s ease-in-out infinite' }} /> animation: 'mesh-float-1 18s 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
<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)' }} /> 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>
)} )}
@@ -134,23 +179,40 @@ export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
backfaceVisibility: 'hidden', backfaceVisibility: 'hidden',
}} }}
> >
<div className={isActive ? 'relative h-full w-full rounded-[3rem] overflow-hidden bg-[#050505] isolate' : 'w-full h-full'} <div
style={{ transform: isActive ? 'translateZ(0)' : 'none' }}> 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 && ( {isActive && (
<> <>
<div className="absolute inset-0 rounded-[3rem] border border-white/[0.08] pointer-events-none z-50" /> <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" <div
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' }} /> 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="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"} <div
className={
isActive
? 'w-full h-full rounded-[3rem] overflow-hidden relative'
: 'w-full h-full relative'
}
style={{ style={{
WebkitMaskImage: isActive ? '-webkit-radial-gradient(white, black)' : 'none', WebkitMaskImage: isActive ? '-webkit-radial-gradient(white, black)' : 'none',
transform: isActive ? 'translateZ(0)' : 'none' transform: isActive ? 'translateZ(0)' : 'none',
}}> }}
>
{isActive && iframeUrl ? ( {isActive && iframeUrl ? (
<iframe <iframe
src={iframeUrl} src={iframeUrl}
@@ -161,11 +223,17 @@ export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
scrollbarWidth: 'none', scrollbarWidth: 'none',
msOverflowStyle: 'none', msOverflowStyle: 'none',
height: '100%', height: '100%',
width: '100%' width: '100%',
}} }}
/> />
) : ( ) : (
<div className={isActive ? 'blur-2xl opacity-20 pointer-events-none scale-95 transition-all duration-700' : 'transition-all duration-700'}> <div
className={
isActive
? 'blur-2xl opacity-20 pointer-events-none scale-95 transition-all duration-700'
: 'transition-all duration-700'
}
>
{children} {children}
</div> </div>
)} )}
@@ -173,7 +241,9 @@ export function RecordModeVisuals({ children }: { children: React.ReactNode }) {
</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,7 +252,9 @@ 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
View File

@@ -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.

View File

@@ -1,5 +1,12 @@
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<{
@@ -9,24 +16,32 @@ export const WebsiteVideo: React.FC<{
const { fps, width, height, durationInFrames } = useVideoConfig(); const { fps, width, height, durationInFrames } = useVideoConfig();
const frame = useCurrentFrame(); const frame = useCurrentFrame();
const sortedEvents = useMemo(() => {
if (!session) return [];
return [...session.events].sort((a, b) => a.timestamp - b.timestamp);
}, [session]);
if (!session || !session.events.length) { if (!session || !session.events.length) {
return ( return (
<AbsoluteFill style={{ backgroundColor: 'black', color: 'white', justifyContent: 'center', alignItems: 'center' }}> <AbsoluteFill
style={{
backgroundColor: 'black',
color: 'white',
justifyContent: 'center',
alignItems: 'center',
}}
>
No session data found. No session data found.
</AbsoluteFill> </AbsoluteFill>
); );
} }
const sortedEvents = useMemo(() => {
return [...session.events].sort((a, b) => a.timestamp - b.timestamp);
}, [session]);
const elapsedTimeMs = (frame / fps) * 1000; const elapsedTimeMs = (frame / fps) * 1000;
// --- Interpolation Logic --- // --- Interpolation Logic ---
// 1. Find the current window (between which two events are we?) // 1. Find the current window (between which two events are we?)
const nextEventIndex = sortedEvents.findIndex(e => e.timestamp > elapsedTimeMs); const nextEventIndex = sortedEvents.findIndex((e) => e.timestamp > elapsedTimeMs);
let currentEventIndex; let currentEventIndex;
if (nextEventIndex === -1) { if (nextEventIndex === -1) {
@@ -38,7 +53,7 @@ export const WebsiteVideo: React.FC<{
const currentEvent = sortedEvents[currentEventIndex]; const currentEvent = sortedEvents[currentEventIndex];
// If there is no next event, we just stay at current (next=current) // If there is no next event, we just stay at current (next=current)
const nextEvent = (nextEventIndex !== -1) ? sortedEvents[nextEventIndex] : currentEvent; const nextEvent = nextEventIndex !== -1 ? sortedEvents[nextEventIndex] : currentEvent;
// 2. Calculate Progress between events // 2. Calculate Progress between events
const gap = nextEvent.timestamp - currentEvent.timestamp; const gap = nextEvent.timestamp - currentEvent.timestamp;
@@ -50,7 +65,7 @@ export const WebsiteVideo: React.FC<{
if (event.rect) { if (event.rect) {
return { return {
x: event.rect.x + event.rect.width / 2, x: event.rect.x + event.rect.width / 2,
y: event.rect.y + event.rect.height / 2 y: event.rect.y + event.rect.height / 2,
}; };
} }
return { x: width / 2, y: height / 2 }; return { x: width / 2, y: height / 2 };
@@ -68,15 +83,17 @@ export const WebsiteVideo: React.FC<{
return ( return (
<AbsoluteFill style={{ backgroundColor: '#000' }}> <AbsoluteFill style={{ backgroundColor: '#000' }}>
<div style={{ <div
style={{
width: '100%', width: '100%',
height: '100%', height: '100%',
position: 'relative', position: 'relative',
transform: `scale(${zoom})`, transform: `scale(${zoom})`,
transformOrigin: `${cursorX}px ${cursorY}px`, transformOrigin: `${cursorX}px ${cursorY}px`,
filter: isBlurry ? 'blur(8px)' : 'none', filter: isBlurry ? 'blur(8px)' : 'none',
transition: 'filter 0.1s ease-out' transition: 'filter 0.1s ease-out',
}}> }}
>
<iframe <iframe
src={siteUrl} src={siteUrl}
style={{ width: '100%', height: '100%', border: 'none' }} style={{ width: '100%', height: '100%', border: 'none' }}
@@ -85,11 +102,13 @@ export const WebsiteVideo: React.FC<{
</div> </div>
{/* Visual Cursor */} {/* Visual Cursor */}
<div style={{ <div
style={{
position: 'absolute', position: 'absolute',
left: cursorX, left: cursorX,
top: cursorY, top: cursorY,
width: 34, height: 34, width: 34,
height: 34,
backgroundColor: 'white', backgroundColor: 'white',
borderRadius: '50%', borderRadius: '50%',
border: '3px solid black', border: '3px solid black',
@@ -98,8 +117,9 @@ export const WebsiteVideo: React.FC<{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
zIndex: 100 zIndex: 100,
}}> }}
>
<div style={{ width: 12, height: 12, backgroundColor: '#3b82f6', borderRadius: '50%' }} /> <div style={{ width: 12, height: 12, backgroundColor: '#3b82f6', borderRadius: '50%' }} />
</div> </div>
</AbsoluteFill> </AbsoluteFill>