Files
AudioCore/modules/admin/views/settings.php

695 lines
35 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="custom_css" role="tab" aria-selected="false">Custom CSS</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 &lt;i ...&gt; 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="custom_css" role="tabpanel" hidden>
<div class="admin-card" style="padding:16px;">
<div class="badge" style="opacity:0.7;">Custom CSS</div>
<div style="margin-top:12px; display:grid; gap:12px;">
<label class="label">Site-wide Custom CSS</label>
<textarea class="input" name="site_custom_css" rows="14" style="resize:vertical; font-family:'IBM Plex Mono', monospace;" placeholder=".my-custom-class { color: #22f2a5; }"><?= htmlspecialchars($site_custom_css ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
<div style="font-size:12px; color:var(--muted);">Applied on all frontend pages after theme styles.</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, '&quot;') + '\">' +
'<input class=\"input\" name=\"footer_link_url[]\" placeholder=\"/privacy\" value=\"' + (item.url || '').replace(/\"/g, '&quot;') + '\">' +
'<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, '&quot;') + '" 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';