13 KiB
Hackerspace.sh - Component Reference
Last Updated: 2026-03-10
Complete reference for all React components, their props, and usage patterns.
Table of Contents
1. App Shell Components
AppSidebar
File: components/app-sidebar.tsx
Purpose: Main navigation sidebar for the app.
Props:
interface AppSidebarProps {
member: SpaceMember & { spaces: Space }
taskBadge?: number // Count for tasks badge
commsBadge?: number // Count for unread messages
paymentBadge?: number // Count for unlinked payments
}
Usage:
<AppSidebar
member={member}
taskBadge={5}
paymentBadge={3}
/>
Features:
- Renders navigation grouped by section (Workspace, People, Admin)
- Shows badges for tasks and payments
- Displays user info with sign out
- Highlights active route
- Admin section only visible to admin/board roles
Navigation Structure:
WORKSPACE
├── Dashboard /dashboard
├── Tasks & Chores /tasks
├── Projects /projects
├── Ops & Facilities /ops
└── Comms /comms
PEOPLE
├── Members /members
├── Payments /payments
└── Contacts /contacts
ADMIN (admin/board only)
├── Import / Sync /import
└── Settings /settings
AppShell
File: components/app-shell.tsx
Purpose: Wrapper component for app layout (currently unused).
Status: Available but not integrated into layout.
2. Page Components
Page Structure Pattern
All feature pages follow a consistent Server Component + Client Component pattern:
feature/
├── page.tsx # Server Component (data fetching)
└── feature-client.tsx # Client Component (UI + interactions)
Dashboard Page
File: app/(app)/dashboard/page.tsx
Type: Server Component
Data Fetched:
- Member stats (active members count)
- Task stats (open tasks count)
- Payment stats (unlinked payments count)
- Recent tasks (top 5)
- Active projects (in_progress, blocked, review)
- Recent activity (last 6 items)
Features:
- 4 stat cards with badges
- Quick chores list with claim buttons
- Active projects with progress bars
- Payment alerts section
- Recent activity feed
Tasks Page
Files:
app/(app)/tasks/page.tsx(Server)app/(app)/tasks/tasks-client.tsx(Client)
Client Props:
interface Props {
tasks: Task[]
members: { id: string; display_name: string; user_id: string }[]
currentUserId: string
spaceId: string
}
Features:
- Tabbed view (Chores, Ongoing, My Tasks, Done)
- Area filter dropdown
- Create task modal
- Claim/Complete/Delete actions
- Status badges
State Management:
- Local state for tasks (optimistic updates)
- Form state for create modal
- Tab and filter state
Projects Page
Files:
app/(app)/projects/page.tsx(Server)app/(app)/projects/projects-client.tsx(Client)
Client Props:
interface Props {
projects: Project[]
spaceId: string
}
Features:
- 4-column Kanban (Backlog, In Progress, Review, Done)
- Status change via dropdown
- Create project modal
- Delete action
- Progress bars
Members Page
Files:
app/(app)/members/page.tsx(Server)app/(app)/members/members-client.tsx(Client)
Client Props:
interface Props {
members: Member[]
currentRole: string
}
Features:
- Tabbed view (All, Payment Issues, Pending Approval, Inactive)
- Search and tier filter
- Add member modal (admin/board only)
- Edit member modal
- Approve/Remove actions
- Payment status indicators
Payments Page
Files:
app/(app)/payments/page.tsx(Server)app/(app)/payments/payments-client.tsx(Client)
Client Props:
interface Props {
payments: Payment[]
members: Member[]
integrations: Integration[]
currentRole: string
spaceId: string
}
Features:
- Platform summary cards (PayPal, Zeffy, Venmo, Cash)
- Transactions table with filtering
- Log cash modal
- Link member modal
- Platform connection status
Comms Page
Files:
app/(app)/comms/page.tsx(Server)app/(app)/comms/comms-client.tsx(Client)
Client Props:
interface Props {
member: any
space: any
channels: any[]
}
Features:
- Channel sidebar (General, Areas, Projects)
- Real-time message subscription
- Message input with send
- User avatars and timestamps
- @mention and #channel highlighting
Real-time:
const subscription = supabase
.channel(`channel:${selectedChannel.id}`)
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'comms_messages',
filter: `channel_id=eq.${selectedChannel.id}`
}, payload => {
if (payload.eventType === 'INSERT') {
setMessages(prev => [...prev, payload.new])
}
})
.subscribe()
Contacts Page
Files:
app/(app)/contacts/page.tsx(Server)app/(app)/contacts/contacts-client.tsx(Client)
Features:
- Search functionality
- Grouped by type (Vendors, Landlord, Partners)
- Add/Edit contact modals
- Delete with confirmation
- Contact type badges
Ops Page
File: app/(app)/ops/page.tsx (Server Component)
Features:
- Tabbed view (Knowledge Base, Processes, Secrets, Area Leads)
- Search knowledge base
- Pinned/Critical entries section
- Area-based entries
- Area leads sidebar
- Secrets vault (admin/board only)
Settings Page
Files:
app/(app)/settings/page.tsx(Server)app/(app)/settings/settings-client.tsx(Client)
Client Props:
interface Props {
space: Space
isAdmin: boolean
integrations: Integration[]
currentRole: string
}
Features:
- Space settings tab (name, slug, approval settings)
- Roles & permissions info
- Integrations management (PayPal, Zeffy, Venmo, Stripe)
- Webhook secret management
Import Page
File: app/(app)/import/page.tsx (Server Component)
Features:
- Step indicator (Upload, Map, Preview, Import)
- File drop zone
- Column mapping preview
- Database connector form
Status: UI only, file processing not implemented.
3. Client Components
TaskClaimButton
File: components/task-claim-button.tsx
Props:
interface Props {
taskId: string
onClaim?: () => void
}
Usage:
<TaskClaimButton taskId={task.id} onClaim={() => refresh()} />
ThemeProvider
File: components/theme-provider.tsx
Purpose: Wraps app with next-themes provider.
Usage (in layout):
<ThemeProvider attribute="class" defaultTheme="dark">
{children}
</ThemeProvider>
4. UI Components
All shadcn/ui components are available in components/ui/. Key components:
Layout Components
Card,CardHeader,CardTitle,CardContent,CardFooterDialog,DialogTrigger,DialogContent,DialogHeaderSheet,SheetTrigger,SheetContentTabs,TabsList,TabsTrigger,TabsContentTable,TableHeader,TableRow,TableCell
Form Components
Button- Primary actionsInput- Text inputTextarea- Multi-line inputSelect,SelectTrigger,SelectContent,SelectItemCheckbox- Boolean inputSwitch- Toggle inputLabel- Form labels
Feedback Components
Badge- Status indicatorsAlert,AlertTitle,AlertDescriptionSpinner- Loading indicatorSkeleton- Loading placeholderToast,Toaster- Notifications
Navigation Components
Breadcrumb,BreadcrumbItem,BreadcrumbLinkDropdownMenu,DropdownMenuTrigger,DropdownMenuContentNavigationMenu
5. Component Patterns
Server/Client Split
// page.tsx (Server Component)
export default async function FeaturePage() {
const supabase = await createClient()
const { data } = await supabase.from('table').select('*')
return <FeatureClient data={data ?? []} />
}
// feature-client.tsx (Client Component)
'use client'
export function FeatureClient({ data }: { data: Item[] }) {
const [items, setItems] = useState(data)
// Interactive UI
}
Modal Pattern
const [showModal, setShowModal] = useState(false)
const [form, setForm] = useState({ field: '' })
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setLoading(true)
setError('')
const result = await serverAction(form)
if (result.error) {
setError(result.error)
setLoading(false)
return
}
// Update local state
setShowModal(false)
setForm({ field: '' })
setLoading(false)
}
return (
<>
<button onClick={() => setShowModal(true)}>Open</button>
{showModal && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div className="bg-card border border-border rounded-lg">
<form onSubmit={handleSubmit}>
{/* Form fields */}
{error && <p className="text-red-500">{error}</p>}
<button disabled={loading}>
{loading ? 'Saving...' : 'Save'}
</button>
</form>
</div>
</div>
)}
</>
)
Optimistic Update Pattern
async function handleAction(id: string) {
// Optimistically update UI
setItems(prev => prev.map(item =>
item.id === id ? { ...item, status: 'updated' } : item
))
// Call server
const result = await serverAction(id)
if (result.error) {
// Revert on error
setItems(prev => prev.map(item =>
item.id === id ? { ...item, status: 'original' } : item
))
}
}
Tab Pattern
const [activeTab, setActiveTab] = useState<'tab1' | 'tab2'>('tab1')
const tabData = {
tab1: items.filter(i => i.type === 'type1'),
tab2: items.filter(i => i.type === 'type2'),
}
return (
<>
<div className="flex gap-4 border-b">
{(['tab1', 'tab2'] as const).map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={activeTab === tab ? 'border-b-2 border-primary' : ''}
>
{tab} ({tabData[tab].length})
</button>
))}
</div>
<div>
{tabData[activeTab].map(item => (
<div key={item.id}>{/* Render item */}</div>
))}
</div>
</>
)
Filter Pattern
const [search, setSearch] = useState('')
const [filter, setFilter] = useState('')
const filtered = items.filter(item => {
const matchesSearch = !search ||
item.name.toLowerCase().includes(search.toLowerCase())
const matchesFilter = !filter || item.type === filter
return matchesSearch && matchesFilter
})
return (
<>
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search..."
/>
<select value={filter} onChange={e => setFilter(e.target.value)}>
<option value="">All</option>
<option value="type1">Type 1</option>
</select>
{filtered.map(item => (
<div key={item.id}>{/* Render */}</div>
))}
</>
)
6. Styling Patterns
Page Header
<div className="bg-sidebar px-6 py-3 flex items-center justify-between">
<h1 className="text-white font-sans text-lg font-semibold">Page Title</h1>
<button className="flex items-center gap-1.5 bg-primary text-white text-xs font-sans px-3 py-1.5 rounded">
<PlusIcon className="w-3.5 h-3.5" />
Action
</button>
</div>
Tab Bar
<div className="bg-card border-b border-border px-6 flex gap-6">
{tabs.map(tab => (
<button
key={tab}
className={`font-sans text-sm py-3 border-b-2 transition ${
activeTab === tab
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground'
}`}
>
{tab}
</button>
))}
</div>
Card List
<div className="bg-card rounded border border-border divide-y divide-border">
{items.map(item => (
<div key={item.id} className="flex items-center gap-3 px-4 py-3">
{/* Item content */}
</div>
))}
</div>
Status Badge
<span className={`font-mono text-[10px] px-2 py-0.5 rounded ${
status === 'active' ? 'text-primary bg-primary/10' :
status === 'warning' ? 'text-orange-600 bg-orange-50' :
'text-muted-foreground bg-muted'
}`}>
{status.toUpperCase()}
</span>
Monospace Label
<p className="font-mono text-[10px] tracking-widest text-muted-foreground uppercase">
SECTION LABEL
</p>