Files
examjam-frontend/epaper-client/src/components/NewspaperViewer.tsx
2025-07-03 01:43:25 +06:00

281 lines
8.0 KiB
TypeScript

import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { useDateContext } from "@/context/DateContext";
import { useEffect, useMemo, useState } from "react";
import {
autoPlaceArticles,
createArticlesFromFilenames,
isSameDay,
} from "../../utils/helpers";
import { usePageContext } from "@/context/PageContext";
// Type Definitions
interface ArticleImage {
page: number;
date: string;
articles: string[];
}
interface Origin {
x: number;
y: number;
}
const NewspaperViewer = () => {
const { currentPage, setCurrentPage } = usePageContext();
const [selectedArticle, setSelectedArticle] = useState<string | null>(null);
const [images, setImages] = useState<ArticleImage[]>([]);
const [colCount, setColCount] = useState(8);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { date } = useDateContext();
const [origin, setOrigin] = useState<Origin>({ x: 0, y: 0 });
// Update column count based on page
useEffect(() => {
setColCount(currentPage === 1 ? 6 : 8);
}, [currentPage]);
// Simulated async fetch (replace with actual fetch if needed)
useEffect(() => {
const fetchImages = async () => {
try {
setIsLoading(true);
setError(null);
// Simulate delay
await new Promise((res) => setTimeout(res, 500));
setImages([
{
page: 1,
date: "Mon May 13 2025 22:00:46 GMT+0600 (Bangladesh Standard Time)",
articles: [
"1_r2_c6.jpg",
"1_r3_c3.jpg",
"1_r3_c1.jpg",
"1_r3_c2.jpg",
"1_r4_c2.jpg",
"(1_r4_c1).jpg",
"(1_r4_c2).jpg",
"1_r2_c1.jpg",
"1_r2_c1_g6.jpg",
],
},
{
page: 2,
date: "Mon May 13 2025 22:00:00 GMT+0600 (Bangladesh Standard Time)",
articles: [
"2_r1_c8.jpg",
"2_r2_c2_g1).jpg",
"2_r1_c2_g3.jpg",
"2_r1_c4_g5.jpg",
"(2_r1_c4_g5.jpg",
"2_r1_c1_g8).jpg",
"2_r1_c1_g8.jpg",
"2_r1_c2_g3).jpg",
"2_r2_c2_g3.jpg",
"2_r2_c3_g5.jpg",
"2_r3_c3_g3.jpg",
"(2_r3_c3)_g5.jpg",
"(2_r1_c2)_g1.jpg",
"((2_r1_c2)_g1.jpg",
"2_r2_c2_g1.jpg",
],
},
]);
} catch (err) {
setError("Failed to load newspaper data.");
} finally {
setIsLoading(false);
}
};
fetchImages();
}, []);
const zoomIn = (articleUrl: string, e: React.MouseEvent) => {
const { clientX, clientY } = e;
setOrigin({ x: clientX, y: clientY });
setSelectedArticle(articleUrl);
};
const zoomOut = () => {
setSelectedArticle(null);
};
const dateFilteredContent = useMemo(
() => images.filter((item) => isSameDay(item.date, date)),
[images, date]
);
const hasContentForDate = dateFilteredContent.length > 0;
const dateArticles = useMemo(
() =>
hasContentForDate
? dateFilteredContent.flatMap((pageGroup) =>
createArticlesFromFilenames(pageGroup.articles, pageGroup.page)
)
: [],
[dateFilteredContent, hasContentForDate]
);
const currentPageArticles = dateArticles.filter(
(a) => a.page === currentPage
);
const placedArticles = useMemo(
() => autoPlaceArticles(currentPageArticles, colCount),
[currentPageArticles, colCount]
);
const totalPages = hasContentForDate
? Math.max(...dateFilteredContent.map((item) => item.page))
: 0;
return (
<div className="w-3/4 mx-auto">
{isLoading ? (
<div className="flex justify-center items-center h-64">Loading...</div>
) : error ? (
<div className="text-red-600 text-center h-64 flex items-center justify-center">
{error}
</div>
) : hasContentForDate ? (
<>
{/* Pagination */}
<section className="my-4">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault();
setCurrentPage((prev: number) => Math.max(1, prev - 1));
}}
/>
</PaginationItem>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
(n) => (
<PaginationItem key={n}>
<PaginationLink
href="#"
isActive={n === currentPage}
onClick={(e) => {
e.preventDefault();
setCurrentPage(n);
}}
>
{n}
</PaginationLink>
</PaginationItem>
)
)}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault();
setCurrentPage((prev) => Math.min(totalPages, prev + 1));
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</section>
{/* Articles */}
<section
className="gap-1"
style={{
display: "grid",
gridTemplateColumns: `repeat(${colCount}, 1fr)`,
}}
>
{placedArticles.map((article) => (
<div
key={article.id}
style={{
gridColumn: `${article.gridColumnStart} / span ${article.colSpan}`,
gridRow: `${article.gridRowStart} / span ${article.rowSpan}`,
overflow: "hidden",
}}
className="hover:opacity-60 hover:cursor-pointer transition-opacity"
>
<img
onClick={(e) => zoomIn(`/${article.image}`, e)}
src={`/${article.image}`}
alt={`Article ${article.id}`}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
display: "block",
}}
/>
</div>
))}
</section>
</>
) : (
<div className="flex flex-col items-center justify-center h-64 text-center">
<h2 className="text-2xl font-semibold mb-2">
No newspaper available
</h2>
<p className="text-gray-600">
There is no newspaper edition for{" "}
{date?.toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</p>
</div>
)}
{/* Zoomed View */}
{selectedArticle && (
<div
className="fixed inset-0 bg-black/70 flex justify-center items-center z-50"
onClick={zoomOut}
>
<div
className="relative bg-white rounded-lg overflow-hidden shadow-lg"
style={{
animation: "zoomIn 0.3s ease-out",
transformOrigin: `${origin.x}px ${origin.y}px`,
}}
onClick={(e) => e.stopPropagation()}
>
<img
src={selectedArticle}
alt="Zoomed Article"
className="rounded max-h-[90vh] max-w-[90vw]"
/>
<button
onClick={zoomOut}
className="absolute top-2 right-2 text-black text-2xl"
aria-label="Close"
>
&times;
</button>
</div>
</div>
)}
</div>
);
};
export default NewspaperViewer;