diff --git a/app/(landing)/layout.tsx b/app/(landing)/layout.tsx new file mode 100644 index 0000000..b76986f --- /dev/null +++ b/app/(landing)/layout.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'hackerspace.sh — Operational Software for Makerspaces', + description: 'Member management, dues tracking, task coordination, and project visibility. Built by people who have actually run these spaces.', +} + +export default function LandingLayout({ + children, +}: { + children: React.ReactNode +}) { + return children +} diff --git a/app/(landing)/page.tsx b/app/(landing)/page.tsx new file mode 100644 index 0000000..a570b32 --- /dev/null +++ b/app/(landing)/page.tsx @@ -0,0 +1,504 @@ +'use client' + +import { useState, useEffect } from 'react' +import Link from 'next/link' + +// Icons +function IcoUsers({ className }: { className?: string }) { + return +} +function IcoTask({ className }: { className?: string }) { + return +} +function IcoFolder({ className }: { className?: string }) { + return +} +function IcoCreditCard({ className }: { className?: string }) { + return +} +function IcoLock({ className }: { className?: string }) { + return +} +function IcoDoc({ className }: { className?: string }) { + return +} +function IcoArrow({ className }: { className?: string }) { + return +} +function IcoTerminal({ className }: { className?: string }) { + return +} +function IcoEye({ className }: { className?: string }) { + return +} +function IcoPin({ className }: { className?: string }) { + return +} + +// ─── Mini UI Previews ────────────────────────────────────────────────────────── + +function MiniMemberTable() { + const [activeRow, setActiveRow] = useState(0) + const members = [ + { name: 'Alex Chen', tier: 'PLUS', status: 'current' }, + { name: 'Jordan Mills', tier: 'BASIC', status: 'current' }, + { name: 'Sam Rivera', tier: 'PLUS', status: 'late' }, + { name: 'Casey Wong', tier: 'BASIC', status: 'current' }, + ] + useEffect(() => { + const t = setInterval(() => setActiveRow(p => (p + 1) % members.length), 2000) + return () => clearInterval(t) + }, [members.length]) + return ( +
+
+ Members + {members.length} +
+
+ {members.map((m, i) => ( +
+
+ {m.name.split(' ').map(n => n[0]).join('')} +
+ {m.name} + {m.tier} + +
+ ))} +
+
+ ) +} + +function MiniTaskList() { + const [checkedIndex, setCheckedIndex] = useState(-1) + const tasks = [ + { title: 'Clean laser cutter lens', area: 'Laser' }, + { title: 'Restock filament', area: '3D Print' }, + { title: 'Update wiki docs', area: 'Admin' }, + ] + useEffect(() => { + const t = setInterval(() => { + setCheckedIndex(p => (p >= tasks.length - 1 ? -1 : p + 1)) + }, 1500) + return () => clearInterval(t) + }, [tasks.length]) + return ( +
+
+ Tasks + + +
+
+ {tasks.map((t, i) => ( +
+
+ {i <= checkedIndex && ( + + + + )} +
+ {t.title} + {t.area} +
+ ))} +
+
+ ) +} + +function MiniProjectBoard() { + const [movingCard, setMovingCard] = useState(0) + useEffect(() => { + const t = setInterval(() => setMovingCard(p => (p + 1) % 3), 2500) + return () => clearInterval(t) + }, []) + const columns = ['BACKLOG', 'IN PROGRESS', 'DONE'] + return ( +
+
+ Projects +
+
+ {columns.map((col, colIndex) => ( +
+
+ + {col} +
+ {colIndex === movingCard && ( +
+

CNC Upgrade

+
+
+
+
+ )} + {colIndex === 1 && colIndex !== movingCard && ( +
+

Network rack

+
+ )} +
+ ))} +
+
+ ) +} + +function MiniPayments() { + const [linking, setLinking] = useState(false) + useEffect(() => { + const t = setInterval(() => setLinking(p => !p), 2000) + return () => clearInterval(t) + }, []) + return ( +
+
+ Payments + 2 unlinked +
+
+
+ PAYPAL + $50.00 + + {linking ? 'LINKED' : '+ LINK'} + +
+
+ VENMO + $75.00 + LINKED +
+
+
+ ) +} + +function MiniOps() { + const [revealedIndex, setRevealedIndex] = useState(-1) + const [tab, setTab] = useState<'secrets' | 'kb'>('secrets') + + const secrets = [ + { label: 'WiFi Password', area: 'Network' }, + { label: 'Alarm Code', area: 'Security' }, + { label: 'PayPal API Key', area: 'Finance' }, + ] + const kb = [ + { title: 'Open / Close Procedure', area: 'Ops', pinned: true }, + { title: 'Laser Cutter Safety', area: 'Laser', pinned: false }, + { title: 'Board Meeting Runbook', area: 'Admin', pinned: true }, + ] + + useEffect(() => { + if (tab !== 'secrets') return + const t = setInterval(() => { + setRevealedIndex(p => (p >= secrets.length - 1 ? -1 : p + 1)) + }, 1800) + return () => clearInterval(t) + }, [tab, secrets.length]) + + useEffect(() => { + const t = setInterval(() => { + setTab(p => (p === 'secrets' ? 'kb' : 'secrets')) + setRevealedIndex(-1) + }, 5000) + return () => clearInterval(t) + }, []) + + return ( +
+
+ Ops +
+
+ {(['secrets', 'kb'] as const).map(t => ( + + ))} +
+
+ {tab === 'secrets' && secrets.map((s, i) => ( +
+
+ +
+ {s.label} + {s.area} +
+ + {i === revealedIndex ? ( + ••••1234 + ) : ( + reveal + )} +
+
+ ))} + {tab === 'kb' && kb.map((k) => ( +
+
+ +
+ {k.title} + {k.pinned && } + {k.area} +
+ ))} +
+
+ ) +} + +function MiniDashboard() { + const [tick, setTick] = useState(0) + useEffect(() => { + const t = setInterval(() => setTick(p => p + 1), 3000) + return () => clearInterval(t) + }, []) + const members = 47 + (tick % 3) + return ( +
+
+
+ + Members +
+

{members}

+
+
+
+ + Tasks +
+

12

+
+
+
+ Due this week + 3 late +
+
+ {Array.from({ length: 12 }).map((_, i) => ( +
+ ))} +
+
+
+ ) +} + +// ─── Main Landing Page ───────────────────────────────────────────────────────── + +export default function LandingPage() { + return ( +
+ {/* Nav */} + + + {/* Hero */} +
+
+
+
+

