All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🏗️ Build (push) Successful in 21m19s
Build & Deploy / 🚀 Deploy (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 1m28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 2m59s
Build & Deploy / 🔔 Notify (push) Successful in 9s
Nightly QA / 🔗 Links & Deps (push) Successful in 3m40s
Nightly QA / 🎭 Lighthouse (push) Successful in 4m18s
Nightly QA / 🔍 Static Analysis (push) Successful in 4m34s
Nightly QA / 📝 E2E (push) Successful in 4m45s
Nightly QA / 🔔 Notify (push) Has been skipped
225 lines
14 KiB
TypeScript
225 lines
14 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useEffect, useRef } from 'react';
|
||
import { ComponentShareButton } from '../ComponentShareButton';
|
||
import { Reveal } from '../Reveal';
|
||
import { RotateCcw } from 'lucide-react';
|
||
|
||
export function LoadTimeSimulator({ className = '' }: { className?: string }) {
|
||
const [isRunning, setIsRunning] = useState(false);
|
||
const [timeElapsed, setTimeElapsed] = useState(0);
|
||
const [legacyState, setLegacyState] = useState(0);
|
||
const [hasAutoStarted, setHasAutoStarted] = useState(false);
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const [mintelState, setMintelState] = useState(0);
|
||
|
||
useEffect(() => {
|
||
let interval: NodeJS.Timeout;
|
||
if (isRunning && timeElapsed < 7500) {
|
||
interval = setInterval(() => {
|
||
setTimeElapsed(prev => {
|
||
const next = prev + 50;
|
||
|
||
// Legacy Loading Logic (stuttering, slow, layout shifts)
|
||
if (next >= 1200 && next < 3000) setLegacyState(1); // Header only
|
||
else if (next >= 3000 && next < 5500) setLegacyState(2); // Text appears
|
||
else if (next >= 5500 && next < 7000) setLegacyState(3); // Image loads (CLS)
|
||
else if (next >= 7000) setLegacyState(4); // Finally interactive
|
||
|
||
// Mintel Loading Logic (Instant Edge Delivery)
|
||
if (next >= 350) setMintelState(1); // Fully loaded instantly
|
||
|
||
return next;
|
||
});
|
||
}, 50);
|
||
} else if (timeElapsed >= 7500) {
|
||
setIsRunning(false);
|
||
}
|
||
return () => clearInterval(interval);
|
||
}, [isRunning, timeElapsed]);
|
||
|
||
// Auto-start the race when scrolled into viewport
|
||
useEffect(() => {
|
||
if (hasAutoStarted) return;
|
||
const el = containerRef.current;
|
||
if (!el) return;
|
||
const observer = new IntersectionObserver(
|
||
([entry]) => {
|
||
if (entry.isIntersecting) {
|
||
setHasAutoStarted(true);
|
||
setIsRunning(true);
|
||
observer.disconnect();
|
||
}
|
||
},
|
||
{ threshold: 0.4 }
|
||
);
|
||
observer.observe(el);
|
||
return () => observer.disconnect();
|
||
}, [hasAutoStarted]);
|
||
|
||
const startRace = () => {
|
||
setTimeElapsed(0);
|
||
setLegacyState(0);
|
||
setMintelState(0);
|
||
setIsRunning(true);
|
||
};
|
||
|
||
return (
|
||
<Reveal direction="up" delay={0.1}>
|
||
<div ref={containerRef} className={`not-prose max-w-4xl mx-auto my-12 relative group ${className}`}>
|
||
<div className="absolute -inset-1 bg-gradient-to-r from-red-100 to-emerald-100 rounded-3xl blur opacity-30" />
|
||
|
||
<div id="sim-load-time" className="relative bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden flex flex-col">
|
||
<div data-share-wrapper="true" className="absolute top-4 right-4 md:opacity-0 group-hover:opacity-100 transition-opacity z-50">
|
||
<ComponentShareButton targetId="sim-load-time" title="Ladezeit Simulator" />
|
||
</div>
|
||
|
||
<div className="p-6 border-b border-slate-200 bg-slate-50 flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
||
<div>
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<span className="text-xl">🏎️</span>
|
||
<h3 className="font-bold text-slate-900 m-0">Architektur-Rennen: Legacy CMS vs. Mintel Stack</h3>
|
||
</div>
|
||
<p className="text-xs text-slate-500 m-0 leading-relaxed">
|
||
Simulieren Sie den Unterschied zwischen dynamischem Server-Rendering (PHP/MySQL) und statischer Edge-Auslieferung (<span className="font-mono bg-slate-200 px-1 rounded text-[10px]">TTV < 500ms</span>).
|
||
</p>
|
||
</div>
|
||
{timeElapsed > 0 && !isRunning && (
|
||
<button
|
||
onClick={startRace}
|
||
className="shrink-0 flex items-center gap-2 px-6 py-2.5 bg-slate-900 !text-white rounded-full font-bold text-sm hover:hover:bg-black hover:scale-105 active:scale-95 transition-all shadow-md"
|
||
>
|
||
<RotateCcw size={16} />
|
||
Neustart
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="grid md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-slate-100 bg-slate-50/50">
|
||
|
||
{/* LEGACY LANE */}
|
||
<div className="p-6 flex flex-col items-center">
|
||
<div className="w-full flex justify-between items-center mb-6">
|
||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Legacy Monolith</span>
|
||
<span className={`font-mono font-bold ${legacyState === 4 ? 'text-red-500' : 'text-slate-400'}`}>
|
||
{legacyState === 4 ? "7.00s" : (timeElapsed / 1000).toFixed(2) + "s"}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Browser Mockup */}
|
||
<div className="w-full max-w-[280px] aspect-[3/4] bg-white border border-slate-200 rounded-lg shadow-sm overflow-hidden flex flex-col">
|
||
<div className="h-6 bg-slate-100 border-b border-slate-200 flex items-center px-2 gap-1.5 shrink-0">
|
||
<div className="w-2 h-2 rounded-full bg-slate-300" />
|
||
<div className="w-2 h-2 rounded-full bg-slate-300" />
|
||
<div className="w-2 h-2 rounded-full bg-slate-300" />
|
||
</div>
|
||
<div className="p-4 flex-1 flex flex-col relative">
|
||
{/* Spinner */}
|
||
{legacyState === 0 && (
|
||
<div className="absolute inset-0 flex flex-col items-center justify-center text-slate-300 gap-2">
|
||
<div className="w-6 h-6 border-2 border-slate-200 border-t-slate-400 rounded-full animate-spin" />
|
||
<span className="text-[10px] font-mono tracking-widest uppercase">TTFB Waiting...</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* Content that shuffles around to simulate CLS */}
|
||
<div className={`transition-opacity duration-300 ${legacyState >= 1 ? 'opacity-100' : 'opacity-0'}`}>
|
||
<div className="h-4 w-3/4 bg-slate-200 rounded mb-4" /> {/* Header */}
|
||
</div>
|
||
|
||
{/*
|
||
Simulate Layout Shift:
|
||
Text loads first (state 2), then at state 3 an image drops in above it, pushing the text down.
|
||
*/}
|
||
<div className="flex flex-col gap-3 transition-opacity duration-300 mt-2">
|
||
{legacyState >= 3 && (
|
||
<div className="h-24 w-full bg-slate-200 relative overflow-hidden rounded animate-pulse">
|
||
<div className="absolute inset-0 flex items-center justify-center">
|
||
<span className="text-2xl opacity-50">🖼️</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div className={`space-y-2 transition-transform duration-300 ${legacyState >= 2 ? 'opacity-100' : 'opacity-0'} ${legacyState === 3 ? 'translate-y-2' : ''}`}>
|
||
<div className="h-2 w-full bg-slate-100 rounded" />
|
||
<div className="h-2 w-5/6 bg-slate-100 rounded" />
|
||
<div className="h-2 w-4/6 bg-slate-100 rounded" />
|
||
</div>
|
||
</div>
|
||
|
||
{legacyState === 4 && (
|
||
<div className="absolute bottom-4 left-4 right-4 animate-in slide-in-from-bottom-2 fade-in">
|
||
<div className="h-8 bg-slate-900 rounded flex items-center justify-center">
|
||
<span className="text-[10px] font-bold text-white uppercase">Interaktiv (Hydrated)</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="h-6 mt-4 flex items-center">
|
||
{legacyState === 0 && timeElapsed > 0 && <span className="text-[10px] text-slate-400 uppercase tracking-widest animate-pulse">Waiting for Server...</span>}
|
||
{legacyState === 1 && <span className="text-[10px] text-amber-500 uppercase tracking-widest">Parsing HTML...</span>}
|
||
{legacyState === 2 && <span className="text-[10px] text-amber-500 uppercase tracking-widest">Downloading Assets...</span>}
|
||
{legacyState === 3 && <span className="text-[10px] text-red-500 font-bold uppercase tracking-widest">Layout Shift Detected!</span>}
|
||
{legacyState === 4 && <span className="text-[10px] text-red-500 font-bold uppercase tracking-widest">Finished in 7.0s</span>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* MINTEL LANE */}
|
||
<div className="p-6 flex flex-col items-center border-t md:border-t-0 border-slate-100">
|
||
<div className="w-full flex justify-between items-center mb-6">
|
||
<span className="text-[10px] font-bold text-emerald-500 uppercase tracking-widest">Mintel Stack (Edge)</span>
|
||
<span className={`font-mono font-bold ${mintelState === 1 ? 'text-emerald-500' : 'text-slate-400'}`}>
|
||
{mintelState === 1 ? "0.35s" : (timeElapsed / 1000).toFixed(2) + "s"}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Browser Mockup */}
|
||
<div className="w-full max-w-[280px] aspect-[3/4] bg-white border-2 border-emerald-100 rounded-lg shadow-sm overflow-hidden flex flex-col relative ring-4 ring-emerald-50 ring-opacity-50 transition-all duration-500">
|
||
<div className="h-6 bg-emerald-50 border-b border-emerald-100 flex items-center px-2 gap-1.5 shrink-0">
|
||
<div className="w-2 h-2 rounded-full bg-emerald-200" />
|
||
<div className="w-2 h-2 rounded-full bg-emerald-200" />
|
||
<div className="w-2 h-2 rounded-full bg-emerald-200" />
|
||
</div>
|
||
<div className="p-4 flex-1 flex flex-col relative">
|
||
|
||
{mintelState === 0 && (
|
||
<div className="absolute inset-0 flex flex-col items-center justify-center text-emerald-300 gap-2">
|
||
<span className="text-[10px] font-mono tracking-widest uppercase">CDN Hit...</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* Instant Load: Everything drops in perfectly immediately */}
|
||
<div className={`transition-opacity duration-300 h-full flex flex-col ${mintelState === 1 ? 'opacity-100' : 'opacity-0'}`}>
|
||
<div className="h-4 w-3/4 bg-emerald-100 rounded mb-4 shrink-0" />
|
||
|
||
<div className="h-24 w-full bg-emerald-50 relative overflow-hidden rounded mb-3 shrink-0 flex items-center justify-center border border-emerald-100">
|
||
<span className="text-2xl opacity-80">🚀</span>
|
||
</div>
|
||
|
||
<div className="space-y-2 shrink-0">
|
||
<div className="h-2 w-full bg-slate-100 rounded" />
|
||
<div className="h-2 w-5/6 bg-slate-100 rounded" />
|
||
<div className="h-2 w-4/6 bg-slate-100 rounded" />
|
||
</div>
|
||
|
||
<div className="mt-auto pt-4 shrink-0">
|
||
<div className="h-8 bg-emerald-500 rounded flex items-center justify-center shadow-lg shadow-emerald-500/20">
|
||
<span className="text-[10px] font-bold text-white uppercase">Instant Interactive</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="h-6 mt-4 flex items-center">
|
||
{mintelState === 0 && timeElapsed > 0 && <span className="text-[10px] text-emerald-400 uppercase tracking-widest">Routing to Edge...</span>}
|
||
{mintelState === 1 && <span className="text-[10px] text-emerald-500 font-bold uppercase tracking-widest">Loaded & Ready in 350ms</span>}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Reveal>
|
||
);
|
||
}
|