import { AlignLeft, BookCopy, Bookmark, FileText, Heart, Menu, MessageCircle, Share2, X, } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { useLocation } from "wouter"; import { AuthorChip } from "@/components/common/AuthorChip"; import { LanguageTag } from "@/components/common/LanguageTag"; import { AnnotationSystem } from "@/components/reading/AnnotationSystem"; import { EnhancedLineNumberedText } from "@/components/reading/EnhancedLineNumberedText"; import { ReadingControls } from "@/components/reading/ReadingControls"; import { TranslationSelector } from "@/components/reading/TranslationSelector"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, } from "@/components/ui/drawer"; import { useMediaQuery } from "@/hooks/use-media-query"; import { useReadingSettings } from "@/hooks/use-reading-settings"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; import type { TranslationWithDetails, WorkWithDetails } from "@/lib/types"; interface EnhancedReadingViewProps { work: WorkWithDetails; translations: TranslationWithDetails[]; } export function EnhancedReadingView({ work, translations, }: EnhancedReadingViewProps) { const { settings, increaseFontSize, decreaseFontSize, toggleZenMode } = useReadingSettings(); const [selectedTranslationId, setSelectedTranslationId] = useState< number | undefined >(translations.length > 0 ? translations[0].id : undefined); const [readingProgress, setReadingProgress] = useState(0); const [selectedLineNumber, setSelectedLineNumber] = useState( null, ); const [isAnnotationOpen, setIsAnnotationOpen] = useState(false); const [isActionPanelOpen, setIsActionPanelOpen] = useState(false); const [isLiked, setIsLiked] = useState(false); const [isBookmarked, setIsBookmarked] = useState(false); const isMobile = useMediaQuery("(max-width: 768px)"); const { toast } = useToast(); const [, navigate] = useLocation(); const mainContentRef = useRef(null); // Get the selected translation const selectedTranslation = translations.find( (t) => t.id === selectedTranslationId, ); // Determine if original text is selected const isOriginalSelected = !selectedTranslationId; // Content to display - either the translation or original work const contentToDisplay = selectedTranslation ? selectedTranslation.content : work.content; // Handler for viewing original text const handleViewOriginal = () => { setSelectedTranslationId(undefined); }; // Check if there's a line number in the URL hash useEffect(() => { if (window.location.hash) { const hash = window.location.hash; const lineMatch = hash.match(/^#line-(\d+)$/); if (lineMatch?.[1]) { const lineNumber = parseInt(lineMatch[1], 10); // Scroll to the line setTimeout(() => { const lineElement = document.getElementById(`line-${lineNumber}`); if (lineElement) { lineElement.scrollIntoView({ behavior: "smooth", block: "center" }); setSelectedLineNumber(lineNumber); setIsAnnotationOpen(true); } }, 500); } } }, []); // Update reading progress in backend const updateReadingProgress = useCallback( 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 ? Number(selectedTranslationId) : undefined, progress, }); } catch (error) { console.error("Failed to update reading progress:", error); } }, [work.id, selectedTranslationId], ); // 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); }, [updateReadingProgress]); // Handle line annotation const handleLineAnnotation = (lineNumber: number) => { setSelectedLineNumber(lineNumber); setIsAnnotationOpen(true); // Update the URL hash window.history.replaceState(null, "", `#line-${lineNumber}`); // On mobile, scroll to top of content to see the annotation if (isMobile && mainContentRef.current) { mainContentRef.current.scrollIntoView({ behavior: "smooth" }); } }; // Close annotation panel const handleCloseAnnotation = () => { setIsAnnotationOpen(false); // Remove the line number from the URL hash window.history.replaceState( null, "", window.location.pathname + window.location.search, ); }; // Toggle like for the work const handleLikeToggle = () => { setIsLiked(!isLiked); toast({ description: isLiked ? "Removed from favorites" : "Added to favorites", }); }; // Toggle bookmark for the work const handleBookmarkToggle = () => { setIsBookmarked(!isBookmarked); toast({ description: isBookmarked ? "Removed from your bookmarks" : "Added to your bookmarks", }); }; // Share the work const handleShare = async () => { try { if (navigator.share) { await navigator.share({ title: work.title, text: `Reading ${work.title} on Tercul`, url: window.location.href, }); } else { // Fallback for browsers that don't support the Web Share API navigator.clipboard.writeText(window.location.href); toast({ description: "Link copied to clipboard", }); } } catch (error) { console.error("Error sharing:", error); } }; return (
{/* Mobile contextual menu */} {isMobile && (

{work.title}

)} {/* Context sidebar (sticky on desktop, drawer on mobile) */} {!isMobile ? ( ) : ( About this work
{work.year && (

Written in {work.year}

)}

Tags

{work.tags?.map((tag) => ( {tag.name} ))}
{selectedTranslation && (

Translation

{selectedTranslation.language}

Translated by User {selectedTranslation.translatorId}{" "} {selectedTranslation.year && `(${selectedTranslation.year})`}

)}

Reading stats

~{Math.ceil(contentToDisplay.length / 1000)} min read

{work.likes || 0} favorites

{readingProgress}% completed

Actions

)} {/* Main reading area */}
{!isMobile && ( )}

{work.title}

{/* Text content with enhanced annotation features */}
{selectedTranslation?.notes && (

Translation Notes

{selectedTranslation.notes}

)}
{/* Progress bar (fixed at bottom) - only visible when not in zen mode */} {!settings.zenMode && (
)} {/* Annotation panel for desktop */} {!isMobile && isAnnotationOpen && selectedLineNumber && ( )} {/* Mobile annotation drawer */} {isMobile && ( { if (!open) handleCloseAnnotation(); }} >
Line {selectedLineNumber} Annotations
{selectedLineNumber && ( )}
)} {/* Mobile reading controls */} {isMobile && (
)}
); }