Refactor about.php

- Hardcode source code URL and credits in about template, remove from DB/admin interface; only contacts remains editable
- Merge apropos editables into one À propos section, remove charte, add editable source code URL
This commit is contained in:
Pontoporeia
2026-05-07 18:44:30 +02:00
parent 24d68dda59
commit e0c748d8e7
15 changed files with 259 additions and 250 deletions

2
.gitignore vendored
View File

@@ -8,6 +8,8 @@ app/storage/test.db
### Logs ### ### Logs ###
error.log error.log
app/storage/logs/*.log
!app/storage/logs/.gitkeep
app/storage/maintenance.flag app/storage/maintenance.flag
app/storage/cache/* app/storage/cache/*
!app/storage/cache/.gitkeep !app/storage/cache/.gitkeep

15
TODO.md
View File

@@ -1,5 +1,20 @@
# XAMXAM TODO # XAMXAM TODO
## Merge apropos editables into À propos page + remove charte + source code URL
- [x] `actions/apropos.php` — only `contacts`; removed credits, erg_url
- [x] `actions/page.php` — remove `charte` from allowed slugs
- [x] `contenus.php` (front controller) — filter pages to only show `about` + `licenses`
- [x] `templates/admin/contenus.php` — restored "Pages statiques" table
- [x] `contenus-edit.php` (front controller) — `about` slug loads contacts for unified edit
- [x] `templates/admin/contenus-edit.php``about_page` type: Markdown editor + contacts on one page
- [x] `templates/admin/apropos-groups-form.php` — reusable partial for contacts
- [x] `Database.php` — simplified getAproposContent/saveAproposContent (contacts-only JSON)
- [x] `storage/schema.sql` — removed credits, erg_url; only contacts remains
- [x] `AboutController.php` — removed credits, sourceCode DB loading
- [x] `templates/public/about.php` — hardcoded source code URL, hardcoded credits HTML
- [x] `apropos.css``.apropos-toc-source` styles
- [x] `.gitignore` — ignore `app/storage/logs/*.log`
## Duplicate TFE submission prevention (fixes) ## Duplicate TFE submission prevention (fixes)
- [x] `DuplicateThesisException` — typed exception carrying existing thesis metadata - [x] `DuplicateThesisException` — typed exception carrying existing thesis metadata
- [x] `Database::findDuplicateThesis()` — year + author + normalised-title matching (exact, prefix, Levenshtein ≤10%) - [x] `Database::findDuplicateThesis()` — year + author + normalised-title matching (exact, prefix, Levenshtein ≤10%)

View File

@@ -1,7 +1,7 @@
<?php <?php
/** /**
* Save handler for apropos contents (contacts, credits). * Save handler for apropos contacts.
* Structure: groups[] with label/role, each having entries[] of {text, url, email}. * Structure: groups[] with role, each having entries[] of {text, url, email}.
*/ */
require_once __DIR__ . "/../../../bootstrap.php"; require_once __DIR__ . "/../../../bootstrap.php";
require_once __DIR__ . '/../../../src/AdminAuth.php'; require_once __DIR__ . '/../../../src/AdminAuth.php';
@@ -13,7 +13,7 @@ if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) ||
die("Erreur de sécurité : token invalide."); die("Erreur de sécurité : token invalide.");
} }
$allowedKeys = ['contacts', 'credits']; $allowedKeys = ['contacts'];
$aproposKey = $_POST['apropos_key'] ?? ''; $aproposKey = $_POST['apropos_key'] ?? '';
if (!in_array($aproposKey, $allowedKeys)) { if (!in_array($aproposKey, $allowedKeys)) {
die("Clé invalide."); die("Clé invalide.");
@@ -28,21 +28,6 @@ try {
$cleaned = []; $cleaned = [];
foreach ($groups as $group) { foreach ($groups as $group) {
if ($aproposKey === 'credits') {
$label = trim($group['label'] ?? '');
if ($label === '') continue;
$entries = [];
foreach ($group['entries'] ?? [] as $entry) {
$text = trim($entry['text'] ?? '');
if ($text === '') continue;
$e = ['text' => $text];
$url = trim($entry['url'] ?? '');
if ($url !== '') $e['url'] = $url;
$entries[] = $e;
}
if (empty($entries)) continue;
$cleaned[] = ['label' => $label, 'entries' => $entries];
} else { // contacts
$role = trim($group['role'] ?? ''); $role = trim($group['role'] ?? '');
if ($role === '') continue; if ($role === '') continue;
$entries = []; $entries = [];
@@ -60,7 +45,6 @@ try {
if (empty($entries)) continue; if (empty($entries)) continue;
$cleaned[] = ['role' => $role, 'entries' => $entries]; $cleaned[] = ['role' => $role, 'entries' => $entries];
} }
}
if (empty($cleaned)) { if (empty($cleaned)) {
die("Au moins un groupe avec des entrées est requis."); die("Au moins un groupe avec des entrées est requis.");

View File

@@ -13,7 +13,7 @@ if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
exit; exit;
} }
$allowedSlugs = ['about', 'licenses', 'charte']; $allowedSlugs = ['about', 'licenses'];
$slug = $_POST['slug'] ?? ''; $slug = $_POST['slug'] ?? '';
$content = $_POST['content'] ?? ''; $content = $_POST['content'] ?? '';

View File

@@ -9,8 +9,8 @@ if (empty($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32)); $_SESSION["csrf_token"] = bin2hex(random_bytes(32));
} }
$allowedPageSlugs = ["about", "licenses", "charte"]; $allowedPageSlugs = ["about", "licenses"];
$allowedApropos = ["contacts", "credits"]; $allowedApropos = ["contacts"];
$pageSlug = $_GET["slug"] ?? ""; $pageSlug = $_GET["slug"] ?? "";
$aproposKey = $_GET["apropos"] ?? ""; $aproposKey = $_GET["apropos"] ?? "";
@@ -40,7 +40,13 @@ try {
die("Page introuvable."); die("Page introuvable.");
} }
$editTitle = $page["title"]; $editTitle = $page["title"];
if ($pageSlug === 'about') {
$editType = 'about_page';
$aboutContacts = $db->getAproposContent('contacts');
$aboutContacts = is_array($aboutContacts) ? $aboutContacts : [];
} else {
$editType = "page"; $editType = "page";
}
} elseif ($formHelpKey) { } elseif ($formHelpKey) {
$editType = "form_help"; $editType = "form_help";
$formHelpContent = $db->getFormHelpBlock($formHelpKey); $formHelpContent = $db->getFormHelpBlock($formHelpKey);
@@ -50,7 +56,6 @@ try {
$value = $db->getAproposContent($aproposKey); $value = $db->getAproposContent($aproposKey);
$editTitle = match($aproposKey) { $editTitle = match($aproposKey) {
'contacts' => 'Contacts', 'contacts' => 'Contacts',
'credits' => 'Crédits',
}; };
} }
} catch (Exception $e) { } catch (Exception $e) {
@@ -58,6 +63,13 @@ try {
} }
$pageTitle = "Éditer : " . $editTitle; $pageTitle = "Éditer : " . $editTitle;
$initialContent = '';
$extraJs = [];
$extraJsInline = '';
if ($editType === 'page' || $editType === 'about_page') {
$initialContent = $page["content"] ?? "";
$extraJs = ["/assets/js/overtype.min.js"]; $extraJs = ["/assets/js/overtype.min.js"];
$extraJsInline = <<<'JS' $extraJsInline = <<<'JS'
var OT = window.OverType.default || window.OverType; var OT = window.OverType.default || window.OverType;
@@ -69,12 +81,19 @@ var editor = new OT(document.getElementById('editor'), {
onChange: function(value) { hidden.value = value; } onChange: function(value) { hidden.value = value; }
}); });
JS; JS;
$initialContent = '';
if ($editType === 'page') {
$initialContent = $page["content"] ?? "";
} elseif ($editType === 'form_help') { } elseif ($editType === 'form_help') {
$initialContent = $formHelpContent; $initialContent = $formHelpContent;
$extraJs = ["/assets/js/overtype.min.js"];
$extraJsInline = <<<'JS'
var OT = window.OverType.default || window.OverType;
var hidden = document.getElementById('content');
var editor = new OT(document.getElementById('editor'), {
value: hidden.value,
minHeight: '400px',
spellcheck: false,
onChange: function(value) { hidden.value = value; }
});
JS;
} }
$isAdmin = true; $isAdmin = true;

