1 Commits

Author SHA1 Message Date
864c68d004 Remove zines' flipbook view, as it's hard to get the zoom working with that
Some checks failed
continuous-integration/drone/push Build is failing
2026-06-10 13:58:26 -07:00
8 changed files with 91 additions and 432 deletions

29
package-lock.json generated
View File

@ -26,7 +26,6 @@
"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",
@ -5177,12 +5176,6 @@
"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",
@ -5676,15 +5669,6 @@
"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",
@ -10307,11 +10291,6 @@
"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",
@ -10563,14 +10542,6 @@
"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",

View File

@ -27,7 +27,6 @@
"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",

View File

@ -5,18 +5,15 @@ import { ShareZineButton } from "./ShareZineButton";
interface ZineCardProps {
zine: Zine;
onClick: () => void;
}
export function ZineCard({ zine, onClick }: ZineCardProps) {
const handleDownload = (e: React.MouseEvent) => {
e.stopPropagation();
};
export function ZineCard({ zine }: ZineCardProps) {
const href = `/projects/zines/${zine.id}`;
return (
<div className="flex flex-col items-center gap-2 rounded-lg border p-4">
<button
onClick={onClick}
<a
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"
>
<div className="aspect-[3/4] w-48 overflow-hidden rounded bg-muted">
@ -30,20 +27,15 @@ export function ZineCard({ zine, onClick }: ZineCardProps) {
{zine.description && (
<p className="text-sm text-muted-foreground">{zine.description}</p>
)}
</button>
</a>
<div className="flex gap-2 mt-2">
<Button variant="outline" size="sm" onClick={onClick}>
Read Online
<Button variant="outline" size="sm" asChild>
<a href={href}>Read Online</a>
</Button>
<Button
variant="outline"
size="sm"
asChild
onClick={handleDownload}
>
<Button variant="outline" size="sm" asChild>
<a href={zine.printablePdf} download>
<Download className="h-4 w-4 mr-1" />
<Download />
Printable
</a>
</Button>

View File

@ -1,55 +1,12 @@
import { useState } from "react";
import { zines, type Zine } from "../../data/zines";
import { zines } 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}
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>
</>
<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>
);
}

View File

@ -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>
);
}

View File

@ -27,64 +27,66 @@ import "../styles/globals.css";
</div> -->
<div class="justify-items-center">
<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>
<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
>
</div>
<div class="justify-items-center">
<a href="/projects/alternatives/">
<img
<img
src="/assets/computer.png"
alt="computer"
width="250"
class="border"
/>
<p class="underline decoration-solid">Big Tech Alternatives</p>
</a>
/>
<a href="/projects/alternatives/" class="underline decoration-solid">Big Tech Alternatives</a>
</div>
<div class="justify-items-center">
<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>
<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>
</div>
<div class="justify-items-center">
<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>
<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
>
</div>
<div class="justify-items-center">
<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>
<img
src="/assets/pihole.jpeg"
alt="pihole parts"
width="250"
class="border"
/>
<a
href="/projects/pihole/"
class="underline decoration-solid">Pihole Workshop</a
>
</div>
</div>
</div>
</div>

View File

@ -22,23 +22,3 @@ 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>

View File

@ -1,8 +1,10 @@
---
import { Download } from "lucide-react";
import Navbar from "../../../components/Navbar";
import Footer from "../../../components/Footer";
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 "../../../styles/globals.css";
@ -31,34 +33,38 @@ const { zine } = Astro.props;
>
← Back to all zines
</a>
<ZineViewer
client:load
pages={zine.pages}
title={zine.title}
zineId={zine.id}
/>
<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>
</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>