Files
FCBizoniUH/admin/new.html
T
Tomáš Dvořák 71942e45b9 update
2025-09-23 20:15:36 +02:00

313 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nový článek Bizoni UH</title>
<link rel="icon" type="image/x-icon" href="../img/logo.png" />
<link rel="stylesheet" href="../css/bootstrap.css" />
<link rel="stylesheet" href="../css/bizoni.css" />
<link rel="stylesheet" href="../css/admin.css" />
<style>
body { padding: 24px; max-width: 980px; margin: 0 auto; }
header { display:flex; justify-content: space-between; align-items:center; margin-bottom: 16px; }
.badge { background: #111827; color: #fff; padding: 6px 10px; border-radius: 999px; font-size: 12px; text-decoration: none; }
form { background: #fff; border:1px solid #e5e7eb; border-radius: 10px; padding: 16px; }
.row { display: grid; grid-template-columns: 1fr; gap: 12px; }
label { font-weight: 600; }
input[type="text"], textarea { width: 100%; padding: 10px; border:1px solid #d1d5db; border-radius: 8px; font-size: 14px; }
input[type="file"] { padding: 6px; }
button { background: #111827; color: #fff; padding: 10px 16px; border: 0; border-radius: 8px; cursor: pointer; }
.muted { color: #6b7280; }
.result { margin-top: 16px; padding: 12px; border-radius: 8px; display:none; }
.ok { background: #ecfdf5; color: #065f46; }
.err { background: #fef2f2; color: #991b1b; }
.preview { margin-top: 12px; display:flex; gap: 12px; align-items: center; }
.preview img { width: 160px; height: 100px; object-fit: cover; border:1px solid #e5e7eb; border-radius: 6px; }
.note { background:#FFF7D6; border:1px solid #F7E6A7; padding:10px; border-radius:8px; margin-bottom:12px; }
/* Quill tweaks */
.ql-container { min-height: 320px; }
</style>
<!-- Quill (no API key required) -->
<link href="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.snow.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.min.js"></script>
<script src="../js/admin-auth.js"></script>
</head>
<body class="admin-with-sidenav">
<aside class="admin-sidenav">
<div class="brand"><img src="../img/logo.png" alt=""/> Bizoni UH</div>
<nav>
<a href="/admin/dashboard.html">Dashboard</a>
<a href="/admin/posts.html">Příspěvky</a>
<a class="active" href="/admin/new.html">Nový článek</a>
<a href="/admin/index.html">Přehled</a>
<a href="/" target="_blank">↗ Zpět na web</a>
</nav>
<div class="spacer"></div>
<div class="footer">Admin</div>
</aside>
<header>
<h1 id="page-title" style="margin:0; font-size: 20px;">Nový článek</h1>
<nav style="display:flex; gap:8px;">
<a href="/admin/" class="badge">← Přehled</a>
<a href="/" class="badge">Domů</a>
</nav>
</header>
<!-- Note removed: uploading is supported and handled by the backend -->
<form id="new-post" enctype="multipart/form-data">
<div class="row">
<div>
<label for="title">Titulek</label>
<input type="text" id="title" name="title" placeholder="Např. FC Bizoni vyhráli finále" required />
</div>
<div>
<label for="categories">Kategorie (oddělené čárkou)</label>
<input type="text" id="categories" name="categories" placeholder="Zápasy, O nás" />
<div class="muted" style="margin-top:6px">Předdefinované:</div>
<div class="row" style="gap:12px; align-items:center; grid-template-columns: repeat(auto-fill, minmax(140px,1fr));">
<label><input type="checkbox" class="cat-predef" value="Zápasy"/> Zápasy</label>
<label><input type="checkbox" class="cat-predef" value="O nás"/> O nás</label>
<label><input type="checkbox" class="cat-predef" value="Novinky"/> Novinky</label>
</div>
</div>
<div>
<label for="image">Obrázek (.png / .jpg)</label>
<input type="file" id="image" name="image" accept="image/png,image/jpeg" />
<div class="muted" style="margin-top:6px">Tip: můžete také vložit obrázek přes Ctrl+V nebo načíst z URL.</div>
<div class="row" style="grid-template-columns: 1fr auto; align-items: end; gap:8px; margin-top:6px;">
<input type="url" id="image-url" placeholder="https://… (URL obrázku)" />
<button type="button" id="btn-load-url">Načíst z URL</button>
</div>
<div class="preview" id="preview" style="display:none">
<img id="preview-img" alt="náhled" />
<span class="muted" id="preview-name"></span>
</div>
</div>
<div>
<label for="editor">Obsah (vizuální editor)</label>
<div id="editor"></div>
<!-- Hidden textarea to submit HTML (kept focusable-safe by moving offscreen) -->
<textarea id="content" name="content" rows="12" style="position:absolute; left:-10000px; width:1px; height:1px; overflow:hidden;"></textarea>
<div class="muted">Obsah bude vložen do sekce <code>&lt;div class="text lte-text-page clearfix"&gt;...&lt;/div&gt;</code> podle šablony <code>blog/0030.html</code>.</div>
</div>
<div>
<button id="submit-btn" type="submit">Vytvořit článek</button>
</div>
</div>
<!-- hidden id for edit mode -->
<input type="hidden" id="post-id" name="id" />
</form>
<div id="result" class="result"></div>
<script>
// Reconcile checkboxes into the text input before submit
function reconcileCategories(){
const input = document.getElementById('categories');
const checks = document.querySelectorAll('.cat-predef');
const set = new Set();
if (input && input.value.trim()) {
input.value.split(',').forEach(s => { const v = s.trim(); if (v) set.add(v); });
}
checks.forEach(ch => { if (ch.checked) set.add(ch.value); });
if (input) input.value = Array.from(set).join(', ');
}
// Initialize Quill editor
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block', 'clean']
]
}
});
const form = document.getElementById('new-post');
const result = document.getElementById('result');
const pageTitle = document.getElementById('page-title');
const imageInput = document.getElementById('image');
const preview = document.getElementById('preview');
const previewImg = document.getElementById('preview-img');
const previewName = document.getElementById('preview-name');
const submitBtn = document.getElementById('submit-btn');
const inputUrl = document.getElementById('image-url');
const btnLoadUrl = document.getElementById('btn-load-url');
const inputId = document.getElementById('post-id');
const inputTitle = document.getElementById('title');
const inputCats = document.getElementById('categories');
let pastedBlob = null; // holds clipboard/fetched blob if provided
function setPreviewFromBlob(blob, name){
pastedBlob = blob;
preview.style.display = 'flex';
previewName.textContent = name || 'Vložený obrázek';
const url = URL.createObjectURL(blob);
previewImg.onload = () => URL.revokeObjectURL(url);
previewImg.src = url;
}
imageInput.addEventListener('change', () => {
const file = imageInput.files && imageInput.files[0];
if (!file) { preview.style.display = 'none'; return; }
pastedBlob = null; // prefer explicit file over pasted blob
setPreviewFromBlob(file, file.name);
});
// Paste handler (Ctrl+V) prefer image from clipboard; fallback to URL text
document.addEventListener('paste', async (ev) => {
try {
const items = ev.clipboardData && ev.clipboardData.items ? Array.from(ev.clipboardData.items) : [];
const imgItem = items.find(it => it.type && it.type.startsWith('image/'));
if (imgItem) {
const blob = imgItem.getAsFile();
if (blob) {
imageInput.value = '';
setPreviewFromBlob(blob, 'vložený obrázek');
return;
}
}
const txt = ev.clipboardData && ev.clipboardData.getData ? ev.clipboardData.getData('text/plain') : '';
if (txt && /^https?:\/\//i.test(txt)) {
inputUrl.value = txt.trim();
await (btnLoadUrl.click());
}
} catch (_) {}
});
// Load from URL handler
btnLoadUrl.addEventListener('click', async () => {
const u = (inputUrl.value || '').trim();
if (!u) return;
try {
const res = await fetch(u, {mode: 'cors'});
if (!res.ok) throw new Error('HTTP '+res.status);
const blob = await res.blob();
if (!blob.type.startsWith('image/')) throw new Error('URL nevrací obrázek');
imageInput.value = '';
setPreviewFromBlob(blob, 'z URL');
} catch (e) {
alert('Nelze načíst obrázek z URL: ' + (e.message || e));
}
});
// --- Edit mode support ---
const params = new URLSearchParams(location.search);
const editId = (params.get('edit') || '').trim();
let editMode = false;
async function loadForEdit(id){
try {
result.style.display = 'none';
const res = await fetch('/api/blog/get?id='+encodeURIComponent(id), { headers: window.AdminAuth ? window.AdminAuth.getHeaders() : {} });
if (!res.ok) throw new Error('HTTP '+res.status);
const data = await res.json();
inputId.value = data.id || id;
inputTitle.value = data.title || '';
inputCats.value = Array.isArray(data.categories) ? data.categories.join(', ') : '';
quill.root.innerHTML = data.content_html || '';
// show current image preview
preview.style.display = 'flex';
previewImg.src = '/img/blog/' + (data.id || id) + '.png';
previewName.textContent = 'Aktuální obrázek';
// pre-check category checkboxes based on loaded categories
const set = new Set((Array.isArray(data.categories)? data.categories : []).map(v => v.toLowerCase()));
document.querySelectorAll('.cat-predef').forEach(ch => {
ch.checked = set.has(ch.value.toLowerCase());
});
} catch (e) {
console.error(e);
result.textContent = 'Chyba načítání článku: ' + (e.message || e);
result.className = 'result err';
result.style.display = 'block';
}
}
if (editId) {
editMode = true;
pageTitle.textContent = 'Upravit článek ' + editId;
submitBtn.textContent = 'Uložit změny';
// image optional in edit
imageInput.removeAttribute('required');
// content still required, but keep UX flexible
loadForEdit(editId);
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
result.style.display = 'none';
result.className = 'result';
// Move Quill HTML into the hidden textarea
// Merge category checkboxes into text input
reconcileCategories();
const html = quill.root.innerHTML;
document.getElementById('content').value = html;
// Validate content is not empty (avoid browser required on hidden field)
const plain = quill.getText().trim();
if (!plain) {
result.textContent = 'Vyplňte obsah článku.';
result.classList.add('err');
result.style.display = 'block';
return;
}
const fd = new FormData(form);
// Prefer pasted/fetched blob if present when no file was chosen
if (pastedBlob && !(imageInput.files && imageInput.files[0])) {
const ext = (pastedBlob.type === 'image/jpeg') ? 'jpg' : 'png';
fd.set('image', new File([pastedBlob], 'pasted.'+ext, {type: pastedBlob.type||'image/png'}));
}
// Validate presence of image (file or pasted/url)
if (!fd.get('image')) {
result.textContent = 'Přidejte obrázek (soubor, vložení přes Ctrl+V, nebo URL).';
result.classList.add('err');
result.style.display = 'block';
return;
}
try {
let url = '/api/blog/new';
if (editMode) {
// ensure id is present in payload
if (!fd.get('id')) fd.set('id', editId);
url = '/api/blog/edit';
}
const res = await fetch(url, { method: 'POST', body: fd, headers: window.AdminAuth ? window.AdminAuth.getHeaders() : {} });
if (!editMode && !res.ok) {
const txt = await res.text();
throw new Error(txt || ('HTTP '+res.status));
}
if (editMode && res.status !== 204 && !res.ok) {
const txt = await res.text();
throw new Error(txt || ('HTTP '+res.status));
}
// Success UI
if (editMode) {
result.textContent = 'Uloženo';
result.classList.add('ok');
result.style.display = 'block';
} else {
const data = await res.json();
result.textContent = `Vytvořeno: ${data.id}`;
result.classList.add('ok');
result.style.display = 'block';
// Offer a link to open the new post
const a = document.createElement('a');
a.href = data.link; a.target = '_blank'; a.style.marginLeft = '8px'; a.textContent = 'Otevřít';
result.appendChild(a);
form.reset(); preview.style.display = 'none';
quill.setContents([]);
}
} catch (err) {
result.textContent = 'Chyba: ' + (err.message || err);
result.classList.add('err');
result.style.display = 'block';
}
});
</script>
</body>
</html>