2026-03-04 20:46:11 +00:00
< ? 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 >
2026-03-05 17:09:01 +00:00
< button type = " button " class = " settings-tab " data - tab = " custom_css " role = " tab " aria - selected = " false " > Custom CSS </ button >
2026-03-04 20:46:11 +00:00
< 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 >
2026-04-01 14:12:17 +00:00
< input class = " input " name = " site_header_title " value = " <?= htmlspecialchars( $site_header_title ?? '', ENT_QUOTES, 'UTF-8') ?> " placeholder = " AudioCore V1.5.1 " >
2026-03-04 20:46:11 +00:00
< 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 >
2026-04-01 14:12:17 +00:00
< input class = " input " name = " footer_text " value = " <?= htmlspecialchars( $footer_text ?? '', ENT_QUOTES, 'UTF-8') ?> " placeholder = " AudioCore V1.5.1 " >
2026-03-04 20:46:11 +00:00
< 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 >
2026-04-01 14:12:17 +00:00
< div style = " display:grid; gap:10px; padding:14px; border:1px solid rgba(255,255,255,.08); border-radius:16px; background:rgba(255,255,255,.02); " >
< div class = " badge " style = " opacity:.7; " > Visitor Access Password </ div >
< div style = " font-size:12px; color:var(--muted); " >
Set a password to let non - admin users unlock the site during maintenance mode . Leave blank to keep the current password .
</ div >
< input class = " input " type = " password " name = " site_maintenance_access_password " placeholder = " <?= (( $site_maintenance_access_password_enabled ?? '0') === '1') ? 'Password already set - enter a new one to replace it' : 'Set an access password' ?> " >
< label style = " display:inline-flex; align-items:center; gap:8px; font-size:13px; " >
< input type = " checkbox " name = " site_maintenance_access_password_clear " value = " 1 " >
Clear the current access password
</ label >
</ div >
2026-03-04 20:46:11 +00:00
< 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 >
2026-04-01 14:12:17 +00:00
< input class = " input " name = " seo_title_suffix " value = " <?= htmlspecialchars( $seo_title_suffix ?? '', ENT_QUOTES, 'UTF-8') ?> " placeholder = " AudioCore V1.5.1 " >
2026-03-04 20:46:11 +00:00
< 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 >
2026-03-05 17:09:01 +00:00
< 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 >
2026-03-04 20:46:11 +00:00
< 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 : 14 px ;
border - radius : 12 px ;
padding : 10 px 12 px ;
font - size : 13 px ;
border : 1 px 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 : 8 px ;
border - bottom : 1 px solid rgba ( 255 , 255 , 255 , 0.08 );
padding - bottom : 12 px ;
}
. settings - tab {
height : 34 px ;
padding : 0 14 px ;
border - radius : 999 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.14 );
background : rgba ( 255 , 255 , 255 , 0.03 );
color : var ( -- muted );
text - transform : uppercase ;
letter - spacing : 0.12 em ;
font - size : 10 px ;
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 : 1 px dashed rgba ( 255 , 255 , 255 , 0.22 );
border - radius : 12 px ;
min - height : 108 px ;
padding : 12 px ;
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 : 10 px ;
margin - top : 8 px ;
}
. settings - logo - preview {
min - height : 78 px ;
border - radius : 12 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.12 );
background : rgba ( 255 , 255 , 255 , 0.02 );
display : grid ;
place - items : center ;
overflow : hidden ;
padding : 8 px ;
}
. settings - logo - preview img {
max - height : 62 px ;
max - width : 100 % ;
object - fit : contain ;
display : block ;
}
. settings - logo - preview span {
color : var ( -- muted );
font - size : 12 px ;
}
. settings - logo - actions {
display : flex ;
gap : 8 px ;
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 : 20 px ;
}
. settings - media - modal . is - open {
display : flex ;
}
. settings - media - panel {
width : min ( 980 px , 100 % );
max - height : 80 vh ;
overflow : auto ;
border - radius : 16 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.12 );
background : #12151f;
padding : 16 px ;
}
. settings - media - grid {
display : grid ;
grid - template - columns : repeat ( auto - fill , minmax ( 140 px , 1 fr ));
gap : 10 px ;
margin - top : 12 px ;
}
. settings - media - item {
border : 1 px solid rgba ( 255 , 255 , 255 , 0.12 );
border - radius : 10 px ;
overflow : hidden ;
background : rgba ( 255 , 255 , 255 , 0.03 );
cursor : pointer ;
}
. settings - media - item img {
width : 100 % ;
height : 104 px ;
object - fit : cover ;
display : block ;
}
. settings - media - item div {
padding : 8 px ;
font - size : 11 px ;
color : var ( -- muted );
word - break : break - word ;
}
. settings - file - input {
display : none ;
}
. settings - upload - text {
display : grid ;
gap : 6 px ;
}
. settings - file - name {
font - size : 12 px ;
color : var ( -- muted );
}
@ media ( max - width : 760 px ) {
. settings - tabs {
display : grid ;
grid - template - columns : 1 fr 1 fr ;
}
. 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' ;