View File

@@ -10,9 +10,12 @@ if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32)); $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
} }
$allowedPageSlugs = ['about', 'licenses'];
try { try {
$db = new Database(); $db = new Database();
$pages = $db->getAllPages(); $allPages = $db->getAllPages();
$pages = array_values(array_filter($allPages, fn($p) => in_array($p['slug'], $allowedPageSlugs, true)));
$aproposKeys = $db->getAllAproposContents(); $aproposKeys = $db->getAllAproposContents();
$formHelpBlocks = $db->getAllFormHelpBlocks(); $formHelpBlocks = $db->getAllFormHelpBlocks();
} catch (Exception $e) { } catch (Exception $e) {

View File

@@ -89,6 +89,21 @@
opacity: 0.75; opacity: 0.75;
} }
.apropos-toc-source {
padding-top: var(--space-xs);
}
.apropos-toc-source a {
font-size: var(--step--2);
color: var(--accent-primary);
text-decoration: none;
transition: opacity 0.15s;
}
.apropos-toc-source a:hover {
opacity: 0.75;
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Right — main content area */ /* Right — main content area */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */

View File

@@ -22,14 +22,11 @@ class AboutController
$rawContent = $this->defaultContent; $rawContent = $this->defaultContent;
} }
$contacts = $db->getAproposContent('contacts'); $contacts = $db->getAproposContent('contacts');
$credits = $db->getAproposContent('credits');
$contacts = is_array($contacts) && !empty($contacts) ? $contacts : null; $contacts = is_array($contacts) && !empty($contacts) ? $contacts : null;
$credits = is_array($credits) && !empty($credits) ? $credits : null;
} catch (Exception $e) { } catch (Exception $e) {
error_log('Error loading about page: ' . $e->getMessage()); error_log('Error loading about page: ' . $e->getMessage());
$rawContent = $this->defaultContent; $rawContent = $this->defaultContent;
$contacts = null; $contacts = null;
$credits = null;
} }
$pd = new Parsedown(); $pd = new Parsedown();
@@ -39,7 +36,6 @@ class AboutController
'currentNav' => 'apropos', 'currentNav' => 'apropos',
'aboutHtml' => $pd->text($rawContent), 'aboutHtml' => $pd->text($rawContent),
'contacts' => $contacts, 'contacts' => $contacts,
'credits' => $credits,
'pageTitle' => 'À Propos XAMXAM', 'pageTitle' => 'À Propos XAMXAM',
'metaDescription' => "À propos de XAMXAM, le répertoire des mémoires de fin d'études de l'erg École de Recherches Graphiques de Bruxelles.", 'metaDescription' => "À propos de XAMXAM, le répertoire des mémoires de fin d'études de l'erg École de Recherches Graphiques de Bruxelles.",
'extraCss' => ['/assets/css/apropos.css'], 'extraCss' => ['/assets/css/apropos.css'],

