From 7e15dd1e63c08577642946aa9a7852faf1f7c58e Mon Sep 17 00:00:00 2001 From: Moheeb Zara Date: Thu, 14 May 2026 15:18:54 -0700 Subject: [PATCH] feat: bake hackerspace-sh subsite into the management app Ports the existing hackerspace.sh Vue site (sibling repo) into this codebase as a route group `app/(resources)/` so a single domain serves both the public research / games subsite and the authenticated management app. Routes added (all public, all prerendered): /resources landing page (the original HomePage.vue) /zine iframe to the printable hackerspace zine /governance iframe to Governance in a Box /space-after-dark iframe to the isometric exploration game /proposal-duel iframe to the proposal-debate game /atlas iframe to the bundled atlas.html (2.2 MB) The individual resource paths match the originals exactly so existing deep links and bookmarks survive the domain switch-over. Ports six Vue components to React: Wordmark, SpaceIcon, TraceIcon, CubeIcon, KeyIcon, GridIcon, PromptIcon, AtlasLogo, ProjectCard, IframeWrapper. CSS scoped under `.resources-root` so the dark IBM Plex Mono / Libre Baskerville theme cannot bleed into the management app's shadcn design system. Fonts loaded via next/font/google. The 2.2 MB atlas.html lives in /public. proxy.ts adds the six routes plus /atlas.html to PUBLIC_ROUTES so anonymous visitors can reach them without redirecting to /login. The management landing page links to /resources twice: a "Resources" nav link, and a callout under the CTA buttons ("Not running a space? Read the hackerspace research and games"). --- app/(landing)/page.tsx | 9 + app/(resources)/atlas/page.tsx | 10 + app/(resources)/governance/page.tsx | 14 + app/(resources)/layout.tsx | 33 + app/(resources)/proposal-duel/page.tsx | 14 + app/(resources)/resources.css | 508 + app/(resources)/resources/page.tsx | 153 + app/(resources)/space-after-dark/page.tsx | 14 + app/(resources)/zine/page.tsx | 14 + components/resources/atlas-logo.tsx | 74 + components/resources/icons.tsx | 113 + components/resources/iframe-wrapper.tsx | 24 + components/resources/project-card.tsx | 67 + proxy.ts | 20 +- public/atlas.html | 48112 ++++++++++++++++++++ 15 files changed, 49178 insertions(+), 1 deletion(-) create mode 100644 app/(resources)/atlas/page.tsx create mode 100644 app/(resources)/governance/page.tsx create mode 100644 app/(resources)/layout.tsx create mode 100644 app/(resources)/proposal-duel/page.tsx create mode 100644 app/(resources)/resources.css create mode 100644 app/(resources)/resources/page.tsx create mode 100644 app/(resources)/space-after-dark/page.tsx create mode 100644 app/(resources)/zine/page.tsx create mode 100644 components/resources/atlas-logo.tsx create mode 100644 components/resources/icons.tsx create mode 100644 components/resources/iframe-wrapper.tsx create mode 100644 components/resources/project-card.tsx create mode 100644 public/atlas.html diff --git a/app/(landing)/page.tsx b/app/(landing)/page.tsx index a570b32..94007ae 100644 --- a/app/(landing)/page.tsx +++ b/app/(landing)/page.tsx @@ -315,6 +315,9 @@ export default function LandingPage() { hackerspace.sh
+ + Resources + Log in @@ -482,6 +485,12 @@ export default function LandingPage() { Log in
+

+ Not running a space? Read the{' '} + + hackerspace research and games → + +

diff --git a/app/(resources)/atlas/page.tsx b/app/(resources)/atlas/page.tsx new file mode 100644 index 0000000..cf21ebe --- /dev/null +++ b/app/(resources)/atlas/page.tsx @@ -0,0 +1,10 @@ +import { IframeWrapper } from '@/components/resources/iframe-wrapper' + +export const metadata = { + title: 'Hackerspace Atlas — hackerspace.sh', +} + +export default function AtlasPage() { + // atlas.html lives in /public, served by Next.js at the same path. + return +} diff --git a/app/(resources)/governance/page.tsx b/app/(resources)/governance/page.tsx new file mode 100644 index 0000000..3cc79c9 --- /dev/null +++ b/app/(resources)/governance/page.tsx @@ -0,0 +1,14 @@ +import { IframeWrapper } from '@/components/resources/iframe-wrapper' + +export const metadata = { + title: 'Governance in a Box — hackerspace.sh', +} + +export default function GovernancePage() { + return ( + + ) +} diff --git a/app/(resources)/layout.tsx b/app/(resources)/layout.tsx new file mode 100644 index 0000000..f5d9cf9 --- /dev/null +++ b/app/(resources)/layout.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from 'next' +import { IBM_Plex_Mono, Libre_Baskerville } from 'next/font/google' +import './resources.css' + +const ibmPlexMono = IBM_Plex_Mono({ + subsets: ['latin'], + weight: ['400', '500'], + variable: '--font-ibm-plex-mono', + display: 'swap', +}) + +const libreBaskerville = Libre_Baskerville({ + subsets: ['latin'], + weight: ['400', '700'], + style: ['normal', 'italic'], + variable: '--font-libre-baskerville', + display: 'swap', +}) + +export const metadata: Metadata = { + title: 'hackerspace.sh', + description: + 'How hackerspaces actually work. Research, tools, and games for community workshop governance.', +} + +export default function ResourcesLayout({ children }: { children: React.ReactNode }) { + return ( +
+ + ) +} diff --git a/app/(resources)/proposal-duel/page.tsx b/app/(resources)/proposal-duel/page.tsx new file mode 100644 index 0000000..a030d3a --- /dev/null +++ b/app/(resources)/proposal-duel/page.tsx @@ -0,0 +1,14 @@ +import { IframeWrapper } from '@/components/resources/iframe-wrapper' + +export const metadata = { + title: 'Proposal Duel — hackerspace.sh', +} + +export default function ProposalDuelPage() { + return ( + + ) +} diff --git a/app/(resources)/resources.css b/app/(resources)/resources.css new file mode 100644 index 0000000..1c4e6aa --- /dev/null +++ b/app/(resources)/resources.css @@ -0,0 +1,508 @@ +/* ============================================================================= + * Resources subsite stylesheet — ported from the hackerspace-sh Vue project. + * + * Scoped under .resources-root so it cannot bleed into the management app's + * shadcn/Tailwind design system. Every selector below must descend from + * .resources-root. + * ===========================================================================*/ + +.resources-root { + --res-bg: #0c0c0c; + --res-surface: #161616; + --res-border: #252525; + --res-text: #e8e8e8; + --res-text-dim: #707070; + --res-text-muted: #404040; + --res-accent: #d4ff00; + + background: var(--res-bg); + color: var(--res-text); + font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 16px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + min-height: 100vh; + position: relative; +} + +.resources-root ::selection { + background: var(--res-accent); + color: var(--res-bg); +} + +.resources-root a { + color: inherit; + text-decoration: none; +} + +.resources-root .wordmark { + height: 1.5em; + width: auto; +} + +/* Grid background */ +.resources-root .resources-grid-bg { + position: fixed; + inset: 0; + background-image: + linear-gradient(var(--res-border) 1px, transparent 1px), + linear-gradient(90deg, var(--res-border) 1px, transparent 1px); + background-size: 60px 60px; + opacity: 0.3; + pointer-events: none; + z-index: 0; +} + +.resources-root > *:not(.resources-grid-bg):not(.resources-iframe-wrapper) { + position: relative; + z-index: 1; +} + +/* Container */ +.resources-root .resources-container { + max-width: 1100px; + margin: 0 auto; + padding: 0 32px; +} + +/* Header */ +.resources-root header { + padding: 48px 0; + border-bottom: 1px solid var(--res-border); +} + +.resources-root .resources-header-inner { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24px; + flex-wrap: wrap; +} + +.resources-root .resources-site-title { + display: flex; + align-items: center; +} + +.resources-root .resources-site-title svg { + height: 24px; + width: auto; +} + +.resources-root .resources-header-links { + display: flex; + gap: 32px; + font-size: 0.8125rem; + color: var(--res-text-dim); +} + +.resources-root .resources-header-links a:hover { + color: var(--res-text); +} + +.resources-root .resources-header-links a.resources-header-link-accent { + color: var(--res-accent); +} + +.resources-root .resources-header-links a.resources-header-link-accent:hover { + color: var(--res-text); +} + +/* Hero */ +.resources-root .resources-hero { + padding: 32px 0; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 32px; + align-items: center; +} + +.resources-root .resources-hero-content { + display: flex; + flex-direction: column; + gap: 16px; +} + +.resources-root .resources-hero-title { + font-family: 'Libre Baskerville', Georgia, serif; + font-size: clamp(1.75rem, 4vw, 2.5rem); + font-weight: 400; + line-height: 1.15; + letter-spacing: -0.02em; +} + +.resources-root .resources-hero-title em { + font-style: italic; + color: var(--res-accent); +} + +.resources-root .resources-hero-prompt { + width: 32px; + height: 32px; + color: var(--res-text); +} + +.resources-root .resources-hero-sub { + font-size: 0.9375rem; + color: var(--res-text-dim); + line-height: 1.7; + max-width: 380px; +} + +/* Section header */ +.resources-root .resources-section-header { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 48px 0 40px; + border-top: 1px solid var(--res-border); +} + +.resources-root .resources-section-label { + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--res-text-dim); +} + +.resources-root .resources-section-count { + font-size: 0.75rem; + color: var(--res-text-muted); +} + +/* Projects grid */ +.resources-root .resources-projects { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1px; + background: var(--res-border); + border: 1px solid var(--res-border); +} + +.resources-root .resources-card { + background: var(--res-bg); + padding: 40px; + display: flex; + flex-direction: column; + min-height: 280px; + position: relative; + transition: background 0.3s ease; +} + +.resources-root .resources-card:hover { + background: var(--res-surface); +} + +.resources-root .resources-card-content { + display: flex; + flex-direction: column; + flex: 1; +} + +.resources-root .resources-card-icon { + position: absolute; + top: 24px; + right: 24px; + width: 48px; + height: 48px; + color: var(--res-text); + transition: all 0.3s ease; +} + +.resources-root .resources-card-icon svg { + width: 100%; + height: 100%; +} + +.resources-root .resources-card-has-icon .resources-card-num { + padding-right: 60px; +} + +.resources-root .resources-card-num { + font-size: 0.6875rem; + color: var(--res-text-muted); + letter-spacing: 0.1em; + margin-bottom: 24px; +} + +.resources-root .resources-card-title { + font-family: 'Libre Baskerville', Georgia, serif; + font-size: 1.625rem; + font-weight: 400; + letter-spacing: -0.01em; + margin-bottom: 12px; + transition: color 0.3s ease; +} + +.resources-root .resources-card:hover .resources-card-title { + color: var(--res-accent); +} + +.resources-root .resources-card-desc { + font-size: 0.8125rem; + color: var(--res-text-dim); + line-height: 1.65; + flex-grow: 1; +} + +.resources-root .resources-card-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 32px; + padding-top: 20px; + border-top: 1px solid var(--res-border); +} + +.resources-root .resources-card-type { + font-size: 0.6875rem; + color: var(--res-text-muted); + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.resources-root .resources-card-arrow { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--res-border); + border-radius: 50%; + transition: all 0.3s ease; +} + +.resources-root .resources-card-arrow svg { + width: 12px; + height: 12px; + color: var(--res-text-dim); + transition: all 0.3s ease; +} + +.resources-root .resources-card:hover .resources-card-arrow { + background: var(--res-accent); + border-color: var(--res-accent); +} + +.resources-root .resources-card:hover .resources-card-arrow svg { + color: var(--res-bg); + transform: translateX(2px); +} + +.resources-root .resources-card-featured { + grid-column: 1 / -1; + flex-direction: row; + align-items: center; + gap: 60px; + min-height: auto; + padding: 48px; +} + +.resources-root .resources-card-featured .resources-card-content { + flex: 1; +} + +.resources-root .resources-card-visual { + width: 180px; + height: 180px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--res-text); +} + +.resources-root .resources-card-visual svg { + width: 100%; + height: 100%; +} + +/* Author */ +.resources-root .resources-author { + padding: 80px 0; + border-top: 1px solid var(--res-border); + display: grid; + grid-template-columns: 1fr auto; + gap: 48px; + align-items: center; +} + +.resources-root .resources-author-content { + display: flex; + flex-direction: column; + gap: 8px; +} + +.resources-root .resources-author-label { + font-size: 0.6875rem; + color: var(--res-text-muted); + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.resources-root .resources-author-name { + font-family: 'Libre Baskerville', Georgia, serif; + font-size: 1.5rem; + font-weight: 400; +} + +.resources-root .resources-author-bio { + font-size: 0.8125rem; + color: var(--res-text-dim); +} + +.resources-root .resources-author-link { + font-size: 0.8125rem; + padding: 14px 28px; + border: 1px solid var(--res-border); + display: inline-flex; + align-items: center; + gap: 10px; + transition: all 0.3s ease; +} + +.resources-root .resources-author-link:hover { + background: var(--res-accent); + border-color: var(--res-accent); + color: var(--res-bg); +} + +.resources-root .resources-author-link svg { + width: 12px; + height: 12px; +} + +/* Footer */ +.resources-root footer { + padding: 40px 0; + border-top: 1px solid var(--res-border); + font-size: 0.75rem; + color: var(--res-text-muted); +} + +.resources-root .resources-footer-inner { + display: flex; + justify-content: space-between; + align-items: center; +} + +.resources-root footer a { + color: var(--res-text-dim); + transition: color 0.2s; +} + +.resources-root footer a:hover { + color: var(--res-text); +} + +.resources-root .resources-footer-links { + display: flex; + gap: 24px; +} + +/* Iframe wrapper (used by /zine, /governance, /space-after-dark, /proposal-duel, /atlas) */ +.resources-root .resources-iframe-wrapper { + position: fixed; + inset: 0; + display: flex; + flex-direction: column; + background: var(--res-bg); + z-index: 2; +} + +.resources-root .resources-iframe-topbar { + height: 48px; + min-height: 48px; + background: var(--res-bg); + border-bottom: 1px solid var(--res-border); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 24px; + font-size: 0.8125rem; +} + +.resources-root .resources-iframe-back { + display: flex; + align-items: center; + gap: 8px; + color: var(--res-text-dim); + transition: color 0.2s; +} + +.resources-root .resources-iframe-back:hover { + color: var(--res-text); +} + +.resources-root .resources-iframe-back > svg:first-child { + width: 14px; + height: 14px; +} + +.resources-root .resources-iframe-back .wordmark { + height: 18px; + width: auto; +} + +.resources-root .resources-iframe-title { + color: var(--res-text-muted); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.resources-root .resources-iframe-frame { + flex: 1; + width: 100%; + border: none; + background: #fff; +} + +/* Mobile */ +@media (max-width: 768px) { + .resources-root .resources-container { + padding: 0 20px; + } + + .resources-root .resources-hero { + grid-template-columns: 1fr; + gap: 16px; + padding: 24px 0; + } + + .resources-root .resources-hero-sub { + max-width: 100%; + } + + .resources-root .resources-projects { + grid-template-columns: 1fr; + } + + .resources-root .resources-card { + padding: 32px 24px; + min-height: auto; + } + + .resources-root .resources-card-featured { + flex-direction: column; + gap: 32px; + } + + .resources-root .resources-card-featured .resources-card-visual { + width: 140px; + height: 140px; + margin: 0 auto; + } + + .resources-root .resources-author { + grid-template-columns: 1fr; + gap: 24px; + } + + .resources-root .resources-footer-inner { + flex-direction: column; + gap: 16px; + text-align: center; + } +} diff --git a/app/(resources)/resources/page.tsx b/app/(resources)/resources/page.tsx new file mode 100644 index 0000000..75ca01f --- /dev/null +++ b/app/(resources)/resources/page.tsx @@ -0,0 +1,153 @@ +import Link from 'next/link' +import { ProjectCard } from '@/components/resources/project-card' +import { + Wordmark, + PromptIcon, + SpaceIcon, + TraceIcon, + CubeIcon, + KeyIcon, + GridIcon, +} from '@/components/resources/icons' +import { AtlasLogo } from '@/components/resources/atlas-logo' + +export const metadata = { + title: 'hackerspace.sh — projects', + description: + 'How hackerspaces actually work: research, governance frameworks, games, and a global atlas of community workshops.', +} + +export default function ResourcesLandingPage() { + return ( + <> +
+
+
+ + + + +
+
+
+ +
+
+
+
+

+ How hackerspaces actually work +

+ +
+

+ The dynamics, the drama, the governance patterns that make or break community + workshops. Research turned into tools you can use. +

+
+ +
+
+ Projects + 06 tools +
+ +
+ } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> +
+
+ +
+
+ Made by +

