import { lazy, Suspense, useEffect, useState } from 'react' import { Loading, ErrorState } from '../../../components/PageState.jsx' import { api } from '../../../api/client.js' import { useSite } from '../../../contexts/SiteContext.jsx' import EmailDelivery from './EmailDelivery.jsx' // Lazy-loaded so the heavy rich-text editor stays code-split (matches PostEditor). const RichTextEditor = lazy(() => import('../../../components/RichTextEditor.jsx')) // Editable settings shown on this screen (key -> label + control type). const FIELDS = [ { key: 'site_title', label: 'Site title' }, { key: 'homepage_teaser', label: 'Homepage teaser', rich: true, help: 'Rich text shown under the hero heading on the portal (when no custom hero layout is published).', }, { key: 'maintenance_message', label: 'Maintenance message', long: true }, { key: 'status_message', label: 'Status message' }, { key: 'contact_email', label: 'Contact email', help: 'Where contact-form messages (and test emails) are delivered. Also the address shown when email delivery is unconfigured and the form falls back to a mailto: link.', }, { key: 'player_registration', label: 'Player registration', help: 'Who can create a player account, and how. Off by default.', options: [ { value: 'disabled', label: 'Disabled — no self-registration' }, { value: 'password', label: 'Password — username + password sign-up' }, { value: 'sso', label: 'SSO — sign up with a linked provider' }, { value: 'both', label: 'Both — password and SSO' }, ], fallback: 'disabled', }, { key: 'game_account_signup', label: 'Game-account creation', help: 'Whether players can create a GAME account (for the game client) from the site. The game server’s own SignupMode (Bridge.cfg) must agree: website/hybrid accept site-created accounts, game refuses them. When enabled, a “Create a game account” form appears in the player portal.', options: [ { value: 'disabled', label: 'Disabled — link an existing account only' }, { value: 'website', label: 'Website — the site creates game accounts' }, { value: 'hybrid', label: 'Hybrid — site or in-game (recommended)' }, { value: 'game', label: 'Game only — created in the game client, not the site' }, ], fallback: 'disabled', }, ] export default function SettingsAdmin() { const { refresh: refreshSite } = useSite() const [values, setValues] = useState(null) const [initial, setInitial] = useState({}) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [busy, setBusy] = useState(false) const [saved, setSaved] = useState(false) useEffect(() => { let active = true api.admin .getSettings() .then((all) => { if (!active) return const v = {} FIELDS.forEach((f) => (v[f.key] = all[f.key] ?? f.fallback ?? '')) setValues(v) setInitial(v) }) .catch(() => active && setError('Could not load settings.')) .finally(() => active && setLoading(false)) return () => { active = false } }, []) if (loading) return if (error) return // setRaw takes the next value directly (rich editor onChange), set adapts a // DOM change event onto it. const setRaw = (k) => (val) => { setValues((v) => ({ ...v, [k]: val })) setSaved(false) } const set = (k) => (e) => setRaw(k)(e.target.value) async function save() { setBusy(true) setError('') try { await api.admin.updateSettings(values) setInitial(values) setSaved(true) await refreshSite() } catch (err) { setError(err.message || 'Could not save settings.') } finally { setBusy(false) } } return (
{FIELDS.map((f) => { // A rich field can't live inside a