683 lines
34 KiB
PHP
683 lines
34 KiB
PHP
|
|
<?php
|
||
|
|
$pageTitle = 'Settings';
|
||
|
|
ob_start();
|
||
|
|
?>
|
||
|
|
<section class="admin-card settings-shell">
|
||
|
|
<div class="badge">Settings</div>
|
||
|
|
<h1 style="margin-top:16px; font-size:28px;">Site Settings</h1>
|
||
|
|
<p style="color: var(--muted); margin-top:8px;">Configure branding, maintenance, icons, and integrations.</p>
|
||
|
|
<?php if (!empty($status_message ?? '')): ?>
|
||
|
|
<div class="settings-status <?= (($status ?? '') === 'ok') ? 'is-ok' : 'is-error' ?>">
|
||
|
|
<?= htmlspecialchars((string)$status_message, ENT_QUOTES, 'UTF-8') ?>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<form method="post" action="/admin/settings" enctype="multipart/form-data" style="margin-top:20px; display:grid; gap:16px;">
|
||
|
|
<div class="settings-tabs" role="tablist" aria-label="Settings sections">
|
||
|
|
<button type="button" class="settings-tab is-active" data-tab="branding" role="tab" aria-selected="true">Branding</button>
|
||
|
|
<button type="button" class="settings-tab" data-tab="footer" role="tab" aria-selected="false">Footer</button>
|
||
|
|
<button type="button" class="settings-tab" data-tab="maintenance" role="tab" aria-selected="false">Maintenance</button>
|
||
|
|
<button type="button" class="settings-tab" data-tab="icons" role="tab" aria-selected="false">Icons</button>
|
||
|
|
<button type="button" class="settings-tab" data-tab="smtp" role="tab" aria-selected="false">SMTP</button>
|
||
|
|
<button type="button" class="settings-tab" data-tab="mailchimp" role="tab" aria-selected="false">Mailchimp</button>
|
||
|
|
<button type="button" class="settings-tab" data-tab="seo" role="tab" aria-selected="false">SEO</button>
|
||
|
|
<button type="button" class="settings-tab" data-tab="redirects" role="tab" aria-selected="false">Redirects</button>
|
||
|
|
<button type="button" class="settings-tab" data-tab="permissions" role="tab" aria-selected="false">Permissions</button>
|
||
|
|
<button type="button" class="settings-tab" data-tab="audit" role="tab" aria-selected="false">Audit Log</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="settings-panel is-active" data-panel="branding" role="tabpanel">
|
||
|
|
<div class="admin-card" style="padding:16px;">
|
||
|
|
<div class="badge" style="opacity:0.7;">Branding</div>
|
||
|
|
<div style="margin-top:12px; display:grid; gap:12px;">
|
||
|
|
<label class="label">Header Title</label>
|
||
|
|
<input class="input" name="site_header_title" value="<?= htmlspecialchars($site_header_title ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="AudioCore V1.5">
|
||
|
|
|
||
|
|
<label class="label">Header Tagline</label>
|
||
|
|
<input class="input" name="site_header_tagline" value="<?= htmlspecialchars($site_header_tagline ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="Core CMS for DJs & Producers">
|
||
|
|
|
||
|
|
<label class="label">Header Badge Text (right side)</label>
|
||
|
|
<input class="input" name="site_header_badge_text" value="<?= htmlspecialchars($site_header_badge_text ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="Independent catalog">
|
||
|
|
|
||
|
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
|
||
|
|
<div>
|
||
|
|
<label class="label">Header Left Mode</label>
|
||
|
|
<select class="input" name="site_header_brand_mode">
|
||
|
|
<option value="default" <?= (($site_header_brand_mode ?? 'default') === 'default') ? 'selected' : '' ?>>Text + mark</option>
|
||
|
|
<option value="logo_only" <?= (($site_header_brand_mode ?? '') === 'logo_only') ? 'selected' : '' ?>>Logo only</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label class="label">Mark Content</label>
|
||
|
|
<select class="input" name="site_header_mark_mode">
|
||
|
|
<option value="text" <?= (($site_header_mark_mode ?? 'text') === 'text') ? 'selected' : '' ?>>Text</option>
|
||
|
|
<option value="icon" <?= (($site_header_mark_mode ?? '') === 'icon') ? 'selected' : '' ?>>Font Awesome icon</option>
|
||
|
|
<option value="logo" <?= (($site_header_mark_mode ?? '') === 'logo') ? 'selected' : '' ?>>Logo in mark</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
|
||
|
|
<div>
|
||
|
|
<label class="label">Mark Text</label>
|
||
|
|
<input class="input" name="site_header_mark_text" value="<?= htmlspecialchars($site_header_mark_text ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="AC">
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label class="label">Mark Icon Class</label>
|
||
|
|
<input class="input" name="site_header_mark_icon" value="<?= htmlspecialchars($site_header_mark_icon ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="fa-solid fa-music">
|
||
|
|
<div style="font-size:12px; color:var(--muted); margin-top:6px;">Use class only (or paste full <i ...> and it will be normalized).</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
|
||
|
|
<div>
|
||
|
|
<label class="label">Mark Gradient Start</label>
|
||
|
|
<input class="input" name="site_header_mark_bg_start" value="<?= htmlspecialchars($site_header_mark_bg_start ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="#22f2a5">
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label class="label">Mark Gradient End</label>
|
||
|
|
<input class="input" name="site_header_mark_bg_end" value="<?= htmlspecialchars($site_header_mark_bg_end ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="#10252e">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<label class="label">Logo URL (for logo-only or mark logo mode)</label>
|
||
|
|
<input class="input" name="site_header_logo_url" value="<?= htmlspecialchars($site_header_logo_url ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="https://example.com/logo.png">
|
||
|
|
<div class="settings-logo-tools">
|
||
|
|
<div class="settings-logo-preview">
|
||
|
|
<?php if (!empty($site_header_logo_url ?? '')): ?>
|
||
|
|
<img src="<?= htmlspecialchars((string)$site_header_logo_url, ENT_QUOTES, 'UTF-8') ?>" alt="">
|
||
|
|
<?php else: ?>
|
||
|
|
<span>No logo set</span>
|
||
|
|
<?php endif; ?>
|
||
|
|
</div>
|
||
|
|
<div class="settings-logo-actions">
|
||
|
|
<button type="button" class="btn outline" id="openLogoMediaPicker">Use from Media</button>
|
||
|
|
<button type="submit" class="btn outline danger" name="settings_action" value="remove_logo">Remove current logo</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="admin-card" style="padding:12px; margin-top:4px;">
|
||
|
|
<div class="label" style="margin-bottom:8px;">Upload Logo</div>
|
||
|
|
<label class="settings-upload-dropzone" for="headerLogoFile">
|
||
|
|
<input id="headerLogoFile" class="settings-file-input" type="file" name="header_logo_file" accept="image/*,.svg">
|
||
|
|
<div class="settings-upload-text">
|
||
|
|
<div style="font-size:11px; letter-spacing:0.2em; text-transform:uppercase; color:rgba(255,255,255,0.6);">Drag & Drop</div>
|
||
|
|
<div style="font-size:14px; color:var(--text);">or click to upload</div>
|
||
|
|
<div class="settings-file-name" id="headerLogoFileName">No file selected</div>
|
||
|
|
</div>
|
||
|
|
</label>
|
||
|
|
<div style="display:flex; justify-content:flex-end; margin-top:10px;">
|
||
|
|
<button type="submit" class="btn" name="settings_action" value="upload_logo">Upload logo</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="settings-panel" data-panel="footer" role="tabpanel" hidden>
|
||
|
|
<div class="admin-card" style="padding:16px;">
|
||
|
|
<div class="badge" style="opacity:0.7;">Footer</div>
|
||
|
|
<div style="margin-top:12px; display:grid; gap:12px;">
|
||
|
|
<label class="label">Footer Text</label>
|
||
|
|
<input class="input" name="footer_text" value="<?= htmlspecialchars($footer_text ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="AudioCore V1.5">
|
||
|
|
<div style="font-size:12px; color:var(--muted);">Shown in the site footer.</div>
|
||
|
|
|
||
|
|
<div class="label">Footer Links</div>
|
||
|
|
<div class="admin-card" style="padding:12px;">
|
||
|
|
<div id="footerLinksList" style="display:grid; gap:8px;"></div>
|
||
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:10px; gap:8px; flex-wrap:wrap;">
|
||
|
|
<button type="button" class="btn outline" id="addFooterLinkRow">Add footer link</button>
|
||
|
|
<div style="font-size:12px; color:var(--muted);">Examples: Privacy, Terms, Refund Policy.</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<input type="hidden" name="footer_links_json" id="footerLinksJson" value="">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="settings-panel" data-panel="maintenance" role="tabpanel" hidden>
|
||
|
|
<div class="admin-card" style="padding:16px;">
|
||
|
|
<div class="badge" style="opacity:0.7;">Coming Soon / Maintenance</div>
|
||
|
|
<div style="margin-top:12px; display:grid; gap:12px;">
|
||
|
|
<label style="display:inline-flex; align-items:center; gap:8px; font-size:13px;">
|
||
|
|
<input type="checkbox" name="site_maintenance_enabled" value="1" <?= (($site_maintenance_enabled ?? '0') === '1') ? 'checked' : '' ?>>
|
||
|
|
Enable maintenance mode for visitors (admins still see full site when logged in)
|
||
|
|
</label>
|
||
|
|
<label class="label">Title</label>
|
||
|
|
<input class="input" name="site_maintenance_title" value="<?= htmlspecialchars($site_maintenance_title ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="Coming Soon">
|
||
|
|
<label class="label">Message</label>
|
||
|
|
<textarea class="input" name="site_maintenance_message" rows="3" style="resize:vertical;"><?= htmlspecialchars($site_maintenance_message ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
|
||
|
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
|
||
|
|
<div>
|
||
|
|
<label class="label">Button Label (optional)</label>
|
||
|
|
<input class="input" name="site_maintenance_button_label" value="<?= htmlspecialchars($site_maintenance_button_label ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="Admin Login">
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label class="label">Button URL (optional)</label>
|
||
|
|
<input class="input" name="site_maintenance_button_url" value="<?= htmlspecialchars($site_maintenance_button_url ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="/admin/login">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<label class="label">Custom HTML (optional, overrides title/message layout)</label>
|
||
|
|
<textarea class="input" name="site_maintenance_html" rows="6" style="resize:vertical; font-family:'IBM Plex Mono', monospace;"><?= htmlspecialchars($site_maintenance_html ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="settings-panel" data-panel="icons" role="tabpanel" hidden>
|
||
|
|
<div class="admin-card" style="padding:16px;">
|
||
|
|
<div class="badge" style="opacity:0.7;">Icons</div>
|
||
|
|
<div style="margin-top:12px; display:grid; gap:12px;">
|
||
|
|
<label class="label">Font Awesome Pro URL</label>
|
||
|
|
<input class="input" name="fontawesome_pro_url" value="<?= htmlspecialchars($fontawesome_pro_url ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="https://kit.fontawesome.com/your-kit-id.css">
|
||
|
|
<div style="font-size:12px; color:var(--muted);">Use your Pro kit URL to enable duotone icons.</div>
|
||
|
|
<label class="label">Font Awesome URL (Fallback)</label>
|
||
|
|
<input class="input" name="fontawesome_url" value="<?= htmlspecialchars($fontawesome_url ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
||
|
|
<div style="font-size:12px; color:var(--muted);">Used if Pro URL is empty.</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="settings-panel" data-panel="smtp" role="tabpanel" hidden>
|
||
|
|
<div class="admin-card" style="padding:16px;">
|
||
|
|
<div class="badge" style="opacity:0.7;">SMTP</div>
|
||
|
|
<div style="margin-top:12px; display:grid; gap:12px;">
|
||
|
|
<label class="label">SMTP Host</label>
|
||
|
|
<input class="input" name="smtp_host" value="<?= htmlspecialchars($smtp_host ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="smtp.example.com">
|
||
|
|
<label class="label">SMTP Port</label>
|
||
|
|
<input class="input" name="smtp_port" value="<?= htmlspecialchars($smtp_port ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="587">
|
||
|
|
<label class="label">SMTP User</label>
|
||
|
|
<input class="input" name="smtp_user" value="<?= htmlspecialchars($smtp_user ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="user@example.com">
|
||
|
|
<label class="label">SMTP Password</label>
|
||
|
|
<input class="input" type="password" name="smtp_pass" value="<?= htmlspecialchars($smtp_pass ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="password">
|
||
|
|
<label class="label">SMTP Encryption</label>
|
||
|
|
<input class="input" name="smtp_encryption" value="<?= htmlspecialchars($smtp_encryption ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="tls">
|
||
|
|
<label class="label">From Email</label>
|
||
|
|
<input class="input" name="smtp_from_email" value="<?= htmlspecialchars($smtp_from_email ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="no-reply@example.com">
|
||
|
|
<label class="label">From Name</label>
|
||
|
|
<input class="input" name="smtp_from_name" value="<?= htmlspecialchars($smtp_from_name ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="AudioCore">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="settings-panel" data-panel="mailchimp" role="tabpanel" hidden>
|
||
|
|
<div class="admin-card" style="padding:16px;">
|
||
|
|
<div class="badge" style="opacity:0.7;">Mailchimp</div>
|
||
|
|
<div style="margin-top:12px; display:grid; gap:12px;">
|
||
|
|
<label class="label">API Key</label>
|
||
|
|
<input class="input" name="mailchimp_api_key" value="<?= htmlspecialchars($mailchimp_api_key ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="xxxx-us1">
|
||
|
|
<label class="label">List ID</label>
|
||
|
|
<input class="input" name="mailchimp_list_id" value="<?= htmlspecialchars($mailchimp_list_id ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="abcd1234">
|
||
|
|
<div style="font-size:12px; color:var(--muted);">Used for syncing subscriber signups.</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="settings-panel" data-panel="seo" role="tabpanel" hidden>
|
||
|
|
<div class="admin-card" style="padding:16px;">
|
||
|
|
<div class="badge" style="opacity:0.7;">Global SEO</div>
|
||
|
|
<div style="margin-top:12px; display:grid; gap:12px;">
|
||
|
|
<label class="label">Title Suffix</label>
|
||
|
|
<input class="input" name="seo_title_suffix" value="<?= htmlspecialchars($seo_title_suffix ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="AudioCore V1.5">
|
||
|
|
<label class="label">Default Meta Description</label>
|
||
|
|
<textarea class="input" name="seo_meta_description" rows="3" style="resize:vertical;"><?= htmlspecialchars($seo_meta_description ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
|
||
|
|
<label class="label">Open Graph Image URL</label>
|
||
|
|
<input class="input" name="seo_og_image" value="<?= htmlspecialchars($seo_og_image ?? '', ENT_QUOTES, 'UTF-8') ?>" placeholder="https://example.com/og-image.jpg">
|
||
|
|
<div style="display:flex; gap:20px; flex-wrap:wrap;">
|
||
|
|
<label style="display:inline-flex; align-items:center; gap:8px; font-size:13px;">
|
||
|
|
<input type="checkbox" name="seo_robots_index" value="1" <?= (($seo_robots_index ?? '1') === '1') ? 'checked' : '' ?>>
|
||
|
|
Allow indexing
|
||
|
|
</label>
|
||
|
|
<label style="display:inline-flex; align-items:center; gap:8px; font-size:13px;">
|
||
|
|
<input type="checkbox" name="seo_robots_follow" value="1" <?= (($seo_robots_follow ?? '1') === '1') ? 'checked' : '' ?>>
|
||
|
|
Allow link following
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="settings-panel" data-panel="redirects" role="tabpanel" hidden>
|
||
|
|
<div class="admin-card" style="padding:16px; display:grid; gap:12px;">
|
||
|
|
<div class="badge" style="opacity:0.7;">Redirects Manager</div>
|
||
|
|
<div style="font-size:12px; color:var(--muted);">Exact-path redirects. Example source <code>/old-page</code> → target <code>/new-page</code>.</div>
|
||
|
|
<div style="display:grid; gap:10px;">
|
||
|
|
<input class="input" name="redirect_source_path" placeholder="/old-url">
|
||
|
|
<input class="input" name="redirect_target_url" placeholder="/new-url or https://external.site/path">
|
||
|
|
<div style="display:grid; grid-template-columns: 180px 1fr auto; gap:10px; align-items:center;">
|
||
|
|
<select class="input" name="redirect_status_code">
|
||
|
|
<option value="301">301 Permanent</option>
|
||
|
|
<option value="302">302 Temporary</option>
|
||
|
|
<option value="307">307 Temporary</option>
|
||
|
|
<option value="308">308 Permanent</option>
|
||
|
|
</select>
|
||
|
|
<label style="display:inline-flex; align-items:center; gap:8px; font-size:13px;">
|
||
|
|
<input type="checkbox" name="redirect_is_active" value="1" checked>
|
||
|
|
Active
|
||
|
|
</label>
|
||
|
|
<button type="submit" class="btn" name="settings_action" value="save_redirect">Save redirect</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="admin-card" style="padding:12px;">
|
||
|
|
<div class="badge" style="margin-bottom:8px;">Existing Redirects</div>
|
||
|
|
<?php if (empty($redirects ?? [])): ?>
|
||
|
|
<div style="color:var(--muted); font-size:13px;">No redirects configured.</div>
|
||
|
|
<?php else: ?>
|
||
|
|
<div style="display:grid; gap:8px;">
|
||
|
|
<?php foreach (($redirects ?? []) as $redirect): ?>
|
||
|
|
<div style="display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto auto auto; gap:10px; align-items:center; border:1px solid rgba(255,255,255,0.1); border-radius:10px; padding:8px 10px;">
|
||
|
|
<div style="font-family:'IBM Plex Mono', monospace; font-size:12px;"><?= htmlspecialchars((string)$redirect['source_path'], ENT_QUOTES, 'UTF-8') ?></div>
|
||
|
|
<div style="font-family:'IBM Plex Mono', monospace; font-size:12px; color:var(--muted);"><?= htmlspecialchars((string)$redirect['target_url'], ENT_QUOTES, 'UTF-8') ?></div>
|
||
|
|
<div style="font-size:12px; color:var(--muted);"><?= (int)$redirect['status_code'] ?></div>
|
||
|
|
<div style="font-size:12px; color:<?= ((int)($redirect['is_active'] ?? 0) === 1) ? '#9df6d3' : '#ffb7c2' ?>;"><?= ((int)($redirect['is_active'] ?? 0) === 1) ? 'active' : 'inactive' ?></div>
|
||
|
|
<button type="submit" class="btn outline danger" name="settings_action" value="delete_redirect" onclick="document.getElementById('redirectDeleteId').value='<?= (int)$redirect['id'] ?>';">Delete</button>
|
||
|
|
</div>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
<input type="hidden" name="redirect_id" id="redirectDeleteId" value="0">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="settings-panel" data-panel="permissions" role="tabpanel" hidden>
|
||
|
|
<div class="admin-card" style="padding:16px;">
|
||
|
|
<div class="badge" style="opacity:0.7;">Role Permissions Matrix</div>
|
||
|
|
<div style="margin-top:10px; font-size:12px; color:var(--muted);">Plugin/module-level restrictions by admin role.</div>
|
||
|
|
<div style="margin-top:12px; overflow:auto;">
|
||
|
|
<table style="width:100%; border-collapse:collapse; min-width:640px;">
|
||
|
|
<thead>
|
||
|
|
<tr style="text-align:left; border-bottom:1px solid rgba(255,255,255,0.12);">
|
||
|
|
<th style="padding:10px 8px; font-size:12px; letter-spacing:0.14em; text-transform:uppercase;">Permission</th>
|
||
|
|
<th style="padding:10px 8px; font-size:12px; letter-spacing:0.14em; text-transform:uppercase;">Admin</th>
|
||
|
|
<th style="padding:10px 8px; font-size:12px; letter-spacing:0.14em; text-transform:uppercase;">Manager</th>
|
||
|
|
<th style="padding:10px 8px; font-size:12px; letter-spacing:0.14em; text-transform:uppercase;">Editor</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<?php $currentGroup = ''; ?>
|
||
|
|
<?php foreach (($permission_definitions ?? []) as $permission): ?>
|
||
|
|
<?php
|
||
|
|
$pKey = (string)($permission['key'] ?? '');
|
||
|
|
$pLabel = (string)($permission['label'] ?? $pKey);
|
||
|
|
$pGroup = (string)($permission['group'] ?? 'Other');
|
||
|
|
$row = $permission_matrix[$pKey] ?? ['admin' => true, 'manager' => false, 'editor' => false];
|
||
|
|
?>
|
||
|
|
<?php if ($pGroup !== $currentGroup): $currentGroup = $pGroup; ?>
|
||
|
|
<tr><td colspan="4" style="padding:12px 8px 6px; color:var(--muted); font-size:11px; letter-spacing:0.22em; text-transform:uppercase;"><?= htmlspecialchars($pGroup, ENT_QUOTES, 'UTF-8') ?></td></tr>
|
||
|
|
<?php endif; ?>
|
||
|
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.08);">
|
||
|
|
<td style="padding:10px 8px;"><?= htmlspecialchars($pLabel, ENT_QUOTES, 'UTF-8') ?></td>
|
||
|
|
<td style="padding:10px 8px;"><input type="checkbox" name="permissions[<?= htmlspecialchars($pKey, ENT_QUOTES, 'UTF-8') ?>][admin]" value="1" <?= !empty($row['admin']) ? 'checked' : '' ?>></td>
|
||
|
|
<td style="padding:10px 8px;"><input type="checkbox" name="permissions[<?= htmlspecialchars($pKey, ENT_QUOTES, 'UTF-8') ?>][manager]" value="1" <?= !empty($row['manager']) ? 'checked' : '' ?>></td>
|
||
|
|
<td style="padding:10px 8px;"><input type="checkbox" name="permissions[<?= htmlspecialchars($pKey, ENT_QUOTES, 'UTF-8') ?>][editor]" value="1" <?= !empty($row['editor']) ? 'checked' : '' ?>></td>
|
||
|
|
</tr>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
<div style="margin-top:12px; display:flex; justify-content:flex-end;">
|
||
|
|
<button type="submit" class="btn" name="settings_action" value="save_permissions">Save permissions</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="settings-panel" data-panel="audit" role="tabpanel" hidden>
|
||
|
|
<div class="admin-card" style="padding:16px;">
|
||
|
|
<div class="badge" style="opacity:0.7;">Audit Log</div>
|
||
|
|
<div style="margin-top:10px; max-height:460px; overflow:auto; border:1px solid rgba(255,255,255,0.12); border-radius:12px;">
|
||
|
|
<?php if (empty($audit_logs ?? [])): ?>
|
||
|
|
<div style="padding:12px; color:var(--muted); font-size:13px;">No audit events yet.</div>
|
||
|
|
<?php else: ?>
|
||
|
|
<table style="width:100%; border-collapse:collapse; min-width:820px;">
|
||
|
|
<thead>
|
||
|
|
<tr style="text-align:left; border-bottom:1px solid rgba(255,255,255,0.12);">
|
||
|
|
<th style="padding:10px 8px;">Time</th>
|
||
|
|
<th style="padding:10px 8px;">Actor</th>
|
||
|
|
<th style="padding:10px 8px;">Action</th>
|
||
|
|
<th style="padding:10px 8px;">IP</th>
|
||
|
|
<th style="padding:10px 8px;">Context</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<?php foreach (($audit_logs ?? []) as $log): ?>
|
||
|
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.08);">
|
||
|
|
<td style="padding:9px 8px; font-family:'IBM Plex Mono', monospace; font-size:11px;"><?= htmlspecialchars((string)($log['created_at'] ?? ''), ENT_QUOTES, 'UTF-8') ?></td>
|
||
|
|
<td style="padding:9px 8px;"><?= htmlspecialchars(trim((string)($log['actor_name'] ?? 'System') . ' (' . (string)($log['actor_role'] ?? '-') . ')'), ENT_QUOTES, 'UTF-8') ?></td>
|
||
|
|
<td style="padding:9px 8px; font-family:'IBM Plex Mono', monospace; font-size:11px;"><?= htmlspecialchars((string)($log['action'] ?? ''), ENT_QUOTES, 'UTF-8') ?></td>
|
||
|
|
<td style="padding:9px 8px; font-family:'IBM Plex Mono', monospace; font-size:11px;"><?= htmlspecialchars((string)($log['ip_address'] ?? ''), ENT_QUOTES, 'UTF-8') ?></td>
|
||
|
|
<td style="padding:9px 8px; font-family:'IBM Plex Mono', monospace; font-size:11px; color:var(--muted);"><?= htmlspecialchars((string)($log['context_json'] ?? ''), ENT_QUOTES, 'UTF-8') ?></td>
|
||
|
|
</tr>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
<?php endif; ?>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style="display:flex; justify-content:flex-end;">
|
||
|
|
<button type="submit" class="btn">Save settings</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.settings-status {
|
||
|
|
margin-top: 14px;
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 10px 12px;
|
||
|
|
font-size: 13px;
|
||
|
|
border: 1px solid rgba(255,255,255,0.14);
|
||
|
|
}
|
||
|
|
.settings-status.is-ok {
|
||
|
|
border-color: rgba(34,242,165,0.35);
|
||
|
|
color: #baf8e3;
|
||
|
|
background: rgba(34,242,165,0.10);
|
||
|
|
}
|
||
|
|
.settings-status.is-error {
|
||
|
|
border-color: rgba(255,100,120,0.35);
|
||
|
|
color: #ffc9d2;
|
||
|
|
background: rgba(255,100,120,0.10);
|
||
|
|
}
|
||
|
|
.settings-tabs {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 8px;
|
||
|
|
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||
|
|
padding-bottom: 12px;
|
||
|
|
}
|
||
|
|
.settings-tab {
|
||
|
|
height: 34px;
|
||
|
|
padding: 0 14px;
|
||
|
|
border-radius: 999px;
|
||
|
|
border: 1px solid rgba(255,255,255,0.14);
|
||
|
|
background: rgba(255,255,255,0.03);
|
||
|
|
color: var(--muted);
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.12em;
|
||
|
|
font-size: 10px;
|
||
|
|
font-family: 'IBM Plex Mono', monospace;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
.settings-tab.is-active {
|
||
|
|
border-color: rgba(34,242,165,0.4);
|
||
|
|
color: #89f3cc;
|
||
|
|
background: rgba(34,242,165,0.1);
|
||
|
|
}
|
||
|
|
.settings-panel {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
.settings-panel.is-active {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
.settings-upload-dropzone {
|
||
|
|
border: 1px dashed rgba(255,255,255,0.22);
|
||
|
|
border-radius: 12px;
|
||
|
|
min-height: 108px;
|
||
|
|
padding: 12px;
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
background: rgba(255,255,255,0.02);
|
||
|
|
cursor: pointer;
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
.settings-upload-dropzone:hover {
|
||
|
|
border-color: rgba(34,242,165,0.45);
|
||
|
|
background: rgba(34,242,165,0.06);
|
||
|
|
}
|
||
|
|
.settings-logo-tools {
|
||
|
|
display: grid;
|
||
|
|
gap: 10px;
|
||
|
|
margin-top: 8px;
|
||
|
|
}
|
||
|
|
.settings-logo-preview {
|
||
|
|
min-height: 78px;
|
||
|
|
border-radius: 12px;
|
||
|
|
border: 1px solid rgba(255,255,255,0.12);
|
||
|
|
background: rgba(255,255,255,0.02);
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
overflow: hidden;
|
||
|
|
padding: 8px;
|
||
|
|
}
|
||
|
|
.settings-logo-preview img {
|
||
|
|
max-height: 62px;
|
||
|
|
max-width: 100%;
|
||
|
|
object-fit: contain;
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
.settings-logo-preview span {
|
||
|
|
color: var(--muted);
|
||
|
|
font-size: 12px;
|
||
|
|
}
|
||
|
|
.settings-logo-actions {
|
||
|
|
display: flex;
|
||
|
|
gap: 8px;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
}
|
||
|
|
.settings-media-modal {
|
||
|
|
position: fixed;
|
||
|
|
inset: 0;
|
||
|
|
background: rgba(3,4,8,0.75);
|
||
|
|
z-index: 2000;
|
||
|
|
display: none;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
padding: 20px;
|
||
|
|
}
|
||
|
|
.settings-media-modal.is-open {
|
||
|
|
display: flex;
|
||
|
|
}
|
||
|
|
.settings-media-panel {
|
||
|
|
width: min(980px, 100%);
|
||
|
|
max-height: 80vh;
|
||
|
|
overflow: auto;
|
||
|
|
border-radius: 16px;
|
||
|
|
border: 1px solid rgba(255,255,255,0.12);
|
||
|
|
background: #12151f;
|
||
|
|
padding: 16px;
|
||
|
|
}
|
||
|
|
.settings-media-grid {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||
|
|
gap: 10px;
|
||
|
|
margin-top: 12px;
|
||
|
|
}
|
||
|
|
.settings-media-item {
|
||
|
|
border: 1px solid rgba(255,255,255,0.12);
|
||
|
|
border-radius: 10px;
|
||
|
|
overflow: hidden;
|
||
|
|
background: rgba(255,255,255,0.03);
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
.settings-media-item img {
|
||
|
|
width: 100%;
|
||
|
|
height: 104px;
|
||
|
|
object-fit: cover;
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
.settings-media-item div {
|
||
|
|
padding: 8px;
|
||
|
|
font-size: 11px;
|
||
|
|
color: var(--muted);
|
||
|
|
word-break: break-word;
|
||
|
|
}
|
||
|
|
.settings-file-input {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
.settings-upload-text {
|
||
|
|
display: grid;
|
||
|
|
gap: 6px;
|
||
|
|
}
|
||
|
|
.settings-file-name {
|
||
|
|
font-size: 12px;
|
||
|
|
color: var(--muted);
|
||
|
|
}
|
||
|
|
@media (max-width: 760px) {
|
||
|
|
.settings-tabs {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: 1fr 1fr;
|
||
|
|
}
|
||
|
|
.settings-tab {
|
||
|
|
width: 100%;
|
||
|
|
justify-self: stretch;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
(function () {
|
||
|
|
const tabs = Array.from(document.querySelectorAll('.settings-tab'));
|
||
|
|
const panels = Array.from(document.querySelectorAll('.settings-panel'));
|
||
|
|
if (!tabs.length || !panels.length) return;
|
||
|
|
|
||
|
|
function activate(tabName) {
|
||
|
|
tabs.forEach((tab) => {
|
||
|
|
const isActive = tab.dataset.tab === tabName;
|
||
|
|
tab.classList.toggle('is-active', isActive);
|
||
|
|
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||
|
|
});
|
||
|
|
panels.forEach((panel) => {
|
||
|
|
const isActive = panel.dataset.panel === tabName;
|
||
|
|
panel.classList.toggle('is-active', isActive);
|
||
|
|
panel.hidden = !isActive;
|
||
|
|
});
|
||
|
|
try { localStorage.setItem('ac_settings_tab', tabName); } catch (e) {}
|
||
|
|
}
|
||
|
|
|
||
|
|
tabs.forEach((tab) => {
|
||
|
|
tab.addEventListener('click', () => activate(tab.dataset.tab));
|
||
|
|
});
|
||
|
|
|
||
|
|
let start = 'branding';
|
||
|
|
try {
|
||
|
|
const saved = localStorage.getItem('ac_settings_tab');
|
||
|
|
if (saved && tabs.some((tab) => tab.dataset.tab === saved)) {
|
||
|
|
start = saved;
|
||
|
|
}
|
||
|
|
} catch (e) {}
|
||
|
|
activate(start);
|
||
|
|
})();
|
||
|
|
|
||
|
|
(function () {
|
||
|
|
const list = document.getElementById('footerLinksList');
|
||
|
|
const hidden = document.getElementById('footerLinksJson');
|
||
|
|
const addBtn = document.getElementById('addFooterLinkRow');
|
||
|
|
if (!list || !hidden || !addBtn) return;
|
||
|
|
|
||
|
|
const initial = <?= json_encode(is_array($footer_links ?? null) ? $footer_links : [], JSON_UNESCAPED_SLASHES) ?>;
|
||
|
|
|
||
|
|
function syncHidden() {
|
||
|
|
const rows = Array.from(list.querySelectorAll('.footer-link-row'));
|
||
|
|
const data = rows.map((row) => ({
|
||
|
|
label: (row.querySelector('input[name=\"footer_link_label[]\"]')?.value || '').trim(),
|
||
|
|
url: (row.querySelector('input[name=\"footer_link_url[]\"]')?.value || '').trim(),
|
||
|
|
})).filter((item) => item.label !== '' && item.url !== '');
|
||
|
|
hidden.value = JSON.stringify(data);
|
||
|
|
}
|
||
|
|
|
||
|
|
function createRow(item) {
|
||
|
|
const row = document.createElement('div');
|
||
|
|
row.className = 'footer-link-row';
|
||
|
|
row.style.display = 'grid';
|
||
|
|
row.style.gridTemplateColumns = '1fr 1fr auto';
|
||
|
|
row.style.gap = '8px';
|
||
|
|
row.innerHTML = '' +
|
||
|
|
'<input class=\"input\" name=\"footer_link_label[]\" placeholder=\"Label\" value=\"' + (item.label || '').replace(/\"/g, '"') + '\">' +
|
||
|
|
'<input class=\"input\" name=\"footer_link_url[]\" placeholder=\"/privacy\" value=\"' + (item.url || '').replace(/\"/g, '"') + '\">' +
|
||
|
|
'<button type=\"button\" class=\"btn outline danger\">Remove</button>';
|
||
|
|
row.querySelectorAll('input').forEach((inp) => inp.addEventListener('input', syncHidden));
|
||
|
|
row.querySelector('button').addEventListener('click', () => {
|
||
|
|
row.remove();
|
||
|
|
syncHidden();
|
||
|
|
});
|
||
|
|
return row;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (initial.length) {
|
||
|
|
initial.forEach((item) => list.appendChild(createRow(item)));
|
||
|
|
} else {
|
||
|
|
list.appendChild(createRow({ label: '', url: '' }));
|
||
|
|
}
|
||
|
|
syncHidden();
|
||
|
|
|
||
|
|
addBtn.addEventListener('click', () => {
|
||
|
|
list.appendChild(createRow({ label: '', url: '' }));
|
||
|
|
syncHidden();
|
||
|
|
});
|
||
|
|
|
||
|
|
document.querySelector('form[action=\"/admin/settings\"]')?.addEventListener('submit', syncHidden);
|
||
|
|
})();
|
||
|
|
|
||
|
|
(function () {
|
||
|
|
const input = document.getElementById('headerLogoFile');
|
||
|
|
const label = document.getElementById('headerLogoFileName');
|
||
|
|
if (!input || !label) return;
|
||
|
|
input.addEventListener('change', function () {
|
||
|
|
const file = input.files && input.files.length ? input.files[0].name : 'No file selected';
|
||
|
|
label.textContent = file;
|
||
|
|
});
|
||
|
|
})();
|
||
|
|
|
||
|
|
(function () {
|
||
|
|
const openBtn = document.getElementById('openLogoMediaPicker');
|
||
|
|
const logoInput = document.querySelector('input[name="site_header_logo_url"]');
|
||
|
|
if (!openBtn || !logoInput) return;
|
||
|
|
|
||
|
|
const modal = document.createElement('div');
|
||
|
|
modal.className = 'settings-media-modal';
|
||
|
|
modal.innerHTML = '' +
|
||
|
|
'<div class="settings-media-panel">' +
|
||
|
|
' <div style="display:flex; justify-content:space-between; align-items:center; gap:10px;">' +
|
||
|
|
' <div class="badge">Media Library</div>' +
|
||
|
|
' <button type="button" class="btn outline" id="closeLogoMediaPicker">Close</button>' +
|
||
|
|
' </div>' +
|
||
|
|
' <div id="settingsMediaList" class="settings-media-grid"></div>' +
|
||
|
|
'</div>';
|
||
|
|
document.body.appendChild(modal);
|
||
|
|
|
||
|
|
const list = modal.querySelector('#settingsMediaList');
|
||
|
|
const closeBtn = modal.querySelector('#closeLogoMediaPicker');
|
||
|
|
|
||
|
|
function closeModal() { modal.classList.remove('is-open'); }
|
||
|
|
closeBtn.addEventListener('click', closeModal);
|
||
|
|
modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });
|
||
|
|
|
||
|
|
openBtn.addEventListener('click', async function () {
|
||
|
|
modal.classList.add('is-open');
|
||
|
|
list.innerHTML = '<div style="color:var(--muted);">Loading media...</div>';
|
||
|
|
try {
|
||
|
|
const res = await fetch('/admin/media/picker', { credentials: 'same-origin' });
|
||
|
|
const data = await res.json();
|
||
|
|
const items = Array.isArray(data.items) ? data.items : [];
|
||
|
|
if (!items.length) {
|
||
|
|
list.innerHTML = '<div style="color:var(--muted);">No media found.</div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
list.innerHTML = '';
|
||
|
|
items.forEach((item) => {
|
||
|
|
const url = (item.file_url || '').toString();
|
||
|
|
const type = (item.file_type || '').toString().toLowerCase();
|
||
|
|
const isImage = type.startsWith('image/');
|
||
|
|
const node = document.createElement('button');
|
||
|
|
node.type = 'button';
|
||
|
|
node.className = 'settings-media-item';
|
||
|
|
node.innerHTML = isImage
|
||
|
|
? '<img src="' + url.replace(/"/g, '"') + '" alt="">' + '<div>' + (item.file_name || url) + '</div>'
|
||
|
|
: '<div style="height:104px;display:grid;place-items:center;">' + (item.file_type || 'FILE') + '</div><div>' + (item.file_name || url) + '</div>';
|
||
|
|
node.addEventListener('click', () => {
|
||
|
|
logoInput.value = url;
|
||
|
|
closeModal();
|
||
|
|
});
|
||
|
|
list.appendChild(node);
|
||
|
|
});
|
||
|
|
} catch (err) {
|
||
|
|
list.innerHTML = '<div style="color:#ffb7c2;">Failed to load media picker.</div>';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
})();
|
||
|
|
</script>
|
||
|
|
<?php
|
||
|
|
$content = ob_get_clean();
|
||
|
|
require __DIR__ . '/layout.php';
|