Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Failing after 14s
Build & Deploy / 🧪 QA (push) Failing after 1m48s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
101 lines
5.8 KiB
TypeScript
101 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import React from 'react';
|
|
import { ComponentShareButton } from './ComponentShareButton';
|
|
import { Reveal } from './Reveal';
|
|
|
|
interface WaterfallEvent {
|
|
name: string;
|
|
start: number;
|
|
duration: number;
|
|
}
|
|
|
|
interface WaterfallChartProps {
|
|
title?: string;
|
|
events: WaterfallEvent[];
|
|
totalDuration?: number;
|
|
showShare?: boolean;
|
|
}
|
|
|
|
export const WaterfallChart: React.FC<WaterfallChartProps> = ({ title = 'Resource Waterfall', events, totalDuration }) => {
|
|
const maxTime = totalDuration || Math.max(...events.map(e => e.start + e.duration));
|
|
|
|
const getDefaultColor = (name: string) => {
|
|
const n = name.toLowerCase();
|
|
if (n.includes('html') || n.includes('document')) return 'bg-slate-800';
|
|
if (n.includes('js') || n.includes('script')) return 'bg-amber-400';
|
|
if (n.includes('css') || n.includes('style')) return 'bg-blue-400';
|
|
if (n.includes('img') || n.includes('image')) return 'bg-emerald-400';
|
|
if (n.includes('font')) return 'bg-pink-400';
|
|
return 'bg-slate-300';
|
|
};
|
|
|
|
// Generate stable hash for share button
|
|
const shareId = `waterfall-${title.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
|
|
|
return (
|
|
<Reveal direction="up" delay={0.1}>
|
|
<figure id={shareId} className="my-16 not-prose font-sans group relative transition-all duration-500 ease-out z-10">
|
|
{/* Ambient Background Glow matching the homepage feel */}
|
|
<div className="absolute -inset-1 bg-gradient-to-r from-blue-100/30 to-slate-100/30 rounded-[2rem] blur opacity-20 group-hover:opacity-40 transition duration-1000 -z-10" />
|
|
|
|
<div className="glass bg-white/80 backdrop-blur-xl border border-slate-100 rounded-2xl shadow-sm group-hover:shadow-md group-hover:border-slate-200 transition-all duration-500 overflow-hidden relative">
|
|
|
|
{/* Subtle top shine */}
|
|
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white to-transparent opacity-80" />
|
|
|
|
<div className="p-6 md:p-8 lg:p-10 relative z-10">
|
|
{/* Share Button top right */}
|
|
<div className="absolute top-6 right-6 md:opacity-0 group-hover:opacity-100 transition-opacity duration-500 z-50">
|
|
<ComponentShareButton targetId={shareId} title={title} />
|
|
</div>
|
|
|
|
<header className="mb-8 flex flex-col md:flex-row md:justify-between md:items-end gap-2 border-b border-slate-100 pb-4">
|
|
<h3 className="text-xl md:text-2xl font-bold text-slate-900 tracking-tight m-0">{title}</h3>
|
|
<div className="font-mono text-xs text-slate-400 bg-slate-50 px-3 py-1 rounded-full border border-slate-100 inline-flex items-center gap-2">
|
|
<span className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse" />
|
|
Total Time: {maxTime}ms
|
|
</div>
|
|
</header>
|
|
|
|
<div className="relative pt-6 pb-2">
|
|
{/* Elegant Grid Lines */}
|
|
<div className="absolute inset-x-0 top-0 bottom-0 flex justify-between pointer-events-none z-0 ml-[100px] md:ml-[160px]">
|
|
{[0, 0.25, 0.5, 0.75, 1].map((tick) => (
|
|
<div key={tick} className="w-px h-full bg-slate-100 flex flex-col justify-start">
|
|
<span className="text-[9px] text-slate-400 font-mono -ml-3 -mt-5 bg-white/80 backdrop-blur-sm px-1.5 py-0.5 rounded border border-slate-100 shadow-sm">
|
|
{Math.round(maxTime * tick)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="relative z-10 space-y-3">
|
|
{events.map((event, i) => {
|
|
const left = (event.start / maxTime) * 100;
|
|
const width = Math.max((event.duration / maxTime) * 100, 0.5);
|
|
|
|
return (
|
|
<div key={i} className="group/row relative flex items-center h-10 w-full hover:bg-slate-50/50 rounded-lg transition-colors px-2 -mx-2">
|
|
<div className="w-[100px] md:w-[160px] shrink-0 pr-4 flex flex-col justify-center z-20">
|
|
<span className="font-bold text-slate-700 text-[10px] md:text-xs truncate uppercase tracking-widest">{event.name}</span>
|
|
</div>
|
|
<div className="flex-1 relative h-full flex items-center">
|
|
<div
|
|
className={`h-2.5 md:h-3 rounded-full relative shadow-sm transition-all duration-300 group-hover/row:scale-y-110 group-hover/row:brightness-110 ${getDefaultColor(event.name)}`}
|
|
style={{ left: `${left}%`, width: `${width}%` }}
|
|
/>
|
|
</div>
|
|
<span className="w-12 text-right font-mono text-[9px] md:text-xs text-slate-400 group-hover/row:text-slate-900 transition-colors z-20">{event.duration}ms</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</figure>
|
|
</Reveal>
|
|
);
|
|
};
|