fix #29 : envoyer le lien magique par email (envoyer_mail_smtp)
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
ob_start();
|
||||
?>
|
||||
<div class="posts-list site-page">
|
||||
<?= $siteContent ?>
|
||||
</div>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = ($siteMeta['seo_title'] ?? $siteMeta['title'] ?? 'À propos') . ' — ' . siteTitle();
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
ob_start();
|
||||
$existingFiles = $articles->getFiles($addFilesArticle['uuid']);
|
||||
$articleUuid = $addFilesArticle['uuid'];
|
||||
$articleTitle = $addFilesArticle['title'];
|
||||
|
||||
// Extraire 1-3 mots significatifs du titre pour l'auto-recherche
|
||||
$_sfStop = ['ou','et','un','une','le','la','les','de','du','des','en','au','aux','ce','cet',
|
||||
'cette','ces','que','qui','par','sur','dans','son','sa','ses','mon','ton','nos',
|
||||
'vos','leur','leurs','voir','comment','quoi','dont','votre','notre','selon','car',
|
||||
'mais','donc','puis','plus','très','avec','pour','pas','est','sont','était',
|
||||
'être','avoir','faire','tout','tous','toute','toutes'];
|
||||
$_sfWords = preg_split('/[^a-zA-ZÀ-ÿ0-9]+/u', $articleTitle) ?: [];
|
||||
$_sfKw = [];
|
||||
foreach ($_sfWords as $_w) {
|
||||
if (mb_strlen($_w) >= 3 && !in_array(mb_strtolower($_w), $_sfStop, true)) {
|
||||
$_sfKw[] = $_w;
|
||||
if (count($_sfKw) >= 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$autoSearchQuery = !empty($_sfKw) ? implode(' ', $_sfKw) : $articleTitle;
|
||||
unset($_sfStop, $_sfWords, $_sfKw, $_w);
|
||||
?>
|
||||
|
||||
<?php if (!empty($addFilesError)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($addFilesError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="d-flex align-items-center gap-3 mb-4">
|
||||
<a href="/edit/<?= rawurlencode($articleUuid) ?>" 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($articleTitle) ?></strong>
|
||||
</p>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Upload -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title h6 mb-3">Uploader</h5>
|
||||
<form method="POST"
|
||||
action="/files/<?= rawurlencode($articleUuid) ?>/add"
|
||||
enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<input type="file" class="form-control" id="files" name="files[]" multiple required>
|
||||
<div class="form-text">
|
||||
Images → <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 btn-sm">Uploader</button>
|
||||
<a href="/edit/<?= rawurlencode($articleUuid) ?>" class="btn btn-outline-secondary btn-sm">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fichiers déjà présents -->
|
||||
<?php if ($existingFiles): ?>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title h6 mb-3">Fichiers existants</h5>
|
||||
<div class="list-group list-group-flush">
|
||||
<?php foreach ($existingFiles as $f):
|
||||
$fileUrl = '/file?uuid=' . rawurlencode($articleUuid) . '&name=' . rawurlencode($f['name']);
|
||||
?>
|
||||
<div class="list-group-item d-flex align-items-center gap-2 px-0 py-1">
|
||||
<?php if ($f['is_image']): ?>
|
||||
<img src="<?= htmlspecialchars($fileUrl) ?>" alt=""
|
||||
style="width:40px;height:40px;object-fit:cover;border-radius:4px;flex-shrink:0">
|
||||
<?php else: ?>
|
||||
<span style="width:40px;text-align:center;font-size:1.3rem;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="overflow-hidden" style="min-width:0">
|
||||
<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 ms-auto flex-shrink-0">cover</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Recherche dans les autres articles -->
|
||||
<div class="col-lg-8">
|
||||
<div id="sf-panel" data-uuid="<?= htmlspecialchars($articleUuid) ?>" class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title h6 mb-3">Fichiers d'autres articles</h5>
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<input type="text" id="sf-input" class="form-control form-control-sm"
|
||||
value="<?= htmlspecialchars($autoSearchQuery) ?>"
|
||||
placeholder="Titre, mot-clé…" autocomplete="off">
|
||||
<button type="button" id="sf-btn" class="btn btn-sm btn-outline-secondary text-nowrap">
|
||||
Chercher
|
||||
</button>
|
||||
</div>
|
||||
<div id="sf-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/add_files.js"></script>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = 'Ajouter des fichiers — ' . htmlspecialchars($articleTitle);
|
||||
include __DIR__ . '/layout.php';
|
||||
+1067
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
ob_start();
|
||||
$isAdminRole = ($editRole['name'] === 'admin');
|
||||
?>
|
||||
|
||||
<div class="d-flex align-items-center gap-3 mb-4">
|
||||
<a href="/admin/roles" class="btn btn-secondary btn-sm">← Retour</a>
|
||||
<h1 class="h4 mb-0">Rôle : <?= htmlspecialchars($editRole['label']) ?></h1>
|
||||
<code class="text-muted"><?= htmlspecialchars($editRole['name']) ?></code>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/admin/role/<?= rawurlencode($editRole['name']) ?>">
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Permissions -->
|
||||
<div class="col-lg-8">
|
||||
|
||||
<?php if ($isAdminRole): ?>
|
||||
<div class="alert alert-warning">
|
||||
Le rôle <code>admin</code> a toutes les permissions implicitement — les cases à cocher sont ignorées.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<?php foreach (CAPABILITY_GROUPS as $groupLabel => $groupCaps): ?>
|
||||
<div class="mb-4">
|
||||
<h6 class="fw-semibold text-muted text-uppercase small mb-3"><?= htmlspecialchars($groupLabel) ?></h6>
|
||||
<?php foreach ($groupCaps as $cap): ?>
|
||||
<?php if (!array_key_exists($cap, KNOWN_CAPABILITIES)) {
|
||||
continue;
|
||||
} ?>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
name="caps[]" value="<?= htmlspecialchars($cap) ?>"
|
||||
id="cap_<?= htmlspecialchars($cap) ?>"
|
||||
<?= in_array($cap, $editRoleCaps, true) ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="cap_<?= htmlspecialchars($cap) ?>">
|
||||
<?= htmlspecialchars(KNOWN_CAPABILITIES[$cap]) ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Label + Actions -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="role_label" class="form-label fw-semibold small">Label affiché</label>
|
||||
<input type="text" id="role_label" name="label" class="form-control form-control-sm"
|
||||
value="<?= htmlspecialchars($editRole['label']) ?>" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Enregistrer</button>
|
||||
<a href="/admin/roles" class="btn btn-outline-secondary w-100 mt-2">Annuler</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = 'Rôle — ' . $editRole['label'];
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
require_once BASE_PATH . '/src/Parsedown.php';
|
||||
$Parsedown = new Parsedown();
|
||||
|
||||
$_apName = $authorRow['display_name'] ?? '';
|
||||
$_apSlug = $authorRow['profile_slug'] ?? '';
|
||||
$_base = '/profil/' . rawurlencode($_apSlug) . '/article';
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div class="mb-4">
|
||||
<a href="/profil/<?= rawurlencode($_apSlug) ?>" class="text-muted">← <?= htmlspecialchars($_apName) ?></a>
|
||||
<h1 class="mt-1">Articles de <?= htmlspecialchars($_apName) ?></h1>
|
||||
</div>
|
||||
|
||||
<?php if (empty($posts)): ?>
|
||||
<p class="text-muted">Aucun article publié.</p>
|
||||
<?php else: ?>
|
||||
<div class="post-grid">
|
||||
<?php foreach ($posts as $post):
|
||||
$html = $Parsedown->text($post['content']);
|
||||
$preview = mb_strimwidth(strip_tags($html), 0, 120, '…');
|
||||
$category = trim((string)($post['category'] ?? ''));
|
||||
$gradient = coverGradient($category !== '' ? $category : $post['uuid'], $allCats ?? []);
|
||||
$postUrl = '/post/' . rawurlencode($post['slug']);
|
||||
$coverFile = $post['cover'] ?? '';
|
||||
$coverStyle = $coverFile !== ''
|
||||
? 'background-image: url(\'/file?uuid=' . rawurlencode($post['uuid']) . '&name=' . rawurlencode($coverFile) . '\')'
|
||||
: 'background: ' . $gradient;
|
||||
$isAvantPremiere = $post['published'] && strtotime((string)($post['published_at'] ?? '')) > time();
|
||||
$isLocked = $isAvantPremiere && !hasCapability('view_previews');
|
||||
?>
|
||||
<article class="card">
|
||||
<?php if ($isAvantPremiere): ?>
|
||||
<div class="premiere-ribbon">Avant-première</div>
|
||||
<?php endif; ?>
|
||||
<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): ?>
|
||||
<?= htmlspecialchars($post['title']) ?>
|
||||
<?php else: ?>
|
||||
<a href="<?= htmlspecialchars($postUrl) ?>"><?= htmlspecialchars($post['title']) ?></a>
|
||||
<?php endif; ?>
|
||||
</h2>
|
||||
<p class="card-text flex-grow-1"><?= htmlspecialchars($preview) ?></p>
|
||||
<div class="post-entry-meta mt-auto">
|
||||
<?php if ($isAvantPremiere): ?>
|
||||
<span class="text-muted">Disponible le <?= htmlspecialchars(date('d/m/Y \à H\hi', strtotime((string)($post['published_at'] ?? '')))) ?></span>
|
||||
<?php else: ?>
|
||||
<span><?= htmlspecialchars(date('d/m/Y', strtotime((string)($post['published_at'] ?? $post['created_at'] ?? '')))) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (function_exists('isAdmin') && isAdmin()): ?>
|
||||
<a href="/edit/<?= htmlspecialchars($post['uuid']) ?>" class="post-entry-edit">modifier</a>
|
||||
<?php endif; ?>
|
||||
<?php if (!$isLocked): ?>
|
||||
<a href="<?= htmlspecialchars($postUrl) ?>" class="post-entry-read">→ lire</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!$isLocked): ?>
|
||||
<a href="<?= htmlspecialchars($postUrl) ?>" class="stretched-link"></a>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($prevCursor !== null || $nextCursor !== null): ?>
|
||||
<nav class="pagination-nav mt-5" aria-label="Navigation">
|
||||
<?php if ($prevCursor !== null): ?>
|
||||
<?php $prevHref = $prevCursor === '' ? $_base : $_base . '/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="<?= htmlspecialchars($_base . '/cursor/' . rawurlencode($nextCursor)) ?>">Plus anciens →</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = 'Articles de ' . htmlspecialchars($_apName) . ' — ' . siteTitle();
|
||||
$seoTitle = 'Articles de ' . $_apName . ' — ' . siteTitle();
|
||||
$canonical = rtrim(APP_URL, '/') . $_base;
|
||||
$ogUrl = $canonical;
|
||||
if (!empty($cursor)) {
|
||||
$metaRobots = 'noindex, follow';
|
||||
}
|
||||
$mainClass = 'container-fluid';
|
||||
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
require_once BASE_PATH . '/src/Parsedown.php';
|
||||
$Parsedown = new Parsedown();
|
||||
|
||||
ob_start();
|
||||
|
||||
$_apName = $authorRow['display_name'] ?? '';
|
||||
$_apUrl = $authorRow['profile_url'] ?? '';
|
||||
$_apSlug = $authorRow['profile_slug'] ?? '';
|
||||
$_apBio = $authorRow['bio'] ?? '';
|
||||
$_initials = mb_strtoupper(mb_substr($_apName, 0, 1, 'UTF-8'), 'UTF-8');
|
||||
?>
|
||||
|
||||
<div class="author-profile-hero mb-5">
|
||||
<div class="author-avatar"><?= htmlspecialchars($_initials) ?></div>
|
||||
<div class="author-profile-info">
|
||||
<h1 class="author-profile-name"><?= htmlspecialchars($_apName) ?></h1>
|
||||
<?php if ($_apUrl !== ''): ?>
|
||||
<a href="<?= htmlspecialchars($_apUrl) ?>" target="_blank" rel="noopener" class="author-profile-link">
|
||||
<?= htmlspecialchars(parse_url($_apUrl, PHP_URL_HOST) ?: $_apUrl) ?> ↗
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<a href="/liens/<?= rawurlencode($_apSlug) ?>" class="liens-cta">Mes liens</a>
|
||||
</div>
|
||||
<?php if ($_apBio !== ''): ?>
|
||||
<div class="author-bio-wrap">
|
||||
<p class="author-profile-bio bio-clamped" id="author-bio"><?= nl2br(htmlspecialchars($_apBio)) ?></p>
|
||||
<button class="bio-toggle" id="bio-toggle" hidden>plus</button>
|
||||
</div>
|
||||
<script src="/assets/js/bio-toggle.js" defer></script>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (empty($authorArticles)): ?>
|
||||
<p class="text-muted">Aucun article publié.</p>
|
||||
<?php else: ?>
|
||||
<div class="post-grid">
|
||||
<?php foreach (array_slice($authorArticles, 0, 6) as $post):
|
||||
$html = $Parsedown->text($post['content']);
|
||||
$preview = mb_strimwidth(strip_tags($html), 0, 120, '…');
|
||||
$category = trim((string)($post['category'] ?? ''));
|
||||
$gradient = coverGradient($category !== '' ? $category : $post['uuid'], $allCats ?? []);
|
||||
$postUrl = '/post/' . rawurlencode($post['slug']);
|
||||
$coverFile = $post['cover'] ?? '';
|
||||
$coverStyle = $coverFile !== ''
|
||||
? 'background-image: url(\'/file?uuid=' . rawurlencode($post['uuid']) . '&name=' . rawurlencode($coverFile) . '\')'
|
||||
: 'background: ' . $gradient;
|
||||
?>
|
||||
<article class="card">
|
||||
<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">
|
||||
<a href="<?= htmlspecialchars($postUrl) ?>"><?= htmlspecialchars($post['title']) ?></a>
|
||||
</h2>
|
||||
<p class="card-text flex-grow-1"><?= htmlspecialchars($preview) ?></p>
|
||||
<div class="post-entry-meta mt-auto">
|
||||
<span><?= htmlspecialchars(date('d/m/Y', strtotime((string)($post['published_at'] ?? $post['created_at'] ?? '')))) ?></span>
|
||||
<a href="<?= htmlspecialchars($postUrl) ?>" class="post-entry-read">→ lire</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="<?= htmlspecialchars($postUrl) ?>" class="stretched-link"></a>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if (count($authorArticles) > 6): ?>
|
||||
<p class="text-center mt-4">
|
||||
<a href="/profil/<?= rawurlencode($_apSlug) ?>/article" class="author-profile-link">Voir tous les articles →</a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = htmlspecialchars($_apName) . ' — ' . siteTitle();
|
||||
$seoTitle = $_apName . ' — ' . siteTitle();
|
||||
$canonical = rtrim(APP_URL, '/') . '/profil/' . rawurlencode($_apSlug);
|
||||
$ogUrl = $canonical;
|
||||
$mainClass = 'container-fluid';
|
||||
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,158 @@
|
||||
<?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 if (isAdmin()): ?>
|
||||
<hr class="my-5">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="h4 mb-0">Types de tags</h2>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Types existants -->
|
||||
<div class="col-lg-9">
|
||||
<?php if (empty($tagTypes)): ?>
|
||||
<p class="text-muted">Aucun type de tag défini.</p>
|
||||
<?php else: ?>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<?php foreach ($tagTypes as $_key => $_label): ?>
|
||||
<div class="card">
|
||||
<div class="card-body py-2 px-3 d-flex align-items-center gap-3">
|
||||
<code class="text-muted small" style="min-width:8rem"><?= htmlspecialchars($_key) ?></code>
|
||||
<strong class="flex-grow-1"><?= htmlspecialchars($_label) ?></strong>
|
||||
<form method="POST" action="/?action=delete_tag_type"
|
||||
data-confirm="Supprimer le type «<?= htmlspecialchars($_label) ?>» ? Les tags associés aux articles ne seront pas supprimés.">
|
||||
<input type="hidden" name="type_key" value="<?= htmlspecialchars($_key) ?>">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">Supprimer</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Nouveau type -->
|
||||
<div class="col-lg-3">
|
||||
<h5 class="mb-3">Nouveau type</h5>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/?action=create_tag_type">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-semibold">Identifiant</label>
|
||||
<input type="text" name="type_key" class="form-control form-control-sm font-monospace"
|
||||
placeholder="ex : logiciels" required
|
||||
pattern="[a-z0-9_]+" title="Minuscules, chiffres et _ uniquement">
|
||||
<div class="form-text">Minuscules, chiffres, _ (sans accent)</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Libellé</label>
|
||||
<input type="text" name="type_label" class="form-control form-control-sm"
|
||||
placeholder="ex : Logiciels" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100">Créer</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = 'Catégories';
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
// Variables attendues (injectées depuis index.php) :
|
||||
// $article — tableau article courant (uuid, slug, title)
|
||||
// $reactionStats — array<string, int>
|
||||
// $visitorReactions — string[] (types déjà cliqués par ce visiteur)
|
||||
// $comments — array de commentaires publiés
|
||||
// $commentFlash — bool|null (commentaire soumis, email envoyé)
|
||||
// $commentVerified — bool|null (commentaire vérifié et publié)
|
||||
// $commentError — string|null (message d'erreur)
|
||||
|
||||
$_reactionDefs = [
|
||||
'useful' => ['👍', 'Utile'],
|
||||
'important' => ['🔥', 'Important'],
|
||||
'interesting' => ['🤔', 'À creuser'],
|
||||
];
|
||||
|
||||
$_csrfToken = bin2hex(random_bytes(16));
|
||||
$_SESSION['comment_csrf'] = $_csrfToken;
|
||||
?>
|
||||
|
||||
<?php if (!empty($alsoReadArticles ?? [])): ?>
|
||||
<!-- ── À lire aussi ──────────────────────────────────────────────── -->
|
||||
<div class="also-read mb-4" id="also-read">
|
||||
<h6 class="also-read-title">À lire aussi</h6>
|
||||
<div class="also-read-grid">
|
||||
<?php foreach ($alsoReadArticles as $_also):
|
||||
$_alsoCover = $_also['cover'] ?? '';
|
||||
$_alsoCat = trim($_also['category'] ?? '');
|
||||
$_alsoGradient = coverGradient($_alsoCat !== '' ? $_alsoCat : $_also['uuid'], $allCats ?? []);
|
||||
$_alsoDate = date('d/m/Y', strtotime((string)($_also['published_at'] ?? $_also['created_at'] ?? '')));
|
||||
?>
|
||||
<a href="/post/<?= rawurlencode($_also['slug'] ?? '') ?>" class="related-card">
|
||||
<div class="related-card-thumb" style="<?= $_alsoCover !== ''
|
||||
? 'background-image:url(/file?uuid=' . rawurlencode($_also['uuid']) . '&name=' . rawurlencode($_alsoCover) . ');background-size:cover;background-position:center'
|
||||
: 'background:' . htmlspecialchars($_alsoGradient) ?>"></div>
|
||||
<div class="related-card-body">
|
||||
<div class="related-card-title"><?= htmlspecialchars($_also['title']) ?></div>
|
||||
<div class="related-card-date"><?= $_alsoDate ?></div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- ── Commentaires ───────────────────────────────────────────────── -->
|
||||
<div id="comments" class="mb-4">
|
||||
|
||||
<h5 class="mb-3">
|
||||
Commentaires
|
||||
<?php if (!empty($comments)): ?>
|
||||
<span class="badge bg-secondary ms-1"><?= count($comments) ?></span>
|
||||
<?php endif; ?>
|
||||
</h5>
|
||||
|
||||
<?php if ($commentFlash ?? false): ?>
|
||||
<div class="alert alert-info">
|
||||
Un code de confirmation vous a été envoyé par email.
|
||||
Cliquez sur le lien reçu, puis saisissez le code à 6 chiffres pour publier votre commentaire.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($commentVerified ?? false): ?>
|
||||
<div class="alert alert-success">Votre commentaire a été publié. Merci !</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($commentError ?? null)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($commentError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($comments as $c): ?>
|
||||
<div class="card mb-3 comment-card" id="comment-<?= (int)$c['id'] ?>">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<strong class="comment-author"><?= htmlspecialchars($c['author_name']) ?></strong>
|
||||
<small class="text-muted">
|
||||
<?= htmlspecialchars(date('d/m/Y à H\hi', strtotime((string)$c['created_at']))) ?>
|
||||
</small>
|
||||
</div>
|
||||
<div class="comment-content"><?= nl2br(htmlspecialchars((string)$c['content'])) ?></div>
|
||||
<?php if (function_exists('isAdmin') && isAdmin()): ?>
|
||||
<div class="mt-2">
|
||||
<form method="post" action="/comment-moderate" class="d-inline">
|
||||
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
|
||||
<input type="hidden" name="pub" value="0">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">Masquer</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if (empty($comments) && !($commentFlash ?? false) && !($commentVerified ?? false)): ?>
|
||||
<p class="text-muted mb-4">Aucun commentaire pour l'instant. Soyez le premier !</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Formulaire -->
|
||||
<div class="card mt-3" id="comment-form-card">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-3">Laisser un commentaire</h6>
|
||||
<form method="post" action="/comment" id="comment-form">
|
||||
<input type="hidden" name="_token" value="<?= htmlspecialchars($_csrfToken) ?>">
|
||||
<input type="hidden" name="uuid" value="<?= htmlspecialchars($article['uuid']) ?>">
|
||||
<!-- honeypot -->
|
||||
<div class="d-none" aria-hidden="true">
|
||||
<input type="text" name="website" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="comment-name">Nom <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="comment-name" name="author_name"
|
||||
maxlength="100" required placeholder="Votre nom">
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label" for="comment-email">
|
||||
Email <span class="text-danger">*</span>
|
||||
<span class="text-muted fw-normal">(non publié)</span>
|
||||
</label>
|
||||
<input type="email" class="form-control" id="comment-email" name="author_email"
|
||||
maxlength="254" required placeholder="votre@email.fr">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="comment-content">Commentaire <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="comment-content" name="content"
|
||||
rows="4" maxlength="2000" required
|
||||
placeholder="Votre commentaire (2000 caractères max)"></textarea>
|
||||
</div>
|
||||
<div class="col-12 d-flex align-items-center gap-3">
|
||||
<button type="submit" class="btn btn-primary">Envoyer</button>
|
||||
<small class="text-muted">Un code de vérification sera envoyé à votre adresse email.</small>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
// Session pour CSRF et rate-limit
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$contactEmail = $_ENV['CONTACT_EMAIL'] ?? '';
|
||||
$error = null;
|
||||
$success = false;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// CSRF
|
||||
$token = $_POST['_token'] ?? '';
|
||||
if (!hash_equals($_SESSION['contact_csrf'] ?? '', $token)) {
|
||||
$error = 'Requête invalide. Veuillez réessayer.';
|
||||
}
|
||||
|
||||
// Honeypot (champ caché que les bots remplissent)
|
||||
if (!$error && ($_POST['_hp'] ?? '') !== '') {
|
||||
$error = 'Requête invalide.';
|
||||
}
|
||||
|
||||
// Rate-limit : 1 message par 5 minutes par session
|
||||
if (!$error) {
|
||||
$lastSent = $_SESSION['contact_last_sent'] ?? 0;
|
||||
if (time() - $lastSent < 300) {
|
||||
$error = 'Merci d\'attendre quelques minutes avant d\'envoyer un nouveau message.';
|
||||
}
|
||||
}
|
||||
|
||||
// Validation des champs
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$from = trim($_POST['email'] ?? '');
|
||||
$subject = trim($_POST['subject'] ?? 'Contact depuis varlog');
|
||||
$body = trim($_POST['message'] ?? '');
|
||||
|
||||
if (!$error) {
|
||||
if ($name === '' || mb_strlen($name) > 100) {
|
||||
$error = 'Nom invalide.';
|
||||
} elseif (!filter_var($from, FILTER_VALIDATE_EMAIL)) {
|
||||
$error = 'Adresse e-mail invalide.';
|
||||
} elseif ($body === '' || mb_strlen($body) > 5000) {
|
||||
$error = 'Message vide ou trop long (max 5000 caractères).';
|
||||
}
|
||||
}
|
||||
|
||||
if (!$error && $contactEmail !== '') {
|
||||
$subjectClean = mb_encode_mimeheader(
|
||||
'[' . siteTitle() . ' contact] ' . mb_strimwidth($subject, 0, 100, '…'),
|
||||
'UTF-8',
|
||||
'B'
|
||||
);
|
||||
$nameClean = mb_encode_mimeheader($name, 'UTF-8', 'B');
|
||||
|
||||
$fromEmail = $_ENV['CONTACT_FROM_EMAIL'] ?? ('noreply@' . (parse_url(APP_URL, PHP_URL_HOST) ?? 'localhost'));
|
||||
$headers = 'From: =?UTF-8?B?' . base64_encode(siteTitle() . ' contact') . "?= <{$fromEmail}>\r\n";
|
||||
$headers .= "Reply-To: {$nameClean} <{$from}>\r\n";
|
||||
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
||||
$headers .= "Content-Transfer-Encoding: 8bit\r\n";
|
||||
|
||||
$fullBody = "De : {$name} <{$from}>\n\n{$body}\n\n---\nEnvoyé depuis varlog";
|
||||
|
||||
if (@mail($contactEmail, $subjectClean, $fullBody, $headers)) {
|
||||
$_SESSION['contact_last_sent'] = time();
|
||||
$success = true;
|
||||
} else {
|
||||
$error = 'Erreur lors de l\'envoi. Veuillez réessayer plus tard.';
|
||||
}
|
||||
} elseif (!$error) {
|
||||
$error = 'Formulaire de contact non configuré.';
|
||||
}
|
||||
}
|
||||
|
||||
// Génère un nouveau token CSRF à chaque affichage du formulaire
|
||||
if (!$success) {
|
||||
$_SESSION['contact_csrf'] = bin2hex(random_bytes(16));
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div class="posts-list">
|
||||
<h1 class="mb-1">Contact</h1>
|
||||
<p class="text-muted mb-4">Envoyez-moi un message. Votre adresse e-mail ne sera pas publiée.</p>
|
||||
|
||||
<?php if ($success): ?>
|
||||
|
||||
<div class="alert alert-success" role="alert">
|
||||
Message envoyé. Je vous répondrai dès que possible.
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger" role="alert"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/?action=contact" novalidate>
|
||||
<input type="hidden" name="_token" value="<?= htmlspecialchars($_SESSION['contact_csrf']) ?>">
|
||||
<!-- Honeypot -->
|
||||
<div style="display:none" aria-hidden="true">
|
||||
<input type="text" name="_hp" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="contact-name" class="form-label">Nom <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="contact-name" name="name"
|
||||
value="<?= htmlspecialchars($_POST['name'] ?? '') ?>"
|
||||
maxlength="100" required autocomplete="name">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="contact-email" class="form-label">Adresse e-mail <span class="text-danger">*</span></label>
|
||||
<input type="email" class="form-control" id="contact-email" name="email"
|
||||
value="<?= htmlspecialchars($_POST['email'] ?? '') ?>"
|
||||
required autocomplete="email">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="contact-subject" class="form-label">Sujet</label>
|
||||
<input type="text" class="form-control" id="contact-subject" name="subject"
|
||||
value="<?= htmlspecialchars($_POST['subject'] ?? '') ?>"
|
||||
maxlength="150" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="contact-message" class="form-label">Message <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="contact-message" name="message"
|
||||
rows="7" maxlength="5000" required><?= htmlspecialchars($_POST['message'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success">Envoyer</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = 'Contact — ' . siteTitle();
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -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="/import/<?= 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="/import/<?= 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';
|
||||
@@ -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="/edit/<?= htmlspecialchars($article['uuid']) ?>" class="btn btn-secondary btn-sm">← Retour</a>
|
||||
<div>
|
||||
<strong><?= htmlspecialchars($article['title']) ?></strong>
|
||||
— 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">− <?= htmlspecialchars($line) ?></div>
|
||||
<?php elseif ($op === '+'): ?>
|
||||
<div class="diff-ins px-2">+ <?= htmlspecialchars($line) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="diff-eq px-2 text-muted"> <?= 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';
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
ob_start();
|
||||
|
||||
$_editUrl = '/edit/' . rawurlencode($uuid);
|
||||
$_postUrl = '/edit/' . rawurlencode($uuid) . '/tags/' . rawurlencode($tagType);
|
||||
$_typeLabel = $isCatField ? 'Catégorie' : ($tagTypes[$tagType] ?? ucfirst($tagType));
|
||||
?>
|
||||
|
||||
<div class="d-flex align-items-center gap-3 mb-4 flex-wrap">
|
||||
<a href="<?= htmlspecialchars($_editUrl) ?>" class="btn btn-outline-secondary btn-sm">← Retour à l'édition</a>
|
||||
<div>
|
||||
<h1 class="h4 mb-0">
|
||||
Suggestions : <span class="text-primary"><?= htmlspecialchars($_typeLabel) ?></span>
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">
|
||||
<?= htmlspecialchars($article['title']) ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="<?= htmlspecialchars($_postUrl) ?>">
|
||||
|
||||
<?php if ($isCatField): /* ── Mode catégorie ───────────────────────────── */ ?>
|
||||
|
||||
<p class="text-muted small mb-3">
|
||||
Sélectionnez une catégorie. Les chiffres indiquent combien de fois le mot apparaît dans le texte.
|
||||
</p>
|
||||
|
||||
<div class="row g-2 mb-4">
|
||||
<?php foreach ($suggestions as $_cat => $_cnt): ?>
|
||||
<?php $_isCurrentCat = ($_cat === $currentCat); ?>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<label class="d-flex align-items-center gap-2 p-2 border rounded <?= $_isCurrentCat ? 'border-primary bg-primary bg-opacity-10' : '' ?>"
|
||||
style="cursor:pointer">
|
||||
<input type="radio" name="category" value="<?= htmlspecialchars($_cat) ?>"
|
||||
<?= $_isCurrentCat ? 'checked' : '' ?>>
|
||||
<span class="flex-grow-1"><?= htmlspecialchars($_cat) ?></span>
|
||||
<?php if ($_cnt > 0): ?>
|
||||
<span class="badge bg-secondary"><?= $_cnt ?>×</span>
|
||||
<?php endif; ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php else: /* ── Mode tags ─────────────────────────────────────────────── */ ?>
|
||||
|
||||
<?php
|
||||
$_current = array_filter($suggestions, fn ($s) => $s['current']);
|
||||
$_known = array_filter($suggestions, fn ($s) => !$s['current'] && $s['known']);
|
||||
$_abbrevs = array_filter($suggestions, fn ($s) => !$s['current'] && !$s['known'] && $s['group'] === 'abbrev');
|
||||
$_camel = array_filter($suggestions, fn ($s) => !$s['current'] && !$s['known'] && $s['group'] === 'camel');
|
||||
$_proper = array_filter($suggestions, fn ($s) => !$s['current'] && !$s['known'] && in_array($s['group'], ['proper'], true));
|
||||
?>
|
||||
|
||||
<p class="text-muted small mb-3">
|
||||
Cochez les termes à associer à cet article. Les termes déjà taggués sont présélectionnés.
|
||||
</p>
|
||||
|
||||
<?php
|
||||
function renderTagGroup(string $title, array $items, bool $checked, bool $collapsible = false, string $collapseId = ''): void
|
||||
{ ?>
|
||||
<?php if (empty($items)): return; endif; ?>
|
||||
<div class="mb-4">
|
||||
<?php if ($collapsible && count($items) > 10): ?>
|
||||
<details>
|
||||
<summary class="fw-semibold mb-2 text-muted" style="cursor:pointer;list-style:none">
|
||||
<span class="me-1">▸</span><?= htmlspecialchars($title) ?>
|
||||
<span class="badge bg-secondary ms-1"><?= count($items) ?></span>
|
||||
</summary>
|
||||
<div class="mt-2">
|
||||
<?php else: ?>
|
||||
<h6 class="fw-semibold text-muted mb-2"><?= htmlspecialchars($title) ?>
|
||||
<span class="badge bg-secondary ms-1"><?= count($items) ?></span>
|
||||
</h6>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<?php foreach ($items as $_term => $_info): ?>
|
||||
<label class="d-inline-flex align-items-center gap-1 px-2 py-1 rounded border
|
||||
<?= $_info['current'] ? 'border-primary bg-primary bg-opacity-10 fw-semibold' : 'border-secondary-subtle' ?>"
|
||||
style="cursor:pointer;font-size:.875rem">
|
||||
<input type="checkbox" name="selected[]" value="<?= htmlspecialchars($_term) ?>"
|
||||
<?= ($checked || $_info['current']) ? 'checked' : '' ?>>
|
||||
<span><?= htmlspecialchars($_term) ?></span>
|
||||
<span class="text-muted" style="font-size:.75rem"><?= $_info['count'] ?>×</span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($collapsible && count($items) > 10): ?>
|
||||
</div>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<?php renderTagGroup('Déjà taggués', $_current, true); ?>
|
||||
<?php renderTagGroup('Valeurs connues dans d\'autres articles', $_known, false); ?>
|
||||
<?php renderTagGroup('Abréviations détectées', $_abbrevs, false, true); ?>
|
||||
<?php renderTagGroup('Noms composés détectés', $_camel + $_proper, false, true); ?>
|
||||
|
||||
<?php if (empty($suggestions)): ?>
|
||||
<p class="text-muted">Aucun terme détecté dans cet article.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="d-flex gap-3 mt-4 pt-3 border-top">
|
||||
<button type="submit" class="btn btn-success">Enregistrer</button>
|
||||
<a href="<?= htmlspecialchars($_editUrl) ?>" class="btn btn-outline-secondary">Annuler</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = 'Tags — ' . $article['title'];
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php ob_start(); ?>
|
||||
|
||||
<div class="d-flex align-items-center gap-3 mb-5">
|
||||
<h1 class="h4 mb-0">Flux agrégés</h1>
|
||||
</div>
|
||||
|
||||
<?php if (empty($fluxItems)): ?>
|
||||
<p class="text-muted">Aucun article disponible pour l'instant.</p>
|
||||
<?php else: ?>
|
||||
<div class="flux-list">
|
||||
<?php foreach ($fluxItems as $_item):
|
||||
$_date = $_item['date'] > 0 ? date('d/m/Y', $_item['date']) : '';
|
||||
$_authorName = $_item['author_name'] ?? '';
|
||||
$_authorSlug = $_item['author_slug'] ?? '';
|
||||
?>
|
||||
<article class="flux-item">
|
||||
<div class="flux-item-meta">
|
||||
<?php if ($_authorSlug !== ''): ?>
|
||||
<a href="/profil/<?= rawurlencode($_authorSlug) ?>" class="flux-author"><?= htmlspecialchars($_authorName) ?></a>
|
||||
<?php elseif ($_authorName !== ''): ?>
|
||||
<span class="flux-author"><?= htmlspecialchars($_authorName) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($_item['feed_title'] !== ''): ?>
|
||||
<span class="flux-feed-name"><?= htmlspecialchars($_item['feed_title']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($_date !== ''): ?>
|
||||
<span class="flux-date"><?= $_date ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<h2 class="flux-item-title">
|
||||
<a href="<?= htmlspecialchars($_item['url']) ?>" target="_blank" rel="noopener">
|
||||
<?= htmlspecialchars($_item['title']) ?> ↗
|
||||
</a>
|
||||
</h2>
|
||||
<?php if ($_item['summary'] !== ''): ?>
|
||||
<p class="flux-item-summary"><?= htmlspecialchars($_item['summary']) ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = 'Flux — ' . siteTitle();
|
||||
$canonical = rtrim(APP_URL, '/') . '/flux';
|
||||
$mainClass = 'container-fluid';
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="container">
|
||||
<footer class="py-3 my-4">
|
||||
<ul class="nav justify-content-center border-bottom pb-3 mb-3">
|
||||
<li class="nav-item"><a href="https://alpinux.org/mentions-legales" class="nav-link px-2 text-body-secondary">Mentions légales</a></li>
|
||||
<li class="nav-item"><a href="/index/a-propos" class="nav-link px-2 text-body-secondary">A propos</a></li>
|
||||
</ul>
|
||||
<p class="text-center text-body-secondary">Association 1901 - <a href="https://alpinux.org/">Alpinux, le LUG de Savoie</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
|
||||
<div class="container">
|
||||
<header class="d-flex flex-wrap align-items-center justify-content-center justify-content-md-between py-3 mb-4 border-bottom">
|
||||
<a href="/" class="d-flex align-items-center text-body-emphasis text-decoration-none">
|
||||
<img width="32" src="/img/logo-mail.svg" class="bi me-2" >
|
||||
<span class="fs-4">Mug ALPINUX</span>
|
||||
</a>
|
||||
|
||||
<ul class="nav col-12 col-md-auto mb-2 justify-content-center mb-md-0">
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
<?php
|
||||
// Créer une instance de MessageManager avec le fichier de base de données SQLite
|
||||
$messageManager = new ace\MessageManager('database.db');
|
||||
|
||||
if ($messageManager->sessionAlready()) {
|
||||
?>
|
||||
|
||||
<div class="dropdown text-end">
|
||||
<a href="#" class="d-block link-dark text-decoration-none dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<?php echo $messageManager->getUsername($_SESSION['user_id']); ?>
|
||||
</a>
|
||||
<ul class="dropdown-menu text-small">
|
||||
<li><a class="dropdown-item" href="/user/parametres">Paramètres <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-sliders" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M11.5 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM9.05 3a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0V3h9.05zM4.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM2.05 8a2.5 2.5 0 0 1 4.9 0H16v1H6.95a2.5 2.5 0 0 1-4.9 0H0V8h2.05zm9.45 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-2.45 1a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0v-1h9.05z"/>
|
||||
</svg></a></li>
|
||||
<li><a class="dropdown-item" href="/user/profil">Profil <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-vcard" viewBox="0 0 16 16">
|
||||
<path d="M5 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm4-2.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5ZM9 8a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4A.5.5 0 0 1 9 8Zm1 2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5Z"/>
|
||||
<path d="M2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H2ZM1 4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H8.96c.026-.163.04-.33.04-.5C9 10.567 7.21 9 5 9c-2.086 0-3.8 1.398-3.984 3.181A1.006 1.006 0 0 1 1 12V4Z"/>
|
||||
</svg></a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="/user/disconnect">Déconnexion <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
|
||||
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
||||
</svg></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php ob_start(); ?>
|
||||
|
||||
<div class="d-flex align-items-center gap-3 mb-4">
|
||||
<a href="/edit/<?= 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="/edit/<?= 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';
|
||||
@@ -0,0 +1,212 @@
|
||||
<?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="/import/<?= 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 ($step2Meta['blocked'] ?? false): ?>
|
||||
<div class="alert alert-warning small mb-4">
|
||||
Le site a bloqué la récupération automatique des métadonnées (protection anti-bot).
|
||||
Renseignez le titre manuellement ci-dessous.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?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="/edit/<?= 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';
|
||||
@@ -0,0 +1,139 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= htmlspecialchars(siteLang()) ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><?= htmlspecialchars(($seoTitle ?? '') ?: ($title ?? siteTitle())) ?></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<!-- SEO -->
|
||||
<meta name="description" content="<?= htmlspecialchars(($seoDescription ?? '') ?: siteClaim()) ?>">
|
||||
<meta name="robots" content="<?= htmlspecialchars($metaRobots ?? 'index, follow') ?>">
|
||||
<link rel="canonical" href="<?= htmlspecialchars($canonical ?? $ogUrl ?? rtrim(APP_URL, '/') . '/') ?>">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="<?= htmlspecialchars(($seoTitle ?? '') ?: ($title ?? siteTitle())) ?>">
|
||||
<meta property="og:description" content="<?= htmlspecialchars(($seoDescription ?? '') ?: siteClaim()) ?>">
|
||||
<meta property="og:type" content="<?= htmlspecialchars($ogType ?? 'website') ?>">
|
||||
<meta property="og:locale" content="<?= htmlspecialchars(siteLangOgLocale()) ?>">
|
||||
<meta property="og:url" content="<?= htmlspecialchars($ogUrl ?? APP_URL) ?>">
|
||||
<meta property="og:site_name" content="<?= htmlspecialchars(siteTitle()) ?>">
|
||||
<?php if (!empty($ogImage ?? '')): ?>
|
||||
<meta property="og:image" content="<?= htmlspecialchars($ogImage) ?>">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($metaAuthor ?? '')): ?>
|
||||
<meta name="author" content="<?= htmlspecialchars($metaAuthor) ?>">
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($articlePublishedAt ?? '')): ?>
|
||||
<meta property="article:published_time" content="<?= htmlspecialchars(date('c', strtotime((string)$articlePublishedAt))) ?>">
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($metaAuthorUrl ?? '')): ?>
|
||||
<meta property="article:author" content="<?= htmlspecialchars($metaAuthorUrl) ?>">
|
||||
<?php elseif (!empty($metaAuthor ?? '')): ?>
|
||||
<meta property="article:author" content="<?= htmlspecialchars($metaAuthor) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($jsonLd ?? '')): ?>
|
||||
<script type="application/ld+json"><?= $jsonLd ?></script>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- RSS autodiscovery -->
|
||||
<link rel="alternate" type="application/rss+xml" title="<?= htmlspecialchars(siteTitle()) ?>" href="/feed">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
|
||||
|
||||
<!-- CSS -->
|
||||
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
</head>
|
||||
|
||||
<body<?php if (!empty($bodyClass ?? '')): ?> class="<?= htmlspecialchars($bodyClass) ?>"<?php endif; ?>>
|
||||
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark mb-0" role="navigation" aria-label="Navigation principale">
|
||||
<div class="container-fluid">
|
||||
<?php
|
||||
$_layoutAction = $_GET['action'] ?? 'list';
|
||||
$_layoutPrivateCats = isset($articles) ? $articles->getPrivateCategories() : [];
|
||||
$_layoutCats = isset($articles) ? array_filter(
|
||||
$articles->getCategories(),
|
||||
function ($cat) use ($_layoutPrivateCats) {
|
||||
return isLoggedIn() || !in_array($cat, $_layoutPrivateCats, true);
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
) : [];
|
||||
$_layoutCurrentCat = trim($_GET['cat'] ?? '');
|
||||
?>
|
||||
<a class="navbar-brand d-flex flex-column lh-1" href="/">
|
||||
<span><?= htmlspecialchars(siteTitle()) ?></span>
|
||||
<small class="navbar-tagline"><?= htmlspecialchars(siteClaim()) ?></small>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent" aria-controls="navbarContent" aria-expanded="false" aria-label="Basculer la navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarContent">
|
||||
<ul class="navbar-nav me-auto"></ul>
|
||||
<form class="search-form d-flex" action="/search" method="GET" role="search">
|
||||
<input class="form-control form-control-sm search-input"
|
||||
type="search" name="q"
|
||||
value="<?= htmlspecialchars($_GET['q'] ?? '') ?>"
|
||||
placeholder="Rechercher…"
|
||||
aria-label="Rechercher">
|
||||
</form>
|
||||
|
||||
<ul class="navbar-nav">
|
||||
<?php if (function_exists('isLoggedIn') && isLoggedIn()): ?>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<?= htmlspecialchars(function_exists('currentUserName') ? currentUserName() : (currentUserEmail() ?? '')) ?>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="/profile">Mon identité</a></li>
|
||||
<li><a class="dropdown-item" href="/admin"><?= (function_exists('isAdmin') && isAdmin()) ? 'Administration' : 'Mes articles' ?></a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-muted" href="/logout.php">Déconnexion</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<li class="nav-item"><a class="nav-link" href="/login">Connexion</a></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="<?= htmlspecialchars($mainClass ?? 'container') ?>" role="main">
|
||||
<?= $content ?>
|
||||
</main>
|
||||
|
||||
<footer class="py-5 mt-5" role="contentinfo">
|
||||
<div class="container">
|
||||
<div class="footer-inner">
|
||||
<div class="footer-about">
|
||||
<strong><?= htmlspecialchars(siteTitle()) ?></strong>
|
||||
<p><?= htmlspecialchars(siteClaim()) ?></p>
|
||||
<small>© <?= date('Y') ?> — <a href="<?= htmlspecialchars(siteLicenseUrl()) ?>" target="_blank" rel="noopener"><?= htmlspecialchars(siteLicenseLabel()) ?></a></small>
|
||||
</div>
|
||||
<nav class="footer-nav" aria-label="Liens du site">
|
||||
<a href="/about">À propos</a>
|
||||
<a href="/contact">Contact</a>
|
||||
<a href="/legal">Mentions légales</a>
|
||||
<a href="/licenses">Licences</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- JS -->
|
||||
<script src="/assets/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/assets/js/app.js"></script>
|
||||
<?php if (isset($reactionStats)): ?>
|
||||
<script src="/assets/js/reactions.js"></script>
|
||||
<?php endif; ?>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
ob_start();
|
||||
?>
|
||||
<div class="posts-list site-page">
|
||||
<?= $siteContent ?>
|
||||
</div>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = ($siteMeta['seo_title'] ?? $siteMeta['title'] ?? 'Mentions légales') . ' — ' . siteTitle();
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
ob_start();
|
||||
?>
|
||||
<div class="posts-list site-page">
|
||||
<?= $siteContent ?>
|
||||
</div>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = ($siteMeta['seo_title'] ?? $siteMeta['title'] ?? 'Licences') . ' — ' . siteTitle();
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php ob_start(); ?>
|
||||
|
||||
<div class="liens-page">
|
||||
|
||||
<div class="liens-header">
|
||||
<div class="liens-avatar"><?= htmlspecialchars($_lInitials) ?></div>
|
||||
<h1 class="liens-name"><?= htmlspecialchars($_lName) ?></h1>
|
||||
<?php if ($_lBio !== ''): ?>
|
||||
<p class="liens-bio"><?= nl2br(htmlspecialchars($_lBio)) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($profileLinks)): ?>
|
||||
<hr class="liens-sep">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="liens-list">
|
||||
<?php
|
||||
$_palette = ['#0d9488','#16a34a','#d97706','#db2777','#7c3aed','#1d4ed8','#0ea5e9'];
|
||||
foreach ($profileLinks as $_i => $_link):
|
||||
$_btnBg = $_palette[$_i % count($_palette)];
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($_link['url']) ?>" target="_blank" rel="noopener"
|
||||
class="liens-item" style="--btn-bg:<?= htmlspecialchars($_btnBg) ?>">
|
||||
<span class="liens-item-title"><?= htmlspecialchars($_link['title'] ?: $_link['url']) ?></span>
|
||||
<?php if ($_link['description'] !== ''): ?>
|
||||
<span class="liens-item-desc"><?= htmlspecialchars($_link['description']) ?></span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($_lSlug !== ''): ?>
|
||||
<div class="liens-footer">
|
||||
<a href="/profil/<?= rawurlencode($_lSlug) ?>"><?= htmlspecialchars($_lName) ?> sur <?= htmlspecialchars(siteTitle()) ?></a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$bodyClass = 'liens-bg';
|
||||
$content = ob_get_clean();
|
||||
$title = 'Liens de ' . htmlspecialchars($_lName) . ' — ' . siteTitle();
|
||||
$seoTitle = $title;
|
||||
$canonical = rtrim(APP_URL, '/') . '/liens/' . rawurlencode($_lSlug);
|
||||
$ogUrl = $canonical;
|
||||
$mainClass = 'container-fluid';
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
ob_start();
|
||||
$CONTEXT = 3;
|
||||
|
||||
$base = rtrim(APP_URL, '/');
|
||||
$effectiveTitle = ($seoTitle !== '') ? $seoTitle : $title;
|
||||
$effectiveDesc = ($seoDescription !== '') ? $seoDescription : $autoSeoDesc;
|
||||
$effectiveSlug = $postSlug;
|
||||
|
||||
$coverFilename = ($newCover ?? '') !== '' ? $newCover : ($article['cover'] ?? '');
|
||||
$suggestedOgImage = $coverFilename !== ''
|
||||
? $base . '/file?uuid=' . rawurlencode($uuid) . '&name=' . rawurlencode($coverFilename)
|
||||
: '';
|
||||
?>
|
||||
|
||||
<div class="d-flex align-items-center gap-3 mb-4 flex-wrap">
|
||||
<a href="/edit/<?= htmlspecialchars($uuid) ?>" class="btn btn-outline-secondary">← Retour à l'édition</a>
|
||||
<h1 class="h4 mb-0">Confirmer les modifications</h1>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/edit/<?= htmlspecialchars($uuid) ?>">
|
||||
<input type="hidden" name="_confirm" value="1">
|
||||
<input type="hidden" name="title" value="<?= htmlspecialchars($title) ?>">
|
||||
<input type="hidden" name="content" value="<?= htmlspecialchars($content) ?>">
|
||||
<?php if ($published): ?>
|
||||
<input type="hidden" name="published" value="1">
|
||||
<?php endif; ?>
|
||||
<input type="hidden" name="published_at" value="<?= htmlspecialchars($published_at) ?>">
|
||||
<!-- seo_title et seo_description sont des champs visibles dans la colonne droite -->
|
||||
<!-- og_image est calculé depuis la couverture côté serveur -->
|
||||
<input type="hidden" name="category" value="<?= htmlspecialchars($category) ?>">
|
||||
<?php foreach ($confirmTags ?? [] as $_ctk => $_ctv): ?>
|
||||
<input type="hidden" name="tags[<?= htmlspecialchars($_ctk) ?>]" value="<?= htmlspecialchars($_ctv) ?>">
|
||||
<?php endforeach; ?>
|
||||
<?php if (($newCover ?? '') !== ''): ?>
|
||||
<input type="hidden" name="cover_file" value="<?= htmlspecialchars($newCover) ?>">
|
||||
<?php endif; ?>
|
||||
<?php foreach ($fmetaNames as $fi => $fname): ?>
|
||||
<input type="hidden" name="fmeta_name[]" value="<?= htmlspecialchars($fname) ?>">
|
||||
<input type="hidden" name="fmeta_author[]" value="<?= htmlspecialchars($fmetaAuthors[$fi] ?? '') ?>">
|
||||
<input type="hidden" name="fmeta_source[]" value="<?= htmlspecialchars($fmetaSources[$fi] ?? '') ?>">
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
|
||||
<?php if (!empty($changes)): ?>
|
||||
<div class="alert alert-info py-2 mb-3">
|
||||
<?= htmlspecialchars(ucfirst(implode(' · ', $changes))) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-secondary py-2 mb-3">Aucune modification détectée.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- ─── Diff ─────────────────────────────────────────────────────────────── -->
|
||||
<div class="mb-4">
|
||||
<h2 class="h6 fw-semibold mb-2">Diff du contenu</h2>
|
||||
<?php if ($diffLines === []): ?>
|
||||
<div class="text-muted small">Contenu identique.</div>
|
||||
<?php else:
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
$hasChanges = false;
|
||||
foreach ($diffLines as [$op]) {
|
||||
if ($op !== '=') {
|
||||
$hasChanges = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="d-flex gap-3 mb-1 small">
|
||||
<span class="diff-del px-2 py-1 rounded">− Supprimé</span>
|
||||
<span class="diff-ins px-2 py-1 rounded">+ Ajouté</span>
|
||||
</div>
|
||||
<div class="diff-view font-monospace small">
|
||||
<?php $inEllipsis = false;
|
||||
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">− <?= htmlspecialchars($line) ?></div>
|
||||
<?php elseif ($op === '+'): ?>
|
||||
<div class="diff-ins px-2">+ <?= htmlspecialchars($line) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="diff-eq px-2 text-muted"> <?= htmlspecialchars($line) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- ─── Slug ─────────────────────────────────────────────────────────────── -->
|
||||
<?php
|
||||
$slugDefault = ($titleChanged && $autoSlug !== $postSlug) ? $autoSlug : $postSlug;
|
||||
$slugOriginal = $postSlug;
|
||||
?>
|
||||
<div class="mb-3">
|
||||
<label for="confirm-slug" class="form-label fw-semibold">
|
||||
Slug (URL permanente)
|
||||
<small class="text-muted fw-normal">— /post/<span id="slug-display"><?= htmlspecialchars($slugDefault) ?></span></small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm font-monospace" id="confirm-slug" name="slug"
|
||||
value="<?= htmlspecialchars($slugDefault) ?>"
|
||||
pattern="[a-z0-9][a-z0-9\-]*"
|
||||
oninput="document.getElementById('slug-display').textContent=this.value">
|
||||
<?php if ($titleChanged && $autoSlug !== $slugOriginal): ?>
|
||||
<div class="mt-2 d-flex align-items-center gap-2 flex-wrap">
|
||||
<small class="text-muted">Slug recalculé depuis le nouveau titre. Slug initial :</small>
|
||||
<code class="small"><?= htmlspecialchars($slugOriginal) ?></code>
|
||||
<button type="button" id="slug-btn-keep" class="btn btn-sm btn-outline-secondary py-0"
|
||||
data-slug-keep="<?= htmlspecialchars($slugOriginal) ?>">
|
||||
Conserver
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- ─── Commentaire de révision ──────────────────────────────────────────── -->
|
||||
<div class="mb-4">
|
||||
<label for="revision_comment" class="form-label fw-semibold">
|
||||
Commentaire de révision <small class="text-muted fw-normal">(optionnel)</small>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="revision_comment" name="revision_comment"
|
||||
value="<?= htmlspecialchars($autoRevisionComment) ?>"
|
||||
placeholder="ex. Correction typos, ajout section X…">
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-3 flex-wrap">
|
||||
<button type="submit" class="btn btn-success">Confirmer et enregistrer</button>
|
||||
<a href="/edit/<?= htmlspecialchars($uuid) ?>" class="btn btn-outline-secondary">← Retour</a>
|
||||
</div>
|
||||
|
||||
</div><!-- /col-lg-8 -->
|
||||
|
||||
<!-- ─── SEO ───────────────────────────────────────────────────────────────── -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-secondary mb-3">
|
||||
<div class="card-header bg-transparent py-2">
|
||||
<span class="fw-semibold small">SEO — titre, description, image</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="seo_title" class="form-label small">
|
||||
Titre SEO <small class="text-muted">(og:title, <title>)</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm" id="seo_title" name="seo_title"
|
||||
maxlength="70"
|
||||
value="<?= htmlspecialchars($seoTitle) ?>"
|
||||
placeholder="<?= htmlspecialchars($title) ?>">
|
||||
<div class="d-flex justify-content-between mt-1">
|
||||
<small class="text-muted">Idéal : 30–60 car.</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 small">
|
||||
Description SEO <small class="text-muted">(meta description)</small>
|
||||
</label>
|
||||
<textarea class="form-control form-control-sm" id="seo_description" name="seo_description"
|
||||
rows="3" maxlength="200"
|
||||
placeholder="<?= htmlspecialchars(mb_strimwidth($autoSeoDesc, 0, 80, '…')) ?>"><?= htmlspecialchars($seoDescription) ?></textarea>
|
||||
<div class="d-flex justify-content-between mt-1">
|
||||
<small class="text-muted">Idéal : 120–155 car.</small>
|
||||
<small id="seo_desc_counter" class="text-muted">0 / 155</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-secondary">
|
||||
<div class="card-header bg-transparent py-2">
|
||||
<span class="fw-semibold small">Aperçu dans les moteurs de recherche</span>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<?php
|
||||
$pubTs = strtotime((string)($published_at ?? $article['published_at'] ?? ''));
|
||||
$modTs = time(); // sera mis à jour à la sauvegarde
|
||||
$pubFmt = $pubTs ? date('d/m/Y H:i', $pubTs) : '—';
|
||||
$modFmt = date('d/m/Y H:i', $modTs);
|
||||
$catVal = trim($category ?? '');
|
||||
?>
|
||||
<div class="seo-preview mb-3">
|
||||
<div class="seo-preview-url small text-truncate mb-1" id="preview-url">
|
||||
<?= htmlspecialchars(rtrim($base, '/') . '/post/' . $effectiveSlug) ?>
|
||||
</div>
|
||||
<div class="seo-preview-title mb-1" id="preview-title">
|
||||
<?= htmlspecialchars($effectiveTitle) ?>
|
||||
</div>
|
||||
<div class="seo-preview-desc small" id="preview-desc">
|
||||
<?= htmlspecialchars($effectiveDesc) ?>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-sm table-borderless mb-0 small">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap" style="width:1%">Auteur</th>
|
||||
<td><?= htmlspecialchars(currentUserName() ?: (currentUserEmail() ?? '—')) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap">Publication</th>
|
||||
<td><?= htmlspecialchars($pubFmt) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap">Modification</th>
|
||||
<td><?= htmlspecialchars($modFmt) ?> <small class="text-muted">(après enreg.)</small></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap">Langue</th>
|
||||
<td>fr-FR</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap">Catégorie</th>
|
||||
<td><?= $catVal !== '' ? htmlspecialchars($catVal) : '<span class="text-muted">—</span>' ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap">og:image</th>
|
||||
<td class="text-truncate" style="max-width:0">
|
||||
<?= $suggestedOgImage !== '' ? '<span title="' . htmlspecialchars($suggestedOgImage) . '">' . htmlspecialchars($coverFilename) . '</span>' : '<span class="text-muted">— (pas de couverture)</span>' ?>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /row -->
|
||||
</form>
|
||||
|
||||
<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; }
|
||||
|
||||
.seo-preview { border: 1px solid #dee2e6; border-radius: 6px; padding: 10px 12px; background: #fff; }
|
||||
.seo-preview-url { color: #006621; font-size: .78rem; }
|
||||
.seo-preview-title { color: #1a0dab; font-size: 1.05rem; font-weight: 500; line-height: 1.3; word-break: break-word; }
|
||||
.seo-preview-desc { color: #545454; line-height: 1.5; }
|
||||
</style>
|
||||
<div id="pc-data" hidden
|
||||
data-default-title="<?= htmlspecialchars($effectiveTitle) ?>"
|
||||
data-default-desc="<?= htmlspecialchars($effectiveDesc) ?>"
|
||||
data-base-url="<?= htmlspecialchars(rtrim($base, '/') . '/post/') ?>"></div>
|
||||
<script src="/assets/js/post_confirm.js"></script>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = 'Confirmer — ' . $title;
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,515 @@
|
||||
<?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');
|
||||
?>
|
||||
|
||||
<?php if ($action === 'edit'): ?>
|
||||
<div id="vl-page"
|
||||
data-uuid="<?= htmlspecialchars($uuid) ?>"
|
||||
data-insert-url="<?= htmlspecialchars($insertUrl ?? '') ?>"
|
||||
hidden></div>
|
||||
<form method="POST" action="<?= htmlspecialchars($formAction) ?>">
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
|
||||
<h1 class="mb-4">Modifier l'article</h1>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<h1 class="mb-4">Nouvel article</h1>
|
||||
|
||||
<form method="POST" action="<?= htmlspecialchars($formAction) ?>" enctype="multipart/form-data">
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<?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; ?>
|
||||
|
||||
<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 ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<?php if ($action === 'edit'): ?>
|
||||
<input type="hidden" id="slug" name="slug" value="<?= htmlspecialchars($postSlug ?? '') ?>">
|
||||
<?php else: ?>
|
||||
<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>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($action === 'create'): ?>
|
||||
<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>
|
||||
|
||||
<?php if (!empty($tagTypes)): ?>
|
||||
<div class="mb-3">
|
||||
<p class="form-label fw-semibold small mb-2">Tags</p>
|
||||
<?php foreach ($tagTypes as $_tagKey => $_tagLabel):
|
||||
$_tagVal = implode(', ', $articleTags[$_tagKey] ?? []);
|
||||
$_suggestId = 'tag-suggest-create-' . htmlspecialchars($_tagKey);
|
||||
$_tagVals = $allTagValues[$_tagKey] ?? [];
|
||||
?>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small" for="tag-create-<?= htmlspecialchars($_tagKey) ?>">
|
||||
<?= htmlspecialchars($_tagLabel) ?>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
id="tag-create-<?= htmlspecialchars($_tagKey) ?>"
|
||||
name="tags[<?= htmlspecialchars($_tagKey) ?>]"
|
||||
value="<?= htmlspecialchars($_tagVal) ?>"
|
||||
placeholder="valeur1, valeur2…"
|
||||
list="<?= $_suggestId ?>">
|
||||
<datalist id="<?= $_suggestId ?>">
|
||||
<?php foreach ($_tagVals as $_v): ?>
|
||||
<option value="<?= htmlspecialchars($_v) ?>">
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<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>
|
||||
|
||||
<?php if ($action === 'create'): ?>
|
||||
<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 → nommées <code>sha256-taille.ext</code>. Vidéos, PDF → nom sanitisé.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($action === 'edit'): ?>
|
||||
</div><!-- /col-lg-8 -->
|
||||
|
||||
<div class="col-lg-4">
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="d-flex align-items-center justify-content-between mb-1">
|
||||
<label for="category" class="form-label mb-0">Catégorie</label>
|
||||
<a href="/edit/<?= rawurlencode($uuid) ?>/tags/categorie"
|
||||
class="btn btn-outline-secondary btn-sm py-0 px-1"
|
||||
style="font-size:.7rem" title="Suggestions">⚡ Suggestions</a>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<?php if (!empty($tagTypes)): ?>
|
||||
<div class="mb-3">
|
||||
<p class="form-label fw-semibold small mb-2">Tags</p>
|
||||
<?php foreach ($tagTypes as $_tagKey => $_tagLabel):
|
||||
$_tagVal = implode(', ', $articleTags[$_tagKey] ?? []);
|
||||
$_suggestId = 'tag-suggest-' . htmlspecialchars($_tagKey);
|
||||
$_tagVals = $allTagValues[$_tagKey] ?? [];
|
||||
?>
|
||||
<div class="mb-2">
|
||||
<div class="d-flex align-items-center justify-content-between mb-1">
|
||||
<label class="form-label small mb-0" for="tag-<?= htmlspecialchars($_tagKey) ?>">
|
||||
<?= htmlspecialchars($_tagLabel) ?>
|
||||
</label>
|
||||
<?php if (isset($uuid)): ?>
|
||||
<a href="/edit/<?= rawurlencode($uuid) ?>/tags/<?= rawurlencode($_tagKey) ?>"
|
||||
class="btn btn-outline-secondary btn-sm py-0 px-1"
|
||||
style="font-size:.7rem" title="Suggestions automatiques">⚡ Suggestions</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
id="tag-<?= htmlspecialchars($_tagKey) ?>"
|
||||
name="tags[<?= htmlspecialchars($_tagKey) ?>]"
|
||||
value="<?= htmlspecialchars($_tagVal) ?>"
|
||||
placeholder="valeur1, valeur2…"
|
||||
list="<?= $_suggestId ?>">
|
||||
<datalist id="<?= $_suggestId ?>">
|
||||
<?php foreach ($_tagVals as $_v): ?>
|
||||
<option value="<?= htmlspecialchars($_v) ?>">
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<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 class="form-check mt-2">
|
||||
<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>
|
||||
|
||||
<?php if (function_exists('isAdmin') && isAdmin()): ?>
|
||||
<div class="mb-3">
|
||||
<form method="POST" action="/edit/<?= htmlspecialchars($uuid) ?>">
|
||||
<input type="hidden" name="_toggle_featured" value="1">
|
||||
<button type="submit" class="btn btn-sm <?= !empty($article['featured'] ?? false) ? 'btn-warning' : 'btn-outline-secondary' ?> w-100">
|
||||
<?= !empty($article['featured'] ?? false) ? '★ À la une (actif)' : '☆ Mettre à la une' ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="d-flex align-items-center gap-3 flex-wrap mb-4">
|
||||
<button type="submit" class="btn btn-success">Enregistrer</button>
|
||||
<a href="/" class="btn btn-secondary">Annuler</a>
|
||||
<span id="autosave-indicator" class="text-muted small"></span>
|
||||
</div>
|
||||
|
||||
<hr class="my-3">
|
||||
|
||||
<?php if (!empty($existingFiles)): ?>
|
||||
<?php $coverFile = $article['cover'] ?? ''; ?>
|
||||
<?php $filesMeta = $article['files_meta'] ?? []; ?>
|
||||
<div class="mb-3">
|
||||
<p class="form-label fw-semibold small mb-2">Fichiers existants</p>
|
||||
<div class="list-group">
|
||||
<?php foreach ($existingFiles as $i => $f): ?>
|
||||
<?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 px-2">
|
||||
<div class="d-flex align-items-start gap-2">
|
||||
<!-- 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:48px;height:48px;object-fit:cover;border-radius:4px;<?= $isCoverFile ? 'outline:2px solid #0d6efd' : '' ?>">
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span style="width:48px;text-align:center;font-size:1.4rem;flex-shrink:0;line-height:48px">
|
||||
<?= 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" style="min-width:0">
|
||||
<div class="d-flex align-items-center gap-1 mb-1 flex-wrap">
|
||||
<code class="text-truncate small" style="max-width:100%"><?= 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>
|
||||
<div class="d-flex gap-1 mb-1">
|
||||
<input type="hidden" name="fmeta_name[]" value="<?= htmlspecialchars($f['name']) ?>">
|
||||
<input type="text" name="fmeta_author[]"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Auteur / crédit"
|
||||
value="<?= htmlspecialchars($fmeta['author'] ?? '') ?>">
|
||||
<input type="url" name="fmeta_source[]"
|
||||
class="form-control form-control-sm font-monospace"
|
||||
placeholder="URL source"
|
||||
value="<?= htmlspecialchars($fmeta['source_url'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-1 flex-wrap">
|
||||
<?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 ms-auto">
|
||||
<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>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<?php endif; ?>
|
||||
|
||||
<?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="/files/<?= rawurlencode($uuid) ?>/add" class="btn btn-outline-secondary btn-sm">
|
||||
+ Ajouter des fichiers
|
||||
</a>
|
||||
<a href="/import/<?= 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="/sources/<?= rawurlencode($uuid) ?>" class="btn btn-outline-secondary btn-sm">
|
||||
Sources & métadonnées
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div><!-- /col-lg-4 -->
|
||||
</div><!-- /row -->
|
||||
</form>
|
||||
|
||||
<?php if (!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; ?>
|
||||
|
||||
|
||||
<?php if (!empty($article['revisions'])): ?>
|
||||
<hr class="my-4">
|
||||
<div>
|
||||
<div class="d-flex align-items-center gap-3 mb-1">
|
||||
<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>
|
||||
<form method="POST" action="/?action=delete_all_revisions&uuid=<?= rawurlencode($uuid) ?>" class="d-inline ms-auto">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||
data-confirm="Supprimer tout l'historique des révisions ?">
|
||||
Tout supprimer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<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): ?>
|
||||
<?php $revN = (int)($rev['n'] ?? 0); ?>
|
||||
<tr>
|
||||
<td class="text-muted small"><?= $revN ?></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 class="text-nowrap">
|
||||
<a href="/diff/<?= rawurlencode($uuid) ?>/<?= $revN ?>"
|
||||
class="btn btn-outline-secondary btn-sm" target="_blank">
|
||||
Diff
|
||||
</a>
|
||||
<form method="POST" action="/?action=delete_revision&uuid=<?= rawurlencode($uuid) ?>" class="d-inline">
|
||||
<input type="hidden" name="rev_n" value="<?= $revN ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||
data-confirm="Supprimer la révision #<?= $revN ?> ?">
|
||||
✕
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php else: /* create */ ?>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = $action === 'edit' ? 'Modifier l\'article' : 'Nouvel article';
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,330 @@
|
||||
<?php
|
||||
require_once BASE_PATH . '/src/Parsedown.php';
|
||||
$Parsedown = new Parsedown();
|
||||
|
||||
ob_start();
|
||||
|
||||
// ─── Helpers locaux ────────────────────────────────────────────────────────
|
||||
function _cardCoverStyle(array $post, array $allCats): string
|
||||
{
|
||||
$coverFile = $post['cover'] ?? '';
|
||||
if ($coverFile !== '') {
|
||||
return 'background-image: url(\'/file?uuid=' . rawurlencode($post['uuid']) . '&name=' . rawurlencode($coverFile) . '\')';
|
||||
}
|
||||
$cat = trim($post['category'] ?? '');
|
||||
return 'background: ' . coverGradient($cat !== '' ? $cat : $post['uuid'], $allCats);
|
||||
}
|
||||
|
||||
function _cardExcerpt(array $post, \Parsedown $pd, int $len = 120): string
|
||||
{
|
||||
return mb_strimwidth(strip_tags($pd->text($post['content'])), 0, $len, '…');
|
||||
}
|
||||
|
||||
function _renderCard(array $post, array $privateCats, array $allCats, \Parsedown $pd): void
|
||||
{
|
||||
$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 && !hasCapability('view_previews');
|
||||
$coverStyle = _cardCoverStyle($post, $allCats);
|
||||
$preview = _cardExcerpt($post, $pd);
|
||||
?>
|
||||
<article class="card">
|
||||
<?php if ($isDraft): ?>
|
||||
<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; ?>
|
||||
<div class="card-cover" style="<?= $coverStyle ?>">
|
||||
<?php if ($postCat !== ''): ?>
|
||||
<span class="cover-category"><?= htmlspecialchars($postCat) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h2 class="card-title">
|
||||
<?php if ($isLocked): ?>
|
||||
<?= htmlspecialchars($post['title']) ?>
|
||||
<?php else: ?>
|
||||
<a href="<?= htmlspecialchars($postUrl) ?>"><?= htmlspecialchars($post['title']) ?></a>
|
||||
<?php endif; ?>
|
||||
</h2>
|
||||
<p class="card-text flex-grow-1"><?= htmlspecialchars($preview) ?></p>
|
||||
<div class="post-entry-meta mt-auto">
|
||||
<?php if ($isAvantPremiere): ?>
|
||||
<span class="text-muted">Disponible le <?= htmlspecialchars(date('d/m/Y \à H\hi', strtotime((string)($post['published_at'] ?? '')))) ?></span>
|
||||
<?php else: ?>
|
||||
<span><?= htmlspecialchars(date('d/m/Y', strtotime((string)($post['published_at'] ?? $post['created_at'] ?? '')))) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (!$isLocked): ?>
|
||||
<a href="<?= htmlspecialchars($postUrl) ?>" class="post-entry-read">→ lire</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!$isLocked): ?>
|
||||
<a href="<?= htmlspecialchars($postUrl) ?>" class="stretched-link"></a>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
<?php if ($isHomepage ?? false): ?>
|
||||
|
||||
<?php /* ─── PAGE D'ACCUEIL ─────────────────────────────────────────────── */ ?>
|
||||
|
||||
<div class="hero-search">
|
||||
<form class="hero-search-form" action="/search" method="GET" role="search">
|
||||
<input class="hero-search-input"
|
||||
type="search" name="q"
|
||||
value="<?= htmlspecialchars($_GET['q'] ?? '') ?>"
|
||||
placeholder="Rechercher un article…"
|
||||
aria-label="Rechercher un article"
|
||||
autofocus>
|
||||
<button type="submit" class="hero-search-btn">Rechercher</button>
|
||||
</form>
|
||||
<p class="hero-search-stats">
|
||||
<?= $totalPublished ?> article<?= $totalPublished > 1 ? 's' : '' ?>
|
||||
<?php if ($totalUpcoming > 0): ?>
|
||||
· <?= $totalUpcoming ?> à venir
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php /* ─── Héro + derniers articles ─────────────────────────────────── */ ?>
|
||||
<?php if ($heroPost): ?>
|
||||
<section class="home-section home-section--first">
|
||||
<h2 class="home-section-title">
|
||||
Derniers articles
|
||||
<?php if (!empty($heroPost['featured'] ?? false)): ?>
|
||||
<span class="home-section-badge">À la une</span>
|
||||
<?php endif; ?>
|
||||
</h2>
|
||||
|
||||
<?php
|
||||
$heroUrl = '/post/' . rawurlencode($heroPost['slug']);
|
||||
$heroCat = trim($heroPost['category'] ?? '');
|
||||
$heroCover = $heroPost['cover'] ?? '';
|
||||
$heroCoverStyle = $heroCover !== ''
|
||||
? 'background-image: url(\'/file?uuid=' . rawurlencode($heroPost['uuid']) . '&name=' . rawurlencode($heroCover) . '\')'
|
||||
: 'background: ' . coverGradient($heroCat !== '' ? $heroCat : $heroPost['uuid'], $allCats ?? []);
|
||||
$heroExcerpt = _cardExcerpt($heroPost, $Parsedown, 200);
|
||||
$heroDate = date('d/m/Y', strtotime((string)($heroPost['published_at'] ?? $heroPost['created_at'] ?? '')));
|
||||
$heroLocked = $heroPost['published'] && strtotime((string)($heroPost['published_at'] ?? '')) > time() && !hasCapability('view_previews');
|
||||
?>
|
||||
<a href="<?= $heroLocked ? '#' : htmlspecialchars($heroUrl) ?>" class="home-hero-card">
|
||||
<div class="home-hero-card-cover" style="<?= $heroCoverStyle ?>">
|
||||
<?php if (!empty($heroPost['featured'] ?? false)): ?>
|
||||
<span class="home-hero-badge">À la une</span>
|
||||
<?php endif; ?>
|
||||
<div class="home-hero-card-meta">
|
||||
<?php if ($heroCat !== ''): ?>
|
||||
<div class="home-hero-card-cat"><?= htmlspecialchars($heroCat) ?></div>
|
||||
<?php endif; ?>
|
||||
<h3 class="home-hero-card-title"><?= htmlspecialchars($heroPost['title']) ?></h3>
|
||||
<p class="home-hero-card-excerpt"><?= htmlspecialchars($heroExcerpt) ?></p>
|
||||
<div class="home-hero-card-date"><?= htmlspecialchars($heroDate) ?> → lire</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<?php if (!empty($latestPosts)): ?>
|
||||
<div class="post-grid">
|
||||
<?php foreach ($latestPosts as $_lp): ?>
|
||||
<?php _renderCard($_lp, $privateCats ?? [], $allCats ?? [], $Parsedown); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$moreCursor = $allPosts[5]['uuid'] ?? null;
|
||||
if ($moreCursor === null && !empty($latestPosts)) {
|
||||
$moreCursor = end($latestPosts)['uuid'] ?? null;
|
||||
}
|
||||
?>
|
||||
<?php if ($moreCursor && count($allPosts) > 6): ?>
|
||||
<div class="home-more-link">
|
||||
<a href="/page/<?= rawurlencode($moreCursor) ?>" class="btn btn-outline-secondary btn-sm">
|
||||
Voir plus d'articles →
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ─── Tendances ───────────────────────────────────────────────────── */ ?>
|
||||
<?php if (!empty($popularPosts)): ?>
|
||||
<section class="home-section">
|
||||
<h2 class="home-section-title">
|
||||
Tendances <span class="home-section-title-sub">· 10 derniers jours</span>
|
||||
</h2>
|
||||
<div class="post-grid">
|
||||
<?php foreach ($popularPosts as $_pp): ?>
|
||||
<?php _renderCard($_pp, $privateCats ?? [], $allCats ?? [], $Parsedown); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ─── Récemment mis à jour ──────────────────────────────────────── */ ?>
|
||||
<?php if (!empty($recentlyUpdated)): ?>
|
||||
<section class="home-section">
|
||||
<h2 class="home-section-title">Récemment mis à jour</h2>
|
||||
<div class="home-compact-list">
|
||||
<?php foreach ($recentlyUpdated as $_ru):
|
||||
$_ruUrl = '/post/' . rawurlencode($_ru['slug']);
|
||||
$_ruCat = trim($_ru['category'] ?? '');
|
||||
$_ruThumb = _cardCoverStyle($_ru, $allCats ?? []);
|
||||
$_ruDate = date('d/m/Y', strtotime((string)($_ru['updated_at'] ?? $_ru['published_at'] ?? '')));
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($_ruUrl) ?>" class="home-compact-item">
|
||||
<div class="home-compact-thumb" style="<?= $_ruThumb ?>"></div>
|
||||
<div class="home-compact-meta">
|
||||
<div class="home-compact-title"><?= htmlspecialchars($_ru['title']) ?></div>
|
||||
<div class="home-compact-date">mis à jour le <?= htmlspecialchars($_ruDate) ?></div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ─── Redécouvertes ──────────────────────────────────────────────── */ ?>
|
||||
<?php if (!empty($redecouvertes)): ?>
|
||||
<section class="home-section">
|
||||
<h2 class="home-section-title">Redécouvertes</h2>
|
||||
<div class="home-compact-list">
|
||||
<?php foreach ($redecouvertes as $_rd):
|
||||
$_rdUrl = '/post/' . rawurlencode($_rd['slug']);
|
||||
$_rdCat = trim($_rd['category'] ?? '');
|
||||
$_rdThumb = _cardCoverStyle($_rd, $allCats ?? []);
|
||||
$_rdDate = date('d/m/Y', strtotime((string)($_rd['published_at'] ?? $_rd['created_at'] ?? '')));
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($_rdUrl) ?>" class="home-compact-item">
|
||||
<div class="home-compact-thumb" style="<?= $_rdThumb ?>"></div>
|
||||
<div class="home-compact-meta">
|
||||
<div class="home-compact-title"><?= htmlspecialchars($_rd['title']) ?></div>
|
||||
<div class="home-compact-date"><?= htmlspecialchars($_rdDate) ?></div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php else: /* ─── VUE PAGINÉE / FILTRÉE ─────────────────────────────── */ ?>
|
||||
|
||||
<?php if ($cursor === '' && $filterCat === ''): ?>
|
||||
<div class="hero-search">
|
||||
<form class="hero-search-form" action="/search" method="GET" role="search">
|
||||
<input class="hero-search-input"
|
||||
type="search" name="q"
|
||||
value="<?= htmlspecialchars($_GET['q'] ?? '') ?>"
|
||||
placeholder="Rechercher un article…"
|
||||
aria-label="Rechercher un article"
|
||||
autofocus>
|
||||
<button type="submit" class="hero-search-btn">Rechercher</button>
|
||||
</form>
|
||||
<p class="hero-search-stats">
|
||||
<?= $totalPublished ?> article<?= $totalPublished > 1 ? 's' : '' ?>
|
||||
<?php if ($totalUpcoming > 0): ?>· <?= $totalUpcoming ?> à venir<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="post-grid">
|
||||
<?php foreach ($posts as $post): ?>
|
||||
<?php _renderCard($post, $privateCats ?? [], $allCats ?? [], $Parsedown); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($prevCursor !== null || $nextCursor !== null): ?>
|
||||
<nav class="pagination-nav mt-5" aria-label="Navigation">
|
||||
<?php
|
||||
$hasCat = $filterCat !== '';
|
||||
$catBase = $hasCat ? '/categorie/' . rawurlencode($filterCat) : null;
|
||||
?>
|
||||
<?php if ($prevCursor !== null): ?>
|
||||
<?php
|
||||
if ($prevCursor === '') {
|
||||
$prevHref = $hasCat ? $catBase : '/';
|
||||
} elseif ($hasCat) {
|
||||
$prevHref = $catBase . '?cursor=' . rawurlencode($prevCursor);
|
||||
} else {
|
||||
$prevHref = '/page/' . rawurlencode($prevCursor);
|
||||
}
|
||||
?>
|
||||
<a class="pagination-btn" href="<?= htmlspecialchars($prevHref) ?>">← Plus récents</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($nextCursor !== null): ?>
|
||||
<?php $nextHref = $hasCat ? $catBase . '?cursor=' . rawurlencode($nextCursor) : '/page/' . rawurlencode($nextCursor); ?>
|
||||
<a class="pagination-btn ms-auto" href="<?= htmlspecialchars($nextHref) ?>">Plus anciens →</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; /* fin homepage / paginated */ ?>
|
||||
|
||||
<?php
|
||||
$_tagCats = array_filter(
|
||||
$allCats ?? [],
|
||||
static fn ($cat) => isLoggedIn() || !in_array($cat, $privateCats ?? [], true),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
arsort($_tagCats);
|
||||
if (!empty($_tagCats)):
|
||||
$_minCount = min($_tagCats);
|
||||
$_maxCount = max($_tagCats);
|
||||
?>
|
||||
<nav class="tag-cloud mt-5" aria-label="Catégories">
|
||||
<?php foreach ($_tagCats as $_catName => $_catCount):
|
||||
$_size = $_minCount === $_maxCount
|
||||
? 1.1
|
||||
: round(0.8 + ($_catCount - $_minCount) / ($_maxCount - $_minCount) * 0.9, 2);
|
||||
$_active = ($filterCat === $_catName);
|
||||
?>
|
||||
<a href="/categorie/<?= rawurlencode($_catName) ?>"
|
||||
class="tag-cloud-item<?= $_active ? ' active' : '' ?>">
|
||||
<?= htmlspecialchars($_catName) ?>
|
||||
<span class="tag-count"><?= $_catCount ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($filterCat !== ''): ?>
|
||||
<a href="/" class="tag-cloud-reset">× tout afficher</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (function_exists('isLoggedIn') && isLoggedIn()): ?>
|
||||
<a href="/new" class="fab-new" title="Nouvel article" aria-label="Nouvel article">+</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = siteTitle();
|
||||
|
||||
if (!empty($cursor)) {
|
||||
$metaRobots = 'noindex, follow';
|
||||
$canonical = rtrim(APP_URL, '/') . '/';
|
||||
} elseif ($filterCat !== '') {
|
||||
$canonical = rtrim(APP_URL, '/') . '/categorie/' . rawurlencode($filterCat);
|
||||
} else {
|
||||
$canonical = rtrim(APP_URL, '/') . '/';
|
||||
}
|
||||
|
||||
if (empty($cursor) && $filterCat === '') {
|
||||
$jsonLd = json_encode([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'WebSite',
|
||||
'name' => siteTitle(),
|
||||
'url' => rtrim(APP_URL, '/') . '/',
|
||||
'description' => siteClaim(),
|
||||
'inLanguage' => siteLang(),
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
$mainClass = 'container-fluid';
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,354 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../src/Parsedown.php';
|
||||
$Parsedown = new Parsedown();
|
||||
|
||||
$_accentMap = [
|
||||
'à' => 'a','â' => 'a','ä' => 'a','á' => 'a','é' => 'e','è' => 'e','ê' => 'e','ë' => 'e',
|
||||
'î' => 'i','ï' => 'i','í' => 'i','ô' => 'o','ö' => 'o','ó' => 'o','ù' => 'u','û' => 'u',
|
||||
'ü' => 'u','ú' => 'u','ç' => 'c','ñ' => 'n','æ' => 'ae','œ' => 'oe',
|
||||
];
|
||||
$_tocItems = [];
|
||||
$_tocSeen = [];
|
||||
$_renderedContent = preg_replace_callback(
|
||||
'/<(h[23])>(.+?)<\/h[23]>/i',
|
||||
function ($m) use (&$_tocItems, &$_tocSeen, $_accentMap) {
|
||||
$tag = $m[1];
|
||||
$inner = $m[2];
|
||||
$level = (int) substr($tag, 1);
|
||||
$plain = strip_tags($inner);
|
||||
$slug = trim(preg_replace(
|
||||
'/[^a-z0-9]+/',
|
||||
'-',
|
||||
mb_strtolower(strtr($plain, $_accentMap), 'UTF-8')
|
||||
), '-') ?: 'section';
|
||||
if (isset($_tocSeen[$slug])) {
|
||||
$_tocSeen[$slug]++;
|
||||
$id = $slug . '-' . $_tocSeen[$slug];
|
||||
} else {
|
||||
$_tocSeen[$slug] = 0;
|
||||
$id = $slug;
|
||||
}
|
||||
$_tocItems[] = ['level' => $level, 'text' => $plain, 'id' => $id];
|
||||
return "<{$tag} id=\"" . htmlspecialchars($id) . "\">{$inner}</{$tag}>";
|
||||
},
|
||||
$Parsedown->text($rawContent)
|
||||
);
|
||||
|
||||
ob_start();
|
||||
|
||||
$coverFile = $article['cover'] ?? '';
|
||||
$ogImage = $coverFile !== ''
|
||||
? url('file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($coverFile))
|
||||
: null;
|
||||
|
||||
$category = trim((string)($article['category'] ?? ''));
|
||||
$gradient = coverGradient($category !== '' ? $category : $article['uuid'], $allCats ?? []);
|
||||
|
||||
// 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_values(array_filter(
|
||||
$files,
|
||||
static fn ($f) =>
|
||||
!isset($referenced[$f['name']])
|
||||
&& !str_starts_with($f['name'], '_thumb_')
|
||||
&& $f['name'] !== $coverFile
|
||||
));
|
||||
}
|
||||
|
||||
$externalLinks = $article['external_links'] ?? [];
|
||||
?>
|
||||
<div class="row g-4 align-items-start flex-lg-nowrap">
|
||||
|
||||
<!-- Colonne principale -->
|
||||
<div class="col">
|
||||
|
||||
<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) : '';
|
||||
$authorProfileUrl = ($authorEmail !== '' && function_exists('authorProfileUrl')) ? authorProfileUrl($authorEmail) : '';
|
||||
$authorSlugVal = ($authorEmail !== '' && function_exists('authorSlug')) ? authorSlug($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="/edit/<?= rawurlencode($article['uuid']) ?>" class="hero-btn ms-auto">✎ Modifier</a>
|
||||
<a href="/delete/<?= 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 !== ''): ?>
|
||||
<?php if ($authorSlugVal !== ''): ?>
|
||||
<a href="/profil/<?= rawurlencode($authorSlugVal) ?>" class="text-reset"><?= htmlspecialchars($authorName) ?></a>
|
||||
<?php else: ?>
|
||||
<span><?= htmlspecialchars($authorName) ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="mx-1 opacity-50">·</span>
|
||||
<?php endif; ?>
|
||||
<?= $pubDate ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="article-hero-right">
|
||||
<?php if ($hasSources): ?>
|
||||
<a href="/sources/<?= rawurlencode($article['uuid']) ?>" class="hero-btn">ℹ Sources</a>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$_heroReactionDefs = [
|
||||
'useful' => ['👍', 'Utile'],
|
||||
'important' => ['🔥', 'Important'],
|
||||
'interesting' => ['🤔', 'À creuser'],
|
||||
];
|
||||
?>
|
||||
<div class="hero-reactions" id="reactions">
|
||||
<?php foreach ($_heroReactionDefs as $type => [$icon, $label]): ?>
|
||||
<?php $active = in_array($type, $visitorReactions ?? [], true); ?>
|
||||
<form method="post" action="/react" class="reaction-form d-inline">
|
||||
<input type="hidden" name="uuid" value="<?= htmlspecialchars($article['uuid']) ?>">
|
||||
<input type="hidden" name="type" value="<?= htmlspecialchars($type) ?>">
|
||||
<input type="hidden" name="_back" value="/post/<?= rawurlencode($article['slug'] ?? '') ?>#reactions">
|
||||
<button type="submit"
|
||||
class="hero-reaction-btn<?= $active ? ' hero-reaction-btn--active' : '' ?> reaction-btn"
|
||||
data-type="<?= htmlspecialchars($type) ?>"
|
||||
data-uuid="<?= htmlspecialchars($article['uuid']) ?>"
|
||||
title="<?= htmlspecialchars($label) ?>">
|
||||
<span><?= $icon ?></span>
|
||||
<span class="reaction-count" data-type="<?= htmlspecialchars($type) ?>"><?= (int)($reactionStats[$type] ?? 0) ?></span>
|
||||
</button>
|
||||
</form>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-text post-content">
|
||||
<?= $_renderedContent ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<?php include __DIR__ . '/comments_section.php'; ?>
|
||||
|
||||
</div><!-- /col principale -->
|
||||
|
||||
<div class="post-sidebar-col order-3">
|
||||
<aside class="related-sidebar">
|
||||
|
||||
<?php if (count($_tocItems) >= 3): ?>
|
||||
<h6 class="related-sidebar-title">Table des matières</h6>
|
||||
<ul class="toc-list">
|
||||
<?php foreach ($_tocItems as $_ti): ?>
|
||||
<li class="toc-h<?= (int) $_ti['level'] ?>">
|
||||
<a href="#<?= htmlspecialchars($_ti['id']) ?>"><?= htmlspecialchars($_ti['text']) ?></a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="toc-nav">
|
||||
<button class="toc-nav-btn" id="toc-go-top">↑ Haut</button>
|
||||
<button class="toc-nav-btn" id="toc-go-bottom">↓ Bas</button>
|
||||
</div>
|
||||
<ul class="toc-list mt-2">
|
||||
<?php if (!empty($alsoReadArticles ?? [])): ?>
|
||||
<li><a href="#also-read">À lire aussi</a></li>
|
||||
<?php endif; ?>
|
||||
<li><a href="#reactions">Réactions</a></li>
|
||||
<li><a href="#comments">Commentaires</a></li>
|
||||
</ul>
|
||||
|
||||
<?php if (!empty($backlinks ?? [])): ?>
|
||||
<h6 class="related-sidebar-title">Rétroliens</h6>
|
||||
<?php foreach ($backlinks as $_bl):
|
||||
$_blCover = $_bl['cover'] ?? '';
|
||||
$_blCat = trim($_bl['category'] ?? '');
|
||||
$_blGradient = coverGradient($_blCat !== '' ? $_blCat : $_bl['uuid'], $allCats ?? []);
|
||||
$_blDate = date('d/m/Y', strtotime((string)($_bl['published_at'] ?? $_bl['created_at'] ?? '')));
|
||||
?>
|
||||
<a href="/post/<?= rawurlencode($_bl['slug'] ?? '') ?>" class="related-card">
|
||||
<div class="related-card-thumb" style="<?= $_blCover !== ''
|
||||
? 'background-image:url(/file?uuid=' . rawurlencode($_bl['uuid']) . '&name=' . rawurlencode($_blCover) . ');background-size:cover;background-position:center'
|
||||
: 'background:' . htmlspecialchars($_blGradient) ?>">
|
||||
</div>
|
||||
<div class="related-card-body">
|
||||
<div class="related-card-title"><?= htmlspecialchars($_bl['title']) ?></div>
|
||||
<div class="related-card-date"><?= $_blDate ?></div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($attachments)): ?>
|
||||
<?php $filesMeta = $article['files_meta'] ?? []; ?>
|
||||
<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']);
|
||||
$fmeta = $filesMeta[$file['name']] ?? [];
|
||||
$fAuthor = trim($fmeta['author'] ?? '');
|
||||
$fSource = trim($fmeta['source_url'] ?? '');
|
||||
?>
|
||||
<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
|
||||
<?php if ($fAuthor !== ''): ?>
|
||||
· <?= htmlspecialchars($fAuthor) ?>
|
||||
<?php endif; ?>
|
||||
<?php if ($fSource !== ''): ?>
|
||||
· <?= htmlspecialchars(parse_url($fSource, PHP_URL_HOST) ?: $fSource) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($externalLinks)): ?>
|
||||
<h6 class="related-sidebar-title">Liens & 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; ?>
|
||||
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
</div><!-- /row -->
|
||||
|
||||
<script src="/assets/js/toc.js" defer></script>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = htmlspecialchars($article['title']);
|
||||
$seoTitle = ($article['seo_title'] ?? '') ?: $article['title'];
|
||||
$ogType = 'article';
|
||||
$ogUrl = url('post/' . rawurlencode($article['slug'] ?? ''));
|
||||
$canonical = $ogUrl;
|
||||
$articlePublishedAt = $article['published_at'] ?? '';
|
||||
$mainClass = 'container-fluid';
|
||||
|
||||
// Auto-description depuis le contenu si le champ SEO est vide
|
||||
$seoDescription = $article['seo_description'] ?? '';
|
||||
if ($seoDescription === '') {
|
||||
$plain = strip_tags($Parsedown->text($article['content']));
|
||||
$plain = preg_replace('/\s+/', ' ', $plain);
|
||||
$seoDescription = mb_strimwidth(trim((string)$plain), 0, 155, '…');
|
||||
}
|
||||
|
||||
// og:image : cover puis fallback og_image du meta
|
||||
if ($ogImage === null || $ogImage === '') {
|
||||
$ogImage = $article['og_image'] ?? '';
|
||||
}
|
||||
|
||||
// Auteur : nom et URL de profil résolus depuis le champ author du JSON de l'article
|
||||
$metaAuthor = $authorName;
|
||||
$metaAuthorUrl = $authorProfileUrl;
|
||||
|
||||
// JSON-LD Article
|
||||
$jsonLdData = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'BlogPosting',
|
||||
'headline' => $seoTitle,
|
||||
'description' => $seoDescription,
|
||||
'url' => $canonical,
|
||||
'datePublished' => date('c', strtotime((string)$articlePublishedAt)),
|
||||
'dateModified' => date('c', strtotime((string)($article['updated_at'] ?? $articlePublishedAt))),
|
||||
'author' => array_filter([
|
||||
'@type' => 'Person',
|
||||
'name' => $metaAuthor !== '' ? $metaAuthor : siteTitle(),
|
||||
'url' => $metaAuthorUrl !== '' ? $metaAuthorUrl : null,
|
||||
]),
|
||||
'publisher' => [
|
||||
'@type' => 'Blog',
|
||||
'name' => siteTitle(),
|
||||
'url' => rtrim(APP_URL, '/'),
|
||||
],
|
||||
'inLanguage' => siteLang(),
|
||||
];
|
||||
if (!empty($ogImage)) {
|
||||
$jsonLdData['image'] = $ogImage;
|
||||
}
|
||||
$jsonLd = json_encode($jsonLdData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php ob_start(); ?>
|
||||
|
||||
<form method="post" action="/profile">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 mb-4">
|
||||
<h1 class="h4 mb-0">Mon profil</h1>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Enregistrer</button>
|
||||
</div>
|
||||
|
||||
<?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; ?>
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Colonne gauche : identité -->
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header small fw-semibold">Identité</div>
|
||||
<div class="card-body d-flex flex-column gap-3">
|
||||
<div>
|
||||
<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.
|
||||
<?php if (($profileCurrentSlug ?? '') !== ''): ?>
|
||||
<br>Page publique : <a href="/profil/<?= rawurlencode($profileCurrentSlug) ?>">/profil/<?= htmlspecialchars($profileCurrentSlug) ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label fw-semibold text-muted">Email</label>
|
||||
<input type="text" class="form-control" value="<?= htmlspecialchars(currentUserEmail() ?? '') ?>" disabled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colonne droite : page publique -->
|
||||
<div class="col-md-8">
|
||||
<div class="card h-100">
|
||||
<div class="card-header small fw-semibold">Page publique</div>
|
||||
<div class="card-body d-flex flex-column gap-3">
|
||||
<div>
|
||||
<label class="form-label fw-semibold" for="bio">Biographie</label>
|
||||
<textarea id="bio" name="bio" class="form-control" rows="5"
|
||||
placeholder="Quelques mots sur vous…"><?= htmlspecialchars($profileCurrentBio ?? '') ?></textarea>
|
||||
<div class="form-text">Affichée sur votre page de profil public.</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label fw-semibold" for="profile_url">URL externe</label>
|
||||
<input type="url" id="profile_url" name="profile_url"
|
||||
class="form-control"
|
||||
value="<?= htmlspecialchars($profileCurrentUrl ?? '') ?>"
|
||||
placeholder="https://example.com/~vous">
|
||||
<div class="form-text">Lien vers un site ou profil externe (utilisé dans les métadonnées article:author, JSON-LD).</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Flux RSS -->
|
||||
<!-- Mes liens -->
|
||||
<div class="mt-4" id="links">
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<h2 class="h6 text-muted mb-0">Mes liens</h2>
|
||||
<?php if (($profileCurrentSlug ?? '') !== ''): ?>
|
||||
<a href="/liens/<?= rawurlencode($profileCurrentSlug) ?>" class="small" target="_blank">↗ voir la page publique</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="row g-3 align-items-start">
|
||||
<div class="col-md-8">
|
||||
<?php if (!empty($profileLinks)): ?>
|
||||
<div class="card mb-3">
|
||||
<ul class="list-group list-group-flush" id="links-sortable">
|
||||
<?php foreach ($profileLinks as $_link): ?>
|
||||
<li class="list-group-item d-flex align-items-center gap-2 py-2" data-id="<?= (int)$_link['id'] ?>">
|
||||
<span class="drag-handle text-muted me-1" style="cursor:grab">⠿</span>
|
||||
<div class="flex-grow-1 min-w-0">
|
||||
<div class="fw-semibold small text-truncate"><?= htmlspecialchars($_link['title'] ?: $_link['url']) ?></div>
|
||||
<div class="text-muted small text-truncate"><?= htmlspecialchars($_link['url']) ?></div>
|
||||
</div>
|
||||
<form method="post" action="/link/delete" class="flex-shrink-0">
|
||||
<input type="hidden" name="link_id" value="<?= (int)$_link['id'] ?>">
|
||||
<button class="btn btn-sm btn-outline-danger py-0" data-confirm="Supprimer ce lien ?">✕</button>
|
||||
</form>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<form method="post" action="/link/reorder" id="reorder-form" class="d-none">
|
||||
<?php foreach ($profileLinks as $__i => $_link): ?>
|
||||
<input type="hidden" name="order[]" value="<?= (int)$_link['id'] ?>">
|
||||
<?php endforeach; ?>
|
||||
</form>
|
||||
<script src="/assets/js/links-sortable.js" defer></script>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header small fw-semibold">Ajouter un lien</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/link/add">
|
||||
<div class="mb-2">
|
||||
<input type="url" name="link_url" class="form-control form-control-sm"
|
||||
placeholder="https://…" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<input type="text" name="link_title" class="form-control form-control-sm"
|
||||
placeholder="Titre" maxlength="100">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="text" name="link_desc" class="form-control form-control-sm"
|
||||
placeholder="Description courte (optionnel)" maxlength="200">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm w-100">Ajouter</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4" id="feeds">
|
||||
<h2 class="h6 text-muted mb-3">Flux RSS</h2>
|
||||
<div class="row g-3 align-items-start">
|
||||
<div class="col-md-8">
|
||||
<?php if (!empty($profileFeeds)): ?>
|
||||
<div class="card mb-3">
|
||||
<ul class="list-group list-group-flush">
|
||||
<?php foreach ($profileFeeds as $_feed): ?>
|
||||
<li class="list-group-item d-flex align-items-center gap-2 py-2">
|
||||
<div class="flex-grow-1 min-w-0">
|
||||
<div class="fw-semibold small text-truncate"><?= htmlspecialchars($_feed['label'] ?: $_feed['feed_url']) ?></div>
|
||||
<div class="text-muted small text-truncate"><?= htmlspecialchars($_feed['feed_url']) ?></div>
|
||||
</div>
|
||||
<form method="post" action="/feed/delete" class="flex-shrink-0">
|
||||
<input type="hidden" name="feed_id" value="<?= (int)$_feed['id'] ?>">
|
||||
<button class="btn btn-sm btn-outline-danger py-0" data-confirm="Supprimer ce flux ?">✕</button>
|
||||
</form>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header small fw-semibold">Ajouter un flux</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/feed/add">
|
||||
<div class="mb-2">
|
||||
<input type="url" name="feed_url" class="form-control form-control-sm"
|
||||
placeholder="https://example.com/feed.xml" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="text" name="feed_label" class="form-control form-control-sm"
|
||||
placeholder="Libellé (optionnel)" maxlength="100">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm w-100">Ajouter</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$pdo = dbPdo();
|
||||
$_profileRoles = [];
|
||||
if ($pdo) {
|
||||
$st = $pdo->prepare(
|
||||
'SELECT r.name, r.label, COALESCE(array_agg(rc.capability) FILTER (WHERE rc.capability IS NOT NULL), \'{}\') AS caps
|
||||
FROM user_roles ur
|
||||
JOIN roles r ON r.id = ur.role_id
|
||||
LEFT JOIN role_capabilities rc ON rc.role_id = r.id
|
||||
WHERE ur.user_email = :email
|
||||
GROUP BY r.id, r.name, r.label
|
||||
ORDER BY r.name'
|
||||
);
|
||||
$st->execute([':email' => currentUserEmail()]);
|
||||
$_profileRoles = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
if (!empty($_profileRoles)): ?>
|
||||
<div class="mt-4">
|
||||
<h2 class="h6 text-muted mb-3">Rôles & droits</h2>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($_profileRoles as $_role):
|
||||
$_caps = array_filter(
|
||||
explode(',', trim((string)$_role['caps'], '{}')),
|
||||
static fn ($c) => $c !== ''
|
||||
);
|
||||
?>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center gap-2 py-2">
|
||||
<strong><?= htmlspecialchars($_role['label']) ?></strong>
|
||||
<code class="text-muted small"><?= htmlspecialchars($_role['name']) ?></code>
|
||||
</div>
|
||||
<?php if (!empty($_caps)): ?>
|
||||
<ul class="list-group list-group-flush small">
|
||||
<?php foreach ($_caps as $_cap):
|
||||
$_label = KNOWN_CAPABILITIES[trim($_cap)] ?? trim($_cap); ?>
|
||||
<li class="list-group-item py-1"><?= htmlspecialchars($_label) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<div class="card-body py-2 small text-muted">Aucun droit associé à ce rôle.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = 'Mon profil';
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php ob_start(); ?>
|
||||
|
||||
<div class="search-page">
|
||||
|
||||
<form class="search-bar mb-5" action="/search" method="GET" role="search">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control form-control-lg" name="q"
|
||||
value="<?= htmlspecialchars($searchQuery) ?>"
|
||||
placeholder="Rechercher…" autofocus autocomplete="off"
|
||||
aria-label="Rechercher">
|
||||
<button class="btn btn-primary" type="submit">Rechercher</button>
|
||||
</div>
|
||||
<div class="form-text mt-1">
|
||||
Plusieurs mots = recherche ET · Fautes de frappe tolérées · Accents ignorés
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if ($searchQuery !== ''): ?>
|
||||
|
||||
<?php if (empty($searchResults)): ?>
|
||||
<p class="text-muted">Aucun résultat pour <strong><?= htmlspecialchars($searchQuery) ?></strong>.</p>
|
||||
<?php else: ?>
|
||||
<p class="text-muted mb-4">
|
||||
<?= count($searchResults) ?> résultat<?= count($searchResults) > 1 ? 's' : '' ?>
|
||||
pour <strong><?= htmlspecialchars($searchQuery) ?></strong>
|
||||
</p>
|
||||
|
||||
<?php
|
||||
$tierLabels = [1 => 'Dans le titre', 2 => 'Dans le texte', 3 => 'À peu près'];
|
||||
$byTier = [];
|
||||
foreach ($searchResults as $r) {
|
||||
$byTier[$r['tier'] ?? 3][] = $r;
|
||||
}
|
||||
$multiTier = count($byTier) > 1;
|
||||
?>
|
||||
|
||||
<div class="search-results">
|
||||
<?php foreach ($byTier as $tierNum => $tierResults): ?>
|
||||
<?php if ($multiTier): ?>
|
||||
<h3 class="h6 text-muted mt-4 mb-2 border-bottom pb-1"><?= $tierLabels[$tierNum] ?? '' ?></h3>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($tierResults as $r):
|
||||
$a = $r['article'];
|
||||
$postUrl = '/post/' . rawurlencode($a['slug'] ?? '');
|
||||
$cat = trim($a['category'] ?? '');
|
||||
$date = $a['published_at'] ? date('d/m/Y', strtotime((string)$a['published_at'])) : '';
|
||||
?>
|
||||
<article class="search-result">
|
||||
<div class="search-result-meta">
|
||||
<?php if ($cat !== ''): ?>
|
||||
<a class="search-result-cat" href="/categorie/<?= rawurlencode($cat) ?>"><?= htmlspecialchars($cat) ?></a>
|
||||
<?php endif; ?>
|
||||
<?php if ($date !== ''): ?>
|
||||
<time class="search-result-date"><?= $date ?></time>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<h2 class="search-result-title">
|
||||
<a href="<?= htmlspecialchars($postUrl) ?>"><?= htmlspecialchars($a['title'] ?? '') ?></a>
|
||||
</h2>
|
||||
<?php if ($r['snippet'] !== ''): ?>
|
||||
<p class="search-result-snippet"><?= $r['snippet'] ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
$title = $searchQuery !== '' ? 'Recherche : ' . $searchQuery : 'Recherche';
|
||||
$metaRobots = 'noindex, follow';
|
||||
$canonical = rtrim(APP_URL, '/') . '/';
|
||||
include __DIR__ . '/layout.php';
|
||||
@@ -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="/edit/<?= rawurlencode($article['uuid']) ?>" class="btn btn-secondary btn-sm">← Modifier</a>
|
||||
<h1 class="h4 mb-0">Sources & 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 & 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';
|
||||
Reference in New Issue
Block a user