Compare commits
87 Commits
simplified
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 864c68d004 | |||
| a2bb2ef92b | |||
| 4753715e9f | |||
| c499d0e2d3 | |||
| 19317d3572 | |||
| 058550fec3 | |||
| 34d1da02d0 | |||
| e523c1a02c | |||
| 2c3664d767 | |||
| 9a203563b5 | |||
| e83af716ac | |||
| 0f12e1c0dc | |||
| 0ea1a26182 | |||
| bd232caade | |||
| cbcd8e7bed | |||
| c8517a4488 | |||
| 6559bfae33 | |||
| 1fad0f2ff1 | |||
| 91aee1ea49 | |||
| e9c22e0dd2 | |||
| c4df0bf232 | |||
| 70437d2b31 | |||
| e9583bae4e | |||
| 8321986c90 | |||
| 392dc2d44f | |||
| dde00997e6 | |||
| 64939c8b65 | |||
| 6d6ef3dbf3 | |||
| 9b828168f7 | |||
| f8a808f1e4 | |||
| 4a2cf1e983 | |||
| 234d93c99f | |||
| 6d2ce212ae | |||
| c381a1605b | |||
| 3c525cfa82 | |||
| f4118f5c91 | |||
| ae163746e7 | |||
| 5cef42d976 | |||
| 041e60295d | |||
| 64fedb4050 | |||
| d892188c81 | |||
| f571315b8c | |||
| b8991c6f2c | |||
| 2e01e39150 | |||
| ad998497aa | |||
| 2d2bf06055 | |||
| fe5f0ca2ef | |||
| 1e4f2add82 | |||
| 428e1a7ce2 | |||
| 16879ca4fc | |||
| 5f69492ca7 | |||
| 6a9c18f784 | |||
| 344b2fd8f9 | |||
| 0dd6b2bb82 | |||
| 0c663b03da | |||
| b6b0fac277 | |||
| 52538643fd | |||
| 828c779c57 | |||
| 64be0a4ea6 | |||
| 66c56b0fd3 | |||
| 00d9e92e47 | |||
| a64d2b32cb | |||
| dbff57eda1 | |||
| 4bcaba420d | |||
| 602037c4df | |||
| 98e69d80f8 | |||
| 8db08b091b | |||
| a59abe9e61 | |||
| 73c5743818 | |||
| 666cea39d9 | |||
| b7c7fb9a6f | |||
| 01da8eb6ed | |||
| 695bb40ef8 | |||
| 4da3e02d1d | |||
| 6f075b5280 | |||
| 11d17706a7 | |||
| fe96e90ba9 | |||
| b2f75f203f | |||
| aafed46359 | |||
| 1c2c51761a | |||
| 24ad35ff28 | |||
| b6472fd57d | |||
| ce1b9ad91b | |||
| 4a520d412c | |||
| 43db3343aa | |||
| dec175fe89 | |||
| e12dd81a7a |
66
README.md
@ -55,7 +55,7 @@ version=<specify-version>
|
||||
docker build --platform linux/amd64 -t git.coopcloud.tech/rtm/rtmwebsite:$version .
|
||||
```
|
||||
|
||||
## Push the image to gitea registry
|
||||
## Push the image to gitea registery
|
||||
|
||||
Check out [this documentation](https://docs.gitea.com/next/usage/packages/container) for how to login with gitea registery.
|
||||
|
||||
@ -66,19 +66,63 @@ docker push git.coopcloud.tech/rtm/rtmwebsite:$version
|
||||
|
||||
At [our co-op cloud's packages site](https://git.coopcloud.tech/RTM/-/packages), click on "rtmwebsite" and check that the version mentioned is the version you specified!
|
||||
|
||||
## Deploy changes to resisttechmonopolies.online
|
||||
## Update recipe
|
||||
|
||||
We use coop cloud tooling [private recipe](https://git.coopcloud.tech/RTM/rtm-astro-recipe) to deploy this website to our [fleet](https://git.coopcloud.tech/RTM/rtm-config) of lil cat-named machines.
|
||||
We use a [private recipe](https://git.coopcloud.tech/RTM/rtm-astro-recipe) to deploy this website. This step needs Wireguard to be activated (download Wireguard and ask Sootie's owner to create a config for you and give you Docker permissions). The following examples will assume your name in Sootie's config is "blueberry"!
|
||||
|
||||
Read the "Fleet Setup and access" collectives page on our RTM nextcloud to get the `rtm-config` repo set up with the `rtm-astro-recipe` submodule and install the `abra` command line tool!
|
||||
You will need to have wget (`brew install wget` on mac) and [abra](https://docs.coopcloud.tech/abra/) installed.
|
||||
|
||||
Then, in your `rtm-config` repo update the RTM website image version to the one you just built and published by running:
|
||||
Create an SSH key to use with Sootie with the following command. Take note of the file where you save the key. The following examples will assume it is saved to `rtm` and that the `.ssh` directory is in your home directory (which you can find with the command `echo $HOME`).
|
||||
|
||||
``` bash
|
||||
$ abra app config resisttechmonopolies.online # Change VERSION to the docker image you just pushed
|
||||
$ abra app deploy -f resisttechmonopolies.online # Re-deploy the RTM website, now with your changes!
|
||||
$ git add abra/servers/laylotta.resisttechmonopolies.online/resisttechmonopolies.online
|
||||
$ git commit -m 'Updated website to x.x.x' # Publish this change to the rtm-config repo either via direct commit or a PR
|
||||
```ssh-keygen -t ed25519```
|
||||
|
||||
|
||||
Run the following commands to install the SSH key to Sootie as an authorized key:
|
||||
|
||||
```
|
||||
ssh-copy-id -i $HOME/.ssh/rtm.pub blueberry@resisttechmonopolies.online
|
||||
ssh -i $HOME/.ssh/rtm 'blueberry@resisttechmonopolies.online'
|
||||
```
|
||||
|
||||
Done! Thank you for your contributions 🏋️⚡📖!
|
||||
|
||||
In the `$HOME/.ssh/config` file (which you may have to create if it does not exist), paste the following:
|
||||
|
||||
```
|
||||
Host resisttechmonopolies.online
|
||||
Hostname resisttechmonopolies.online
|
||||
User blueberry
|
||||
UseKeychain yes
|
||||
IdentityFile ~/.ssh/rtm
|
||||
```
|
||||
|
||||
You should now be able to SSH into Sootie with just the command `ssh resisttechmonopolies.online`
|
||||
|
||||
|
||||
Run the following command (outside of the terminal in which you ran ssh in the previous step)
|
||||
|
||||
```abra server add resisttechmonopolies.online```
|
||||
|
||||
Clone the `sootie-config` repo into your `$HOME/.abra/servers/resisttechmonopolies.online` directory:
|
||||
|
||||
``` bash
|
||||
git clone https://git.coopcloud.tech/RTM/sootie-config.git .
|
||||
# DON'T FORGET THE . AT THE END OF THE COMMAND
|
||||
```
|
||||
|
||||
Clone the `rtm-astro-recipe` repo into your `$HOME/.abra/recipes` directory:
|
||||
|
||||
```git clone https://git.coopcloud.tech/RTM/rtm-astro-recipe.git```
|
||||
|
||||
|
||||
Update the version number to the latest in
|
||||
|
||||
``` bash
|
||||
.abra/servers/resisttechmonopolies.online/resisttechmonopolies.online.env
|
||||
```
|
||||
|
||||
Then
|
||||
``` bash
|
||||
abra app undeploy resisttechmonopolies.online
|
||||
# wait 10 seconds
|
||||
abra app deploy resisttechmonopolies.online
|
||||
```
|
||||
|
||||
29
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
BIN
public/assets/zines/signal-201/cover.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
public/assets/zines/signal-201/page-1.jpg
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
public/assets/zines/signal-201/page-2.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
public/assets/zines/signal-201/page-3.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/assets/zines/signal-201/page-4.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/assets/zines/signal-201/page-5.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/assets/zines/signal-201/page-6.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
public/assets/zines/signal-201/page-7.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
public/assets/zines/signal-201/printable.pdf
Normal file
40
src/components/zines/ShareZineButton.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Check, Share2 } from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
interface ShareZineButtonProps {
|
||||
zineId: string;
|
||||
}
|
||||
|
||||
export function ShareZineButton({ zineId }: ShareZineButtonProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!copied) return;
|
||||
const timer = setTimeout(() => setCopied(false), 1500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [copied]);
|
||||
|
||||
const handleClick = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const url = `${window.location.origin}/projects/zines/${zineId}`;
|
||||
await navigator.clipboard.writeText(url);
|
||||
setCopied(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button variant="outline" size="sm" onClick={handleClick}>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Share2 />
|
||||
Share
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@ -1,21 +1,19 @@
|
||||
import { Download } from "lucide-react";
|
||||
import type { Zine } from "../../data/zines";
|
||||
import { Button } from "../ui/button";
|
||||
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">
|
||||
@ -29,23 +27,19 @@ 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>
|
||||
<ShareZineButton zineId={zine.id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,54 +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}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,245 +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";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const MIN_ZOOM = 1;
|
||||
const MAX_ZOOM = 2.5;
|
||||
const ZOOM_STEP = 0.5;
|
||||
|
||||
export function ZineViewer({ pages, title }: 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>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -93,4 +93,21 @@ export const zines: Zine[] = [
|
||||
],
|
||||
printablePdf: "/assets/zines/signal-101/printable.pdf",
|
||||
},
|
||||
{
|
||||
id: "signal-201",
|
||||
title: "Signal 201",
|
||||
description: "Advanced usage of signal",
|
||||
coverImage: "/assets/zines/signal-201/cover.jpg",
|
||||
pages: [
|
||||
"/assets/zines/signal-201/cover.jpg",
|
||||
"/assets/zines/signal-201/page-1.jpg",
|
||||
"/assets/zines/signal-201/page-2.jpg",
|
||||
"/assets/zines/signal-201/page-3.jpg",
|
||||
"/assets/zines/signal-201/page-4.jpg",
|
||||
"/assets/zines/signal-201/page-5.jpg",
|
||||
"/assets/zines/signal-201/page-6.jpg",
|
||||
"/assets/zines/signal-201/page-7.jpg",
|
||||
],
|
||||
printablePdf: "/assets/zines/signal-201/printable.pdf",
|
||||
},
|
||||
];
|
||||
|
||||
@ -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>
|
||||
|
||||
70
src/pages/projects/zines/[id].astro
Normal file
@ -0,0 +1,70 @@
|
||||
---
|
||||
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 { zines, type Zine } from "../../../data/zines";
|
||||
import "../../../styles/globals.css";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return zines.map((zine) => ({
|
||||
params: { id: zine.id },
|
||||
props: { zine },
|
||||
}));
|
||||
}
|
||||
|
||||
interface Props {
|
||||
zine: Zine;
|
||||
}
|
||||
|
||||
const { zine } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<main class="flex min-h-screen flex-col justify-between">
|
||||
<div>
|
||||
<Navbar client:load activePage="OurProjects" />
|
||||
<div class="px-4">
|
||||
<a
|
||||
href="/projects/zines"
|
||||
class="inline-block pb-4 pt-4 text-sm underline"
|
||||
>
|
||||
← 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>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
</Layout>
|
||||