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