diff --git a/client/src/App.tsx b/client/src/App.tsx index ab95290..3937981 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -9,7 +9,7 @@ import Explore from "@/pages/Explore"; import Search from "@/pages/Search"; import AuthorProfile from "@/pages/authors/AuthorProfile"; import Authors from "@/pages/authors/Authors"; -import NewWorkReading from "@/pages/works/NewWorkReading"; +import SimpleWorkReading from "@/pages/works/SimpleWorkReading"; import WorkCompare from "@/pages/works/WorkCompare"; import Collections from "@/pages/collections/Collections"; import CreateCollection from "@/pages/collections/CreateCollection"; @@ -25,7 +25,7 @@ function Router() { - + diff --git a/client/src/pages/works/NewWorkReading.tsx b/client/src/pages/works/NewWorkReading.tsx index e1f0070..5d6c277 100644 --- a/client/src/pages/works/NewWorkReading.tsx +++ b/client/src/pages/works/NewWorkReading.tsx @@ -322,16 +322,9 @@ export default function NewWorkReading() { const lines = contentToLines(content); const totalPages = Math.ceil(lines.length / linesPerPage); - // Make sure active page is in bounds but without causing re-renders + // Make sure active page is in bounds const safePage = Math.min(Math.max(1, activePage), Math.max(1, totalPages)); - // Only update active page in a useEffect to avoid infinite re-render - useEffect(() => { - if (safePage !== activePage) { - setActivePage(safePage); - } - }, [safePage, activePage]); - const startIdx = (safePage - 1) * linesPerPage; const endIdx = startIdx + linesPerPage; @@ -342,6 +335,20 @@ export default function NewWorkReading() { startLineNumber: startIdx + 1 }; }; + + // Add a separate effect to handle page bounds + useEffect(() => { + if (work) { + const content = getSelectedContent(); + const lines = contentToLines(content); + const totalPages = Math.ceil(lines.length / 20); + const safePage = Math.min(Math.max(1, activePage), Math.max(1, totalPages)); + + if (safePage !== activePage) { + setActivePage(safePage); + } + } + }, [work, activePage, selectedTranslationId]); // Toggle bookmark status const handleBookmarkToggle = () => { diff --git a/client/src/pages/works/SimpleWorkReading.tsx b/client/src/pages/works/SimpleWorkReading.tsx new file mode 100644 index 0000000..0a694b8 --- /dev/null +++ b/client/src/pages/works/SimpleWorkReading.tsx @@ -0,0 +1,366 @@ +import { useState, useEffect, useRef } from "react"; +import { useParams, useLocation } from "wouter"; +import { useQuery } from "@tanstack/react-query"; +import { WorkWithDetails, TranslationWithDetails } from "@/lib/types"; +import { PageLayout } from "@/components/layout/PageLayout"; + +// UI Components +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; +import { toast } from "@/hooks/use-toast"; +import { AuthorChip } from "@/components/common/AuthorChip"; +import { useMediaQuery } from "@/hooks/use-media-query"; + +// Icons +import { + BookOpen, + ArrowLeft, + ArrowRight, + Languages, + Heart, + Bookmark, + Share2 +} from "lucide-react"; + +export default function SimpleWorkReading() { + const { slug } = useParams(); + const [, navigate] = useLocation(); + const isMobile = useMediaQuery("(max-width: 768px)"); + + // Main content states + const [activePage, setActivePage] = useState(1); + const [selectedTranslationId, setSelectedTranslationId] = useState(undefined); + const [isBookmarked, setIsBookmarked] = useState(false); + const [isLiked, setIsLiked] = useState(false); + const contentRef = useRef(null); + + // Queries + const { data: work, isLoading: workLoading, error: workError } = useQuery({ + queryKey: [`/api/works/${slug}`], + }); + + const { data: translations, isLoading: translationsLoading } = useQuery({ + queryKey: [`/api/works/${slug}/translations`], + enabled: !!work, + }); + + // Page validation effect + useEffect(() => { + if (work) { + const content = getSelectedContent(); + const lines = contentToLines(content); + const totalPages = Math.ceil(lines.length / 20); + const safePage = Math.min(Math.max(1, activePage), Math.max(1, totalPages)); + + if (safePage !== activePage) { + setActivePage(safePage); + } + } + }, [work, activePage, selectedTranslationId]); + + // Get the selected translation content + const getSelectedContent = () => { + if (!work) return ""; + if (!selectedTranslationId) return work.content; + + const translation = translations?.find(t => t.id === selectedTranslationId); + return translation?.content || work.content; + }; + + // Split content into lines and pages for display + const contentToLines = (content: string) => { + return content.split('\n').filter(line => line.length > 0); + }; + + const getPagedContent = (content: string, linesPerPage = 20) => { + const lines = contentToLines(content); + const totalPages = Math.ceil(lines.length / linesPerPage); + + // Make sure active page is in bounds + const safePage = Math.min(Math.max(1, activePage), Math.max(1, totalPages)); + + const startIdx = (safePage - 1) * linesPerPage; + const endIdx = startIdx + linesPerPage; + + return { + lines: lines.slice(startIdx, endIdx), + page: safePage, + totalPages, + startLineNumber: startIdx + 1 + }; + }; + + // Toggle bookmark status + const handleBookmarkToggle = () => { + setIsBookmarked(!isBookmarked); + toast({ + description: isBookmarked + ? "Removed from your bookmarks" + : "Added to your bookmarks", + duration: 3000, + }); + }; + + // Toggle like status + const handleLikeToggle = () => { + setIsLiked(!isLiked); + toast({ + description: isLiked + ? "Removed from your favorites" + : "Added to your favorites", + duration: 3000, + }); + }; + + // Share the work + const handleShare = async () => { + try { + if (navigator.share) { + await navigator.share({ + title: work?.title || "Literary Work", + text: `Reading ${work?.title || "a work"} on Tercul`, + url: window.location.href + }); + } else { + navigator.clipboard.writeText(window.location.href); + toast({ + description: "Link copied to clipboard", + duration: 3000, + }); + } + } catch (error) { + console.error('Error sharing:', error); + } + }; + + // Handle navigation between pages + const handleNextPage = () => { + if (!work) return; + const { totalPages } = getPagedContent(getSelectedContent()); + if (activePage < totalPages) { + setActivePage(activePage + 1); + // Scroll to top of content area + if (contentRef.current) { + contentRef.current.scrollIntoView({ behavior: 'smooth' }); + } + } + }; + + const handlePreviousPage = () => { + if (activePage > 1) { + setActivePage(activePage - 1); + // Scroll to top of content area + if (contentRef.current) { + contentRef.current.scrollIntoView({ behavior: 'smooth' }); + } + } + }; + + // Loading state + if (workLoading) { + return ( + +
+
+ +
+ + +
+
+ +
+
+ +
+ +
+ +
+ {Array.from({ length: 15 }).map((_, i) => ( + + ))} +
+
+
+
+
+ ); + } + + // Error state + if (workError || !work) { + return ( + +
+

