mirror of
https://github.com/Dvorinka/bizoni.git
synced 2026-06-03 18:22:57 +00:00
Add slug support and new admin features
This commit is contained in:
+113
-4
@@ -62,6 +62,23 @@
|
||||
<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" />
|
||||
@@ -87,7 +104,12 @@
|
||||
</div>
|
||||
<div>
|
||||
<label for="editor">Obsah (vizuální editor)</label>
|
||||
<div id="editor"></div>
|
||||
<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>
|
||||
@@ -140,6 +162,60 @@
|
||||
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
|
||||
|
||||
@@ -209,8 +285,21 @@
|
||||
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(', ') : '';
|
||||
quill.root.innerHTML = data.content_html || '';
|
||||
|
||||
// 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';
|
||||
@@ -245,10 +334,22 @@
|
||||
// Move Quill HTML into the hidden textarea
|
||||
// Merge category checkboxes into text input
|
||||
reconcileCategories();
|
||||
const html = quill.root.innerHTML;
|
||||
|
||||
// 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 = quill.getText().trim();
|
||||
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');
|
||||
@@ -256,6 +357,10 @@
|
||||
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';
|
||||
@@ -300,6 +405,10 @@
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user