'use client' import { useState } from 'react' import { 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' import type { Tables } from '@/types/database' import { PageTitle } from '@/components/ui/page-title' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { StripeBillingPanel } from '@/components/settings/stripe-billing-panel' import { DuesPaymentMethodsPanel } from '@/components/settings/dues-payment-methods-panel' type Space = Tables<'spaces'> type Integration = Tables<'integrations'> // Roles, tiers, areas, invites, and onboarding moved to the dedicated // /customize hub. This page now owns space identity, visibility, // integrations, and webhooks only. interface Props { space: Space isAdmin: boolean integrations: Integration[] currentRole: string } 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/', }, // Stripe is intentionally NOT listed here. Stripe recurring dues is // configured in the dedicated Dues tab (saveStripeSettings: mode, tier->price // map, grace days, vaulted keys). The generic saveIntegration path OVERWRITES // integrations.config with a different shape, which would clobber the dues // config (drop prices/mode/grace_days). One Stripe config UI, in Dues. ] export default function SettingsClient({ space, isAdmin, integrations, currentRole }: Props) { const [activeTab, setActiveTab] = useState<'space' | 'integrations' | 'dues' | 'webhooks'>('space') 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 as Record | undefined)?.[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', 'integrations', 'dues', '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)