213 lines
7.4 KiB
TypeScript
213 lines
7.4 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* Table of Contents - Variant C: Modern & Visual (Shopify-inspired)
|
|
*
|
|
* Design Philosophy: Floating card with colorful visual indicators
|
|
*
|
|
* Features:
|
|
* - Wrapped in large gradient-bordered card
|
|
* - Colorful dot indicators (alternating purple/cyan/amber)
|
|
* - Large text sizes (text-base for items)
|
|
* - Generous spacing (p-6, gap-4)
|
|
* - Floating shadow effect with colored glow
|
|
* - NO section numbers (more visual/playful)
|
|
* - Active state with gradient background
|
|
* - Smooth hover animations with scale
|
|
* - Fun, engaging design with emoji header
|
|
*
|
|
* Visual Elements:
|
|
* - Large colored dots instead of numbers
|
|
* - Gradient progress bar with rainbow colors
|
|
* - Active item with gradient highlight
|
|
* - Hover effects with shadow and scale
|
|
*
|
|
* Behavior:
|
|
* - Extracts H2 and H3 headings (no numbers)
|
|
* - Shows reading progress with colorful bar
|
|
* - Click to smooth scroll to section
|
|
* - Visual feedback with animations
|
|
*/
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
interface TocItem {
|
|
id: string;
|
|
text: string;
|
|
level: number;
|
|
}
|
|
|
|
interface DocsTOCCProps {
|
|
items: TocItem[];
|
|
}
|
|
|
|
const getColorForIndex = (index: number): string => {
|
|
const colors = ['purple', 'cyan', 'amber'];
|
|
return colors[index % colors.length];
|
|
};
|
|
|
|
const getDotClasses = (color: string, isActive: boolean): string => {
|
|
const baseClasses = 'w-2.5 h-2.5 rounded-full flex-shrink-0 transition-all duration-300';
|
|
|
|
if (isActive) {
|
|
return `${baseClasses} shadow-lg ${
|
|
color === 'purple'
|
|
? 'bg-purple-500 shadow-purple-500/50'
|
|
: color === 'cyan'
|
|
? 'bg-cyan-500 shadow-cyan-500/50'
|
|
: 'bg-amber-500 shadow-amber-500/50'
|
|
}`;
|
|
}
|
|
|
|
return `${baseClasses} ${
|
|
color === 'purple'
|
|
? 'bg-purple-500/40'
|
|
: color === 'cyan'
|
|
? 'bg-cyan-500/40'
|
|
: 'bg-amber-500/40'
|
|
}`;
|
|
};
|
|
|
|
export const DocsTOCC = ({ items }: DocsTOCCProps) => {
|
|
const [activeId, setActiveId] = useState<string>('');
|
|
const [scrollProgress, setScrollProgress] = useState(0);
|
|
|
|
useEffect(() => {
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
setActiveId(entry.target.id);
|
|
}
|
|
});
|
|
},
|
|
{ rootMargin: '-20% 0px -35% 0px' }
|
|
);
|
|
|
|
items.forEach((item) => {
|
|
const element = document.getElementById(item.id);
|
|
if (element) observer.observe(element);
|
|
});
|
|
|
|
const handleScroll = () => {
|
|
const windowHeight = window.innerHeight;
|
|
const documentHeight = document.documentElement.scrollHeight;
|
|
const scrollTop = window.scrollY;
|
|
const progress = (scrollTop / (documentHeight - windowHeight)) * 100;
|
|
setScrollProgress(Math.min(progress, 100));
|
|
};
|
|
|
|
window.addEventListener('scroll', handleScroll);
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
window.removeEventListener('scroll', handleScroll);
|
|
};
|
|
}, [items]);
|
|
|
|
const scrollToSection = (id: string) => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
};
|
|
|
|
if (items.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<nav className="p-6 sticky top-6" aria-label="Table of contents">
|
|
{/* Floating Card with Gradient Border */}
|
|
<div className="rounded-2xl border-2 border-purple-500/40 bg-slate-900/60 backdrop-blur-md shadow-xl shadow-purple-500/20 overflow-hidden">
|
|
{/* Header with Icon and Progress */}
|
|
<div className="p-6 pb-4 border-b-2 border-purple-500/20">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<span className="text-2xl">📍</span>
|
|
<h3 className="text-sm font-bold text-white uppercase tracking-wider">
|
|
On This Page
|
|
</h3>
|
|
</div>
|
|
|
|
{/* Rainbow Progress Bar */}
|
|
<div className="relative h-2 bg-slate-800 rounded-full overflow-hidden">
|
|
<div
|
|
className="absolute top-0 left-0 h-full bg-gradient-to-r from-purple-500 via-cyan-500 to-amber-500 transition-all duration-300 shadow-lg"
|
|
style={{ width: `${scrollProgress}%` }}
|
|
></div>
|
|
</div>
|
|
<div className="mt-2 flex items-center justify-between">
|
|
<p className="text-xs text-gray-400">{Math.round(scrollProgress)}% complete</p>
|
|
<span className="text-xs text-purple-400 font-semibold">
|
|
{items.findIndex((item) => item.id === activeId) + 1}/{items.length}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* TOC Items with Colorful Dots */}
|
|
<ul className="p-6 space-y-3">
|
|
{items.map((item, index) => {
|
|
const isActive = activeId === item.id;
|
|
const isH3 = item.level === 3;
|
|
const color = getColorForIndex(index);
|
|
const cleanText = item.text.replace(/^\d+\.?\s*/, '');
|
|
|
|
return (
|
|
<li key={item.id} className={isH3 ? 'ml-6' : ''}>
|
|
<button
|
|
onClick={() => scrollToSection(item.id)}
|
|
className={`
|
|
flex items-start gap-3 text-left w-full transition-all duration-300 py-2 px-3 rounded-lg
|
|
${
|
|
isActive
|
|
? 'bg-gradient-to-br from-purple-500/20 to-cyan-500/20 text-white font-semibold shadow-lg scale-105'
|
|
: 'text-gray-400 hover:text-white hover:bg-slate-800/50 hover:scale-102'
|
|
}
|
|
`}
|
|
>
|
|
{/* Colorful Dot Indicator */}
|
|
<span className="mt-1.5">
|
|
{isH3 ? (
|
|
<span className="text-gray-600 text-xs ml-0.5">↳</span>
|
|
) : (
|
|
<span className={getDotClasses(color, isActive)}></span>
|
|
)}
|
|
</span>
|
|
|
|
{/* Text */}
|
|
<span className="flex-1 text-base leading-relaxed">{cleanText}</span>
|
|
</button>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
|
|
{/* Back to Top Button - Colorful */}
|
|
{scrollProgress > 20 && (
|
|
<div className="p-6 pt-0">
|
|
<button
|
|
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
|
className="w-full px-4 py-3 text-sm bg-gradient-to-r from-purple-600/30 to-cyan-600/30 hover:from-purple-600/50 hover:to-cyan-600/50 text-white rounded-xl transition-all duration-300 flex items-center justify-center gap-2 border-2 border-purple-500/40 hover:border-purple-500/60 shadow-lg hover:shadow-purple-500/30 hover:scale-105"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
|
</svg>
|
|
<span className="font-semibold">Back to top</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Fun Floating Tip Card */}
|
|
<div className="mt-6 p-4 rounded-xl bg-gradient-to-br from-amber-500/10 to-transparent border-2 border-amber-500/30 shadow-lg">
|
|
<div className="flex items-start gap-2">
|
|
<span className="text-xl">💡</span>
|
|
<p className="text-xs text-amber-300">
|
|
<strong>Tip:</strong> Click any item to jump directly to that section!
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
);
|
|
};
|