feat: licence page, admin pages editor, license types, gradient card placeholders, latest-year home view

- Feature 1: public /licence.php fetches 'licenses' page from DB, renders Markdown
- Feature 1: nav.php adds 'Licence' link with active state
- Feature 2: Database::getPage(), savePage(), getAllPages() methods
- Feature 2: bundled src/Parsedown.php (MIT, zero-dependency)
- Feature 2: apropos.php now renders 'about' page content from DB via Parsedown
- Feature 2: admin/pages.php (list) + admin/pages-edit.php (EasyMDE editor)
- Feature 2: admin/actions/page.php (auth+CSRF+validation+save)
- Feature 2: admin/head.php adds 'Pages statiques' nav link
- Feature 3: storage/schema.sql seeds 8 CC license types
- Feature 3: storage/migrations/003_seed_license_types.sql (applied to live DB)
- Feature 3: Database::getLicenseTypes() / getAllLicenseTypes()
- Feature 3: admin/add.php + formulaire.php: license_id field on add form
- Feature 3: admin/edit.php: license_id field on edit form with raw FK lookup
- Feature 3: tfe.php: shows 'Licence :' meta row when non-null
- Feature 6: main.css: .card__media--gradient styles
- Feature 6: index.php: deterministic HSL gradient placeholder cards
- Feature 6: Database::getLatestYearTheses() + getLatestPublishedYear()
- Feature 6: index.php default home = random latest-year theses with info label
This commit is contained in:
Pontoporeia
2026-03-24 13:12:48 +01:00
parent 86a2082edc
commit d87348c388
20 changed files with 2553 additions and 152 deletions

View File

