pagination curseur, layout 3 colonnes article, sidebar fixe

This commit is contained in:
Cedric Abonnel
2026-05-12 00:42:51 +02:00
parent d774042be9
commit be09fad48f
91 changed files with 8152 additions and 816 deletions
+87
View File
@@ -0,0 +1,87 @@
<?php
ob_start();
$existingFiles = $articles->getFiles($addFilesArticle['uuid']);
?>
<div class="d-flex align-items-center gap-3 mb-4">
<a href="/?action=edit&uuid=<?= rawurlencode($addFilesArticle['uuid']) ?>" class="btn btn-secondary btn-sm">← Retour</a>
<h1 class="h4 mb-0">Ajouter des fichiers</h1>
</div>
<p class="text-muted small mb-4">
Article : <strong><?= htmlspecialchars($addFilesArticle['title']) ?></strong>
</p>
<div class="row g-4">
<!-- Formulaire d'upload -->
<div class="col-lg-5">
<div class="card">
<div class="card-body">
<form method="POST"
action="/?action=add_files&uuid=<?= rawurlencode($addFilesArticle['uuid']) ?>"
enctype="multipart/form-data">
<div class="mb-3">
<label for="files" class="form-label fw-semibold">Fichiers à uploader</label>
<input type="file" class="form-control" id="files" name="files[]"
multiple required>
<div class="form-text">
Images → nommées <code>sha256-taille.ext</code><br>
Vidéos, PDF, autres → nom sanitisé
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Uploader</button>
<a href="/?action=edit&uuid=<?= rawurlencode($addFilesArticle['uuid']) ?>"
class="btn btn-outline-secondary">Annuler</a>
</div>
</form>
</div>
</div>
</div>
<!-- Fichiers déjà présents -->
<?php if ($existingFiles): ?>
<div class="col-lg-7">
<h5 class="mb-3">Fichiers existants</h5>
<div class="list-group">
<?php foreach ($existingFiles as $f): ?>
<?php $fileUrl = '/file?uuid=' . rawurlencode($addFilesArticle['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" class="flex-shrink-0">
<img src="<?= htmlspecialchars($fileUrl) ?>" alt=""
style="width:48px;height:48px;object-fit:cover;border-radius:4px">
</a>
<?php else: ?>
<span style="width:48px;text-align:center;font-size:1.5rem;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; ?>
<div class="flex-grow-1 overflow-hidden">
<code class="d-block small text-truncate"><?= htmlspecialchars($f['name']) ?></code>
<small class="text-muted"><?= number_format($f['size'] / 1024, 1) ?> Ko</small>
</div>
<?php if ($addFilesArticle['cover'] ?? '' === $f['name']): ?>
<span class="badge bg-primary flex-shrink-0">cover</span>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<?php
$content = ob_get_clean();
$title = 'Ajouter des fichiers — ' . htmlspecialchars($addFilesArticle['title']);
include __DIR__ . '/layout.php';
+394
View File
@@ -0,0 +1,394 @@
<?php
ob_start();
$now = time();
function adminStatusBadge(array $a, int $now): string
{
if (!$a['published']) {
return '<span class="badge bg-secondary">Brouillon</span>';
}
if (strtotime((string)($a['published_at'] ?? '')) > $now) {
return '<span class="badge bg-warning text-dark">Avant-première</span>';
}
return '<span class="badge bg-success">Publié</span>';
}
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">Administration</h1>
<a href="/?action=create" class="btn btn-primary btn-sm">+ Nouvel article</a>
</div>
<!-- Onglets -->
<ul class="nav nav-tabs mb-4">
<?php if (isAdmin()): ?>
<li class="nav-item">
<a class="nav-link <?= $tab === 'dashboard' ? 'active' : '' ?>"
href="/?action=admin&tab=dashboard">Tableau de bord</a>
</li>
<?php endif; ?>
<li class="nav-item">
<a class="nav-link <?= $tab === 'articles' ? 'active' : '' ?>"
href="/?action=admin&tab=articles"><?= isAdmin() ? 'Articles' : 'Mes articles' ?></a>
</li>
<?php if (isAdmin()): ?>
<li class="nav-item">
<a class="nav-link <?= $tab === 'users' ? 'active' : '' ?>"
href="/?action=admin&tab=users">Utilisateurs</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'roles' ? 'active' : '' ?>"
href="/?action=admin&tab=roles">Rôles</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/?action=categories">Catégories</a>
</li>
<?php endif; ?>
</ul>
<!-- ─────────────────────────── DASHBOARD ─────────────────────────── -->
<?php if ($tab === 'dashboard' && isAdmin()): ?>
<div class="row g-3 mb-4">
<?php
$stats = [
['label' => 'Publiés', 'value' => $adminData['published'], 'color' => 'success'],
['label' => 'Avant-premières', 'value' => $adminData['previews'], 'color' => 'warning'],
['label' => 'Brouillons', 'value' => $adminData['drafts'], 'color' => 'secondary'],
['label' => 'Total', 'value' => $adminData['total'], 'color' => 'primary'],
];
foreach ($stats as $s): ?>
<div class="col-6 col-md-3">
<div class="card text-center">
<div class="card-body">
<div class="display-6 fw-bold text-<?= $s['color'] ?>"><?= $s['value'] ?></div>
<div class="text-muted small"><?= $s['label'] ?></div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<h5>Activité récente</h5>
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Titre</th>
<th>Auteur</th>
<th>Statut</th>
<th>Modifié le</th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['recent'] as $a): ?>
<tr>
<td>
<a href="/post/<?= htmlspecialchars($a['slug'] ?? '') ?>">
<?= htmlspecialchars($a['title']) ?>
</a>
</td>
<td class="text-muted small"><?= htmlspecialchars($a['author'] ?? '') ?></td>
<td><?= adminStatusBadge($a, $now) ?></td>
<td class="text-muted small">
<?= htmlspecialchars(date('d/m/Y H:i', strtotime((string)($a['updated_at'] ?? $a['created_at'] ?? '')))) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- ─────────────────────────── ARTICLES ─────────────────────────── -->
<?php elseif ($tab === 'articles'): ?>
<?php if (empty($adminData['articles'])): ?>
<p class="text-muted">Aucun article.</p>
<?php else: ?>
<table class="table table-sm table-hover align-middle">
<thead>
<tr>
<th>Titre</th>
<?php if (isAdmin()): ?><th>Auteur</th><?php endif; ?>
<th>Catégorie</th>
<th>Statut</th>
<th>Date</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['articles'] as $a): ?>
<tr>
<td>
<a href="/post/<?= htmlspecialchars($a['slug'] ?? '') ?>">
<?= htmlspecialchars($a['title']) ?>
</a>
</td>
<?php if (isAdmin()): ?>
<td class="text-muted small"><?= htmlspecialchars($a['author'] ?? '') ?></td>
<?php endif; ?>
<td class="text-muted small"><?= htmlspecialchars($a['category'] ?? '') ?></td>
<td><?= adminStatusBadge($a, $now) ?></td>
<td class="text-muted small text-nowrap">
<?= htmlspecialchars(date('d/m/Y', strtotime((string)($a['published_at'] ?? $a['created_at'] ?? '')))) ?>
</td>
<td class="text-end text-nowrap">
<a href="/?action=edit&uuid=<?= htmlspecialchars($a['uuid']) ?>"
class="btn btn-outline-secondary btn-sm">Modifier</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<!-- ─────────────────────────── UTILISATEURS ─────────────────────────── -->
<?php elseif ($tab === 'users' && isAdmin()): ?>
<!-- Ajouter / attribuer un rôle -->
<div class="card mb-4">
<div class="card-header">Attribuer un rôle</div>
<div class="card-body">
<form method="post" action="/?action=admin_grant_role" class="row g-2 align-items-end">
<div class="col-md-5">
<label class="form-label small">Email</label>
<input type="email" name="email" class="form-control form-control-sm"
placeholder="utilisateur@exemple.fr" required>
</div>
<div class="col-md-4">
<label class="form-label small">Rôle</label>
<select name="role" class="form-select form-select-sm" required>
<?php foreach ($adminData['roles'] as $r): ?>
<option value="<?= htmlspecialchars($r['name']) ?>">
<?= htmlspecialchars($r['label']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary btn-sm w-100">Attribuer</button>
</div>
</form>
</div>
</div>
<!-- Liste des utilisateurs -->
<?php if (empty($adminData['users'])): ?>
<p class="text-muted">Aucun utilisateur.</p>
<?php else: ?>
<table class="table table-sm table-hover align-middle">
<thead>
<tr>
<th>Email</th>
<th>Statut</th>
<th>Rôles</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['users'] as $u): ?>
<tr>
<td class="small"><?= htmlspecialchars($u['email']) ?></td>
<td>
<?php if ($u['is_active'] === null): ?>
<span class="badge bg-light text-muted">Pré-inscrit</span>
<?php elseif ($u['is_active']): ?>
<span class="badge bg-success">Actif</span>
<?php else: ?>
<span class="badge bg-danger">Inactif</span>
<?php endif; ?>
</td>
<td>
<?php foreach ($u['roles'] as $roleName): ?>
<span class="badge bg-primary me-1"><?= htmlspecialchars($roleName) ?></span>
<form method="post" action="/?action=admin_revoke_role"
class="d-inline"
data-confirm="Retirer le rôle <?= htmlspecialchars($roleName) ?> à <?= htmlspecialchars($u['email']) ?> ?">
<input type="hidden" name="email" value="<?= htmlspecialchars($u['email']) ?>">
<input type="hidden" name="role" value="<?= htmlspecialchars($roleName) ?>">
<button type="submit" class="btn btn-link btn-sm p-0 text-danger" title="Retirer">×</button>
</form>
<?php endforeach; ?>
<?php if (empty($u['roles'])): ?>
<span class="text-muted small"></span>
<?php endif; ?>
</td>
<td class="text-end">
<!-- Ajout rapide d'un rôle existant -->
<?php
$currentRoles = $u['roles'];
$missing = array_filter($adminData['roles'], fn ($r) => !in_array($r['name'], $currentRoles, true));
?>
<?php if ($missing): ?>
<form method="post" action="/?action=admin_grant_role" class="d-inline-flex gap-1">
<input type="hidden" name="email" value="<?= htmlspecialchars($u['email']) ?>">
<select name="role" class="form-select form-select-sm" style="width:auto">
<?php foreach ($missing as $r): ?>
<option value="<?= htmlspecialchars($r['name']) ?>">
<?= htmlspecialchars($r['label']) ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-outline-primary btn-sm">+</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<!-- ─────────────────────────── RÔLES ─────────────────────────── -->
<?php elseif ($tab === 'roles' && isAdmin()): ?>
<div class="row g-4">
<!-- Liste des rôles existants -->
<div class="col-lg-8">
<h5 class="mb-3">Rôles existants</h5>
<?php if (empty($adminData['roles'])): ?>
<p class="text-muted">Aucun rôle défini.</p>
<?php else: ?>
<table class="table table-sm table-hover align-middle">
<thead>
<tr>
<th style="width:160px">Nom technique</th>
<th>Label affiché</th>
<th class="text-center" style="width:90px">Utilisateurs</th>
<th style="width:100px"></th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['roles'] as $r): ?>
<tr>
<td><code class="text-body"><?= htmlspecialchars($r['name']) ?></code></td>
<td>
<form method="post" action="/?action=admin_update_role"
class="d-flex gap-2 align-items-center">
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
<input type="text" name="label"
value="<?= htmlspecialchars($r['label']) ?>"
class="form-control form-control-sm" required>
<button type="submit" class="btn btn-outline-secondary btn-sm text-nowrap">
Sauver
</button>
</form>
</td>
<td class="text-center">
<span class="badge bg-secondary"><?= (int)$r['user_count'] ?></span>
</td>
<td class="text-end">
<form method="post" action="/?action=admin_delete_role"
data-confirm="Supprimer le rôle «<?= htmlspecialchars($r['name']) ?>» ?<?= (int)$r['user_count'] > 0 ? ' ' . (int)$r['user_count'] . ' utilisateur(s) perdront ce rôle.' : '' ?>">
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
<button type="submit" class="btn btn-outline-danger btn-sm">
Supprimer
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Créer un rôle -->
<div class="col-lg-4">
<div class="card">
<div class="card-header">Nouveau rôle</div>
<div class="card-body">
<form method="post" action="/?action=admin_create_role">
<div class="mb-3">
<label class="form-label small fw-semibold">Nom technique</label>
<input type="text" name="name" class="form-control form-control-sm"
placeholder="ex : moderator"
pattern="[a-z0-9_-]+"
title="Lettres minuscules, chiffres, tirets et underscores uniquement"
required>
<div class="form-text">Utilisé dans le code — ne change pas.</div>
</div>
<div class="mb-3">
<label class="form-label small fw-semibold">Label affiché</label>
<input type="text" name="label" class="form-control form-control-sm"
placeholder="ex : Modérateur" required>
</div>
<button type="submit" class="btn btn-primary btn-sm w-100">Créer</button>
</form>
</div>
</div>
</div>
<!-- Permissions par rôle -->
<?php if (!empty($adminData['roles'])): ?>
<div class="col-12 mt-2">
<h5 class="mb-3">Permissions par rôle</h5>
<p class="text-muted small mb-3">Le rôle <code>admin</code> a toutes les permissions implicitement.</p>
<div class="row g-3">
<?php foreach ($adminData['roles'] as $r):
if ($r['name'] === 'admin') {
continue;
} ?>
<div class="col-md-6 col-lg-4">
<div class="card h-100">
<div class="card-header py-2 d-flex align-items-center justify-content-between">
<span class="fw-semibold"><?= htmlspecialchars($r['label']) ?></span>
<code class="text-muted small"><?= htmlspecialchars($r['name']) ?></code>
</div>
<div class="card-body py-3">
<form method="post" action="/?action=admin_update_role_caps">
<input type="hidden" name="role_id" value="<?= (int)$r['id'] ?>">
<?php foreach (CAPABILITY_GROUPS as $group): ?>
<?php if (isset($group['single'])): ?>
<?php $cap = $group['single']; ?>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox"
name="caps[]" value="<?= htmlspecialchars($cap) ?>"
id="cap_<?= (int)$r['id'] ?>_<?= htmlspecialchars($cap) ?>"
<?= in_array($cap, $r['capabilities'], true) ? 'checked' : '' ?>>
<label class="form-check-label small fw-semibold"
for="cap_<?= (int)$r['id'] ?>_<?= htmlspecialchars($cap) ?>">
<?= htmlspecialchars($group['label']) ?>
</label>
</div>
<?php else: ?>
<div class="mb-3">
<div class="small fw-semibold mb-1"><?= htmlspecialchars($group['label']) ?></div>
<div class="d-flex gap-3 ps-1">
<?php foreach (['own' => 'Propres articles', 'all' => 'Tous'] as $scope => $scopeLabel):
$cap = $group[$scope]; ?>
<div class="form-check">
<input class="form-check-input" type="checkbox"
name="caps[]" value="<?= htmlspecialchars($cap) ?>"
id="cap_<?= (int)$r['id'] ?>_<?= htmlspecialchars($cap) ?>"
<?= in_array($cap, $r['capabilities'], true) ? 'checked' : '' ?>>
<label class="form-check-label small"
for="cap_<?= (int)$r['id'] ?>_<?= htmlspecialchars($cap) ?>">
<?= $scopeLabel ?>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
<button type="submit" class="btn btn-outline-secondary btn-sm mt-1 w-100">
Enregistrer
</button>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php
$content = ob_get_clean();
$title = 'Administration — varlog';
include __DIR__ . '/layout.php';
+99
View File
@@ -0,0 +1,99 @@
<?php ob_start(); ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="mb-0">Catégories</h1>
<a href="/" class="btn btn-secondary btn-sm">← Retour</a>
</div>
<div class="row g-4">
<!-- Liste des catégories existantes -->
<div class="col-lg-9">
<h5 class="mb-3">Catégories existantes</h5>
<?php if (empty($cats)): ?>
<p class="text-muted">Aucune catégorie définie.</p>
<?php else: ?>
<div class="d-flex flex-column gap-2">
<?php foreach ($cats as $cat => $count):
$gradient = coverGradient($cat, $cats); ?>
<div class="card">
<div class="card-body py-2 px-3 d-flex align-items-center gap-3">
<!-- Swatch -->
<div style="width:40px;height:40px;border-radius:8px;flex-shrink:0;background:<?= htmlspecialchars($gradient) ?>"></div>
<!-- Nom + count -->
<div style="min-width:140px">
<strong><?= htmlspecialchars($cat) ?></strong>
<small class="text-muted ms-2"><?= $count ?> article<?= $count > 1 ? 's' : '' ?></small>
</div>
<!-- Renommer -->
<form method="POST" action="/?action=rename_category"
class="d-flex align-items-center gap-2 flex-grow-1"
data-confirm="Renommer « <?= htmlspecialchars($cat) ?> » ?">
<input type="hidden" name="old" value="<?= htmlspecialchars($cat) ?>">
<input type="text" name="new" class="form-control form-control-sm"
placeholder="Nouveau nom" required>
<button type="submit" class="btn btn-outline-primary btn-sm text-nowrap">Renommer</button>
</form>
<!-- Privée -->
<?php $isPriv = in_array($cat, $privateCats, true); ?>
<form method="POST" action="/?action=toggle_private_category">
<input type="hidden" name="category" value="<?= htmlspecialchars($cat) ?>">
<button type="submit"
class="btn btn-sm text-nowrap <?= $isPriv ? 'btn-secondary' : 'btn-outline-secondary' ?>">
🔒 <?= $isPriv ? 'Privée' : 'Publique' ?>
</button>
</form>
<!-- Supprimer -->
<form method="POST" action="/?action=delete_category"
data-confirm="Retirer la catégorie « <?= htmlspecialchars($cat) ?> » de tous les articles ?">
<input type="hidden" name="category" value="<?= htmlspecialchars($cat) ?>">
<button type="submit" class="btn btn-outline-danger btn-sm">Supprimer</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Palette & Nouvelle catégorie -->
<div class="col-lg-3">
<h5 class="mb-3">Nouvelle catégorie</h5>
<div class="card mb-3">
<div class="card-body">
<p class="text-muted small mb-3">
Créez une catégorie en l'assignant à un article.
La prochaine reçoit la couleur n°<?= count($cats) % 16 + 1 ?>.
</p>
<!-- Palette des 16 couleurs -->
<div class="d-flex flex-wrap gap-2">
<?php
$nextIdx = count($cats) % 16;
foreach (COLOR_PALETTE_16 as $i => $rgb):
$g = _paletteGradient($rgb, 0);
$active = $i === $nextIdx;
?>
<div title="Couleur <?= $i + 1 ?>"
style="width:28px;height:28px;border-radius:6px;background:<?= htmlspecialchars($g) ?>;
<?= $active ? 'outline:2px solid #0d6efd;outline-offset:2px' : 'opacity:.75' ?>">
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<?php
$content = ob_get_clean();
$title = 'Catégories';
include __DIR__ . '/layout.php';
+113
View File
@@ -0,0 +1,113 @@
<?php ob_start(); ?>
<div class="row justify-content-center">
<div class="col-lg-8 col-xl-7">
<div class="d-flex align-items-center gap-3 mb-4">
<a href="/?action=import_image&uuid=<?= rawurlencode($ackArticle['uuid']) ?>"
class="btn btn-secondary btn-sm">← Retour</a>
<h1 class="h4 mb-0">Confirmation — droits d'auteur</h1>
</div>
<div class="alert alert-warning d-flex gap-2 mb-4">
<span style="font-size:1.3rem;flex-shrink:0">⚠</span>
<div>
<strong>Vous êtes sur le point de copier ce fichier sur votre serveur :</strong>
<code class="d-block mt-1 text-break small"><?= htmlspecialchars($ackUrl) ?></code>
</div>
</div>
<!-- Contexte légal -->
<div class="card mb-3">
<div class="card-header fw-semibold">Ce que dit la loi française</div>
<div class="card-body small lh-base">
<p>En France, <strong>toute œuvre de l'esprit est protégée dès sa création</strong> sans
formalité d'enregistrement (art. L.111-1 CPI). Cela inclut les photographies, illustrations,
textes, vidéos et musiques.</p>
<p>Reproduire ou diffuser publiquement une œuvre sans l'autorisation de son auteur constitue
une <strong>contrefaçon</strong> (art. L.335-2 CPI), passible de :</p>
<ul class="mb-2">
<li><strong>3 ans d'emprisonnement</strong></li>
<li><strong>300 000 € d'amende</strong></li>
</ul>
<p class="mb-0">L'exception d'<em>usage privé</em> (art. L.122-5 1° CPI) est strictement
personnelle et <strong>ne couvre pas la publication sur un blog</strong>, même non commercial
et même à audience restreinte.</p>
</div>
</div>
<!-- Cas autorisés -->
<div class="card mb-3 border-success">
<div class="card-header fw-semibold text-success-emphasis bg-success-subtle">
✓ Cas où vous pouvez légalement télécharger ce fichier
</div>
<ul class="list-group list-group-flush small">
<li class="list-group-item">
<strong>Vous êtes l'auteur</strong> ou co-auteur du fichier
</li>
<li class="list-group-item">
Le fichier est distribué sous une <strong>licence libre compatible</strong> avec la
reproduction publique : <span class="font-monospace">CC0</span>,
<span class="font-monospace">CC BY</span>,
<span class="font-monospace">CC BY-SA</span>,
<span class="font-monospace">CC BY-ND</span>,
domaine public, etc.<br>
<small class="text-danger">⚠ CC BY-NC ne suffit pas si le blog génère des revenus,
même indirects.</small>
</li>
<li class="list-group-item">
Vous disposez d'une <strong>autorisation écrite explicite</strong> de l'auteur ou
du titulaire des droits patrimoniaux
</li>
<li class="list-group-item">
L'œuvre est dans le <strong>domaine public</strong> : 70 ans révolus après le décès
de l'auteur en Union Européenne (art. L.123-1 CPI)
</li>
</ul>
</div>
<!-- Formulaire de confirmation -->
<form method="POST"
action="/?action=add_file_from_url&uuid=<?= rawurlencode($ackArticle['uuid']) ?>">
<input type="hidden" name="image_url" value="<?= htmlspecialchars($ackUrl) ?>">
<input type="hidden" name="img_title" value="<?= htmlspecialchars($ackTitle) ?>">
<input type="hidden" name="img_author" value="<?= htmlspecialchars($ackAuthor) ?>">
<input type="hidden" name="img_source" value="<?= htmlspecialchars($ackSource) ?>">
<input type="hidden" name="meta_json" value="<?= htmlspecialchars($ackMetaJson) ?>">
<input type="hidden" name="mode" value="download">
<input type="hidden" name="copyright_acked" value="1">
<?php if ($ackIsCover): ?>
<input type="hidden" name="is_cover" value="1">
<?php endif; ?>
<div class="card border-primary mb-4">
<div class="card-body">
<div class="form-check">
<input class="form-check-input" type="checkbox"
id="ack_check" name="ack_check" required>
<label class="form-check-label" for="ack_check">
<strong>Je certifie disposer des droits nécessaires</strong> pour reproduire
et publier ce fichier sur ce blog, conformément au Code de la Propriété
Intellectuelle. Je comprends que cette déclaration engage ma responsabilité
personnelle en cas de contentieux.
</label>
</div>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Télécharger et insérer</button>
<a href="/?action=import_image&uuid=<?= rawurlencode($ackArticle['uuid']) ?>"
class="btn btn-outline-secondary">Annuler</a>
</div>
</form>
</div>
</div>
<?php
$content = ob_get_clean();
$title = 'Droits d\'auteur — confirmation';
include __DIR__ . '/layout.php';
+78
View File
@@ -0,0 +1,78 @@
<?php
ob_start();
$revMeta = $revisions[$revIndex] ?? [];
?>
<div class="d-flex align-items-center gap-3 mb-3 flex-wrap">
<a href="/?action=edit&uuid=<?= htmlspecialchars($article['uuid']) ?>" class="btn btn-secondary btn-sm">← Retour</a>
<div>
<strong><?= htmlspecialchars($article['title']) ?></strong>
&mdash; révision #<?= (int)($revMeta['n'] ?? $revIndex + 1) ?>
<span class="text-muted small">
du <?= htmlspecialchars(date('d/m/Y H:i', strtotime((string)($revMeta['date'] ?? '')))) ?>
<?= !empty($revMeta['comment']) ? '— ' . htmlspecialchars($revMeta['comment']) : '' ?>
</span>
</div>
</div>
<div class="d-flex gap-4 mb-2 small">
<span class="diff-del px-2 py-1 rounded"> Supprimé</span>
<span class="diff-ins px-2 py-1 rounded">+ Ajouté</span>
<span class="diff-eq px-2 py-1 rounded text-muted">= Inchangé</span>
</div>
<?php if ($diffLines === []): ?>
<div class="alert alert-success">Aucune différence — le contenu est identique.</div>
<?php else: ?>
<?php
// Groupe les lignes : affiche contexte de 3 lignes autour des changements
$CONTEXT = 3;
$total = count($diffLines);
$show = [];
for ($i = 0; $i < $total; $i++) {
if ($diffLines[$i][0] !== '=') {
for ($c = max(0, $i - $CONTEXT); $c <= min($total - 1, $i + $CONTEXT); $c++) {
$show[$c] = true;
}
}
}
$inEllipsis = false;
?>
<div class="diff-view font-monospace small">
<?php for ($i = 0; $i < $total; $i++): ?>
<?php [$op, $line] = $diffLines[$i]; ?>
<?php if (!isset($show[$i])): ?>
<?php if (!$inEllipsis): $inEllipsis = true; ?>
<div class="diff-ellipsis text-muted px-2">⋯</div>
<?php endif;
continue; ?>
<?php else: $inEllipsis = false; endif; ?>
<?php if ($op === '!'): ?>
<div class="diff-warning text-warning px-2"><?= htmlspecialchars($line) ?></div>
<?php elseif ($op === '-'): ?>
<div class="diff-del px-2">&nbsp;<?= htmlspecialchars($line) ?></div>
<?php elseif ($op === '+'): ?>
<div class="diff-ins px-2">+&nbsp;<?= htmlspecialchars($line) ?></div>
<?php else: ?>
<div class="diff-eq px-2 text-muted">&nbsp;&nbsp;<?= htmlspecialchars($line) ?></div>
<?php endif; ?>
<?php endfor; ?>
</div>
<?php endif; ?>
<style>
.diff-view { border: 1px solid var(--bs-border-color, #dee2e6); border-radius: 6px; overflow-x: auto; }
.diff-view > div { padding: 1px 8px; white-space: pre; line-height: 1.5; }
.diff-del { background: #ffeef0; color: #b91c1c; }
.diff-ins { background: #e6ffec; color: #15803d; }
.diff-eq { }
.diff-ellipsis { background: #f8f9fa; padding: 2px 8px; user-select: none; }
</style>
<?php
$content = ob_get_clean();
$title = 'Diff — ' . htmlspecialchars($article['title']);
include __DIR__ . '/layout.php';
+41
View File
@@ -0,0 +1,41 @@
<?php ob_start(); ?>
<div class="d-flex align-items-center gap-3 mb-4">
<a href="/?action=edit&uuid=<?= rawurlencode($importArticle['uuid']) ?>" class="btn btn-secondary btn-sm">← Retour</a>
<h1 class="h4 mb-0">Importer un fichier depuis une URL</h1>
</div>
<p class="text-muted small mb-4">
Article : <strong><?= htmlspecialchars($importArticle['title']) ?></strong>
</p>
<?php if ($importError): ?>
<div class="alert alert-warning">
URL invalide ou inaccessible — vérifiez que le lien est correct et que le serveur peut y accéder.
</div>
<?php endif; ?>
<div class="card" style="max-width:640px">
<div class="card-body">
<form method="POST" action="/?action=import_image_step2&uuid=<?= rawurlencode($importArticle['uuid']) ?>">
<div class="mb-4">
<label class="form-label fw-semibold">URL du fichier <span class="text-danger">*</span></label>
<input type="url" name="image_url" class="form-control font-monospace"
placeholder="https://…/document.pdf"
value="<?= htmlspecialchars($_GET['image_url'] ?? '') ?>"
required autofocus>
<div class="form-text">Les métadonnées seront récupérées automatiquement à l'étape suivante.</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Suivant →</button>
<a href="/?action=edit&uuid=<?= rawurlencode($importArticle['uuid']) ?>"
class="btn btn-outline-secondary">Annuler</a>
</div>
</form>
</div>
</div>
<?php
$content = ob_get_clean();
$title = 'Importer un fichier — ' . htmlspecialchars($importArticle['title']);
include __DIR__ . '/layout.php';
+205
View File
@@ -0,0 +1,205 @@
<?php
ob_start();
$isPdf = ($step2Meta['mime'] ?? '') === 'application/pdf';
$isHtml = str_starts_with($step2Meta['mime'] ?? '', 'text/html');
// Ordre + labels contextuels
$metaRows = [
'mime' => 'Type',
'size' => 'Taille',
// PDF
'pages' => 'Pages',
'page_size' => 'Format',
'pdf_version' => 'Version PDF',
// Image
'width' => 'Dimensions',
'camera' => 'Appareil',
// HTML
'site_name' => 'Site',
'og_type' => 'Type',
'language' => 'Langue',
// Commun
'author' => $isHtml ? 'Auteur' : ($isPdf ? 'Auteur' : 'Auteur EXIF'),
'date' => $isPdf ? 'Créé le' : ($isHtml ? 'Publié le' : 'Prise de vue'),
'description' => 'Description',
'subject' => 'Sujet',
'keywords' => 'Mots-clés',
'copyright' => 'Copyright',
// PDF logiciel
'creator' => 'Créé avec',
'producer' => 'Produit par',
// HTML liens
'canonical' => 'URL canonique',
'og_image' => 'Image OG',
];
$hasTitle = !empty($step2Meta['title']);
$preAuthor = $step2Meta['author'] ?? $step2Meta['credit'] ?? '';
$preSource = $step2Meta['canonical'] ?? $step2Meta['source'] ?? $step2Url;
?>
<div class="d-flex align-items-center gap-3 mb-4">
<a href="/?action=import_image&uuid=<?= rawurlencode($step2Article['uuid']) ?>" class="btn btn-secondary btn-sm">← Retour</a>
<h1 class="h4 mb-0">Importer un fichier</h1>
</div>
<p class="text-muted small mb-4">
Article : <strong><?= htmlspecialchars($step2Article['title']) ?></strong>
</p>
<?php if ($step2Screenshot ?? null): ?>
<!-- Aperçu screenshot -->
<div class="mb-4">
<p class="fw-semibold small mb-2">Aperçu de la page</p>
<?php
$previewMtime = @filemtime(BASE_PATH . '/data/' . $step2Article['uuid'] . '/files/' . $step2Screenshot) ?: time();
?>
<img src="/file?uuid=<?= rawurlencode($step2Article['uuid']) ?>&name=<?= rawurlencode($step2Screenshot) ?>&v=<?= $previewMtime ?>"
class="img-fluid rounded shadow-sm d-block"
style="max-height:320px;object-fit:cover;object-position:top"
alt="Aperçu">
</div>
<?php endif; ?>
<!-- Résumé métadonnées -->
<?php
$visibleRows = array_filter($metaRows, fn ($label, $key) => !empty($step2Meta[$key]), ARRAY_FILTER_USE_BOTH);
if ($visibleRows): ?>
<div class="card mb-4" style="max-width:640px">
<div class="card-header small fw-semibold">Métadonnées du fichier</div>
<div class="card-body p-0">
<table class="table table-sm table-borderless mb-0 small">
<tbody>
<?php foreach ($visibleRows as $key => $label): ?>
<?php
$val = $step2Meta[$key];
$cellHtml = match($key) {
'size' => htmlspecialchars(round($val / 1024) . ' Ko'),
'width' => htmlspecialchars($val . ' × ' . ($step2Meta['height'] ?? '?') . ' px'),
'og_image' => '<img src="' . htmlspecialchars((string)$val) . '" style="max-height:72px;max-width:200px;border-radius:4px" alt="">',
'canonical' => '<a href="' . htmlspecialchars((string)$val) . '" target="_blank" rel="noopener" class="small text-break">' . htmlspecialchars((string)$val) . '</a>',
default => htmlspecialchars((string)$val),
};
?>
<tr>
<th class="text-muted fw-normal ps-3 pe-3 text-nowrap align-top" style="width:130px"><?= $label ?></th>
<td class="pe-3"><?= $cellHtml ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<!-- Formulaire -->
<div class="card" style="max-width:640px">
<div class="card-body">
<form method="POST" action="/?action=add_file_from_url&uuid=<?= rawurlencode($step2Article['uuid']) ?>">
<input type="hidden" name="image_url" value="<?= htmlspecialchars($step2Url) ?>">
<?php if ($step2Screenshot ?? null): ?>
<input type="hidden" name="screenshot_file" value="<?= htmlspecialchars($step2Screenshot) ?>">
<?php endif; ?>
<?php
$metaToStore = array_filter(
array_diff_key($step2Meta, array_flip(['ok', 'height'])),
fn ($v) => $v !== null && $v !== ''
);
?>
<input type="hidden" name="meta_json"
value="<?= htmlspecialchars(json_encode($metaToStore, JSON_UNESCAPED_UNICODE)) ?>">
<!-- Titre (obligatoire) -->
<div class="mb-3">
<label class="form-label fw-semibold">
Titre <span class="text-danger">*</span>
</label>
<input type="text" name="img_title" class="form-control"
placeholder="ex. Compte rendu du conseil municipal"
value="<?= htmlspecialchars($step2Meta['title'] ?? '') ?>"
required autofocus>
<?php if (!$hasTitle): ?>
<div class="form-text text-warning small">
Titre non trouvé dans les métadonnées — saisie requise.
</div>
<?php endif; ?>
</div>
<!-- Mode -->
<div class="mb-4">
<p class="form-label fw-semibold mb-2">Mode</p>
<div class="form-check mb-1">
<input class="form-check-input" type="radio" name="mode" id="mode_link" value="link" checked>
<label class="form-check-label" for="mode_link">
<strong>Lien externe</strong>
<span class="text-muted small"> — insère une référence, le fichier reste chez l'hôte</span>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="mode" id="mode_download" value="download">
<label class="form-check-label" for="mode_download">
<strong>Télécharger sur le serveur</strong>
<span class="text-muted small"> — copie locale du fichier</span>
</label>
</div>
<?php if ($step2Screenshot ?? null): ?>
<div class="form-check">
<input class="form-check-input" type="radio" name="mode" id="mode_screenshot" value="screenshot">
<label class="form-check-label" for="mode_screenshot">
<strong>Enregistrer la capture d'écran</strong>
<span class="text-muted small"> — sauvegarde l'aperçu comme image</span>
</label>
</div>
<?php endif; ?>
<div id="copyright-warning" class="alert alert-warning mt-3 mb-0 small" style="display:none">
<strong>Droits d'auteur.</strong> Une page de confirmation légale vous sera
présentée avant le téléchargement.
</div>
</div>
<!-- Auteur / source : toujours visibles pour les pages web, download-only sinon -->
<div id="download-fields" <?= $isHtml ? '' : 'style="display:none"' ?>>
<?php if ($preAuthor || $isHtml): ?>
<div class="mb-3">
<label class="form-label fw-semibold">Auteur / crédit</label>
<input type="text" name="img_author" class="form-control"
placeholder="ex. Jane Doe"
value="<?= htmlspecialchars($preAuthor) ?>">
</div>
<?php endif; ?>
<div class="mb-3">
<label class="form-label fw-semibold">URL source</label>
<input type="url" name="img_source" class="form-control font-monospace"
placeholder="https://…"
value="<?= htmlspecialchars($preSource) ?>">
<div class="form-text">Laissé vide → URL du fichier utilisée comme source.</div>
</div>
<?php if (!$isHtml || ($step2Screenshot ?? null)): ?>
<div class="mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="is_cover" id="is_cover">
<label class="form-check-label" for="is_cover">
Définir comme image de couverture
<span class="text-muted small">(images uniquement, sera nommée <code>cover.jpg</code>)</span>
</label>
</div>
</div>
<?php endif; ?>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Valider</button>
<a href="/?action=edit&uuid=<?= rawurlencode($step2Article['uuid']) ?>"
class="btn btn-outline-secondary">Annuler</a>
</div>
</form>
</div>
</div>
<?php
$content = ob_get_clean();
$title = 'Importer un fichier — ' . htmlspecialchars($step2Article['title']);
include __DIR__ . '/layout.php';
+44 -7
View File
@@ -49,17 +49,54 @@
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarContent">
<ul class="navbar-nav ms-auto">
<?php if (function_exists('isAdmin') && isAdmin()): ?>
<li class="nav-item"><a class="nav-link" href="/?action=create">Nouveau post</a></li>
<?php
$_layoutAction = $_GET['action'] ?? 'list';
if (($_layoutAction === 'list' || $_layoutAction === '') && isset($articles)):
$_layoutPrivateCats = $articles->getPrivateCategories();
$_layoutCats = array_filter(
$articles->getCategories(),
function ($cat) use ($_layoutPrivateCats) {
return isLoggedIn() || !in_array($cat, $_layoutPrivateCats, true);
},
ARRAY_FILTER_USE_KEY
);
$_layoutCurrentCat = trim($_GET['cat'] ?? '');
if (!empty($_layoutCats)):
?>
<ul class="navbar-nav me-auto navbar-cats flex-nowrap overflow-auto gap-1">
<li class="nav-item">
<a class="nav-link nav-cat <?= $_layoutCurrentCat === '' ? 'active' : '' ?>" href="/">Tous</a>
</li>
<?php foreach ($_layoutCats as $catName => $catCount):
$isPriv = in_array($catName, $_layoutPrivateCats, true); ?>
<li class="nav-item">
<a class="nav-link nav-cat <?= $_layoutCurrentCat === $catName ? 'active' : '' ?>"
href="/?cat=<?= rawurlencode($catName) ?>">
<?= htmlspecialchars($catName) ?>
<?php if ($isPriv): ?><span class="ms-1" style="font-size:.65em;opacity:.6">🔒</span><?php endif; ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<ul class="navbar-nav me-auto"></ul>
<?php endif;
else: ?>
<ul class="navbar-nav me-auto"></ul>
<?php endif; ?>
<ul class="navbar-nav">
<?php if (function_exists('isLoggedIn') && isLoggedIn()): ?>
<li class="nav-item"><a class="nav-link" href="/?action=admin">Admin</a></li>
<?php endif; ?>
<?php if (function_exists('isLoggedIn') && isLoggedIn()): ?>
<li class="nav-item">
<a class="nav-link" href="/logout.php" title="Déconnexion">
<?= htmlspecialchars(currentUserEmail() ?? '') ?>
<small class="text-muted">(déconnexion)</small>
<a class="nav-link" href="/?action=profile">
<?= htmlspecialchars(function_exists('currentUserName') ? currentUserName() : (currentUserEmail() ?? '')) ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link text-muted" href="/logout.php" title="Déconnexion">Déconnexion</a>
</li>
<?php else: ?>
<li class="nav-item"><a class="nav-link" href="/login">Connexion</a></li>
<?php endif; ?>
@@ -69,7 +106,7 @@
</nav>
</header>
<main class="container" role="main">
<main class="<?= htmlspecialchars($mainClass ?? 'container') ?>" role="main">
<?= $content ?>
</main>
+234 -81
View File
@@ -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
&mdash; <?= 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 &amp; 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}](${name})`;
navigator.clipboard.writeText(ref).then(() => {
const orig = btn.textContent;
btn.textContent = 'Copié !';
setTimeout(() => { btn.textContent = orig; }, 1500);
});
}
</script>
<?php
$content = ob_get_clean();
+26 -11
View File
@@ -2,15 +2,6 @@
require_once BASE_PATH . '/src/Parsedown.php';
$Parsedown = new Parsedown();
$coverGradients = [
'linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%)',
'linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%)',
'linear-gradient(135deg, #fce7f3 0%, #fbcfe8 100%)',
'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
'linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)',
'linear-gradient(135deg, #ede9fe 0%, #ddd6fe 100%)',
];
ob_start();
?>
@@ -19,10 +10,13 @@ ob_start();
<?php
$html = $Parsedown->text($post['content']);
$preview = mb_strimwidth(strip_tags($html), 0, 120, '…');
$gradient = $coverGradients[$i % count($coverGradients)];
$category = trim((string)($post['category'] ?? ''));
$gradient = coverGradient($category !== '' ? $category : $post['uuid'], $allCats ?? []);
$postUrl = '/post/' . rawurlencode($post['slug']);
$isDraft = !$post['published'];
$isAvantPremiere = $post['published'] && strtotime((string)($post['published_at'] ?? '')) > time();
$postCat = trim($post['category'] ?? '');
$isPrivate = $postCat !== '' && in_array($postCat, $privateCats ?? [], true);
$isLocked = $isAvantPremiere;
?>
<div class="col">
@@ -31,6 +25,8 @@ ob_start();
<div class="draft-ribbon">Brouillon</div>
<?php elseif ($isAvantPremiere): ?>
<div class="premiere-ribbon">Avant-première</div>
<?php elseif ($isPrivate): ?>
<div class="private-ribbon">Privé</div>
<?php endif; ?>
<?php
$coverFile = $post['cover'] ?? '';
@@ -38,7 +34,11 @@ ob_start();
? 'background-image: url(\'/file?uuid=' . rawurlencode($post['uuid']) . '&name=' . rawurlencode($coverFile) . '\')'
: 'background: ' . $gradient;
?>
<div class="card-cover" style="<?= $coverStyle ?>"></div>
<div class="card-cover" style="<?= $coverStyle ?>">
<?php if ($category !== ''): ?>
<span class="cover-category"><?= htmlspecialchars($category) ?></span>
<?php endif; ?>
</div>
<div class="card-body d-flex flex-column">
<h2 class="card-title">
<?php if ($isLocked): ?>
@@ -72,6 +72,21 @@ ob_start();
<?php endforeach; ?>
</div>
<?php if ($prevCursor !== null || $nextCursor !== null): ?>
<nav class="pagination-nav mt-5" aria-label="Navigation">
<?php
$catParam = $filterCat !== '' ? 'cat=' . rawurlencode($filterCat) . '&' : '';
?>
<?php if ($prevCursor !== null): ?>
<?php $prevHref = $prevCursor === '' ? '/?' . rtrim($catParam, '&') : '/?' . $catParam . 'cursor=' . rawurlencode($prevCursor); ?>
<a class="pagination-btn" href="<?= htmlspecialchars($prevHref) ?>">← Plus récents</a>
<?php endif; ?>
<?php if ($nextCursor !== null): ?>
<a class="pagination-btn ms-auto" href="/?<?= $catParam ?>cursor=<?= rawurlencode($nextCursor) ?>">Plus anciens →</a>
<?php endif; ?>
</nav>
<?php endif; ?>
<?php
$content = ob_get_clean();
$title = 'varlog';
+231 -65
View File
@@ -3,89 +3,254 @@ require_once __DIR__ . '/../src/Parsedown.php';
$Parsedown = new Parsedown();
ob_start();
?>
<a href="/" class="btn btn-secondary mb-3">← Retour</a>
<?php
$coverFile = $article['cover'] ?? '';
$ogImage = $coverFile !== ''
? url('file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($coverFile))
: null;
?>
<div class="card mb-4">
<?php if (!$article['published']): ?>
<div class="draft-ribbon">Brouillon</div>
<?php endif; ?>
<?php if ($coverFile !== ''): ?>
<div class="article-cover">
<img src="/file?uuid=<?= rawurlencode($article['uuid']) ?>&name=<?= rawurlencode($coverFile) ?>"
alt="<?= htmlspecialchars($article['title']) ?>">
</div>
<?php endif; ?>
<div class="card-body">
<h2 class="card-title"><?= htmlspecialchars($article['title']) ?></h2>
<div class="card-text post-content">
<?= $Parsedown->text($rawContent) ?>
</div>
$category = trim((string)($article['category'] ?? ''));
$gradient = coverGradient($category !== '' ? $category : $article['uuid'], $allCats ?? []);
<p class="text-muted small mt-2">
Publié le <?= htmlspecialchars(date('d/m/Y H:i', strtotime((string)($article['published_at'] ?? $article['created_at'] ?? '')))) ?>
</p>
</div>
</div>
<?php if ($files): ?>
<?php
// Sépare les fichiers intégrés (référencés dans le contenu) des pièces jointes
// Pièces jointes (hors fichiers intégrés, thumbs et cover)
$attachments = [];
if ($files) {
$referenced = [];
preg_match_all('/\(\/file\?uuid=[^&]+&name=([^)]+)\)/', $rawContent, $m);
foreach ($m[1] as $encodedName) {
$referenced[rawurldecode($encodedName)] = true;
}
$attachments = array_filter($files, static fn ($f) => !isset($referenced[$f['name']]));
?>
<?php if ($attachments): ?>
<section class="mb-4">
<h5>Pièces jointes</h5>
<div class="row g-3">
<?php foreach ($attachments as $file): ?>
<div class="col-sm-6 col-md-4">
<div class="card">
<?php
$fileUrl = '/file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($file['name']);
?>
<?php if ($file['is_image']): ?>
<img src="<?= htmlspecialchars($fileUrl) ?>" class="card-img-top" alt="<?= htmlspecialchars($file['name']) ?>" style="max-height:200px;object-fit:cover">
<?php elseif ($file['is_video']): ?>
<video controls class="w-100" style="max-height:200px"><source src="<?= htmlspecialchars($fileUrl) ?>"></video>
<?php elseif ($file['is_audio']): ?>
<audio controls class="w-100"><source src="<?= htmlspecialchars($fileUrl) ?>"></audio>
<?php endif; ?>
<div class="card-body p-2">
<a href="<?= htmlspecialchars($fileUrl) ?>" class="card-title small d-block text-truncate" target="_blank">
<?= htmlspecialchars($file['name']) ?>
</a>
<small class="text-muted"><?= htmlspecialchars(number_format($file['size'] / 1024, 1)) ?> Ko</small>
</div>
</div>
</div>
$attachments = array_values(array_filter(
$files,
static fn ($f) =>
!isset($referenced[$f['name']])
&& !str_starts_with($f['name'], '_thumb_')
&& $f['name'] !== $coverFile
));
}
$externalLinks = $article['external_links'] ?? [];
$hasLeftSidebar = !empty($categorySidebar ?? []);
?>
<div class="row g-4 align-items-start flex-nowrap-lg">
<?php if ($hasLeftSidebar): ?>
<div class="post-sidebar-col order-2 order-lg-1">
<aside class="left-sidebar">
<?php foreach ($categorySidebar as $catName => $catArticles): ?>
<div class="left-sidebar-section">
<a href="/?cat=<?= rawurlencode($catName) ?>" class="left-sidebar-cat">
<?= htmlspecialchars($catName) ?>
</a>
<ul class="left-sidebar-list">
<?php foreach ($catArticles as $ca): ?>
<li>
<a href="/post/<?= rawurlencode($ca['slug'] ?? '') ?>">
<?= htmlspecialchars($ca['title']) ?>
</a>
</li>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
</ul>
</div>
<?php endforeach; ?>
</aside>
</div>
<?php endif; ?>
<?php if (function_exists('isAdmin') && isAdmin()): ?>
<div class="d-flex gap-2 mt-3">
<a href="/?action=edit&uuid=<?= htmlspecialchars($article['uuid']) ?>" class="btn btn-primary">Modifier</a>
<a href="/?action=delete&uuid=<?= htmlspecialchars($article['uuid']) ?>"
class="btn btn-danger"
onclick="return confirm('Supprimer cet article définitivement ?')">Supprimer</a>
<!-- Colonne principale -->
<div class="col order-1 order-lg-2">
<div class="card mb-4">
<?php if (!$article['published']): ?>
<div class="draft-ribbon">Brouillon</div>
<?php elseif ($isPrivateCat ?? false): ?>
<div class="private-ribbon">Privé</div>
<?php endif; ?>
<?php
$authorEmail = $article['author'] ?? '';
$authorName = ($authorEmail !== '' && function_exists('authorDisplayName')) ? authorDisplayName($authorEmail) : '';
$pubDate = htmlspecialchars(date('d/m/Y', strtotime((string)($article['published_at'] ?? $article['created_at'] ?? ''))));
$hasCover = $coverFile !== '';
$heroExtraClass = $hasCover ? '' : ' article-cover--gradient';
$heroStyle = $hasCover ? '' : ' style="background:' . htmlspecialchars($gradient) . '"';
$hasSources = (!empty($externalLinks) || !empty($files))
&& function_exists('canDoOnArticle') && canDoOnArticle('view_sources', $article);
?>
<div class="article-cover article-cover--hero<?= $heroExtraClass ?>"<?= $heroStyle ?>>
<?php if ($hasCover): ?>
<img src="/file?uuid=<?= rawurlencode($article['uuid']) ?>&name=<?= rawurlencode($coverFile) ?>"
alt="<?= htmlspecialchars($article['title']) ?>">
<?php endif; ?>
<div class="article-hero-text">
<!-- Haut : retour + actions admin -->
<div class="article-hero-top">
<a href="/" class="hero-btn">← Retour</a>
<?php if (function_exists('isAdmin') && isAdmin()): ?>
<a href="/?action=edit&uuid=<?= rawurlencode($article['uuid']) ?>" class="hero-btn ms-auto">✎ Modifier</a>
<a href="/?action=delete&uuid=<?= rawurlencode($article['uuid']) ?>"
class="hero-btn hero-btn--danger"
data-confirm="Supprimer cet article définitivement ?">🗑 Supprimer</a>
<?php endif; ?>
</div>
<!-- Bas : titre + actions secondaires -->
<div class="article-hero-bottom">
<div class="article-hero-left">
<?php if ($category !== ''): ?>
<span class="cover-category"><?= htmlspecialchars($category) ?></span>
<?php endif; ?>
<h1 class="article-title"><?= htmlspecialchars($article['title']) ?></h1>
<p class="article-hero-meta">
<?php if ($authorName !== ''): ?>
<span><?= htmlspecialchars($authorName) ?></span>
<span class="mx-1 opacity-50">·</span>
<?php endif; ?>
<?= $pubDate ?>
</p>
</div>
<div class="article-hero-right">
<?php if ($hasSources): ?>
<a href="/?action=sources&uuid=<?= rawurlencode($article['uuid']) ?>" class="hero-btn"> Sources</a>
<?php endif; ?>
<?php if (function_exists('isLoggedIn') && isLoggedIn()): ?>
<form method="post" action="/?action=rate" class="d-flex align-items-center gap-2">
<input type="hidden" name="uuid" value="<?= htmlspecialchars($article['uuid']) ?>">
<?php if ($ratingStats['count'] > 0): ?>
<span class="hero-rating-score">
<?= number_format((float)($ratingStats['avg'] ?? 0), 1) ?> <span style="opacity:.6">/ 5</span>
</span>
<?php endif; ?>
<div class="star-rating star-rating--hero">
<?php for ($s = 5; $s >= 1; $s--): ?>
<input type="radio" id="star<?= $s ?>-<?= $article['uuid'] ?>"
name="rating" value="<?= $s ?>"
<?= (int)($userRating ?? 0) === $s ? 'checked' : '' ?>
onchange="this.form.submit()">
<label for="star<?= $s ?>-<?= $article['uuid'] ?>" title="<?= $s ?>★">★</label>
<?php endfor; ?>
</div>
</form>
<?php elseif ($ratingStats['count'] > 0): ?>
<span class="hero-rating-score">
★ <?= number_format((float)($ratingStats['avg'] ?? 0), 1) ?>
<span style="opacity:.6">(<?= $ratingStats['count'] ?>)</span>
</span>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="card-body">
<div class="card-text post-content">
<?= $Parsedown->text($rawContent) ?>
</div>
</div>
</div>
<?php if (!isLoggedIn() && $ratingStats['count'] > 0): ?>
<p class="text-muted small mt-3">
Note : <?= number_format((float)($ratingStats['avg'] ?? 0), 1) ?>/5
— <a href="/login">Connectez-vous</a> pour noter.
</p>
<?php endif; ?>
</div><!-- /col principale -->
<div class="post-sidebar-col order-3">
<aside class="related-sidebar">
<?php if (!empty($attachments)): ?>
<h6 class="related-sidebar-title">Pièces jointes</h6>
<div class="d-flex flex-column gap-2 mb-4">
<?php foreach ($attachments as $file):
$fileUrl = '/file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($file['name']);
?>
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener" class="source-card">
<?php if ($file['is_image']): ?>
<div class="source-card-thumb" style="background-image:url(<?= htmlspecialchars($fileUrl) ?>);background-size:cover;background-position:center"></div>
<?php elseif ($file['is_video']): ?>
<div class="source-card-thumb source-card-thumb--link">▶</div>
<?php elseif ($file['is_audio']): ?>
<div class="source-card-thumb source-card-thumb--link">♪</div>
<?php else: ?>
<div class="source-card-thumb source-card-thumb--pdf">📎</div>
<?php endif; ?>
<div class="source-card-body">
<div class="source-card-title"><?= htmlspecialchars($file['name']) ?></div>
<div class="source-card-meta"><?= htmlspecialchars(number_format($file['size'] / 1024, 1)) ?> Ko</div>
</div>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($externalLinks)): ?>
<h6 class="related-sidebar-title">Liens &amp; sources</h6>
<div class="d-flex flex-column gap-2 mb-4">
<?php foreach ($externalLinks as $lnk):
$lMeta = $lnk['meta'] ?? [];
$lTitle = $lnk['name'] ?? '';
$lUrl = $lnk['url'] ?? '';
$lHost = parse_url($lUrl, PHP_URL_HOST) ?? $lUrl;
$lDate = $lMeta['date'] ?? '';
$lSite = $lMeta['site_name'] ?? $lHost;
$lImage = $lMeta['og_image'] ?? '';
$lMime = $lMeta['mime'] ?? 'text/html';
$lPages = $lMeta['pages'] ?? null;
$lFormat = $lMeta['page_size'] ?? '';
$isPdf = ($lMime === 'application/pdf');
?>
<a href="<?= htmlspecialchars($lUrl) ?>" target="_blank" rel="noopener" class="source-card">
<?php if ($lImage && str_starts_with($lImage, '/')): ?>
<div class="source-card-thumb" style="background-image:url(<?= htmlspecialchars($lImage) ?>);background-size:cover;background-position:center"></div>
<?php elseif ($isPdf): ?>
<div class="source-card-thumb source-card-thumb--pdf">📑</div>
<?php else: ?>
<div class="source-card-thumb source-card-thumb--link">↗</div>
<?php endif; ?>
<div class="source-card-body">
<div class="source-card-title"><?= htmlspecialchars($lTitle) ?></div>
<div class="source-card-meta">
<?= htmlspecialchars($lSite) ?>
<?php if ($lDate): ?> · <?= htmlspecialchars(substr($lDate, 0, 10)) ?><?php endif; ?>
<?php if ($isPdf && $lPages): ?> · PDF <?= $lPages ?>p.<?php endif; ?>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<h6 class="related-sidebar-title">Dans la même catégorie</h6>
<?php if (!empty($relatedArticles ?? [])): ?>
<?php foreach ($relatedArticles as $rel):
$relCover = $rel['cover'] ?? '';
$relCat = trim($rel['category'] ?? '');
$relGradient = coverGradient($relCat !== '' ? $relCat : $rel['uuid'], $allCats ?? []);
$relDate = date('d/m/Y', strtotime((string)($rel['published_at'] ?? $rel['created_at'] ?? '')));
?>
<a href="/post/<?= rawurlencode($rel['slug'] ?? '') ?>" class="related-card">
<div class="related-card-thumb" style="<?= $relCover !== ''
? 'background-image:url(/file?uuid=' . rawurlencode($rel['uuid']) . '&name=' . rawurlencode($relCover) . ');background-size:cover;background-position:center'
: 'background:' . htmlspecialchars($relGradient) ?>">
</div>
<div class="related-card-body">
<div class="related-card-title"><?= htmlspecialchars($rel['title']) ?></div>
<div class="related-card-date"><?= $relDate ?></div>
</div>
</a>
<?php endforeach; ?>
<?php else: ?>
<p class="text-muted small">Aucun autre article dans cette catégorie.</p>
<?php endif; ?>
</aside>
</div>
</div><!-- /row -->
<?php
$content = ob_get_clean();
$title = htmlspecialchars($article['title']);
@@ -95,4 +260,5 @@ $ogImage = $article['og_image'] ?? '';
$ogType = 'article';
$ogUrl = url('post/' . rawurlencode($article['slug'] ?? ''));
$articlePublishedAt = $article['published_at'] ?? '';
$mainClass = 'container-fluid';
include __DIR__ . '/layout.php';
+41
View File
@@ -0,0 +1,41 @@
<?php ob_start(); ?>
<div class="d-flex align-items-center gap-3 mb-4">
<h1 class="h4 mb-0">Mon profil</h1>
</div>
<div class="row">
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-body">
<?php if ($profileSuccess): ?>
<div class="alert alert-success py-2 small mb-3">Profil mis à jour.</div>
<?php endif; ?>
<?php if ($profileError !== ''): ?>
<div class="alert alert-danger py-2 small mb-3"><?= htmlspecialchars($profileError) ?></div>
<?php endif; ?>
<form method="post" action="/?action=profile">
<div class="mb-3">
<label class="form-label fw-semibold" for="display_name">Nom affiché</label>
<input type="text" id="display_name" name="display_name"
class="form-control"
value="<?= htmlspecialchars($profileCurrentName) ?>"
placeholder="Prénom Nom" required>
<div class="form-text">Affiché comme auteur sur vos articles.</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold text-muted">Email</label>
<input type="text" class="form-control" value="<?= htmlspecialchars(currentUserEmail() ?? '') ?>" disabled>
</div>
<button type="submit" class="btn btn-primary w-100">Enregistrer</button>
</form>
</div>
</div>
</div>
</div>
<?php
$content = ob_get_clean();
$title = 'Mon profil';
include __DIR__ . '/layout.php';
+242
View File
@@ -0,0 +1,242 @@
<?php ob_start();
$metaLabels = [
'mime' => 'Type MIME',
'size' => 'Taille originale',
'pages' => 'Pages',
'page_size' => 'Format',
'pdf_version' => 'Version PDF',
'width' => 'Dimensions',
'camera' => 'Appareil photo',
'site_name' => 'Site',
'og_type' => 'Type OG',
'language' => 'Langue',
'date' => 'Date',
'description' => 'Description',
'subject' => 'Sujet',
'keywords' => 'Mots-clés',
'copyright' => 'Copyright',
'credit' => 'Crédit',
'creator' => 'Créé avec',
'producer' => 'Produit par',
'canonical' => 'URL canonique',
'og_image' => 'Image OG',
];
function renderMetaCell(string $key, mixed $val, array $row = []): string
{
return match($key) {
'size' => htmlspecialchars(number_format((float)$val / 1024, 1)) . ' Ko',
'width' => htmlspecialchars((string)$val) . ' × ' . htmlspecialchars((string)($row['height'] ?? '?')) . ' px',
'og_image' => str_starts_with((string)$val, '/')
? '<img src="' . htmlspecialchars((string)$val) . '" style="max-height:64px;max-width:160px;border-radius:4px" alt="">'
: '<a href="' . htmlspecialchars((string)$val) . '" target="_blank" rel="noopener" class="small text-break font-monospace">' . htmlspecialchars((string)$val) . '</a>',
'canonical' => '<a href="' . htmlspecialchars((string)$val) . '" target="_blank" rel="noopener" class="text-break font-monospace small">' . htmlspecialchars((string)$val) . '</a>',
default => htmlspecialchars((string)$val),
};
}
?>
<div class="d-flex align-items-center gap-3 mb-1">
<a href="/?action=edit&uuid=<?= rawurlencode($article['uuid']) ?>" class="btn btn-secondary btn-sm">← Modifier</a>
<h1 class="h4 mb-0">Sources &amp; médias</h1>
</div>
<p class="text-muted small mb-4"><?= htmlspecialchars($article['title']) ?></p>
<?php
// ── Liens & sources externes ──────────────────────────────────────────────────
$externalLinks = $article['external_links'] ?? [];
?>
<section class="mb-5">
<h2 class="h5 border-bottom pb-2 mb-3">
Liens &amp; sources externes
<span class="badge bg-secondary fw-normal ms-1"><?= count($externalLinks) ?></span>
</h2>
<?php if (empty($externalLinks)): ?>
<p class="text-muted">Aucun lien externe enregistré.</p>
<?php else: ?>
<div class="d-flex flex-column gap-3">
<?php foreach ($externalLinks as $lnk):
$lMeta = $lnk['meta'] ?? [];
$lMime = $lMeta['mime'] ?? 'text/html';
$isPdf = ($lMime === 'application/pdf');
$isImg = str_starts_with($lMime, 'image/');
$lHost = parse_url($lnk['url'] ?? '', PHP_URL_HOST) ?? '';
?>
<div class="card">
<div class="card-body p-3">
<div class="d-flex gap-3 align-items-start">
<!-- Vignette (locale uniquement pour respecter la CSP) -->
<?php if (!empty($lMeta['og_image']) && str_starts_with($lMeta['og_image'], '/')): ?>
<img src="<?= htmlspecialchars($lMeta['og_image']) ?>" alt=""
style="width:80px;height:60px;object-fit:cover;border-radius:4px;flex-shrink:0">
<?php else: ?>
<div style="width:40px;font-size:1.8rem;flex-shrink:0;text-align:center;padding-top:2px">
<?= $isPdf ? '📑' : ($isImg ? '🖼' : '🔗') ?>
</div>
<?php endif; ?>
<div class="flex-grow-1 overflow-hidden">
<!-- Titre + URL -->
<div class="fw-semibold mb-1"><?= htmlspecialchars($lnk['name'] ?? '') ?></div>
<a href="<?= htmlspecialchars($lnk['url'] ?? '') ?>" target="_blank" rel="noopener"
class="small text-break font-monospace text-muted">
<?= htmlspecialchars($lnk['url'] ?? '') ?>
</a>
<!-- Auteur · Date ajout -->
<div class="d-flex flex-wrap gap-3 mt-2 small">
<?php if (!empty($lnk['author'])): ?>
<span><span class="text-muted">Auteur :</span> <?= htmlspecialchars($lnk['author']) ?></span>
<?php endif; ?>
<?php if (!empty($lnk['added_at'])): ?>
<span class="text-muted">Ajouté le <?= htmlspecialchars(date('d/m/Y à H:i', strtotime((string)$lnk['added_at']))) ?></span>
<?php endif; ?>
</div>
<!-- Métadonnées -->
<?php
$visibleMeta = array_filter($lMeta, fn ($v, $k) => isset($metaLabels[$k]) && $v !== null && $v !== '' && $k !== 'height', ARRAY_FILTER_USE_BOTH);
?>
<?php if ($visibleMeta): ?>
<table class="table table-sm table-borderless mb-0 mt-2 small">
<tbody>
<?php foreach ($metaLabels as $key => $label):
if (!isset($lMeta[$key]) || $lMeta[$key] === '' || $lMeta[$key] === null) {
continue;
}
?>
<tr>
<th class="text-muted fw-normal text-nowrap pe-3 align-top" style="width:140px"><?= $label ?></th>
<td><?= renderMetaCell($key, $lMeta[$key], $lMeta) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<?php
// ── Pièces jointes ────────────────────────────────────────────────────────────
$filesMeta = $article['files_meta'] ?? [];
$coverFile = $article['cover'] ?? '';
$realFiles = array_values(array_filter($sourcesFiles, fn ($f) => !str_starts_with($f['name'], '_thumb_')));
?>
<section class="mb-5">
<h2 class="h5 border-bottom pb-2 mb-3">
Pièces jointes
<span class="badge bg-secondary fw-normal ms-1"><?= count($realFiles) ?></span>
</h2>
<?php if (empty($realFiles)): ?>
<p class="text-muted">Aucun fichier joint.</p>
<?php else: ?>
<div class="d-flex flex-column gap-3">
<?php foreach ($sourcesFiles as $f):
if (str_starts_with($f['name'], '_thumb_')) {
continue;
}
$fmeta = $filesMeta[$f['name']] ?? [];
$fExtra = $fmeta['meta'] ?? [];
$fileUrl = '/file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($f['name']);
$isCover = ($f['name'] === $coverFile);
?>
<div class="card">
<div class="card-body p-3">
<div class="d-flex gap-3 align-items-start">
<!-- Vignette -->
<?php if ($f['is_image']): ?>
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener" class="flex-shrink-0">
<img src="<?= htmlspecialchars($fileUrl) ?>" alt=""
style="width:80px;height:60px;object-fit:cover;border-radius:4px;<?= $isCover ? 'outline:2px solid #0d6efd' : '' ?>">
</a>
<?php else: ?>
<div style="width:40px;font-size:1.8rem;flex-shrink:0;text-align:center;padding-top:2px">
<?= match(true) {
str_starts_with($f['mime'], 'video/') => '🎬',
str_starts_with($f['mime'], 'audio/') => '🎵',
$f['mime'] === 'application/pdf' => '📑',
default => '📄',
} ?>
</div>
<?php endif; ?>
<div class="flex-grow-1 overflow-hidden">
<!-- Titre + nom fichier -->
<div class="d-flex align-items-center gap-2 flex-wrap mb-1">
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener" class="fw-semibold">
<?= htmlspecialchars($fmeta['title'] ?? $f['name']) ?>
</a>
<?php if (!empty($fmeta['title'])): ?>
<code class="small text-muted"><?= htmlspecialchars($f['name']) ?></code>
<?php endif; ?>
<?php if ($isCover): ?>
<span class="badge bg-primary">cover</span>
<?php endif; ?>
</div>
<!-- Auteur · Source · Taille -->
<div class="d-flex flex-wrap gap-3 small mb-1">
<span class="text-muted"><?= htmlspecialchars(number_format($f['size'] / 1024, 1)) ?> Ko</span>
<?php if (!empty($fmeta['author'])): ?>
<span><span class="text-muted">Auteur :</span> <?= htmlspecialchars($fmeta['author']) ?></span>
<?php endif; ?>
<?php if (!empty($fmeta['source_url'])): ?>
<span>
<span class="text-muted">Source :</span>
<a href="<?= htmlspecialchars($fmeta['source_url']) ?>" target="_blank" rel="noopener"
class="font-monospace text-break small">
<?= htmlspecialchars($fmeta['source_url']) ?>
</a>
</span>
<?php endif; ?>
</div>
<!-- Métadonnées EXIF/PDF -->
<?php
$visibleExtra = array_filter($fExtra, fn ($v, $k) => isset($metaLabels[$k]) && $v !== null && $v !== '' && $k !== 'height', ARRAY_FILTER_USE_BOTH);
?>
<?php if ($visibleExtra): ?>
<table class="table table-sm table-borderless mb-0 mt-1 small">
<tbody>
<?php foreach ($metaLabels as $key => $label):
if (!isset($fExtra[$key]) || $fExtra[$key] === '' || $fExtra[$key] === null) {
continue;
}
?>
<tr>
<th class="text-muted fw-normal text-nowrap pe-3 align-top" style="width:140px"><?= $label ?></th>
<td><?= renderMetaCell($key, $fExtra[$key], $fExtra) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php if (empty($fmeta) && empty($fExtra)): ?>
<span class="text-muted small">Pas de métadonnées enregistrées.</span>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<?php
$content = ob_get_clean();
$title = 'Sources — ' . htmlspecialchars($article['title']);
include __DIR__ . '/layout.php';