feat: Add static assets and content for the KLZ Cables showcase.
This commit is contained in:
@@ -37,29 +37,33 @@ export const Header: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
|
||||
<nav className="flex items-center gap-8">
|
||||
<Link
|
||||
href="/about"
|
||||
className={`text-xs font-bold uppercase tracking-widest transition-colors ${
|
||||
isActive('/about') ? 'text-slate-900' : 'text-slate-400 hover:text-slate-900'
|
||||
}`}
|
||||
className={`text-xs font-bold uppercase tracking-widest transition-colors ${isActive('/about') ? 'text-slate-900' : 'text-slate-400 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
Über mich
|
||||
</Link>
|
||||
<Link
|
||||
href="/websites"
|
||||
className={`text-xs font-bold uppercase tracking-widest transition-colors ${
|
||||
isActive('/websites') ? 'text-slate-900' : 'text-slate-400 hover:text-slate-900'
|
||||
}`}
|
||||
className={`text-xs font-bold uppercase tracking-widest transition-colors ${isActive('/websites') ? 'text-slate-900' : 'text-slate-400 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
Websites
|
||||
</Link>
|
||||
<Link
|
||||
href="/case-studies"
|
||||
className={`text-xs font-bold uppercase tracking-widest transition-colors ${isActive('/case-studies') || pathname?.startsWith('/case-studies/') ? 'text-slate-900' : 'text-slate-400 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
Case Studies
|
||||
</Link>
|
||||
<Link
|
||||
href="/blog"
|
||||
className={`text-xs font-bold uppercase tracking-widest transition-colors ${
|
||||
isActive('/blog') || pathname?.startsWith('/blog/') ? 'text-slate-900' : 'text-slate-400 hover:text-slate-900'
|
||||
}`}
|
||||
className={`text-xs font-bold uppercase tracking-widest transition-colors ${isActive('/blog') || pathname?.startsWith('/blog/') ? 'text-slate-900' : 'text-slate-400 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
Blog
|
||||
</Link>
|
||||
|
||||
166
src/components/IframeSection.tsx
Normal file
166
src/components/IframeSection.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { cn } from '../utils/cn';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 = 0,
|
||||
delay = 0,
|
||||
noScale = false
|
||||
}) => {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [scale, setScale] = React.useState(1);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!containerRef.current || noScale) {
|
||||
setScale(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateScale = () => {
|
||||
if (containerRef.current) {
|
||||
const currentWidth = containerRef.current.offsetWidth;
|
||||
if (currentWidth > 0) {
|
||||
const newScale = zoom || (currentWidth / desktopWidth);
|
||||
setScale(newScale);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateScale();
|
||||
const observer = new ResizeObserver(updateScale);
|
||||
observer.observe(containerRef.current);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [desktopWidth, zoom, noScale]);
|
||||
|
||||
const containerStyle = clipHeight
|
||||
? { height: `${clipHeight * (noScale ? 1 : scale)}px` }
|
||||
: { height };
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("w-full group", !minimal && "space-y-6", className)}
|
||||
style={{
|
||||
opacity: 0,
|
||||
animation: `fadeIn 0.8s ease-out ${delay}s forwards`
|
||||
}}
|
||||
>
|
||||
{!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>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
"w-full relative overflow-hidden transition-all duration-700 ease-[cubic-bezier(0.23,1,0.32,1)]",
|
||||
minimal ? "bg-transparent" : "bg-slate-100",
|
||||
!minimal && (browserFrame ? "rounded-t-3xl border-t border-x border-slate-200 shadow-[0_32px_128px_-16px_rgba(0,0,0,0.1)]" : "rounded-3xl border border-slate-200/60 shadow-xl"),
|
||||
perspective && "hover:scale-[1.01] hover:-translate-y-1",
|
||||
noScale && "overflow-x-auto"
|
||||
)}
|
||||
style={{
|
||||
...containerStyle,
|
||||
perspective: perspective ? '1000px' : 'none',
|
||||
transform: rotate ? `rotateY(${rotate}deg)` : 'none',
|
||||
}}
|
||||
>
|
||||
{/* Loader Overlay */}
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-slate-50 z-30 transition-opacity duration-300">
|
||||
<div className="w-8 h-8 border-2 border-slate-200 border-t-slate-900 rounded-full animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{browserFrame && !minimal && (
|
||||
<div className="absolute top-0 left-0 right-0 h-12 bg-slate-50/80 backdrop-blur-md border-b border-slate-200 flex items-center px-6 gap-3 z-10">
|
||||
<div className="flex gap-2">
|
||||
<div className="w-3.5 h-3.5 rounded-full bg-slate-200 group-hover:bg-red-200 transition-colors" />
|
||||
<div className="w-3.5 h-3.5 rounded-full bg-slate-200 group-hover:bg-amber-200 transition-colors" />
|
||||
<div className="w-3.5 h-3.5 rounded-full bg-slate-200 group-hover:bg-green-200 transition-colors" />
|
||||
</div>
|
||||
<div className="flex-1 max-w-md mx-auto bg-white/50 rounded-xl flex items-center justify-center px-4 h-7 border border-slate-200/50">
|
||||
<span className="text-[11px] text-slate-400 font-bold tracking-tight truncate whitespace-nowrap">
|
||||
https://klz-cables.com
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-16" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"transition-transform duration-700",
|
||||
(browserFrame && !minimal) ? "mt-12" : "mt-0",
|
||||
noScale ? "static" : "origin-top-left absolute left-0 right-0 h-full"
|
||||
)}
|
||||
style={{
|
||||
width: noScale ? '100%' : `${desktopWidth}px`,
|
||||
transform: noScale ? 'none' : `scale(${scale})`,
|
||||
height: noScale ? (clipHeight ? `${clipHeight}px` : '100%') : (clipHeight ? `${clipHeight + Math.abs(offsetY)}px` : `${100 / scale}%`),
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
src={src}
|
||||
scrolling={allowScroll ? "yes" : "no"}
|
||||
className={cn(
|
||||
"w-full h-full border-none transition-opacity duration-500",
|
||||
isLoading ? "opacity-0" : "opacity-100"
|
||||
)}
|
||||
onLoad={() => setIsLoading(false)}
|
||||
style={{
|
||||
transform: `translateY(-${offsetY}px)`,
|
||||
pointerEvents: allowScroll ? 'auto' : 'none',
|
||||
}}
|
||||
title={title || "Interactive Project Preview"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!allowScroll && !minimal && <div className="absolute inset-0 pointer-events-auto cursor-default z-20" />}
|
||||
</div>
|
||||
|
||||
<style jsx global>{`
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user