Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions src/components/Charts/Map2D/Map2D.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ function Map2DChartComponent({
// Create a memoized function to generate scatter series config
const createScatterSeries = useCallback(
(data: Array<{ name?: string; value: number[] }>) => ({
id: 'scatter-points',
type: 'scatter',
coordinateSystem: 'geo',
zlevel: 3,
Expand Down Expand Up @@ -222,21 +223,14 @@ function Map2DChartComponent({
nodes: point.nodes,
}));

// Use cached scatter series index (routes don't change after init)
const scatterSeriesIndex = routes.length > 0 ? 1 : 0;

// Build minimal series update array
const seriesUpdate: (Record<string, unknown> | null)[] = [];
for (let i = 0; i <= scatterSeriesIndex; i++) {
seriesUpdate.push(i === scatterSeriesIndex ? { data: pointData } : null);
}

// Update chart with lazy update enabled for better performance
chart.setOption({ series: seriesUpdate }, { notMerge: false, lazyUpdate: true });
// Update the scatter series by stable ID to avoid index mismatches
// when stories or props toggle route series on/off.
chart.setOption({ series: [{ id: 'scatter-points', data: pointData }] }, { notMerge: false, lazyUpdate: true });
}, [points, mapLoaded, resetKey, routes.length]);

// Prepare initial options - only calculated once on mount
// We update data via setOption in useEffect, so option should never recalculate
// Prepare initial options.
// Route and geo settings must update when related props change, while point data
// is still streamed via setOption in the points effect above.
const option = useMemo(() => {
// Don't create option until map is loaded to avoid accessing undefined map data
if (!mapLoaded) {
Expand Down Expand Up @@ -326,8 +320,19 @@ function Map2DChartComponent({
},
series,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mapLoaded]); // Only recalculate when map loads - after that we update via setOption
}, [
mapLoaded,
routes,
showEffect,
lineColor,
title,
roam,
createScatterSeries,
themeColors.primary,
themeColors.foreground,
themeColors.muted,
themeColors.border,
]);

// Handle chart events for hover panel
const onEvents = useMemo(
Expand Down
17 changes: 11 additions & 6 deletions src/components/Ethereum/NetworkIcon/NetworkIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,25 @@ export function NetworkIcon({ networkName, className }: NetworkIconProps): JSX.E
);
}

// Emoji icons: render in a flex container for proper centering
// Use the className for sizing the container, and scale emoji to fit
// Emoji icons: @container lets the emoji scale with the container via cqw units
if (icon?.type === 'emoji') {
return (
<span className={clsx('inline-flex items-center justify-center', className || 'size-6')} aria-hidden="true">
<span className="text-[1.1em] leading-none">{icon.content}</span>
<span
className={clsx('@container inline-flex items-center justify-center', className || 'size-6')}
aria-hidden="true"
>
<span className="text-[70cqw] leading-none">{icon.content}</span>
</span>
);
}

// Fallback for unknown networks
return (
<span className={clsx('inline-flex items-center justify-center', className || 'size-6')} aria-hidden="true">
<span className="text-[1.1em] leading-none">🧪</span>
<span
className={clsx('@container inline-flex items-center justify-center', className || 'size-6')}
aria-hidden="true"
>
<span className="text-[70cqw] leading-none">🧪</span>
</span>
);
}
3 changes: 1 addition & 2 deletions src/components/Ethereum/NetworkSelect/NetworkSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { type JSX, useMemo } from 'react';
import { useNetwork, type Network } from '@/hooks/useNetwork';
import { SelectMenu, type SelectMenuOption } from '@/components/Forms/SelectMenu';
import { NetworkIcon } from '@/components/Ethereum/NetworkIcon';
import { NETWORK_ORDER } from '@/utils/networks';
import type { NetworkSelectProps } from './NetworkSelect.types';

const NETWORK_ORDER = ['mainnet', 'holesky', 'sepolia', 'hoodi'];

/**
* Sorts networks by the predefined order, then alphabetically for any remaining networks.
*/
Expand Down
46 changes: 45 additions & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,22 @@
--animate-fade-in-delay: fade-in 0.8s ease-out 0.2s forwards;
--animate-fade-in-delay-2: fade-in 0.8s ease-out 0.4s forwards;
--animate-fade-in-delay-3: fade-in 0.8s ease-out 0.6s forwards;
--animate-fade-in-delay-4: fade-in 0.8s ease-out 0.8s forwards;
--animate-fade-in-delay-5: fade-in 0.8s ease-out 1s forwards;
--animate-fade-in-delay-6: fade-in 0.8s ease-out 1.2s forwards;
--animate-scroll-travel: scroll-travel 2s ease-in-out infinite;

