Refine ACP general settings navigation and tabbed layout
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-28
|
||||
- Updated ACP General to use section navigation with `Overview` as the default landing view and a dedicated `Settings` view.
|
||||
- Reorganized ACP General placeholders by moving `Client communication` and `Server configuration` into the Settings area as dedicated sub-tabs.
|
||||
- Added nested Settings tab grouping and bordered tab-content containers to match the ACP tabbed layout pattern.
|
||||
- Refined ACP tab visual states so inactive tabs render muted and active tabs use the configured accent color.
|
||||
- Standardized key ACP refresh actions with explicit icon + spacing so repeated controls render consistently.
|
||||
- Added icon support to additional primary UI actions (update modal/footer actions, auth screens, and forum/thread actions).
|
||||
- Synced board version/build display in stats from `composer.json` and added safe DB setting synchronization fallback logic.
|
||||
- Applied global accent-based Bootstrap button variable overrides so primary button styling remains consistent across ACP and user-facing screens.
|
||||
|
||||
## 2026-02-27
|
||||
- Reworked ACP System navigation into `Health` and `Updates`.
|
||||
- Moved update/version actions into the new `Updates` area and grouped update checks under `Live Update`, `CLI`, and `CI/CD`.
|
||||
|
||||
@@ -32,8 +32,10 @@ class StatsController extends Controller
|
||||
$avatarSizeBytes = $this->resolveAvatarDirectorySize();
|
||||
$orphanAttachments = $this->resolveOrphanAttachments();
|
||||
|
||||
$version = Setting::query()->where('key', 'version')->value('value');
|
||||
$build = Setting::query()->where('key', 'build')->value('value');
|
||||
$composer = $this->readComposerMetadata();
|
||||
$this->syncVersionBuildSettings($composer);
|
||||
$version = $composer['version'] ?? Setting::query()->where('key', 'version')->value('value');
|
||||
$build = $composer['build'] ?? Setting::query()->where('key', 'build')->value('value');
|
||||
$boardVersion = $version
|
||||
? ($build ? "{$version} (build {$build})" : $version)
|
||||
: null;
|
||||
@@ -158,4 +160,59 @@ class StatsController extends Controller
|
||||
$value = ini_get('zlib.output_compression');
|
||||
return in_array(strtolower((string) $value), ['1', 'on', 'true'], true);
|
||||
}
|
||||
|
||||
private function readComposerMetadata(): array
|
||||
{
|
||||
$path = base_path('composer.json');
|
||||
if (!is_file($path) || !is_readable($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$raw = file_get_contents($path);
|
||||
if ($raw === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
if (!is_array($data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$version = trim((string) ($data['version'] ?? ''));
|
||||
$build = trim((string) ($data['build'] ?? ''));
|
||||
|
||||
return [
|
||||
'version' => $version !== '' ? $version : null,
|
||||
'build' => ctype_digit($build) ? (int) $build : null,
|
||||
];
|
||||
}
|
||||
|
||||
private function syncVersionBuildSettings(array $composer): void
|
||||
{
|
||||
$version = $composer['version'] ?? null;
|
||||
$build = $composer['build'] ?? null;
|
||||
|
||||
if ($version === null && $build === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($version !== null) {
|
||||
$currentVersion = Setting::query()->where('key', 'version')->value('value');
|
||||
if ((string) $currentVersion !== (string) $version) {
|
||||
Setting::updateOrCreate(['key' => 'version'], ['value' => (string) $version]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($build !== null) {
|
||||
$buildString = (string) $build;
|
||||
$currentBuild = Setting::query()->where('key', 'build')->value('value');
|
||||
if ((string) $currentBuild !== $buildString) {
|
||||
Setting::updateOrCreate(['key' => 'build'], ['value' => $buildString]);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Stats endpoint should remain readable even if settings sync fails.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,5 +98,5 @@
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"version": "26.0.3",
|
||||
"build": "110"
|
||||
"build": "111"
|
||||
}
|
||||
|
||||
@@ -576,6 +576,7 @@ function AppShell() {
|
||||
className="bb-accent-button"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
<i className="bi bi-arrow-clockwise me-2" aria-hidden="true" />
|
||||
{t('version.update_available_short')} (build {availableBuild}) ·{' '}
|
||||
{t('version.update_now')}
|
||||
</Button>
|
||||
@@ -591,9 +592,11 @@ function AppShell() {
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="justify-content-between">
|
||||
<Button variant="outline-secondary" onClick={() => setShowUpdateModal(false)}>
|
||||
<i className="bi bi-clock me-2" aria-hidden="true" />
|
||||
{t('version.remind_later')}
|
||||
</Button>
|
||||
<Button className="bb-accent-button" onClick={() => window.location.reload()}>
|
||||
<i className="bi bi-arrow-repeat me-2" aria-hidden="true" />
|
||||
{t('version.update_now')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
|
||||
@@ -947,7 +947,7 @@ a {
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
color: var(--bb-accent, #f29b3f);
|
||||
color: var(--bb-ink-muted);
|
||||
border: 1px solid var(--bb-border);
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 10px 10px 0 0;
|
||||
@@ -956,7 +956,7 @@ a {
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: inherit;
|
||||
color: var(--bb-accent, #f29b3f);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: var(--bb-border);
|
||||
border-bottom-color: transparent;
|
||||
@@ -2227,6 +2227,25 @@ a {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.btn:not(.btn-close) {
|
||||
--bs-btn-bg: var(--bb-accent, #f29b3f) !important;
|
||||
--bs-btn-border-color: var(--bb-accent, #f29b3f) !important;
|
||||
--bs-btn-color: #0e121b !important;
|
||||
--bs-btn-hover-bg: color-mix(in srgb, var(--bb-accent, #f29b3f) 85%, #000) !important;
|
||||
--bs-btn-hover-border-color: color-mix(in srgb, var(--bb-accent, #f29b3f) 85%, #000) !important;
|
||||
--bs-btn-hover-color: #fff !important;
|
||||
--bs-btn-active-bg: color-mix(in srgb, var(--bb-accent, #f29b3f) 80%, #000) !important;
|
||||
--bs-btn-active-border-color: color-mix(in srgb, var(--bb-accent, #f29b3f) 80%, #000) !important;
|
||||
--bs-btn-active-color: #fff !important;
|
||||
--bs-btn-disabled-bg: var(--bb-accent, #f29b3f) !important;
|
||||
--bs-btn-disabled-border-color: var(--bb-accent, #f29b3f) !important;
|
||||
--bs-btn-disabled-color: #0e121b !important;
|
||||
}
|
||||
|
||||
.btn:not(.btn-close):focus-visible {
|
||||
box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--bb-accent, #f29b3f) 35%, transparent);
|
||||
}
|
||||
|
||||
.modal-content .modal-header {
|
||||
background: #0f1218;
|
||||
color: #e6e8eb;
|
||||
|
||||
@@ -97,6 +97,7 @@ function Acp({ isAdmin }) {
|
||||
const [systemStatus, setSystemStatus] = useState(null)
|
||||
const [systemLoading, setSystemLoading] = useState(false)
|
||||
const [systemError, setSystemError] = useState('')
|
||||
const [generalSection, setGeneralSection] = useState('overview')
|
||||
const [systemSection, setSystemSection] = useState('overview')
|
||||
const [systemUpdateSection, setSystemUpdateSection] = useState('insite')
|
||||
const [usersPage, setUsersPage] = useState(1)
|
||||
@@ -1187,6 +1188,7 @@ function Acp({ isAdmin }) {
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
<i className="bi bi-arrow-clockwise me-2" aria-hidden="true" />
|
||||
{t('acp.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1229,6 +1231,7 @@ function Acp({ isAdmin }) {
|
||||
onClick={loadSystemStatus}
|
||||
disabled={systemLoading}
|
||||
>
|
||||
<i className="bi bi-arrow-repeat me-2" aria-hidden="true" />
|
||||
{t('system.recheck')}
|
||||
</Button>
|
||||
</td>
|
||||
@@ -2896,134 +2899,191 @@ function Acp({ isAdmin }) {
|
||||
return (
|
||||
<Container fluid className="bb-acp py-4">
|
||||
<h2 className="mb-4">{t('acp.title')}</h2>
|
||||
<Tabs defaultActiveKey="general" className="mb-3">
|
||||
<Tabs
|
||||
defaultActiveKey="general"
|
||||
className="mb-0"
|
||||
contentClassName="pt-2"
|
||||
>
|
||||
<Tab eventKey="general" title={t('acp.general')}>
|
||||
<div className="border border-1 border-dark border-top-0 rounded-bottom p-3">
|
||||
<Row className="g-4">
|
||||
<Col xs={12} lg="auto">
|
||||
<div className="bb-acp-sidebar">
|
||||
<div className="bb-acp-sidebar-section">
|
||||
<div className="bb-acp-sidebar-title">{t('acp.quick_access')}</div>
|
||||
<div className="list-group">
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
{t('acp.users')}
|
||||
</button>
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
{t('acp.groups')}
|
||||
</button>
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
{t('acp.forums')}
|
||||
</button>
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
{t('acp.ranks')}
|
||||
</button>
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
{t('acp.attachments')}
|
||||
</button>
|
||||
<div className="bb-acp-sidebar">
|
||||
<div className="bb-acp-sidebar-section">
|
||||
<div className="bb-acp-sidebar-title">{t('acp.general')}</div>
|
||||
<div className="list-group">
|
||||
<button
|
||||
type="button"
|
||||
className={`list-group-item list-group-item-action ${
|
||||
generalSection === 'overview' ? 'is-active' : ''
|
||||
}`}
|
||||
onClick={() => setGeneralSection('overview')}
|
||||
>
|
||||
<i className="bi bi-speedometer2 me-2" aria-hidden="true" />
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`list-group-item list-group-item-action ${
|
||||
generalSection === 'settings' ? 'is-active' : ''
|
||||
}`}
|
||||
onClick={() => setGeneralSection('settings')}
|
||||
>
|
||||
<i className="bi bi-sliders2 me-2" aria-hidden="true" />
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bb-acp-sidebar-section">
|
||||
<div className="bb-acp-sidebar-title">{t('acp.board_configuration')}</div>
|
||||
<div className="list-group">
|
||||
<button type="button" className="list-group-item list-group-item-action is-active">
|
||||
{t('acp.general')}
|
||||
</button>
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
{t('acp.forums')}
|
||||
</button>
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
{t('acp.users')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bb-acp-sidebar-section">
|
||||
<div className="bb-acp-sidebar-title">{t('acp.client_communication')}</div>
|
||||
<div className="list-group">
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
{t('acp.authentication')}
|
||||
</button>
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
{t('acp.email_settings')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bb-acp-sidebar-section">
|
||||
<div className="bb-acp-sidebar-title">{t('acp.server_configuration')}</div>
|
||||
<div className="list-group">
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
{t('acp.security_settings')}
|
||||
</button>
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
{t('acp.search_settings')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={12} lg>
|
||||
<div className="bb-acp-panel mb-4">
|
||||
<div className="bb-acp-panel-header">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<h5 className="mb-0">{t('acp.statistics')}</h5>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={refreshBoardStats}
|
||||
disabled={boardStatsLoading}
|
||||
>
|
||||
{t('acp.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bb-acp-panel-body">
|
||||
{boardStatsError && <p className="text-danger mb-2">{boardStatsError}</p>}
|
||||
{boardStatsLoading && <p className="bb-muted mb-0">{t('acp.loading')}</p>}
|
||||
{!boardStatsLoading && (
|
||||
<div className="bb-acp-stats-grid">
|
||||
<table className="bb-acp-stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('stats.statistic')}</th>
|
||||
<th>{t('stats.value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{statsLeft.map((stat) => (
|
||||
<tr key={stat.label}>
|
||||
<td>{stat.label}</td>
|
||||
<td className="bb-acp-stats-value">{stat.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<table className="bb-acp-stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('stats.statistic')}</th>
|
||||
<th>{t('stats.value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{statsRight.map((stat) => (
|
||||
<tr key={stat.label}>
|
||||
<td>{stat.label}</td>
|
||||
<td className="bb-acp-stats-value">{stat.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{generalSection === 'overview' && (
|
||||
<>
|
||||
<div className="bb-acp-panel mb-4">
|
||||
<div className="bb-acp-panel-header">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<h5 className="mb-0">{t('acp.statistics')}</h5>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="dark"
|
||||
className="d-inline-flex align-items-center gap-2 px-3"
|
||||
onClick={refreshBoardStats}
|
||||
disabled={boardStatsLoading}
|
||||
>
|
||||
<i className="bi bi-arrow-clockwise" aria-hidden="true" />
|
||||
{t('acp.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{generalError && <p className="text-danger">{generalError}</p>}
|
||||
<div className="bb-acp-panel">
|
||||
<div className="bb-acp-panel-header">
|
||||
<h5 className="mb-0">{t('acp.general_settings')}</h5>
|
||||
</div>
|
||||
<div className="bb-acp-panel-body">
|
||||
<Form onSubmit={handleGeneralSave} className="bb-acp-general">
|
||||
<Row className="g-3">
|
||||
<div className="bb-acp-panel-body">
|
||||
{boardStatsError && <p className="text-danger mb-2">{boardStatsError}</p>}
|
||||
{boardStatsLoading && <p className="bb-muted mb-0">{t('acp.loading')}</p>}
|
||||
{!boardStatsLoading && (
|
||||
<div className="bb-acp-stats-grid">
|
||||
<table className="bb-acp-stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('stats.statistic')}</th>
|
||||
<th>{t('stats.value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{statsLeft.map((stat) => (
|
||||
<tr key={stat.label}>
|
||||
<td>{stat.label}</td>
|
||||
<td className="bb-acp-stats-value">{stat.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<table className="bb-acp-stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('stats.statistic')}</th>
|
||||
<th>{t('stats.value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{statsRight.map((stat) => (
|
||||
<tr key={stat.label}>
|
||||
<td>{stat.label}</td>
|
||||
<td className="bb-acp-stats-value">{stat.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bb-acp-panel mt-4">
|
||||
<div className="bb-acp-panel-header">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h5 className="mb-1">{t('acp.admin_log_title')}</h5>
|
||||
<p className="bb-muted mb-0">{t('acp.admin_log_hint')}</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="dark"
|
||||
className="d-inline-flex align-items-center gap-2 px-3"
|
||||
onClick={refreshAuditLogs}
|
||||
disabled={auditLoading}
|
||||
>
|
||||
<i className="bi bi-arrow-clockwise" aria-hidden="true" />
|
||||
{t('acp.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bb-acp-panel-body">
|
||||
{auditLoading && <p className="bb-muted mb-0">{t('acp.loading')}</p>}
|
||||
{!auditLoading && recentAdminLogs.length === 0 && (
|
||||
<p className="bb-muted mb-0">{t('admin_log.empty')}</p>
|
||||
)}
|
||||
{!auditLoading && recentAdminLogs.length > 0 && (
|
||||
<div className="bb-acp-admin-log">
|
||||
<table className="bb-acp-admin-log__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('admin_log.username')}</th>
|
||||
<th>{t('admin_log.user_ip')}</th>
|
||||
<th>{t('admin_log.time')}</th>
|
||||
<th>{t('admin_log.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recentAdminLogs.map((entry) => (
|
||||
<tr key={entry.id}>
|
||||
<td>{entry.user?.name || entry.user?.email || '—'}</td>
|
||||
<td>{entry.ip_address || '—'}</td>
|
||||
<td>{formatDateTime(entry.created_at)}</td>
|
||||
<td>{formatAuditAction(entry.action)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={4}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link p-0"
|
||||
onClick={() => {
|
||||
const target = document.querySelector('[data-rb-event-key="audit"]')
|
||||
if (target) target.click()
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-box-arrow-up-right me-2" aria-hidden="true" />
|
||||
{t('acp.view_admin_log')}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{generalSection === 'settings' && (
|
||||
<>
|
||||
{generalError && <p className="text-danger">{generalError}</p>}
|
||||
<Tabs
|
||||
defaultActiveKey="general"
|
||||
className="mb-0"
|
||||
contentClassName="pt-2"
|
||||
>
|
||||
<Tab eventKey="general" title={t('acp.general')}>
|
||||
<div className="border border-1 border-dark border-top-0 rounded-bottom p-3">
|
||||
<div className="bb-acp-panel">
|
||||
<div className="bb-acp-panel-header">
|
||||
<h5 className="mb-0">{t('acp.general_settings')}</h5>
|
||||
</div>
|
||||
<div className="bb-acp-panel-body">
|
||||
<Form onSubmit={handleGeneralSave} className="bb-acp-general">
|
||||
<Row className="g-3">
|
||||
<Col lg={6}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.forum_name')}</Form.Label>
|
||||
@@ -3303,86 +3363,72 @@ function Acp({ isAdmin }) {
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bb-acp-panel mt-4">
|
||||
<div className="bb-acp-panel-header">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h5 className="mb-1">{t('acp.admin_log_title')}</h5>
|
||||
<p className="bb-muted mb-0">{t('acp.admin_log_hint')}</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onClick={refreshAuditLogs}
|
||||
disabled={auditLoading}
|
||||
>
|
||||
{t('acp.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bb-acp-panel-body">
|
||||
{auditLoading && <p className="bb-muted mb-0">{t('acp.loading')}</p>}
|
||||
{!auditLoading && recentAdminLogs.length === 0 && (
|
||||
<p className="bb-muted mb-0">{t('admin_log.empty')}</p>
|
||||
)}
|
||||
{!auditLoading && recentAdminLogs.length > 0 && (
|
||||
<div className="bb-acp-admin-log">
|
||||
<table className="bb-acp-admin-log__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('admin_log.username')}</th>
|
||||
<th>{t('admin_log.user_ip')}</th>
|
||||
<th>{t('admin_log.time')}</th>
|
||||
<th>{t('admin_log.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recentAdminLogs.map((entry) => (
|
||||
<tr key={entry.id}>
|
||||
<td>{entry.user?.name || entry.user?.email || '—'}</td>
|
||||
<td>{entry.ip_address || '—'}</td>
|
||||
<td>{formatDateTime(entry.created_at)}</td>
|
||||
<td>{formatAuditAction(entry.action)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={4}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link p-0"
|
||||
onClick={() => {
|
||||
const target = document.querySelector('[data-rb-event-key="audit"]')
|
||||
if (target) target.click()
|
||||
}}
|
||||
>
|
||||
{t('acp.view_admin_log')}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Col xs={12} className="d-flex justify-content-end">
|
||||
<Button
|
||||
type="submit"
|
||||
className="bb-accent-button"
|
||||
disabled={generalSaving || generalUploading}
|
||||
>
|
||||
<i className="bi bi-floppy me-2" aria-hidden="true" />
|
||||
{generalSaving ? t('form.saving') : t('acp.save')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab eventKey="client-communication" title={t('acp.client_communication')}>
|
||||
<div className="border border-1 border-dark border-top-0 rounded-bottom p-3">
|
||||
<div className="bb-acp-panel">
|
||||
<div className="bb-acp-panel-header">
|
||||
<h5 className="mb-0">{t('acp.client_communication')}</h5>
|
||||
</div>
|
||||
<div className="bb-acp-panel-body">
|
||||
<p className="bb-muted mb-3">Placeholder</p>
|
||||
<div className="list-group">
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
<i className="bi bi-shield-lock me-2" aria-hidden="true" />
|
||||
{t('acp.authentication')}
|
||||
</button>
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
<i className="bi bi-envelope me-2" aria-hidden="true" />
|
||||
{t('acp.email_settings')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab eventKey="server-configuration" title={t('acp.server_configuration')}>
|
||||
<div className="border border-1 border-dark border-top-0 rounded-bottom p-3">
|
||||
<div className="bb-acp-panel">
|
||||
<div className="bb-acp-panel-header">
|
||||
<h5 className="mb-0">{t('acp.server_configuration')}</h5>
|
||||
</div>
|
||||
<div className="bb-acp-panel-body">
|
||||
<p className="bb-muted mb-3">Placeholder</p>
|
||||
<div className="list-group">
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
<i className="bi bi-shield-check me-2" aria-hidden="true" />
|
||||
{t('acp.security_settings')}
|
||||
</button>
|
||||
<button type="button" className="list-group-item list-group-item-action">
|
||||
<i className="bi bi-search me-2" aria-hidden="true" />
|
||||
{t('acp.search_settings')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab eventKey="forums" title={t('acp.forums')}>
|
||||
<p className="bb-muted">{t('acp.forums_hint')}</p>
|
||||
@@ -3482,6 +3528,7 @@ function Acp({ isAdmin }) {
|
||||
className="bb-accent-button"
|
||||
onClick={() => setShowRoleCreate(true)}
|
||||
>
|
||||
<i className="bi bi-plus-circle me-2" aria-hidden="true" />
|
||||
{t('group.create')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -3573,6 +3620,7 @@ function Acp({ isAdmin }) {
|
||||
className="bb-accent-button"
|
||||
onClick={() => setShowRankCreate(true)}
|
||||
>
|
||||
<i className="bi bi-plus-circle me-2" aria-hidden="true" />
|
||||
{t('rank.create')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -3735,6 +3783,7 @@ function Acp({ isAdmin }) {
|
||||
className="bb-accent-button"
|
||||
disabled={attachmentSettingsSaving}
|
||||
>
|
||||
<i className="bi bi-floppy me-2" aria-hidden="true" />
|
||||
{attachmentSettingsSaving ? t('form.saving') : t('acp.save')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -3750,6 +3799,7 @@ function Acp({ isAdmin }) {
|
||||
onClick={handleSeedAttachmentDefaults}
|
||||
disabled={attachmentSeedSaving}
|
||||
>
|
||||
<i className="bi bi-database-add me-2" aria-hidden="true" />
|
||||
{attachmentSeedSaving
|
||||
? t('attachment.seed_in_progress')
|
||||
: t('attachment.seed_defaults')}
|
||||
@@ -3759,6 +3809,7 @@ function Acp({ isAdmin }) {
|
||||
variant="outline-secondary"
|
||||
onClick={handleAttachmentGroupExpandAll}
|
||||
>
|
||||
<i className="bi bi-arrows-expand me-2" aria-hidden="true" />
|
||||
{t('acp.expand_all')}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -3766,6 +3817,7 @@ function Acp({ isAdmin }) {
|
||||
variant="outline-secondary"
|
||||
onClick={handleAttachmentGroupCollapseAll}
|
||||
>
|
||||
<i className="bi bi-arrows-collapse me-2" aria-hidden="true" />
|
||||
{t('acp.collapse_all')}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -3773,6 +3825,7 @@ function Acp({ isAdmin }) {
|
||||
className="bb-accent-button"
|
||||
onClick={() => openAttachmentGroupModal()}
|
||||
>
|
||||
<i className="bi bi-folder-plus me-2" aria-hidden="true" />
|
||||
{t('attachment.group_create')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -3787,6 +3840,7 @@ function Acp({ isAdmin }) {
|
||||
onClick={handleSeedAttachmentDefaults}
|
||||
disabled={attachmentSeedSaving}
|
||||
>
|
||||
<i className="bi bi-database-add me-2" aria-hidden="true" />
|
||||
{attachmentSeedSaving
|
||||
? t('attachment.seed_in_progress')
|
||||
: t('attachment.seed_defaults')}
|
||||
@@ -3805,6 +3859,7 @@ function Acp({ isAdmin }) {
|
||||
onClick={handleSeedAttachmentDefaults}
|
||||
disabled={attachmentSeedSaving}
|
||||
>
|
||||
<i className="bi bi-database-add me-2" aria-hidden="true" />
|
||||
{attachmentSeedSaving
|
||||
? t('attachment.seed_in_progress')
|
||||
: t('attachment.seed_defaults')}
|
||||
@@ -3815,6 +3870,7 @@ function Acp({ isAdmin }) {
|
||||
onClick={handleAttachmentGroupAutoNest}
|
||||
disabled={attachmentSeedSaving}
|
||||
>
|
||||
<i className="bi bi-diagram-2 me-2" aria-hidden="true" />
|
||||
{attachmentSeedSaving
|
||||
? t('attachment.seed_in_progress')
|
||||
: t('attachment.group_auto_nest')}
|
||||
@@ -3856,6 +3912,7 @@ function Acp({ isAdmin }) {
|
||||
onClick={refreshAuditLogs}
|
||||
disabled={auditLoading}
|
||||
>
|
||||
<i className="bi bi-arrow-clockwise me-2" aria-hidden="true" />
|
||||
{t('acp.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -3898,6 +3955,7 @@ function Acp({ isAdmin }) {
|
||||
}`}
|
||||
onClick={() => setSystemSection('overview')}
|
||||
>
|
||||
<i className="bi bi-heart-pulse me-2" aria-hidden="true" />
|
||||
Health
|
||||
</button>
|
||||
<button
|
||||
@@ -3907,6 +3965,7 @@ function Acp({ isAdmin }) {
|
||||
}`}
|
||||
onClick={() => setSystemSection('updates')}
|
||||
>
|
||||
<i className="bi bi-arrow-repeat me-2" aria-hidden="true" />
|
||||
Updates
|
||||
</button>
|
||||
</div>
|
||||
@@ -3930,6 +3989,7 @@ function Acp({ isAdmin }) {
|
||||
onClick={handleVersionCheck}
|
||||
disabled={versionChecking}
|
||||
>
|
||||
<i className="bi bi-arrow-clockwise me-2" aria-hidden="true" />
|
||||
{t('version.recheck')}
|
||||
</Button>
|
||||
{systemUpdateAvailable && (
|
||||
@@ -3939,6 +3999,7 @@ function Acp({ isAdmin }) {
|
||||
onClick={() => setUpdateModalOpen(true)}
|
||||
disabled={updateRunning}
|
||||
>
|
||||
<i className="bi bi-download me-2" aria-hidden="true" />
|
||||
{t('version.update_now')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -3953,6 +4014,7 @@ function Acp({ isAdmin }) {
|
||||
variant={systemUpdateSection === 'insite' ? 'primary' : 'dark'}
|
||||
onClick={() => setSystemUpdateSection('insite')}
|
||||
>
|
||||
<i className="bi bi-activity me-2" aria-hidden="true" />
|
||||
Live Update
|
||||
</Button>
|
||||
<Button
|
||||
@@ -3960,6 +4022,7 @@ function Acp({ isAdmin }) {
|
||||
variant={systemUpdateSection === 'cli' ? 'primary' : 'dark'}
|
||||
onClick={() => setSystemUpdateSection('cli')}
|
||||
>
|
||||
<i className="bi bi-terminal me-2" aria-hidden="true" />
|
||||
CLI
|
||||
</Button>
|
||||
<Button
|
||||
@@ -3967,6 +4030,7 @@ function Acp({ isAdmin }) {
|
||||
variant={systemUpdateSection === 'ci' ? 'primary' : 'dark'}
|
||||
onClick={() => setSystemUpdateSection('ci')}
|
||||
>
|
||||
<i className="bi bi-diagram-3 me-2" aria-hidden="true" />
|
||||
CI/CD
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
@@ -4190,9 +4254,11 @@ function Acp({ isAdmin }) {
|
||||
</Form.Group>
|
||||
<div className="d-flex gap-2 justify-content-between">
|
||||
<Button type="button" variant="outline-secondary" onClick={handleReset}>
|
||||
<i className="bi bi-x-circle me-2" aria-hidden="true" />
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" className="bb-accent-button">
|
||||
<i className="bi bi-check2-circle me-2" aria-hidden="true" />
|
||||
{selectedId ? t('acp.save') : t('acp.create')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -4405,9 +4471,11 @@ function Acp({ isAdmin }) {
|
||||
onClick={() => setShowUserModal(false)}
|
||||
disabled={userSaving}
|
||||
>
|
||||
<i className="bi bi-x-circle me-2" aria-hidden="true" />
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" className="bb-accent-button" variant="dark" disabled={userSaving}>
|
||||
<i className="bi bi-floppy me-2" aria-hidden="true" />
|
||||
{userSaving ? t('form.saving') : t('acp.save')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -4495,9 +4563,11 @@ function Acp({ isAdmin }) {
|
||||
onClick={() => setShowRoleModal(false)}
|
||||
disabled={roleSaving}
|
||||
>
|
||||
<i className="bi bi-x-circle me-2" aria-hidden="true" />
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" className="bb-accent-button" variant="dark" disabled={roleSaving}>
|
||||
<i className="bi bi-floppy me-2" aria-hidden="true" />
|
||||
{roleSaving ? t('form.saving') : t('acp.save')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -4548,6 +4618,7 @@ function Acp({ isAdmin }) {
|
||||
variant="dark"
|
||||
disabled={roleSaving || !roleFormName.trim()}
|
||||
>
|
||||
<i className="bi bi-plus-circle me-2" aria-hidden="true" />
|
||||
{roleSaving ? t('form.saving') : t('group.create')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -4717,9 +4788,11 @@ function Acp({ isAdmin }) {
|
||||
onClick={() => setShowRankModal(false)}
|
||||
disabled={rankSaving}
|
||||
>
|
||||
<i className="bi bi-x-circle me-2" aria-hidden="true" />
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" className="bb-accent-button" variant="dark" disabled={rankSaving}>
|
||||
<i className="bi bi-floppy me-2" aria-hidden="true" />
|
||||
{rankSaving ? t('form.saving') : t('acp.save')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -4745,6 +4818,7 @@ function Acp({ isAdmin }) {
|
||||
onClick={() => setUpdateModalOpen(false)}
|
||||
disabled={updateRunning}
|
||||
>
|
||||
<i className="bi bi-x-circle me-2" aria-hidden="true" />
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -4752,6 +4826,7 @@ function Acp({ isAdmin }) {
|
||||
onClick={handleRunUpdate}
|
||||
disabled={updateRunning}
|
||||
>
|
||||
<i className="bi bi-download me-2" aria-hidden="true" />
|
||||
{updateRunning ? t('version.updating') : t('version.update_now')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
@@ -4863,6 +4938,7 @@ function Acp({ isAdmin }) {
|
||||
variant="dark"
|
||||
disabled={rankSaving || !rankFormName.trim()}
|
||||
>
|
||||
<i className="bi bi-award me-2" aria-hidden="true" />
|
||||
{rankSaving ? t('form.saving') : t('rank.create')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -4951,6 +5027,7 @@ function Acp({ isAdmin }) {
|
||||
onClick={() => setShowAttachmentGroupModal(false)}
|
||||
disabled={attachmentGroupSaving}
|
||||
>
|
||||
<i className="bi bi-x-circle me-2" aria-hidden="true" />
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -4959,6 +5036,7 @@ function Acp({ isAdmin }) {
|
||||
variant="dark"
|
||||
disabled={attachmentGroupSaving}
|
||||
>
|
||||
<i className="bi bi-floppy me-2" aria-hidden="true" />
|
||||
{attachmentGroupSaving ? t('form.saving') : t('acp.save')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -5038,6 +5116,7 @@ function Acp({ isAdmin }) {
|
||||
onClick={() => setShowAttachmentExtensionModal(false)}
|
||||
disabled={attachmentExtensionSaving}
|
||||
>
|
||||
<i className="bi bi-x-circle me-2" aria-hidden="true" />
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -5046,6 +5125,7 @@ function Acp({ isAdmin }) {
|
||||
variant="dark"
|
||||
disabled={attachmentExtensionSaving || !newAttachmentExtension.extension.trim()}
|
||||
>
|
||||
<i className="bi bi-floppy me-2" aria-hidden="true" />
|
||||
{attachmentExtensionSaving
|
||||
? t('form.saving')
|
||||
: attachmentExtensionEdit
|
||||
@@ -5074,18 +5154,20 @@ function Acp({ isAdmin }) {
|
||||
variant="outline-secondary"
|
||||
onClick={() => setShowAttachmentExtensionDelete(false)}
|
||||
disabled={attachmentExtensionSaving}
|
||||
>
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
>
|
||||
<i className="bi bi-x-circle me-2" aria-hidden="true" />
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="bb-accent-button"
|
||||
variant="dark"
|
||||
onClick={confirmAttachmentExtensionDelete}
|
||||
disabled={attachmentExtensionSaving}
|
||||
>
|
||||
{attachmentExtensionSaving ? t('form.saving') : t('acp.delete')}
|
||||
</Button>
|
||||
>
|
||||
<i className="bi bi-trash me-2" aria-hidden="true" />
|
||||
{attachmentExtensionSaving ? t('form.saving') : t('acp.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
|
||||
@@ -397,6 +397,7 @@ export default function ForumView() {
|
||||
className={`bb-attachment-tab ${attachmentTab === 'options' ? 'is-active' : ''}`}
|
||||
onClick={() => setAttachmentTab('options')}
|
||||
>
|
||||
<i className="bi bi-sliders me-2" aria-hidden="true" />
|
||||
{t('attachment.tab_options')}
|
||||
</button>
|
||||
<button
|
||||
@@ -404,6 +405,7 @@ export default function ForumView() {
|
||||
className={`bb-attachment-tab ${attachmentTab === 'attachments' ? 'is-active' : ''}`}
|
||||
onClick={() => setAttachmentTab('attachments')}
|
||||
>
|
||||
<i className="bi bi-paperclip me-2" aria-hidden="true" />
|
||||
{t('attachment.tab_attachments')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -499,6 +501,7 @@ export default function ForumView() {
|
||||
variant="outline-secondary"
|
||||
onClick={() => document.getElementById('bb-thread-attachment-input')?.click()}
|
||||
>
|
||||
<i className="bi bi-upload me-2" aria-hidden="true" />
|
||||
{t('attachment.add_files')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -635,13 +638,14 @@ export default function ForumView() {
|
||||
</span>
|
||||
<div className="bb-topic-pagination">
|
||||
<Button size="sm" variant="outline-secondary" disabled>
|
||||
‹
|
||||
<i className="bi bi-chevron-left" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline-secondary" className="is-active" disabled>
|
||||
<i className="bi bi-dot me-1" aria-hidden="true" />
|
||||
1
|
||||
</Button>
|
||||
<Button size="sm" variant="outline-secondary" disabled>
|
||||
›
|
||||
<i className="bi bi-chevron-right" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -755,6 +759,7 @@ export default function ForumView() {
|
||||
document.getElementById('bb-thread-attachment-input')?.click()
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-folder2-open me-2" aria-hidden="true" />
|
||||
{t('attachment.drop_browse')}
|
||||
</button>
|
||||
</span>
|
||||
@@ -762,6 +767,7 @@ export default function ForumView() {
|
||||
{renderAttachmentFooter()}
|
||||
<Modal.Footer className="d-flex gap-2 justify-content-between mt-auto pt-2 px-0 border-0 mb-0 pb-0">
|
||||
<Button type="button" variant="outline-secondary" onClick={() => setShowModal(false)}>
|
||||
<i className="bi bi-x-circle me-2" aria-hidden="true" />
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<div className="d-flex gap-2">
|
||||
@@ -771,6 +777,7 @@ export default function ForumView() {
|
||||
onClick={handlePreview}
|
||||
disabled={!token || saving || uploading || previewLoading}
|
||||
>
|
||||
<i className="bi bi-eye me-2" aria-hidden="true" />
|
||||
{t('form.preview')}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -778,6 +785,7 @@ export default function ForumView() {
|
||||
className="bb-accent-button"
|
||||
disabled={!token || saving || uploading}
|
||||
>
|
||||
<i className="bi bi-plus-circle me-2" aria-hidden="true" />
|
||||
{saving || uploading ? t('form.posting') : t('form.create_thread')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -59,9 +59,11 @@ export default function Login() {
|
||||
</Form.Group>
|
||||
<div className="d-flex w-100 align-items-center gap-2">
|
||||
<Button as={Link} to="/" type="button" variant="outline-secondary" disabled={loading}>
|
||||
<i className="bi bi-x-circle me-2" aria-hidden="true" />
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" className="ms-auto bb-accent-button" disabled={loading}>
|
||||
<i className="bi bi-box-arrow-in-right me-2" aria-hidden="true" />
|
||||
{loading ? t('form.signing_in') : t('form.sign_in')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -70,6 +70,7 @@ export default function Register() {
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button type="submit" variant="dark" disabled={loading}>
|
||||
<i className="bi bi-person-plus me-2" aria-hidden="true" />
|
||||
{loading ? t('form.registering') : t('form.create_account')}
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
@@ -93,9 +93,14 @@ export default function ResetPassword() {
|
||||
)}
|
||||
<div className="d-flex w-100 align-items-center gap-2">
|
||||
<Button as={Link} to="/login" type="button" variant="outline-secondary" disabled={loading}>
|
||||
<i className="bi bi-x-circle me-2" aria-hidden="true" />
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" className="ms-auto bb-accent-button" disabled={loading}>
|
||||
<i
|
||||
className={`bi ${isResetFlow ? 'bi-key-fill' : 'bi-envelope-arrow-up-fill'} me-2`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{loading
|
||||
? isResetFlow
|
||||
? t('auth.resetting_password')
|
||||
|
||||
@@ -284,6 +284,7 @@ export default function ThreadView() {
|
||||
className={`bb-attachment-tab ${replyAttachmentTab === 'options' ? 'is-active' : ''}`}
|
||||
onClick={() => setReplyAttachmentTab('options')}
|
||||
>
|
||||
<i className="bi bi-sliders me-2" aria-hidden="true" />
|
||||
{t('attachment.tab_options')}
|
||||
</button>
|
||||
<button
|
||||
@@ -291,6 +292,7 @@ export default function ThreadView() {
|
||||
className={`bb-attachment-tab ${replyAttachmentTab === 'attachments' ? 'is-active' : ''}`}
|
||||
onClick={() => setReplyAttachmentTab('attachments')}
|
||||
>
|
||||
<i className="bi bi-paperclip me-2" aria-hidden="true" />
|
||||
{t('attachment.tab_attachments')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -374,6 +376,7 @@ export default function ThreadView() {
|
||||
variant="outline-secondary"
|
||||
onClick={() => document.getElementById('bb-reply-attachment-input')?.click()}
|
||||
>
|
||||
<i className="bi bi-upload me-2" aria-hidden="true" />
|
||||
{t('attachment.add_files')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1040,6 +1043,7 @@ export default function ThreadView() {
|
||||
document.getElementById('bb-reply-attachment-input')?.click()
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-folder2-open me-2" aria-hidden="true" />
|
||||
{t('attachment.drop_browse')}
|
||||
</button>
|
||||
</span>
|
||||
@@ -1053,6 +1057,7 @@ export default function ThreadView() {
|
||||
onClick={handlePreview}
|
||||
disabled={!token || saving || replyUploading || previewLoading}
|
||||
>
|
||||
<i className="bi bi-eye me-2" aria-hidden="true" />
|
||||
{t('form.preview')}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -1060,6 +1065,7 @@ export default function ThreadView() {
|
||||
className="bb-accent-button"
|
||||
disabled={!token || saving || replyUploading}
|
||||
>
|
||||
<i className="bi bi-reply-fill me-2" aria-hidden="true" />
|
||||
{saving || replyUploading ? t('form.posting') : t('form.post_reply')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1119,6 +1125,7 @@ export default function ThreadView() {
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="justify-content-between">
|
||||
<Button variant="outline-secondary" onClick={() => setEditPost(null)}>
|
||||
<i className="bi bi-x-circle me-2" aria-hidden="true" />
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -1126,6 +1133,7 @@ export default function ThreadView() {
|
||||
onClick={handleEditSave}
|
||||
disabled={editSaving || !editBody.trim() || (editPost?.isRoot && !editTitle.trim())}
|
||||
>
|
||||
<i className="bi bi-floppy me-2" aria-hidden="true" />
|
||||
{editSaving ? t('form.saving') : t('acp.save')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
@@ -1180,6 +1188,7 @@ export default function ThreadView() {
|
||||
onClick={() => setDeleteTarget(null)}
|
||||
disabled={deleteLoading}
|
||||
>
|
||||
<i className="bi bi-x-circle me-2" aria-hidden="true" />
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -1187,6 +1196,7 @@ export default function ThreadView() {
|
||||
onClick={handleDeleteConfirm}
|
||||
disabled={deleteLoading}
|
||||
>
|
||||
<i className="bi bi-trash me-2" aria-hidden="true" />
|
||||
{deleteLoading ? t('form.saving') : t('acp.delete')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
|
||||
@@ -116,6 +116,7 @@ export default function Ucp({ theme, setTheme, accentOverride, setAccentOverride
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-floppy me-2" aria-hidden="true" />
|
||||
{profileSaving ? t('form.saving') : t('ucp.save_profile')}
|
||||
</Button>
|
||||
</Col>
|
||||
|
||||
Reference in New Issue
Block a user