'use client'; import { useEffect, useState } from 'react'; import Link from 'next/link'; import AutoTextarea from '@/components/AutoTextarea'; import { useToast } from '@/lib/contexts/ToastContext'; import { useAccentColor } from '@/lib/contexts/AccentColorContext'; export default function AdminPage() { const { showToast } = useToast(); const { refreshAccentColor } = useAccentColor(); const [isAdmin, setIsAdmin] = useState(null); const [loading, setLoading] = useState(false); const [nodeSettings, setNodeSettings] = useState({ name: '', description: '', longDescription: '', rules: '', bannerUrl: '', logoUrl: '', faviconUrl: '', accentColor: '#FFFFFF', isNsfw: false, turnstileSiteKey: '', turnstileSecretKey: '', }); const [savingSettings, setSavingSettings] = useState(false); const [isUploadingBanner, setIsUploadingBanner] = useState(false); const [bannerUploadError, setBannerUploadError] = useState(null); const [isUploadingLogo, setIsUploadingLogo] = useState(false); const [logoUploadError, setLogoUploadError] = useState(null); const [isUploadingFavicon, setIsUploadingFavicon] = useState(false); const [faviconUploadError, setFaviconUploadError] = useState(null); useEffect(() => { fetch('/api/admin/me') .then((res) => res.json()) .then((data) => setIsAdmin(!!data.isAdmin)) .catch(() => setIsAdmin(false)); }, []); const loadNodeSettings = async () => { setLoading(true); try { const res = await fetch('/api/node'); const data = await res.json(); setNodeSettings({ name: data.name || '', description: data.description || '', longDescription: data.longDescription || '', rules: data.rules || '', bannerUrl: data.bannerUrl || '', logoUrl: data.logoUrl || '', faviconUrl: data.faviconUrl || '', accentColor: data.accentColor || '#FFFFFF', isNsfw: data.isNsfw || false, turnstileSiteKey: data.turnstileSiteKey || '', turnstileSecretKey: data.turnstileSecretKey || '', }); } catch { // error } finally { setLoading(false); } }; useEffect(() => { if (isAdmin) { loadNodeSettings(); } }, [isAdmin]); const handleSaveSettings = async (override?: typeof nodeSettings) => { const payload = override ?? nodeSettings; setSavingSettings(true); try { const res = await fetch('/api/admin/node', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (res.ok) { showToast('Settings saved!', 'success'); refreshAccentColor(); } else { showToast('Failed to save settings.', 'error'); } } catch { showToast('Failed to save settings.', 'error'); } finally { setSavingSettings(false); } }; const handleBannerUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ''; if (!file) return; setBannerUploadError(null); setIsUploadingBanner(true); try { const formData = new FormData(); formData.append('file', file); const res = await fetch('/api/media/upload', { method: 'POST', body: formData, }); const data = await res.json(); if (!res.ok || !data.url) { throw new Error(data.error || 'Upload failed'); } const nextSettings = { ...nodeSettings, bannerUrl: data.media?.url || data.url, }; setNodeSettings(nextSettings); await handleSaveSettings(nextSettings); } catch (error) { console.error('Banner upload failed', error); setBannerUploadError('Upload failed. Please try again.'); } finally { setIsUploadingBanner(false); } }; const handleLogoUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ''; if (!file) return; setLogoUploadError(null); setIsUploadingLogo(true); try { const formData = new FormData(); formData.append('file', file); formData.append('type', 'logo'); const res = await fetch('/api/admin/node/upload', { method: 'POST', body: formData, }); const data = await res.json(); if (!res.ok || !data.url) { throw new Error(data.error || 'Upload failed'); } const nextSettings = { ...nodeSettings, logoUrl: data.url, }; setNodeSettings(nextSettings); await handleSaveSettings(nextSettings); } catch (error) { console.error('Logo upload failed', error); setLogoUploadError(error instanceof Error ? error.message : 'Upload failed. Please try again.'); } finally { setIsUploadingLogo(false); } }; const handleFaviconUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ''; if (!file) return; setFaviconUploadError(null); setIsUploadingFavicon(true); try { const formData = new FormData(); formData.append('file', file); formData.append('type', 'favicon'); const res = await fetch('/api/admin/node/upload', { method: 'POST', body: formData, }); const data = await res.json(); if (!res.ok || !data.url) { throw new Error(data.error || 'Upload failed'); } const nextSettings = { ...nodeSettings, faviconUrl: data.url, }; setNodeSettings(nextSettings); await handleSaveSettings(nextSettings); } catch (error) { console.error('Favicon upload failed', error); setFaviconUploadError(error instanceof Error ? error.message : 'Upload failed. Please try again.'); } finally { setIsUploadingFavicon(false); } }; if (isAdmin === null) { return (
Checking permissions...
); } if (!isAdmin) { return (

Admin Settings

You do not have access to this page.

Back to home
); } return ( <>

Admin Settings

{loading ? (
Loading settings...
) : (
setNodeSettings({ ...nodeSettings, name: e.target.value })} placeholder="My Synapsis Node" />

Replaces the default logo in the sidebar. Max width: 200px.

{nodeSettings.logoUrl && ( )} {logoUploadError && ( {logoUploadError} )}
{nodeSettings.logoUrl && (
Custom logo
)}

The icon shown in browser tabs. Recommended: 32x32 or 64x64 PNG.

{nodeSettings.faviconUrl && ( )} {faviconUploadError && ( {faviconUploadError} )}
{nodeSettings.faviconUrl && (
Custom favicon
)}
setNodeSettings({ ...nodeSettings, description: e.target.value })} placeholder="A brief tagline for your node." rows={2} />
setNodeSettings({ ...nodeSettings, accentColor: e.target.value })} style={{ width: '44px', height: '36px', padding: 0, border: '1px solid var(--border)', background: 'transparent', borderRadius: '8px' }} /> setNodeSettings({ ...nodeSettings, accentColor: e.target.value })} placeholder="#FFFFFF" />
{bannerUploadError && ( {bannerUploadError} )}
{nodeSettings.bannerUrl && (
)}
setNodeSettings({ ...nodeSettings, longDescription: e.target.value })} placeholder="Detailed information about your node/community." rows={5} />
setNodeSettings({ ...nodeSettings, rules: e.target.value })} placeholder="Community rules and guidelines." rows={5} />

{nodeSettings.isNsfw ? 'This node is marked as NSFW. All content will be hidden from users who haven\'t enabled NSFW viewing.' : 'Enable this if your node primarily hosts adult or sensitive content. All posts from this node will be treated as NSFW across the swarm.'}

Add Cloudflare Turnstile to protect registration and login from bots. Get your keys from the{' '} Cloudflare Dashboard .

{nodeSettings.turnstileSiteKey && nodeSettings.turnstileSecretKey && (
✓ Turnstile is enabled and will be shown on login/registration
)}
setNodeSettings({ ...nodeSettings, turnstileSiteKey: e.target.value })} placeholder="0x4AAAAAAA..." style={{ fontFamily: 'monospace', fontSize: '13px' }} />

Public key shown to users

setNodeSettings({ ...nodeSettings, turnstileSecretKey: e.target.value })} placeholder={nodeSettings.turnstileSecretKey ? '••••••••••••••••' : '0x4AAAAAAA...'} style={{ fontFamily: 'monospace', fontSize: '13px' }} />

{nodeSettings.turnstileSecretKey ? 'Secret key is configured (hidden for security)' : 'Secret key for server-side verification'}

)} ); }