@@ -105,6 +105,9 @@ try {
// Formats (optional, multiple selection)
$formatIds = isset($_POST["formats"]) ? array_map('intval', $_POST["formats"]) : [];
// License
$licenseId = filter_var($_POST['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null;
// External link
$lien = $_POST["lien"] ?? '';
if (!empty($lien)) {
@@ -135,9 +138,9 @@ try {
identifier, title, subtitle, year,
orientation_id, ap_program_id, finality_id,
synopsis, file_size_info,
baiu_link,
baiu_link, license_id,
submitted_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
");
$stmt->execute([
@@ -150,7 +153,8 @@ try {
$finalityId,
$synopsis,
!empty($durationInfo) ? $durationInfo : null,
!empty($lien) ? $lien : null
!empty($lien) ? $lien : null,
$licenseId
]);
$thesisId = $pdo->lastInsertId();

View File

@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php';
AdminAuth::requireLogin();
// CSRF check
if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) ||
!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
die("Erreur de sécurité : token invalide.");
}
$allowedSlugs = ['about', 'licenses', 'charte', 'contact'];
$slug = $_POST['slug'] ?? '';
if (!in_array($slug, $allowedSlugs)) {
die("Slug invalide.");
}
$content = $_POST['content'] ?? '';
if (strlen($content) > 65535) {
die("Contenu trop long (max 65 535 caractères).");
}
require_once __DIR__ . '/../../src/Database.php';
try {
$db = new Database();
$db->savePage($slug, $content);
$_SESSION['success'] = "Page «" . $slug . "» mise à jour avec succès.";
} catch (Exception $e) {
error_log("Page save error: " . $e->getMessage());
die("Erreur lors de la sauvegarde : " . htmlspecialchars($e->getMessage()));
}
header('Location: /admin/pages.php');
exit;

View File

@@ -18,6 +18,7 @@ try {
$finalityTypes = $db->getAllFinalityTypes();
$languages = $db->getAllLanguages();
$formatTypes = $db->getAllFormatTypes();
$licenseTypes = $db->getAllLicenseTypes();
} catch (Exception $e) {
error_log("Failed to load form data: " . $e->getMessage());
die("Erreur lors du chargement du formulaire.");
@@ -191,6 +192,20 @@ function wasSelected($key, $value) {
rows="7" required><?= old('synopsis') ?></textarea>
</div>
<!-- Licence -->
<div class="admin-form-row">
<label class="admin-label" for="license_id">Licence :</label>
<select class="admin-select" id="license_id" name="license_id">
<option value="">— Inconnue —</option>
<?php foreach ($licenseTypes as $lt): ?>
<option value="<?= htmlspecialchars($lt['id']) ?>"
<?= wasSelected('license_id', $lt['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($lt['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- Durée/Taille -->
<div class="admin-form-row">
<label class="admin-label" for="duration_info">Durée / Taille :</label>

View File

@@ -36,6 +36,8 @@ try {
$db->beginTransaction();
// Update thesis basic info
$editLicenseId = filter_var($_POST['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null;
$stmt = $pdo->prepare("
UPDATE theses SET
title = ?,
@@ -47,6 +49,7 @@ try {
synopsis = ?,
file_size_info = ?,
baiu_link = ?,
license_id = ?,
is_published = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
@@ -62,6 +65,7 @@ try {
trim($_POST['synopsis']),
!empty($_POST['duration_info']) ? trim($_POST['duration_info']) : null,
!empty($_POST['lien']) ? trim($_POST['lien']) : null,
$editLicenseId,
isset($_POST['is_published']) ? 1 : 0,
$thesisId
]);
@@ -164,7 +168,13 @@ try {
$finalityTypes = $db->getAllFinalityTypes();
$languages = $db->getAllLanguages();
$formatTypes = $db->getAllFormatTypes();
$licenseTypes = $db->getAllLicenseTypes();
// Fetch raw license_id FK (view only exposes license_type name string)
$licenseStmt = $pdo->prepare("SELECT license_id FROM theses WHERE id = ?");
$licenseStmt->execute([$thesisId]);
$currentLicenseId = $licenseStmt->fetchColumn();
// Set page title for header
$pageTitle = "Éditer TFE - " . htmlspecialchars($thesis['title']);
@@ -247,6 +257,19 @@ try {
value="<?= htmlspecialchars($thesis['supervisors'] ?? '') ?>">
</div>
<div class="admin-form-row">
<label class="admin-label" for="license_id">Licence :</label>
<select class="admin-select" id="license_id" name="license_id">
<option value="">— Inconnue —</option>
<?php foreach ($licenseTypes as $lt): ?>
<option value="<?= $lt['id'] ?>"
<?= ($currentLicenseId == $lt['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($lt['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="admin-form-row">
<label class="admin-label" for="titre">Titre :</label>
<input class="admin-input" type="text" id="titre" name="titre"

View File

@@ -0,0 +1,67 @@
<?php
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php';
AdminAuth::requireLogin();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
require_once __DIR__ . '/../../src/Database.php';
$allowedSlugs = ['about', 'licenses', 'charte', 'contact'];
$slug = $_GET['slug'] ?? '';
if (!in_array($slug, $allowedSlugs)) {
header('Location: /admin/pages.php');
exit;
}
try {
$db = new Database();
$page = $db->getPage($slug);
if (!$page) {
die("Page introuvable.");
}
} catch (Exception $e) {
die("Erreur: " . htmlspecialchars($e->getMessage()));
}
$pageTitle = "Éditer : " . htmlspecialchars($page['title']);
?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
<main class="admin-main">
<h1 class="admin-page-title">Éditer : <?= htmlspecialchars($page['title']) ?></h1>
<form action="/admin/actions/page.php" method="post" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="slug" value="<?= htmlspecialchars($slug) ?>">
<div class="admin-form-row" style="align-items:start;">
<label class="admin-label" for="content">Contenu (Markdown) :</label>
<textarea class="admin-textarea" id="content" name="content"
rows="20"><?= htmlspecialchars($page['content'] ?? '') ?></textarea>
</div>
<div class="admin-submit-wrap">
<button type="submit" class="admin-btn">Enregistrer</button>
<a href="/admin/pages.php" class="admin-btn-secondary" style="margin-left:.75rem;">Annuler</a>
</div>
</form>
</main>
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
<script>
var easyMDE = new EasyMDE({
element: document.getElementById('content'),
spellChecker: false,
status: ['lines', 'words'],
minHeight: '400px',
toolbarTips: true
});
</script>
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>

55
public/admin/pages.php Normal file
View File

@@ -0,0 +1,55 @@
<?php
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php';
AdminAuth::requireLogin();
require_once __DIR__ . '/../../src/Database.php';
$pageTitle = "Pages statiques";
try {
$db = new Database();
$pages = $db->getAllPages();
} catch (Exception $e) {
error_log("Error loading pages: " . $e->getMessage());
die("Erreur lors du chargement des pages.");
}
$success = $_SESSION['success'] ?? null;
unset($_SESSION['success']);
?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<main class="admin-main">
<h1 class="admin-page-title">Pages statiques</h1>
<?php if ($success): ?>
<div class="admin-alert admin-alert--success">✓ <?= htmlspecialchars($success) ?></div>
<?php endif; ?>
<table class="admin-table">
<thead>
<tr>
<th>Slug</th>
<th>Titre</th>
<th>Mis à jour</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($pages as $p): ?>
<tr>
<td><code><?= htmlspecialchars($p['slug']) ?></code></td>
<td><?= htmlspecialchars($p['title']) ?></td>
<td><?= htmlspecialchars($p['updated_at'] ?? '—') ?></td>
<td>
<a href="/admin/pages-edit.php?slug=<?= urlencode($p['slug']) ?>"
class="admin-btn" style="font-size:.8rem;padding:.3rem .75rem;">Éditer</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</main>
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>