Files
hackerspace.sh/app/(app)/profile/chip-input.tsx
T
Moheeb Zara ea8d7a5f91 feat: governance kernel, deployment hardening, local-dev stack
Adds proposals + proposal_votes + policies + incidents + incident_updates
tables with full RLS, async voting flow, dismissal-appeal pathway,
versioned policies with provenance, and an anonymous-incident token mode.
Adds per-space configurable areas, financial visibility settings, member
self-profile + COI disclosure, and a mission statement that renders in
the app footer.

Reorganises lib/actions.ts into per-domain files (tasks, projects,
members, payments, contacts, knowledge-base, secrets, area-leads,
settings, imports, proposals, incidents, policies, areas) plus a barrel.
Adds lib/permissions.ts and lib/auth-helpers.ts so action bodies stop
duplicating the user-to-member-to-role check. Wires every action that has
a Zod schema; introduces a flexibleDateTime preprocessor so HTML date
pickers no longer hit "Invalid datetime".

Hardens RLS: drops NOT NULL on space_members.user_id for offline members,
adds a privilege-escalation trigger that blocks self-edits to role / tier
/ status / approved / has_card_access / space_id, closes four
cross-tenant holes in the governance kernel, and adds a partial unique
index so two admins cannot concurrently activate two drafts of the same
policy slug.

Ships every deployment artifact: Dockerfile, docker-compose,
.do/app.yaml, .env.example, /api/health, plus a managed-Supabase guide
(docs/DEPLOYMENT.md), an end-to-end self-hosted Droplet guide
(docs/DEPLOY_DO_SELFHOSTED.md), and a Supabase-CLI local-dev quickstart
(docs/LOCAL_DEV.md). Replaces middleware.ts with proxy.ts for Next.js 16.
Adds scripts/schema.sql as the canonical idempotent full deploy plus
fourteen numbered incremental migrations.

Adds scripts/seed.sql so a fresh local DB renders something instead of
empty pages: a demo space, five members across all roles, a v1 code of
conduct, three proposals in different states with seven votes already
cast, an open incident, a project, three tasks, a KB entry, and matching
activity-log rows.

Replaces v0-generated test scaffolding with 266 real tests across
__tests__/{actions,auth-helpers,components,governance,permissions,
security,utils,validations}.test.ts. Adds react-markdown + remark-gfm
for proposal / incident / policy body rendering. Deletes four dead v0
fix-* scripts, the unused components/app-shell.tsx, and the duplicate
app/auth/login/page.tsx.

Routes (31 total): /api/health, /api/paypal/sync, /auth/callback, /,
/login, /signup, /signup/confirm, /dashboard, /tasks, /projects,
/members, /payments, /financials, /contacts, /ops, /ops/[id], /ops/new,
/proposals, /proposals/[id], /proposals/new, /incidents, /incidents/[id],
/incidents/new, /policies, /policies/[slug], /policies/new, /profile,
/recruitment, /settings, /comms, /import.

Full pass-by-pass history is in docs/HANDOFF.md; the roadmap is in
docs/GOVERNANCE_FEATURES.md.
2026-05-14 04:52:11 -07:00

101 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, type KeyboardEvent } from 'react'
type Props = {
values: string[]
onChange: (next: string[]) => void
placeholder?: string
maxLength?: number
maxItems?: number
suggestions?: string[]
}
/**
* Minimal chip / token input. Press Enter or comma to commit the current
* value. Backspace on an empty input removes the last chip. Optional
* suggestion list shows un-used suggestions as click-to-add chips below.
*/
export function ChipInput({
values,
onChange,
placeholder,
maxLength = 80,
maxItems = 50,
suggestions,
}: Props) {
const [draft, setDraft] = useState('')
function commit(raw: string) {
const v = raw.trim()
if (!v) return
if (values.includes(v)) return
if (values.length >= maxItems) return
onChange([...values, v.slice(0, maxLength)])
setDraft('')
}
function remove(idx: number) {
onChange(values.filter((_, i) => i !== idx))
}
function handleKeyDown(e: KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault()
commit(draft)
} else if (e.key === 'Backspace' && draft === '' && values.length > 0) {
e.preventDefault()
onChange(values.slice(0, -1))
}
}
const remainingSuggestions = (suggestions ?? []).filter(s => !values.includes(s))
return (
<div>
<div className="flex flex-wrap items-center gap-1.5 bg-background border border-border rounded px-2 py-1.5 focus-within:border-primary">
{values.map((v, i) => (
<span
key={`${v}-${i}`}
className="inline-flex items-center gap-1 bg-muted rounded px-2 py-0.5 font-mono text-xs"
>
{v}
<button
type="button"
onClick={() => remove(i)}
className="text-muted-foreground hover:text-foreground"
aria-label={`Remove ${v}`}
>
×
</button>
</span>
))}
<input
type="text"
value={draft}
onChange={e => setDraft(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={() => commit(draft)}
placeholder={values.length === 0 ? placeholder : ''}
maxLength={maxLength}
className="flex-1 min-w-[120px] bg-transparent text-foreground font-sans text-sm py-1 px-1 focus:outline-none"
/>
</div>
{remainingSuggestions.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{remainingSuggestions.map(s => (
<button
key={s}
type="button"
onClick={() => commit(s)}
className="font-mono text-[10px] tracking-widest border border-border text-muted-foreground hover:text-foreground hover:border-primary rounded px-2 py-0.5"
>
+ {s}
</button>
))}
</div>
)}
</div>
)
}