Skip to content
Open
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
98 changes: 97 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,103 @@
</script>
</head>
<body>
<div id="root"></div>
<div id="root">
<div class="root-loading" role="status" aria-live="polite" aria-label="Loading">
<div class="root-loading-spinner">
<div class="root-loading-glow"></div>
<div class="root-loading-ring">
<div class="root-loading-inner">
<img src="/forkcast-logo.svg" alt="" class="root-loading-logo" width="56" height="17">
</div>
</div>
</div>
<p class="root-loading-text">Loading</p>
<div class="root-loading-dots">
<span class="root-loading-dot"></span>
<span class="root-loading-dot"></span>
<span class="root-loading-dot"></span>
</div>
</div>
</div>
<style>
html, body { margin: 0; padding: 0; width: 100%; min-height: 100%; background: #f8fafc; }
.dark, .dark body { background: #0f172a; }
#root { min-height: 100vh; }
.root-loading {
min-height: 100vh;
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f8fafc;
font-family: system-ui, -apple-system, sans-serif;
}
.dark .root-loading { background: #0f172a; }
.root-loading-spinner { position: relative; width: 80px; height: 80px; margin-bottom: 2rem; display: flex; align-items: center; justify-content: center; }
.root-loading-glow {
position: absolute;
width: 88px;
height: 88px;
border-radius: 50%;
background: conic-gradient(from 0deg, #6366f1, #8b5cf6, #a855f7, #6366f1);
animation: root-spin 1.4s linear infinite;
opacity: 0.35;
filter: blur(12px);
}
.dark .root-loading-glow { opacity: 0.25; }
.root-loading-ring {
position: relative;
width: 72px;
height: 72px;
border-radius: 50%;
padding: 3px;
background: conic-gradient(from 0deg, #6366f1, #8b5cf6, #a855f7, #c084fc, #6366f1);
animation: root-spin 1.2s linear infinite;
}
.root-loading-inner {
position: absolute;
inset: 3px;
border-radius: 50%;
background: #f8fafc;
display: flex;
align-items: center;
justify-content: center;
}
.dark .root-loading-inner { background: #0f172a; }
.root-loading-logo {
width: 56px;
height: auto;
opacity: 0.9;
animation: root-pulse 2s ease-in-out infinite;
}
.root-loading-text {
margin: 0 0 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: #64748b;
}
.dark .root-loading-text { color: #94a3b8; }
.root-loading-dots {
display: inline-flex;
gap: 4px;
align-items: center;
}
.root-loading-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #8b5cf6;
animation: root-bounce 1.4s ease-in-out infinite;
}
.root-loading-dot:nth-child(1) { animation-delay: 0ms; }
.root-loading-dot:nth-child(2) { animation-delay: 160ms; }
.root-loading-dot:nth-child(3) { animation-delay: 320ms; }
@keyframes root-spin { to { transform: rotate(360deg); } }
@keyframes root-pulse { 0%, 100% { opacity: 0.85; transform: scale(1); } 50% { opacity: 1; transform: scale(1.05); } }
@keyframes root-bounce { 0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; } 40% { transform: scale(1); opacity: 1; } }
</style>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
14 changes: 9 additions & 5 deletions src/components/GlobalCallSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,14 @@ export default function GlobalCallSearch({ isOpen, onClose, initialQuery = '' }:
<div
className="absolute inset-0 bg-slate-900/50 backdrop-blur-sm"
onClick={onClose}
aria-hidden="true"
/>

{/* Search Modal */}
<div className="relative w-full max-w-4xl bg-white dark:bg-slate-800 rounded-xl sm:rounded-xl rounded-t-xl shadow-2xl overflow-hidden animate-[slideDown_0.2s_ease-out] max-h-[90vh] sm:max-h-none">
{/* Search Modal - stop propagation so clicks inside don't close */}
<div
className="relative z-10 w-full max-w-4xl bg-white dark:bg-slate-800 rounded-xl sm:rounded-xl rounded-t-xl shadow-2xl overflow-hidden animate-[slideDown_0.2s_ease-out] max-h-[90vh] sm:max-h-none"
onClick={(e) => e.stopPropagation()}
>
{/* Search Header */}
<div className="border-b border-slate-200 dark:border-slate-700">
<div className="flex items-center gap-3 p-3 sm:p-4">
Expand All @@ -237,7 +241,7 @@ export default function GlobalCallSearch({ isOpen, onClose, initialQuery = '' }:
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search across all calls..."
className="flex-1 bg-transparent text-slate-900 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500 outline-none text-base sm:text-base text-lg min-h-[44px] sm:min-h-0"
className="flex-1 bg-transparent text-slate-900 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500 outline-none text-base min-h-[44px] sm:min-h-0"
/>
{loading && (
<div className="w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
Expand Down Expand Up @@ -309,7 +313,7 @@ export default function GlobalCallSearch({ isOpen, onClose, initialQuery = '' }:
{/* Search Results */}
<div
ref={resultsContainerRef}
className="max-h-96 sm:max-h-96 max-h-[60vh] overflow-y-auto"
className="max-h-[60vh] sm:max-h-96 overflow-y-auto"
>
{query && results.length === 0 && !loading && !indexBuilding ? (
<div className="p-8 text-center text-slate-500 dark:text-slate-400">
Expand Down Expand Up @@ -361,7 +365,7 @@ export default function GlobalCallSearch({ isOpen, onClose, initialQuery = '' }:
)}

{/* Text */}
<p className="text-sm sm:text-sm text-base text-slate-900 dark:text-slate-100 line-clamp-3 sm:line-clamp-2 leading-relaxed">
<p className="text-sm text-slate-900 dark:text-slate-100 line-clamp-3 sm:line-clamp-2 leading-relaxed">
{highlightMatch(result.text, query)}
</p>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ const HomePage = () => {
<div className="min-h-screen bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-100 p-6">
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="mb-12 text-center relative">
<div className="mb-12 text-center relative pr-12">
<div className="absolute top-0 right-0">
<ThemeToggle />
</div>
Expand Down
8 changes: 4 additions & 4 deletions src/components/RankPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate, Link } from "react-router-dom";
import { EIP } from "../types/eip";
import {
isHeadliner,
Expand Down Expand Up @@ -757,12 +757,12 @@ const RankPage: React.FC = () => {
</p>
<p className="text-xs text-slate-600 dark:text-slate-300 leading-relaxed">
Download the image to share your rankings and start a conversation.{" "}
<a
href="https://forkcast.org/upgrade/glamsterdam"
<Link
to="/upgrade/glamsterdam"
className="text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300"
>
Learn more about Glamsterdam
</a>
</Link>
.
</p>
<div className="mt-4 flex items-start gap-2.5 rounded-lg border border-slate-200 bg-slate-100 p-3 dark:border-slate-700 dark:bg-slate-800">
Expand Down
13 changes: 6 additions & 7 deletions src/components/call/CallPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import TldrSummary from './TldrSummary';
import CallSearch from './CallSearch';
import ThemeToggle from '../ui/ThemeToggle';
import { Logo } from '../ui/Logo';
import { LoadingScreen } from '../ui/LoadingScreen';
import { protocolCalls, callTypeNames, type CallType } from '../../data/calls';
import { eipsData } from '../../data/eips';
import { EIP, ForkRelationship } from '../../types/eip';
Expand Down Expand Up @@ -799,13 +800,11 @@ const CallPage: React.FC = () => {

if (loading) {
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-100 p-6">
<div className="max-w-4xl mx-auto">
<div className="text-center">
<p className="text-slate-600 dark:text-slate-400">Loading call data...</p>
</div>
</div>
</div>
<LoadingScreen
message="Loading call"
subtitle="Fetching transcript and video..."
skeleton
/>
);
}

Expand Down
95 changes: 95 additions & 0 deletions src/components/ui/LoadingScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
interface LoadingScreenProps {
message?: string;
/** Optional subtitle or context (e.g. "Fetching transcript...") */
subtitle?: string;
/** Show skeleton lines below (good for call/page context) */
skeleton?: boolean;
className?: string;
}

export function LoadingScreen({
message = 'Loading',
subtitle,
skeleton = false,
className = '',
}: LoadingScreenProps) {
return (
<div
className={`min-h-screen w-full min-w-full box-border bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-100 flex flex-col items-center justify-center p-6 ${className}`}
role="status"
aria-live="polite"
aria-label={`${message}${subtitle ? ` – ${subtitle}` : ''}`}
>
<div
className="flex flex-col items-center text-center max-w-sm animate-[loadingFadeInUp_0.5s_ease-out]"
style={{ animationFillMode: 'both' }}
>
{/* Gradient ring spinner with soft glow */}
<div className="relative mb-8 w-20 h-20 flex items-center justify-center">
<div
className="absolute inset-0 rounded-full opacity-40 dark:opacity-30 blur-md"
style={{
background: 'conic-gradient(from 0deg, #6366f1, #8b5cf6, #a855f7, #6366f1)',
animation: 'loadingGradientSpin 1.4s linear infinite',
}}
/>
<div
className="relative w-16 h-16 rounded-full p-[3px]"
style={{
background: 'conic-gradient(from 0deg, #6366f1, #8b5cf6, #a855f7, #c084fc, #6366f1)',
animation: 'loadingGradientSpin 1.2s linear infinite',
}}
>
<div className="w-full h-full rounded-full bg-slate-50 dark:bg-slate-900 flex items-center justify-center">
<img
src="/forkcast-logo.svg"
alt=""
className="h-5 w-auto opacity-90 animate-[loadingPulse_2s_ease-in-out_infinite]"
aria-hidden
/>
</div>
</div>
</div>

{/* Message with animated dots */}
<p className="text-slate-600 dark:text-slate-400 font-medium text-sm mb-1">
{message}
<span className="inline-flex ml-0.5" aria-hidden>
<span
className="inline-block w-1.5 h-1.5 rounded-full bg-purple-500 dark:bg-purple-400 ml-0.5 animate-[loadingBounce_1.4s_ease-in-out_infinite]"
style={{ animationDelay: '0ms' }}
/>
<span
className="inline-block w-1.5 h-1.5 rounded-full bg-purple-500 dark:bg-purple-400 ml-0.5 animate-[loadingBounce_1.4s_ease-in-out_infinite]"
style={{ animationDelay: '160ms' }}
/>
<span
className="inline-block w-1.5 h-1.5 rounded-full bg-purple-500 dark:bg-purple-400 ml-0.5 animate-[loadingBounce_1.4s_ease-in-out_infinite]"
style={{ animationDelay: '320ms' }}
/>
</span>
</p>
{subtitle && (
<p className="text-slate-500 dark:text-slate-500 text-xs animate-[loadingFadeInUp_0.4s_ease-out_0.2s_both]">
{subtitle}
</p>
)}

{/* Optional skeleton lines */}
{skeleton && (
<div className="w-full mt-8 space-y-3 animate-[loadingFadeInUp_0.4s_ease-out_0.15s_both]">
<div className="h-3 rounded bg-slate-200 dark:bg-slate-700 overflow-hidden">
<div className="h-full w-full rounded animate-loading-shimmer" />
</div>
<div className="h-3 rounded bg-slate-200 dark:bg-slate-700 overflow-hidden">
<div className="h-full w-full rounded animate-loading-shimmer" style={{ animationDelay: '0.15s' }} />
</div>
<div className="h-3 rounded bg-slate-200 dark:bg-slate-700 overflow-hidden">
<div className="h-full w-full rounded animate-loading-shimmer" style={{ animationDelay: '0.3s' }} />
</div>
</div>
)}
</div>
</div>
);
}
7 changes: 5 additions & 2 deletions src/components/ui/UpgradeCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,10 @@ const UpgradeCarousel = ({ upgrades, getStatusColor }: UpgradeCarouselProps) =>
<div className="relative">
{/* Left Arrow */}
<button
type="button"
onClick={goToPrevious}
disabled={currentIndex === 0}
className={`absolute left-0 top-1/2 -translate-y-1/2 -translate-x-12 z-10 w-10 h-10 flex items-center justify-center rounded-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 shadow-md transition-all duration-200 ${
className={`absolute left-0 top-1/2 -translate-y-1/2 -translate-x-12 z-20 w-10 h-10 flex items-center justify-center rounded-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 shadow-md transition-all duration-200 ${
currentIndex === 0
? 'opacity-40 cursor-not-allowed'
: 'hover:bg-slate-50 dark:hover:bg-slate-700 hover:shadow-lg cursor-pointer'
Expand Down Expand Up @@ -179,9 +180,10 @@ const UpgradeCarousel = ({ upgrades, getStatusColor }: UpgradeCarouselProps) =>

{/* Right Arrow */}
<button
type="button"
onClick={goToNext}
disabled={currentIndex >= maxIndex}
className={`absolute right-0 top-1/2 -translate-y-1/2 translate-x-12 z-10 w-10 h-10 flex items-center justify-center rounded-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 shadow-md transition-all duration-200 ${
className={`absolute right-0 top-1/2 -translate-y-1/2 translate-x-12 z-20 w-10 h-10 flex items-center justify-center rounded-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 shadow-md transition-all duration-200 ${
currentIndex >= maxIndex
? 'opacity-40 cursor-not-allowed'
: 'hover:bg-slate-50 dark:hover:bg-slate-700 hover:shadow-lg cursor-pointer'
Expand Down Expand Up @@ -210,6 +212,7 @@ const UpgradeCarousel = ({ upgrades, getStatusColor }: UpgradeCarouselProps) =>
{Array.from({ length: dotCount }).map((_, index) => (
<button
key={index}
type="button"
onClick={() => goToIndex(index)}
className={`w-2 h-2 rounded-full transition-all duration-200 ${
currentIndex === index
Expand Down
1 change: 1 addition & 0 deletions src/components/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './Tooltip';
export * from './CopyLinkButton';
export * from './StatusBadge';
export * from './LoadingScreen';
export { default as ThemeToggle } from './ThemeToggle';
export { default as AnnouncementBanner } from './AnnouncementBanner';
Loading