add zines to our projects
@@ -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",
|
||||
@@ -40,4 +41,4 @@
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 411 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 374 KiB |
|
After Width: | Height: | Size: 373 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 388 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 379 KiB |
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 426 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 452 KiB |
|
After Width: | Height: | Size: 471 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 470 KiB |
|
After Width: | Height: | Size: 452 KiB |
|
After Width: | Height: | Size: 469 KiB |
|
After Width: | Height: | Size: 422 KiB |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||