generated from muhtadeetaron/nextjs-template
initial commit
This commit is contained in:
280
epaper-client/src/components/NewspaperViewer.tsx
Normal file
280
epaper-client/src/components/NewspaperViewer.tsx
Normal file
@ -0,0 +1,280 @@
|
||||
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"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewspaperViewer;
|
||||
Reference in New Issue
Block a user