PaperStack / components /BlogView.tsx
Akhil-Theerthala's picture
checkpoint-2
ade3003 verified
raw
history blame
17.2 kB
import React, { useEffect, useState, useRef } from 'react';
import { BlogSection as BlogSectionType, PaperStructure } from '../types';
import BlogSectionComponent from './BlogSection';
import Sidebar from './Sidebar';
import { Clock, BookOpen, FileText, Share2, Download, Sparkles, CheckCircle2, Loader2, AlertCircle, RefreshCw } from 'lucide-react';
// Loading placeholder for sections being generated
const SectionLoadingPlaceholder: React.FC<{ title: string; index: number; isCurrentlyGenerating: boolean }> = ({
title,
index,
isCurrentlyGenerating
}) => (
<section className="relative scroll-mt-32 animate-in fade-in duration-500">
<div className="flex gap-8">
<article className="flex-1 min-w-0">
<header className="mb-8">
<div className="flex items-center gap-4 mb-4">
<span className={`
flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center text-white font-bold text-lg shadow-lg
${isCurrentlyGenerating
? 'bg-gradient-to-br from-brand-500 to-purple-600 animate-pulse'
: 'bg-gray-300 dark:bg-gray-700'
}
`}>
{isCurrentlyGenerating ? (
<Loader2 size={20} className="animate-spin" />
) : (
index + 1
)}
</span>
<h2 className={`text-2xl md:text-3xl font-display font-bold leading-tight ${
isCurrentlyGenerating ? 'text-gray-900 dark:text-gray-50' : 'text-gray-400 dark:text-gray-600'
}`}>
{title}
</h2>
</div>
<div className={`w-20 h-1 rounded-full ${
isCurrentlyGenerating
? 'bg-gradient-to-r from-brand-500 to-purple-500 animate-pulse'
: 'bg-gray-200 dark:bg-gray-800'
}`} />
</header>
{/* Loading skeleton */}
<div className="space-y-4">
{isCurrentlyGenerating ? (
<>
<div className="flex items-center gap-2 text-sm text-brand-600 dark:text-brand-400 mb-4">
<Loader2 size={14} className="animate-spin" />
<span>Generating content...</span>
</div>
<div className="space-y-3">
<div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-full animate-pulse" />
<div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-11/12 animate-pulse delay-75" />
<div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-4/5 animate-pulse delay-100" />
<div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-full animate-pulse delay-150" />
<div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-3/4 animate-pulse delay-200" />
</div>
<div className="mt-6 p-4 rounded-xl bg-gray-100 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700">
<div className="h-32 bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse" />
</div>
</>
) : (
<div className="p-8 rounded-xl border-2 border-dashed border-gray-200 dark:border-gray-800 text-center">
<p className="text-gray-400 dark:text-gray-600 text-sm">
Waiting to be generated...
</p>
</div>
)}
</div>
</article>
</div>
{/* Section Divider */}
<div className="my-16 flex items-center gap-4">
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-gray-200 dark:via-gray-800 to-transparent" />
</div>
</section>
);
// Error state for failed sections
const SectionErrorState: React.FC<{ title: string; error: string; index: number }> = ({ title, error, index }) => (
<section className="relative scroll-mt-32">
<div className="flex gap-8">
<article className="flex-1 min-w-0">
<header className="mb-8">
<div className="flex items-center gap-4 mb-4">
<span className="flex-shrink-0 w-10 h-10 rounded-xl bg-red-500 flex items-center justify-center text-white font-bold text-lg shadow-lg">
<AlertCircle size={20} />
</span>
<h2 className="text-2xl md:text-3xl font-display font-bold text-gray-900 dark:text-gray-50 leading-tight">
{title}
</h2>
</div>
</header>
<div className="p-6 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
<div className="flex items-start gap-3">
<AlertCircle size={20} className="text-red-500 flex-shrink-0 mt-0.5" />
<div>
<p className="font-semibold text-red-700 dark:text-red-300">
Failed to generate this section
</p>
<p className="mt-1 text-sm text-red-600 dark:text-red-400">
{error}
</p>
</div>
</div>
</div>
</article>
</div>
<div className="my-16 flex items-center gap-4">
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-gray-300 dark:via-gray-700 to-transparent" />
</div>
</section>
);
interface Props {
sections: BlogSectionType[];
paperTitle: string;
theme: 'light' | 'dark';
onExport: () => void;
onShare: () => void;
isLoading?: boolean;
loadingStage?: 'idle' | 'analyzing' | 'generating';
currentSection?: number;
paperStructure?: PaperStructure | null;
}
const BlogView: React.FC<Props> = ({
sections,
paperTitle,
theme,
onExport,
onShare,
isLoading = false,
loadingStage = 'idle',
currentSection = -1,
paperStructure = null
}) => {
const [activeSection, setActiveSection] = useState<string>(sections[0]?.id || '');
const [readProgress, setReadProgress] = useState(0);
const contentRef = useRef<HTMLDivElement>(null);
// Calculate reading time (rough estimate: 200 words per minute)
const completedSections = sections.filter(s => !s.isLoading && s.content);
const totalWords = completedSections.reduce((acc, section) => {
return acc + (section.content?.split(/\s+/).length || 0);
}, 0);
const readingTime = Math.max(1, Math.ceil(totalWords / 200));
// Count completed sections
const completedCount = sections.filter(s => !s.isLoading && !s.error).length;
// Intersection Observer for active section tracking
useEffect(() => {
const options = {
root: null,
rootMargin: '-20% 0px -60% 0px',
threshold: 0
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const id = entry.target.id.replace('section-', '');
setActiveSection(id);
}
});
}, options);
sections.forEach((section) => {
const element = document.getElementById(`section-${section.id}`);
if (element) observer.observe(element);
});
return () => observer.disconnect();
}, [sections]);
// Scroll progress tracking
useEffect(() => {
const handleScroll = () => {
const winScroll = document.documentElement.scrollTop;
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
const scrolled = (winScroll / height) * 100;
setReadProgress(scrolled);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<div className="min-h-screen">
{/* Reading Progress Bar */}
<div className="fixed top-0 left-0 right-0 h-1 bg-gray-200 dark:bg-gray-800 z-50">
<div
className="h-full bg-gradient-to-r from-brand-500 via-purple-500 to-pink-500 transition-all duration-150"
style={{ width: `${readProgress}%` }}
/>
</div>
{/* Sidebar Navigation */}
{sections.length > 0 && (
<Sidebar
sections={sections}
activeSection={activeSection}
onSectionClick={setActiveSection}
/>
)}
{/* Main Content */}
<div className="lg:ml-72 xl:mr-8">
<div ref={contentRef} className="max-w-3xl mx-auto px-6 py-8">
{/* Loading State - Paper Analysis */}
{loadingStage === 'analyzing' && (
<div className="flex flex-col items-center justify-center min-h-[60vh] animate-in fade-in duration-500">
<div className="relative">
<div className="w-24 h-24 rounded-full bg-gradient-to-r from-brand-500 to-purple-600 animate-pulse flex items-center justify-center">
<Sparkles size={40} className="text-white animate-bounce" />
</div>
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-brand-500 to-purple-600 animate-ping opacity-20" />
</div>
<h2 className="mt-8 text-2xl font-display font-bold text-gray-900 dark:text-white">
Analyzing Paper Structure
</h2>
<p className="mt-3 text-gray-500 dark:text-gray-400 text-center max-w-md">
Understanding the paper's key contributions, methodology, and findings to create the optimal narrative structure...
</p>
</div>
)}
{/* Generation Progress Banner */}
{loadingStage === 'generating' && (
<div className="mb-8 p-4 rounded-2xl bg-gradient-to-r from-brand-50 to-purple-50 dark:from-brand-900/20 dark:to-purple-900/20 border border-brand-200 dark:border-brand-800 animate-in slide-in-from-top duration-500">
<div className="flex items-center gap-4">
<div className="flex-shrink-0">
<div className="w-10 h-10 rounded-full bg-brand-500 flex items-center justify-center">
<Loader2 size={20} className="text-white animate-spin" />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-semibold text-brand-700 dark:text-brand-300">
Generating sections...
</span>
<span className="text-sm text-brand-600 dark:text-brand-400">
{completedCount} / {sections.length}
</span>
</div>
<div className="h-2 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: `${(completedCount / sections.length) * 100}%` }}
/>
</div>
{currentSection >= 0 && sections[currentSection] && (
<p className="mt-2 text-xs text-gray-600 dark:text-gray-400 truncate">
Currently writing: <span className="font-medium">{sections[currentSection].title}</span>
</p>
)}
</div>
</div>
</div>
)}
{/* Article Header */}
{(sections.length > 0 || paperStructure) && (
<header className="mb-16 animate-in fade-in slide-in-from-bottom-8 duration-700">
{/* Paper Badge */}
<div className="flex items-center gap-2 mb-6">
<span className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300 text-xs font-semibold uppercase tracking-wider">
<FileText size={12} />
Research Summary
</span>
{loadingStage === 'generating' && (
<span className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300 text-xs font-semibold">
<Loader2 size={12} className="animate-spin" />
Generating...
</span>
)}
</div>
{/* Title */}
<h1 className="text-4xl md:text-5xl lg:text-6xl font-display font-bold text-gray-900 dark:text-white leading-[1.1] mb-8">
{(paperStructure?.paperTitle || paperTitle).replace('.pdf', '')}
</h1>
{/* Abstract Preview */}
{paperStructure?.paperAbstract && (
<p className="text-xl text-gray-600 dark:text-gray-300 leading-relaxed mb-8 italic border-l-4 border-brand-500 pl-4">
{paperStructure.paperAbstract}
</p>
)}
{/* Meta Info */}
<div className="flex flex-wrap items-center gap-6 text-sm text-gray-500 dark:text-gray-400">
<div className="flex items-center gap-2">
<Clock size={16} />
<span>{readingTime} min read</span>
</div>
<div className="flex items-center gap-2">
<BookOpen size={16} />
<span>{sections.length} sections</span>
</div>
{completedCount === sections.length && sections.length > 0 && (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<CheckCircle2 size={16} />
<span>Complete</span>
</div>
)}
<div className="flex items-center gap-1 ml-auto">
<button
onClick={onShare}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
title="Share"
>
<Share2 size={18} />
</button>
<button
onClick={onExport}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
title="Export"
>
<Download size={18} />
</button>
</div>
</div>
{/* Decorative Line */}
<div className="mt-10 flex items-center gap-4">
<div className="flex-1 h-px bg-gradient-to-r from-brand-500 via-purple-500 to-transparent" />
<div className="w-2 h-2 rounded-full bg-brand-500 animate-pulse" />
</div>
</header>
)}
{/* Key Contribution Highlight */}
{paperStructure?.mainContribution && (
<div className="mb-16 p-8 rounded-2xl bg-gradient-to-br from-brand-50 to-purple-50 dark:from-brand-900/20 dark:to-purple-900/20 border border-brand-200 dark:border-brand-800 shadow-xl shadow-brand-200/20 dark:shadow-none animate-in fade-in slide-in-from-bottom-4 duration-700">
<div className="flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-brand-600 dark:text-brand-400 mb-4">
<Sparkles size={14} />
Key Contribution
</div>
<p className="text-xl md:text-2xl leading-relaxed text-gray-800 dark:text-gray-200 font-medium">
{paperStructure.mainContribution}
</p>
</div>
)}
{/* Sections */}
<div className="space-y-4">
{sections.map((section, index) => (
<div key={section.id}>
{section.isLoading ? (
<SectionLoadingPlaceholder
title={section.title}
index={index}
isCurrentlyGenerating={index === currentSection}
/>
) : section.error ? (
<SectionErrorState title={section.title} error={section.error} index={index} />
) : (
<BlogSectionComponent
section={section}
theme={theme}
index={index}
/>
)}
</div>
))}
</div>
{/* Footer */}
<footer className="mt-20 pt-10 border-t border-gray-200 dark:border-gray-800">
<div className="text-center">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
Generated with PaperStack • Powered by Gemini AI
</p>
<div className="flex justify-center gap-4">
<button
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
className="px-6 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-sm font-semibold transition-colors"
>
Back to Top ↑
</button>
</div>
</div>
</footer>
</div>
</div>
</div>
);
};
export default BlogView;