Add attachment thumbnails and ACP refinements
This commit is contained in:
@@ -116,6 +116,14 @@ export default function Acp({ isAdmin }) {
|
||||
const [attachmentExtensionSaving, setAttachmentExtensionSaving] = useState(false)
|
||||
const [attachmentExtensionSavingId, setAttachmentExtensionSavingId] = useState(null)
|
||||
const [attachmentSeedSaving, setAttachmentSeedSaving] = useState(false)
|
||||
const [attachmentSettingsSaving, setAttachmentSettingsSaving] = useState(false)
|
||||
const [attachmentSettings, setAttachmentSettings] = useState({
|
||||
display_images_inline: 'true',
|
||||
create_thumbnails: 'true',
|
||||
thumbnail_max_width: '300',
|
||||
thumbnail_max_height: '300',
|
||||
thumbnail_quality: '85',
|
||||
})
|
||||
const [showAttachmentExtensionModal, setShowAttachmentExtensionModal] = useState(false)
|
||||
const [attachmentExtensionEdit, setAttachmentExtensionEdit] = useState(null)
|
||||
const [showAttachmentExtensionDelete, setShowAttachmentExtensionDelete] = useState(false)
|
||||
@@ -241,6 +249,13 @@ export default function Acp({ isAdmin }) {
|
||||
favicon_256: settingsMap.get('favicon_256') || '',
|
||||
}
|
||||
setGeneralSettings(next)
|
||||
setAttachmentSettings({
|
||||
display_images_inline: settingsMap.get('attachments.display_images_inline') || 'true',
|
||||
create_thumbnails: settingsMap.get('attachments.create_thumbnails') || 'true',
|
||||
thumbnail_max_width: settingsMap.get('attachments.thumbnail_max_width') || '300',
|
||||
thumbnail_max_height: settingsMap.get('attachments.thumbnail_max_height') || '300',
|
||||
thumbnail_quality: settingsMap.get('attachments.thumbnail_quality') || '85',
|
||||
})
|
||||
} catch (err) {
|
||||
if (active) setGeneralError(err.message)
|
||||
}
|
||||
@@ -297,6 +312,24 @@ export default function Acp({ isAdmin }) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAttachmentSettingsSave = async (event) => {
|
||||
event.preventDefault()
|
||||
setAttachmentSettingsSaving(true)
|
||||
setAttachmentGroupsError('')
|
||||
try {
|
||||
await saveSettings(
|
||||
Object.entries(attachmentSettings).map(([key, value]) => ({
|
||||
key: `attachments.${key}`,
|
||||
value: typeof value === 'string' ? value.trim() : String(value ?? ''),
|
||||
}))
|
||||
)
|
||||
} catch (err) {
|
||||
setAttachmentGroupsError(err.message)
|
||||
} finally {
|
||||
setAttachmentSettingsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogoUpload = async (file, settingKey) => {
|
||||
if (!file) return
|
||||
setGeneralUploading(true)
|
||||
@@ -1656,44 +1689,60 @@ export default function Acp({ isAdmin }) {
|
||||
className="bb-attachment-extension-table"
|
||||
style={{ marginLeft: (depth + 1) * 16 }}
|
||||
>
|
||||
<div className="bb-attachment-extension-header tr-header">
|
||||
<span>{t('attachment.extension')}</span>
|
||||
<span>{t('attachment.allowed_mimes')}</span>
|
||||
<span>{t('attachment.extension_group')}</span>
|
||||
<span>{t('attachment.actions')}</span>
|
||||
</div>
|
||||
{groupExtensions.map((extension) => (
|
||||
<div key={extension.id} className="bb-attachment-extension-row">
|
||||
<span className="bb-attachment-extension-name">
|
||||
{extension.extension}
|
||||
</span>
|
||||
<span className="bb-attachment-extension-meta">
|
||||
{(extension.allowed_mimes || []).join(', ')}
|
||||
</span>
|
||||
<span className="bb-attachment-extension-meta">
|
||||
{attachmentGroups.find((group) => group.id === extension.attachment_group_id)?.name
|
||||
|| t('attachment.extension_unassigned')}
|
||||
</span>
|
||||
<div className="bb-attachment-extension-actions">
|
||||
<ButtonGroup size="sm" className="bb-action-group">
|
||||
<Button
|
||||
variant="dark"
|
||||
onClick={() => openAttachmentExtensionEdit(extension)}
|
||||
disabled={attachmentExtensionSaving}
|
||||
>
|
||||
<i className="bi bi-pencil" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="dark"
|
||||
onClick={() => handleAttachmentExtensionDelete(extension)}
|
||||
disabled={attachmentExtensionSaving}
|
||||
>
|
||||
<i className="bi bi-trash" aria-hidden="true" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<table className="table table-sm mb-0">
|
||||
<thead className="tr-header">
|
||||
<tr>
|
||||
<th scope="col" className="text-start">
|
||||
{t('attachment.extension')}
|
||||
</th>
|
||||
<th scope="col" className="text-start">
|
||||
{t('attachment.allowed_mimes')}
|
||||
</th>
|
||||
<th scope="col" className="text-start">
|
||||
{t('attachment.extension_group')}
|
||||
</th>
|
||||
<th scope="col" className="text-start">
|
||||
{t('attachment.actions')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupExtensions.map((extension) => (
|
||||
<tr key={extension.id} className="bb-attachment-extension-row">
|
||||
<td className="bb-attachment-extension-name text-start">
|
||||
{extension.extension}
|
||||
</td>
|
||||
<td className="bb-attachment-extension-meta text-start">
|
||||
{(extension.allowed_mimes || []).join(', ')}
|
||||
</td>
|
||||
<td className="bb-attachment-extension-meta text-start">
|
||||
{attachmentGroups.find((group) => group.id === extension.attachment_group_id)?.name
|
||||
|| t('attachment.extension_unassigned')}
|
||||
</td>
|
||||
<td>
|
||||
<div className="bb-attachment-extension-actions">
|
||||
<ButtonGroup size="sm" className="bb-action-group">
|
||||
<Button
|
||||
variant="dark"
|
||||
onClick={() => openAttachmentExtensionEdit(extension)}
|
||||
disabled={attachmentExtensionSaving}
|
||||
>
|
||||
<i className="bi bi-pencil" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="dark"
|
||||
onClick={() => handleAttachmentExtensionDelete(extension)}
|
||||
disabled={attachmentExtensionSaving}
|
||||
>
|
||||
<i className="bi bi-trash" aria-hidden="true" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -2143,297 +2192,376 @@ export default function Acp({ isAdmin }) {
|
||||
<h2 className="mb-4">{t('acp.title')}</h2>
|
||||
<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.forum_name}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({ ...prev, forum_name: 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.show_header_name !== 'false'}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
show_header_name: event.target.checked ? 'true' : 'false',
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.default_theme')}</Form.Label>
|
||||
<Form.Select
|
||||
value={generalSettings.default_theme}
|
||||
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.accent_color_dark}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
accent_color_dark: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="#f29b3f"
|
||||
/>
|
||||
<Form.Control
|
||||
type="color"
|
||||
value={generalSettings.accent_color_dark || '#f29b3f'}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
accent_color_dark: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Row className="g-4">
|
||||
<Col lg={3} xl={2}>
|
||||
<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>
|
||||
</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.accent_color_light}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
accent_color_light: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="#f29b3f"
|
||||
/>
|
||||
<Form.Control
|
||||
type="color"
|
||||
value={generalSettings.accent_color_light || '#f29b3f'}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
accent_color_light: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
</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.logo_dark ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.logo_dark} 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>
|
||||
<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>
|
||||
</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.logo_light ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.logo_light} 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>
|
||||
<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>
|
||||
</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.favicon_ico ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_ico} 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.favicon_16 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_16} 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.favicon_32 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_32} 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.favicon_48 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_48} 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.favicon_64 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_64} 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.favicon_128 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_128} 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.favicon_256 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_256} 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>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col lg={9} xl={10}>
|
||||
<div className="bb-acp-panel mb-4">
|
||||
<div className="bb-acp-panel-header">
|
||||
<h5 className="mb-1">{t('acp.welcome_title')}</h5>
|
||||
<p className="bb-muted mb-0">{t('acp.general_hint')}</p>
|
||||
</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">
|
||||
<Col lg={6}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.forum_name')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={generalSettings.forum_name}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
forum_name: 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.show_header_name !== 'false'}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
show_header_name: event.target.checked ? 'true' : 'false',
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('acp.default_theme')}</Form.Label>
|
||||
<Form.Select
|
||||
value={generalSettings.default_theme}
|
||||
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.accent_color_dark}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
accent_color_dark: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="#f29b3f"
|
||||
/>
|
||||
<Form.Control
|
||||
type="color"
|
||||
value={generalSettings.accent_color_dark || '#f29b3f'}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
accent_color_dark: 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.accent_color_light}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
accent_color_light: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="#f29b3f"
|
||||
/>
|
||||
<Form.Control
|
||||
type="color"
|
||||
value={generalSettings.accent_color_light || '#f29b3f'}
|
||||
onChange={(event) =>
|
||||
setGeneralSettings((prev) => ({
|
||||
...prev,
|
||||
accent_color_light: 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.logo_dark ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.logo_dark} 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.logo_light ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.logo_light} 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.favicon_ico ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_ico} 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.favicon_16 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_16} 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.favicon_32 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_32} 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.favicon_48 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_48} 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.favicon_64 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_64} 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.favicon_128 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_128} 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.favicon_256 ? (
|
||||
<div className="bb-dropzone-preview">
|
||||
<img src={generalSettings.favicon_256} 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>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Tab>
|
||||
<Tab eventKey="forums" title={t('acp.forums')}>
|
||||
<p className="bb-muted">{t('acp.forums_hint')}</p>
|
||||
@@ -2702,6 +2830,95 @@ export default function Acp({ isAdmin }) {
|
||||
{attachmentGroupsError && <p className="text-danger">{attachmentGroupsError}</p>}
|
||||
{attachmentExtensionsError && <p className="text-danger">{attachmentExtensionsError}</p>}
|
||||
<div className="bb-attachment-admin">
|
||||
<div className="bb-attachment-admin-section">
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 className="mb-0">{t('attachment.settings_title')}</h5>
|
||||
</div>
|
||||
<Form onSubmit={handleAttachmentSettingsSave}>
|
||||
<div className="row g-3 align-items-end">
|
||||
<div className="col-12 col-md-6">
|
||||
<Form.Check
|
||||
type="switch"
|
||||
id="attachment-display-inline"
|
||||
label={t('attachment.display_images_inline')}
|
||||
checked={attachmentSettings.display_images_inline !== 'false'}
|
||||
onChange={(event) =>
|
||||
setAttachmentSettings((prev) => ({
|
||||
...prev,
|
||||
display_images_inline: event.target.checked ? 'true' : 'false',
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<Form.Check
|
||||
type="switch"
|
||||
id="attachment-create-thumbnails"
|
||||
label={t('attachment.create_thumbnails')}
|
||||
checked={attachmentSettings.create_thumbnails !== 'false'}
|
||||
onChange={(event) =>
|
||||
setAttachmentSettings((prev) => ({
|
||||
...prev,
|
||||
create_thumbnails: event.target.checked ? 'true' : 'false',
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<Form.Label>{t('attachment.thumbnail_max_width')}</Form.Label>
|
||||
<Form.Control
|
||||
type="number"
|
||||
min="0"
|
||||
value={attachmentSettings.thumbnail_max_width}
|
||||
onChange={(event) =>
|
||||
setAttachmentSettings((prev) => ({
|
||||
...prev,
|
||||
thumbnail_max_width: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<Form.Label>{t('attachment.thumbnail_max_height')}</Form.Label>
|
||||
<Form.Control
|
||||
type="number"
|
||||
min="0"
|
||||
value={attachmentSettings.thumbnail_max_height}
|
||||
onChange={(event) =>
|
||||
setAttachmentSettings((prev) => ({
|
||||
...prev,
|
||||
thumbnail_max_height: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<Form.Label>{t('attachment.thumbnail_quality')}</Form.Label>
|
||||
<Form.Control
|
||||
type="number"
|
||||
min="10"
|
||||
max="95"
|
||||
value={attachmentSettings.thumbnail_quality}
|
||||
onChange={(event) =>
|
||||
setAttachmentSettings((prev) => ({
|
||||
...prev,
|
||||
thumbnail_quality: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Button
|
||||
type="submit"
|
||||
className="bb-accent-button"
|
||||
disabled={attachmentSettingsSaving}
|
||||
>
|
||||
{attachmentSettingsSaving ? t('form.saving') : t('acp.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
<div className="bb-attachment-admin-section">
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 className="mb-0">{t('attachment.groups_title')}</h5>
|
||||
|
||||
@@ -34,6 +34,7 @@ export default function ThreadView() {
|
||||
const [previewHtml, setPreviewHtml] = useState('')
|
||||
const [previewLoading, setPreviewLoading] = useState(false)
|
||||
const [previewUrls, setPreviewUrls] = useState([])
|
||||
const [lightboxImage, setLightboxImage] = useState('')
|
||||
const [replyAttachmentTab, setReplyAttachmentTab] = useState('options')
|
||||
const [replyAttachmentOptions, setReplyAttachmentOptions] = useState({
|
||||
disableBbcode: false,
|
||||
@@ -452,19 +453,40 @@ export default function ThreadView() {
|
||||
return (
|
||||
<div className="bb-attachment-list">
|
||||
{attachments.map((attachment) => (
|
||||
<a
|
||||
key={attachment.id}
|
||||
href={attachment.download_url}
|
||||
className="bb-attachment-item"
|
||||
download
|
||||
>
|
||||
<i className="bi bi-paperclip" aria-hidden="true" />
|
||||
<span className="bb-attachment-name">{attachment.original_name}</span>
|
||||
<span className="bb-attachment-meta">
|
||||
{attachment.mime_type}
|
||||
{attachment.size_bytes ? ` · ${formatBytes(attachment.size_bytes)}` : ''}
|
||||
</span>
|
||||
</a>
|
||||
attachment.is_image ? (
|
||||
<button
|
||||
key={attachment.id}
|
||||
type="button"
|
||||
className="bb-attachment-item border-0 text-start"
|
||||
onClick={() => setLightboxImage(attachment.download_url)}
|
||||
>
|
||||
<img
|
||||
src={attachment.thumbnail_url || attachment.download_url}
|
||||
alt={attachment.original_name}
|
||||
className="img-fluid rounded"
|
||||
style={{ width: 72, height: 72, objectFit: 'cover' }}
|
||||
/>
|
||||
<span className="bb-attachment-name">{attachment.original_name}</span>
|
||||
<span className="bb-attachment-meta">
|
||||
{attachment.mime_type}
|
||||
{attachment.size_bytes ? ` · ${formatBytes(attachment.size_bytes)}` : ''}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
key={attachment.id}
|
||||
href={attachment.download_url}
|
||||
className="bb-attachment-item"
|
||||
download
|
||||
>
|
||||
<i className="bi bi-paperclip" aria-hidden="true" />
|
||||
<span className="bb-attachment-name">{attachment.original_name}</span>
|
||||
<span className="bb-attachment-meta">
|
||||
{attachment.mime_type}
|
||||
{attachment.size_bytes ? ` · ${formatBytes(attachment.size_bytes)}` : ''}
|
||||
</span>
|
||||
</a>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@@ -727,6 +749,12 @@ export default function ThreadView() {
|
||||
</div>
|
||||
<div
|
||||
className="bb-post-body"
|
||||
onClick={(event) => {
|
||||
if (event.target?.tagName === 'IMG') {
|
||||
event.preventDefault()
|
||||
setLightboxImage(event.target.src)
|
||||
}
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: post.body_html || post.body }}
|
||||
/>
|
||||
{renderAttachments(post.attachments)}
|
||||
@@ -852,6 +880,18 @@ export default function ThreadView() {
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
<Modal
|
||||
show={Boolean(lightboxImage)}
|
||||
onHide={() => setLightboxImage('')}
|
||||
centered
|
||||
size="lg"
|
||||
>
|
||||
<Modal.Body className="text-center">
|
||||
{lightboxImage && (
|
||||
<img src={lightboxImage} alt="" className="img-fluid rounded" />
|
||||
)}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user