Add asset import/export and local dev server setup
CI/CD Pipeline / deploy (push) Successful in 38s
CI/CD Pipeline / promote_stable (push) Successful in 2s

- Configure Vite dev server with localhost binding and public asset proxy
- Add npm scripts for concurrent Laravel/Vite development (dev:local, dev:test)
- Implement asset import/export in ACP via ZIP file upload/download
- Create AssetController for asset management endpoints
- Add asset management UI tab in admin panel
This commit is contained in:
2026-05-16 16:33:22 +02:00
parent a2fe31925f
commit d4d7934c89
7 changed files with 280 additions and 0 deletions
+116
View File
@@ -43,6 +43,8 @@ import {
createAttachmentExtension,
updateAttachmentExtension,
deleteAttachmentExtension,
exportAssets,
importAssets,
} from '../api/client'
const StatusIcon = ({ status = 'bad', tooltip }) => {
@@ -222,6 +224,10 @@ function Acp({ isAdmin }) {
const [systemCliChecking, setSystemCliChecking] = useState(false)
const [systemCliError, setSystemCliError] = useState('')
const [systemCliToast, setSystemCliToast] = useState({ show: false, variant: 'success', message: '' })
const [assetExporting, setAssetExporting] = useState(false)
const [assetImporting, setAssetImporting] = useState(false)
const [assetError, setAssetError] = useState('')
const [assetToast, setAssetToast] = useState({ show: false, variant: 'success', message: '' })
const settingsDetailMap = {
forum_name: 'forumName',
default_theme: 'defaultTheme',
@@ -787,6 +793,44 @@ function Acp({ isAdmin }) {
}
}
const handleAssetExport = async () => {
setAssetExporting(true)
setAssetError('')
try {
await exportAssets()
setAssetToast({ show: true, variant: 'success', message: 'Assets exported successfully' })
} catch (err) {
setAssetError(err.message)
setAssetToast({ show: true, variant: 'danger', message: 'Failed to export assets' })
} finally {
setAssetExporting(false)
}
}
const handleAssetImport = async (file) => {
if (!file) return
setAssetImporting(true)
setAssetError('')
try {
await importAssets(file)
setAssetToast({ show: true, variant: 'success', message: 'Assets imported successfully' })
window.location.reload()
} catch (err) {
setAssetError(err.message)
setAssetToast({ show: true, variant: 'danger', message: 'Failed to import assets' })
} finally {
setAssetImporting(false)
}
}
const assetImportDropzone = useDropzone({
accept: {
'application/zip': ['.zip'],
},
maxFiles: 1,
onDrop: (files) => handleAssetImport(files[0]),
})
const faviconIcoDropzone = useDropzone({
accept: {
'image/png': ['.png'],
@@ -4337,6 +4381,78 @@ function Acp({ isAdmin }) {
</Col>
</Row>
</Tab>
<Tab eventKey="assets" title="Assets">
<Row className="g-4">
<Col xs={12}>
<div className="bb-acp-panel">
<div className="bb-acp-panel-header">
<h5 className="mb-0">Asset Management</h5>
</div>
<div className="bb-acp-panel-body">
{assetToast.show && (
<div
className={`alert alert-${assetToast.variant} mb-3`}
role="alert"
>
{assetToast.message}
</div>
)}
{assetError && (
<div className="alert alert-danger mb-3" role="alert">
{assetError}
</div>
)}
<div className="row mb-4">
<div className="col-md-6">
<h6 className="mb-2">Export Assets</h6>
<p className="text-muted mb-3">
Download all logos, favicons, and other assets as a ZIP file.
</p>
<Button
variant="primary"
onClick={handleAssetExport}
disabled={assetExporting}
>
{assetExporting ? 'Exporting...' : 'Export Assets'}
</Button>
</div>
<div className="col-md-6">
<h6 className="mb-2">Import Assets</h6>
<p className="text-muted mb-3">
Upload a ZIP file containing assets to import them.
</p>
<div
{...assetImportDropzone.getRootProps()}
className="border border-dashed rounded p-4 text-center"
style={{
cursor: 'pointer',
backgroundColor: assetImportDropzone.isDragActive
? 'rgba(0, 0, 0, 0.05)'
: 'transparent',
}}
>
<input {...assetImportDropzone.getInputProps()} />
{assetImporting ? (
<p className="mb-0">Importing assets...</p>
) : (
<>
<i
className="bi bi-cloud-upload"
style={{ fontSize: '2rem', marginBottom: '0.5rem' }}
></i>
<p className="mb-0">
Drag and drop a ZIP file here, or click to select
</p>
</>
)}
</div>
</div>
</div>
</div>
</div>
</Col>
</Row>
</Tab>
</Tabs>
<Modal show={showModal} onHide={handleReset} centered size="lg">
<Modal.Header closeButton closeVariant="white">