mirror of
https://github.com/Dvorinka/bizoni.git
synced 2026-07-29 05:13:47 +00:00
update
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Admin Dashboard – 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>
|
||||
:root {
|
||||
--bg: #f8fafc;
|
||||
--card: #ffffff;
|
||||
--muted: #6b7280;
|
||||
--border: #e5e7eb;
|
||||
--brand: #111827;
|
||||
--accent: #2563eb;
|
||||
}
|
||||
body { background: var(--bg); padding: 24px; }
|
||||
header { display:flex; gap:16px; justify-content: space-between; align-items:center; margin-bottom: 20px; }
|
||||
.badge { background: var(--brand); color: #fff; padding: 8px 12px; border-radius: 999px; font-size: 12px; text-decoration: none; }
|
||||
.grid { display: grid; gap: 16px; grid-template-columns: 1fr; }
|
||||
@media (min-width: 1000px) { .grid { grid-template-columns: 1fr 1fr; } }
|
||||
.card { border: 1px solid var(--border); border-radius: 12px; overflow: hidden; background: var(--card); }
|
||||
.card > .hd { display:flex; align-items:center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border); background: #f9fafb; }
|
||||
.card > .bd { padding: 16px; }
|
||||
.muted { color: var(--muted); }
|
||||
.btn { display:inline-flex; gap:8px; align-items:center; border:1px solid var(--border); background:#fff; padding:8px 12px; border-radius:8px; cursor:pointer; }
|
||||
.btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
.btn.danger { background: #ef4444; color: #fff; border-color: #ef4444; }
|
||||
.row { display:flex; gap: 10px; flex-wrap: wrap; }
|
||||
.row > * { flex: 1 1 auto; }
|
||||
.input, textarea { width: 100%; border:1px solid var(--border); border-radius: 8px; padding: 8px 10px; }
|
||||
.list { display:grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:12px; }
|
||||
.thumb { border:1px solid var(--border); border-radius: 10px; overflow: hidden; background:#fff; }
|
||||
.thumb img { width:100%; height:140px; object-fit:cover; }
|
||||
.thumb .cap { padding:8px 10px; font-size: 13px; }
|
||||
.kpi { display:grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 10px; }
|
||||
.kpi .it { background:#f3f4f6; border:1px solid var(--border); border-radius:8px; padding:10px; text-align:center; }
|
||||
small.code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; background:#f3f4f6; border:1px solid var(--border); border-radius:6px; padding:2px 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="admin-with-sidenav">
|
||||
<aside class="admin-sidenav">
|
||||
<div class="brand"><img src="../img/logo.png" alt=""/> Bizoni UH</div>
|
||||
<nav>
|
||||
<a class="active" href="/admin/dashboard.html">Dashboard</a>
|
||||
<a href="/admin/posts.html">Příspěvky</a>
|
||||
<a 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 style="margin:0; font-size:22px;">Admin Dashboard</h1>
|
||||
<div style="display:flex; gap:8px; align-items:center;">
|
||||
<a class="badge" href="/admin/index.html">← Přehled</a>
|
||||
<a class="badge" href="/admin/posts.html">Moje příspěvky</a>
|
||||
<a class="badge" href="/admin/new.html">Nový článek</a>
|
||||
<a class="badge" href="../index.html">Zpět na web</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Left: YouTube highlights -->
|
||||
<section class="card">
|
||||
<div class="hd">
|
||||
<div>
|
||||
<strong>Youtube – sestřihy zápasů</strong>
|
||||
<div class="muted" id="yt-status">Načítám…</div>
|
||||
</div>
|
||||
<div class="row" style="justify-content:flex-end;">
|
||||
<button class="btn" id="btn-yt-reload">Načíst</button>
|
||||
<button class="btn primary" id="btn-yt-refresh">Aktualizovat (POST)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bd">
|
||||
<div class="kpi">
|
||||
<div class="it"><div class="muted">Kanál</div><div id="yt-channel">–</div></div>
|
||||
<div class="it"><div class="muted">Počet</div><div id="yt-count">–</div></div>
|
||||
<div class="it"><div class="muted">Staženo</div><div id="yt-fetched">–</div></div>
|
||||
</div>
|
||||
<div class="list" id="yt-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Right: Latest blogs -->
|
||||
<section class="card">
|
||||
<div class="hd">
|
||||
<div>
|
||||
<strong>Nejnovější články</strong>
|
||||
<div class="muted" id="blog-latest-status">Načítám…</div>
|
||||
</div>
|
||||
<div class="row" style="justify-content:flex-end;">
|
||||
<button class="btn" id="btn-blog-reload">Načíst</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bd">
|
||||
<div class="list" id="blog-latest"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Create post -->
|
||||
<section class="card">
|
||||
<div class="hd"><strong>Vytvořit nový článek</strong></div>
|
||||
<div class="bd">
|
||||
<form id="form-new" enctype="multipart/form-data">
|
||||
<div class="row">
|
||||
<div><label class="muted">Titulek</label><input class="input" type="text" name="title" required /></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div style="flex:1 1 100%">
|
||||
<label class="muted">Kategorie (oddělené čárkou)</label>
|
||||
<input class="input" type="text" 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;">
|
||||
<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>
|
||||
<div class="row">
|
||||
<div style="flex:1 1 100%"><label class="muted">Obsah (HTML)</label><textarea name="content" rows="8" required></textarea></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div><label class="muted">Obrázek (.png)</label><input class="input" type="file" name="image" accept="image/png" required /></div>
|
||||
</div>
|
||||
<div class="row" style="justify-content:flex-end; margin-top:8px;">
|
||||
<button class="btn primary" type="submit">Vytvořit</button>
|
||||
</div>
|
||||
<div class="muted" id="new-status"></div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Edit/Delete post -->
|
||||
<section class="card">
|
||||
<div class="hd"><strong>Upravit / smazat článek</strong></div>
|
||||
<div class="bd">
|
||||
<form id="form-load" class="row" style="align-items:flex-end;">
|
||||
<div style="max-width:160px;">
|
||||
<label class="muted">ID příspěvku</label>
|
||||
<input class="input" type="text" name="id" placeholder="0010" required />
|
||||
</div>
|
||||
<button class="btn" type="submit">Načíst</button>
|
||||
<div class="muted" id="edit-load-status"></div>
|
||||
</form>
|
||||
<form id="form-edit" enctype="multipart/form-data" style="display:none; margin-top:12px;">
|
||||
<input type="hidden" name="id" />
|
||||
<div class="row">
|
||||
<div><label class="muted">Titulek</label><input class="input" type="text" name="title" required /></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div style="flex:1 1 100%"><label class="muted">Kategorie (oddělené čárkou)</label><input class="input" type="text" name="categories" placeholder="Zápasy, O nás" /></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div style="flex:1 1 100%"><label class="muted">Obsah (HTML)</label><textarea name="content" rows="8" required></textarea></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div><label class="muted">Nový obrázek (.png) – volitelný</label><input class="input" type="file" name="image" accept="image/png" /></div>
|
||||
</div>
|
||||
<div class="row" style="justify-content: space-between; margin-top:8px;">
|
||||
<button class="btn primary" type="submit">Uložit změny</button>
|
||||
<button class="btn danger" id="btn-delete" type="button">Smazat</button>
|
||||
</div>
|
||||
<div class="muted" id="edit-status"></div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Small helpers
|
||||
const fmtDate = (iso) => {
|
||||
if (!iso) return '–';
|
||||
try { return new Date(iso).toLocaleString('cs-CZ'); } catch (_) { return iso; }
|
||||
};
|
||||
|
||||
// ---------- YOUTUBE ----------
|
||||
async function loadVideos() {
|
||||
const s = document.getElementById('yt-status');
|
||||
const c = document.getElementById('yt-channel');
|
||||
const n = document.getElementById('yt-count');
|
||||
const f = document.getElementById('yt-fetched');
|
||||
const list = document.getElementById('yt-list');
|
||||
try {
|
||||
s.textContent = 'Načítám…';
|
||||
const res = await fetch('/api/videos/latest');
|
||||
if (!res.ok) throw new Error('HTTP '+res.status);
|
||||
const data = await res.json();
|
||||
c.textContent = data.channel || '–';
|
||||
n.textContent = (data.items||[]).length;
|
||||
f.textContent = fmtDate(data.fetched_at);
|
||||
list.innerHTML='';
|
||||
(data.items||[]).forEach(v => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'thumb';
|
||||
el.innerHTML = `
|
||||
<img src="${v.thumbnail_url}" alt=""/>
|
||||
<div class="cap"><strong>${v.title||''}</strong><br/><span class="muted">${v.published_text||''}</span></div>
|
||||
`;
|
||||
list.appendChild(el);
|
||||
});
|
||||
s.textContent = 'Hotovo';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
s.textContent = 'Chyba při načítání';
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshVideos() {
|
||||
const s = document.getElementById('yt-status');
|
||||
try {
|
||||
s.textContent = 'Aktualizuji…';
|
||||
const res = await fetch('/api/videos/latest', { method: 'POST' });
|
||||
if (!res.ok && res.status !== 204) throw new Error('HTTP '+res.status);
|
||||
await loadVideos();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
s.textContent = 'Chyba při aktualizaci';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- BLOG LATEST ----------
|
||||
async function loadBlogLatest() {
|
||||
const s = document.getElementById('blog-latest-status');
|
||||
const grid = document.getElementById('blog-latest');
|
||||
try {
|
||||
s.textContent = 'Načítám…';
|
||||
const res = await fetch('/api/blog/latest?limit=12');
|
||||
if (!res.ok) throw new Error('HTTP '+res.status);
|
||||
const items = await res.json();
|
||||
grid.innerHTML='';
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
grid.innerHTML = '<div class="muted">Žádné příspěvky.</div>';
|
||||
} else {
|
||||
items.forEach(it => {
|
||||
const el = document.createElement('a');
|
||||
el.className = 'thumb';
|
||||
el.href = it.link;
|
||||
el.target = '_blank';
|
||||
el.innerHTML = `
|
||||
<img src="${it.image}" alt=""/>
|
||||
<div class="cap"><strong>${it.title || ('Článek '+it.id)}</strong><br/><span class="muted">ID ${it.id}</span></div>
|
||||
`;
|
||||
grid.appendChild(el);
|
||||
});
|
||||
}
|
||||
s.textContent = `Nalezeno: ${Array.isArray(items)? items.length : 0}`;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
s.textContent = 'Chyba při načítání';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- BLOG CREATE ----------
|
||||
function reconcileCategoriesToInput(containerSelector) {
|
||||
const form = document.querySelector(containerSelector);
|
||||
if (!form) return;
|
||||
const input = form.querySelector('input[name="categories"]');
|
||||
const checks = form.querySelectorAll('.cat-predef, .cat-predef-edit');
|
||||
const set = new Set();
|
||||
// include from text input first
|
||||
if (input && input.value.trim()) {
|
||||
input.value.split(',').forEach(s => { const v = s.trim(); if (v) set.add(v); });
|
||||
}
|
||||
// include from checked boxes
|
||||
checks.forEach(ch => { if (ch.checked) set.add(ch.value); });
|
||||
if (input) input.value = Array.from(set).join(', ');
|
||||
}
|
||||
|
||||
document.getElementById('form-new').addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault();
|
||||
const out = document.getElementById('new-status');
|
||||
out.textContent = 'Odesílám…';
|
||||
try {
|
||||
reconcileCategoriesToInput('#form-new');
|
||||
const fd = new FormData(ev.target);
|
||||
const res = await fetch('/api/blog/new', { method: 'POST', body: fd });
|
||||
if (!res.ok) {
|
||||
const t = await res.text();
|
||||
throw new Error('HTTP '+res.status+' '+t);
|
||||
}
|
||||
const data = await res.json();
|
||||
out.textContent = 'Vytvořeno: '+ data.id;
|
||||
ev.target.reset();
|
||||
// reset checkboxes
|
||||
document.querySelectorAll('#form-new .cat-predef').forEach(ch => ch.checked = false);
|
||||
loadBlogLatest();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
out.textContent = 'Chyba: '+e.message;
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- BLOG EDIT/DELETE ----------
|
||||
document.getElementById('form-load').addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault();
|
||||
const st = document.getElementById('edit-load-status');
|
||||
st.textContent = 'Načítám…';
|
||||
const id = new FormData(ev.target).get('id').toString().trim();
|
||||
try {
|
||||
const res = await fetch('/api/blog/get?id='+encodeURIComponent(id));
|
||||
if (!res.ok) throw new Error('HTTP '+res.status);
|
||||
const data = await res.json();
|
||||
const form = document.getElementById('form-edit');
|
||||
form.style.display='block';
|
||||
form.elements['id'].value = data.id;
|
||||
form.elements['title'].value = data.title || '';
|
||||
form.elements['content'].value = data.content_html || '';
|
||||
form.elements['categories'].value = Array.isArray(data.categories) ? data.categories.join(', ') : '';
|
||||
// tick predefined checkboxes according to categories
|
||||
markEditCheckboxes(Array.isArray(data.categories) ? data.categories : []);
|
||||
st.textContent = '';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
st.textContent = 'Chyba: '+e.message;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('form-edit').addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault();
|
||||
const out = document.getElementById('edit-status');
|
||||
out.textContent = 'Ukládám…';
|
||||
try {
|
||||
// sync checkboxes to input
|
||||
reconcileCategoriesToInput('#form-edit');
|
||||
const fd = new FormData(ev.target);
|
||||
const res = await fetch('/api/blog/edit', { method: 'POST', body: fd });
|
||||
if (res.status !== 204 && !res.ok) throw new Error('HTTP '+res.status);
|
||||
out.textContent = 'Uloženo';
|
||||
loadBlogLatest();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
out.textContent = 'Chyba: '+e.message;
|
||||
}
|
||||
});
|
||||
|
||||
// When loading for edit, mark checkboxes according to categories
|
||||
function markEditCheckboxes(vals){
|
||||
const set = new Set((vals||[]).map(v => v.toLowerCase()));
|
||||
document.querySelectorAll('#form-edit .cat-predef-edit').forEach(ch => {
|
||||
ch.checked = set.has(ch.value.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('btn-delete').addEventListener('click', async () => {
|
||||
const form = document.getElementById('form-edit');
|
||||
const id = form.elements['id'].value;
|
||||
if (!id) return;
|
||||
if (!confirm('Opravdu smazat článek '+id+'?')) return;
|
||||
const out = document.getElementById('edit-status');
|
||||
out.textContent = 'Mažu…';
|
||||
try {
|
||||
const res = await fetch('/api/blog/delete?id='+encodeURIComponent(id), { method: 'DELETE' });
|
||||
if (res.status !== 204 && !res.ok) throw new Error('HTTP '+res.status);
|
||||
out.textContent = 'Smazáno';
|
||||
document.getElementById('form-edit').reset();
|
||||
document.getElementById('form-edit').style.display = 'none';
|
||||
loadBlogLatest();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
out.textContent = 'Chyba: '+e.message;
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- BINDINGS ----------
|
||||
document.getElementById('btn-yt-reload').addEventListener('click', loadVideos);
|
||||
document.getElementById('btn-yt-refresh').addEventListener('click', refreshVideos);
|
||||
document.getElementById('btn-blog-reload').addEventListener('click', loadBlogLatest);
|
||||
|
||||
// On load: trigger refresh to autofill highlights if empty
|
||||
(async () => {
|
||||
await loadVideos();
|
||||
// if none, trigger refresh to autofill
|
||||
const count = document.getElementById('yt-count').textContent;
|
||||
if (count === '0' || count === '–') {
|
||||
await refreshVideos();
|
||||
}
|
||||
loadBlogLatest();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Admin – 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; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
|
||||
.card { border: 1px solid #e5e7eb; border-radius: 10px; overflow: hidden; background: #fff; }
|
||||
.card img { width: 100%; height: 160px; object-fit: cover; }
|
||||
.card .body { padding: 12px; }
|
||||
.muted { color: #6b7280; font-size: 12px; }
|
||||
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; }
|
||||
</style>
|
||||
</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 href="/admin/new.html">Nový článek</a>
|
||||
<a class="active" 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 style="margin:0; font-size: 20px;">Admin – Nejnovější články</h1>
|
||||
<div style="display:flex; gap:8px; align-items:center;">
|
||||
<a href="/admin/dashboard.html" class="badge">Dashboard</a>
|
||||
<a href="/admin/posts.html" class="badge">Moje příspěvky</a>
|
||||
<a href="/admin/new.html" class="badge">Nový článek</a>
|
||||
<a href="../index.html" class="badge">← Zpět na web</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p class="muted" id="status">Načítám…</p>
|
||||
<div class="grid" id="admin-latest"></div>
|
||||
|
||||
<script>
|
||||
async function loadAdminLatest(){
|
||||
const status = document.getElementById('status');
|
||||
const mount = document.getElementById('admin-latest');
|
||||
try {
|
||||
const res = await fetch('/api/blog/latest?limit=12');
|
||||
if (!res.ok) throw new Error('HTTP '+res.status);
|
||||
const items = await res.json();
|
||||
status.textContent = `Nalezeno: ${items.length}`;
|
||||
mount.innerHTML = '';
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
mount.innerHTML = '<div class="muted">Žádné příspěvky.</div>';
|
||||
return;
|
||||
}
|
||||
const frag = document.createDocumentFragment();
|
||||
items.forEach(it => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
const a = document.createElement('a');
|
||||
a.href = it.link;
|
||||
a.target = '_blank';
|
||||
const img = document.createElement('img');
|
||||
img.src = it.image;
|
||||
img.alt = '';
|
||||
a.appendChild(img);
|
||||
const body = document.createElement('div');
|
||||
body.className = 'body';
|
||||
const h3 = document.createElement('div');
|
||||
h3.style.fontWeight = '600';
|
||||
h3.style.fontSize = '14px';
|
||||
h3.textContent = it.title || ('Článek ' + it.id);
|
||||
const small = document.createElement('div');
|
||||
small.className = 'muted';
|
||||
small.textContent = `ID: ${it.id}`;
|
||||
body.appendChild(h3);
|
||||
body.appendChild(small);
|
||||
card.appendChild(a);
|
||||
card.appendChild(body);
|
||||
frag.appendChild(card);
|
||||
});
|
||||
mount.appendChild(frag);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
status.textContent = 'Chyba při načítání.';
|
||||
}
|
||||
}
|
||||
loadAdminLatest();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
<!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/[email protected]/dist/quill.snow.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/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><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');
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,159 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Příspěvky – 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" />
|
||||
<script src="../js/admin-auth.js"></script>
|
||||
<style>
|
||||
body { padding: 24px; }
|
||||
header { display:flex; justify-content: space-between; align-items:center; margin-bottom: 16px; }
|
||||
nav a { margin-right: 8px; text-decoration: none; }
|
||||
.badge { background: #111827; color: #fff; padding: 6px 10px; border-radius: 999px; font-size: 12px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { border-bottom: 1px solid #e5e7eb; padding: 8px; text-align: left; font-size: 14px; }
|
||||
th { background: #f9fafb; font-weight: 700; }
|
||||
.thumb { width: 80px; height: 48px; object-fit: cover; border:1px solid #e5e7eb; border-radius: 6px; }
|
||||
.muted { color: #6b7280; font-size: 12px; }
|
||||
.toolbar { margin: 12px 0; display:flex; gap:8px; align-items:center; }
|
||||
.search { border:1px solid #d1d5db; padding: 6px 10px; border-radius: 8px; min-width: 260px; }
|
||||
.status { margin: 12px 0; color: #6b7280; }
|
||||
</style>
|
||||
</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 class="active" href="/admin/posts.html">Příspěvky</a>
|
||||
<a 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 style="margin:0; font-size: 20px;">Příspěvky</h1>
|
||||
<nav>
|
||||
<a href="/admin/" class="badge">Přehled</a>
|
||||
<a href="/admin/new.html" class="badge">Nový článek</a>
|
||||
<a href="/" class="badge">Domů</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="toolbar">
|
||||
<input type="search" id="q" class="search" placeholder="Hledat v titulcích…" />
|
||||
<span class="muted" id="counter"></span>
|
||||
</div>
|
||||
|
||||
<div class="status" id="status">Načítám…</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="posts">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:80px;">Obrázek</th>
|
||||
<th>ID</th>
|
||||
<th>Titulek</th>
|
||||
<th>Datum</th>
|
||||
<th>Odkazy</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const statusEl = document.getElementById('status');
|
||||
const tbody = document.querySelector('#posts tbody');
|
||||
const q = document.getElementById('q');
|
||||
const counter = document.getElementById('counter');
|
||||
let allItems = [];
|
||||
|
||||
function fmtDate(iso){
|
||||
if (!iso) return '';
|
||||
try { const d = new Date(iso); return d.toLocaleDateString(); } catch { return iso; }
|
||||
}
|
||||
|
||||
function render(list){
|
||||
tbody.innerHTML = '';
|
||||
const frag = document.createDocumentFragment();
|
||||
list.forEach(it => {
|
||||
const tr = document.createElement('tr');
|
||||
const tdImg = document.createElement('td');
|
||||
const img = document.createElement('img'); img.src = it.image; img.className = 'thumb'; img.alt='';
|
||||
tdImg.appendChild(img);
|
||||
const tdId = document.createElement('td'); tdId.textContent = it.id;
|
||||
const tdTitle = document.createElement('td'); tdTitle.textContent = it.title || ('Článek ' + it.id);
|
||||
const tdDate = document.createElement('td'); tdDate.textContent = fmtDate(it.mtime || it.MTime);
|
||||
const tdLinks = document.createElement('td');
|
||||
const aView = document.createElement('a'); aView.href = it.link; aView.target = '_blank'; aView.textContent = 'Otevřít'; aView.style.marginRight='8px';
|
||||
const aImg = document.createElement('a'); aImg.href = it.image; aImg.target = '_blank'; aImg.textContent = 'Obrázek';
|
||||
tdLinks.appendChild(aView); tdLinks.appendChild(aImg);
|
||||
const tdActions = document.createElement('td');
|
||||
const btnEdit = document.createElement('button'); btnEdit.textContent = 'Upravit'; btnEdit.style.marginRight='8px';
|
||||
btnEdit.addEventListener('click', ()=>{
|
||||
window.location.href = '/admin/new.html?edit=' + encodeURIComponent(it.id);
|
||||
});
|
||||
const btnDel = document.createElement('button'); btnDel.textContent = 'Smazat'; btnDel.style.background = '#991b1b'; btnDel.style.color='#fff';
|
||||
btnDel.addEventListener('click', async ()=>{
|
||||
if (!confirm(`Opravdu smazat článek ${it.id}?`)) return;
|
||||
try {
|
||||
const res = await fetch('/api/blog/delete?id='+encodeURIComponent(it.id), { method: 'DELETE', headers: window.AdminAuth ? window.AdminAuth.getHeaders() : {} });
|
||||
if (!res.ok) throw new Error('HTTP '+res.status);
|
||||
// remove from UI
|
||||
tr.remove();
|
||||
} catch (e) {
|
||||
alert('Smazání selhalo');
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
tdActions.appendChild(btnEdit);
|
||||
tdActions.appendChild(btnDel);
|
||||
tr.appendChild(tdImg); tr.appendChild(tdId); tr.appendChild(tdTitle); tr.appendChild(tdDate); tr.appendChild(tdLinks); tr.appendChild(tdActions);
|
||||
frag.appendChild(tr);
|
||||
});
|
||||
tbody.appendChild(frag);
|
||||
counter.textContent = `Zobrazeno: ${list.length} / ${allItems.length}`;
|
||||
}
|
||||
|
||||
function applyFilter(){
|
||||
const needle = (q.value||'').toLowerCase();
|
||||
if (!needle){ render(allItems); return; }
|
||||
render(allItems.filter(it => (it.title||'').toLowerCase().includes(needle) || (it.id||'').includes(needle)));
|
||||
}
|
||||
|
||||
q.addEventListener('input', applyFilter);
|
||||
|
||||
async function load(){
|
||||
statusEl.textContent = 'Načítám…';
|
||||
try {
|
||||
const res = await fetch('/api/blog/latest?limit=10000');
|
||||
if (!res.ok) throw new Error('HTTP '+res.status);
|
||||
let items = await res.json();
|
||||
if (!Array.isArray(items)) items = [];
|
||||
// numeric desc
|
||||
items.sort((a,b)=>{
|
||||
const ai = parseInt(a.id,10); const bi = parseInt(b.id,10);
|
||||
if (!isNaN(ai) && !isNaN(bi)) return bi-ai;
|
||||
return (b.id||'').localeCompare(a.id||'');
|
||||
});
|
||||
allItems = items;
|
||||
statusEl.textContent = '';
|
||||
render(allItems);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
statusEl.textContent = 'Chyba při načítání.';
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user