Moheeb Zara

+

+ Hardware hacker, new media artist, maker community advocate +

+
+ + hack.build + + + + +
+
+
+ +
+
+

+ A project from{' '} + HeatSync Labs. Run your own space?{' '} + Manage it here → +

+ +
+
+ + ) +} diff --git a/app/(resources)/space-after-dark/page.tsx b/app/(resources)/space-after-dark/page.tsx new file mode 100644 index 0000000..f3842d8 --- /dev/null +++ b/app/(resources)/space-after-dark/page.tsx @@ -0,0 +1,14 @@ +import { IframeWrapper } from '@/components/resources/iframe-wrapper' + +export const metadata = { + title: 'The Space After Dark — hackerspace.sh', +} + +export default function SpaceAfterDarkPage() { + return ( + + ) +} diff --git a/app/(resources)/zine/page.tsx b/app/(resources)/zine/page.tsx new file mode 100644 index 0000000..99b6452 --- /dev/null +++ b/app/(resources)/zine/page.tsx @@ -0,0 +1,14 @@ +import { IframeWrapper } from '@/components/resources/iframe-wrapper' + +export const metadata = { + title: 'The Hackerspace Game — hackerspace.sh', +} + +export default function ZinePage() { + return ( + + ) +} diff --git a/components/resources/atlas-logo.tsx b/components/resources/atlas-logo.tsx new file mode 100644 index 0000000..6d0ca18 --- /dev/null +++ b/components/resources/atlas-logo.tsx @@ -0,0 +1,74 @@ +import type { SVGProps } from 'react' + +export function AtlasLogo(props: SVGProps) { + return ( + + {/* Globe outer */} + + {/* Meridians */} + + + {/* Equator */} + + {/* Latitude rings */} + + + {/* Vias at intersections */} + + + + + + + + + + + {/* Hackerspace markers */} + + + + + + + + + + + + + + + + {/* Soldering iron compass needle */} + + + + + + + + + + + + + + + + + {/* East/west pointers */} + + + {/* Center hub */} + + + + ) +} diff --git a/components/resources/icons.tsx b/components/resources/icons.tsx new file mode 100644 index 0000000..c295967 --- /dev/null +++ b/components/resources/icons.tsx @@ -0,0 +1,113 @@ +// Iconography ported from the original hackerspace-sh Vue subsite. +// All SVGs render with `currentColor` so they inherit the surrounding +// text colour and can be tinted by parent styles. The lime accent +// (#d4ff00) is hardcoded where the original components used it. + +import type { SVGProps } from 'react' + +type IconProps = SVGProps + +export function Wordmark(props: IconProps) { + return ( + + + hackerspace + .sh + + + ) +} + +export function SpaceIcon(props: IconProps) { + return ( + + + + + + ) +} + +export function TraceIcon(props: IconProps) { + return ( + + + + + + + ) +} + +export function CubeIcon(props: IconProps) { + return ( + + + + + + ) +} + +export function KeyIcon(props: IconProps) { + return ( + + + + + + + ) +} + +export function GridIcon(props: IconProps) { + return ( + + + + + + + ) +} + +export function PromptIcon(props: IconProps) { + return ( + + + + + ) +} diff --git a/components/resources/iframe-wrapper.tsx b/components/resources/iframe-wrapper.tsx new file mode 100644 index 0000000..0da3509 --- /dev/null +++ b/components/resources/iframe-wrapper.tsx @@ -0,0 +1,24 @@ +import Link from 'next/link' +import { Wordmark } from './icons' + +/** + * Renders an iframe at full-viewport size with a thin top bar that links + * back to /resources and labels the embedded content. Used by /zine, + * /governance, /space-after-dark, /proposal-duel, and /atlas. + */ +export function IframeWrapper({ url, title }: { url: string; title: string }) { + return ( +
+
+ + + + + + + {title} +
+