Work not found

+

The literary work you're looking for could not be found.

+
+ + +
+
+
+ ); + } + + // Get content for current view and page + const mainContent = getPagedContent(getSelectedContent()); + + // Get the selected translation details + const selectedTranslation = translations?.find(t => t.id === selectedTranslationId); + + // Calculate reading time estimation + const wordCount = getSelectedContent().split(/\s+/).length; + const readingTimeMinutes = Math.ceil(wordCount / 200); // Average reading speed + + return ( + +
+ {/* Work header */} +
+

{work.title}

+
+ + {work.year && Published: {work.year}} + Reading time: ~{readingTimeMinutes} min +
+ +
+ {work.tags?.map(tag => ( + + {tag.name} + + ))} +
+ +
+ + + + + +
+
+ + {/* Translation selector */} + {translations && translations.length > 0 && ( +
+

Translations

+
+ + + {translations.map(translation => ( + + ))} +
+
+ )} + + {/* Reading content */} +
+ + + Text + Annotations + + + + + {mainContent.lines.map((line, i) => ( +
+ {line} +
+ ))} +
+
+ + + +

No annotations yet.

+
+
+
+
+ + {/* Pagination controls */} + {mainContent.totalPages > 1 && ( +
+ + +
+ Page {activePage} of {mainContent.totalPages} +
+ + +
+ )} +
+
+ ); +} \ No newline at end of file