mirror of
https://github.com/Dvorinka/bizoni.git
synced 2026-06-03 18:22:57 +00:00
423 lines
18 KiB
HTML
423 lines
18 KiB
HTML
<!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><div class="text lte-text-page clearfix">...</div></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>
|