235 lines
11 KiB
PHP
235 lines
11 KiB
PHP
<?php
|
||
ob_start();
|
||
|
||
$dateValue = isset($published_at)
|
||
? (str_contains($published_at, ' ')
|
||
? date('Y-m-d\TH:i', strtotime($published_at))
|
||
: $published_at)
|
||
: date('Y-m-d\TH:i');
|
||
?>
|
||
|
||
<h1 class="mb-4"><?= $action === 'edit' ? 'Modifier l\'article' : 'Nouvel article' ?></h1>
|
||
|
||
<?php if (!empty($errors)): ?>
|
||
<div class="alert alert-danger">
|
||
<ul class="mb-0">
|
||
<?php foreach ($errors as $error): ?>
|
||
<li><?= htmlspecialchars($error) ?></li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<form method="POST" action="<?= htmlspecialchars($formAction) ?>" 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)">
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="slug" class="form-label">
|
||
Slug <small class="text-muted">(URL : /post/<span id="slug-preview"><?= htmlspecialchars($postSlug ?? '') ?></span>)</small>
|
||
</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-]*"
|
||
placeholder="généré automatiquement depuis le titre">
|
||
</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 :
|
||
<code></code>
|
||
</small>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="content" class="form-label">Contenu</label>
|
||
<textarea class="form-control font-monospace" id="content" name="content" rows="12"><?= htmlspecialchars($content ?? '') ?></textarea>
|
||
</div>
|
||
|
||
<div class="row mb-3">
|
||
<div class="col-md-6">
|
||
<label for="published_at" class="form-label">Date de publication</label>
|
||
<input type="datetime-local" class="form-control" id="published_at" name="published_at" value="<?= $dateValue ?>">
|
||
</div>
|
||
<div class="col-md-6 d-flex align-items-end">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="published" name="published"
|
||
<?= ($published ?? false) ? 'checked' : '' ?>>
|
||
<label class="form-check-label" for="published">Publié</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<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>
|
||
|
||
<?php if ($action === 'edit' && !empty($existingFiles)): ?>
|
||
<div class="mb-3">
|
||
<p class="form-label">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>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($action === 'edit'): ?>
|
||
<div class="mb-3">
|
||
<label for="revision_comment" class="form-label">Commentaire de révision <small class="text-muted">(optionnel)</small></label>
|
||
<input type="text" class="form-control" id="revision_comment" name="revision_comment"
|
||
placeholder="ex. Correction typos, ajout section X…">
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="card mb-3 border-secondary">
|
||
<div class="card-header bg-transparent py-2">
|
||
<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="#seoPanel" aria-expanded="false">
|
||
▸ SEO — titre, description, image
|
||
</button>
|
||
</div>
|
||
<div class="collapse" id="seoPanel">
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<label for="seo_title" class="form-label">
|
||
Titre SEO
|
||
<small class="text-muted">(balise <title> et og:title)</small>
|
||
</label>
|
||
<input type="text" class="form-control" id="seo_title" name="seo_title"
|
||
maxlength="70"
|
||
value="<?= htmlspecialchars($seoTitle ?? '') ?>"
|
||
placeholder="Généré automatiquement depuis le titre">
|
||
<div class="d-flex justify-content-between mt-1">
|
||
<small class="text-muted">Idéal : 30–60 caractères</small>
|
||
<small id="seo_title_counter" class="text-muted">0 / 60</small>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="seo_description" class="form-label">
|
||
Description SEO
|
||
<small class="text-muted">(meta description et og:description)</small>
|
||
</label>
|
||
<textarea class="form-control" id="seo_description" name="seo_description"
|
||
rows="3" maxlength="200"
|
||
placeholder="Générée automatiquement depuis le début du contenu"><?= htmlspecialchars($seoDescription ?? '') ?></textarea>
|
||
<div class="d-flex justify-content-between mt-1">
|
||
<small class="text-muted">Idéal : 120–155 caractères</small>
|
||
<small id="seo_desc_counter" class="text-muted">0 / 155</small>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-2">
|
||
<label for="og_image" class="form-label">
|
||
Image Open Graph
|
||
<small class="text-muted">(URL absolue, optionnel)</small>
|
||
</label>
|
||
<input type="url" class="form-control font-monospace" id="og_image" name="og_image"
|
||
value="<?= htmlspecialchars($ogImage ?? '') ?>"
|
||
placeholder="https://varlog.a5l.fr/…">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-success">Enregistrer</button>
|
||
<a href="/" class="btn btn-secondary">Annuler</a>
|
||
</form>
|
||
|
||
<?php if ($action === 'edit' && !empty($existingFiles)): ?>
|
||
<?php foreach ($existingFiles as $i => $f): ?>
|
||
<form id="del-file-<?= $i ?>" method="POST"
|
||
action="/?action=delete_file&uuid=<?= rawurlencode($uuid) ?>">
|
||
<input type="hidden" name="name" value="<?= htmlspecialchars($f['name']) ?>">
|
||
</form>
|
||
<?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;
|
||
})();
|
||
|
||
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();
|
||
$title = $action === 'edit' ? 'Modifier l\'article' : 'Nouvel article';
|
||
include __DIR__ . '/layout.php';
|