View File

@@ -2239,31 +2239,21 @@ class Database
return null; return null;
} }
$value = $row['value']; $decoded = json_decode($row['value'], true);
if ($key === 'erg_url') {
return $value;
}
$decoded = json_decode($value, true);
return is_array($decoded) ? $decoded : null; return is_array($decoded) ? $decoded : null;
} }
/** /**
* Save an apropos content value by key. * Save an apropos content value by key (contacts JSON).
* @param string $key
* @param mixed $value array for contacts/credits, string for erg_url
*/ */
public function saveAproposContent(string $key, $value): void public function saveAproposContent(string $key, array $value): void
{ {
$stmt = $this->pdo->prepare('SELECT id FROM apropos_contents WHERE key = ?'); $storedValue = json_encode($value, JSON_UNESCAPED_UNICODE);
$stmt->execute([$key]);
if (!$stmt->fetch()) {
throw new Exception("Apropos key not found: $key");
}
$storedValue = is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : (string)$value;
$stmt = $this->pdo->prepare( $stmt = $this->pdo->prepare(
'UPDATE apropos_contents SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?' 'INSERT INTO apropos_contents (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP'
); );
$stmt->execute([$storedValue, $key]); $stmt->execute([$key, $storedValue]);
} }
/** /**

View File

@@ -11,3 +11,4 @@
{"timestamp":"2026-05-05T16:57:57+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":15,"ids":[53,52,51,50,49,48,47,46,45,44,43,42,41,40,39]}} {"timestamp":"2026-05-05T16:57:57+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":15,"ids":[53,52,51,50,49,48,47,46,45,44,43,42,41,40,39]}}
{"timestamp":"2026-05-05T16:58:02+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":25,"ids":[178,177,176,175,174,173,172,171,170,169,168,167,166,165,164,163,162,161,160,159,158,157,156,155,154]}} {"timestamp":"2026-05-05T16:58:02+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":25,"ids":[178,177,176,175,174,173,172,171,170,169,168,167,166,165,164,163,162,161,160,159,158,157,156,155,154]}}
{"timestamp":"2026-05-07T16:15:27+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"system","action":"files_export","status":"success","context":{"file_count":0,"byte_size":248}} {"timestamp":"2026-05-07T16:15:27+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"system","action":"files_export","status":"success","context":{"file_count":0,"byte_size":248}}
{"timestamp":"2026-05-07T16:56:50+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"apropos","action":"edit","status":"success","context":{"key":"credits"}}

View File

@@ -408,8 +408,8 @@ SELECT * FROM smtp_settings WHERE id = 1;
CREATE TABLE IF NOT EXISTS apropos_contents ( CREATE TABLE IF NOT EXISTS apropos_contents (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL UNIQUE, -- 'contacts', 'credits', 'erg_url' key TEXT NOT NULL UNIQUE, -- 'contacts'
value TEXT, -- JSON array or plain string value TEXT, -- JSON array
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
@@ -433,23 +433,6 @@ INSERT OR IGNORE INTO apropos_contents (key, value) VALUES
{"text": "Brigitte Ledune", "url": "", "email": "brigitte.ledune@erg.be"} {"text": "Brigitte Ledune", "url": "", "email": "brigitte.ledune@erg.be"}
] ]
} }
]'),
('credits', '[
{
"label": "Design & développement",
"entries": [
{"text": "Olivia Marly", "url": ""},
{"text": "Théophile Gerveau-Mercie", "url": ""},
{"text": "Théo Hennequin", "url": ""}
]
},
{
"label": "Typographies",
"entries": [
{"text": "Ductus (Amélie Dumont)", "url": ""},
{"text": "BBB DM Sans", "url": ""}
]
}
]'); ]');
-- ============================================================================ -- ============================================================================

View File

@@ -0,0 +1,115 @@
<?php
/**
* Reusable partial for apropos contacts groups form.
* Expected variables:
* $aproposKey string 'contacts'
* $groups array Existing groups data
*/
?>
<form action="/admin/actions/apropos.php" method="post" class="admin-form" id="apropos-form-<?= $aproposKey ?>">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="apropos_key" value="<?= htmlspecialchars($aproposKey) ?>">
<?php foreach ($groups as $gi => $group): ?>
<fieldset class="apropos-group">
<legend>Contact <?= $gi + 1 ?></legend>
<label for="group_f_<?= $aproposKey ?>_<?= $gi ?>_role">Rôle :</label>
<input type="text" id="group_f_<?= $aproposKey ?>_<?= $gi ?>_role"
name="groups[<?= $gi ?>][role]"
value="<?= htmlspecialchars($group['role'] ?? '') ?>">
<?php $entries = is_array($group['entries'] ?? null) ? $group['entries'] : []; ?>
<?php foreach ($entries as $ei => $entry): ?>
<div class="apropos-entry">
<label for="entry_f_<?= $aproposKey ?>_<?= $gi ?>_<?= $ei ?>_text">Nom :</label>
<input type="text" id="entry_f_<?= $aproposKey ?>_<?= $gi ?>_<?= $ei ?>_text"
name="groups[<?= $gi ?>][entries][<?= $ei ?>][text]"
value="<?= htmlspecialchars($entry['text'] ?? '') ?>">
<label for="entry_f_<?= $aproposKey ?>_<?= $gi ?>_<?= $ei ?>_email">Email :</label>
<input type="email" id="entry_f_<?= $aproposKey ?>_<?= $gi ?>_<?= $ei ?>_email"
name="groups[<?= $gi ?>][entries][<?= $ei ?>][email]"
value="<?= htmlspecialchars($entry['email'] ?? '') ?>">
<label for="entry_f_<?= $aproposKey ?>_<?= $gi ?>_<?= $ei ?>_url">Lien (optionnel) :</label>
<input type="url" id="entry_f_<?= $aproposKey ?>_<?= $gi ?>_<?= $ei ?>_url"
name="groups[<?= $gi ?>][entries][<?= $ei ?>][url]"
value="<?= htmlspecialchars($entry['url'] ?? '') ?>">
</div>
<?php endforeach; ?>
<button type="button" class="btn btn--primary btn--sm add-entry-btn-f"
data-group="<?= $gi ?>"
data-key="<?= $aproposKey ?>">+ Ajouter une entrée</button>
</fieldset>
<?php endforeach; ?>
<button type="button" class="btn btn--primary add-group-btn-f"
data-key="<?= $aproposKey ?>">+ Ajouter un contact</button>
<div class="admin-form-footer">
<button type="submit" class="btn btn--primary">Enregistrer</button>
<a href="/admin/contenus.php" class="btn btn--secondary admin-cancel-link">Annuler</a>
</div>
<template id="entry-template-f-<?= $aproposKey ?>">
<div class="apropos-entry">
<label>Entrée :</label>
<input type="text" name="groups[{{gi}}][entries][{{ei}}][text]">
<label>Email :</label>
<input type="email" name="groups[{{gi}}][entries][{{ei}}][email]">
<label>Lien (optionnel) :</label>
<input type="url" name="groups[{{gi}}][entries][{{ei}}][url]">
</div>
</template>
<template id="group-template-f-<?= $aproposKey ?>">
<fieldset class="apropos-group">
<legend>Contact {{gi}}</legend>
<label>Rôle :</label>
<input type="text" name="groups[{{gi}}][role]">
<button type="button" class="btn btn--primary btn--sm add-entry-btn-f"
data-group="{{gi}}"
data-key="<?= $aproposKey ?>">+ Ajouter une entrée</button>
</fieldset>
</template>
</form>
<script>
(function() {
var key = '<?= $aproposKey ?>';
var form = document.getElementById('apropos-form-' + key);
var groupCount = <?= count($groups) ?>;
var entryTpl = document.getElementById('entry-template-f-' + key).innerHTML;
var groupTpl = document.getElementById('group-template-f-' + key).innerHTML;
form.querySelectorAll('.add-entry-btn-f').forEach(function(btn) {
btn.addEventListener('click', function() {
var gi = parseInt(this.dataset.group);
var fieldset = this.closest('fieldset');
var entryCount = fieldset.querySelectorAll('.apropos-entry').length;
var html = entryTpl.replaceAll('{{gi}}', gi).replaceAll('{{ei}}', entryCount);
this.insertAdjacentHTML('beforebegin', html);
});
});
form.querySelector('.add-group-btn-f').addEventListener('click', function() {
groupCount++;
var html = groupTpl.replaceAll('{{gi}}', groupCount);
this.insertAdjacentHTML('beforebegin', html);
var newGroup = this.previousElementSibling;
if (newGroup && newGroup.classList.contains('apropos-group')) {
var btn = newGroup.querySelector('.add-entry-btn-f');
if (btn) {
btn.dataset.group = groupCount;
btn.addEventListener('click', function() {
var gi = parseInt(this.dataset.group);
var fieldset = this.closest('fieldset');
var entryCount = fieldset.querySelectorAll('.apropos-entry').length;
var html = entryTpl.replaceAll('{{gi}}', gi).replaceAll('{{ei}}', entryCount);
this.insertAdjacentHTML('beforebegin', html);
});
}
}
});
})();
</script>

