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").
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import type { SVGProps } from 'react'
|
||||
|
||||
export function AtlasLogo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="60 -5 280 350"
|
||||
role="img"
|
||||
aria-label="Hackerspace Atlas logo"
|
||||
{...props}
|
||||
>
|
||||
{/* Globe outer */}
|
||||
<circle cx="200" cy="185" r="115" stroke="currentColor" fill="none" strokeWidth={7} strokeLinecap="round" strokeLinejoin="round" />
|
||||
{/* Meridians */}
|
||||
<ellipse cx="200" cy="185" rx="45" ry="115" stroke="currentColor" fill="none" strokeWidth={4} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<ellipse cx="200" cy="185" rx="85" ry="115" stroke="currentColor" fill="none" strokeWidth={4} strokeLinecap="round" strokeLinejoin="round" />
|
||||
{/* Equator */}
|
||||
<line x1="85" y1="185" x2="315" y2="185" stroke="currentColor" fill="none" strokeWidth={4} strokeLinecap="round" strokeLinejoin="round" />
|
||||
{/* Latitude rings */}
|
||||
<ellipse cx="200" cy="133" rx="100" ry="28" stroke="currentColor" fill="none" strokeWidth={4} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<ellipse cx="200" cy="237" rx="100" ry="28" stroke="currentColor" fill="none" strokeWidth={4} strokeLinecap="round" strokeLinejoin="round" />
|
||||
{/* Vias at intersections */}
|
||||
<g fill="currentColor" stroke="none">
|
||||
<circle cx="85" cy="185" r="9" /><circle cx="85" cy="185" r="3.5" fill="#0c0c0c" />
|
||||
<circle cx="115" cy="185" r="7" /><circle cx="115" cy="185" r="2.5" fill="#0c0c0c" />
|
||||
<circle cx="155" cy="185" r="7" /><circle cx="155" cy="185" r="2.5" fill="#0c0c0c" />
|
||||
<circle cx="245" cy="185" r="7" /><circle cx="245" cy="185" r="2.5" fill="#0c0c0c" />
|
||||
<circle cx="285" cy="185" r="7" /><circle cx="285" cy="185" r="2.5" fill="#0c0c0c" />
|
||||
<circle cx="315" cy="185" r="9" /><circle cx="315" cy="185" r="3.5" fill="#0c0c0c" />
|
||||
<circle cx="200" cy="70" r="8" /><circle cx="200" cy="70" r="3" fill="#0c0c0c" />
|
||||
<circle cx="200" cy="300" r="8" /><circle cx="200" cy="300" r="3" fill="#0c0c0c" />
|
||||
</g>
|
||||
{/* Hackerspace markers */}
|
||||
<g>
|
||||
<circle cx="145" cy="160" r="11" fill="currentColor" />
|
||||
<circle cx="145" cy="160" r="4.5" fill="#0c0c0c" />
|
||||
<circle cx="145" cy="160" r="16" stroke="currentColor" fill="none" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</g>
|
||||
<g>
|
||||
<circle cx="260" cy="205" r="10" fill="currentColor" />
|
||||
<circle cx="260" cy="205" r="4" fill="#0c0c0c" />
|
||||
<circle cx="260" cy="205" r="15" stroke="currentColor" fill="none" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</g>
|
||||
<g>
|
||||
<circle cx="170" cy="255" r="9" fill="currentColor" />
|
||||
<circle cx="170" cy="255" r="3.5" fill="#0c0c0c" />
|
||||
<circle cx="170" cy="255" r="14" stroke="currentColor" fill="none" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</g>
|
||||
{/* Soldering iron compass needle */}
|
||||
<g>
|
||||
<line x1="200" y1="75" x2="200" y2="285" stroke="currentColor" strokeWidth={10} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M200 28 L186 82 L200 98 L214 82 Z" fill="currentColor" />
|
||||
<rect x="192" y="96" width="16" height="12" rx="2" fill="currentColor" />
|
||||
<rect x="186" y="258" width="28" height="55" rx="6" fill="currentColor" />
|
||||
<line x1="189" y1="272" x2="211" y2="272" stroke="#0c0c0c" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<line x1="189" y1="286" x2="211" y2="286" stroke="#0c0c0c" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<line x1="189" y1="300" x2="211" y2="300" stroke="#0c0c0c" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<rect x="190" y="310" width="20" height="12" rx="4" fill="currentColor" />
|
||||
<path d="M200 335 L212 295 L200 278 L188 295 Z" fill="currentColor" opacity={0.9} />
|
||||
<g stroke="currentColor" fill="none" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" opacity={0.8}>
|
||||
<path d="M184 28 Q174 14 184 4" />
|
||||
<path d="M216 28 Q226 14 216 4" />
|
||||
<path d="M200 22 Q200 12 200 2" />
|
||||
</g>
|
||||
</g>
|
||||
{/* East/west pointers */}
|
||||
<path d="M330 185 L290 170 L275 185 L290 200 Z" fill="currentColor" opacity={0.9} />
|
||||
<path d="M70 185 L110 200 L125 185 L110 170 Z" fill="currentColor" opacity={0.9} />
|
||||
{/* Center hub */}
|
||||
<circle cx="200" cy="185" r="18" fill="currentColor" />
|
||||
<circle cx="200" cy="185" r="7" fill="#d4ff00" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -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<SVGSVGElement>
|
||||
|
||||
export function Wordmark(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 240 48"
|
||||
className="wordmark"
|
||||
{...props}
|
||||
>
|
||||
<text
|
||||
x="0"
|
||||
y="34"
|
||||
fontFamily="'IBM Plex Mono', monospace"
|
||||
fontSize="28"
|
||||
fontWeight="500"
|
||||
fill="currentColor"
|
||||
letterSpacing="-1"
|
||||
>
|
||||
hackerspace
|
||||
<tspan fill="#d4ff00">.sh</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SpaceIcon(props: IconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
|
||||
<path d="M6 6 L6 30 L18 30 L18 18 L30 18 L30 6 Z" fill="currentColor" />
|
||||
<path d="M18 18 L18 42 L42 42 L42 18 Z" fill="none" stroke="currentColor" strokeWidth={2.5} />
|
||||
<circle cx="30" cy="30" r="5" fill="#d4ff00" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function TraceIcon(props: IconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
|
||||
<path
|
||||
d="M8 8 L8 24 L24 24 L24 40 L40 40"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3.5}
|
||||
strokeLinecap="square"
|
||||
/>
|
||||
<circle cx="8" cy="8" r="5" fill="currentColor" />
|
||||
<circle cx="24" cy="24" r="5" fill="currentColor" />
|
||||
<circle cx="40" cy="40" r="6" fill="#d4ff00" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function CubeIcon(props: IconProps) {
|
||||
return (
|
||||
<svg viewBox="0 0 48 48" fill="none" {...props}>
|
||||
<path d="M24 4 L44 16 L44 36 L24 48 L4 36 L4 16 Z" stroke="currentColor" strokeWidth={2.5} />
|
||||
<path d="M24 4 L24 24 M4 16 L24 24 L44 16" stroke="currentColor" strokeWidth={2.5} />
|
||||
<path d="M24 24 L24 48" stroke="#d4ff00" strokeWidth={2.5} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function KeyIcon(props: IconProps) {
|
||||
return (
|
||||
<svg viewBox="0 0 48 48" fill="none" {...props}>
|
||||
<circle cx="14" cy="14" r="10" stroke="currentColor" strokeWidth={3} />
|
||||
<path d="M22 22 L42 42" stroke="currentColor" strokeWidth={4} strokeLinecap="round" />
|
||||
<path d="M34 34 L40 28" stroke="#d4ff00" strokeWidth={4} strokeLinecap="round" />
|
||||
<path d="M38 38 L44 32" stroke="#d4ff00" strokeWidth={4} strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GridIcon(props: IconProps) {
|
||||
return (
|
||||
<svg viewBox="0 0 48 48" fill="none" {...props}>
|
||||
<rect x="6" y="6" width="16" height="16" fill="currentColor" />
|
||||
<rect x="26" y="6" width="16" height="16" fill="currentColor" opacity={0.5} />
|
||||
<rect x="6" y="26" width="16" height="16" fill="currentColor" opacity={0.5} />
|
||||
<rect x="26" y="26" width="16" height="16" fill="#d4ff00" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function PromptIcon(props: IconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
|
||||
<path
|
||||
d="M8 12 L24 24 L8 36"
|
||||
fill="none"
|
||||
stroke="#d4ff00"
|
||||
strokeWidth={5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M28 36 L40 36"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={5}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="resources-iframe-wrapper">
|
||||
<div className="resources-iframe-topbar">
|
||||
<Link href="/resources" className="resources-iframe-back">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path d="M10 4L6 8l4 4" />
|
||||
</svg>
|
||||
<Wordmark />
|
||||
</Link>
|
||||
<span className="resources-iframe-title">{title}</span>
|
||||
</div>
|
||||
<iframe src={url} className="resources-iframe-frame" title={title} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import Link from 'next/link'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
type CommonProps = {
|
||||
num: string
|
||||
title: string
|
||||
description: string
|
||||
type: string
|
||||
featured?: boolean
|
||||
visual?: ReactNode
|
||||
}
|
||||
|
||||
type InternalProps = CommonProps & { to: string; href?: never; external?: false }
|
||||
type ExternalProps = CommonProps & { href: string; to?: never; external?: true }
|
||||
type Props = InternalProps | ExternalProps
|
||||
|
||||
/**
|
||||
* Project card on /resources. Behaves as either an internal next/link
|
||||
* (when `to` is set) or an external anchor with target=_blank (when
|
||||
* `href` is set). Featured cards span full width with the visual on the
|
||||
* right; non-featured render with a small icon in the top-right corner.
|
||||
*/
|
||||
export function ProjectCard(props: Props) {
|
||||
const { num, title, description, type, featured = false, visual } = props
|
||||
const hasIcon = visual && !featured
|
||||
|
||||
const body = (
|
||||
<>
|
||||
{hasIcon && <span className="resources-card-icon">{visual}</span>}
|
||||
<div className="resources-card-content">
|
||||
<span className="resources-card-num">{num}</span>
|
||||
<h2 className="resources-card-title">{title}</h2>
|
||||
<p className="resources-card-desc">{description}</p>
|
||||
<div className="resources-card-footer">
|
||||
<span className="resources-card-type">{type}</span>
|
||||
<span className="resources-card-arrow">
|
||||
{'external' in props && props.external ? (
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path d="M4 12L12 4M12 4H6M12 4v6" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path d="M3 8h10M9 4l4 4-4 4" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{featured && visual && <div className="resources-card-visual">{visual}</div>}
|
||||
</>
|
||||
)
|
||||
|
||||
const className = `resources-card${featured ? ' resources-card-featured' : ''}${hasIcon ? ' resources-card-has-icon' : ''}`
|
||||
|
||||
if ('to' in props && props.to) {
|
||||
return (
|
||||
<Link href={props.to} className={className}>
|
||||
{body}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<a href={(props as ExternalProps).href} className={className} target="_blank" rel="noopener noreferrer">
|
||||
{body}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user