@keyframes scroll-travel {
0%,
100% {
opacity: 1;
transform: translateY(0);
}
50% {
opacity: 0.3;
transform: translateY(8px);
}
}

@keyframes shimmer {
0% {
Expand Down Expand Up @@ -340,7 +356,10 @@
.animate-fade-in,
.animate-fade-in-delay,
.animate-fade-in-delay-2,
.animate-fade-in-delay-3 {
.animate-fade-in-delay-3,
.animate-fade-in-delay-4,
.animate-fade-in-delay-5,
.animate-fade-in-delay-6 {
opacity: 0;
}

Expand All @@ -354,3 +373,28 @@
display: none; /* Chrome, Safari, Opera */
}
}

/* Digit flip animations for scoreboard display.
* Placed outside @theme inline so they're always emitted
* (keyframes inside @theme are only output when referenced by a token). */
@keyframes digit-out {
from {
transform: translateY(0);
opacity: 1;
}
to {
transform: translateY(-100%);
opacity: 0;
}
}

@keyframes digit-in {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
70 changes: 3 additions & 67 deletions src/pages/home/IndexPage.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,10 @@
import { type JSX } from 'react';
import { Link } from '@tanstack/react-router';
import { Card } from '@/components/Layout/Card';
import { BeakerIcon, UserGroupIcon } from '@heroicons/react/24/outline';
import { AlgorithmicArt } from './components/AlgorithmicArt';
import { HeroSection } from './components/HeroSection';

export function IndexPage(): JSX.Element {
return (
<div className="relative h-[calc(100dvh-65px)] overflow-hidden lg:h-screen">
{/* Full-page Network Visualization - fills viewport */}
<div className="absolute inset-0 z-0">
<AlgorithmicArt height={typeof window !== 'undefined' ? window.innerHeight : 800} seed={12345} speed={1} />
</div>

{/* Content Layer - positioned on top of visualization */}
<div className="absolute inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full flex-col px-4 pt-20 sm:pt-40">
{/* Hero Section - centered and spacious */}
<div className="mb-16 flex flex-col items-center text-center sm:mb-48">
{/* Main Title - matching sidebar style */}
<h1 className="mb-4 animate-fade-in font-sans text-5xl font-bold tracking-tight text-foreground drop-shadow-2xl sm:mb-6 sm:text-7xl md:text-8xl lg:text-9xl">
The Lab
</h1>

{/* Subtitle with subtle animation */}
<p className="max-w-xl animate-fade-in-delay font-sans text-base font-light tracking-wide text-foreground/80 drop-shadow-lg sm:text-lg md:text-sm">
by ethPandaOps
</p>

{/* Optional tagline */}
<p className="mt-3 animate-fade-in-delay-2 font-sans text-xs font-light tracking-widest text-muted/60 uppercase sm:mt-4 sm:text-sm">
Explore · Analyze · Discover
</p>
</div>

{/* Featured Cards - cleaner spacing */}
<div className="mx-auto grid w-full max-w-5xl animate-fade-in-delay-3 gap-6 pb-16 sm:grid-cols-2 sm:gap-8 sm:pb-32">
{/* Ethereum Live */}
<Link to="/ethereum/live" className="group block">
<Card
isInteractive
rounded
className="h-full bg-surface/95 backdrop-blur-md transition-all duration-300 hover:bg-surface"
>
<div className="flex flex-col gap-4">
<BeakerIcon className="size-10 text-primary transition-transform duration-300 group-hover:scale-110" />
<h3 className="text-xl font-semibold tracking-tight text-foreground">Ethereum Live</h3>
<p className="text-base/relaxed text-muted">
Visualize beacon chain slots and block proposals in real-time
</p>
</div>
</Card>
</Link>

{/* Xatu Contributors */}
<Link to="/xatu/contributors" className="group block">
<Card
isInteractive
rounded
className="h-full bg-surface/95 backdrop-blur-md transition-all duration-300 hover:bg-surface"
>
<div className="flex flex-col gap-4">
<UserGroupIcon className="size-10 text-primary transition-transform duration-300 group-hover:scale-110" />
<h3 className="text-xl font-semibold tracking-tight text-foreground">Xatu Contributoors</h3>
<p className="text-base/relaxed text-muted">Explore the contributors to the Xatu project</p>
</div>
</Card>
</Link>
</div>
</div>
</div>
<div className="h-dvh overflow-hidden bg-background">
<HeroSection />
</div>
);
}
Loading
Loading