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, fetchSetting, listAllForums, listUsers, reorderForums, saveSetting, 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({ forumName: '', defaultTheme: 'auto', darkAccent: '', lightAccent: '', darkLogo: '', lightLogo: '', showHeaderName: true, faviconIco: '', favicon16: '', favicon32: '', favicon48: '', favicon64: '', favicon128: '', 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 keys = [ 'forum_name', 'default_theme', 'accent_color_dark', 'accent_color_light', 'logo_dark', 'logo_light', 'show_header_name', 'favicon_ico', 'favicon_16', 'favicon_32', 'favicon_48', 'favicon_64', 'favicon_128', 'favicon_256', ] const results = await Promise.all(keys.map((key) => fetchSetting(key))) if (!active) return const next = { forumName: results[0]?.value || '', defaultTheme: results[1]?.value || 'auto', darkAccent: results[2]?.value || '', lightAccent: results[3]?.value || '', darkLogo: results[4]?.value || '', lightLogo: results[5]?.value || '', showHeaderName: results[6]?.value !== 'false', faviconIco: results[7]?.value || '', favicon16: results[8]?.value || '', favicon32: results[9]?.value || '', favicon48: results[10]?.value || '', favicon64: results[11]?.value || '', favicon128: results[12]?.value || '', favicon256: results[13]?.value || '', } 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 Promise.all([ saveSetting('forum_name', generalSettings.forumName.trim() || ''), saveSetting('default_theme', generalSettings.defaultTheme || 'auto'), saveSetting('accent_color_dark', generalSettings.darkAccent.trim() || ''), saveSetting('accent_color_light', generalSettings.lightAccent.trim() || ''), saveSetting('logo_dark', generalSettings.darkLogo.trim() || ''), saveSetting('logo_light', generalSettings.lightLogo.trim() || ''), saveSetting('show_header_name', generalSettings.showHeaderName ? 'true' : 'false'), saveSetting('favicon_ico', generalSettings.faviconIco.trim() || ''), saveSetting('favicon_16', generalSettings.favicon16.trim() || ''), saveSetting('favicon_32', generalSettings.favicon32.trim() || ''), saveSetting('favicon_48', generalSettings.favicon48.trim() || ''), saveSetting('favicon_64', generalSettings.favicon64.trim() || ''), saveSetting('favicon_128', generalSettings.favicon128.trim() || ''), saveSetting('favicon_256', generalSettings.favicon256.trim() || ''), ]) window.dispatchEvent( new CustomEvent('speedbb-settings-updated', { detail: { forumName: generalSettings.forumName.trim() || '', defaultTheme: generalSettings.defaultTheme || 'auto', accentDark: generalSettings.darkAccent.trim() || '', accentLight: generalSettings.lightAccent.trim() || '', logoDark: generalSettings.darkLogo.trim() || '', logoLight: generalSettings.lightLogo.trim() || '', showHeaderName: generalSettings.showHeaderName, faviconIco: generalSettings.faviconIco.trim() || '', favicon16: generalSettings.favicon16.trim() || '', favicon32: generalSettings.favicon32.trim() || '', favicon48: generalSettings.favicon48.trim() || '', favicon64: generalSettings.favicon64.trim() || '', favicon128: generalSettings.favicon128.trim() || '', favicon256: generalSettings.favicon256.trim() || '', }, }) ) } catch (err) { setGeneralError(err.message) } finally { setGeneralSaving(false) } } const handleDefaultThemeChange = async (value) => { const previous = generalSettings.defaultTheme setGeneralSettings((prev) => ({ ...prev, defaultTheme: value })) setGeneralError('') try { await saveSetting('default_theme', value) window.dispatchEvent( new CustomEvent('speedbb-settings-updated', { detail: { defaultTheme: value }, }) ) } catch (err) { setGeneralSettings((prev) => ({ ...prev, defaultTheme: previous })) setGeneralError(err.message) } } const handleLogoUpload = async (file, variantKey) => { if (!file) return setGeneralUploading(true) setGeneralError('') try { const result = await uploadLogo(file) const url = result?.url || '' const settingKey = variantKey === 'darkLogo' ? 'logo_dark' : 'logo_light' setGeneralSettings((prev) => ({ ...prev, [variantKey]: url })) if (url) { await saveSetting(settingKey, url) } } catch (err) { setGeneralError(err.message) } finally { setGeneralUploading(false) } } const handleFaviconUpload = async (file, settingKey, stateKey) => { if (!file) return setGeneralUploading(true) setGeneralError('') try { const result = await uploadFavicon(file) const url = result?.url || '' setGeneralSettings((prev) => ({ ...prev, [stateKey]: url })) if (url) { await saveSetting(settingKey, url) window.dispatchEvent( new CustomEvent('speedbb-settings-updated', { detail: { [stateKey]: 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', 'faviconIco'), }) const favicon16Dropzone = useDropzone({ accept: { 'image/png': ['.png'], 'image/x-icon': ['.ico'], }, maxFiles: 1, onDrop: (files) => handleFaviconUpload(files[0], 'favicon_16', 'favicon16'), }) const favicon32Dropzone = useDropzone({ accept: { 'image/png': ['.png'], 'image/x-icon': ['.ico'], }, maxFiles: 1, onDrop: (files) => handleFaviconUpload(files[0], 'favicon_32', 'favicon32'), }) const favicon48Dropzone = useDropzone({ accept: { 'image/png': ['.png'], 'image/x-icon': ['.ico'], }, maxFiles: 1, onDrop: (files) => handleFaviconUpload(files[0], 'favicon_48', 'favicon48'), }) const favicon64Dropzone = useDropzone({ accept: { 'image/png': ['.png'], 'image/x-icon': ['.ico'], }, maxFiles: 1, onDrop: (files) => handleFaviconUpload(files[0], 'favicon_64', 'favicon64'), }) const favicon128Dropzone = useDropzone({ accept: { 'image/png': ['.png'], 'image/x-icon': ['.ico'], }, maxFiles: 1, onDrop: (files) => handleFaviconUpload(files[0], 'favicon_128', 'favicon128'), }) const favicon256Dropzone = useDropzone({ accept: { 'image/png': ['.png'], 'image/x-icon': ['.ico'], }, maxFiles: 1, onDrop: (files) => handleFaviconUpload(files[0], 'favicon_256', 'favicon256'), }) const darkLogoDropzone = useDropzone({ accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'], }, maxFiles: 1, onDrop: (files) => handleLogoUpload(files[0], 'darkLogo'), }) const lightLogoDropzone = useDropzone({ accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'], }, maxFiles: 1, onDrop: (files) => handleLogoUpload(files[0], 'lightLogo'), }) 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) => (