diff --git a/app/(landing)/landing.css b/app/(landing)/landing.css index f308aa9..0784365 100644 --- a/app/(landing)/landing.css +++ b/app/(landing)/landing.css @@ -77,6 +77,26 @@ /* Hero */ .landing-root .landing-hero { padding: 88px 0 72px; border-bottom: 1px solid var(--ln-border); } +.landing-root .landing-hero-grid { + display: grid; grid-template-columns: 1fr 1fr; gap: 56px; align-items: center; +} +.landing-root .landing-preview-frame { + border: 1px solid var(--ln-border); + background: var(--ln-surface); + border-radius: 8px; + padding: 14px; +} +.landing-root .landing-preview-chrome { + display: flex; gap: 6px; padding: 2px 4px 12px; +} +.landing-root .landing-preview-chrome span { + width: 9px; height: 9px; border-radius: 50%; + background: var(--ln-border); +} +@media (max-width: 900px) { + .landing-root .landing-hero-grid { grid-template-columns: 1fr; gap: 40px; } + .landing-root .landing-hero-visual { display: none; } +} .landing-root .landing-eyebrow { font-size: 0.75rem; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ln-text-dim); margin-bottom: 24px; diff --git a/app/(landing)/page.tsx b/app/(landing)/page.tsx index aa9cd83..8793ead 100644 --- a/app/(landing)/page.tsx +++ b/app/(landing)/page.tsx @@ -2,6 +2,7 @@ import Link from 'next/link' import { BrandMark } from '@/components/brand-mark' import { GithubIcon, ArrowIcon } from '@/components/landing/icons' import { ResourceShowcase } from '@/components/landing/resource-showcase' +import { HeroPreviewCluster } from '@/components/landing/mini-previews' const MODULES = [ { num: '01', title: 'Members', desc: 'Tiers, dues status, contact info, custom roles. Know who is current and who is not, at a glance.' }, @@ -45,21 +46,31 @@ export default function LandingPage() {
-
-

Operating system for hackerspaces

-

- Run your space, not a spreadsheet -

-

- Members, dues, tasks, projects, ops docs, and governance in one - place. Built by people who have actually run these spaces, for the - people who keep them running. -

-
- - Get started - - Log in +
+
+

Operating system for hackerspaces

+

+ Run your space, not a spreadsheet +

+

+ Members, dues, tasks, projects, ops docs, and governance in one + place. Built by people who have actually run these spaces, for the + people who keep them running. +

+
+ + Get started + + Log in +
+
+
+
+ + +
diff --git a/components/landing/mini-previews.tsx b/components/landing/mini-previews.tsx new file mode 100644 index 0000000..3a4e228 --- /dev/null +++ b/components/landing/mini-previews.tsx @@ -0,0 +1,260 @@ +'use client' + +import { useState, useEffect } from 'react' + +// Animated product mini-previews shown framed on the landing hero, as if +// they were small screenshots of the real app. They intentionally use the +// app's own design tokens (bg-card, text-foreground, etc.) so they read as +// the actual product UI sitting inside the editorial marketing page. + +function IcoUsers({ className }: { className?: string }) { + return +} +function IcoTask({ className }: { className?: string }) { + return +} +function IcoLock({ className }: { className?: string }) { + return +} +function IcoDoc({ className }: { className?: string }) { + return +} +function IcoEye({ className }: { className?: string }) { + return +} +function IcoPin({ className }: { className?: string }) { + return +} + +export 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} + +
+ ))} +
+
+ ) +} + +export 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} +
+ ))} +
+
+ ) +} + +export 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 +
+
+
+ ) +} + +export 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} +
+ ))} +
+
+ ) +} + +export 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) => ( +
+ ))} +
+
+
+ ) +} + +// Curated cluster used on the landing hero. +export function HeroPreviewCluster() { + return ( +
+
+ + +
+
+ + + +
+
+ ) +}