Compare commits
1 Commits
image-link
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 864c68d004 |
29
package-lock.json
generated
29
package-lock.json
generated
@ -26,7 +26,6 @@
|
|||||||
"nanostores": "^0.11.3",
|
"nanostores": "^0.11.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-pageflip": "^2.0.3",
|
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@ -5177,12 +5176,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
"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": {
|
"node_modules/parse-latin": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz",
|
||||||
@ -5676,15 +5669,6 @@
|
|||||||
"react": "^19.0.0"
|
"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": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.14.2",
|
"version": "0.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||||
@ -10307,11 +10291,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
"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": {
|
"parse-latin": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz",
|
||||||
@ -10563,14 +10542,6 @@
|
|||||||
"scheduler": "^0.25.0"
|
"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": {
|
"react-refresh": {
|
||||||
"version": "0.14.2",
|
"version": "0.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||||
|
|||||||
@ -27,7 +27,6 @@
|
|||||||
"nanostores": "^0.11.3",
|
"nanostores": "^0.11.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-pageflip": "^2.0.3",
|
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|||||||
@ -5,18 +5,15 @@ import { ShareZineButton } from "./ShareZineButton";
|
|||||||
|
|
||||||
interface ZineCardProps {
|
interface ZineCardProps {
|
||||||
zine: Zine;
|
zine: Zine;
|
||||||
onClick: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ZineCard({ zine, onClick }: ZineCardProps) {
|
export function ZineCard({ zine }: ZineCardProps) {
|
||||||
const handleDownload = (e: React.MouseEvent) => {
|
const href = `/projects/zines/${zine.id}`;
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-2 rounded-lg border p-4">
|
<div className="flex flex-col items-center gap-2 rounded-lg border p-4">
|
||||||
<button
|
<a
|
||||||
onClick={onClick}
|
href={href}
|
||||||
className="group flex flex-col items-center gap-2 transition-all focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
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">
|
<div className="aspect-[3/4] w-48 overflow-hidden rounded bg-muted">
|
||||||
@ -30,20 +27,15 @@ export function ZineCard({ zine, onClick }: ZineCardProps) {
|
|||||||
{zine.description && (
|
{zine.description && (
|
||||||
<p className="text-sm text-muted-foreground">{zine.description}</p>
|
<p className="text-sm text-muted-foreground">{zine.description}</p>
|
||||||
)}
|
)}
|
||||||
</button>
|
</a>
|
||||||
|
|
||||||
<div className="flex gap-2 mt-2">
|
<div className="flex gap-2 mt-2">
|
||||||
<Button variant="outline" size="sm" onClick={onClick}>
|
<Button variant="outline" size="sm" asChild>
|
||||||
Read Online
|
<a href={href}>Read Online</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="outline" size="sm" asChild>
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
asChild
|
|
||||||
onClick={handleDownload}
|
|
||||||
>
|
|
||||||
<a href={zine.printablePdf} download>
|
<a href={zine.printablePdf} download>
|
||||||
<Download className="h-4 w-4 mr-1" />
|
<Download />
|
||||||
Printable
|
Printable
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,55 +1,12 @@
|
|||||||
import { useState } from "react";
|
import { zines } from "../../data/zines";
|
||||||
import { zines, type Zine } from "../../data/zines";
|
|
||||||
import { ZineCard } from "./ZineCard";
|
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() {
|
export function ZineGrid() {
|
||||||
const [selectedZine, setSelectedZine] = useState<Zine | null>(null);
|
|
||||||
|
|
||||||
const handleOpenZine = (zine: Zine) => {
|
|
||||||
setSelectedZine(zine);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseZine = () => {
|
|
||||||
setSelectedZine(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
{zines.map((zine) => (
|
||||||
{zines.map((zine) => (
|
<ZineCard key={zine.id} zine={zine} />
|
||||||
<ZineCard
|
))}
|
||||||
key={zine.id}
|
</div>
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,248 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -22,23 +22,3 @@ import "../../styles/globals.css";
|
|||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</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,8 +1,10 @@
|
|||||||
---
|
---
|
||||||
|
import { Download } from "lucide-react";
|
||||||
import Navbar from "../../../components/Navbar";
|
import Navbar from "../../../components/Navbar";
|
||||||
import Footer from "../../../components/Footer";
|
import Footer from "../../../components/Footer";
|
||||||
import Layout from "../../../layouts/Layout.astro";
|
import Layout from "../../../layouts/Layout.astro";
|
||||||
import { ZineViewer } from "../../../components/zines/ZineViewer";
|
import { ShareZineButton } from "../../../components/zines/ShareZineButton";
|
||||||
|
import { buttonVariants } from "../../../components/ui/button";
|
||||||
import { zines, type Zine } from "../../../data/zines";
|
import { zines, type Zine } from "../../../data/zines";
|
||||||
import "../../../styles/globals.css";
|
import "../../../styles/globals.css";
|
||||||
|
|
||||||
@ -31,34 +33,38 @@ const { zine } = Astro.props;
|
|||||||
>
|
>
|
||||||
← Back to all zines
|
← Back to all zines
|
||||||
</a>
|
</a>
|
||||||
<ZineViewer
|
|
||||||
client:load
|
<div class="mx-auto max-w-3xl">
|
||||||
pages={zine.pages}
|
<h1 class="text-2xl font-semibold">{zine.title}</h1>
|
||||||
title={zine.title}
|
{zine.description && (
|
||||||
zineId={zine.id}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</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