Changeset bf4596d in Klonkt for src/views/pages/post-edit.ejs
- Timestamp:
- 06/25/2026 07:55:32 PM (3 months ago)
- Branches:
- main
- Children:
- 49edc72
- Parents:
- fd95b1b
- File:
-
- 1 edited
-
src/views/pages/post-edit.ejs (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/views/pages/post-edit.ejs
rfd95b1b rbf4596d 18 18 19 19 <form method="post" action="<%= _base %><%= isNew ? '/posts/create' : '/posts/' + post.slug + '/save' %>" class="post-edit-form"> 20 21 <%# ── TYPE (segmented) + type-specific inputs ──────────────── 22 Post type lives at the TOP (above the body), and the selected type 23 reveals its own input: Audio → inline upload, Video → embed URL, 24 Foto → cover/insert hint. %> 25 <% var ptype = post.type || 'post'; %> 26 <% var TYPE_ICONS = { post: '📝', foto: '📷', video: '🎬', audio: '🎵' }; %> 27 <section class="pe-card pe-type-card"> 28 <div class="pe-section-title"><%= t('pedit.s_type') %></div> 29 <input type="hidden" name="type" id="pe-type-input" value="<%= ptype %>"> 30 <div class="pe-typeseg" role="radiogroup" aria-label="<%= t('pedit.s_type') %>"> 31 <% ['post','foto','video','audio'].forEach(function (tt) { %> 32 <button type="button" class="pe-typeseg-btn<%= ptype === tt ? ' is-active' : '' %>" 33 data-type="<%= tt %>" role="radio" aria-checked="<%= ptype === tt ? 'true' : 'false' %>"> 34 <span class="pe-typeseg-ic" aria-hidden="true"><%= TYPE_ICONS[tt] %></span> 35 <span><%= t('pedit.type_' + tt) %></span> 36 </button> 37 <% }); %> 38 </div> 39 40 <%# Audio: inline upload (transcodes + drops [[track]] into the post) %> 41 <div class="pe-type-panel" data-panel="audio" hidden> 42 <div class="pe-audio-up" id="pe-audio-drop" tabindex="0" role="button" aria-label="<%= t('pedit.audio_up_drop') %>"> 43 <input type="file" id="pe-audio-file" accept="audio/*,.mp3,.m4a,.ogg,.opus,.flac,.wav" multiple hidden> 44 <span class="pe-audio-up-ic" aria-hidden="true">🎵</span> 45 <strong><%= t('pedit.audio_up_drop') %></strong> 46 <small><%= t('pedit.audio_up_hint') %></small> 47 </div> 48 <ul class="pe-audio-list" id="pe-audio-list"></ul> 49 </div> 50 51 <%# Video: paste a URL → embed chip %> 52 <div class="pe-type-panel" data-panel="video" hidden> 53 <label class="pe-field"> 54 <span><%= t('pedit.video_up_title') %></span> 55 <div class="pe-video-row"> 56 <input type="url" id="pe-video-url" inputmode="url" autocapitalize="none" spellcheck="false" 57 placeholder="<%= t('pedit.video_up_ph') %>"> 58 <button type="button" class="pe-btn pe-btn-secondary" id="pe-video-insert"><%= t('pedit.video_up_btn') %></button> 59 </div> 60 </label> 61 </div> 62 63 <%# Foto: light hint toward cover + insert-image %> 64 <div class="pe-type-panel" data-panel="foto" hidden> 65 <p class="pe-foto-hint">🖼 <%= t('pedit.foto_up_hint') %></p> 66 </div> 67 </section> 20 68 21 69 <%# ── IDENTITY ─────────────────────────────────────────────── %> … … 162 210 <section class="pe-card"> 163 211 <div class="pe-section-title"><%= t('pedit.s_publication') %></div> 164 <div class="pe-row pe-row-2"> 165 <label class="pe-field"> 166 <span><%= t('pedit.f_status') %></span> 167 <% var _st = isNew ? 'published' : (post.status || 'draft'); %> 168 <select name="status"> 169 <option value="published" <%= _st === 'published' ? 'selected' : '' %>><%= t('pedit.status_published') %></option> 170 <option value="draft" <%= _st === 'draft' ? 'selected' : '' %>><%= t('pedit.status_draft') %></option> 171 <option value="archived" <%= _st === 'archived' ? 'selected' : '' %>><%= t('pedit.status_archived') %></option> 172 </select> 173 </label> 174 <label class="pe-field"> 175 <span><%= t('pedit.f_type') %></span> 176 <select name="type"> 177 <% var ptype = post.type || 'post'; %> 178 <option value="post" <%= ptype === 'post' ? 'selected' : '' %>><%= t('pedit.type_post') %></option> 179 <option value="foto" <%= ptype === 'foto' ? 'selected' : '' %>><%= t('pedit.type_foto') %></option> 180 <option value="video" <%= ptype === 'video' ? 'selected' : '' %>><%= t('pedit.type_video') %></option> 181 <option value="audio" <%= ptype === 'audio' ? 'selected' : '' %>><%= t('pedit.type_audio') %></option> 182 </select> 183 </label> 184 </div> 212 <label class="pe-field"> 213 <span><%= t('pedit.f_status') %></span> 214 <% var _st = isNew ? 'published' : (post.status || 'draft'); %> 215 <select name="status"> 216 <option value="published" <%= _st === 'published' ? 'selected' : '' %>><%= t('pedit.status_published') %></option> 217 <option value="draft" <%= _st === 'draft' ? 'selected' : '' %>><%= t('pedit.status_draft') %></option> 218 <option value="archived" <%= _st === 'archived' ? 'selected' : '' %>><%= t('pedit.status_archived') %></option> 219 </select> 220 </label> 185 221 <div class="pe-checkboxes"> 186 222 <div class="pe-pin"> … … 440 476 } 441 477 478 /* ─── Post type: segmented control + type-aware panels ──────────── */ 479 .pe-typeseg { 480 display: grid; 481 grid-template-columns: repeat(4, 1fr); 482 gap: 0.4rem; 483 } 484 .pe-typeseg-btn { 485 display: flex; flex-direction: column; align-items: center; gap: 0.25rem; 486 padding: 0.7rem 0.4rem; 487 border: 1px solid var(--rule); 488 border-radius: 9px; 489 background: var(--paper-2); 490 color: var(--ink-soft); 491 font-size: 0.85rem; font-weight: 600; 492 cursor: pointer; 493 transition: border-color 120ms, background 120ms, color 120ms; 494 } 495 .pe-typeseg-btn:hover { border-color: var(--accent); color: var(--ink); } 496 .pe-typeseg-btn .pe-typeseg-ic { font-size: 1.3rem; line-height: 1; } 497 .pe-typeseg-btn.is-active { 498 border-color: var(--accent); 499 background: color-mix(in srgb, var(--accent) 14%, var(--paper-2)); 500 color: var(--ink); 501 } 502 .pe-typeseg-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } 503 @media (max-width: 480px) { 504 .pe-typeseg-btn { font-size: 0.78rem; padding: 0.6rem 0.25rem; } 505 .pe-typeseg-btn .pe-typeseg-ic { font-size: 1.15rem; } 506 } 507 508 .pe-type-panel { margin-top: 0.2rem; } 509 510 /* Audio drop/upload zone */ 511 .pe-audio-up { 512 display: flex; flex-direction: column; align-items: center; gap: 0.3rem; 513 text-align: center; 514 padding: 1.4rem 1rem; 515 border: 2px dashed var(--rule); 516 border-radius: 10px; 517 background: var(--paper-2); 518 color: var(--ink-soft); 519 cursor: pointer; 520 transition: border-color 120ms, background 120ms; 521 } 522 .pe-audio-up:hover, 523 .pe-audio-up:focus-visible { border-color: var(--accent); outline: none; } 524 .pe-audio-up.is-drag { 525 border-color: var(--accent); 526 background: color-mix(in srgb, var(--accent) 12%, var(--paper-2)); 527 } 528 .pe-audio-up .pe-audio-up-ic { font-size: 1.7rem; line-height: 1; } 529 .pe-audio-up strong { color: var(--ink); font-size: 0.95rem; } 530 .pe-audio-up small { font-size: 0.78rem; color: var(--ink-muted, var(--ink-soft)); max-width: 42ch; } 531 532 .pe-audio-list { list-style: none; margin: 0.7rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; } 533 .pe-audio-item { 534 display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; 535 padding: 0.5rem 0.7rem; 536 border: 1px solid var(--rule); 537 border-radius: 7px; 538 background: var(--paper-2); 539 font-size: 0.85rem; 540 } 541 .pe-audio-item-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 542 .pe-audio-item-state { flex: none; font-size: 0.8rem; color: var(--ink-soft); } 543 .pe-audio-item.is-done { border-color: color-mix(in srgb, var(--accent) 50%, var(--rule)); } 544 .pe-audio-item.is-done .pe-audio-item-state { color: var(--accent); } 545 .pe-audio-item.is-fail .pe-audio-item-state { color: #d9534f; } 546 547 /* Video URL row */ 548 .pe-video-row { display: flex; gap: 0.5rem; } 549 .pe-video-row input { flex: 1 1 auto; min-width: 0; } 550 .pe-video-row input { 551 box-sizing: border-box; padding: 0.6rem 0.75rem; 552 border: 1px solid var(--rule); border-radius: 6px; 553 background: var(--paper-2); color: var(--ink); font-size: 0.95rem; 554 } 555 .pe-video-row input:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); } 556 .pe-video-row .pe-btn { flex: none; } 557 558 .pe-foto-hint { margin: 0; font-size: 0.88rem; color: var(--ink-soft); } 559 442 560 /* ─── Editor frame (WYSIWYG: top toolbar, contenteditable body, status bar) ─── */ 443 561 .pe-editor-frame { … … 1817 1935 } 1818 1936 1937 // ── Post type: segmented control + type-aware panels ────────── 1938 (function () { 1939 const typeInput = document.getElementById('pe-type-input'); 1940 const card = document.querySelector('.pe-type-card'); 1941 if (!typeInput || !card) return; 1942 const seg = card.querySelector('.pe-typeseg'); 1943 const panels = card.querySelectorAll('.pe-type-panel'); 1944 1945 function applyType(tt) { 1946 typeInput.value = tt; 1947 seg.querySelectorAll('.pe-typeseg-btn').forEach(b => { 1948 const on = b.dataset.type === tt; 1949 b.classList.toggle('is-active', on); 1950 b.setAttribute('aria-checked', on ? 'true' : 'false'); 1951 }); 1952 panels.forEach(p => { p.hidden = (p.dataset.panel !== tt); }); 1953 } 1954 seg.addEventListener('click', (e) => { 1955 const btn = e.target.closest('.pe-typeseg-btn'); 1956 if (btn) applyType(btn.dataset.type); 1957 }); 1958 applyType(typeInput.value || 'post'); 1959 1960 // Video URL → [[embed:url]] chip 1961 const vBtn = document.getElementById('pe-video-insert'); 1962 const vUrl = document.getElementById('pe-video-url'); 1963 if (vBtn && vUrl) { 1964 const doInsert = () => { 1965 const url = (vUrl.value || '').trim(); 1966 if (!/^https?:\/\//i.test(url)) { alert('<%= t('pedit.js_embed_invalid') %>'); return; } 1967 insertChip('embed', url); 1968 vUrl.value = ''; 1969 }; 1970 vBtn.addEventListener('click', doInsert); 1971 vUrl.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); doInsert(); } }); 1972 } 1973 1974 // Audio: inline upload → transcodes server-side → [[track:id]] chip 1975 const drop = document.getElementById('pe-audio-drop'); 1976 const fileInput = document.getElementById('pe-audio-file'); 1977 const list = document.getElementById('pe-audio-list'); 1978 if (drop && fileInput && list) { 1979 const pick = () => fileInput.click(); 1980 drop.addEventListener('click', pick); 1981 drop.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(); } }); 1982 ['dragenter', 'dragover'].forEach(ev => drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.add('is-drag'); })); 1983 ['dragleave', 'drop'].forEach(ev => drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.remove('is-drag'); })); 1984 drop.addEventListener('drop', (e) => { if (e.dataTransfer && e.dataTransfer.files) handleFiles(e.dataTransfer.files); }); 1985 fileInput.addEventListener('change', () => { handleFiles(fileInput.files); fileInput.value = ''; }); 1986 1987 function clientDuration(f) { 1988 return new Promise((resolve) => { 1989 try { 1990 const u = URL.createObjectURL(f); 1991 const a = document.createElement('audio'); 1992 a.preload = 'metadata'; 1993 a.onloadedmetadata = () => { URL.revokeObjectURL(u); resolve(Number.isFinite(a.duration) ? Math.round(a.duration) : null); }; 1994 a.onerror = () => { URL.revokeObjectURL(u); resolve(null); }; 1995 a.src = u; 1996 } catch (_) { resolve(null); } 1997 }); 1998 } 1999 async function handleFiles(files) { 2000 for (const f of Array.from(files || [])) await uploadOne(f); 2001 } 2002 async function uploadOne(f) { 2003 const li = document.createElement('li'); 2004 li.className = 'pe-audio-item'; 2005 const nameEl = document.createElement('span'); 2006 nameEl.className = 'pe-audio-item-name'; 2007 nameEl.textContent = f.name; 2008 const stateEl = document.createElement('span'); 2009 stateEl.className = 'pe-audio-item-state'; 2010 stateEl.textContent = '⏳ <%= t('pedit.audio_up_busy') %>'; 2011 li.appendChild(nameEl); li.appendChild(stateEl); 2012 list.appendChild(li); 2013 try { 2014 const dur = await clientDuration(f); 2015 const fd = new FormData(); 2016 fd.append('audio', f); 2017 if (dur) fd.append('duration', String(dur)); 2018 const res = await fetch('/admin/audio/upload', { 2019 method: 'POST', body: fd, 2020 headers: { 'Accept': 'application/json' }, 2021 credentials: 'same-origin', 2022 }); 2023 const j = await res.json().catch(() => ({})); 2024 if (!res.ok || !j.ok || !j.id) throw new Error(j.error || ('HTTP ' + res.status)); 2025 insertChip('track', j.id); 2026 stateEl.textContent = '✓ <%= t('pedit.audio_up_done') %>'; 2027 li.classList.add('is-done'); 2028 } catch (err) { 2029 stateEl.textContent = '✕ <%= t('pedit.audio_up_fail') %>: ' + err.message; 2030 li.classList.add('is-fail'); 2031 } 2032 } 2033 } 2034 })(); 2035 1819 2036 // ── Submit: serialize editor contents into the hidden field 1820 2037 if (form && hiddenField) {
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)