'use client' import { useState } from 'react' import { X, Check, Copy, Eye, EyeOff, RotateCcw } from 'lucide-react' import { updateSpaceSettings, saveIntegration, disconnectIntegration, rotateWebhookSecret, updateSpaceVisibility, createArea, updateArea, deleteArea, createTier, updateTier, deleteTier, createInvite, updateInvite, deleteInvite, createOnboardingStep, updateOnboardingStep, deleteOnboardingStep } from '@/lib/actions' import { toast } from 'sonner' import { financialVisibilities, directoryVisibilities } from '@/lib/validations' type Area = { id: string code: string name: string icon: string | null sort_order: number is_archived: boolean } import type { Tables } from '@/types/database' type Space = Tables<'spaces'> type Integration = Tables<'integrations'> interface Props { space: Space isAdmin: boolean integrations: Integration[] currentRole: string areas: Area[] tiers: Tier[] invites: Invite[] onboardingSteps: OnboardingStep[] } export interface OnboardingStep { id: string step_key: string step_type: 'welcome' | 'code_of_conduct' | 'profile' | 'payment' | 'content' title: string body: string | null config: Record is_enabled: boolean is_required: boolean is_system: boolean sort_order: number } export interface Tier { id: string slug: string name: string description: string | null monthly_price_cents: number billing_cadence: string is_system: boolean is_archived: boolean sort_order: number } export interface Invite { id: string code: string label: string | null expires_at: string | null max_uses: number | null uses_count: number is_enabled: boolean created_at: string } const ROLES_INFO = [ { name: 'Admin', color: 'text-primary', description: 'Full access. Secrets, roles, integrations, all data.', permissions: ['ALL ACCESS'] }, { name: 'Board', color: 'text-primary', description: 'Members, payments, projects, board secrets.', permissions: ['MEMBERS', 'PAYMENTS', 'PROJECTS'] }, { name: 'Treasurer', color: 'text-primary', description: 'Payments, financial secrets, member payment status.', permissions: [] }, { name: 'Member', color: 'text-foreground', description: 'Tasks, chores, projects, public KB, comms.', permissions: [] }, { name: 'Associate', color: 'text-foreground', description: 'Read-only most areas. Can claim chores.', permissions: [] }, ] const INTEGRATIONS_CONFIG = [ { platform: 'paypal', name: 'PayPal', description: 'Sync transactions for member dues', icon: ( ), fields: [ { key: 'client_id', label: 'Client ID', type: 'text', placeholder: 'Sandbox or Live Client ID' }, { key: 'client_secret', label: 'Client Secret', type: 'password', placeholder: 'Client Secret from PayPal Developer' }, { key: 'mode', label: 'Mode', type: 'select', options: ['sandbox', 'live'] }, ], docs: 'https://developer.paypal.com/api/rest/', }, { platform: 'zeffy', name: 'Zeffy', description: 'Nonprofit payment and donation sync', icon: ( ), fields: [ { key: 'api_key', label: 'API Key', type: 'password', placeholder: 'Zeffy API Key' }, { key: 'organization_id', label: 'Organization ID', type: 'text', placeholder: 'Your Zeffy Org ID' }, ], docs: 'https://docs.zeffy.com/', }, { platform: 'venmo', name: 'Venmo', description: 'Pull Venmo business transactions', icon: ( ), fields: [ { key: 'access_token', label: 'Access Token', type: 'password', placeholder: 'Venmo API Access Token' }, ], docs: 'https://developer.venmo.com/', }, { platform: 'stripe', name: 'Stripe', description: 'Accept card payments and subscriptions', icon: ( ), fields: [ { key: 'publishable_key', label: 'Publishable Key', type: 'text', placeholder: 'pk_test_...' }, { key: 'secret_key', label: 'Secret Key', type: 'password', placeholder: 'sk_test_...' }, { key: 'webhook_secret', label: 'Webhook Secret', type: 'password', placeholder: 'whsec_...' }, ], docs: 'https://stripe.com/docs/api', }, ] export default function SettingsClient({ space, isAdmin, integrations, currentRole, areas: initialAreas, tiers: initialTiers, invites: initialInvites, onboardingSteps: initialOnboarding }: Props) { const [activeTab, setActiveTab] = useState<'space' | 'roles' | 'tiers' | 'invites' | 'onboarding' | 'integrations' | 'webhooks'>('space') const [onboardingSteps, setOnboardingSteps] = useState(initialOnboarding) const [areas, setAreas] = useState(initialAreas) const [tiers, setTiers] = useState(initialTiers) const [invites, setInvites] = useState(initialInvites) const [showNewTier, setShowNewTier] = useState(false) const [newTier, setNewTier] = useState({ slug: '', name: '', description: '', monthly_price_cents: 0, billing_cadence: 'monthly' as 'monthly' | 'quarterly' | 'annual' | 'one_time' | 'custom' }) const [showNewInvite, setShowNewInvite] = useState(false) const [newInvite, setNewInvite] = useState({ code: '', label: '', expires_at: '', max_uses: '' }) const [newArea, setNewArea] = useState({ code: '', name: '' }) const [saving, setSaving] = useState(false) const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) // Space form (basic + mission statement) const spaceExt = space as Space & { mission_statement?: string | null financial_visibility?: string member_directory_visibility?: string } const [spaceForm, setSpaceForm] = useState({ name: space.name || '', slug: space.slug || '', city: space.city || '', require_approval: space.require_approval ?? true, public_member_directory: space.public_member_directory ?? false, mission_statement: spaceExt.mission_statement || '', }) // Visibility form (governance) — separate server action, separate save. const [visibilityForm, setVisibilityForm] = useState({ financial_visibility: (spaceExt.financial_visibility as string | undefined) || 'board_visible', member_directory_visibility: (spaceExt.member_directory_visibility as string | undefined) || 'members_visible', }) // Integration config modal const [editingIntegration, setEditingIntegration] = useState(null) const [integrationForm, setIntegrationForm] = useState>({}) const [showSecrets, setShowSecrets] = useState>({}) // Webhook const [webhookSecret, setWebhookSecret] = useState(space.webhook_secret || '') const [copiedWebhook, setCopiedWebhook] = useState(false) const [copiedSecret, setCopiedSecret] = useState(false) const [showWebhookSecret, setShowWebhookSecret] = useState(false) async function handleSaveSpace(e: React.FormEvent) { e.preventDefault() if (!isAdmin) return setSaving(true) setMessage(null) const result = await updateSpaceSettings(spaceForm) if (result.error) { setMessage({ type: 'error', text: result.error }) } else { setMessage({ type: 'success', text: 'Settings saved!' }) } setSaving(false) } async function handleSaveVisibility(e: React.FormEvent) { e.preventDefault() if (!isAdmin) return setSaving(true) setMessage(null) const result = await updateSpaceVisibility(visibilityForm) if ('error' in result && result.error) { setMessage({ type: 'error', text: result.error }) } else { setMessage({ type: 'success', text: 'Visibility updated.' }) } setSaving(false) } async function handleSaveIntegration(e: React.FormEvent) { e.preventDefault() if (!editingIntegration || !isAdmin) return setSaving(true) setMessage(null) const result = await saveIntegration(editingIntegration.platform, integrationForm) if (result.error) { setMessage({ type: 'error', text: result.error }) } else { setMessage({ type: 'success', text: `${editingIntegration.name} connected!` }) setEditingIntegration(null) } setSaving(false) } async function handleDisconnect(platform: string) { if (!isAdmin) return setSaving(true) await disconnectIntegration(platform) setSaving(false) } async function handleRotateWebhook() { if (!isAdmin) return setSaving(true) const result = await rotateWebhookSecret() if (result.secret) { setWebhookSecret(result.secret) setShowWebhookSecret(true) setMessage({ type: 'success', text: 'New signing secret generated. Copy it now and store it securely.' }) } setSaving(false) } function copyToClipboard(text: string) { navigator.clipboard.writeText(text) setCopiedWebhook(true) setTimeout(() => setCopiedWebhook(false), 2000) } function copySecretToClipboard(text: string) { if (!text) return navigator.clipboard.writeText(text) setCopiedSecret(true) setTimeout(() => setCopiedSecret(false), 2000) } function openIntegrationModal(config: typeof INTEGRATIONS_CONFIG[0]) { const existing = integrations.find(i => i.platform === config.platform) const initialForm: Record = {} config.fields.forEach(f => { initialForm[f.key] = existing?.config?.[f.key] || '' }) setIntegrationForm(initialForm) setEditingIntegration(config) } const getIntegrationStatus = (platform: string) => integrations.find(i => i.platform === platform)?.is_connected ?? false return (
{/* Header */}

