Spaces:
Sleeping
Sleeping
| 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; | |