Files
FCBizoniUH/admin/new.html
T
Your Name 2f65bc03e6 rybbit
2026-04-18 14:30:18 +02:00

423 lines
18 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>
<script src="https://rybbit.tdvorak.dev/api/script.js" data-site-id="d40b7ffffffa" defer></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="slug">URL slug (použije se v adrese)</label>
<input type="text" id="slug" name="slug" placeholder="napr-fc-bizoni-vyhrali-finale" pattern="[a-z0-9-]+" title="Pouze malá písmena, číslice a pomlčky" />
<div class="muted" style="margin-top:6px">Automaticky se vygeneruje z titulku, pokud nezadáte vlastní.</div>
</div>
<div>
<label for="annotation">Anotace (krátký popis pro SEO a sociální sítě)</label>
<input type="text" id="annotation" name="annotation" placeholder="Krátký popis článku (150-300 znaků)" maxlength="300" />
<div class="muted" style="margin-top:6px">Použije se pro SEO description a při sdílení na sociálních sítích.</div>
</div>
<div>
<label for="content-mode">Způsob zadávání obsahu</label>
<select id="content-mode" name="content-mode" style="width: 100%; padding: 10px; border:1px solid #d1d5db; border-radius: 8px; font-size: 14px;">
<option value="visual">Vizuální editor (Quill)</option>
<option value="html">HTML kód</option>
</select>
</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="visual-editor-wrapper">
<div id="editor"></div>
</div>
<div id="html-editor-wrapper" style="display: none;">
<textarea id="html-content" name="html-content" rows="12" placeholder="Zadejte HTML kód obsahu..." style="width: 100%; padding: 10px; border:1px solid #d1d5db; border-radius: 8px; font-size: 14px; font-family: 'Courier New', monospace;"></textarea>
</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');
const inputSlug = document.getElementById('slug');
const inputAnnotation = document.getElementById('annotation');
const contentModeSelect = document.getElementById('content-mode');
const visualEditorWrapper = document.getElementById('visual-editor-wrapper');
const htmlEditorWrapper = document.getElementById('html-editor-wrapper');
const htmlContentTextarea = document.getElementById('html-content');
// Content mode switching
contentModeSelect.addEventListener('change', () => {
const mode = contentModeSelect.value;
if (mode === 'visual') {
visualEditorWrapper.style.display = 'block';
htmlEditorWrapper.style.display = 'none';
} else {
visualEditorWrapper.style.display = 'none';
htmlEditorWrapper.style.display = 'block';
// Sync current content to HTML textarea
const currentContent = quill.root.innerHTML;
htmlContentTextarea.value = currentContent;
}
});
// Sync content from HTML to visual when switching back
contentModeSelect.addEventListener('change', () => {
if (contentModeSelect.value === 'visual') {
const htmlContent = htmlContentTextarea.value;
if (htmlContent.trim()) {
quill.root.innerHTML = htmlContent;
}
}
});
// Auto-generate slug from title
function generateSlug(text) {
return text
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '') // Remove special characters
.replace(/[\s_-]+/g, '-') // Replace spaces and underscores with hyphen
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
}
// Update slug when title changes (if slug is empty)
inputTitle.addEventListener('input', () => {
if (!inputSlug.value || inputSlug.dataset.autoGenerated === 'true') {
inputSlug.value = generateSlug(inputTitle.value);
inputSlug.dataset.autoGenerated = 'true';
}
});
// Mark slug as manually edited when user types in it
inputSlug.addEventListener('input', () => {
inputSlug.dataset.autoGenerated = 'false';
});
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 || '';
inputSlug.value = data.slug || '';
inputAnnotation.value = data.annotation || '';
inputCats.value = Array.isArray(data.categories) ? data.categories.join(', ') : '';
// Set content mode and load content
if (data.content_mode === 'html') {
contentModeSelect.value = 'html';
htmlContentTextarea.value = data.content_html || '';
visualEditorWrapper.style.display = 'none';
htmlEditorWrapper.style.display = 'block';
} else {
contentModeSelect.value = 'visual';
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();
// Get content based on mode
let html;
if (contentModeSelect.value === 'html') {
html = htmlContentTextarea.value.trim();
} else {
html = quill.root.innerHTML;
}
document.getElementById('content').value = html;
// Validate content is not empty (avoid browser required on hidden field)
const plain = contentModeSelect.value === 'html' ?
html.replace(/<[^>]*>/g, '').trim() : // Strip HTML for validation
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);
// Add annotation and content mode to form data
fd.set('annotation', inputAnnotation.value);
fd.set('content_mode', contentModeSelect.value);
// 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([]);
htmlContentTextarea.value = '';
contentModeSelect.value = 'visual';
visualEditorWrapper.style.display = 'block';
htmlEditorWrapper.style.display = 'none';
}
} catch (err) {
result.textContent = 'Chyba: ' + (err.message || err);
result.classList.add('err');
result.style.display = 'block';
}
});
</script>
</body>
</html>