Settings & Admin

{/* Main content */}
{/* Tabs */}
{(['space', 'roles', 'tiers', 'invites', 'onboarding', 'integrations', 'webhooks'] as const).map(tab => ( ))}
{message && (
{message.text}
)} {/* Space Settings Tab */} {activeTab === 'space' && (

Space Settings

setSpaceForm(f => ({ ...f, name: e.target.value }))} disabled={!isAdmin} className="w-full bg-background border border-border rounded px-3 py-2 font-sans text-sm text-foreground disabled:opacity-50 focus:outline-none focus:border-primary" />
hackerspace.sh/ setSpaceForm(f => ({ ...f, slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '') }))} disabled={!isAdmin} className="flex-1 bg-background border border-border rounded px-3 py-2 font-mono text-sm text-foreground disabled:opacity-50 focus:outline-none focus:border-primary" />
setSpaceForm(f => ({ ...f, city: e.target.value }))} placeholder="Mesa, AZ" disabled={!isAdmin} className="w-full max-w-md bg-background border border-border rounded px-3 py-2 font-sans text-sm text-foreground placeholder:text-muted-foreground disabled:opacity-50 focus:outline-none focus:border-primary" />

Require approval to join

New members must be approved by an admin

Public member directory

Anyone can see member list (names only)