Refine ACP general settings navigation and tabbed layout
All checks were successful
CI/CD Pipeline / deploy (push) Successful in 31s
CI/CD Pipeline / promote_stable (push) Successful in 2s

This commit is contained in:
2026-02-28 19:13:33 +01:00
parent 94f665192d
commit ef84b73cb5
12 changed files with 409 additions and 211 deletions

View File

@@ -1,5 +1,15 @@
# Changelog # 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 ## 2026-02-27
- Reworked ACP System navigation into `Health` and `Updates`. - 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`. - Moved update/version actions into the new `Updates` area and grouped update checks under `Live Update`, `CLI`, and `CI/CD`.

View File

@@ -32,8 +32,10 @@ class StatsController extends Controller
$avatarSizeBytes = $this->resolveAvatarDirectorySize(); $avatarSizeBytes = $this->resolveAvatarDirectorySize();
$orphanAttachments = $this->resolveOrphanAttachments(); $orphanAttachments = $this->resolveOrphanAttachments();
$version = Setting::query()->where('key', 'version')->value('value'); $composer = $this->readComposerMetadata();
$build = Setting::query()->where('key', 'build')->value('value'); $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 $boardVersion = $version
? ($build ? "{$version} (build {$build})" : $version) ? ($build ? "{$version} (build {$build})" : $version)
: null; : null;
@@ -158,4 +160,59 @@ class StatsController extends Controller
$value = ini_get('zlib.output_compression'); $value = ini_get('zlib.output_compression');
return in_array(strtolower((string) $value), ['1', 'on', 'true'], true); 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.
}
}
} }

View File

@@ -98,5 +98,5 @@
"minimum-stability": "stable", "minimum-stability": "stable",
"prefer-stable": true, "prefer-stable": true,
"version": "26.0.3", "version": "26.0.3",
"build": "110" "build": "111"
} }

View File

@@ -576,6 +576,7 @@ function AppShell() {
className="bb-accent-button" className="bb-accent-button"
onClick={() => window.location.reload()} 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_available_short')} (build {availableBuild}) ·{' '}
{t('version.update_now')} {t('version.update_now')}
</Button> </Button>
@@ -591,9 +592,11 @@ function AppShell() {
</Modal.Body> </Modal.Body>
<Modal.Footer className="justify-content-between"> <Modal.Footer className="justify-content-between">
<Button variant="outline-secondary" onClick={() => setShowUpdateModal(false)}> <Button variant="outline-secondary" onClick={() => setShowUpdateModal(false)}>
<i className="bi bi-clock me-2" aria-hidden="true" />
{t('version.remind_later')} {t('version.remind_later')}
</Button> </Button>
<Button className="bb-accent-button" onClick={() => window.location.reload()}> <Button className="bb-accent-button" onClick={() => window.location.reload()}>
<i className="bi bi-arrow-repeat me-2" aria-hidden="true" />
{t('version.update_now')} {t('version.update_now')}
</Button> </Button>
</Modal.Footer> </Modal.Footer>

View File

@@ -947,7 +947,7 @@ a {
} }
.nav-tabs .nav-link { .nav-tabs .nav-link {
color: var(--bb-accent, #f29b3f); color: var(--bb-ink-muted);
border: 1px solid var(--bb-border); border: 1px solid var(--bb-border);
border-bottom-color: transparent; border-bottom-color: transparent;
border-radius: 10px 10px 0 0; border-radius: 10px 10px 0 0;
@@ -956,7 +956,7 @@ a {
} }
.nav-tabs .nav-link.active { .nav-tabs .nav-link.active {
color: inherit; color: var(--bb-accent, #f29b3f);
background: rgba(255, 255, 255, 0.04); background: rgba(255, 255, 255, 0.04);
border-color: var(--bb-border); border-color: var(--bb-border);
border-bottom-color: transparent; border-bottom-color: transparent;
@@ -2227,6 +2227,25 @@ a {
opacity: 0.6; 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 { .modal-content .modal-header {
background: #0f1218; background: #0f1218;
color: #e6e8eb; color: #e6e8eb;

View File

@@ -97,6 +97,7 @@ function Acp({ isAdmin }) {
const [systemStatus, setSystemStatus] = useState(null) const [systemStatus, setSystemStatus] = useState(null)
const [systemLoading, setSystemLoading] = useState(false) const [systemLoading, setSystemLoading] = useState(false)
const [systemError, setSystemError] = useState('') const [systemError, setSystemError] = useState('')
const [generalSection, setGeneralSection] = useState('overview')
const [systemSection, setSystemSection] = useState('overview') const [systemSection, setSystemSection] = useState('overview')
const [systemUpdateSection, setSystemUpdateSection] = useState('insite') const [systemUpdateSection, setSystemUpdateSection] = useState('insite')
const [usersPage, setUsersPage] = useState(1) const [usersPage, setUsersPage] = useState(1)
@@ -1187,6 +1188,7 @@ function Acp({ isAdmin }) {
onClick={loadSystemStatus} onClick={loadSystemStatus}
disabled={systemLoading} disabled={systemLoading}
> >
<i className="bi bi-arrow-clockwise me-2" aria-hidden="true" />
{t('acp.refresh')} {t('acp.refresh')}
</Button> </Button>
</div> </div>
@@ -1229,6 +1231,7 @@ function Acp({ isAdmin }) {
onClick={loadSystemStatus} onClick={loadSystemStatus}
disabled={systemLoading} disabled={systemLoading}
> >
<i className="bi bi-arrow-repeat me-2" aria-hidden="true" />
{t('system.recheck')} {t('system.recheck')}
</Button> </Button>
</td> </td>
@@ -2896,134 +2899,191 @@ function Acp({ isAdmin }) {
return ( return (
<Container fluid className="bb-acp py-4"> <Container fluid className="bb-acp py-4">
<h2 className="mb-4">{t('acp.title')}</h2> <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')}> <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"> <Row className="g-4">
<Col xs={12} lg="auto"> <Col xs={12} lg="auto">
<div className="bb-acp-sidebar"> <div className="bb-acp-sidebar">
<div className="bb-acp-sidebar-section"> <div className="bb-acp-sidebar-section">
<div className="bb-acp-sidebar-title">{t('acp.quick_access')}</div> <div className="bb-acp-sidebar-title">{t('acp.general')}</div>
<div className="list-group"> <div className="list-group">
<button type="button" className="list-group-item list-group-item-action"> <button
{t('acp.users')} type="button"
</button> className={`list-group-item list-group-item-action ${
<button type="button" className="list-group-item list-group-item-action"> generalSection === 'overview' ? 'is-active' : ''
{t('acp.groups')} }`}
</button> onClick={() => setGeneralSection('overview')}
<button type="button" className="list-group-item list-group-item-action"> >
{t('acp.forums')} <i className="bi bi-speedometer2 me-2" aria-hidden="true" />
</button> Overview
<button type="button" className="list-group-item list-group-item-action"> </button>
{t('acp.ranks')} <button
</button> type="button"
<button type="button" className="list-group-item list-group-item-action"> className={`list-group-item list-group-item-action ${
{t('acp.attachments')} generalSection === 'settings' ? 'is-active' : ''
</button> }`}
onClick={() => setGeneralSection('settings')}
>
<i className="bi bi-sliders2 me-2" aria-hidden="true" />
Settings
</button>
</div>
</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> </div>
</Col> </Col>
<Col xs={12} lg> <Col xs={12} lg>
<div className="bb-acp-panel mb-4"> {generalSection === 'overview' && (
<div className="bb-acp-panel-header"> <>
<div className="d-flex align-items-center justify-content-between"> <div className="bb-acp-panel mb-4">
<h5 className="mb-0">{t('acp.statistics')}</h5> <div className="bb-acp-panel-header">
<Button <div className="d-flex align-items-center justify-content-between">
type="button" <h5 className="mb-0">{t('acp.statistics')}</h5>
size="sm" <Button
variant="dark" type="button"
onClick={refreshBoardStats} size="sm"
disabled={boardStatsLoading} variant="dark"
> className="d-inline-flex align-items-center gap-2 px-3"
{t('acp.refresh')} onClick={refreshBoardStats}
</Button> disabled={boardStatsLoading}
</div> >
</div> <i className="bi bi-arrow-clockwise" aria-hidden="true" />
<div className="bb-acp-panel-body"> {t('acp.refresh')}
{boardStatsError && <p className="text-danger mb-2">{boardStatsError}</p>} </Button>
{boardStatsLoading && <p className="bb-muted mb-0">{t('acp.loading')}</p>} </div>
{!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 className="bb-acp-panel-body">
</div> {boardStatsError && <p className="text-danger mb-2">{boardStatsError}</p>}
</div> {boardStatsLoading && <p className="bb-muted mb-0">{t('acp.loading')}</p>}
{generalError && <p className="text-danger">{generalError}</p>} {!boardStatsLoading && (
<div className="bb-acp-panel"> <div className="bb-acp-stats-grid">
<div className="bb-acp-panel-header"> <table className="bb-acp-stats-table">
<h5 className="mb-0">{t('acp.general_settings')}</h5> <thead>
</div> <tr>
<div className="bb-acp-panel-body"> <th>{t('stats.statistic')}</th>
<Form onSubmit={handleGeneralSave} className="bb-acp-general"> <th>{t('stats.value')}</th>
<Row className="g-3"> </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}> <Col lg={6}>
<Form.Group> <Form.Group>
<Form.Label>{t('acp.forum_name')}</Form.Label> <Form.Label>{t('acp.forum_name')}</Form.Label>
@@ -3303,86 +3363,72 @@ function Acp({ isAdmin }) {
</Accordion.Item> </Accordion.Item>
</Accordion> </Accordion>
</Col> </Col>
<Col xs={12} className="d-flex justify-content-end"> <Col xs={12} className="d-flex justify-content-end">
<Button <Button
type="submit" type="submit"
className="bb-accent-button" className="bb-accent-button"
disabled={generalSaving || generalUploading} disabled={generalSaving || generalUploading}
> >
{generalSaving ? t('form.saving') : t('acp.save')} <i className="bi bi-floppy me-2" aria-hidden="true" />
</Button> {generalSaving ? t('form.saving') : t('acp.save')}
</Col> </Button>
</Row> </Col>
</Form> </Row>
</div> </Form>
</div> </div>
<div className="bb-acp-panel mt-4"> </div>
<div className="bb-acp-panel-header"> </div>
<div className="d-flex align-items-center justify-content-between"> </Tab>
<div> <Tab eventKey="client-communication" title={t('acp.client_communication')}>
<h5 className="mb-1">{t('acp.admin_log_title')}</h5> <div className="border border-1 border-dark border-top-0 rounded-bottom p-3">
<p className="bb-muted mb-0">{t('acp.admin_log_hint')}</p> <div className="bb-acp-panel">
</div> <div className="bb-acp-panel-header">
<Button <h5 className="mb-0">{t('acp.client_communication')}</h5>
type="button" </div>
size="sm" <div className="bb-acp-panel-body">
variant="dark" <p className="bb-muted mb-3">Placeholder</p>
onClick={refreshAuditLogs} <div className="list-group">
disabled={auditLoading} <button type="button" className="list-group-item list-group-item-action">
> <i className="bi bi-shield-lock me-2" aria-hidden="true" />
{t('acp.refresh')} {t('acp.authentication')}
</Button> </button>
</div> <button type="button" className="list-group-item list-group-item-action">
</div> <i className="bi bi-envelope me-2" aria-hidden="true" />
<div className="bb-acp-panel-body"> {t('acp.email_settings')}
{auditLoading && <p className="bb-muted mb-0">{t('acp.loading')}</p>} </button>
{!auditLoading && recentAdminLogs.length === 0 && ( </div>
<p className="bb-muted mb-0">{t('admin_log.empty')}</p> </div>
)} </div>
{!auditLoading && recentAdminLogs.length > 0 && ( </div>
<div className="bb-acp-admin-log"> </Tab>
<table className="bb-acp-admin-log__table"> <Tab eventKey="server-configuration" title={t('acp.server_configuration')}>
<thead> <div className="border border-1 border-dark border-top-0 rounded-bottom p-3">
<tr> <div className="bb-acp-panel">
<th>{t('admin_log.username')}</th> <div className="bb-acp-panel-header">
<th>{t('admin_log.user_ip')}</th> <h5 className="mb-0">{t('acp.server_configuration')}</h5>
<th>{t('admin_log.time')}</th> </div>
<th>{t('admin_log.action')}</th> <div className="bb-acp-panel-body">
</tr> <p className="bb-muted mb-3">Placeholder</p>
</thead> <div className="list-group">
<tbody> <button type="button" className="list-group-item list-group-item-action">
{recentAdminLogs.map((entry) => ( <i className="bi bi-shield-check me-2" aria-hidden="true" />
<tr key={entry.id}> {t('acp.security_settings')}
<td>{entry.user?.name || entry.user?.email || '—'}</td> </button>
<td>{entry.ip_address || '—'}</td> <button type="button" className="list-group-item list-group-item-action">
<td>{formatDateTime(entry.created_at)}</td> <i className="bi bi-search me-2" aria-hidden="true" />
<td>{formatAuditAction(entry.action)}</td> {t('acp.search_settings')}
</tr> </button>
))} </div>
</tbody> </div>
<tfoot> </div>
<tr> </div>
<td colSpan={4}> </Tab>
<button </Tabs>
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> </Col>
</Row> </Row>
</div>
</Tab> </Tab>
<Tab eventKey="forums" title={t('acp.forums')}> <Tab eventKey="forums" title={t('acp.forums')}>
<p className="bb-muted">{t('acp.forums_hint')}</p> <p className="bb-muted">{t('acp.forums_hint')}</p>
@@ -3482,6 +3528,7 @@ function Acp({ isAdmin }) {
className="bb-accent-button" className="bb-accent-button"
onClick={() => setShowRoleCreate(true)} onClick={() => setShowRoleCreate(true)}
> >
<i className="bi bi-plus-circle me-2" aria-hidden="true" />
{t('group.create')} {t('group.create')}
</Button> </Button>
</div> </div>
@@ -3573,6 +3620,7 @@ function Acp({ isAdmin }) {
className="bb-accent-button" className="bb-accent-button"
onClick={() => setShowRankCreate(true)} onClick={() => setShowRankCreate(true)}
> >
<i className="bi bi-plus-circle me-2" aria-hidden="true" />
{t('rank.create')} {t('rank.create')}
</Button> </Button>
</div> </div>
@@ -3735,6 +3783,7 @@ function Acp({ isAdmin }) {
className="bb-accent-button" className="bb-accent-button"
disabled={attachmentSettingsSaving} disabled={attachmentSettingsSaving}
> >
<i className="bi bi-floppy me-2" aria-hidden="true" />
{attachmentSettingsSaving ? t('form.saving') : t('acp.save')} {attachmentSettingsSaving ? t('form.saving') : t('acp.save')}
</Button> </Button>
</div> </div>
@@ -3750,6 +3799,7 @@ function Acp({ isAdmin }) {
onClick={handleSeedAttachmentDefaults} onClick={handleSeedAttachmentDefaults}
disabled={attachmentSeedSaving} disabled={attachmentSeedSaving}
> >
<i className="bi bi-database-add me-2" aria-hidden="true" />
{attachmentSeedSaving {attachmentSeedSaving
? t('attachment.seed_in_progress') ? t('attachment.seed_in_progress')
: t('attachment.seed_defaults')} : t('attachment.seed_defaults')}
@@ -3759,6 +3809,7 @@ function Acp({ isAdmin }) {
variant="outline-secondary" variant="outline-secondary"
onClick={handleAttachmentGroupExpandAll} onClick={handleAttachmentGroupExpandAll}
> >
<i className="bi bi-arrows-expand me-2" aria-hidden="true" />
{t('acp.expand_all')} {t('acp.expand_all')}
</Button> </Button>
<Button <Button
@@ -3766,6 +3817,7 @@ function Acp({ isAdmin }) {
variant="outline-secondary" variant="outline-secondary"
onClick={handleAttachmentGroupCollapseAll} onClick={handleAttachmentGroupCollapseAll}
> >
<i className="bi bi-arrows-collapse me-2" aria-hidden="true" />
{t('acp.collapse_all')} {t('acp.collapse_all')}
</Button> </Button>
<Button <Button
@@ -3773,6 +3825,7 @@ function Acp({ isAdmin }) {
className="bb-accent-button" className="bb-accent-button"
onClick={() => openAttachmentGroupModal()} onClick={() => openAttachmentGroupModal()}
> >
<i className="bi bi-folder-plus me-2" aria-hidden="true" />
{t('attachment.group_create')} {t('attachment.group_create')}
</Button> </Button>
</div> </div>
@@ -3787,6 +3840,7 @@ function Acp({ isAdmin }) {
onClick={handleSeedAttachmentDefaults} onClick={handleSeedAttachmentDefaults}
disabled={attachmentSeedSaving} disabled={attachmentSeedSaving}
> >
<i className="bi bi-database-add me-2" aria-hidden="true" />
{attachmentSeedSaving {attachmentSeedSaving
? t('attachment.seed_in_progress') ? t('attachment.seed_in_progress')
: t('attachment.seed_defaults')} : t('attachment.seed_defaults')}
@@ -3805,6 +3859,7 @@ function Acp({ isAdmin }) {
onClick={handleSeedAttachmentDefaults} onClick={handleSeedAttachmentDefaults}
disabled={attachmentSeedSaving} disabled={attachmentSeedSaving}
> >
<i className="bi bi-database-add me-2" aria-hidden="true" />
{attachmentSeedSaving {attachmentSeedSaving
? t('attachment.seed_in_progress') ? t('attachment.seed_in_progress')
: t('attachment.seed_defaults')} : t('attachment.seed_defaults')}
@@ -3815,6 +3870,7 @@ function Acp({ isAdmin }) {
onClick={handleAttachmentGroupAutoNest} onClick={handleAttachmentGroupAutoNest}
disabled={attachmentSeedSaving} disabled={attachmentSeedSaving}
> >
<i className="bi bi-diagram-2 me-2" aria-hidden="true" />
{attachmentSeedSaving {attachmentSeedSaving
? t('attachment.seed_in_progress') ? t('attachment.seed_in_progress')
: t('attachment.group_auto_nest')} : t('attachment.group_auto_nest')}
@@ -3856,6 +3912,7 @@ function Acp({ isAdmin }) {
onClick={refreshAuditLogs} onClick={refreshAuditLogs}
disabled={auditLoading} disabled={auditLoading}
> >
<i className="bi bi-arrow-clockwise me-2" aria-hidden="true" />
{t('acp.refresh')} {t('acp.refresh')}
</Button> </Button>
</div> </div>
@@ -3898,6 +3955,7 @@ function Acp({ isAdmin }) {
}`} }`}
onClick={() => setSystemSection('overview')} onClick={() => setSystemSection('overview')}
> >
<i className="bi bi-heart-pulse me-2" aria-hidden="true" />
Health Health
</button> </button>
<button <button
@@ -3907,6 +3965,7 @@ function Acp({ isAdmin }) {
}`} }`}
onClick={() => setSystemSection('updates')} onClick={() => setSystemSection('updates')}
> >
<i className="bi bi-arrow-repeat me-2" aria-hidden="true" />
Updates Updates
</button> </button>
</div> </div>
@@ -3930,6 +3989,7 @@ function Acp({ isAdmin }) {
onClick={handleVersionCheck} onClick={handleVersionCheck}
disabled={versionChecking} disabled={versionChecking}
> >
<i className="bi bi-arrow-clockwise me-2" aria-hidden="true" />
{t('version.recheck')} {t('version.recheck')}
</Button> </Button>
{systemUpdateAvailable && ( {systemUpdateAvailable && (
@@ -3939,6 +3999,7 @@ function Acp({ isAdmin }) {
onClick={() => setUpdateModalOpen(true)} onClick={() => setUpdateModalOpen(true)}
disabled={updateRunning} disabled={updateRunning}
> >
<i className="bi bi-download me-2" aria-hidden="true" />
{t('version.update_now')} {t('version.update_now')}
</Button> </Button>
)} )}
@@ -3953,6 +4014,7 @@ function Acp({ isAdmin }) {
variant={systemUpdateSection === 'insite' ? 'primary' : 'dark'} variant={systemUpdateSection === 'insite' ? 'primary' : 'dark'}
onClick={() => setSystemUpdateSection('insite')} onClick={() => setSystemUpdateSection('insite')}
> >
<i className="bi bi-activity me-2" aria-hidden="true" />
Live Update Live Update
</Button> </Button>
<Button <Button
@@ -3960,6 +4022,7 @@ function Acp({ isAdmin }) {
variant={systemUpdateSection === 'cli' ? 'primary' : 'dark'} variant={systemUpdateSection === 'cli' ? 'primary' : 'dark'}
onClick={() => setSystemUpdateSection('cli')} onClick={() => setSystemUpdateSection('cli')}
> >
<i className="bi bi-terminal me-2" aria-hidden="true" />
CLI CLI
</Button> </Button>
<Button <Button
@@ -3967,6 +4030,7 @@ function Acp({ isAdmin }) {
variant={systemUpdateSection === 'ci' ? 'primary' : 'dark'} variant={systemUpdateSection === 'ci' ? 'primary' : 'dark'}
onClick={() => setSystemUpdateSection('ci')} onClick={() => setSystemUpdateSection('ci')}
> >
<i className="bi bi-diagram-3 me-2" aria-hidden="true" />
CI/CD CI/CD
</Button> </Button>
</ButtonGroup> </ButtonGroup>
@@ -4190,9 +4254,11 @@ function Acp({ isAdmin }) {
</Form.Group> </Form.Group>
<div className="d-flex gap-2 justify-content-between"> <div className="d-flex gap-2 justify-content-between">
<Button type="button" variant="outline-secondary" onClick={handleReset}> <Button type="button" variant="outline-secondary" onClick={handleReset}>
<i className="bi bi-x-circle me-2" aria-hidden="true" />
{t('acp.cancel')} {t('acp.cancel')}
</Button> </Button>
<Button type="submit" className="bb-accent-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')} {selectedId ? t('acp.save') : t('acp.create')}
</Button> </Button>
</div> </div>
@@ -4405,9 +4471,11 @@ function Acp({ isAdmin }) {
onClick={() => setShowUserModal(false)} onClick={() => setShowUserModal(false)}
disabled={userSaving} disabled={userSaving}
> >
<i className="bi bi-x-circle me-2" aria-hidden="true" />
{t('acp.cancel')} {t('acp.cancel')}
</Button> </Button>
<Button type="submit" className="bb-accent-button" variant="dark" disabled={userSaving}> <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')} {userSaving ? t('form.saving') : t('acp.save')}
</Button> </Button>
</div> </div>
@@ -4495,9 +4563,11 @@ function Acp({ isAdmin }) {
onClick={() => setShowRoleModal(false)} onClick={() => setShowRoleModal(false)}
disabled={roleSaving} disabled={roleSaving}
> >
<i className="bi bi-x-circle me-2" aria-hidden="true" />
{t('acp.cancel')} {t('acp.cancel')}
</Button> </Button>
<Button type="submit" className="bb-accent-button" variant="dark" disabled={roleSaving}> <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')} {roleSaving ? t('form.saving') : t('acp.save')}
</Button> </Button>
</div> </div>
@@ -4548,6 +4618,7 @@ function Acp({ isAdmin }) {
variant="dark" variant="dark"
disabled={roleSaving || !roleFormName.trim()} disabled={roleSaving || !roleFormName.trim()}
> >
<i className="bi bi-plus-circle me-2" aria-hidden="true" />
{roleSaving ? t('form.saving') : t('group.create')} {roleSaving ? t('form.saving') : t('group.create')}
</Button> </Button>
</div> </div>
@@ -4717,9 +4788,11 @@ function Acp({ isAdmin }) {
onClick={() => setShowRankModal(false)} onClick={() => setShowRankModal(false)}
disabled={rankSaving} disabled={rankSaving}
> >
<i className="bi bi-x-circle me-2" aria-hidden="true" />
{t('acp.cancel')} {t('acp.cancel')}
</Button> </Button>
<Button type="submit" className="bb-accent-button" variant="dark" disabled={rankSaving}> <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')} {rankSaving ? t('form.saving') : t('acp.save')}
</Button> </Button>
</div> </div>
@@ -4745,6 +4818,7 @@ function Acp({ isAdmin }) {
onClick={() => setUpdateModalOpen(false)} onClick={() => setUpdateModalOpen(false)}
disabled={updateRunning} disabled={updateRunning}
> >
<i className="bi bi-x-circle me-2" aria-hidden="true" />
{t('acp.cancel')} {t('acp.cancel')}
</Button> </Button>
<Button <Button
@@ -4752,6 +4826,7 @@ function Acp({ isAdmin }) {
onClick={handleRunUpdate} onClick={handleRunUpdate}
disabled={updateRunning} disabled={updateRunning}
> >
<i className="bi bi-download me-2" aria-hidden="true" />
{updateRunning ? t('version.updating') : t('version.update_now')} {updateRunning ? t('version.updating') : t('version.update_now')}
</Button> </Button>
</Modal.Footer> </Modal.Footer>
@@ -4863,6 +4938,7 @@ function Acp({ isAdmin }) {
variant="dark" variant="dark"
disabled={rankSaving || !rankFormName.trim()} disabled={rankSaving || !rankFormName.trim()}
> >
<i className="bi bi-award me-2" aria-hidden="true" />
{rankSaving ? t('form.saving') : t('rank.create')} {rankSaving ? t('form.saving') : t('rank.create')}
</Button> </Button>
</div> </div>
@@ -4951,6 +5027,7 @@ function Acp({ isAdmin }) {
onClick={() => setShowAttachmentGroupModal(false)} onClick={() => setShowAttachmentGroupModal(false)}
disabled={attachmentGroupSaving} disabled={attachmentGroupSaving}
> >
<i className="bi bi-x-circle me-2" aria-hidden="true" />
{t('acp.cancel')} {t('acp.cancel')}
</Button> </Button>
<Button <Button
@@ -4959,6 +5036,7 @@ function Acp({ isAdmin }) {
variant="dark" variant="dark"
disabled={attachmentGroupSaving} disabled={attachmentGroupSaving}
> >
<i className="bi bi-floppy me-2" aria-hidden="true" />
{attachmentGroupSaving ? t('form.saving') : t('acp.save')} {attachmentGroupSaving ? t('form.saving') : t('acp.save')}
</Button> </Button>
</div> </div>
@@ -5038,6 +5116,7 @@ function Acp({ isAdmin }) {
onClick={() => setShowAttachmentExtensionModal(false)} onClick={() => setShowAttachmentExtensionModal(false)}
disabled={attachmentExtensionSaving} disabled={attachmentExtensionSaving}
> >
<i className="bi bi-x-circle me-2" aria-hidden="true" />
{t('acp.cancel')} {t('acp.cancel')}
</Button> </Button>
<Button <Button
@@ -5046,6 +5125,7 @@ function Acp({ isAdmin }) {
variant="dark" variant="dark"
disabled={attachmentExtensionSaving || !newAttachmentExtension.extension.trim()} disabled={attachmentExtensionSaving || !newAttachmentExtension.extension.trim()}
> >
<i className="bi bi-floppy me-2" aria-hidden="true" />
{attachmentExtensionSaving {attachmentExtensionSaving
? t('form.saving') ? t('form.saving')
: attachmentExtensionEdit : attachmentExtensionEdit
@@ -5074,18 +5154,20 @@ function Acp({ isAdmin }) {
variant="outline-secondary" variant="outline-secondary"
onClick={() => setShowAttachmentExtensionDelete(false)} onClick={() => setShowAttachmentExtensionDelete(false)}
disabled={attachmentExtensionSaving} disabled={attachmentExtensionSaving}
> >
{t('acp.cancel')} <i className="bi bi-x-circle me-2" aria-hidden="true" />
</Button> {t('acp.cancel')}
</Button>
<Button <Button
type="button" type="button"
className="bb-accent-button" className="bb-accent-button"
variant="dark" variant="dark"
onClick={confirmAttachmentExtensionDelete} onClick={confirmAttachmentExtensionDelete}
disabled={attachmentExtensionSaving} disabled={attachmentExtensionSaving}
> >
{attachmentExtensionSaving ? t('form.saving') : t('acp.delete')} <i className="bi bi-trash me-2" aria-hidden="true" />
</Button> {attachmentExtensionSaving ? t('form.saving') : t('acp.delete')}
</Button>
</div> </div>
</Modal.Body> </Modal.Body>
</Modal> </Modal>

View File

@@ -397,6 +397,7 @@ export default function ForumView() {
className={`bb-attachment-tab ${attachmentTab === 'options' ? 'is-active' : ''}`} className={`bb-attachment-tab ${attachmentTab === 'options' ? 'is-active' : ''}`}
onClick={() => setAttachmentTab('options')} onClick={() => setAttachmentTab('options')}
> >
<i className="bi bi-sliders me-2" aria-hidden="true" />
{t('attachment.tab_options')} {t('attachment.tab_options')}
</button> </button>
<button <button
@@ -404,6 +405,7 @@ export default function ForumView() {
className={`bb-attachment-tab ${attachmentTab === 'attachments' ? 'is-active' : ''}`} className={`bb-attachment-tab ${attachmentTab === 'attachments' ? 'is-active' : ''}`}
onClick={() => setAttachmentTab('attachments')} onClick={() => setAttachmentTab('attachments')}
> >
<i className="bi bi-paperclip me-2" aria-hidden="true" />
{t('attachment.tab_attachments')} {t('attachment.tab_attachments')}
</button> </button>
</div> </div>
@@ -499,6 +501,7 @@ export default function ForumView() {
variant="outline-secondary" variant="outline-secondary"
onClick={() => document.getElementById('bb-thread-attachment-input')?.click()} onClick={() => document.getElementById('bb-thread-attachment-input')?.click()}
> >
<i className="bi bi-upload me-2" aria-hidden="true" />
{t('attachment.add_files')} {t('attachment.add_files')}
</Button> </Button>
</div> </div>
@@ -635,13 +638,14 @@ export default function ForumView() {
</span> </span>
<div className="bb-topic-pagination"> <div className="bb-topic-pagination">
<Button size="sm" variant="outline-secondary" disabled> <Button size="sm" variant="outline-secondary" disabled>
<i className="bi bi-chevron-left" aria-hidden="true" />
</Button> </Button>
<Button size="sm" variant="outline-secondary" className="is-active" disabled> <Button size="sm" variant="outline-secondary" className="is-active" disabled>
<i className="bi bi-dot me-1" aria-hidden="true" />
1 1
</Button> </Button>
<Button size="sm" variant="outline-secondary" disabled> <Button size="sm" variant="outline-secondary" disabled>
<i className="bi bi-chevron-right" aria-hidden="true" />
</Button> </Button>
</div> </div>
</div> </div>
@@ -755,6 +759,7 @@ export default function ForumView() {
document.getElementById('bb-thread-attachment-input')?.click() document.getElementById('bb-thread-attachment-input')?.click()
}} }}
> >
<i className="bi bi-folder2-open me-2" aria-hidden="true" />
{t('attachment.drop_browse')} {t('attachment.drop_browse')}
</button> </button>
</span> </span>
@@ -762,6 +767,7 @@ export default function ForumView() {
{renderAttachmentFooter()} {renderAttachmentFooter()}
<Modal.Footer className="d-flex gap-2 justify-content-between mt-auto pt-2 px-0 border-0 mb-0 pb-0"> <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)}> <Button type="button" variant="outline-secondary" onClick={() => setShowModal(false)}>
<i className="bi bi-x-circle me-2" aria-hidden="true" />
{t('acp.cancel')} {t('acp.cancel')}
</Button> </Button>
<div className="d-flex gap-2"> <div className="d-flex gap-2">
@@ -771,6 +777,7 @@ export default function ForumView() {
onClick={handlePreview} onClick={handlePreview}
disabled={!token || saving || uploading || previewLoading} disabled={!token || saving || uploading || previewLoading}
> >
<i className="bi bi-eye me-2" aria-hidden="true" />
{t('form.preview')} {t('form.preview')}
</Button> </Button>
<Button <Button
@@ -778,6 +785,7 @@ export default function ForumView() {
className="bb-accent-button" className="bb-accent-button"
disabled={!token || saving || uploading} disabled={!token || saving || uploading}
> >
<i className="bi bi-plus-circle me-2" aria-hidden="true" />
{saving || uploading ? t('form.posting') : t('form.create_thread')} {saving || uploading ? t('form.posting') : t('form.create_thread')}
</Button> </Button>
</div> </div>

View File

@@ -59,9 +59,11 @@ export default function Login() {
</Form.Group> </Form.Group>
<div className="d-flex w-100 align-items-center gap-2"> <div className="d-flex w-100 align-items-center gap-2">
<Button as={Link} to="/" type="button" variant="outline-secondary" disabled={loading}> <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')} {t('acp.cancel')}
</Button> </Button>
<Button type="submit" className="ms-auto bb-accent-button" disabled={loading}> <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')} {loading ? t('form.signing_in') : t('form.sign_in')}
</Button> </Button>
</div> </div>

View File

@@ -70,6 +70,7 @@ export default function Register() {
/> />
</Form.Group> </Form.Group>
<Button type="submit" variant="dark" disabled={loading}> <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')} {loading ? t('form.registering') : t('form.create_account')}
</Button> </Button>
</Form> </Form>

View File

@@ -93,9 +93,14 @@ export default function ResetPassword() {
)} )}
<div className="d-flex w-100 align-items-center gap-2"> <div className="d-flex w-100 align-items-center gap-2">
<Button as={Link} to="/login" type="button" variant="outline-secondary" disabled={loading}> <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')} {t('acp.cancel')}
</Button> </Button>
<Button type="submit" className="ms-auto bb-accent-button" disabled={loading}> <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 {loading
? isResetFlow ? isResetFlow
? t('auth.resetting_password') ? t('auth.resetting_password')

View File

@@ -284,6 +284,7 @@ export default function ThreadView() {
className={`bb-attachment-tab ${replyAttachmentTab === 'options' ? 'is-active' : ''}`} className={`bb-attachment-tab ${replyAttachmentTab === 'options' ? 'is-active' : ''}`}
onClick={() => setReplyAttachmentTab('options')} onClick={() => setReplyAttachmentTab('options')}
> >
<i className="bi bi-sliders me-2" aria-hidden="true" />
{t('attachment.tab_options')} {t('attachment.tab_options')}
</button> </button>
<button <button
@@ -291,6 +292,7 @@ export default function ThreadView() {
className={`bb-attachment-tab ${replyAttachmentTab === 'attachments' ? 'is-active' : ''}`} className={`bb-attachment-tab ${replyAttachmentTab === 'attachments' ? 'is-active' : ''}`}
onClick={() => setReplyAttachmentTab('attachments')} onClick={() => setReplyAttachmentTab('attachments')}
> >
<i className="bi bi-paperclip me-2" aria-hidden="true" />
{t('attachment.tab_attachments')} {t('attachment.tab_attachments')}
</button> </button>
</div> </div>
@@ -374,6 +376,7 @@ export default function ThreadView() {
variant="outline-secondary" variant="outline-secondary"
onClick={() => document.getElementById('bb-reply-attachment-input')?.click()} onClick={() => document.getElementById('bb-reply-attachment-input')?.click()}
> >
<i className="bi bi-upload me-2" aria-hidden="true" />
{t('attachment.add_files')} {t('attachment.add_files')}
</Button> </Button>
</div> </div>
@@ -1040,6 +1043,7 @@ export default function ThreadView() {
document.getElementById('bb-reply-attachment-input')?.click() document.getElementById('bb-reply-attachment-input')?.click()
}} }}
> >
<i className="bi bi-folder2-open me-2" aria-hidden="true" />
{t('attachment.drop_browse')} {t('attachment.drop_browse')}
</button> </button>
</span> </span>
@@ -1053,6 +1057,7 @@ export default function ThreadView() {
onClick={handlePreview} onClick={handlePreview}
disabled={!token || saving || replyUploading || previewLoading} disabled={!token || saving || replyUploading || previewLoading}
> >
<i className="bi bi-eye me-2" aria-hidden="true" />
{t('form.preview')} {t('form.preview')}
</Button> </Button>
<Button <Button
@@ -1060,6 +1065,7 @@ export default function ThreadView() {
className="bb-accent-button" className="bb-accent-button"
disabled={!token || saving || replyUploading} disabled={!token || saving || replyUploading}
> >
<i className="bi bi-reply-fill me-2" aria-hidden="true" />
{saving || replyUploading ? t('form.posting') : t('form.post_reply')} {saving || replyUploading ? t('form.posting') : t('form.post_reply')}
</Button> </Button>
</div> </div>
@@ -1119,6 +1125,7 @@ export default function ThreadView() {
</Modal.Body> </Modal.Body>
<Modal.Footer className="justify-content-between"> <Modal.Footer className="justify-content-between">
<Button variant="outline-secondary" onClick={() => setEditPost(null)}> <Button variant="outline-secondary" onClick={() => setEditPost(null)}>
<i className="bi bi-x-circle me-2" aria-hidden="true" />
{t('acp.cancel')} {t('acp.cancel')}
</Button> </Button>
<Button <Button
@@ -1126,6 +1133,7 @@ export default function ThreadView() {
onClick={handleEditSave} onClick={handleEditSave}
disabled={editSaving || !editBody.trim() || (editPost?.isRoot && !editTitle.trim())} 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')} {editSaving ? t('form.saving') : t('acp.save')}
</Button> </Button>
</Modal.Footer> </Modal.Footer>
@@ -1180,6 +1188,7 @@ export default function ThreadView() {
onClick={() => setDeleteTarget(null)} onClick={() => setDeleteTarget(null)}
disabled={deleteLoading} disabled={deleteLoading}
> >
<i className="bi bi-x-circle me-2" aria-hidden="true" />
{t('acp.cancel')} {t('acp.cancel')}
</Button> </Button>
<Button <Button
@@ -1187,6 +1196,7 @@ export default function ThreadView() {
onClick={handleDeleteConfirm} onClick={handleDeleteConfirm}
disabled={deleteLoading} disabled={deleteLoading}
> >
<i className="bi bi-trash me-2" aria-hidden="true" />
{deleteLoading ? t('form.saving') : t('acp.delete')} {deleteLoading ? t('form.saving') : t('acp.delete')}
</Button> </Button>
</Modal.Footer> </Modal.Footer>

View File

@@ -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')} {profileSaving ? t('form.saving') : t('ucp.save_profile')}
</Button> </Button>
</Col> </Col>