mirror of
https://github.com/SamyRai/tercul-frontend.git
synced 2025-12-27 04:51:34 +00:00
Sets up the project with initial files, components, routes, and UI elements. Replit-Commit-Author: Agent Replit-Commit-Session-Id: cbacfb18-842a-4116-a907-18c0105ad8ec Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/39b5c689-6e8a-4d5a-9792-69cc81a56534/affc56b0-365e-4ece-9cba-9e70bbbf0893.jpg
256 lines
11 KiB
TypeScript
256 lines
11 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { LineNumberedText } from "@/components/reading/LineNumberedText";
|
|
import { TranslationSelector } from "@/components/reading/TranslationSelector";
|
|
import { ReadingControls } from "@/components/reading/ReadingControls";
|
|
import { useReadingSettings } from "@/hooks/use-reading-settings";
|
|
import { WorkWithDetails, TranslationWithDetails } from "@/lib/types";
|
|
import { AuthorChip } from "@/components/common/AuthorChip";
|
|
import { LanguageTag } from "@/components/common/LanguageTag";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { apiRequest } from "@/lib/queryClient";
|
|
|
|
interface ReadingViewProps {
|
|
work: WorkWithDetails;
|
|
translations: TranslationWithDetails[];
|
|
}
|
|
|
|
export function ReadingView({ work, translations }: ReadingViewProps) {
|
|
const { settings, increaseFontSize, decreaseFontSize, toggleZenMode } = useReadingSettings();
|
|
const [selectedTranslationId, setSelectedTranslationId] = useState<number | undefined>(
|
|
translations.length > 0 ? translations[0].id : undefined
|
|
);
|
|
const [readingProgress, setReadingProgress] = useState(0);
|
|
|
|
// Get the selected translation
|
|
const selectedTranslation = translations.find(t => t.id === selectedTranslationId);
|
|
|
|
// Content to display - either the translation or original work
|
|
const contentToDisplay = selectedTranslation ? selectedTranslation.content : work.content;
|
|
|
|
// Update reading progress as user scrolls
|
|
useEffect(() => {
|
|
const handleScroll = () => {
|
|
const windowHeight = window.innerHeight;
|
|
const documentHeight = document.documentElement.scrollHeight;
|
|
const scrollTop = window.scrollY;
|
|
|
|
// Calculate progress percentage
|
|
const progress = Math.min(
|
|
100,
|
|
Math.round((scrollTop / (documentHeight - windowHeight)) * 100)
|
|
);
|
|
|
|
setReadingProgress(progress);
|
|
|
|
// Update reading progress in backend (throttled to avoid too many requests)
|
|
const debounced = setTimeout(() => {
|
|
updateReadingProgress(progress);
|
|
}, 2000);
|
|
|
|
return () => clearTimeout(debounced);
|
|
};
|
|
|
|
window.addEventListener('scroll', handleScroll);
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
}, [work.id, selectedTranslationId]);
|
|
|
|
// Update reading progress in backend
|
|
const updateReadingProgress = async (progress: number) => {
|
|
try {
|
|
// In a real app, this would use the logged-in user ID
|
|
// For demo purposes, we'll use a hard-coded user ID of 1
|
|
await apiRequest('POST', '/api/reading-progress', {
|
|
userId: 1,
|
|
workId: work.id,
|
|
translationId: selectedTranslationId,
|
|
progress
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to update reading progress:', error);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<section className={`reading-view ${settings.zenMode ? 'zen-mode' : ''}`}>
|
|
<div className="flex flex-col lg:flex-row max-w-[var(--content-width)] mx-auto">
|
|
{/* Context sidebar (sticky on desktop) */}
|
|
<aside className="context-sidebar lg:w-64 p-4 lg:sticky lg:top-16 lg:self-start lg:h-[calc(100vh-4rem)] lg:overflow-y-auto">
|
|
<div className="mb-6">
|
|
<AuthorChip author={work.author} withLifeDates />
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<h4 className="text-xs uppercase tracking-wider text-navy/60 dark:text-cream/60 font-sans font-medium mb-2">About this work</h4>
|
|
<div className="space-y-2">
|
|
<div>
|
|
{work.year && (
|
|
<p className="text-xs text-navy/70 dark:text-cream/70 font-sans">Written in {work.year}</p>
|
|
)}
|
|
<p className="text-xs text-navy/70 dark:text-cream/70 font-sans capitalize">
|
|
{work.type} • {work.tags.map(tag => tag.name).join(' • ')}
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{work.tags.map(tag => (
|
|
<Badge
|
|
key={tag.id}
|
|
variant="outline"
|
|
className="inline-block px-2 py-0.5 bg-navy/10 dark:bg-navy/20 rounded text-navy/70 dark:text-cream/70 text-xs font-sans border-none"
|
|
>
|
|
{tag.name}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{selectedTranslation && (
|
|
<div className="mb-4">
|
|
<h4 className="text-xs uppercase tracking-wider text-navy/60 dark:text-cream/60 font-sans font-medium mb-2">Translation</h4>
|
|
<div>
|
|
<p className="text-sm text-navy/90 dark:text-cream/90 font-sans font-medium">{selectedTranslation.language}</p>
|
|
<p className="text-xs text-navy/70 dark:text-cream/70 font-sans">
|
|
Translated by User {selectedTranslation.translatorId} {selectedTranslation.year && `(${selectedTranslation.year})`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mb-4">
|
|
<h4 className="text-xs uppercase tracking-wider text-navy/60 dark:text-cream/60 font-sans font-medium mb-2">Reading stats</h4>
|
|
<div>
|
|
<p className="text-xs text-navy/70 dark:text-cream/70 font-sans">~{Math.ceil(contentToDisplay.length / 1000)} min read</p>
|
|
<p className="text-xs text-navy/70 dark:text-cream/70 font-sans">{work.likes || 0} favorites</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<h4 className="text-xs uppercase tracking-wider text-navy/60 dark:text-cream/60 font-sans font-medium mb-2">Actions</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="btn-feedback flex items-center gap-1 py-1.5 px-3 rounded-lg bg-russet/10 hover:bg-russet/20 dark:bg-russet/20 dark:hover:bg-russet/30 text-russet dark:text-russet/90 font-sans text-xs transition-colors"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="h-4 w-4"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
|
</svg>
|
|
<span>Favorite</span>
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="btn-feedback flex items-center gap-1 py-1.5 px-3 rounded-lg bg-navy/10 hover:bg-navy/20 dark:bg-navy/20 dark:hover:bg-navy/30 text-navy dark:text-cream/90 font-sans text-xs transition-colors"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="h-4 w-4"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
|
|
</svg>
|
|
<span>Share</span>
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="btn-feedback flex items-center gap-1 py-1.5 px-3 rounded-lg bg-navy/10 hover:bg-navy/20 dark:bg-navy/20 dark:hover:bg-navy/30 text-navy dark:text-cream/90 font-sans text-xs transition-colors"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="h-4 w-4"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
|
|
</svg>
|
|
<span>Comment</span>
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="btn-feedback flex items-center gap-1 py-1.5 px-3 rounded-lg bg-navy/10 hover:bg-navy/20 dark:bg-navy/20 dark:hover:bg-navy/30 text-navy dark:text-cream/90 font-sans text-xs transition-colors"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="h-4 w-4"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<span>Cite</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main reading area */}
|
|
<div className="flex-1 px-4 lg:px-8 py-6 lg:py-8">
|
|
<div className="mb-6">
|
|
<ReadingControls
|
|
onZenModeToggle={toggleZenMode}
|
|
onIncreaseFontSize={increaseFontSize}
|
|
onDecreaseFontSize={decreaseFontSize}
|
|
zenMode={settings.zenMode}
|
|
workId={work.id}
|
|
workSlug={work.slug}
|
|
translationId={selectedTranslationId}
|
|
/>
|
|
|
|
<div className="flex items-center gap-3 mt-2">
|
|
<h2 className="text-xl font-serif text-navy/80 dark:text-cream/80">{work.title}</h2>
|
|
<LanguageTag language={work.language} />
|
|
</div>
|
|
|
|
{translations.length > 0 && (
|
|
<TranslationSelector
|
|
translations={translations}
|
|
currentTranslationId={selectedTranslationId}
|
|
workSlug={work.slug}
|
|
onSelectTranslation={setSelectedTranslationId}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Text content */}
|
|
<div className="reading-container max-w-[var(--reading-width)] mx-auto">
|
|
<LineNumberedText
|
|
content={contentToDisplay}
|
|
fontSizeClass={settings.fontSize}
|
|
/>
|
|
|
|
{selectedTranslation && selectedTranslation.notes && (
|
|
<div className="mt-8 border-t border-sage/20 dark:border-sage/10 pt-4">
|
|
<h3 className="text-lg font-medium font-serif text-navy dark:text-cream mb-3">Translation Notes</h3>
|
|
<div className="text-sm text-navy/80 dark:text-cream/80">
|
|
<p>{selectedTranslation.notes}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress bar (fixed at bottom) */}
|
|
<div className="progress-bar fixed bottom-0 left-0 right-0 h-1 bg-sage/20 dark:bg-sage/10">
|
|
<div
|
|
className="progress-indicator h-full bg-russet dark:bg-russet/90"
|
|
style={{ width: `${readingProgress}%` }}
|
|
></div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|