View File

@@ -1,7 +1,34 @@
<main id="main-content"> <main id="main-content">
<h1>Éditer : <?= htmlspecialchars($editTitle) ?></h1> <h1>Éditer : <?= htmlspecialchars($editTitle) ?></h1>
<?php if ($editType === 'page'): ?> <?php if ($editType === 'about_page'): ?>
<!-- ── Markdown content ──────────────────────────────────────────────── -->
<h2>Contenu de la page</h2>
<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="about">
<label for="editor">Contenu (Markdown) :</label>
<input type="hidden" id="content" name="content"
value="<?= htmlspecialchars($initialContent) ?>">
<div id="editor"></div>
<div class="admin-form-footer">
<button type="submit" class="btn btn--primary">Enregistrer</button>
<a href="/admin/contenus.php" class="btn btn--secondary admin-cancel-link">Annuler</a>
</div>
</form>
<!-- ── Contacts ──────────────────────────────────────────────────────── -->
<h2 style="margin-top:3rem;">Contacts</h2>
<?php
$aproposKey = 'contacts';
$groups = $aboutContacts ?? [];
include APP_ROOT . '/templates/admin/apropos-groups-form.php';
?>
<?php elseif ($editType === 'page' && $pageSlug !== 'about'): ?>
<form action="/admin/actions/page.php" method="post" class="admin-form"> <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="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
<input type="hidden" name="slug" value="<?= htmlspecialchars($pageSlug) ?>"> <input type="hidden" name="slug" value="<?= htmlspecialchars($pageSlug) ?>">
@@ -38,125 +65,6 @@
<?php <?php
$groups = is_array($value) ? $value : []; $groups = is_array($value) ? $value : [];
?> ?>
<form action="/admin/actions/apropos.php" method="post" class="admin-form" id="apropos-form"> <?php include APP_ROOT . '/templates/admin/apropos-groups-form.php'; ?>
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
<input type="hidden" name="apropos_key" value="<?= htmlspecialchars($aproposKey) ?>">
<?php foreach ($groups as $gi => $group): ?>
<fieldset class="apropos-group">
<legend><?= htmlspecialchars($aproposKey === 'contacts' ? 'Contact' : 'Crédit') ?> <?= $gi + 1 ?></legend>
<?php if ($aproposKey === 'contacts'): ?>
<label for="group_<?= $gi ?>_role">Rôle :</label>
<input type="text" id="group_<?= $gi ?>_role"
name="groups[<?= $gi ?>][role]"
value="<?= htmlspecialchars($group['role'] ?? '') ?>">
<?php else: ?>
<label for="group_<?= $gi ?>_label">Label :</label>
<input type="text" id="group_<?= $gi ?>_label"
name="groups[<?= $gi ?>][label]"
value="<?= htmlspecialchars($group['label'] ?? '') ?>">
<?php endif; ?>
<?php $entries = is_array($group['entries'] ?? null) ? $group['entries'] : []; ?>
<?php foreach ($entries as $ei => $entry): ?>
<div class="apropos-entry">
<label for="entry_<?= $gi ?>_<?= $ei ?>_text"><?= $aproposKey === 'contacts' ? 'Nom' : 'Texte' ?> :</label>
<input type="text" id="entry_<?= $gi ?>_<?= $ei ?>_text"
name="groups[<?= $gi ?>][entries][<?= $ei ?>][text]"
value="<?= htmlspecialchars($entry['text'] ?? '') ?>">
<?php if ($aproposKey === 'contacts'): ?>
<label for="entry_<?= $gi ?>_<?= $ei ?>_email">Email :</label>
<input type="email" id="entry_<?= $gi ?>_<?= $ei ?>_email"
name="groups[<?= $gi ?>][entries][<?= $ei ?>][email]"
value="<?= htmlspecialchars($entry['email'] ?? '') ?>">
<?php endif; ?>
<label for="entry_<?= $gi ?>_<?= $ei ?>_url">Lien (optionnel) :</label>
<input type="url" id="entry_<?= $gi ?>_<?= $ei ?>_url"
name="groups[<?= $gi ?>][entries][<?= $ei ?>][url]"
value="<?= htmlspecialchars($entry['url'] ?? '') ?>">
</div>
<?php endforeach; ?>
<button type="button" class="btn btn--primary btn--sm add-entry-btn" data-group="<?= $gi ?>">+ Ajouter une entrée</button>
</fieldset>
<?php endforeach; ?>
<button type="button" class="btn btn--primary" id="add-group-btn">+ Ajouter un <?= $aproposKey === 'contacts' ? 'contact' : 'groupe de crédit' ?></button>
<div class="admin-form-footer">
<button type="submit" class="btn btn--primary">Enregistrer</button>
<a href="/admin/contenus.php" class="btn btn--secondary admin-cancel-link">Annuler</a>
</div>
<template id="entry-template-<?= $aproposKey ?>">
<div class="apropos-entry">
<label>Entrée :</label>
<input type="text" name="groups[{{gi}}][entries][{{ei}}][text]">
<?php if ($aproposKey === 'contacts'): ?>
<label>Email :</label>
<input type="email" name="groups[{{gi}}][entries][{{ei}}][email]">
<?php endif; ?>
<label>Lien (optionnel) :</label>
<input type="url" name="groups[{{gi}}][entries][{{ei}}][url]">
</div>
</template>
<template id="group-template-<?= $aproposKey ?>">
<fieldset class="apropos-group">
<legend><?= htmlspecialchars($aproposKey === 'contacts' ? 'Contact' : 'Crédit') ?> {{gi}}</legend>
<?php if ($aproposKey === 'contacts'): ?>
<label>Rôle :</label>
<input type="text" name="groups[{{gi}}][role]">
<?php else: ?>
<label>Label :</label>
<input type="text" name="groups[{{gi}}][label]">
<?php endif; ?>
<button type="button" class="btn btn--primary btn--sm add-entry-btn" data-group="{{gi}}">+ Ajouter une entrée</button>
</fieldset>
</template>
</form>
<script>
(function() {
const aproposKey = '<?= $aproposKey ?>';
let groupCount = <?= count($groups) ?>;
const entryTpl = document.getElementById('entry-template-' + aproposKey).innerHTML;
const groupTpl = document.getElementById('group-template-' + aproposKey).innerHTML;
// Add entry to a group
document.querySelectorAll('.add-entry-btn').forEach(btn => {
btn.addEventListener('click', function() {
const gi = parseInt(this.dataset.group);
const fieldset = this.closest('fieldset');
const entryCount = fieldset.querySelectorAll('.apropos-entry').length;
const html = entryTpl.replaceAll('{{gi}}', gi).replaceAll('{{ei}}', entryCount);
this.insertAdjacentHTML('beforebegin', html);
});
});
// Add new group
document.getElementById('add-group-btn').addEventListener('click', function() {
groupCount++;
const html = groupTpl.replaceAll('{{gi}}', groupCount);
this.insertAdjacentHTML('beforebegin', html);
// Re-bind add-entry buttons for the new group
const newGroup = this.previousElementSibling;
if (newGroup && newGroup.classList.contains('apropos-group')) {
const btn = newGroup.querySelector('.add-entry-btn');
if (btn) {
btn.dataset.group = groupCount;
btn.addEventListener('click', function() {
const gi = parseInt(this.dataset.group);
const fieldset = this.closest('fieldset');
const entryCount = fieldset.querySelectorAll('.apropos-entry').length;
const html = entryTpl.replaceAll('{{gi}}', gi).replaceAll('{{ei}}', entryCount);
this.insertAdjacentHTML('beforebegin', html);
});
}
}
});
})();
</script>
<?php endif; ?> <?php endif; ?>
</main> </main>

