mirror of
https://github.com/SamyRai/tercul-frontend.git
synced 2025-12-27 00:11:35 +00:00
This commit addresses 275 TypeScript compilation errors and improves type safety, code quality, and maintainability across the frontend codebase. The following issues have been resolved: - Standardized `translationId` to `number` - Fixed missing properties on annotation types - Resolved `tags` type mismatch - Corrected `country` type mismatch - Addressed date vs. string mismatches - Fixed variable hoisting issues - Improved server-side type safety - Added missing null/undefined checks - Fixed arithmetic operations on non-numbers - Resolved `RefObject` type issues Note: I was unable to verify the frontend changes due to local setup issues with the development server. The server would not start, and I was unable to run the Playwright tests. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
716 lines
23 KiB
TypeScript
716 lines
23 KiB
TypeScript
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<number | null>(
|
|
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<HTMLDivElement>(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 (
|
|
<section
|
|
className={`enhanced-reading-view ${settings.zenMode ? "zen-mode" : ""}`}
|
|
>
|
|
<div
|
|
className={`flex flex-col lg:flex-row max-w-6xl mx-auto relative pb-12 ${isAnnotationOpen && !isMobile ? "mr-96" : ""}`}
|
|
>
|
|
{/* Mobile contextual menu */}
|
|
{isMobile && (
|
|
<div className="sticky top-0 z-10 bg-cream dark:bg-dark-surface w-full border-b border-sage/20 dark:border-sage/10 flex justify-between items-center px-4 py-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setIsActionPanelOpen(true)}
|
|
className="text-navy/70 dark:text-cream/70"
|
|
>
|
|
<Menu className="h-5 w-5" />
|
|
<span className="sr-only">Menu</span>
|
|
</Button>
|
|
|
|
<h2 className="truncate text-navy dark:text-cream font-medium text-sm">
|
|
{work.title}
|
|
</h2>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={`p-2 ${
|
|
isLiked ? "text-russet" : "text-navy/70 dark:text-cream/70"
|
|
}`}
|
|
onClick={handleLikeToggle}
|
|
>
|
|
<Heart className={`h-5 w-5 ${isLiked ? "fill-russet" : ""}`} />
|
|
<span className="sr-only">{isLiked ? "Unlike" : "Like"}</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="p-2 text-navy/70 dark:text-cream/70"
|
|
onClick={handleShare}
|
|
>
|
|
<Share2 className="h-5 w-5" />
|
|
<span className="sr-only">Share</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Context sidebar (sticky on desktop, drawer on mobile) */}
|
|
{!isMobile ? (
|
|
<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.language} • {work.tags?.length || 0}{" "}
|
|
tags
|
|
</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 className="bg-sage/10 rounded-full h-1.5 mt-2">
|
|
<div
|
|
className="bg-russet h-full rounded-full"
|
|
style={{ width: `${readingProgress}%` }}
|
|
></div>
|
|
</div>
|
|
<p className="text-xs text-navy/70 dark:text-cream/70 font-sans mt-1">
|
|
{readingProgress}% completed
|
|
</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="space-y-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className={`flex items-center gap-1 w-full justify-start py-1.5 px-3 rounded-lg ${
|
|
isLiked
|
|
? "bg-russet/10 hover:bg-russet/20 text-russet"
|
|
: "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`}
|
|
onClick={handleLikeToggle}
|
|
>
|
|
<Heart
|
|
className={`h-4 w-4 ${isLiked ? "fill-russet" : ""}`}
|
|
/>
|
|
<span>
|
|
{isLiked ? "Remove from favorites" : "Add to favorites"}
|
|
</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className={`flex items-center gap-1 w-full justify-start py-1.5 px-3 rounded-lg ${
|
|
isBookmarked
|
|
? "bg-russet/10 hover:bg-russet/20 text-russet"
|
|
: "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`}
|
|
onClick={handleBookmarkToggle}
|
|
>
|
|
<Bookmark
|
|
className={`h-4 w-4 ${isBookmarked ? "fill-russet" : ""}`}
|
|
/>
|
|
<span>
|
|
{isBookmarked ? "Remove bookmark" : "Bookmark for later"}
|
|
</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-1 w-full justify-start 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"
|
|
onClick={handleShare}
|
|
>
|
|
<Share2 className="h-4 w-4" />
|
|
<span>Share</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-1 w-full justify-start 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"
|
|
onClick={() => navigate(`/works/${work.slug}/comments`)}
|
|
>
|
|
<MessageCircle className="h-4 w-4" />
|
|
<span>View all comments</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-1 w-full justify-start 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"
|
|
onClick={() => navigate(`/works/${work.slug}/cite`)}
|
|
>
|
|
<FileText className="h-4 w-4" />
|
|
<span>Cite this work</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-1 w-full justify-start 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"
|
|
onClick={() => navigate(`/collections/add/${work.slug}`)}
|
|
>
|
|
<BookCopy className="h-4 w-4" />
|
|
<span>Add to collection</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
) : (
|
|
<Drawer open={isActionPanelOpen} onOpenChange={setIsActionPanelOpen}>
|
|
<DrawerContent className="max-h-[90%]">
|
|
<DrawerHeader>
|
|
<DrawerTitle>About this work</DrawerTitle>
|
|
<DrawerDescription>
|
|
<div className="mb-2">
|
|
<AuthorChip author={work.author} withLifeDates />
|
|
</div>
|
|
{work.year && (
|
|
<p className="text-xs text-navy/70 dark:text-cream/70 font-sans">
|
|
Written in {work.year}
|
|
</p>
|
|
)}
|
|
</DrawerDescription>
|
|
</DrawerHeader>
|
|
|
|
<div className="px-4">
|
|
<div className="my-4">
|
|
<h4 className="text-xs uppercase tracking-wider text-navy/60 dark:text-cream/60 font-sans font-medium mb-2">
|
|
Tags
|
|
</h4>
|
|
<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>
|
|
|
|
{selectedTranslation && (
|
|
<div className="my-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="my-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 className="bg-sage/10 rounded-full h-1.5 mt-2">
|
|
<div
|
|
className="bg-russet h-full rounded-full"
|
|
style={{ width: `${readingProgress}%` }}
|
|
></div>
|
|
</div>
|
|
<p className="text-xs text-navy/70 dark:text-cream/70 font-sans mt-1">
|
|
{readingProgress}% completed
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="my-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="grid grid-cols-2 gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className={`flex items-center gap-1 justify-start py-1.5 px-3 rounded-lg ${
|
|
isBookmarked
|
|
? "bg-russet/10 hover:bg-russet/20 text-russet"
|
|
: "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`}
|
|
onClick={handleBookmarkToggle}
|
|
>
|
|
<Bookmark
|
|
className={`h-4 w-4 ${isBookmarked ? "fill-russet" : ""}`}
|
|
/>
|
|
<span>{isBookmarked ? "Remove" : "Bookmark"}</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-1 justify-start 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"
|
|
onClick={() => navigate(`/works/${work.slug}/comments`)}
|
|
>
|
|
<MessageCircle className="h-4 w-4" />
|
|
<span>Comments</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-1 justify-start 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"
|
|
onClick={() => navigate(`/works/${work.slug}/cite`)}
|
|
>
|
|
<FileText className="h-4 w-4" />
|
|
<span>Cite</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-1 justify-start 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"
|
|
onClick={() => navigate(`/collections/add/${work.slug}`)}
|
|
>
|
|
<BookCopy className="h-4 w-4" />
|
|
<span>Add to collection</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DrawerFooter>
|
|
<DrawerClose asChild>
|
|
<Button variant="outline">Close</Button>
|
|
</DrawerClose>
|
|
</DrawerFooter>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
)}
|
|
|
|
{/* Main reading area */}
|
|
<div ref={mainContentRef} className="flex-1 px-4 lg:px-8 py-4 lg:py-8">
|
|
<div className="mb-6">
|
|
{!isMobile && (
|
|
<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 md:text-2xl font-serif text-navy/80 dark:text-cream/80 font-medium">
|
|
{work.title}
|
|
</h2>
|
|
<LanguageTag language={work.language} />
|
|
</div>
|
|
|
|
<TranslationSelector
|
|
translations={translations}
|
|
currentTranslationId={selectedTranslationId}
|
|
workSlug={work.slug}
|
|
workLanguage={work.language}
|
|
onSelectTranslation={setSelectedTranslationId}
|
|
onViewOriginal={handleViewOriginal}
|
|
isOriginalSelected={isOriginalSelected}
|
|
/>
|
|
</div>
|
|
|
|
{/* Text content with enhanced annotation features */}
|
|
<div className="reading-container max-w-[var(--reading-width)] mx-auto">
|
|
<EnhancedLineNumberedText
|
|
content={contentToDisplay}
|
|
fontSizeClass={settings.fontSize}
|
|
onAnnotate={handleLineAnnotation}
|
|
highlightedLine={selectedLineNumber || undefined}
|
|
workId={work.id}
|
|
/>
|
|
|
|
{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) - only visible when not in zen mode */}
|
|
{!settings.zenMode && (
|
|
<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>
|
|
)}
|
|
|
|
{/* Annotation panel for desktop */}
|
|
{!isMobile && isAnnotationOpen && selectedLineNumber && (
|
|
<AnnotationSystem
|
|
workId={work.id}
|
|
selectedLineNumber={selectedLineNumber}
|
|
onClose={handleCloseAnnotation}
|
|
translationId={selectedTranslationId}
|
|
/>
|
|
)}
|
|
|
|
{/* Mobile annotation drawer */}
|
|
{isMobile && (
|
|
<Drawer
|
|
open={isAnnotationOpen && !!selectedLineNumber}
|
|
onOpenChange={(open) => {
|
|
if (!open) handleCloseAnnotation();
|
|
}}
|
|
>
|
|
<DrawerContent className="max-h-[80%]">
|
|
<DrawerHeader className="border-b border-sage/20 dark:border-sage/10">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<MessageCircle className="h-5 w-5 text-russet" />
|
|
<DrawerTitle>
|
|
Line {selectedLineNumber} Annotations
|
|
</DrawerTitle>
|
|
</div>
|
|
</div>
|
|
</DrawerHeader>
|
|
|
|
<div className="p-4 overflow-auto">
|
|
{selectedLineNumber && (
|
|
<AnnotationSystem
|
|
workId={work.id}
|
|
selectedLineNumber={selectedLineNumber}
|
|
onClose={handleCloseAnnotation}
|
|
translationId={selectedTranslationId}
|
|
/>
|
|
)}
|
|
</div>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
)}
|
|
|
|
{/* Mobile reading controls */}
|
|
{isMobile && (
|
|
<div className="fixed bottom-1 right-4 flex space-x-1">
|
|
<Button
|
|
size="icon"
|
|
className="rounded-full shadow-lg w-10 h-10 bg-russet text-white"
|
|
onClick={() => {
|
|
if (selectedLineNumber) {
|
|
setIsAnnotationOpen(true);
|
|
} else {
|
|
toast({
|
|
description: "Tap on a line to add annotations",
|
|
duration: 3000,
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
<MessageCircle className="h-5 w-5" />
|
|
<span className="sr-only">Annotations</span>
|
|
</Button>
|
|
|
|
<Button
|
|
size="icon"
|
|
className="rounded-full shadow-lg w-10 h-10 bg-navy dark:bg-navy/80 text-white"
|
|
onClick={toggleZenMode}
|
|
>
|
|
{settings.zenMode ? (
|
|
<AlignLeft className="h-5 w-5" />
|
|
) : (
|
|
<X className="h-5 w-5" />
|
|
)}
|
|
<span className="sr-only">
|
|
{settings.zenMode ? "Exit zen mode" : "Zen mode"}
|
|
</span>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|