Compare commits
1 Commits
main
...
image-link
| Author | SHA1 | Date | |
|---|---|---|---|
| c13049aa00 |
29
package-lock.json
generated
29
package-lock.json
generated
@ -26,6 +26,7 @@
|
||||
"nanostores": "^0.11.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-pageflip": "^2.0.3",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
@ -5176,6 +5177,12 @@
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||
},
|
||||
"node_modules/page-flip": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/page-flip/-/page-flip-2.0.7.tgz",
|
||||
"integrity": "sha512-96lQFUUz7r/LZzEUZJ3yBIMEKU9+m8HMFDzTvTdD6P7Ag/wXINjp9n0W7b4wanwnDbQETo4uNUoL3zMqpFxwGA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parse-latin": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz",
|
||||
@ -5669,6 +5676,15 @@
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-pageflip": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-pageflip/-/react-pageflip-2.0.3.tgz",
|
||||
"integrity": "sha512-k81mHhRvUM52y8jyzTCh5t4O0lepkLhp+XGSUzq2C3uD+iW99Cv0jfRlqFCjZbD5N3jKkIFr7/3giucoXKDP3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"page-flip": "latest"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||
@ -10291,6 +10307,11 @@
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||
},
|
||||
"page-flip": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/page-flip/-/page-flip-2.0.7.tgz",
|
||||
"integrity": "sha512-96lQFUUz7r/LZzEUZJ3yBIMEKU9+m8HMFDzTvTdD6P7Ag/wXINjp9n0W7b4wanwnDbQETo4uNUoL3zMqpFxwGA=="
|
||||
},
|
||||
"parse-latin": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz",
|
||||
@ -10542,6 +10563,14 @@
|
||||
"scheduler": "^0.25.0"
|
||||
}
|
||||
},
|
||||
"react-pageflip": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-pageflip/-/react-pageflip-2.0.3.tgz",
|
||||
"integrity": "sha512-k81mHhRvUM52y8jyzTCh5t4O0lepkLhp+XGSUzq2C3uD+iW99Cv0jfRlqFCjZbD5N3jKkIFr7/3giucoXKDP3Q==",
|
||||
"requires": {
|
||||
"page-flip": "latest"
|
||||
}
|
||||
},
|
||||
"react-refresh": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"nanostores": "^0.11.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-pageflip": "^2.0.3",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
@ -5,15 +5,18 @@ import { ShareZineButton } from "./ShareZineButton";
|
||||
|
||||
interface ZineCardProps {
|
||||
zine: Zine;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function ZineCard({ zine }: ZineCardProps) {
|
||||
const href = `/projects/zines/${zine.id}`;
|
||||
export function ZineCard({ zine, onClick }: ZineCardProps) {
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2 rounded-lg border p-4">
|
||||
<a
|
||||
href={href}
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="group flex flex-col items-center gap-2 transition-all focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
>
|
||||
<div className="aspect-[3/4] w-48 overflow-hidden rounded bg-muted">
|
||||
@ -27,15 +30,20 @@ export function ZineCard({ zine }: ZineCardProps) {
|
||||
{zine.description && (
|
||||
<p className="text-sm text-muted-foreground">{zine.description}</p>
|
||||
)}
|
||||
</a>
|
||||
</button>
|
||||
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href={href}>Read Online</a>
|
||||
<Button variant="outline" size="sm" onClick={onClick}>
|
||||
Read Online
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
asChild
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<a href={zine.printablePdf} download>
|
||||
<Download />
|
||||
<Download className="h-4 w-4 mr-1" />
|
||||
Printable
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
@ -1,12 +1,55 @@
|
||||
import { zines } from "../../data/zines";
|
||||
import { useState } from "react";
|
||||
import { zines, type Zine } from "../../data/zines";
|
||||
import { ZineCard } from "./ZineCard";
|
||||
import { ZineViewer } from "./ZineViewer";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "../ui/dialog";
|
||||
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
|
||||
|
||||
export function ZineGrid() {
|
||||
const [selectedZine, setSelectedZine] = useState<Zine | null>(null);
|
||||
|
||||
const handleOpenZine = (zine: Zine) => {
|
||||
setSelectedZine(zine);
|
||||
};
|
||||
|
||||
const handleCloseZine = () => {
|
||||
setSelectedZine(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{zines.map((zine) => (
|
||||
<ZineCard key={zine.id} zine={zine} />
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{zines.map((zine) => (
|
||||
<ZineCard
|
||||
key={zine.id}
|
||||
zine={zine}
|
||||
onClick={() => handleOpenZine(zine)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Dialog open={selectedZine !== null} onOpenChange={handleCloseZine}>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<VisuallyHidden.Root>
|
||||
<DialogTitle>{selectedZine?.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Interactive flipbook viewer for {selectedZine?.title}
|
||||
</DialogDescription>
|
||||
</VisuallyHidden.Root>
|
||||
{selectedZine && (
|
||||
<ZineViewer
|
||||
pages={selectedZine.pages}
|
||||
title={selectedZine.title}
|
||||
zineId={selectedZine.id}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
248
src/components/zines/ZineViewer.tsx
Normal file
248
src/components/zines/ZineViewer.tsx
Normal file
@ -0,0 +1,248 @@
|
||||
import React, { useRef, useState, useCallback, forwardRef, useEffect } from "react";
|
||||
import HTMLFlipBook from "react-pageflip";
|
||||
import { ChevronLeft, ChevronRight, ZoomIn, ZoomOut, Minimize2 } from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import { ShareZineButton } from "./ShareZineButton";
|
||||
|
||||
interface PageProps {
|
||||
src: string;
|
||||
pageNumber: number;
|
||||
}
|
||||
|
||||
const Page = forwardRef<HTMLDivElement, PageProps>(({ src, pageNumber }, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="page bg-white">
|
||||
<img
|
||||
src={src}
|
||||
alt={`Page ${pageNumber}`}
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Page.displayName = "Page";
|
||||
|
||||
interface ZineViewerProps {
|
||||
pages: string[];
|
||||
title: string;
|
||||
zineId?: string;
|
||||
}
|
||||
|
||||
const MIN_ZOOM = 1;
|
||||
const MAX_ZOOM = 2.5;
|
||||
const ZOOM_STEP = 0.5;
|
||||
|
||||
export function ZineViewer({ pages, title, zineId }: ZineViewerProps) {
|
||||
const flipBookRef = useRef<any>(null);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const totalPages = pages.length;
|
||||
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [pan, setPan] = useState({ x: 0, y: 0 });
|
||||
const [isPanning, setIsPanning] = useState(false);
|
||||
const panStart = useRef({ x: 0, y: 0 });
|
||||
const panOffset = useRef({ x: 0, y: 0 });
|
||||
|
||||
const isZoomed = zoom > MIN_ZOOM;
|
||||
|
||||
const onFlip = useCallback((e: any) => {
|
||||
setCurrentPage(e.data);
|
||||
}, []);
|
||||
|
||||
const goToPrevPage = () => {
|
||||
flipBookRef.current?.pageFlip()?.flipPrev();
|
||||
};
|
||||
|
||||
const goToNextPage = () => {
|
||||
flipBookRef.current?.pageFlip()?.flipNext();
|
||||
};
|
||||
|
||||
const handleZoomIn = () => {
|
||||
setZoom((z) => Math.min(z + ZOOM_STEP, MAX_ZOOM));
|
||||
};
|
||||
|
||||
const handleZoomOut = () => {
|
||||
setZoom((z) => {
|
||||
const next = Math.max(z - ZOOM_STEP, MIN_ZOOM);
|
||||
if (next <= MIN_ZOOM) setPan({ x: 0, y: 0 });
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleZoomReset = () => {
|
||||
setZoom(MIN_ZOOM);
|
||||
setPan({ x: 0, y: 0 });
|
||||
};
|
||||
|
||||
const handlePointerDown = (e: React.PointerEvent) => {
|
||||
if (!isZoomed) return;
|
||||
setIsPanning(true);
|
||||
panStart.current = { x: e.clientX, y: e.clientY };
|
||||
panOffset.current = { x: pan.x, y: pan.y };
|
||||
(e.target as HTMLElement).setPointerCapture?.(e.pointerId);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handlePointerMove = (e: React.PointerEvent) => {
|
||||
if (!isPanning) return;
|
||||
const dx = (e.clientX - panStart.current.x) / zoom;
|
||||
const dy = (e.clientY - panStart.current.y) / zoom;
|
||||
setPan({
|
||||
x: panOffset.current.x + dx,
|
||||
y: panOffset.current.y + dy,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePointerUp = () => {
|
||||
setIsPanning(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "ArrowLeft") {
|
||||
goToPrevPage();
|
||||
} else if (e.key === "ArrowRight") {
|
||||
goToNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
if (pages.length === 0) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center text-muted-foreground">
|
||||
No pages available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<h2 className="text-xl font-semibold">{title}</h2>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleZoomOut}
|
||||
disabled={zoom <= MIN_ZOOM}
|
||||
aria-label="Zoom out"
|
||||
>
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground w-12 text-center">
|
||||
{Math.round(zoom * 100)}%
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleZoomIn}
|
||||
disabled={zoom >= MAX_ZOOM}
|
||||
aria-label="Zoom in"
|
||||
>
|
||||
<ZoomIn className="h-4 w-4" />
|
||||
</Button>
|
||||
{isZoomed && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleZoomReset}
|
||||
aria-label="Reset zoom"
|
||||
>
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{zineId && <ShareZineButton zineId={zineId} />}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flipbook-container"
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
cursor: isZoomed ? (isPanning ? "grabbing" : "grab") : "default",
|
||||
}}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerCancel={handlePointerUp}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
transform: `scale(${zoom}) translate(${pan.x}px, ${pan.y}px)`,
|
||||
transformOrigin: "center center",
|
||||
transition: isPanning ? "none" : "transform 0.2s ease",
|
||||
pointerEvents: isZoomed ? "none" : "auto",
|
||||
}}
|
||||
>
|
||||
{/* @ts-ignore - react-pageflip types are incomplete */}
|
||||
<HTMLFlipBook
|
||||
ref={flipBookRef}
|
||||
width={350}
|
||||
height={500}
|
||||
size="stretch"
|
||||
minWidth={280}
|
||||
maxWidth={500}
|
||||
minHeight={400}
|
||||
maxHeight={700}
|
||||
maxShadowOpacity={0.5}
|
||||
showCover={true}
|
||||
mobileScrollSupport={!isZoomed}
|
||||
onFlip={onFlip}
|
||||
className="flipbook"
|
||||
style={{}}
|
||||
startPage={0}
|
||||
drawShadow={true}
|
||||
flippingTime={600}
|
||||
usePortrait={true}
|
||||
startZIndex={0}
|
||||
autoSize={true}
|
||||
clickEventForward={true}
|
||||
useMouseEvents={!isZoomed}
|
||||
swipeDistance={30}
|
||||
showPageCorners={!isZoomed}
|
||||
disableFlipByClick={false}
|
||||
>
|
||||
{pages.map((page, index) => (
|
||||
<Page key={index} src={page} pageNumber={index + 1} />
|
||||
))}
|
||||
</HTMLFlipBook>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={goToPrevPage}
|
||||
disabled={currentPage === 0}
|
||||
aria-label="Previous page"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Page {currentPage + 1} of {totalPages}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={goToNextPage}
|
||||
disabled={currentPage >= totalPages - 1}
|
||||
aria-label="Next page"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{isZoomed
|
||||
? "Drag to pan. Use arrow buttons to turn pages"
|
||||
: "Click the page corners or use arrow buttons to turn pages"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -27,66 +27,64 @@ import "../styles/globals.css";
|
||||
</div> -->
|
||||
|
||||
<div class="justify-items-center">
|
||||
<img
|
||||
src="/assets/book-club.jpg"
|
||||
alt="stack of books"
|
||||
width="280"
|
||||
class="border"
|
||||
/>
|
||||
<a
|
||||
href="/projects/bookclub/"
|
||||
class="underline decoration-solid">Book Club</a
|
||||
>
|
||||
<a href="/projects/bookclub/">
|
||||
<img
|
||||
src="/assets/book-club.jpg"
|
||||
alt="stack of books"
|
||||
width="280"
|
||||
class="border"
|
||||
/>
|
||||
<p class="underline decoration-solid">Book Club</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="justify-items-center">
|
||||
<img
|
||||
<a href="/projects/alternatives/">
|
||||
<img
|
||||
src="/assets/computer.png"
|
||||
alt="computer"
|
||||
width="250"
|
||||
class="border"
|
||||
/>
|
||||
<a href="/projects/alternatives/" class="underline decoration-solid">Big Tech Alternatives</a>
|
||||
/>
|
||||
<p class="underline decoration-solid">Big Tech Alternatives</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="justify-items-center">
|
||||
<img
|
||||
src="/assets/zines-thumbnail.png"
|
||||
alt="zines"
|
||||
width="250"
|
||||
class="border object-cover w-[250px] h-[250px]"
|
||||
/>
|
||||
<a href="/projects/zines/" class="underline decoration-solid">Zines</a>
|
||||
<a href="/projects/zines/">
|
||||
<img
|
||||
src="/assets/zines-thumbnail.png"
|
||||
alt="zines"
|
||||
width="250"
|
||||
class="border object-cover w-[250px] h-[250px]"
|
||||
/>
|
||||
<p class="underline decoration-solid">Zines</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="justify-items-center">
|
||||
<img
|
||||
src="/assets/pet-calendar-2026-cover.jpg"
|
||||
alt="photo collage of cats"
|
||||
width="500"
|
||||
class="border"
|
||||
/>
|
||||
<a
|
||||
href="/projects/PetCalendar/"
|
||||
class="underline decoration-solid">Pet Calendar</a
|
||||
>
|
||||
<a href="/projects/PetCalendar/">
|
||||
<img
|
||||
src="/assets/pet-calendar-2026-cover.jpg"
|
||||
alt="photo collage of cats and dogs"
|
||||
width="500"
|
||||
class="border"
|
||||
/>
|
||||
<p class="underline decoration-solid">Pet Calendar</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="justify-items-center">
|
||||
<img
|
||||
src="/assets/pihole.jpeg"
|
||||
alt="pihole parts"
|
||||
width="250"
|
||||
class="border"
|
||||
/>
|
||||
<a
|
||||
href="/projects/pihole/"
|
||||
class="underline decoration-solid">Pihole Workshop</a
|
||||
>
|
||||
<a href="/projects/pihole/">
|
||||
<img
|
||||
src="/assets/pihole.jpeg"
|
||||
alt="pihole parts"
|
||||
width="250"
|
||||
class="border"
|
||||
/>
|
||||
<p class="underline decoration-solid">Pihole Workshop</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -22,3 +22,23 @@ import "../../styles/globals.css";
|
||||
<Footer />
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
:global(.flipbook-container) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 520px;
|
||||
}
|
||||
|
||||
:global(.flipbook) {
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:global(.page) {
|
||||
background-color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
---
|
||||
import { Download } from "lucide-react";
|
||||
import Navbar from "../../../components/Navbar";
|
||||
import Footer from "../../../components/Footer";
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import { ShareZineButton } from "../../../components/zines/ShareZineButton";
|
||||
import { buttonVariants } from "../../../components/ui/button";
|
||||
import { ZineViewer } from "../../../components/zines/ZineViewer";
|
||||
import { zines, type Zine } from "../../../data/zines";
|
||||
import "../../../styles/globals.css";
|
||||
|
||||
@ -33,38 +31,34 @@ const { zine } = Astro.props;
|
||||
>
|
||||
← Back to all zines
|
||||
</a>
|
||||
|
||||
<div class="mx-auto max-w-3xl">
|
||||
<h1 class="text-2xl font-semibold">{zine.title}</h1>
|
||||
{zine.description && (
|
||||
<p class="mt-1 text-sm text-muted-foreground">{zine.description}</p>
|
||||
)}
|
||||
|
||||
<div class="mt-4 flex gap-2">
|
||||
<ShareZineButton client:load zineId={zine.id} />
|
||||
<a
|
||||
href={zine.printablePdf}
|
||||
download
|
||||
class={buttonVariants({ variant: "outline", size: "sm" })}
|
||||
>
|
||||
<Download />
|
||||
Printable
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 py-6">
|
||||
{zine.pages.map((page, i) => (
|
||||
<img
|
||||
src={page}
|
||||
alt={`Page ${i + 1} of ${zine.title}`}
|
||||
loading={i === 0 ? "eager" : "lazy"}
|
||||
class="w-full h-auto rounded shadow-sm bg-white"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ZineViewer
|
||||
client:load
|
||||
pages={zine.pages}
|
||||
title={zine.title}
|
||||
zineId={zine.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
:global(.flipbook-container) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 520px;
|
||||
}
|
||||
|
||||
:global(.flipbook) {
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:global(.page) {
|
||||
background-color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user