View File

@@ -37,38 +37,6 @@
</tbody> </tbody>
</table> </table>
<h2 style="margin-top:2rem;">À propos</h2>
<table>
<thead>
<tr>
<th scope="col">Clé</th>
<th scope="col">Type</th>
<th scope="col">Mis à jour</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($aproposKeys as $a): ?>
<?php
$typeLabel = match($a['key']) {
'contacts' => 'Contacts',
'credits' => 'Crédits',
};
?>
<tr>
<td><code><?= htmlspecialchars($a['key']) ?></code></td>
<td><?= htmlspecialchars($typeLabel) ?></td>
<td><?= htmlspecialchars($a['updated_at'] ?? '—') ?></td>
<td>
<a href="/admin/contenus-edit.php?apropos=<?= urlencode($a['key']) ?>"
class="btn btn--primary btn--sm">Éditer</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- ═══════════════════════════════════════════════════════════════════ <!-- ═══════════════════════════════════════════════════════════════════
Blocs d'aide du formulaire étudiant·e Blocs d'aide du formulaire étudiant·e
═══════════════════════════════════════════════════════════════════ --> ═══════════════════════════════════════════════════════════════════ -->

View File

@@ -33,15 +33,18 @@ function renderEntries(array $entries): string {
<?php if (!empty($contacts)): ?> <?php if (!empty($contacts)): ?>
<li><a href="#apropos-contacts">Contacts</a></li> <li><a href="#apropos-contacts">Contacts</a></li>
<?php endif; ?> <?php endif; ?>
<?php if (!empty($credits)): ?>
<li><a href="#apropos-credits">Crédits</a></li> <li><a href="#apropos-credits">Crédits</a></li>
<?php endif; ?>
</ul> </ul>
<div class="apropos-toc-erg"> <div class="apropos-toc-erg">
<a href="https://erg.be" target="_blank" rel="noopener"> <a href="https://erg.be" target="_blank" rel="noopener">
Site de l'erg ↗ Site de l'erg ↗
</a> </a>
</div> </div>
<div class="apropos-toc-source">
<a href="https://git.erg.school/PostERG/xamxam" target="_blank" rel="noopener">
Code source ↗
</a>
</div>
</nav> </nav>
<!-- MIDDLE: main prose + sections --> <!-- MIDDLE: main prose + sections -->
@@ -77,20 +80,27 @@ function renderEntries(array $entries): string {
</section> </section>
<?php endif; ?> <?php endif; ?>
<?php if (!empty($credits)): ?> <!-- Credits section (hardcoded) -->
<!-- Credits section -->
<section class="apropos-section" id="apropos-credits"> <section class="apropos-section" id="apropos-credits">
<h2 class="apropos-section-title">Crédits</h2> <h2 class="apropos-section-title">Crédits</h2>
<dl class="apropos-credits-list"> <dl class="apropos-credits-list">
<?php foreach ($credits as $group): ?>
<div class="apropos-credit-row"> <div class="apropos-credit-row">
<dt><?= htmlspecialchars($group['label']) ?></dt> <dt>Design & développement</dt>
<dd><?= renderEntries($group['entries'] ?? []) ?></dd> <dd>
<span class="apropos-entry">Olivia Marly</span>,
<span class="apropos-entry">Théophile Gerveau-Mercie</span> &
<span class="apropos-entry">Théo Hennequin</span>
</dd>
</div>
<div class="apropos-credit-row">
<dt>Typographies</dt>
<dd>
<span class="apropos-entry">Ductus (Amélie Dumont)</span> &
<span class="apropos-entry">BBB DM Sans</span>
</dd>
</div> </div>
<?php endforeach; ?>
</dl> </dl>
</section> </section>
<?php endif; ?>
</div> </div>