add zines to our projects

This commit is contained in:
2026-01-18 21:55:24 -08:00
committed by Jordan
parent 6cffcd0113
commit 8680dcfd2b
40 changed files with 392 additions and 1 deletions
+29
View File
@@ -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",
+2 -1
View File
@@ -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",
@@ -40,4 +41,4 @@
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.6.9"
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

+138
View File
@@ -0,0 +1,138 @@
import React, { useRef, useState, useCallback, forwardRef, useEffect } from "react";
import HTMLFlipBook from "react-pageflip";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "../ui/button";
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 FlipbookViewerProps {
pages: string[];
title: string;
}
export function FlipbookViewer({ pages, title }: FlipbookViewerProps) {
const flipBookRef = useRef<any>(null);
const [currentPage, setCurrentPage] = useState(0);
const totalPages = pages.length;
const onFlip = useCallback((e: any) => {
setCurrentPage(e.data);
}, []);
const goToPrevPage = () => {
flipBookRef.current?.pageFlip()?.flipPrev();
};
const goToNextPage = () => {
flipBookRef.current?.pageFlip()?.flipNext();
};
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="flipbook-container">
{/* @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={true}
onFlip={onFlip}
className="flipbook"
style={{}}
startPage={0}
drawShadow={true}
flippingTime={600}
usePortrait={true}
startZIndex={0}
autoSize={true}
clickEventForward={true}
useMouseEvents={true}
swipeDistance={30}
showPageCorners={true}
disableFlipByClick={false}
>
{pages.map((page, index) => (
<Page key={index} src={page} pageNumber={index + 1} />
))}
</HTMLFlipBook>
</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">
Click the page corners or use arrow buttons to turn pages
</p>
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
import { Download } from "lucide-react";
import type { Zine } from "../../data/zines";
import { Button } from "../ui/button";
interface ZineCardProps {
zine: Zine;
onClick: () => void;
}
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">
<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">
<img
src={zine.coverImage}
alt={`Cover of ${zine.title}`}
className="h-full w-full object-cover transition-transform group-hover:scale-105"
/>
</div>
<h3 className="text-lg font-medium">{zine.title}</h3>
{zine.description && (
<p className="text-sm text-muted-foreground">{zine.description}</p>
)}
</button>
<div className="flex gap-2 mt-2">
<Button variant="outline" size="sm" onClick={onClick}>
Read Online
</Button>
<Button
variant="outline"
size="sm"
asChild
onClick={handleDownload}
>
<a href={zine.printablePdf} download>
<Download className="h-4 w-4 mr-1" />
Printable
</a>
</Button>
</div>
</div>
);
}
+54
View File
@@ -0,0 +1,54 @@
import { useState } from "react";
import { zines, type Zine } from "../../data/zines";
import { ZineCard } from "./ZineCard";
import { FlipbookViewer } from "./FlipbookViewer";
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 && (
<FlipbookViewer
pages={selectedZine.pages}
title={selectedZine.title}
/>
)}
</DialogContent>
</Dialog>
</>
);
}
+62
View File
@@ -0,0 +1,62 @@
export interface Zine {
id: string;
title: string;
description: string;
coverImage: string;
pages: string[];
printablePdf: string;
}
export const zines: Zine[] = [
{
id: "internet-for-the-people",
title: "Internet For the People",
description: "",
coverImage: "/assets/zines/internet-for-the-people/cover.jpg",
pages: [
"/assets/zines/internet-for-the-people/page-1.jpg",
"/assets/zines/internet-for-the-people/page-2.jpg",
"/assets/zines/internet-for-the-people/page-3.jpg",
"/assets/zines/internet-for-the-people/page-4.jpg",
"/assets/zines/internet-for-the-people/page-5.jpg",
"/assets/zines/internet-for-the-people/page-6.jpg",
"/assets/zines/internet-for-the-people/page-7.jpg",
"/assets/zines/internet-for-the-people/page-8.jpg",
],
printablePdf: "/assets/zines/internet-for-the-people/printable.pdf",
},
{
id: "de-monopoly-discotech-101",
title: "De-monopoly DiscoTech 101",
description: "",
coverImage: "/assets/zines/de-monopoly-discotech-101/cover.jpg",
pages: [
"/assets/zines/de-monopoly-discotech-101/page-1.jpg",
"/assets/zines/de-monopoly-discotech-101/page-2.jpg",
"/assets/zines/de-monopoly-discotech-101/page-3.jpg",
"/assets/zines/de-monopoly-discotech-101/page-4.jpg",
"/assets/zines/de-monopoly-discotech-101/page-5.jpg",
"/assets/zines/de-monopoly-discotech-101/page-6.jpg",
"/assets/zines/de-monopoly-discotech-101/page-7.jpg",
"/assets/zines/de-monopoly-discotech-101/page-8.jpg",
],
printablePdf: "/assets/zines/de-monopoly-discotech-101/printable.pdf",
},
{
id: "de-monopoly-discotech-101-japanese",
title: "De-monopoly DiscoTech 101 (Japanese)",
description: "",
coverImage: "/assets/zines/de-monopoly-discotech-101-japanese/cover.jpg",
pages: [
"/assets/zines/de-monopoly-discotech-101-japanese/page-1.jpg",
"/assets/zines/de-monopoly-discotech-101-japanese/page-2.jpg",
"/assets/zines/de-monopoly-discotech-101-japanese/page-3.jpg",
"/assets/zines/de-monopoly-discotech-101-japanese/page-4.jpg",
"/assets/zines/de-monopoly-discotech-101-japanese/page-5.jpg",
"/assets/zines/de-monopoly-discotech-101-japanese/page-6.jpg",
"/assets/zines/de-monopoly-discotech-101-japanese/page-7.jpg",
"/assets/zines/de-monopoly-discotech-101-japanese/page-8.jpg",
],
printablePdf: "/assets/zines/de-monopoly-discotech-101-japanese/printable.pdf",
},
];
+10
View File
@@ -66,6 +66,7 @@ import "../styles/globals.css";
<div class="justify-items-center">
<img
<<<<<<< HEAD
src="/assets/pet-calendar-2026-cover.jpg"
alt="photo collage of cats"
width="330"
@@ -78,6 +79,15 @@ import "../styles/globals.css";
>
</div>
=======
src="/assets/zines-thumbnail.png"
alt="zines"
width="250"
class="border object-cover w-[250px] h-[250px]"
/>
<a href="/OurProjectsPages/Zines/" class="underline decoration-solid">Zines</a>
</div>
>>>>>>> 01fd965 (add zines to our projects)
</div>
</div>
</div>
+45
View File
@@ -0,0 +1,45 @@
---
import Navbar from "../../components/Navbar";
import Footer from "../../components/Footer";
import Layout from "../../layouts/Layout.astro";
import { ZineGrid } from "../../components/zines/ZineGrid";
import "../../styles/globals.css";
---
<Layout>
<main class="flex min-h-screen flex-col justify-between">
<div>
<Navbar client:load activePage="OurProjects" />
<div class="px-4">
<h1 class="pb-4 pt-4 text-3xl font-semibold">Zines</h1>
<p class="mb-6">
We create and share zines about technology, power, and resistance.
Click on a zine to read it in our interactive flipbook viewer!
</p>
<ZineGrid client:load />
</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>