ACP: general settings, branding, and favicon uploads
This commit is contained in:
@@ -1,8 +1,20 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Button, ButtonGroup, Col, Container, Form, Modal, Row, Tab, Tabs } from 'react-bootstrap'
|
||||
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 { createForum, deleteForum, listAllForums, listUsers, reorderForums, updateForum } from '../api/client'
|
||||
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()
|
||||
@@ -19,6 +31,25 @@ export default function Acp({ isAdmin }) {
|
||||
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'
|
||||
)
|
||||
@@ -69,6 +100,242 @@ export default function Acp({ isAdmin }) {
|
||||
})
|
||||
}, [])
|
||||
|
||||
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')
|
||||
@@ -695,6 +962,296 @@ export default function Acp({ isAdmin }) {
|
||||
<Tabs defaultActiveKey="general" className="mb-3">
|
||||
<Tab eventKey="general" title={t('acp.general')}>
|
||||
<p className="bb-muted">{t('acp.general_hint')}</p>
|
||||
{generalError && <p className="text-danger">{generalError}</p>}
|
||||
<Form onSubmit={handleGeneralSave} className="bb-acp-general">
|
||||
<Row className="g-3">
|
||||
<Col lg={6}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.forum_name')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={generalSettings.forumName}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({ ...prev, forumName: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mt-2">
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
id="acp-show-header-name"
|
||||
label={t('acp.show_header_name')}
|
||||
checked={generalSettings.showHeaderName}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
showHeaderName: event.target.checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.default_theme')}</Form.Label>
|
||||
<Form.Select
|
||||
value={generalSettings.defaultTheme}
|
||||
onChange={(event) => handleDefaultThemeChange(event.target.value)}
|
||||
>
|
||||
<option value="auto">{t('ucp.system_default')}</option>
|
||||
<option value="dark">{t('nav.theme_dark')}</option>
|
||||
<option value="light">{t('nav.theme_light')}</option>
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.accent_dark')}</Form.Label>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={generalSettings.darkAccent}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
darkAccent: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="#f29b3f"
|
||||
/>
|
||||
<Form.Control
|
||||
type="color"
|
||||
value={generalSettings.darkAccent || '#f29b3f'}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
darkAccent: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.accent_light')}</Form.Label>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={generalSettings.lightAccent}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
lightAccent: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="#f29b3f"
|
||||
/>
|
||||
<Form.Control
|
||||
type="color"
|
||||
value={generalSettings.lightAccent || '#f29b3f'}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
lightAccent: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.logo_dark')}</Form.Label>
|
||||
<div
|
||||
{...darkLogoDropzone.getRootProps({
|
||||
className: 'bb-dropzone',
|
||||
})}
|
||||
>
|
||||
<input {...darkLogoDropzone.getInputProps()} />
|
||||
{generalSettings.darkLogo ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.darkLogo} alt={t('acp.logo_dark')} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="bb-dropzone-placeholder">
|
||||
<i className="bi bi-upload" aria-hidden="true" />
|
||||
<span>{t('acp.logo_upload')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.logo_light')}</Form.Label>
|
||||
<div
|
||||
{...lightLogoDropzone.getRootProps({
|
||||
className: 'bb-dropzone',
|
||||
})}
|
||||
>
|
||||
<input {...lightLogoDropzone.getInputProps()} />
|
||||
{generalSettings.lightLogo ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.lightLogo} alt={t('acp.logo_light')} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="bb-dropzone-placeholder">
|
||||
<i className="bi bi-upload" aria-hidden="true" />
|
||||
<span>{t('acp.logo_upload')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col xs={12}>
|
||||
<Accordion className="bb-acp-accordion">
|
||||
<Accordion.Item eventKey="favicons">
|
||||
<Accordion.Header>{t('acp.favicons')}</Accordion.Header>
|
||||
<Accordion.Body>
|
||||
<Row className="g-3">
|
||||
<Col lg={4}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.favicon_ico')}</Form.Label>
|
||||
<div {...faviconIcoDropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||
<input {...faviconIcoDropzone.getInputProps()} />
|
||||
{generalSettings.faviconIco ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.faviconIco} alt={t('acp.favicon_ico')} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="bb-dropzone-placeholder">
|
||||
<i className="bi bi-upload" aria-hidden="true" />
|
||||
<span>{t('acp.logo_upload')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.favicon_16')}</Form.Label>
|
||||
<div {...favicon16Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||
<input {...favicon16Dropzone.getInputProps()} />
|
||||
{generalSettings.favicon16 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon16} alt={t('acp.favicon_16')} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="bb-dropzone-placeholder">
|
||||
<i className="bi bi-upload" aria-hidden="true" />
|
||||
<span>{t('acp.logo_upload')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.favicon_32')}</Form.Label>
|
||||
<div {...favicon32Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||
<input {...favicon32Dropzone.getInputProps()} />
|
||||
{generalSettings.favicon32 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon32} alt={t('acp.favicon_32')} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="bb-dropzone-placeholder">
|
||||
<i className="bi bi-upload" aria-hidden="true" />
|
||||
<span>{t('acp.logo_upload')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.favicon_48')}</Form.Label>
|
||||
<div {...favicon48Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||
<input {...favicon48Dropzone.getInputProps()} />
|
||||
{generalSettings.favicon48 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon48} alt={t('acp.favicon_48')} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="bb-dropzone-placeholder">
|
||||
<i className="bi bi-upload" aria-hidden="true" />
|
||||
<span>{t('acp.logo_upload')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.favicon_64')}</Form.Label>
|
||||
<div {...favicon64Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||
<input {...favicon64Dropzone.getInputProps()} />
|
||||
{generalSettings.favicon64 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon64} alt={t('acp.favicon_64')} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="bb-dropzone-placeholder">
|
||||
<i className="bi bi-upload" aria-hidden="true" />
|
||||
<span>{t('acp.logo_upload')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.favicon_128')}</Form.Label>
|
||||
<div {...favicon128Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||
<input {...favicon128Dropzone.getInputProps()} />
|
||||
{generalSettings.favicon128 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon128} alt={t('acp.favicon_128')} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="bb-dropzone-placeholder">
|
||||
<i className="bi bi-upload" aria-hidden="true" />
|
||||
<span>{t('acp.logo_upload')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.favicon_256')}</Form.Label>
|
||||
<div {...favicon256Dropzone.getRootProps({ className: 'bb-dropzone' })}>
|
||||
<input {...favicon256Dropzone.getInputProps()} />
|
||||
{generalSettings.favicon256 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon256} alt={t('acp.favicon_256')} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="bb-dropzone-placeholder">
|
||||
<i className="bi bi-upload" aria-hidden="true" />
|
||||
<span>{t('acp.logo_upload')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
</Accordion.Body>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Col>
|
||||
<Col xs={12} className="d-flex justify-content-end">
|
||||
<Button
|
||||
type="submit"
|
||||
className="bb-accent-button"
|
||||
disabled={generalSaving || generalUploading}
|
||||
>
|
||||
{generalSaving ? t('form.saving') : t('acp.save')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Tab>
|
||||
<Tab eventKey="forums" title={t('acp.forums')}>
|
||||
<p className="bb-muted">{t('acp.forums_hint')}</p>
|
||||
|
||||
Reference in New Issue
Block a user