+ hackerspace management +

+

+ Members, payments, tasks, projects, ops docs +

+

+ Track who is in your space, what they owe, what needs doing, and where to find the WiFi password. +

+
+ + Get Started + + + + Log in + +
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ + {/* Features */} +
+
+
+

What it does

+

+ Six modules +

+
+ +
+ {[ + { + Icon: IcoUsers, + title: 'Member Directory', + description: 'Track membership tiers, payment status, and contact info. Bulk actions for common operations. Know who is current and who is not.', + preview: , + }, + { + Icon: IcoTask, + title: 'Task Board', + description: 'Chores, maintenance, and one-off tasks with assignments, due dates, and recurring schedules. Claim and complete from anywhere.', + preview: , + }, + { + Icon: IcoFolder, + title: 'Project Tracking', + description: 'Kanban board for space projects. Track progress, assign areas, and keep the community informed about what is in motion.', + preview: , + }, + { + Icon: IcoCreditCard, + title: 'Payment Reconciliation', + description: 'Import from PayPal, Venmo, or Zeffy. Match transactions to members manually or let it auto-match by email.', + preview: , + }, + { + Icon: IcoLock, + title: 'Ops: Secrets and KB', + description: 'Store WiFi passwords, alarm codes, API keys, and procedures in one place. Role-based visibility. Reveal on demand.', + preview: , + }, + { + Icon: IcoDoc, + title: 'Dashboard', + description: 'At a glance status on membership, open tasks, payment issues, and recent activity. Know what needs attention when you log in.', + preview: , + }, + ].map((feature) => ( +
+
+
+ +
+

{feature.title}

+
+

+ {feature.description} +

+
+ {feature.preview} +
+
+ ))} +
+
+
+ + {/* How it works */} +
+
+
+

Setup

+

+ From nothing to running in under an hour +

+
+
+ {[ + { + step: '01', + title: 'Import your members', + description: 'Paste from a spreadsheet or add them one by one. Set tiers, statuses, and contact info.', + }, + { + step: '02', + title: 'Connect payments', + description: 'Link your PayPal or Zeffy account. Import transactions and match them to members.', + }, + { + step: '03', + title: 'Seed your ops docs', + description: 'Drop in your SOPs, credentials, and area leads. The whole board has one place to look things up.', + }, + ].map((item) => ( +
+

{item.step}

+

{item.title}

+

{item.description}

+
+ ))} +
+
+
+ + {/* CTA */} +
+
+
+ + Get Started + + + + Log in + +
+
+
+ + {/* Footer */} +
+
+
+
+ + hackerspace.sh +
+

+ Management software for makerspaces and community workshops. +

+
+
+
+
+ ) +} diff --git a/app/page.tsx b/app/page.tsx index 54d0f5f..7f790fa 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,12 +1,12 @@ import { createClient } from '@/lib/supabase/server' import { redirect } from 'next/navigation' +import LandingPage from './(landing)/page' export default async function RootPage() { const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() if (user) { redirect('/dashboard') - } else { - redirect('/login') } + return }