Files
mintel.me/apps/web/src/components/IframeSection.tsx
Marc Mintel 6864903cff
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 2m24s
Build & Deploy / 🏗️ Build (push) Failing after 3m40s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
fix(web): remove redundant prop-types and unblock lint pipeline
2026-02-24 11:38:43 +01:00

558 lines
20 KiB
TypeScript

"use client";
import * as React from "react";
import { cn } from "../utils/cn";
import { ShieldCheck, ArrowLeft, ArrowRight, RefreshCw } from "lucide-react";
import { MonoLabel, Label } from "./Typography";
import { motion, AnimatePresence } from "framer-motion";
interface IframeSectionProps {
src: string;
title?: string;
description?: string;
height?: string;
className?: string;
zoom?: number;
offsetY?: number;
clipHeight?: number;
browserFrame?: boolean;
allowScroll?: boolean;
desktopWidth?: number;
minimal?: boolean;
perspective?: boolean;
rotate?: number;
delay?: number;
noScale?: boolean;
dynamicGlow?: boolean;
minHeight?: number;
mobileWidth?: number;
mobileHeight?: string;
desktopHeight?: string;
}
/**
* Reusable Browser UI components to maintain consistency
*/
const BrowserChromeComponent: React.FC<{ url: string; minimal?: boolean }> = ({
url,
minimal,
}) => {
if (minimal) return null;
return (
<div
className="h-12 md:h-14 bg-slate-50 border-b border-slate-200/60 flex items-center px-4 md:px-6 gap-4 md:gap-8 z-[100] flex-shrink-0 relative isolation-auto"
style={{ isolation: "isolate", minHeight: "48px" }}
>
{/* 3D Rim Highlight */}
<div className="absolute top-0 left-0 right-0 h-[1px] bg-white opacity-80" />
{/* Status Indicators (Traffic Lights) - Enhanced with subtle depth */}
<div className="flex gap-1.5 md:gap-2.5">
{[
"bg-slate-300 from-slate-200 to-slate-400",
"bg-slate-300 from-slate-200 to-slate-400",
"bg-slate-300 from-slate-200 to-slate-400",
].map((cls, i) => (
<div
key={i}
className={cn(
"w-2.5 h-2.5 md:w-3 md:h-3 rounded-full bg-gradient-to-br shadow-[inset_0_1px_2px_rgba(0,0,0,0.1),0_1.5px_1px_rgba(255,255,255,0.7)] relative flex items-center justify-center",
cls,
)}
>
{/* Subtle glint, soft blur */}
<div className="absolute top-[20%] left-[20%] w-[30%] h-[30%] rounded-full bg-white/30 blur-[0.7px]" />
</div>
))}
</div>
{/* Navigation Controls - Hidden on mobile */}
<div className="hidden lg:flex items-center gap-4 opacity-30">
<ArrowLeft className="w-3.5 h-3.5" />
<ArrowRight className="w-3.5 h-3.5" />
<RefreshCw className="w-3 h-3 ml-1" />
</div>
{/* URL Bar - Solid high-fidelity layer instead of expensive backdrop-blur */}
<div className="flex-1 max-w-[700px] mx-auto bg-white shadow-[inset_0_1px_3px_rgba(0,0,0,0.06),0_0_0_1px_rgba(255,255,255,0.4)] rounded-xl flex items-center justify-between px-3 md:px-6 h-8 md:h-9 border border-slate-200/40 group">
<div className="flex items-center gap-2 md:gap-3 opacity-60 group-hover:opacity-100 transition-opacity duration-700 overflow-hidden">
<ShieldCheck className="w-3 md:w-3.5 h-3 md:h-3.5 text-blue-500 shrink-0" />
<span className="text-[9px] md:text-[10px] font-mono font-bold tracking-[0.1em] md:tracking-[0.2em] uppercase truncate whitespace-nowrap text-slate-900 md:border-r border-slate-200 md:pr-4 md:mr-1">
{url.replace("varnish-cache://", "")}
</span>
<span className="text-[8px] font-mono text-slate-400 tracking-widest hidden xl:block">
VARNISH_TUNNEL // ALPHA_NODE_04
</span>
</div>
<div className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse hidden sm:block shadow-[0_0_8px_rgba(74,222,128,0.4)]" />
</div>
{/* Industrial Accent / Technical ID - Hidden on mobile */}
<div className="hidden md:flex flex-col items-end opacity-20 pointer-events-none select-none">
<div className="text-[6px] font-mono font-bold tracking-widest uppercase mb-0.5">
SECURE_LINK // ID: VARNISH_4X
</div>
<div className="w-12 h-0.5 bg-slate-900 rounded-full" />
</div>
</div>
);
};
const BrowserChrome = React.memo(BrowserChromeComponent);
BrowserChrome.displayName = "BrowserChrome";
export const IframeSection: React.FC<IframeSectionProps> = ({
src,
title,
description,
height = "500px",
className,
zoom,
offsetY = 0,
clipHeight,
browserFrame = false,
allowScroll = false,
desktopWidth = 1200,
minimal = false,
perspective = false,
rotate: _rotate = 0,
delay: _delay = 0,
noScale = false,
dynamicGlow = true,
minHeight = 400,
mobileWidth = 390,
mobileHeight,
desktopHeight,
}) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const iframeRef = React.useRef<HTMLIFrameElement>(null);
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const [scale, setScale] = React.useState(1);
const [activeInternalWidth, setActiveInternalWidth] =
React.useState(desktopWidth);
const [isLoading, setIsLoading] = React.useState(true);
const [isIframeLoaded, setIsIframeLoaded] = React.useState(false);
const [glowColors, setGlowColors] = React.useState<string[]>([
"rgba(148, 163, 184, 0.1)",
"rgba(148, 163, 184, 0.1)",
"rgba(148, 163, 184, 0.1)",
"rgba(148, 163, 184, 0.1)",
]);
const [scrollState, setScrollState] = React.useState({
atTop: true,
atBottom: false,
isScrollable: false,
});
const [headerHeightPx, setHeaderHeightPx] = React.useState(0);
// Responsive Header Height Calculation
React.useEffect(() => {
if (browserFrame && !minimal) {
const updateHeaderHeight = () => {
setHeaderHeightPx(window.innerWidth < 768 ? 48 : 56);
};
updateHeaderHeight();
window.addEventListener("resize", updateHeaderHeight);
return () => window.removeEventListener("resize", updateHeaderHeight);
} else {
setHeaderHeightPx(0);
}
}, [browserFrame, minimal]);
// Scaling & Adaptive Viewport Logic
React.useEffect(() => {
if (!containerRef.current || noScale) {
setScale(1);
setActiveInternalWidth(desktopWidth);
return;
}
const updateDimensions = () => {
if (containerRef.current) {
const containerWidth = containerRef.current.offsetWidth;
if (containerWidth > 0) {
// Adaptive threshold: Switch to mobile width if container is small
const useMobile = containerWidth < 500;
const internalWidth = useMobile ? mobileWidth : desktopWidth;
setActiveInternalWidth(internalWidth);
// Calculate scale based on the active target width
const newScale = zoom || containerWidth / internalWidth;
setScale(newScale);
}
}
};
updateDimensions();
const observer = new ResizeObserver(updateDimensions);
observer.observe(containerRef.current);
return () => observer.disconnect();
}, [desktopWidth, mobileWidth, zoom, noScale]);
const updateScrollState = React.useCallback(() => {
try {
const doc = iframeRef.current?.contentDocument?.documentElement;
if (doc) {
const atTop = doc.scrollTop <= 5;
const atBottom =
doc.scrollTop + doc.clientHeight >= doc.scrollHeight - 5;
const isScrollable = doc.scrollHeight > doc.clientHeight + 10;
setScrollState({ atTop, atBottom, isScrollable });
}
} catch (_e) {}
}, []);
// Ambilight effect (sampled from iframe if same-origin)
const updateAmbilight = React.useCallback(() => {
if (!dynamicGlow || !iframeRef.current || !canvasRef.current) return;
try {
const iframe = iframeRef.current;
const doc = iframe.contentDocument;
if (!doc) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d", { willReadFrequently: true });
if (!ctx) return;
canvas.width = 100;
canvas.height = 100;
const body = doc.body;
const computedStyle = window.getComputedStyle(body);
const bgColor = computedStyle.backgroundColor || "rgba(255,255,255,1)";
const sampleX = (x: number, y: number) => {
const el = doc.elementFromPoint(x, y);
if (el) return window.getComputedStyle(el).backgroundColor;
return bgColor;
};
const w = doc.documentElement.scrollWidth || iframe.offsetWidth;
const h = doc.documentElement.scrollHeight || iframe.offsetHeight;
const sampleMargin = 20;
const colors = [
sampleX(w / 2, sampleMargin + offsetY),
sampleX(w - sampleMargin, h / 2 + offsetY),
sampleX(w / 2, h - sampleMargin + offsetY),
sampleX(sampleMargin, h / 2 + offsetY),
];
setGlowColors(
colors.map((c) => {
if (!c || c === "transparent") return "rgba(148, 163, 184, 0.1)";
return c.replace("rgb(", "rgba(").replace(")", ", 0.5)");
}),
);
updateScrollState();
} catch (_e) {}
}, [dynamicGlow, offsetY, updateScrollState]);
// Height parse helper
const parseNumericHeight = (h: string | number) => {
if (typeof h === "number") return h;
const match = h.match(/^(\d+(?:\.\d+)?)(px)$/);
return match ? parseFloat(match[1]) : null;
};
const [isMobile, setIsMobile] = React.useState(false);
React.useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);
const activeHeight = isMobile
? mobileHeight || height
: desktopHeight || height;
const baseNumericHeight = parseNumericHeight(activeHeight);
const finalScaledHeight = clipHeight
? clipHeight * scale
: baseNumericHeight
? baseNumericHeight * scale
: null;
const chassisStyle = {
height:
activeHeight === "100%"
? "100%"
: finalScaledHeight
? `${finalScaledHeight + headerHeightPx}px`
: `calc(${activeHeight} + ${headerHeightPx}px)`,
minHeight: minHeight ? `${minHeight}px` : undefined,
};
const [loadingPhase, setLoadingPhase] = React.useState(0);
const loadingPhases = [
"DETERMINING ROUTE",
"INITIALIZING HANDSHAKE",
"ESTABLISHING ENCRYPTED LINK",
"SYNCING ASSETS",
"FINALIZING...",
];
// 1. Safety Trigger: Force-stop loading after 2.5s no matter what
React.useEffect(() => {
if (!isLoading) return;
const timer = setTimeout(() => {
setIsLoading(false);
}, 2500);
return () => clearTimeout(timer);
}, [isLoading]);
// 2. Sync Trigger: Cleanup when BOTH phases and iframe load complete
React.useEffect(() => {
if (!isLoading) return;
// Early exit: if iframe is already loaded, we only need the first 2 "handshake" phases
const phasesRequired = isIframeLoaded
? loadingPhases.length - 2
: loadingPhases.length - 1;
if (loadingPhase >= phasesRequired) {
const timer = setTimeout(() => {
setIsLoading(false);
}, 300);
return () => clearTimeout(timer);
}
}, [isLoading, isIframeLoaded, loadingPhase, loadingPhases.length]);
// 3. Phase Incrementer (Faster: 400ms)
React.useEffect(() => {
if (!isLoading) return;
const interval = setInterval(() => {
setLoadingPhase((prev) => {
if (prev < loadingPhases.length - 1) return prev + 1;
clearInterval(interval);
return prev;
});
}, 400);
return () => clearInterval(interval);
}, [isLoading, loadingPhases.length]);
return (
<div
className={cn(
"w-full group relative",
!minimal && "space-y-6",
className,
)}
style={className?.includes("h-full") ? { height: "100%" } : {}}
>
<canvas ref={canvasRef} className="hidden" aria-hidden="true" />
{!minimal && (title || description) && (
<div className="space-y-2 px-1">
{title && (
<h4 className="text-2xl font-bold text-slate-900 tracking-tight leading-none">
{title}
</h4>
)}
{description && (
<p className="text-slate-400 text-sm font-medium">{description}</p>
)}
</div>
)}
{/* Main Device Chassis */}
<div
ref={containerRef}
className={cn(
"w-full relative flex flex-col z-10",
minimal ? "bg-transparent" : "bg-slate-50",
!minimal &&
"rounded-[2.5rem] border border-slate-200/50 shadow-[0_80px_160px_-40px_rgba(0,0,0,0.18),0_0_1px_rgba(0,0,0,0.1)]",
perspective &&
"hover:scale-[1.03] hover:-translate-y-3 transition-[transform,shadow,scale] duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)]",
"overflow-hidden",
)}
style={chassisStyle}
>
{/* AMBILIGHT DYNAMIC GLOW */}
{dynamicGlow && (
<div className="absolute -inset-[30%] blur-[140px] opacity-30 group-hover:opacity-90 transition-all duration-[2000ms] pointer-events-none z-0">
<div
className="absolute inset-0 rounded-[6rem]"
style={{
background: `
radial-gradient(circle at 50% 10%, ${glowColors[0]} 0%, transparent 60%),
radial-gradient(circle at 95% 50%, ${glowColors[1]} 0%, transparent 60%),
radial-gradient(circle at 50% 90%, ${glowColors[2]} 0%, transparent 60%),
radial-gradient(circle at 5% 50%, ${glowColors[3]} 0%, transparent 60%)
`,
filter: "saturate(2.2) brightness(1.1)",
}}
/>
</div>
)}
{/* Browser Frame */}
{browserFrame && (
<BrowserChrome
url="varnish-cache://secure.klz-cables.com"
minimal={minimal}
/>
)}
{/* Scaled Viewport Container - Solid bg prevents gray flicker */}
<div className="flex-1 relative overflow-hidden bg-white">
{/* Artificial Loader Overlay */}
<AnimatePresence>
{isLoading && (
<motion.div
initial={{ opacity: 1 }}
exit={{
opacity: 0,
transition: { duration: 0.6, ease: [0.23, 1, 0.32, 1] },
}}
className="absolute inset-0 flex items-center justify-center bg-white/95 backdrop-blur-3xl z-50 pointer-events-none transform-gpu"
>
{/* Static Binary Matrix - Stop redundant re-paints */}
<div
className="absolute inset-0 opacity-[0.03] select-none pointer-events-none font-mono text-[8px] p-4 flex flex-col gap-1 overflow-hidden"
aria-hidden="true"
>
{Array.from({ length: 24 }).map((_, i) => (
<div
key={i}
className="whitespace-nowrap tracking-[0.5em] opacity-50"
>
{"01011010010110101101011010101011010101011010101101010101101010"
.split("")
.join(" ")}
</div>
))}
</div>
<div className="flex flex-col items-center gap-6 relative z-10 w-full max-w-xs px-8">
<div className="w-full h-1 bg-slate-100 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{
width: `${((loadingPhase + 1) / loadingPhases.length) * 100}%`,
}}
className="h-full bg-slate-900"
/>
</div>
<div className="flex flex-col items-center gap-2">
<MonoLabel className="text-[10px] text-slate-900 font-bold uppercase tracking-[0.3em] flex items-center gap-2">
<span className="w-1 h-1 rounded-full bg-blue-500 animate-pulse" />
{loadingPhases[loadingPhase]}
</MonoLabel>
<Label className="text-[8px] text-slate-400 font-mono uppercase tracking-widest">
Varnish_Tunnel // Alpha_Node
</Label>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div
className={cn(
"transition-all duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)] origin-top-left absolute left-0 right-0 transform-gpu",
noScale && "relative w-full h-full",
)}
style={{
width: noScale ? "100%" : `${activeInternalWidth}px`,
transform: noScale ? "none" : `scale(${scale})`,
height: noScale ? "100%" : `${100 / scale}%`,
willChange: "opacity",
}}
>
<motion.div
initial={false}
animate={{
opacity: isLoading ? 0 : 1,
}}
transition={{
duration: 0.6,
ease: [0.23, 1, 0.32, 1],
}}
className="w-full h-full relative z-0"
>
<iframe
ref={iframeRef}
src={src}
scrolling={allowScroll ? "yes" : "no"}
className="w-full h-full border-none no-scrollbar"
onLoad={(e) => {
setIsIframeLoaded(true);
try {
const iframe = e.currentTarget;
if (iframe.contentDocument) {
const style =
iframe.contentDocument.createElement("style");
style.textContent = `
*::-webkit-scrollbar { display: none !important; }
* { -ms-overflow-style: none !important; scrollbar-width: none !important; }
body { background: transparent !important; }
`;
iframe.contentDocument.head.appendChild(style);
setTimeout(updateAmbilight, 600);
const onScroll = () => {
requestAnimationFrame(updateAmbilight);
updateScrollState();
};
iframe.contentWindow?.addEventListener(
"scroll",
onScroll,
{
passive: true,
},
);
}
} catch (_e) {
// ignore
}
}}
style={{
transform: `translateY(-${offsetY}px)`,
height: `calc(100% + ${offsetY}px)`,
pointerEvents: allowScroll ? "auto" : "none",
width: "calc(100% + 20px)", // Bleed for seamless edge
marginLeft: "-10px",
}}
title={title || "Project Display"}
/>
</motion.div>
</div>
{/* Custom Industrial Scroll Indicator */}
{allowScroll && scrollState.isScrollable && (
<div className="absolute right-4 top-1/2 -translate-y-1/2 w-1 h-32 bg-slate-200/20 rounded-full z-20 backdrop-blur-sm">
<div
className="w-full bg-slate-900 rounded-full transition-all duration-150 ease-out shadow-[0_0_12px_rgba(15,23,42,0.1)]"
style={{
height: "30px",
transform: `translateY(${(() => {
try {
const doc =
iframeRef.current?.contentDocument?.documentElement;
if (!doc) return 0;
const scrollPct =
doc.scrollTop / (doc.scrollHeight - doc.clientHeight);
return scrollPct * (128 - 30);
} catch (e) {
return 0;
}
})()}px)`,
}}
/>
</div>
)}
</div>
</div>
</div>
);
};