PaperStack / components /Sidebar.tsx
Akhil-Theerthala's picture
checkpoint-2
ade3003 verified
raw
history blame
7.39 kB
import React, { useState } from 'react';
import { BookOpen, ChevronRight, Loader2, CheckCircle } from 'lucide-react';
import { BlogSection } from '../types';
interface Props {
sections: BlogSection[];
activeSection: string;
onSectionClick: (id: string) => void;
}
const Sidebar: React.FC<Props> = ({ sections, activeSection, onSectionClick }) => {
const [isCollapsed, setIsCollapsed] = useState(false);
const handleClick = (id: string) => {
onSectionClick(id);
const element = document.getElementById(`section-${id}`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
return (
<nav
className={`
hidden lg:block fixed left-0 top-24 h-[calc(100vh-8rem)] z-40
transition-all duration-300 ease-out
${isCollapsed ? 'w-16' : 'w-72'}
`}
>
<div className="h-full flex flex-col ml-6">
{/* Header */}
<div className="flex items-center justify-between mb-6 pr-4">
{!isCollapsed && (
<div className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest text-gray-500 dark:text-gray-400">
<BookOpen size={14} />
<span>Contents</span>
</div>
)}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
>
<ChevronRight
size={16}
className={`transition-transform duration-300 ${isCollapsed ? '' : 'rotate-180'}`}
/>
</button>
</div>
{/* Navigation Items */}
<div className="flex-1 overflow-y-auto custom-scrollbar pr-4 space-y-1">
{sections.map((section, index) => {
const isActive = activeSection === section.id;
const isLoading = section.isLoading;
const isComplete = !section.isLoading && section.content && !section.error;
const hasError = section.error;
return (
<button
key={section.id}
onClick={() => !isLoading && handleClick(section.id)}
disabled={isLoading}
className={`
w-full text-left transition-all duration-200 group relative
${isCollapsed ? 'px-2 py-3' : 'px-4 py-3'}
rounded-xl
${isLoading ? 'opacity-60 cursor-wait' : ''}
${hasError ? 'opacity-70' : ''}
${isActive
? 'bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300'
: 'hover:bg-gray-50 dark:hover:bg-gray-800/50 text-gray-600 dark:text-gray-400'
}
`}
>
{/* Active Indicator */}
<div
className={`
absolute left-0 top-1/2 -translate-y-1/2 w-1 rounded-r-full
transition-all duration-300
${isActive ? 'h-8 bg-brand-500' : 'h-0 bg-transparent'}
`}
/>
{isCollapsed ? (
<div className={`
w-8 h-8 rounded-lg flex items-center justify-center text-sm font-bold
${isLoading ? 'bg-gray-200 dark:bg-gray-700' : ''}
${isActive && !isLoading
? 'bg-brand-500 text-white'
: 'bg-gray-100 dark:bg-gray-800 text-gray-500'
}
`}>
{isLoading ? (
<Loader2 size={14} className="animate-spin" />
) : (
index + 1
)}
</div>
) : (
<div className="flex items-start gap-3">
<span className={`
flex-shrink-0 w-6 h-6 rounded-md flex items-center justify-center text-xs font-bold mt-0.5
${isLoading ? 'bg-gray-200 dark:bg-gray-700 animate-pulse' : ''}
${isActive && !isLoading
? 'bg-brand-500 text-white'
: 'bg-gray-100 dark:bg-gray-800 text-gray-500 group-hover:bg-gray-200 dark:group-hover:bg-gray-700'
}
`}>
{isLoading ? (
<Loader2 size={10} className="animate-spin" />
) : isComplete ? (
<CheckCircle size={10} className="text-green-500" />
) : (
index + 1
)}
</span>
<span className={`
text-sm leading-snug transition-colors
${isLoading ? 'text-gray-400 dark:text-gray-600' : ''}
${isActive && !isLoading ? 'font-semibold' : 'font-medium'}
`}>
{section.title}
</span>
</div>
)}
</button>
);
})}
</div>
{/* Progress Indicator */}
{!isCollapsed && (
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-800 pr-4 space-y-3">
{/* Generation Progress */}
{sections.some(s => s.isLoading) && (
<div>
<div className="flex items-center justify-between text-xs text-brand-600 dark:text-brand-400 mb-2">
<span className="flex items-center gap-1">
<Loader2 size={10} className="animate-spin" />
Generating
</span>
<span>
{sections.filter(s => !s.isLoading).length}/{sections.length}
</span>
</div>
<div className="h-1.5 bg-brand-100 dark:bg-brand-900/30 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-brand-500 to-purple-500 rounded-full transition-all duration-500"
style={{
width: `${(sections.filter(s => !s.isLoading).length / sections.length) * 100}%`
}}
/>
</div>
</div>
)}
{/* Reading Progress */}
<div>
<div className="flex items-center justify-between text-xs text-gray-500 mb-2">
<span>Reading</span>
<span>
{sections.findIndex(s => s.id === activeSection) + 1}/{sections.length}
</span>
</div>
<div className="h-1.5 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-gray-400 to-gray-500 dark:from-gray-600 dark:to-gray-500 rounded-full transition-all duration-500"
style={{
width: `${((sections.findIndex(s => s.id === activeSection) + 1) / sections.length) * 100}%`
}}
/>
</div>
</div>
</div>
)}
</div>
</nav>
);
};
export default Sidebar;