Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 501e6f2288 | |||
| ee4ef882dc | |||
| 41c21aa463 | |||
| 6cad58a332 | |||
| 897ef35747 | |||
| a9366cb48b | |||
| aba9f78b03 | |||
| 9beac5d1b1 | |||
| 207ba77559 | |||
| e87c8efd66 | |||
| e67d93768e | |||
| ef95670283 | |||
| d0c7861a1b | |||
| fdaf49d6ca | |||
| 9ed1c4c5b8 | |||
| 630f53ea5d | |||
| b08aaf416f | |||
| d63fddc3ba | |||
| 65afc10fc6 | |||
| 8c771fac6a | |||
| e42b75951a | |||
| f01be89d91 | |||
| d5412264dc | |||
| 87351bafda | |||
| 821ef086eb | |||
| bc4d1dc4db | |||
| 5b4e5694d6 | |||
| 51ba08d267 | |||
| bedc9997ea | |||
| aa14a03761 | |||
| 760c3d0794 | |||
| 152995cc0b | |||
| 8680dcfd2b | |||
| 6cffcd0113 | |||
| 5c6e8a60a0 | |||
| 717b91ed70 | |||
| 10da042ad2 | |||
| 26a7be8070 | |||
| 5f4eb9a6f6 | |||
| 37d49af3e4 | |||
| 956c42811d | |||
| 8d2b02cc1a | |||
| edd81e5bd6 | |||
| a6d355a826 | |||
| 66378e97a9 | |||
| be46a62157 | |||
| d6a89e76b0 | |||
| 9246961f22 | |||
| 26582aebbc | |||
| 255f360825 | |||
| 695431dbc0 | |||
| 7c52ab5b78 | |||
| 314010cbf6 | |||
| 977e5c84ca | |||
| 05fcd26c6e | |||
| ffd26c42ca | |||
| 0c440ac44c | |||
| 57f322a7b0 | |||
| 0e236f67a8 | |||
| 726de1a0db | |||
| a04e99c7f0 | |||
| 2a9aeb5a46 | |||
| e07d5a80c8 | |||
| 7b7b2edc87 | |||
| 14e2e18f54 | |||
| 2d224b8f7d | |||
| 54a544b3c1 | |||
| fe3ad8eea6 | |||
| 635d4a1b21 | |||
| e39a0f3b30 | |||
| a3cc09bf2f | |||
| dff21eb51a | |||
| 5a53feb817 | |||
| 7dbbfbe479 | |||
| fd77268cbe | |||
| 9f3e1b21ef | |||
| e502445a8d | |||
| b1d955edc1 | |||
| 2991171654 | |||
| b6a68f5435 | |||
| 25166508e5 | |||
| 8ed99cd3b2 | |||
| c3f941b5d2 | |||
| d44ce6b8a2 | |||
| c8c6b6f7cd | |||
| f908c69635 | |||
| 5b56c3e8d8 | |||
| 67208841de | |||
| e07be531a4 |
@@ -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
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -14,7 +14,7 @@ export default function Navbar({ activePage = "" }: NavbarProps) {
|
||||
className={cn(
|
||||
activePage === "Home" && "border-b-2 border-black-500",
|
||||
)}
|
||||
href="/">Resist Tech Monopolies</a>
|
||||
href="/">Resist Tech Monopolies - brooke waz here</a>
|
||||
</strong>
|
||||
</div>
|
||||
<strong className={cn("sm:text-1xl flex gap-10 md:text-4xl")}>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface NavbarProps {
|
||||
activePage?: string;
|
||||
}
|
||||
|
||||
const NAV_PILLS = [
|
||||
{ href: "/newlook#points-of-unity", label: "Points of Unity", activeKey: "PointsOfUnity", text: "text-green-700", darkText: "dark:text-green-400", ring: "ring-green-500" },
|
||||
{ href: "/newlook#projects", label: "Our Projects", activeKey: "OurProjects", text: "text-red-700", darkText: "dark:text-red-400", ring: "ring-red-500" },
|
||||
{ href: "/newlook#get-involved", label: "Get Involved", activeKey: "GetInvolved", text: "text-blue-700", darkText: "dark:text-blue-400", ring: "ring-blue-500" },
|
||||
];
|
||||
|
||||
export default function NewLookNavbar({ activePage = "" }: NavbarProps) {
|
||||
return (
|
||||
<>
|
||||
<div className={cn("flex flex-row items-center justify-between pb-3 pl-4 pr-4 pt-4")}>
|
||||
<div>
|
||||
<strong className={cn("sm:text-2xl md:text-4xl")}>
|
||||
<a
|
||||
className={cn(
|
||||
activePage === "Home" && "border-b-2 border-black-500",
|
||||
)}
|
||||
href="/newlook">Resist Tech Monopolies</a>
|
||||
</strong>
|
||||
</div>
|
||||
<strong className={cn("flex items-center gap-3")}>
|
||||
{NAV_PILLS.map(({ href, label, activeKey, text, darkText, ring }) => (
|
||||
<a
|
||||
key={href}
|
||||
href={href}
|
||||
className={cn(
|
||||
"rounded-full px-3 py-1 text-sm transition-colors",
|
||||
"bg-gray-200 hover:bg-gray-300",
|
||||
"dark:bg-gray-800 dark:hover:bg-gray-700",
|
||||
text,
|
||||
darkText,
|
||||
activePage === activeKey && `ring-2 ${ring} ring-offset-2`,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
))}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div className={cn("flex flex-row justify-between pb-3")}>
|
||||
<div className="w-full grid grid-flow-col">
|
||||
{Array.from({ length: 12 }).map((_, index) => (
|
||||
<div key={index} className="flex justify-center">
|
||||
<img
|
||||
src={index % 2 === 0 ? "/assets/computer.png" : "/assets/people.png"}
|
||||
alt={index % 2 === 0 ? "computer icon" : "people icon"}
|
||||
width="30"
|
||||
height="30"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<h1 class="pb-4 text-3xl font-semibold">Our Projects</h1>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<div class="justify-items-center">
|
||||
<img
|
||||
src="/assets/book-club.jpg"
|
||||
alt="stack of books"
|
||||
width="280"
|
||||
loading="lazy"
|
||||
class="border"
|
||||
/>
|
||||
<a href="/projects/bookclub/" class="underline decoration-solid">Book Club</a>
|
||||
</div>
|
||||
|
||||
<div class="justify-items-center">
|
||||
<img
|
||||
src="/assets/computer.png"
|
||||
alt="computer"
|
||||
width="250"
|
||||
loading="lazy"
|
||||
class="border"
|
||||
/>
|
||||
<a href="/projects/alternatives/" class="underline decoration-solid">Big Tech Alternatives</a>
|
||||
</div>
|
||||
|
||||
<div class="justify-items-center">
|
||||
<img
|
||||
src="/assets/zines-thumbnail.png"
|
||||
alt="zines"
|
||||
width="250"
|
||||
loading="lazy"
|
||||
class="border object-cover w-[250px] h-[250px]"
|
||||
/>
|
||||
<a href="/projects/zines/" class="underline decoration-solid">Zines</a>
|
||||
</div>
|
||||
|
||||
<div class="justify-items-center">
|
||||
<img
|
||||
src="/assets/pet-calendar-2026-cover.jpg"
|
||||
alt="photo collage of cats"
|
||||
width="500"
|
||||
loading="lazy"
|
||||
class="border"
|
||||
/>
|
||||
<a href="/projects/PetCalendar/" class="underline decoration-solid">Pet Calendar</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,64 @@
|
||||
<h1 class="text-3xl font-semibold">Points of Unity</h1>
|
||||
|
||||
<p class="mb-4">
|
||||
Our community agreements and decision-making process describe <em>how</em> we work together. The points of unity are the broader philosophies that we're working towards as a group. Perhaps people aren't 100% aligned individually, but this is what the group as a whole believes in when showing up to the work.
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl font-semibold mt-10 mb-2">Grounding</h2>
|
||||
|
||||
<p class="mb-4">
|
||||
We situate ourselves in our context - as Seattle residents and as users and creators of the technologies we aim to resist. We are seeing a long-spanning rise of fascism within national governments here and abroad. In parallel is the increasing power of tech companies over our lives through surveillance and the provisioning of everyday needs, from employment to how we get our internet utilities in the first place. There are rising income disparities across the nation, but also more specifically across everyone who can be considered a "tech worker": from a CEO, to a software engineer to a tech campus cafeteria worker to a child enslaved to mine cobalt in the Congo.
|
||||
</p>
|
||||
|
||||
<p class="mb-4">
|
||||
We know that technology intersects with and amplifies systems of oppression, from anti-Black racism to classism, ableism, cisheteropatriarchy, etc. Big tech permeates all aspects of life in Seattle, but to name a few ways:
|
||||
</p>
|
||||
|
||||
<ul class="list-disc pl-8 mb-4 space-y-2">
|
||||
<li>The rise of a <a href="https://stopsurveillancecity.wordpress.com/" class="font-bold underline hover:text-gray-600">Surveillance City</a> in Seattle facilitates the tracking of out-of-state people seeking abortions, the kidnapping of migrants, and the increased surveillance and criminalization of sex workers, the visibly unhoused, and poor and racialized communities.</li>
|
||||
<li>The <strong>environmental racism</strong> and community impacts of <strong>data centers</strong>, both from the AI bubble and the demand for more data processing that comes with surveillance capitalism.</li>
|
||||
<li><strong>The AI genocide</strong> in Palestine abetted by Google, Amazon, Boeing and Microsoft, all local Seattle employers.</li>
|
||||
<li>Rising <strong>gentrification</strong> and <strong>income inequality</strong> that comes from financialization of real estate and the gentrifying waves of tech workers moving into the city.</li>
|
||||
</ul>
|
||||
|
||||
<p class="mb-4">
|
||||
Some of us are employed by Big Tech companies, and we are all living on stolen Coast Salish land covered by the broken Treaty of Point Elliot of 1855; we understand that we are contributing - directly or indirectly - to these oppressions, and seek to use our skills towards more liberatory ends.
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl font-semibold mt-10 mb-2">Theory of Change</h2>
|
||||
|
||||
<p class="mb-4">
|
||||
We are committed to fostering our collective understanding of resisting tech monopolies and building a constellation of alternative technologies that exist outside of exploitative, oppressive systems. We seek out radical possibilities by building connections across our communities with others struggling to resist tech monopolies and reject work that expands the reach of these monopolies.
|
||||
</p>
|
||||
|
||||
<p class="mb-4">
|
||||
We don't need to passively wait around for somebody to come implement these solutions. Making improvements to our lives and our communities paves the way for collective liberation. Dismantling smaller elements of a system is practice for uprooting the whole and replacing it with a constellation of liberatory alternatives.
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl font-semibold mt-10 mb-2">Role of Technology</h2>
|
||||
|
||||
<p class="mb-4">
|
||||
"We believe our communities and organizing efforts can and should harness the possibilities of new technology." (<a href="https://mijente.net/our-dna/" class="underline hover:text-gray-600">Mijente</a>) However, we don't innovate for the sake of innovation, but aim to create with intention, listen to the perspectives of those we hope to serve (including ourselves), and hold each other accountable in this work.
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl font-semibold mt-10 mb-2">Our Position in the Movement</h2>
|
||||
|
||||
<p class="mb-4">
|
||||
We don't see ourselves as a vanguard organization - that is, we make no claims to being the sole authority on resisting tech monopolies and we will never tell a front-line community or group of tech users what they actually need. Everyone benefits from the work of resisting tech monopolies, and for that reason we do the work for ourselves as much as we do it for anybody we are in solidarity with.
|
||||
</p>
|
||||
|
||||
<p class="mb-4">
|
||||
However we also recognize our own privilege in being highly-skilled tech workers: because of this we center the voices of the people most vulnerable to various tech monopolies, and aim to share our expertise so that others can also become agents in co-creating technologies of liberation and resistance.
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl font-semibold mt-10 mb-2">Friendly Vibes</h2>
|
||||
|
||||
<p class="mb-4">
|
||||
Holding all of the above, we're also a group of friends here for the scrappy, hacky vibes. We don't take our work too seriously and we're here to work joyfully. This is first and foremost a community.
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl font-semibold mt-10 mb-2">Acknowledgments</h2>
|
||||
|
||||
<p class="mb-4">
|
||||
These points of unity were inspired by the Prison Library Solidarity Network, the Coalition of Anti-Racist Whites, Ruha Benjamin's work on Abolitionist Tech, and Mijente.
|
||||
</p>
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import Navbar from "../components/Navbar";
|
||||
import Footer from "../components/Footer";
|
||||
import PointsOfUnity from "../components/PointsOfUnity.astro";
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import "../styles/globals.css";
|
||||
---
|
||||
@@ -10,70 +11,7 @@ import "../styles/globals.css";
|
||||
<div>
|
||||
<Navbar client:load activePage="PointsOfUnity" />
|
||||
<div class="pl-4 pr-4">
|
||||
<h1 class="text-3xl font-semibold">Points of Unity</h1>
|
||||
|
||||
<p class="mb-4">
|
||||
Our community agreements and decision-making process describe <em>how</em> we work together. The points of unity are the broader philosophies that we're working towards as a group. Perhaps people aren't 100% aligned individually, but this is what the group as a whole believes in when showing up to the work.
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl font-semibold mt-10 mb-2">Grounding</h2>
|
||||
|
||||
<p class="mb-4">
|
||||
We situate ourselves in our context - as Seattle residents and as users and creators of the technologies we aim to resist. We are seeing a long-spanning rise of fascism within national governments here and abroad. In parallel is the increasing power of tech companies over our lives through surveillance and the provisioning of everyday needs, from employment to how we get our internet utilities in the first place. There are rising income disparities across the nation, but also more specifically across everyone who can be considered a "tech worker": from a CEO, to a software engineer to a tech campus cafeteria worker to a child enslaved to mine cobalt in the Congo.
|
||||
</p>
|
||||
|
||||
<p class="mb-4">
|
||||
We know that technology intersects with and amplifies systems of oppression, from anti-Black racism to classism, ableism, cisheteropatriarchy, etc. Big tech permeates all aspects of life in Seattle, but to name a few ways:
|
||||
</p>
|
||||
|
||||
<ul class="list-disc pl-8 mb-4 space-y-2">
|
||||
<li>The rise of a <a href="https://stopsurveillancecity.wordpress.com/" class="font-bold underline hover:text-gray-600">Surveillance City</a> in Seattle facilitates the tracking of out-of-state people seeking abortions, the kidnapping of migrants, and the increased surveillance and criminalization of sex workers, the visibly unhoused, and poor and racialized communities.</li>
|
||||
<li>The <strong>environmental racism</strong> and community impacts of <strong>data centers</strong>, both from the AI bubble and the demand for more data processing that comes with surveillance capitalism.</li>
|
||||
<li><strong>The AI genocide</strong> in Palestine abetted by Google, Amazon, Boeing and Microsoft, all local Seattle employers.</li>
|
||||
<li>Rising <strong>gentrification</strong> and <strong>income inequality</strong> that comes from financialization of real estate and the gentrifying waves of tech workers moving into the city.</li>
|
||||
</ul>
|
||||
|
||||
<p class="mb-4">
|
||||
Some of us are employed by Big Tech companies, and we are all living on stolen Coast Salish land covered by the broken Treaty of Point Elliot of 1855; we understand that we are contributing - directly or indirectly - to these oppressions, and seek to use our skills towards more liberatory ends.
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl font-semibold mt-10 mb-2">Theory of Change</h2>
|
||||
|
||||
<p class="mb-4">
|
||||
We are committed to fostering our collective understanding of resisting tech monopolies and building a constellation of alternative technologies that exist outside of exploitative, oppressive systems. We seek out radical possibilities by building connections across our communities with others struggling to resist tech monopolies and reject work that expands the reach of these monopolies.
|
||||
</p>
|
||||
|
||||
<p class="mb-4">
|
||||
We don't need to passively wait around for somebody to come implement these solutions. Making improvements to our lives and our communities paves the way for collective liberation. Dismantling smaller elements of a system is practice for uprooting the whole and replacing it with a constellation of liberatory alternatives.
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl font-semibold mt-10 mb-2">Role of Technology</h2>
|
||||
|
||||
<p class="mb-4">
|
||||
"We believe our communities and organizing efforts can and should harness the possibilities of new technology." (<a href="https://mijente.net/our-dna/" class="underline hover:text-gray-600">Mijente</a>) However, we don't innovate for the sake of innovation, but aim to create with intention, listen to the perspectives of those we hope to serve (including ourselves), and hold each other accountable in this work.
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl font-semibold mt-10 mb-2">Our Position in the Movement</h2>
|
||||
|
||||
<p class="mb-4">
|
||||
We don't see ourselves as a vanguard organization - that is, we make no claims to being the sole authority on resisting tech monopolies and we will never tell a front-line community or group of tech users what they actually need. Everyone benefits from the work of resisting tech monopolies, and for that reason we do the work for ourselves as much as we do it for anybody we are in solidarity with.
|
||||
</p>
|
||||
|
||||
<p class="mb-4">
|
||||
However we also recognize our own privilege in being highly-skilled tech workers: because of this we center the voices of the people most vulnerable to various tech monopolies, and aim to share our expertise so that others can also become agents in co-creating technologies of liberation and resistance.
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl font-semibold mt-10 mb-2">Friendly Vibes</h2>
|
||||
|
||||
<p class="mb-4">
|
||||
Holding all of the above, we're also a group of friends here for the scrappy, hacky vibes. We don't take our work too seriously and we're here to work joyfully. This is first and foremost a community.
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl font-semibold mt-10 mb-2">Acknowledgments</h2>
|
||||
|
||||
<p class="mb-4">
|
||||
These points of unity were inspired by the Prison Library Solidarity Network, the Coalition of Anti-Racist Whites, Ruha Benjamin's work on Abolitionist Tech, and Mijente.
|
||||
</p>
|
||||
<PointsOfUnity />
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
import NewLookNavbar from "@/components/NewLookNavbar";
|
||||
import Footer from "@/components/Footer";
|
||||
import PointsOfUnity from "@/components/PointsOfUnity.astro";
|
||||
import OurProjects from "@/components/OurProjects.astro";
|
||||
import Layout from "@/layouts/Layout.astro";
|
||||
import "@/styles/globals.css";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<main>
|
||||
<section id="home" class="bg-[#FFF8E7] text-gray-900 px-4 pb-2">
|
||||
<NewLookNavbar client:load activePage="Home" />
|
||||
</section>
|
||||
|
||||
<section id="projects" class="bg-[#FFE5D9] text-gray-900 px-4 py-16">
|
||||
<OurProjects />
|
||||
</section>
|
||||
|
||||
<section id="points-of-unity" class="bg-[#D8F3DC] text-gray-900 px-4 py-16">
|
||||
<PointsOfUnity />
|
||||
</section>
|
||||
|
||||
<section id="get-involved" class="bg-[#D4E4F7] text-gray-900 px-4 py-16">
|
||||
<h1 class="text-3xl font-semibold">Get Involved</h1>
|
||||
|
||||
<div class="text-xl leading-[2]">
|
||||
<p>
|
||||
Do you believe technology can and should be used for good? Do you
|
||||
think a democratic internet could liberate us? Join us!
|
||||
</p>
|
||||
|
||||
<p>We share this vision, and we want to work together to achieve it.</p>
|
||||
|
||||
<div class="my-8">
|
||||
<a
|
||||
href="https://shlink.resisttechmonopolies.online/HNrZG"
|
||||
class="px-6 py-3 bg-[#80aaff] text-white font-semibold rounded-md hover:bg-[#6090e0] transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Fill Out Our Interest Form!
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
</Layout>
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import Navbar from "../components/Navbar";
|
||||
import Footer from "../components/Footer";
|
||||
import OurProjects from "../components/OurProjects.astro";
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import "../styles/globals.css";
|
||||
---
|
||||
@@ -10,84 +11,7 @@ import "../styles/globals.css";
|
||||
<div>
|
||||
<Navbar client:load activePage="OurProjects" />
|
||||
<div class="pl-4 pr-4">
|
||||
<h1 class="pb-4 pt-4 text-3xl font-semibold">Our Projects</h1>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<!-- <div class="justify-items-center">
|
||||
<img
|
||||
src="/assets/calendar.png"
|
||||
alt="stack of books"
|
||||
width="250"
|
||||
class="border pb-4"
|
||||
/>
|
||||
|
||||
<a
|
||||
href="/projects/calendar/"
|
||||
class="underline decoration-solid">Calendar</a
|
||||
>
|
||||
</div> -->
|
||||
|
||||
<div class="justify-items-center">
|
||||
<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">
|
||||
<img
|
||||
src="/assets/computer.png"
|
||||
alt="computer"
|
||||
width="250"
|
||||
class="border"
|
||||
/>
|
||||
<a href="/projects/alternatives/" class="underline decoration-solid">Big Tech Alternatives</a>
|
||||
</div>
|
||||
|
||||
<div class="justify-items-center">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
<OurProjects />
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||