| 1 | <div class="container admin-site-edit-page">
|
|---|
| 2 | <p><a href="/admin" class="btn">← <%= t('asite.back_admin') %></a></p>
|
|---|
| 3 | <h1><%= isNew ? t('asite.title_new') : t('asite.title_edit') + ': ' + site.title %></h1>
|
|---|
| 4 |
|
|---|
| 5 | <% if (typeof success !== 'undefined' && success) { %><div class="audio-flash audio-flash--ok"><%= success %></div><% } %>
|
|---|
| 6 | <% if (error) { %><div class="audio-flash audio-flash--err"><%= error %></div><% } %>
|
|---|
| 7 |
|
|---|
| 8 | <form method="post" action="<%= isNew ? '/admin/sites/create' : '/admin/sites/' + site.slug + '/save' %>" class="site-form">
|
|---|
| 9 |
|
|---|
| 10 | <fieldset>
|
|---|
| 11 | <legend><%= t('asite.identity') %></legend>
|
|---|
| 12 | <label>
|
|---|
| 13 | <span><%= t('asite.slug') %> <% if (!isNew) { %><em><%= t('asite.slug_fixed') %></em><% } %></span>
|
|---|
| 14 | <span class="slug-url">
|
|---|
| 15 | <span class="slug-url-host" id="slug-host"
|
|---|
| 16 | data-prefix="<%= (typeof tenancy !== 'undefined' && tenancy === 'hub') ? '/user/' : '/' %>">website.com/</span><input
|
|---|
| 17 | type="text" name="slug" id="slug-input" class="slug-url-input" value="<%= site.slug %>" required <% if (!isNew) { %>readonly<% } %>
|
|---|
| 18 | pattern="[a-z0-9_-]{2,40}" placeholder="<%= t('asite.slug_placeholder') %>"
|
|---|
| 19 | autocapitalize="none" autocorrect="off" spellcheck="false">
|
|---|
| 20 | </span>
|
|---|
| 21 | </label>
|
|---|
| 22 | <label>
|
|---|
| 23 | <span><%= t('asite.field_title') %> <small class="form-hint-inline"><%= t('asite.field_title_hint') %></small></span>
|
|---|
| 24 | <input type="text" name="title" value="<%= site.title || '' %>" required maxlength="200">
|
|---|
| 25 | </label>
|
|---|
| 26 | <%# Assign (or reassign) owner — ONLY god AND only in hub mode. In hub this
|
|---|
| 27 | gives each user their own self-managed Klonkt. In solo (one site,
|
|---|
| 28 | one owner) this is pointless/confusing, so it's hidden. %>
|
|---|
| 29 | <% if (user && user.role === 'god' && (typeof tenancy !== 'undefined' && tenancy === 'hub')) { %>
|
|---|
| 30 | <label>
|
|---|
| 31 | <span><%= t('asite.owner') %> <small class="form-hint-inline"><%= t('asite.owner_hint') %></small></span>
|
|---|
| 32 | <% var _godSuffix = t('asite.owner_god_suffix'); %>
|
|---|
| 33 | <select name="owner_id">
|
|---|
| 34 | <% (typeof users !== 'undefined' ? users : []).forEach(function(u){ %>
|
|---|
| 35 | <option value="<%= u.id %>" <%= (site.owner_id === u.id) ? 'selected' : '' %>><%= u.username %><%= u.role === 'god' ? _godSuffix : '' %></option>
|
|---|
| 36 | <% }); %>
|
|---|
| 37 | </select>
|
|---|
| 38 | </label>
|
|---|
| 39 | <% } %>
|
|---|
| 40 | <label>
|
|---|
| 41 | <span><%= t('asite.tagline') %> <small class="form-hint-inline"><%= t('asite.tagline_hint') %></small></span>
|
|---|
| 42 | <input type="text" name="tagline" value="<%= site.tagline || '' %>" maxlength="200">
|
|---|
| 43 | </label>
|
|---|
| 44 | <label>
|
|---|
| 45 | <span><%= t('asite.bio') %> <small class="form-hint-inline"><%= t('asite.bio_hint') %></small></span>
|
|---|
| 46 | <textarea name="description" rows="3" maxlength="500"><%= site.description || '' %></textarea>
|
|---|
| 47 | </label>
|
|---|
| 48 | <label>
|
|---|
| 49 | <span><%= t('asite.profile_photo') %></span>
|
|---|
| 50 | <div class="photo-picker" id="photo-picker">
|
|---|
| 51 | <div class="photo-thumb" id="photo-thumb" <%= site.profile_photo ? '' : 'data-empty' %>>
|
|---|
| 52 | <img id="photo-preview" src="<%= avatar(site.profile_photo || '', 128) %>" alt="" <%= site.profile_photo ? '' : 'hidden' %>>
|
|---|
| 53 | <span class="photo-empty" aria-hidden="true">
|
|---|
| 54 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
|---|
| 55 | </span>
|
|---|
| 56 | </div>
|
|---|
| 57 | <div class="photo-controls">
|
|---|
| 58 | <input type="text" name="profile_photo" id="photo-url" value="<%= site.profile_photo || '' %>" placeholder="<%= t('asite.photo_url_placeholder') %>">
|
|---|
| 59 | <div class="photo-actions">
|
|---|
| 60 | <button type="button" class="btn" id="photo-upload-trigger"><%= t('asite.photo_upload') %></button>
|
|---|
| 61 | <button type="button" class="btn" id="photo-clear" <%= site.profile_photo ? '' : 'hidden' %>><%= t('asite.photo_clear') %></button>
|
|---|
| 62 | <small id="photo-status" class="form-hint photo-status"></small>
|
|---|
| 63 | </div>
|
|---|
| 64 | <input type="file" id="photo-upload-field" accept="image/jpeg,image/png,image/webp,image/gif" hidden>
|
|---|
| 65 | </div>
|
|---|
| 66 | </div>
|
|---|
| 67 | </label>
|
|---|
| 68 | <label>
|
|---|
| 69 | <span><%= t('asite.language') %></span>
|
|---|
| 70 | <input type="text" name="language" value="<%= site.language || 'nl' %>" maxlength="8">
|
|---|
| 71 | </label>
|
|---|
| 72 | <label class="cb">
|
|---|
| 73 | <input type="checkbox" name="profile_enabled" value="1" <%= site.profile_enabled ? 'checked' : '' %>>
|
|---|
| 74 | <span><%= t('asite.profile_enabled') %></span>
|
|---|
| 75 | </label>
|
|---|
| 76 | </fieldset>
|
|---|
| 77 |
|
|---|
| 78 | <fieldset>
|
|---|
| 79 | <legend><%= t('asite.appearance') %></legend>
|
|---|
| 80 |
|
|---|
| 81 | <%# ── Curated accent picker ───────────────────────────────────────
|
|---|
| 82 | Was a free-form <input type="color">; now a fixed set of 8 hand-
|
|---|
| 83 | picked colors that work in both light and dark themes. The hex is
|
|---|
| 84 | still what's stored in the DB — we just constrain WHICH hex. %>
|
|---|
| 85 | <fieldset class="accent-grid">
|
|---|
| 86 | <legend><%= t('asite.accent_color') %></legend>
|
|---|
| 87 | <% const _curAccent = (site.accent || '#c2410c').toLowerCase(); %>
|
|---|
| 88 | <% accents.forEach(function(a) { %>
|
|---|
| 89 | <label class="accent-swatch" title="<%= a.name %>">
|
|---|
| 90 | <input type="radio" name="accent" value="<%= a.color %>" <%= a.color.toLowerCase() === _curAccent ? 'checked' : '' %>>
|
|---|
| 91 | <span class="accent-dot" style="background: <%= a.color %>;" aria-hidden="true"></span>
|
|---|
| 92 | <span class="accent-swatch-name"><%= a.name %></span>
|
|---|
| 93 | </label>
|
|---|
| 94 | <% }); %>
|
|---|
| 95 | </fieldset>
|
|---|
| 96 |
|
|---|
| 97 | <%# ── Default theme for new visitors ───────────────────────────
|
|---|
| 98 | 'auto' (empty value) lets the browser/OS decide via prefers-color-scheme.
|
|---|
| 99 | 'light' / 'dark' force the site default for visitors who haven't picked
|
|---|
| 100 | their own preference yet. Logged-in users with users.theme set, and
|
|---|
| 101 | visitors with a localStorage override, still win over this default. %>
|
|---|
| 102 | <fieldset class="theme-default">
|
|---|
| 103 | <legend><%= t('asite.theme_default') %></legend>
|
|---|
| 104 | <% const td = site.theme_override || ''; %>
|
|---|
| 105 | <label class="cb">
|
|---|
| 106 | <input type="radio" name="theme_override" value="" <%= td === '' ? 'checked' : '' %>>
|
|---|
| 107 | <span><%= t('asite.theme_auto') %></span>
|
|---|
| 108 | </label>
|
|---|
| 109 | <label class="cb">
|
|---|
| 110 | <input type="radio" name="theme_override" value="light" <%= td === 'light' ? 'checked' : '' %>>
|
|---|
| 111 | <span><%= t('asite.theme_light') %></span>
|
|---|
| 112 | </label>
|
|---|
| 113 | <label class="cb">
|
|---|
| 114 | <input type="radio" name="theme_override" value="dark" <%= td === 'dark' ? 'checked' : '' %>>
|
|---|
| 115 | <span><%= t('asite.theme_dark') %></span>
|
|---|
| 116 | </label>
|
|---|
| 117 | </fieldset>
|
|---|
| 118 |
|
|---|
| 119 | <fieldset class="palette-grid">
|
|---|
| 120 | <legend><%= t('asite.palette') %></legend>
|
|---|
| 121 | <% palettes.forEach(function(p) { %>
|
|---|
| 122 | <label class="palette-swatch">
|
|---|
| 123 | <input type="radio" name="palette" value="<%= p.key %>" <%= (site.palette || 'klonkt') === p.key ? 'checked' : '' %>>
|
|---|
| 124 | <span class="palette-preview" aria-hidden="true">
|
|---|
| 125 | <span class="pp-half pp-light" style="background: <%= p.light.paper %>; color: <%= p.light.ink %>;">
|
|---|
| 126 | <span class="pp-letter">Aa</span>
|
|---|
| 127 | <span class="pp-accent" style="background: <%= p.light.accent %>;"></span>
|
|---|
| 128 | </span>
|
|---|
| 129 | <span class="pp-half pp-dark" style="background: <%= p.dark.paper %>; color: <%= p.dark.ink %>;">
|
|---|
| 130 | <span class="pp-letter">Aa</span>
|
|---|
| 131 | <span class="pp-accent" style="background: <%= p.dark.accent %>;"></span>
|
|---|
| 132 | </span>
|
|---|
| 133 | </span>
|
|---|
| 134 | <span class="palette-swatch-name"><%= p.name %></span>
|
|---|
| 135 | </label>
|
|---|
| 136 | <% }); %>
|
|---|
| 137 | </fieldset>
|
|---|
| 138 | </fieldset>
|
|---|
| 139 |
|
|---|
| 140 | <%# Live preview: selecting an accent / theme / palette re-themes this page
|
|---|
| 141 | instantly (reverts on reload; persists on save). %>
|
|---|
| 142 | <script>
|
|---|
| 143 | (function(){
|
|---|
| 144 | if (window.__themePreviewWired) return; window.__themePreviewWired = true;
|
|---|
| 145 | var html = document.documentElement;
|
|---|
| 146 | function applyAccent(hex){
|
|---|
| 147 | if(!/^#[0-9a-fA-F]{6}$/.test(hex)) return;
|
|---|
| 148 | var sa = document.getElementById('pcms-site-accent');
|
|---|
| 149 | if(!sa){ sa = document.createElement('style'); sa.id = 'pcms-site-accent'; document.head.appendChild(sa); }
|
|---|
| 150 | sa.textContent = ':root,[data-palette]{--accent:'+hex+';--accent-soft:color-mix(in srgb,'+hex+' 80%,white);--accent-tint:color-mix(in srgb,'+hex+' 12%,transparent);}';
|
|---|
| 151 | }
|
|---|
| 152 | document.addEventListener('change', function(e){
|
|---|
| 153 | var t = e.target; if(!t || !t.name) return;
|
|---|
| 154 | if(t.name === 'palette'){ html.setAttribute('data-palette', t.value); }
|
|---|
| 155 | else if(t.name === 'accent'){ applyAccent(t.value); }
|
|---|
| 156 | else if(t.name === 'theme_override'){
|
|---|
| 157 | if(t.value === 'light' || t.value === 'dark'){ html.setAttribute('data-theme', t.value); }
|
|---|
| 158 | else { var dk = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; html.setAttribute('data-theme', dk ? 'dark' : 'light'); }
|
|---|
| 159 | }
|
|---|
| 160 | });
|
|---|
| 161 | })();
|
|---|
| 162 | </script>
|
|---|
| 163 |
|
|---|
| 164 | <fieldset>
|
|---|
| 165 | <legend><%= t('asite.behavior') %></legend>
|
|---|
| 166 | <label class="cb">
|
|---|
| 167 | <input type="checkbox" name="is_public" value="1" <%= site.is_public ? 'checked' : '' %>>
|
|---|
| 168 | <span><%= t('asite.is_public') %></span>
|
|---|
| 169 | </label>
|
|---|
| 170 | <label class="cb">
|
|---|
| 171 | <input type="checkbox" name="robots_index" value="1" <%= site.robots_index ? 'checked' : '' %>>
|
|---|
| 172 | <span><%= t('asite.robots_index') %></span>
|
|---|
| 173 | </label>
|
|---|
| 174 | <% if (typeof audioEnabled === 'undefined' || audioEnabled) { %>
|
|---|
| 175 | <label class="cb">
|
|---|
| 176 | <input type="checkbox" name="enable_audio_player" value="1" <%= site.enable_audio_player ? 'checked' : '' %>>
|
|---|
| 177 | <span><%= t('asite.enable_audio') %></span>
|
|---|
| 178 | </label>
|
|---|
| 179 | <% } %>
|
|---|
| 180 | </fieldset>
|
|---|
| 181 |
|
|---|
| 182 | <fieldset class="profile-links-fieldset">
|
|---|
| 183 | <legend><%= t('asite.links') %></legend>
|
|---|
| 184 | <p class="form-hint"><%= t('asite.links_hint') %></p>
|
|---|
| 185 |
|
|---|
| 186 | <div id="profile-links-rows">
|
|---|
| 187 | <% (parsedLinks || []).forEach(function(l) { %>
|
|---|
| 188 | <div class="profile-link-row">
|
|---|
| 189 | <select name="profile_link_platform[]">
|
|---|
| 190 | <% platforms.forEach(function(p) { %>
|
|---|
| 191 | <option value="<%= p.key %>" <%= l.platform === p.key ? 'selected' : '' %>><%= p.label %></option>
|
|---|
| 192 | <% }); %>
|
|---|
| 193 | </select>
|
|---|
| 194 | <input type="text" name="profile_link_url[]" value="<%= l.url %>" placeholder="https://…" required>
|
|---|
| 195 | <button type="button" class="btn btn-danger pl-remove">×</button>
|
|---|
| 196 | </div>
|
|---|
| 197 | <% }); %>
|
|---|
| 198 | </div>
|
|---|
| 199 |
|
|---|
| 200 | <template id="profile-link-template">
|
|---|
| 201 | <div class="profile-link-row">
|
|---|
| 202 | <select name="profile_link_platform[]">
|
|---|
| 203 | <% platforms.forEach(function(p) { %>
|
|---|
| 204 | <option value="<%= p.key %>"><%= p.label %></option>
|
|---|
| 205 | <% }); %>
|
|---|
| 206 | </select>
|
|---|
| 207 | <input type="text" name="profile_link_url[]" placeholder="https://…" required>
|
|---|
| 208 | <button type="button" class="btn btn-danger pl-remove">×</button>
|
|---|
| 209 | </div>
|
|---|
| 210 | </template>
|
|---|
| 211 |
|
|---|
| 212 | <button type="button" id="profile-link-add" class="btn"><%= t('asite.link_add') %></button>
|
|---|
| 213 | </fieldset>
|
|---|
| 214 |
|
|---|
| 215 | <fieldset>
|
|---|
| 216 | <legend><%= t('asite.aliases') %></legend>
|
|---|
| 217 | <p class="form-hint"><%= t('asite.aliases_hint') %></p>
|
|---|
| 218 | <label>
|
|---|
| 219 | <textarea name="ap_aliases" rows="3" placeholder="@oud@mastodon.social https://andere-klonkt.example/ap/users/naam"><%= apAliases %></textarea>
|
|---|
| 220 | </label>
|
|---|
| 221 | </fieldset>
|
|---|
| 222 |
|
|---|
| 223 | <fieldset>
|
|---|
| 224 | <legend><%= t('asite.feed_view') %></legend>
|
|---|
| 225 | <label>
|
|---|
| 226 | <span><%= t('asite.feed_default') %></span>
|
|---|
| 227 | <select name="feed_view_default">
|
|---|
| 228 | <option value="timeline" <%= (site.feed_view_default || 'timeline') === 'timeline' ? 'selected' : '' %>><%= t('asite.feed_timeline') %></option>
|
|---|
| 229 | <option value="grid" <%= site.feed_view_default === 'grid' ? 'selected' : '' %>><%= t('asite.feed_grid') %></option>
|
|---|
| 230 | </select>
|
|---|
| 231 | </label>
|
|---|
| 232 | <label class="cb">
|
|---|
| 233 | <input type="checkbox" name="feed_view_switch" value="1" <%= site.feed_view_switch ? 'checked' : '' %>>
|
|---|
| 234 | <span><%= t('asite.feed_switch') %></span>
|
|---|
| 235 | </label>
|
|---|
| 236 | <label class="cb">
|
|---|
| 237 | <input type="checkbox" name="show_search" value="1" <%= site.show_search ? 'checked' : '' %>>
|
|---|
| 238 | <span><%= t('asite.show_search') %></span>
|
|---|
| 239 | </label>
|
|---|
| 240 | <label class="cb">
|
|---|
| 241 | <input type="checkbox" name="show_archive_link" value="1" <%= site.show_archive_link ? 'checked' : '' %>>
|
|---|
| 242 | <span><%= t('asite.show_archive') %></span>
|
|---|
| 243 | </label>
|
|---|
| 244 | </fieldset>
|
|---|
| 245 |
|
|---|
| 246 | <fieldset class="seo-pointer">
|
|---|
| 247 | <legend><%= t('asite.seo') %></legend>
|
|---|
| 248 | <p><%= t('asite.seo_pointer') %> <a href="/admin/seo"><%= t('asite.seo_link') %></a>.</p>
|
|---|
| 249 | </fieldset>
|
|---|
| 250 |
|
|---|
| 251 | <fieldset>
|
|---|
| 252 | <legend><%= t('asite.custom_legend') %> <em><%= t('asite.optional') %></em></legend>
|
|---|
| 253 | <label>
|
|---|
| 254 | <span><%= t('asite.custom_css') %></span>
|
|---|
| 255 | <textarea name="custom_css" rows="3" placeholder=".foo { color: red; }"><%= site.custom_css || '' %></textarea>
|
|---|
| 256 | </label>
|
|---|
| 257 | <label>
|
|---|
| 258 | <span><%= t('asite.custom_head') %></span>
|
|---|
| 259 | <textarea name="custom_head_html" rows="3" placeholder="<meta name="...">"><%= site.custom_head_html || '' %></textarea>
|
|---|
| 260 | </label>
|
|---|
| 261 | <label>
|
|---|
| 262 | <span><%= t('asite.custom_foot') %></span>
|
|---|
| 263 | <textarea name="custom_foot_html" rows="3"><%= site.custom_foot_html || '' %></textarea>
|
|---|
| 264 | </label>
|
|---|
| 265 | </fieldset>
|
|---|
| 266 |
|
|---|
| 267 | <button type="submit" class="btn btn-primary"><%= isNew ? t('asite.submit_create') : t('asite.submit_save') %></button>
|
|---|
| 268 | </form>
|
|---|
| 269 | </div>
|
|---|
| 270 |
|
|---|
| 271 | <script>
|
|---|
| 272 | (function() {
|
|---|
| 273 | // Slug URL field: show the real host as a dimmed prefix (the slug itself is
|
|---|
| 274 | // coloured via CSS). Hub → host/user/<slug>, otherwise host/<slug>.
|
|---|
| 275 | var sh = document.getElementById('slug-host');
|
|---|
| 276 | if (sh) sh.textContent = location.host + (sh.dataset.prefix || '/');
|
|---|
| 277 | })();
|
|---|
| 278 | (function() {
|
|---|
| 279 | // Profile-links repeater: add row from <template>, remove on click.
|
|---|
| 280 | var rows = document.getElementById('profile-links-rows');
|
|---|
| 281 | var tpl = document.getElementById('profile-link-template');
|
|---|
| 282 | var add = document.getElementById('profile-link-add');
|
|---|
| 283 | if (!rows || !tpl || !add) return;
|
|---|
| 284 |
|
|---|
| 285 | add.addEventListener('click', function() {
|
|---|
| 286 | var clone = tpl.content.cloneNode(true);
|
|---|
| 287 | rows.appendChild(clone);
|
|---|
| 288 | });
|
|---|
| 289 | rows.addEventListener('click', function(e) {
|
|---|
| 290 | if (e.target && e.target.classList.contains('pl-remove')) {
|
|---|
| 291 | var row = e.target.closest('.profile-link-row');
|
|---|
| 292 | if (row) row.remove();
|
|---|
| 293 | }
|
|---|
| 294 | });
|
|---|
| 295 | })();
|
|---|
| 296 |
|
|---|
| 297 | // P63 — Profile photo picker: upload via /admin/sites/upload-photo,
|
|---|
| 298 | // then write the returned URL into the visible input. Live thumb preview.
|
|---|
| 299 | (function() {
|
|---|
| 300 | var picker = document.getElementById('photo-picker');
|
|---|
| 301 | if (!picker) return;
|
|---|
| 302 | var thumb = document.getElementById('photo-thumb');
|
|---|
| 303 | var preview = document.getElementById('photo-preview');
|
|---|
| 304 | var urlEl = document.getElementById('photo-url');
|
|---|
| 305 | var trigger = document.getElementById('photo-upload-trigger');
|
|---|
| 306 | var clear = document.getElementById('photo-clear');
|
|---|
| 307 | var field = document.getElementById('photo-upload-field');
|
|---|
| 308 | var status = document.getElementById('photo-status');
|
|---|
| 309 |
|
|---|
| 310 | function showPreview(url) {
|
|---|
| 311 | if (url) {
|
|---|
| 312 | preview.src = url;
|
|---|
| 313 | preview.hidden = false;
|
|---|
| 314 | thumb.removeAttribute('data-empty');
|
|---|
| 315 | clear.hidden = false;
|
|---|
| 316 | } else {
|
|---|
| 317 | preview.src = '';
|
|---|
| 318 | preview.hidden = true;
|
|---|
| 319 | thumb.setAttribute('data-empty', '');
|
|---|
| 320 | clear.hidden = true;
|
|---|
| 321 | }
|
|---|
| 322 | }
|
|---|
| 323 |
|
|---|
| 324 | if (urlEl) {
|
|---|
| 325 | urlEl.addEventListener('input', function() {
|
|---|
| 326 | showPreview(urlEl.value.trim());
|
|---|
| 327 | });
|
|---|
| 328 | }
|
|---|
| 329 |
|
|---|
| 330 | if (trigger && field) {
|
|---|
| 331 | trigger.addEventListener('click', function() { field.click(); });
|
|---|
| 332 | field.addEventListener('change', async function() {
|
|---|
| 333 | var file = field.files && field.files[0];
|
|---|
| 334 | if (!file) return;
|
|---|
| 335 | status.classList.remove('is-error');
|
|---|
| 336 | status.textContent = 'Uploaden…';
|
|---|
| 337 | try {
|
|---|
| 338 | var fd = new FormData();
|
|---|
| 339 | fd.append('photo', file);
|
|---|
| 340 | var r = await fetch('/admin/sites/upload-photo', {
|
|---|
| 341 | method: 'POST',
|
|---|
| 342 | body: fd,
|
|---|
| 343 | credentials: 'same-origin',
|
|---|
| 344 | });
|
|---|
| 345 | var j = await r.json();
|
|---|
| 346 | if (!r.ok || !j.ok) throw new Error(j.error || ('Upload mislukt (' + r.status + ')'));
|
|---|
| 347 | urlEl.value = j.url;
|
|---|
| 348 | showPreview(j.url);
|
|---|
| 349 | status.textContent = 'Geüpload ✓';
|
|---|
| 350 | setTimeout(function() { status.textContent = ''; }, 2000);
|
|---|
| 351 | } catch (e) {
|
|---|
| 352 | status.classList.add('is-error');
|
|---|
| 353 | status.textContent = 'Mislukt: ' + e.message;
|
|---|
| 354 | } finally {
|
|---|
| 355 | field.value = '';
|
|---|
| 356 | }
|
|---|
| 357 | });
|
|---|
| 358 | }
|
|---|
| 359 |
|
|---|
| 360 | if (clear) {
|
|---|
| 361 | clear.addEventListener('click', function() {
|
|---|
| 362 | urlEl.value = '';
|
|---|
| 363 | showPreview('');
|
|---|
| 364 | status.textContent = '';
|
|---|
| 365 | // Note: doesn't delete the file from disk — saving the form with empty
|
|---|
| 366 | // URL leaves the file orphaned on the server. Acceptable for now.
|
|---|
| 367 | });
|
|---|
| 368 | }
|
|---|
| 369 | })();
|
|---|
| 370 | </script>
|
|---|
| 371 |
|
|---|
| 372 | <style>
|
|---|
| 373 | .admin-site-edit-page { max-width: 760px; margin: 3rem auto; padding: 0 1rem; }
|
|---|
| 374 | .admin-site-edit-page h1 { font-family: var(--font-display, serif); font-size: 1.8rem; margin: 0 0 1.5rem; }
|
|---|
| 375 |
|
|---|
| 376 | .site-form { display: flex; flex-direction: column; gap: 1.5rem; min-width: 0; }
|
|---|
| 377 | /* Mobile: a <fieldset> has a browser-default min-inline-size:min-content and
|
|---|
| 378 | therefore does NOT shrink → the entire form became wider than the screen. min-width:0
|
|---|
| 379 | forces it to shrink; form fields get max-width:100% + border-box so they
|
|---|
| 380 | never overflow their fieldset. */
|
|---|
| 381 | .site-form fieldset { min-width: 0; }
|
|---|
| 382 | .site-form input, .site-form select, .site-form textarea { min-width: 0; max-width: 100%; box-sizing: border-box; }
|
|---|
| 383 | .site-form fieldset {
|
|---|
| 384 | border: 1px solid var(--rule);
|
|---|
| 385 | border-radius: 8px;
|
|---|
| 386 | padding: 1rem 1.25rem;
|
|---|
| 387 | background: var(--paper-2);
|
|---|
| 388 | display: flex;
|
|---|
| 389 | flex-direction: column;
|
|---|
| 390 | gap: 0.75rem;
|
|---|
| 391 | }
|
|---|
| 392 | .site-form fieldset.palette-grid {
|
|---|
| 393 | display: grid;
|
|---|
| 394 | grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|---|
| 395 | background: var(--paper);
|
|---|
| 396 | margin-top: 0.5rem;
|
|---|
| 397 | }
|
|---|
| 398 | /* Theme default radios — horizontal, sits between accent + palette */
|
|---|
| 399 | .site-form fieldset.theme-default {
|
|---|
| 400 | display: flex; flex-direction: row; flex-wrap: wrap; gap: 1rem;
|
|---|
| 401 | background: var(--paper);
|
|---|
| 402 | margin-top: 0.5rem;
|
|---|
| 403 | }
|
|---|
| 404 | .site-form fieldset.theme-default label.cb { font-size: 0.9rem; color: var(--ink); }
|
|---|
| 405 | .site-form fieldset legend { font-family: var(--font-display, serif); font-size: 1rem; padding: 0 0.4rem; }
|
|---|
| 406 | .site-form label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.85rem; color: var(--ink-muted, var(--ink-soft)); }
|
|---|
| 407 | .site-form label.cb { flex-direction: row; align-items: center; gap: 0.5rem; }
|
|---|
| 408 | .site-form input[type="text"], .site-form input[type="color"], .site-form textarea {
|
|---|
| 409 | padding: 0.55rem 0.85rem;
|
|---|
| 410 | border: 1px solid var(--rule);
|
|---|
| 411 | border-radius: 5px;
|
|---|
| 412 | background: var(--paper);
|
|---|
| 413 | color: var(--ink);
|
|---|
| 414 | font-family: inherit;
|
|---|
| 415 | font-size: 0.95rem;
|
|---|
| 416 | }
|
|---|
| 417 | .site-form input[type="color"] { padding: 0.2rem; height: 38px; }
|
|---|
| 418 | .site-form textarea { resize: vertical; min-height: 60px; font-family: var(--font-mono, monospace); font-size: 0.85rem; }
|
|---|
| 419 | .site-form input[readonly] { opacity: 0.6; cursor: not-allowed; background: var(--paper-2); }
|
|---|
| 420 |
|
|---|
| 421 | /* Slug as URL field: dimmed host prefix + the slug itself in the accent colour. */
|
|---|
| 422 | .slug-url {
|
|---|
| 423 | display: flex; align-items: stretch;
|
|---|
| 424 | border: 1px solid var(--rule); border-radius: 5px;
|
|---|
| 425 | background: var(--paper); overflow: hidden;
|
|---|
| 426 | transition: border-color 120ms;
|
|---|
| 427 | }
|
|---|
| 428 | .slug-url:focus-within { border-color: var(--accent); }
|
|---|
| 429 | .slug-url-host {
|
|---|
| 430 | display: inline-flex; align-items: center;
|
|---|
| 431 | padding-left: 0.85rem;
|
|---|
| 432 | color: var(--ink-muted, var(--ink-soft));
|
|---|
| 433 | white-space: nowrap; user-select: none;
|
|---|
| 434 | font-size: 0.95rem;
|
|---|
| 435 | }
|
|---|
| 436 | .site-form .slug-url-input {
|
|---|
| 437 | flex: 1 1 auto; min-width: 0;
|
|---|
| 438 | border: none !important; border-radius: 0 !important;
|
|---|
| 439 | background: transparent !important;
|
|---|
| 440 | color: var(--accent) !important; font-weight: 700;
|
|---|
| 441 | padding: 0.55rem 0.85rem 0.55rem 0.1rem;
|
|---|
| 442 | }
|
|---|
| 443 | .site-form .slug-url-input:focus { outline: none; }
|
|---|
| 444 | .site-form .slug-url-input[readonly] {
|
|---|
| 445 | opacity: 1; cursor: default; background: transparent !important;
|
|---|
| 446 | }
|
|---|
| 447 |
|
|---|
| 448 | /* Selects: same look as the inputs, with a custom chevron instead of the bare
|
|---|
| 449 | native arrow. The open option list is still rendered by the browser. */
|
|---|
| 450 | .site-form select {
|
|---|
| 451 | appearance: none;
|
|---|
| 452 | -webkit-appearance: none;
|
|---|
| 453 | padding: 0.55rem 2.2rem 0.55rem 0.85rem;
|
|---|
| 454 | border: 1px solid var(--rule);
|
|---|
| 455 | border-radius: 5px;
|
|---|
| 456 | background-color: var(--paper);
|
|---|
| 457 | color: var(--ink);
|
|---|
| 458 | font-family: inherit;
|
|---|
| 459 | font-size: 0.95rem;
|
|---|
| 460 | cursor: pointer;
|
|---|
| 461 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
|---|
| 462 | background-repeat: no-repeat;
|
|---|
| 463 | background-position: right 0.7rem center;
|
|---|
| 464 | transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|---|
| 465 | }
|
|---|
| 466 | .site-form select:hover { border-color: var(--ink-soft, #888); }
|
|---|
| 467 | .site-form select:focus,
|
|---|
| 468 | .site-form input[type="text"]:focus,
|
|---|
| 469 | .site-form textarea:focus {
|
|---|
| 470 | outline: none;
|
|---|
| 471 | border-color: var(--accent, #c2410c);
|
|---|
| 472 | box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, #c2410c) 22%, transparent);
|
|---|
| 473 | }
|
|---|
| 474 |
|
|---|
| 475 | /* Reuse the palette-swatch styles from account.ejs */
|
|---|
| 476 | .palette-swatch {
|
|---|
| 477 | display: flex; flex-direction: column; gap: 0.4rem;
|
|---|
| 478 | padding: 0.5rem;
|
|---|
| 479 | border: 1px solid var(--rule); border-radius: 6px; background: var(--paper);
|
|---|
| 480 | cursor: pointer; font-size: 0.85rem; transition: border-color 120ms;
|
|---|
| 481 | }
|
|---|
| 482 | .palette-swatch:hover { border-color: var(--accent); }
|
|---|
| 483 | .palette-swatch input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
|
|---|
| 484 | .palette-swatch:has(input[type="radio"]:checked) { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
|
|---|
| 485 | .palette-preview {
|
|---|
| 486 | display: grid; grid-template-columns: 1fr 1fr;
|
|---|
| 487 | width: 100%; height: 44px; border-radius: 4px; overflow: hidden;
|
|---|
| 488 | border: 1px solid var(--rule);
|
|---|
| 489 | }
|
|---|
| 490 | .pp-half {
|
|---|
| 491 | position: relative;
|
|---|
| 492 | display: flex; align-items: center; justify-content: center;
|
|---|
| 493 | font-family: var(--font-display, serif); font-weight: 600; font-size: 1rem;
|
|---|
| 494 | }
|
|---|
| 495 | .pp-letter { line-height: 1; }
|
|---|
| 496 | .pp-accent {
|
|---|
| 497 | position: absolute; bottom: 4px; right: 4px;
|
|---|
| 498 | width: 9px; height: 9px; border-radius: 50%;
|
|---|
| 499 | border: 1px solid rgba(0,0,0,0.12);
|
|---|
| 500 | }
|
|---|
| 501 | .palette-swatch-name { font-weight: 500; color: var(--ink); text-align: center; }
|
|---|
| 502 |
|
|---|
| 503 | /* Accent picker: row of small colored swatches */
|
|---|
| 504 | .site-form fieldset.accent-grid {
|
|---|
| 505 | display: grid;
|
|---|
| 506 | grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
|---|
| 507 | gap: 0.5rem;
|
|---|
| 508 | background: var(--paper);
|
|---|
| 509 | margin-top: 0.5rem;
|
|---|
| 510 | }
|
|---|
| 511 | .accent-swatch {
|
|---|
| 512 | display: flex; flex-direction: column; align-items: center; gap: 0.4rem;
|
|---|
| 513 | padding: 0.55rem 0.4rem;
|
|---|
| 514 | border: 1px solid var(--rule); border-radius: 6px; background: var(--paper);
|
|---|
| 515 | cursor: pointer; font-size: 0.78rem;
|
|---|
| 516 | transition: border-color 120ms, box-shadow 120ms;
|
|---|
| 517 | }
|
|---|
| 518 | .accent-swatch:hover { border-color: var(--accent); }
|
|---|
| 519 | .accent-swatch input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
|
|---|
| 520 | .accent-swatch:has(input[type="radio"]:checked) {
|
|---|
| 521 | border-color: var(--accent);
|
|---|
| 522 | box-shadow: 0 0 0 2px var(--accent);
|
|---|
| 523 | }
|
|---|
| 524 | .accent-dot {
|
|---|
| 525 | width: 28px; height: 28px; border-radius: 50%;
|
|---|
| 526 | border: 1px solid rgba(0,0,0,0.1);
|
|---|
| 527 | box-shadow: inset 0 -2px 4px rgba(0,0,0,0.12);
|
|---|
| 528 | }
|
|---|
| 529 | .accent-swatch-name { color: var(--ink); text-align: center; font-weight: 500; }
|
|---|
| 530 |
|
|---|
| 531 | .audio-flash { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
|
|---|
| 532 | .audio-flash--ok { background: rgba(40, 160, 90, 0.15); color: #2a9d5e; border: 1px solid rgba(40, 160, 90, 0.3); }
|
|---|
| 533 | .audio-flash--err { background: rgba(200, 60, 60, 0.15); color: #c33; border: 1px solid rgba(200, 60, 60, 0.3); }
|
|---|
| 534 |
|
|---|
| 535 | .form-hint { font-size: 0.8rem; color: var(--ink-muted, var(--ink-soft)); margin: 0; }
|
|---|
| 536 | .form-hint-inline { font-size: 0.78rem; color: var(--ink-muted, var(--ink-soft)); font-weight: 400; margin-left: .25rem; }
|
|---|
| 537 |
|
|---|
| 538 | /* P63 — Profile photo picker
|
|---|
| 539 | Mobile-first: thumb stacks above input/buttons. ≥520px the thumb floats
|
|---|
| 540 | to the left and the controls fill the remaining width. */
|
|---|
| 541 | .photo-picker {
|
|---|
| 542 | display: flex;
|
|---|
| 543 | flex-direction: column;
|
|---|
| 544 | gap: .65rem;
|
|---|
| 545 | align-items: stretch;
|
|---|
| 546 | }
|
|---|
| 547 | .photo-thumb {
|
|---|
| 548 | position: relative;
|
|---|
| 549 | width: 88px; height: 88px;
|
|---|
| 550 | border-radius: 50%;
|
|---|
| 551 | border: 1px solid var(--rule);
|
|---|
| 552 | background: var(--paper-2);
|
|---|
| 553 | overflow: hidden;
|
|---|
| 554 | flex-shrink: 0;
|
|---|
| 555 | display: flex; align-items: center; justify-content: center;
|
|---|
| 556 | }
|
|---|
| 557 | .photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|---|
| 558 | .photo-thumb .photo-empty {
|
|---|
| 559 | display: flex; color: var(--ink-muted, var(--ink-soft)); opacity: .55;
|
|---|
| 560 | }
|
|---|
| 561 | .photo-thumb .photo-empty svg { width: 36px; height: 36px; }
|
|---|
| 562 | .photo-thumb:not([data-empty]) .photo-empty { display: none; }
|
|---|
| 563 | .photo-thumb[data-empty] img { display: none; }
|
|---|
| 564 |
|
|---|
| 565 | .photo-controls {
|
|---|
| 566 | display: flex;
|
|---|
| 567 | flex-direction: column;
|
|---|
| 568 | gap: .5rem;
|
|---|
| 569 | flex: 1;
|
|---|
| 570 | min-width: 0;
|
|---|
| 571 | }
|
|---|
| 572 | .photo-controls input[type="text"] { width: 100%; }
|
|---|
| 573 | .photo-actions {
|
|---|
| 574 | display: flex; align-items: center; flex-wrap: wrap;
|
|---|
| 575 | gap: .4rem;
|
|---|
| 576 | }
|
|---|
| 577 | .photo-actions .btn { padding: .45rem .75rem; font-size: .85rem; }
|
|---|
| 578 | .photo-status { margin-left: .25rem; }
|
|---|
| 579 | .photo-status.is-error { color: #c2410c; }
|
|---|
| 580 |
|
|---|
| 581 | @media (min-width: 520px) {
|
|---|
| 582 | .photo-picker { flex-direction: row; align-items: flex-start; gap: 1rem; }
|
|---|
| 583 | .photo-controls { gap: .55rem; }
|
|---|
| 584 | }
|
|---|
| 585 | /* Mobile-first: platform selector on its own line (full width), URL input + remove
|
|---|
| 586 | button below. Prevents horizontal overflow on phones (fixed 160px column +
|
|---|
| 587 | non-shrinkable input together ~447px > screen width). */
|
|---|
| 588 | .profile-link-row {
|
|---|
| 589 | display: grid; grid-template-columns: 1fr auto; gap: 0.5rem; margin-bottom: 0.5rem;
|
|---|
| 590 | }
|
|---|
| 591 | .profile-link-row select { padding: 0.45rem 0.6rem; grid-column: 1 / -1; }
|
|---|
| 592 | .profile-link-row input { min-width: 0; }
|
|---|
| 593 | @media (min-width: 520px) {
|
|---|
| 594 | .profile-link-row { grid-template-columns: 160px 1fr auto; }
|
|---|
| 595 | .profile-link-row select { grid-column: auto; }
|
|---|
| 596 | }
|
|---|
| 597 | .profile-links-fieldset details > label { margin-top: 0.4rem; }
|
|---|
| 598 | </style>
|
|---|