'use client' import { useState } from 'react' import { X, Check, Copy, Eye, EyeOff, RotateCcw } from 'lucide-react' import { updateSpaceSettings, saveIntegration, disconnectIntegration, rotateWebhookSecret } from '@/lib/actions' import type { Tables } from '@/types/database' type Space = Tables<'spaces'> type Integration = Tables<'integrations'> interface Props { space: Space isAdmin: boolean integrations: Integration[] currentRole: 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 }: Props) { const [activeTab, setActiveTab] = useState<'space' | 'roles' | 'integrations' | 'webhooks'>('space') const [saving, setSaving] = useState(false) const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) // Space form 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, }) // 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) 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 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) setMessage({ type: 'success', text: 'Webhook secret rotated!' }) } setSaving(false) } function copyToClipboard(text: string) { navigator.clipboard.writeText(text) setCopiedWebhook(true) setTimeout(() => setCopiedWebhook(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', '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)

Invite code

{space.invite_code || 'No code set'} {space.invite_code && ( )}
{isAdmin && (
)}
)} {/* Users & Roles Tab */} {activeTab === 'roles' && (

Roles & Permissions

{ROLES_INFO.map(role => (

{role.name}

{role.description}

{role.permissions.length > 0 && (
{role.permissions.map(p => ( {p} ))}
)}
))}
)} {/* Integrations Tab */} {activeTab === 'integrations' && (

Integrations

{INTEGRATIONS_CONFIG.map(config => { const connected = getIntegrationStatus(config.platform) return (
{config.icon}

{config.name}

{config.description}

{connected ? ( <> Connected {isAdmin && ( )} ) : isAdmin ? ( ) : ( Not connected )}
) })}
)} {/* Webhooks Tab */} {activeTab === 'webhooks' && (

Webhook Endpoint

POST members to this endpoint for external integration or automation.

https://api.hackerspace.sh/{space.slug}/members
View Docs {isAdmin && ( )}
)}
{/* Right sidebar - Roles panel (visible on roles tab) */} {activeTab === 'roles' && (

Role Assignment

Member roles can be changed from the Members page. Only admins can change roles.

)}
{/* Integration Config Modal */} {editingIntegration && (
{editingIntegration.icon}

{editingIntegration.name}

{editingIntegration.fields.map(field => (
{field.type === 'select' ? ( ) : (
setIntegrationForm(f => ({ ...f, [field.key]: e.target.value }))} placeholder={field.placeholder} className="w-full bg-background border border-border rounded px-3 py-2 pr-10 font-mono text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary" /> {field.type === 'password' && ( )}
)}
))}
{getIntegrationStatus(editingIntegration.platform) && ( )}
)}
) }