pagination curseur, layout 3 colonnes article, sidebar fixe
This commit is contained in:
+234
-81
@@ -8,6 +8,15 @@ $dateValue = isset($published_at)
|
||||
: date('Y-m-d\TH:i');
|
||||
?>
|
||||
|
||||
<?php if ($action === 'edit'): ?>
|
||||
<div id="vl-page"
|
||||
data-uuid="<?= htmlspecialchars($uuid) ?>"
|
||||
data-insert-url="<?= htmlspecialchars($insertUrl ?? '') ?>"
|
||||
hidden></div>
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<?php endif; ?>
|
||||
|
||||
<h1 class="mb-4"><?= $action === 'edit' ? 'Modifier l\'article' : 'Nouvel article' ?></h1>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
@@ -20,12 +29,11 @@ $dateValue = isset($published_at)
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="<?= htmlspecialchars($formAction) ?>" enctype="multipart/form-data">
|
||||
<form method="POST" action="<?= htmlspecialchars($formAction) ?>"<?= $action === 'create' ? ' enctype="multipart/form-data"' : '' ?>>
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Titre</label>
|
||||
<input type="text" class="form-control" id="title" name="title" required
|
||||
value="<?= htmlspecialchars($title ?? '') ?>"
|
||||
oninput="autoSlug(this.value)">
|
||||
value="<?= htmlspecialchars($title ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -34,10 +42,24 @@ $dateValue = isset($published_at)
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm font-monospace" id="slug" name="slug"
|
||||
value="<?= htmlspecialchars($postSlug ?? '') ?>"
|
||||
pattern="[a-z0-9][a-z0-9-]*"
|
||||
pattern="[a-z0-9][a-z0-9\-]*"
|
||||
placeholder="généré automatiquement depuis le titre">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="category" class="form-label">Catégorie</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<input type="text" class="form-control form-control-sm" id="category" name="category"
|
||||
value="<?= htmlspecialchars($category ?? '') ?>"
|
||||
placeholder="ex : informatique, loisirs, photo…"
|
||||
autocomplete="off">
|
||||
<div id="cat-swatch" title="" style="width:40px;height:28px;border-radius:6px;flex-shrink:0;background:#e5e7eb;transition:background .25s"></div>
|
||||
</div>
|
||||
<small id="cat-hint" class="text-muted d-block mt-1"></small>
|
||||
<div id="cat-free-swatches" class="d-flex flex-wrap gap-1 mt-2"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">
|
||||
Écris en <strong>Markdown</strong> — les fichiers uploadés sont référençables dans le contenu :
|
||||
@@ -64,55 +86,97 @@ $dateValue = isset($published_at)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($action === 'create'): ?>
|
||||
<div class="mb-3">
|
||||
<label for="files" class="form-label">Ajouter des fichiers</label>
|
||||
<input type="file" class="form-control" id="files" name="files[]" multiple>
|
||||
<div class="form-text">Images, vidéos, PDF… — intègre-les dans le contenu ou laisse-les en pièces jointes.</div>
|
||||
<div class="form-text">Images → nommées <code>sha256-taille.ext</code>. Vidéos, PDF → nom sanitisé.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($action === 'edit' && !empty($existingFiles)): ?>
|
||||
<?php if (!empty($existingFiles)): ?>
|
||||
<?php $coverFile = $article['cover'] ?? ''; ?>
|
||||
<?php $filesMeta = $article['files_meta'] ?? []; ?>
|
||||
<div class="mb-3">
|
||||
<p class="form-label">Fichiers existants</p>
|
||||
<p class="form-label fw-semibold">Fichiers existants</p>
|
||||
<div class="list-group">
|
||||
<?php foreach ($existingFiles as $i => $f): ?>
|
||||
<?php $fileUrl = '/file?uuid=' . rawurlencode($uuid) . '&name=' . rawurlencode($f['name']); ?>
|
||||
<div class="list-group-item d-flex align-items-center gap-3 py-2">
|
||||
<?php if ($f['is_image']): ?>
|
||||
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener">
|
||||
<img src="<?= htmlspecialchars($fileUrl) ?>" alt=""
|
||||
style="width:48px;height:48px;object-fit:cover;border-radius:4px;flex-shrink:0">
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener"
|
||||
style="width:48px;text-align:center;font-size:1.5rem;flex-shrink:0;text-decoration:none">
|
||||
<?php
|
||||
$icon = match(true) {
|
||||
str_starts_with($f['mime'], 'video/') => '🎬',
|
||||
str_starts_with($f['mime'], 'audio/') => '🎵',
|
||||
$f['mime'] === 'application/pdf' => '📑',
|
||||
default => '📄',
|
||||
};
|
||||
echo $icon;
|
||||
?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<code class="d-block text-truncate"><?= htmlspecialchars($f['name']) ?></code>
|
||||
<small class="text-muted">
|
||||
<?= htmlspecialchars(number_format($f['size'] / 1024, 1)) ?> Ko
|
||||
— <?= htmlspecialchars($f['mime']) ?>
|
||||
</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-shrink-0">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
onclick="copyMdRef(<?= htmlspecialchars(json_encode($f['name'])) ?>, <?= $f['is_image'] ? 'true' : 'false' ?>, this)">
|
||||
Référence MD
|
||||
</button>
|
||||
<button type="submit" form="del-file-<?= $i ?>"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
onclick="return confirm('Supprimer « <?= htmlspecialchars(addslashes($f['name'])) ?> » définitivement ?')">
|
||||
Supprimer
|
||||
</button>
|
||||
<?php
|
||||
$fileUrl = '/file?uuid=' . rawurlencode($uuid) . '&name=' . rawurlencode($f['name']);
|
||||
$fmeta = $filesMeta[$f['name']] ?? [];
|
||||
$isCoverFile = ($f['name'] === $coverFile);
|
||||
?>
|
||||
<div class="list-group-item py-2">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<!-- Miniature -->
|
||||
<?php if ($f['is_image']): ?>
|
||||
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener" class="flex-shrink-0">
|
||||
<img src="<?= htmlspecialchars($fileUrl) ?>" alt=""
|
||||
style="width:56px;height:56px;object-fit:cover;border-radius:4px;<?= $isCoverFile ? 'outline:2px solid #0d6efd' : '' ?>">
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span style="width:56px;text-align:center;font-size:1.6rem;flex-shrink:0">
|
||||
<?= match(true) {
|
||||
str_starts_with($f['mime'], 'video/') => '🎬',
|
||||
str_starts_with($f['mime'], 'audio/') => '🎵',
|
||||
$f['mime'] === 'application/pdf' => '📑',
|
||||
default => '📄',
|
||||
} ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Infos + méta -->
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<code class="text-truncate small"><?= htmlspecialchars($f['name']) ?></code>
|
||||
<small class="text-muted text-nowrap"><?= number_format($f['size'] / 1024, 1) ?> Ko</small>
|
||||
<?php if ($isCoverFile): ?>
|
||||
<span class="badge bg-primary">cover</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($f['is_image']): ?>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<input type="hidden" name="fmeta_name[]" value="<?= htmlspecialchars($f['name']) ?>">
|
||||
<input type="text" name="fmeta_author[]"
|
||||
class="form-control form-control-sm"
|
||||
style="max-width:220px"
|
||||
placeholder="Auteur / crédit"
|
||||
value="<?= htmlspecialchars($fmeta['author'] ?? '') ?>">
|
||||
<input type="url" name="fmeta_source[]"
|
||||
class="form-control form-control-sm font-monospace"
|
||||
style="max-width:280px"
|
||||
placeholder="URL source"
|
||||
value="<?= htmlspecialchars($fmeta['source_url'] ?? '') ?>">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="d-flex flex-column gap-1 flex-shrink-0 align-items-end">
|
||||
<?php if ($f['is_image'] && !$isCoverFile): ?>
|
||||
<div class="form-check mb-0">
|
||||
<input class="form-check-input" type="radio"
|
||||
name="cover_file" id="cover_<?= $i ?>"
|
||||
value="<?= htmlspecialchars($f['name']) ?>">
|
||||
<label class="form-check-label small" for="cover_<?= $i ?>">Cover</label>
|
||||
</div>
|
||||
<?php elseif ($isCoverFile): ?>
|
||||
<input type="hidden" name="cover_file" value="<?= htmlspecialchars($f['name']) ?>">
|
||||
<small class="text-primary">✓ Cover</small>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex gap-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
data-copy-md-name="<?= htmlspecialchars($fmeta['title'] ?? $f['name']) ?>"
|
||||
data-copy-md-is-image="<?= $f['is_image'] ? '1' : '0' ?>">
|
||||
MD
|
||||
</button>
|
||||
<button type="submit" form="del-file-<?= $i ?>"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
data-confirm="Supprimer « <?= htmlspecialchars($f['name']) ?> » ?">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
@@ -179,8 +243,13 @@ $dateValue = isset($published_at)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success">Enregistrer</button>
|
||||
<a href="/" class="btn btn-secondary">Annuler</a>
|
||||
<div class="d-flex align-items-center gap-3 flex-wrap">
|
||||
<button type="submit" class="btn btn-success">Enregistrer</button>
|
||||
<a href="/" class="btn btn-secondary">Annuler</a>
|
||||
<?php if ($action === 'edit'): ?>
|
||||
<span id="autosave-indicator" class="text-muted small"></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if ($action === 'edit' && !empty($existingFiles)): ?>
|
||||
@@ -192,41 +261,125 @@ $dateValue = isset($published_at)
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
function slugify(s) {
|
||||
const map = {'à':'a','â':'a','ä':'a','é':'e','è':'e','ê':'e','ë':'e','î':'i','ï':'i','ô':'o','ö':'o','ù':'u','û':'u','ü':'u','ç':'c','æ':'ae','œ':'oe'};
|
||||
return s.toLowerCase().replace(/[àâäéèêëîïôöùûüçæœ]/g, c => map[c] || c)
|
||||
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
||||
}
|
||||
function autoSlug(title) {
|
||||
const slugField = document.getElementById('slug');
|
||||
const preview = document.getElementById('slug-preview');
|
||||
// N'écrase le slug que s'il est vide ou s'il correspond à la génération automatique
|
||||
if (slugField._auto !== false) {
|
||||
const generated = slugify(title);
|
||||
slugField.value = generated;
|
||||
preview.textContent = generated;
|
||||
}
|
||||
}
|
||||
document.getElementById('slug').addEventListener('input', function() {
|
||||
this._auto = (this.value === '');
|
||||
document.getElementById('slug-preview').textContent = this.value;
|
||||
});
|
||||
// En mode édition le champ est pré-rempli : désactive l'auto-génération
|
||||
(function() {
|
||||
const s = document.getElementById('slug');
|
||||
if (s.value !== '') s._auto = false;
|
||||
})();
|
||||
<?php if ($action === 'edit' && !empty($article['revisions'])): ?>
|
||||
<hr class="my-4">
|
||||
<div>
|
||||
<button class="btn btn-sm btn-link text-secondary text-decoration-none p-0 fw-semibold"
|
||||
type="button" data-bs-toggle="collapse" data-bs-target="#historyPanel">
|
||||
▸ Historique des révisions (<?= count($article['revisions']) ?>)
|
||||
</button>
|
||||
<div class="collapse mt-3" id="historyPanel">
|
||||
<table class="table table-sm table-hover align-middle">
|
||||
<thead>
|
||||
<tr><th>#</th><th>Date</th><th>Titre à l'époque</th><th>Commentaire</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach (array_reverse($article['revisions']) as $rev): ?>
|
||||
<tr>
|
||||
<td class="text-muted small"><?= (int)($rev['n'] ?? 0) ?></td>
|
||||
<td class="small text-nowrap">
|
||||
<?= htmlspecialchars(date('d/m/Y H:i', strtotime((string)($rev['date'] ?? '')))) ?>
|
||||
</td>
|
||||
<td class="small text-truncate" style="max-width:200px">
|
||||
<?= htmlspecialchars($rev['title'] ?? '') ?>
|
||||
</td>
|
||||
<td class="small text-muted">
|
||||
<?= htmlspecialchars($rev['comment'] ?? '') ?: '<span class="text-muted">–</span>' ?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/?action=diff&uuid=<?= rawurlencode($uuid) ?>&rev=<?= (int)($rev['n'] ?? 0) ?>"
|
||||
class="btn btn-outline-secondary btn-sm" target="_blank">
|
||||
Diff
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($action === 'edit'): ?>
|
||||
</div><!-- /col-lg-8 -->
|
||||
|
||||
<div class="col-lg-4">
|
||||
<?php $sidebarImages = array_filter($existingFiles ?? [], fn ($f) => $f['is_image']); ?>
|
||||
|
||||
<?php if ($sidebarImages): ?>
|
||||
<div class="mb-3">
|
||||
<p class="fw-semibold small mb-2">
|
||||
Images disponibles
|
||||
<span class="text-muted fw-normal">(clic → insère dans le contenu)</span>
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<?php foreach ($sidebarImages as $img): ?>
|
||||
<?php $imgUrl = '/file?uuid=' . rawurlencode($uuid) . '&name=' . rawurlencode($img['name']); ?>
|
||||
<img src="<?= htmlspecialchars($imgUrl) ?>"
|
||||
alt="<?= htmlspecialchars($img['name']) ?>"
|
||||
title="<?= htmlspecialchars($img['name']) ?>"
|
||||
data-insert-ref="<?= htmlspecialchars($img['name']) ?>"
|
||||
style="width:72px;height:72px;object-fit:cover;border-radius:6px;cursor:pointer;border:2px solid transparent;transition:border-color .15s">
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php $externalLinks = $article['external_links'] ?? []; ?>
|
||||
<?php if ($externalLinks): ?>
|
||||
<div class="mb-3">
|
||||
<p class="fw-semibold small mb-2">Liens externes</p>
|
||||
<ul class="list-group list-group-flush">
|
||||
<?php foreach ($externalLinks as $extLink): ?>
|
||||
<?php
|
||||
$elUrl = $extLink['url'];
|
||||
$elName = $extLink['name'];
|
||||
$elIsImg = (bool)preg_match('/\.(jpe?g|png|gif|webp|svg|avif)(\?.*)?$/i', $elUrl);
|
||||
?>
|
||||
<li class="list-group-item px-0 py-1 d-flex align-items-center gap-2 border-0 border-bottom">
|
||||
<span class="flex-shrink-0" style="font-size:1rem">
|
||||
<?= $elIsImg ? '🖼' : '📄' ?>
|
||||
</span>
|
||||
<span class="flex-grow-1 text-truncate small"
|
||||
title="<?= htmlspecialchars($elUrl) ?>"
|
||||
data-insert-ref="<?= htmlspecialchars($elUrl) ?>"
|
||||
style="cursor:pointer;color:#0d6efd;text-decoration:underline dotted">
|
||||
<?= htmlspecialchars($elName) ?>
|
||||
</span>
|
||||
<form method="POST" action="/?action=delete_external_link&uuid=<?= rawurlencode($uuid) ?>"
|
||||
class="d-inline flex-shrink-0">
|
||||
<input type="hidden" name="url" value="<?= htmlspecialchars($elUrl) ?>">
|
||||
<button type="submit" class="btn btn-link btn-sm text-danger p-0 lh-1"
|
||||
data-confirm="Supprimer ce lien externe ?"
|
||||
title="Supprimer">✕</button>
|
||||
</form>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<a href="/?action=add_files&uuid=<?= rawurlencode($uuid) ?>" class="btn btn-outline-secondary btn-sm">
|
||||
+ Ajouter des fichiers
|
||||
</a>
|
||||
<a href="/?action=import_image&uuid=<?= rawurlencode($uuid) ?>" class="btn btn-outline-secondary btn-sm">
|
||||
+ Importer depuis une URL
|
||||
</a>
|
||||
<?php
|
||||
$hasSources = !empty($article['external_links']) || !empty($existingFiles);
|
||||
if ($hasSources):
|
||||
?>
|
||||
<a href="/?action=sources&uuid=<?= rawurlencode($uuid) ?>" class="btn btn-outline-secondary btn-sm">
|
||||
Sources & métadonnées
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div><!-- /col-lg-4 -->
|
||||
|
||||
</div><!-- /row -->
|
||||
<?php endif; ?>
|
||||
|
||||
function copyMdRef(name, isImage, btn) {
|
||||
const ref = isImage ? `` : `[${name}](${name})`;
|
||||
navigator.clipboard.writeText(ref).then(() => {
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = 'Copié !';
|
||||
setTimeout(() => { btn.textContent = orig; }, 1500);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
|
||||
Reference in New Issue
Block a user