import { useEffect, useMemo, useRef, useState } from 'react' import { Accordion, Button, ButtonGroup, Col, Container, Form, Modal, Row, Tab, Tabs } from 'react-bootstrap' import DataTable, { createTheme } from 'react-data-table-component' import { useTranslation } from 'react-i18next' import { useDropzone } from 'react-dropzone' import { createForum, deleteForum, fetchSettings, listAllForums, listUsers, reorderForums, saveSetting, saveSettings, uploadFavicon, uploadLogo, updateForum, } from '../api/client' export default function Acp({ isAdmin }) { const { t } = useTranslation() const [forums, setForums] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [selectedId, setSelectedId] = useState(null) const [draggingId, setDraggingId] = useState(null) const [overId, setOverId] = useState(null) const pendingOrder = useRef(null) const [createType, setCreateType] = useState(null) const [users, setUsers] = useState([]) const [usersLoading, setUsersLoading] = useState(false) const [usersError, setUsersError] = useState('') const [usersPage, setUsersPage] = useState(1) const [usersPerPage, setUsersPerPage] = useState(10) const [generalSaving, setGeneralSaving] = useState(false) const [generalUploading, setGeneralUploading] = useState(false) const [generalError, setGeneralError] = useState('') const [generalSettings, setGeneralSettings] = useState({ forum_name: '', default_theme: 'auto', accent_color_dark: '', accent_color_light: '', logo_dark: '', logo_light: '', show_header_name: 'true', favicon_ico: '', favicon_16: '', favicon_32: '', favicon_48: '', favicon_64: '', favicon_128: '', favicon_256: '', }) const settingsDetailMap = { forum_name: 'forumName', default_theme: 'defaultTheme', accent_color_dark: 'accentDark', accent_color_light: 'accentLight', logo_dark: 'logoDark', logo_light: 'logoLight', show_header_name: 'showHeaderName', favicon_ico: 'faviconIco', favicon_16: 'favicon16', favicon_32: 'favicon32', favicon_48: 'favicon48', favicon_64: 'favicon64', favicon_128: 'favicon128', favicon_256: 'favicon256', } const [themeMode, setThemeMode] = useState( document.documentElement.getAttribute('data-bs-theme') || 'light' ) useEffect(() => { createTheme('speedbb-dark', { text: { primary: '#e6e8eb', secondary: '#9aa4b2', }, background: { default: 'transparent', }, context: { background: '#1a1f29', text: '#ffffff', }, divider: { default: '#2a2f3a', }, action: { button: 'rgba(230, 232, 235, 0.12)', hover: 'rgba(230, 232, 235, 0.08)', disabled: 'rgba(230, 232, 235, 0.35)', }, }) createTheme('speedbb-light', { text: { primary: '#0e121b', secondary: '#5b6678', }, background: { default: '#ffffff', }, context: { background: '#f7f2ea', text: '#0e121b', }, divider: { default: '#e0d7c7', }, action: { button: 'rgba(14, 18, 27, 0.12)', hover: 'rgba(14, 18, 27, 0.06)', disabled: 'rgba(14, 18, 27, 0.35)', }, }) }, []) useEffect(() => { if (!isAdmin) return let active = true const loadSettings = async () => { try { const allSettings = await fetchSettings() const settingsMap = new Map(allSettings.map((setting) => [setting.key, setting.value])) if (!active) return const next = { forum_name: settingsMap.get('forum_name') || '', default_theme: settingsMap.get('default_theme') || 'auto', accent_color_dark: settingsMap.get('accent_color_dark') || '', accent_color_light: settingsMap.get('accent_color_light') || '', logo_dark: settingsMap.get('logo_dark') || '', logo_light: settingsMap.get('logo_light') || '', show_header_name: settingsMap.get('show_header_name') || 'true', favicon_ico: settingsMap.get('favicon_ico') || '', favicon_16: settingsMap.get('favicon_16') || '', favicon_32: settingsMap.get('favicon_32') || '', favicon_48: settingsMap.get('favicon_48') || '', favicon_64: settingsMap.get('favicon_64') || '', favicon_128: settingsMap.get('favicon_128') || '', favicon_256: settingsMap.get('favicon_256') || '', } setGeneralSettings(next) } catch (err) { if (active) setGeneralError(err.message) } } loadSettings() return () => { active = false } }, [isAdmin]) const handleGeneralSave = async (event) => { event.preventDefault() setGeneralSaving(true) setGeneralError('') try { await saveSettings( Object.entries(generalSettings).map(([key, value]) => ({ key, value: typeof value === 'string' ? value.trim() : String(value ?? ''), })) ) const detail = Object.entries(generalSettings).reduce((acc, [key, value]) => { const mappedKey = settingsDetailMap[key] if (!mappedKey) return acc if (key === 'show_header_name') { acc[mappedKey] = value !== 'false' return acc } acc[mappedKey] = typeof value === 'string' ? value.trim() : String(value ?? '') return acc }, {}) window.dispatchEvent(new CustomEvent('speedbb-settings-updated', { detail })) } catch (err) { setGeneralError(err.message) } finally { setGeneralSaving(false) } } const handleDefaultThemeChange = async (value) => { const previous = generalSettings.default_theme setGeneralSettings((prev) => ({ ...prev, default_theme: value })) setGeneralError('') try { await saveSetting('default_theme', value) window.dispatchEvent( new CustomEvent('speedbb-settings-updated', { detail: { defaultTheme: value }, }) ) } catch (err) { setGeneralSettings((prev) => ({ ...prev, default_theme: previous })) setGeneralError(err.message) } } const handleLogoUpload = async (file, settingKey) => { if (!file) return setGeneralUploading(true) setGeneralError('') try { const result = await uploadLogo(file) const url = result?.url || '' setGeneralSettings((prev) => ({ ...prev, [settingKey]: url })) if (url) { await saveSetting(settingKey, url) const mappedKey = settingsDetailMap[settingKey] if (mappedKey) { window.dispatchEvent( new CustomEvent('speedbb-settings-updated', { detail: { [mappedKey]: url }, }) ) } } } catch (err) { setGeneralError(err.message) } finally { setGeneralUploading(false) } } const handleFaviconUpload = async (file, settingKey) => { if (!file) return setGeneralUploading(true) setGeneralError('') try { const result = await uploadFavicon(file) const url = result?.url || '' setGeneralSettings((prev) => ({ ...prev, [settingKey]: url })) if (url) { await saveSetting(settingKey, url) const mappedKey = settingsDetailMap[settingKey] if (mappedKey) { window.dispatchEvent( new CustomEvent('speedbb-settings-updated', { detail: { [mappedKey]: url }, }) ) } } } catch (err) { setGeneralError(err.message) } finally { setGeneralUploading(false) } } const faviconIcoDropzone = useDropzone({ accept: { 'image/png': ['.png'], 'image/x-icon': ['.ico'], }, maxFiles: 1, onDrop: (files) => handleFaviconUpload(files[0], 'favicon_ico'), }) const favicon16Dropzone = useDropzone({ accept: { 'image/png': ['.png'], 'image/x-icon': ['.ico'], }, maxFiles: 1, onDrop: (files) => handleFaviconUpload(files[0], 'favicon_16'), }) const favicon32Dropzone = useDropzone({ accept: { 'image/png': ['.png'], 'image/x-icon': ['.ico'], }, maxFiles: 1, onDrop: (files) => handleFaviconUpload(files[0], 'favicon_32'), }) const favicon48Dropzone = useDropzone({ accept: { 'image/png': ['.png'], 'image/x-icon': ['.ico'], }, maxFiles: 1, onDrop: (files) => handleFaviconUpload(files[0], 'favicon_48'), }) const favicon64Dropzone = useDropzone({ accept: { 'image/png': ['.png'], 'image/x-icon': ['.ico'], }, maxFiles: 1, onDrop: (files) => handleFaviconUpload(files[0], 'favicon_64'), }) const favicon128Dropzone = useDropzone({ accept: { 'image/png': ['.png'], 'image/x-icon': ['.ico'], }, maxFiles: 1, onDrop: (files) => handleFaviconUpload(files[0], 'favicon_128'), }) const favicon256Dropzone = useDropzone({ accept: { 'image/png': ['.png'], 'image/x-icon': ['.ico'], }, maxFiles: 1, onDrop: (files) => handleFaviconUpload(files[0], 'favicon_256'), }) const darkLogoDropzone = useDropzone({ accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'], }, maxFiles: 1, onDrop: (files) => handleLogoUpload(files[0], 'logo_dark'), }) const lightLogoDropzone = useDropzone({ accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'], }, maxFiles: 1, onDrop: (files) => handleLogoUpload(files[0], 'logo_light'), }) useEffect(() => { const observer = new MutationObserver(() => { setThemeMode(document.documentElement.getAttribute('data-bs-theme') || 'light') }) observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-bs-theme'], }) return () => observer.disconnect() }, []) const userColumns = useMemo( () => [ { name: t('user.name'), selector: (row) => row.name, sortable: true, }, { name: t('user.email'), selector: (row) => row.email, sortable: true, }, { name: '', width: '180px', right: true, cell: (row) => (