tercul-frontend/client/src/components/reading/ReadingView.tsx
mukimovd 024e5d0ef5 Introduce the core functionality and basic structure of the platform
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
2025-05-01 03:05:33 +00:00

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>
);
}