'use client'; import { useEffect, useState, useRef } from 'react'; import { Upload, Trash2, Power, PowerOff, Loader2, Palette, Save, Shield, Lock, LockOpen } from 'lucide-react'; import type { SettingsPolicy } from '@/lib/admin/types'; import { DEFAULT_POLICY, DEFAULT_THEME_POLICY } from '@/lib/admin/types'; import { apiFetch } from '@/lib/browser-navigation'; const BUILTIN_THEME_OPTIONS = [ { id: 'builtin-nord', name: 'Nord' }, { id: 'builtin-catppuccin', name: 'Catppuccin' }, { id: 'builtin-solarized', name: 'Solarized' }, ]; interface ThemeEntry { id: string; name: string; version: string; author: string; description: string; variants: string[]; enabled: boolean; forceEnabled?: boolean; installedAt: string; updatedAt: string; } export function ThemesTab() { const [themes, setThemes] = useState([]); const [loading, setLoading] = useState(true); const [uploading, setUploading] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const fileInputRef = useRef(null); const [policy, setPolicy] = useState({ ...DEFAULT_POLICY }); const [policyDirty, setPolicyDirty] = useState(false); const [savingPolicy, setSavingPolicy] = useState(false); useEffect(() => { fetchThemes(); fetchPolicy(); }, []); async function fetchPolicy() { try { const res = await apiFetch('/api/admin/policy'); if (res.ok) { const data = await res.json(); setPolicy({ ...data, themePolicy: { ...DEFAULT_THEME_POLICY, ...(data.themePolicy || {}) }, }); } } catch { /* ignore */ } } function toggleThemesEnabled() { setPolicy(prev => ({ ...prev, features: { ...prev.features, themesEnabled: !prev.features.themesEnabled }, })); setPolicyDirty(true); setMessage(null); } function toggleUserThemeUploads() { setPolicy(prev => ({ ...prev, features: { ...prev.features, userThemesEnabled: !prev.features.userThemesEnabled }, })); setPolicyDirty(true); setMessage(null); } function toggleBuiltinTheme(themeId: string) { setPolicy(prev => { const disabled = prev.themePolicy?.disabledBuiltinThemes || []; const isDisabled = disabled.includes(themeId); return { ...prev, themePolicy: { ...DEFAULT_THEME_POLICY, ...prev.themePolicy, disabledBuiltinThemes: isDisabled ? disabled.filter((id: string) => id !== themeId) : [...disabled, themeId], }, }; }); setPolicyDirty(true); setMessage(null); } function toggleAdminTheme(themeId: string) { setPolicy(prev => { const disabled = prev.themePolicy?.disabledThemes || []; const isDisabled = disabled.includes(themeId); return { ...prev, themePolicy: { ...DEFAULT_THEME_POLICY, ...prev.themePolicy, disabledThemes: isDisabled ? disabled.filter((id: string) => id !== themeId) : [...disabled, themeId], }, }; }); setPolicyDirty(true); setMessage(null); } function setDefaultTheme(themeId: string | null) { setPolicy(prev => ({ ...prev, themePolicy: { ...DEFAULT_THEME_POLICY, ...prev.themePolicy, defaultThemeId: themeId, }, })); setPolicyDirty(true); setMessage(null); } async function handleSavePolicy() { setSavingPolicy(true); setMessage(null); try { const res = await apiFetch('/api/admin/policy', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(policy), }); if (res.ok) { setMessage({ type: 'success', text: 'Theme policy saved. Users will see changes on next login.' }); setPolicyDirty(false); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || 'Failed to save policy' }); } } catch { setMessage({ type: 'error', text: 'Failed to save policy' }); } finally { setSavingPolicy(false); } } async function fetchThemes() { setLoading(true); try { const res = await apiFetch('/api/admin/themes'); if (res.ok) setThemes(await res.json()); } finally { setLoading(false); } } async function handleUpload(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setUploading(true); setMessage(null); const formData = new FormData(); formData.append('file', file); try { const res = await apiFetch('/api/admin/themes', { method: 'POST', body: formData, }); const data = await res.json(); if (res.ok) { const warnings = data.warnings?.length ? ` (${data.warnings.length} warning(s))` : ''; setMessage({ type: 'success', text: `Theme "${data.theme.name}" installed${warnings}` }); await fetchThemes(); } else { setMessage({ type: 'error', text: data.error || 'Upload failed' }); } } catch { setMessage({ type: 'error', text: 'Upload failed' }); } finally { setUploading(false); if (fileInputRef.current) fileInputRef.current.value = ''; } } async function toggleTheme(id: string, enabled: boolean) { setMessage(null); const res = await apiFetch('/api/admin/themes', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, enabled }), }); if (res.ok) { setThemes(prev => prev.map(t => t.id === id ? { ...t, enabled } : t)); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || 'Update failed' }); } } async function toggleForceEnabled(id: string, forceEnabled: boolean) { setMessage(null); const body: Record = { id, forceEnabled }; if (forceEnabled) body.enabled = true; const res = await apiFetch('/api/admin/themes', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (res.ok) { setThemes(prev => prev.map(t => t.id === id ? { ...t, forceEnabled, ...(forceEnabled ? { enabled: true } : {}) } : t)); setPolicy(prev => { const current = prev.forceEnabledThemes || []; return { ...prev, forceEnabledThemes: forceEnabled ? [...current.filter(tid => tid !== id), id] : current.filter(tid => tid !== id), }; }); setPolicyDirty(true); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || 'Update failed' }); } } async function forceEnableAll() { setMessage(null); const disabled = themes.filter(t => !t.enabled); if (disabled.length === 0) { setMessage({ type: 'success', text: 'All themes are already enabled' }); return; } let failed = 0; for (const t of disabled) { const res = await apiFetch('/api/admin/themes', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: t.id, enabled: true }), }); if (!res.ok) failed++; } if (failed === 0) { await fetchThemes(); setMessage({ type: 'success', text: `All ${disabled.length} theme(s) enabled` }); } else { await fetchThemes(); setMessage({ type: 'error', text: `${failed} theme(s) failed to enable` }); } } async function forceDisableAll() { setMessage(null); const enabled = themes.filter(t => t.enabled); if (enabled.length === 0) { setMessage({ type: 'success', text: 'All themes are already disabled' }); return; } let failed = 0; for (const t of enabled) { const res = await apiFetch('/api/admin/themes', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: t.id, enabled: false }), }); if (!res.ok) failed++; } if (failed === 0) { await fetchThemes(); setMessage({ type: 'success', text: `All ${enabled.length} theme(s) disabled` }); } else { await fetchThemes(); setMessage({ type: 'error', text: `${failed} theme(s) failed to disable` }); } } async function deleteTheme(id: string, name: string) { if (!confirm(`Remove theme "${name}"? This cannot be undone.`)) return; setMessage(null); const res = await apiFetch('/api/admin/themes', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }); if (res.ok) { setThemes(prev => prev.filter(t => t.id !== id)); setMessage({ type: 'success', text: `Theme "${name}" removed` }); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || 'Delete failed' }); } } if (loading) { return
Loading...
; } const themesEnabled = policy.features.themesEnabled ?? true; const userThemesEnabled = policy.features.userThemesEnabled ?? true; return (

Themes

Manage themes and theme policy for all users

{policyDirty && ( )}
{message && (
{message.text}
)}

Theme Policy

Control theme availability and defaults for users

Themes Enabled

Allow users to select and apply themes

User Theme Uploads

Allow users to upload their own theme files

{themes.length > 0 && (
Force Enable / Disable All

Bulk toggle all deployed themes at once

)}
Default Theme

Theme applied when users have not chosen one

Built-in Themes
{BUILTIN_THEME_OPTIONS.map(theme => { const disabled = (policy.themePolicy?.disabledBuiltinThemes || []).includes(theme.id); return (
{theme.name}
); })}
{themes.length > 0 && (
Admin-deployed Themes
{themes.map(theme => { const disabled = (policy.themePolicy?.disabledThemes || []).includes(theme.id); return (
{theme.name}
); })}
)}

Deployed Themes

Admin-uploaded themes available to all users

{themes.length === 0 ? (

No themes installed

Upload a theme ZIP file to get started

) : (
{themes.map(theme => (
{theme.name} v{theme.version} {theme.enabled ? 'Enabled' : 'Disabled'} {theme.forceEnabled && ( Forced )}
{theme.description && (

{theme.description}

)}
by {theme.author} · {theme.variants.join(', ')} · installed {new Date(theme.installedAt).toLocaleDateString()}
))}
)}
); }