mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
add form help blocks: DB table, admin editor, live rendering in partage form
This commit is contained in:
15
TODO.md
15
TODO.md
@@ -39,6 +39,21 @@
|
|||||||
- [x] Update `root` directive in `nginx/posterg.conf`
|
- [x] Update `root` directive in `nginx/posterg.conf`
|
||||||
- [x] Update `STORAGE_ROOT` production path in `app/bootstrap.php`
|
- [x] Update `STORAGE_ROOT` production path in `app/bootstrap.php`
|
||||||
|
|
||||||
|
## Form Help Blocks (student-facing explanatory text)
|
||||||
|
|
||||||
|
- [x] Migration `004_add_form_help_blocks.sql` — `form_help_blocks` table with 8 seeded keys
|
||||||
|
- [x] `Database` methods: `getFormHelpBlock`, `setFormHelpBlock`, `getAllFormHelpBlocks`, `FORM_HELP_KEYS`, `FORM_HELP_LABELS`
|
||||||
|
- [x] `actions/form-help.php` — CSRF-validated save handler
|
||||||
|
- [x] `actions/page.php` — CSRF-validated save handler for static pages (was missing)
|
||||||
|
- [x] `contenus.php` controller — load `$formHelpBlocks`, add CSRF token
|
||||||
|
- [x] `contenus-edit.php` controller — handle `?form_block=<key>` route
|
||||||
|
- [x] `templates/admin/contenus.php` — flash messages + form help blocks table with edit links
|
||||||
|
- [x] `templates/admin/contenus-edit.php` — `form_help` edit branch with OverType Markdown editor
|
||||||
|
- [x] `templates/partials/form/form-help-block.php` — renders Markdown block via Parsedown (safe mode), silent on empty
|
||||||
|
- [x] `partage/index.php` — load all blocks once, inject at all 8 positions (replaced TODO comments)
|
||||||
|
- [x] `form.css` — `.form-help-block` styled with accent left-border
|
||||||
|
- [x] `admin.css` — `.muted` utility class
|
||||||
|
|
||||||
## Centralise Form Templates
|
## Centralise Form Templates
|
||||||
|
|
||||||
- [x] Extract shared fieldset partials: `fieldset-tfe-info.php`, `fieldset-academic.php`, `fieldset-files.php`, `fieldset-metadata.php`, `fieldset-licence-explanation.php`
|
- [x] Extract shared fieldset partials: `fieldset-tfe-info.php`, `fieldset-academic.php`, `fieldset-files.php`, `fieldset-metadata.php`, `fieldset-licence-explanation.php`
|
||||||
|
|||||||
39
app/public/admin/actions/page.php
Normal file
39
app/public/admin/actions/page.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Save handler for static page content (Markdown).
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../../../bootstrap.php';
|
||||||
|
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||||
|
AdminAuth::requireLogin();
|
||||||
|
|
||||||
|
if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||||
|
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||||
|
App::flash('error', 'Erreur de sécurité : token invalide.');
|
||||||
|
header('Location: /admin/contenus.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedSlugs = ['about', 'licenses', 'charte'];
|
||||||
|
$slug = $_POST['slug'] ?? '';
|
||||||
|
$content = $_POST['content'] ?? '';
|
||||||
|
|
||||||
|
if (!in_array($slug, $allowedSlugs, true)) {
|
||||||
|
App::flash('error', 'Slug de page invalide.');
|
||||||
|
header('Location: /admin/contenus.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once APP_ROOT . '/src/Database.php';
|
||||||
|
$db = new Database();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db->savePage($slug, $content);
|
||||||
|
App::flash('success', 'Page « ' . htmlspecialchars($slug) . ' » mise à jour.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('page save error: ' . $e->getMessage());
|
||||||
|
App::flash('error', 'Erreur lors de la sauvegarde : ' . htmlspecialchars($e->getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
header('Location: /admin/contenus.php');
|
||||||
|
exit;
|
||||||
@@ -1643,3 +1643,10 @@
|
|||||||
|
|
||||||
|
|
||||||
/* ── Form group, student mode, thanks page → see form.css ───────────────── */
|
/* ── Form group, student mode, thanks page → see form.css ───────────────── */
|
||||||
|
|
||||||
|
/* ── Utility ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|||||||
@@ -786,3 +786,26 @@ a.recap-file-name:hover {
|
|||||||
background: var(--accent-secondary);
|
background: var(--accent-secondary);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Form help blocks ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.form-help-block {
|
||||||
|
background: color-mix(in srgb, var(--accent-primary) 8%, transparent);
|
||||||
|
border-left: 3px solid var(--accent-primary);
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
padding: var(--space-s) var(--space-m);
|
||||||
|
margin-bottom: var(--space-s);
|
||||||
|
font-size: var(--step--1);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-help-block > *:first-child { margin-top: 0; }
|
||||||
|
.form-help-block > *:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.form-help-block p { margin: 0 0 var(--space-xs); }
|
||||||
|
.form-help-block ul,
|
||||||
|
.form-help-block ol { margin: 0 0 var(--space-xs); padding-left: var(--space-m); }
|
||||||
|
.form-help-block li { margin-bottom: var(--space-3xs); }
|
||||||
|
.form-help-block a { color: var(--accent-primary); }
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ function renderShareLinkForm(string $slug, array $link): void
|
|||||||
$shareOldFn = fn(string $key, string $default = '') => old($formData, $key, $default);
|
$shareOldFn = fn(string $key, string $default = '') => old($formData, $key, $default);
|
||||||
// No autofocus in the share form — identity function.
|
// No autofocus in the share form — identity function.
|
||||||
$shareWithAutofocusFn = fn(string $field, array $attrs = []) => $attrs;
|
$shareWithAutofocusFn = fn(string $field, array $attrs = []) => $attrs;
|
||||||
|
|
||||||
|
// Load all form help blocks in one query.
|
||||||
|
$helpBlocks = Database::getInstance()->getAllFormHelpBlocks();
|
||||||
|
$helpFn = fn(string $key) => $helpBlocks[$key]['content'] ?? '';
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
@@ -243,9 +247,7 @@ function renderShareLinkForm(string $slug, array $link): void
|
|||||||
<div class="flash-success" role="alert"><?= htmlspecialchars($flashSuccess) ?></div>
|
<div class="flash-success" role="alert"><?= htmlspecialchars($flashSuccess) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- TODO: Add an introductory block here explaining the purpose of this form
|
<?php $helpContent = $helpFn('partage_intro'); include APP_ROOT . '/templates/partials/form/form-help-block.php'; ?>
|
||||||
to students: what xamxam is, what happens after submission, who can see
|
|
||||||
their work, and the general timeline before publication. -->
|
|
||||||
|
|
||||||
<p class="required-note"><span class="asterisk">*</span> Champs obligatoires</p>
|
<p class="required-note"><span class="asterisk">*</span> Champs obligatoires</p>
|
||||||
<form action="/partage/<?= urlencode($slug) ?>/submit" method="post" enctype="multipart/form-data" class="admin-form">
|
<form action="/partage/<?= urlencode($slug) ?>/submit" method="post" enctype="multipart/form-data" class="admin-form">
|
||||||
@@ -253,13 +255,15 @@ function renderShareLinkForm(string $slug, array $link): void
|
|||||||
|
|
||||||
<!-- ═══════════════════ Informations du TFE ═══════════════════ -->
|
<!-- ═══════════════════ Informations du TFE ═══════════════════ -->
|
||||||
<?php
|
<?php
|
||||||
// TODO: Add a student-facing explanation block for each fieldset below,
|
|
||||||
// describing what information is expected and why it is collected.
|
|
||||||
$oldFn = $shareOldFn;
|
$oldFn = $shareOldFn;
|
||||||
$withAutofocusFn = $shareWithAutofocusFn;
|
$withAutofocusFn = $shareWithAutofocusFn;
|
||||||
// TODO: Add a contextual note for the synopsis field explaining the
|
// Inject fieldset intro note and synopsis-specific note via the partial's hook.
|
||||||
// expected length, tone, and whether it will be publicly visible.
|
ob_start();
|
||||||
$synopsisExtra = '';
|
$helpContent = $helpFn('fieldset_synopsis');
|
||||||
|
include APP_ROOT . '/templates/partials/form/form-help-block.php';
|
||||||
|
$synopsisExtra = ob_get_clean();
|
||||||
|
$helpContent = $helpFn('fieldset_tfe_info');
|
||||||
|
include APP_ROOT . '/templates/partials/form/form-help-block.php';
|
||||||
include APP_ROOT . '/templates/partials/form/fieldset-tfe-info.php';
|
include APP_ROOT . '/templates/partials/form/fieldset-tfe-info.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -279,9 +283,8 @@ function renderShareLinkForm(string $slug, array $link): void
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: Add a note explaining the jury composition to students:
|
$helpContent = $helpFn('fieldset_jury');
|
||||||
// who counts as external, what role each member plays, and
|
include APP_ROOT . '/templates/partials/form/form-help-block.php';
|
||||||
// whether this information will be publicly visible.
|
|
||||||
require APP_ROOT . '/templates/partials/form/jury-fieldset.php';
|
require APP_ROOT . '/templates/partials/form/jury-fieldset.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -289,17 +292,15 @@ function renderShareLinkForm(string $slug, array $link): void
|
|||||||
<?php
|
<?php
|
||||||
$oldFn = $shareOldFn;
|
$oldFn = $shareOldFn;
|
||||||
$withAutofocusFn = $shareWithAutofocusFn;
|
$withAutofocusFn = $shareWithAutofocusFn;
|
||||||
// TODO: Add a note for the academic context fieldset clarifying what
|
$helpContent = $helpFn('fieldset_academic');
|
||||||
// orientation/AP/finality values correspond to, and where students
|
include APP_ROOT . '/templates/partials/form/form-help-block.php';
|
||||||
// can look them up if unsure.
|
|
||||||
include APP_ROOT . '/templates/partials/form/fieldset-academic.php';
|
include APP_ROOT . '/templates/partials/form/fieldset-academic.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- ═══════════════════ Fichiers ═══════════════════ -->
|
<!-- ═══════════════════ Fichiers ═══════════════════ -->
|
||||||
<?php
|
<?php
|
||||||
// TODO: Add a note before the files fieldset explaining accepted formats,
|
$helpContent = $helpFn('fieldset_files');
|
||||||
// max sizes, what a cover image should look like, and that files
|
include APP_ROOT . '/templates/partials/form/form-help-block.php';
|
||||||
// will only be accessible according to the chosen access level.
|
|
||||||
include APP_ROOT . '/templates/partials/form/fieldset-files.php';
|
include APP_ROOT . '/templates/partials/form/fieldset-files.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -309,10 +310,8 @@ function renderShareLinkForm(string $slug, array $link): void
|
|||||||
$withAutofocusFn = $shareWithAutofocusFn;
|
$withAutofocusFn = $shareWithAutofocusFn;
|
||||||
$showDescription = false;
|
$showDescription = false;
|
||||||
$defaultAccessTypeId = 2;
|
$defaultAccessTypeId = 2;
|
||||||
// TODO: Add an explanation of each access level (Libre / Interne / Interdit)
|
$helpContent = $helpFn('fieldset_access');
|
||||||
// close to the "Visibilité / Accès" select so students understand
|
include APP_ROOT . '/templates/partials/form/form-help-block.php';
|
||||||
// the implications before choosing. Cross-reference the licence
|
|
||||||
// fieldset below.
|
|
||||||
include APP_ROOT . '/templates/partials/form/fieldset-metadata.php';
|
include APP_ROOT . '/templates/partials/form/fieldset-metadata.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -322,8 +321,7 @@ function renderShareLinkForm(string $slug, array $link): void
|
|||||||
<!-- ═══════════════════ E-mail de confirmation ═══════════ -->
|
<!-- ═══════════════════ E-mail de confirmation ═══════════ -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>E-mail de confirmation</legend>
|
<legend>E-mail de confirmation</legend>
|
||||||
<!-- TODO: Add a sentence explaining that the confirmation email is only
|
<?php $helpContent = $helpFn('fieldset_email'); include APP_ROOT . '/templates/partials/form/form-help-block.php'; ?>
|
||||||
used to send the submission recap and will not be shared publicly. -->
|
|
||||||
<?php
|
<?php
|
||||||
$name = 'confirmation_email';
|
$name = 'confirmation_email';
|
||||||
$label = 'Adresse e-mail * :';
|
$label = 'Adresse e-mail * :';
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
<h1>Contenus</h1>
|
<h1>Contenus</h1>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$flashSuccess = App::consumeFlash('success');
|
$flash = App::consumeFlash();
|
||||||
$flashError = App::consumeFlash('error');
|
|
||||||
?>
|
?>
|
||||||
<?php if ($flashSuccess): ?>
|
<?php if ($flash['success']): ?>
|
||||||
<div class="flash-success" role="alert"><?= htmlspecialchars($flashSuccess) ?></div>
|
<div class="flash-success" role="alert"><?= htmlspecialchars($flash['success']) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($flashError): ?>
|
<?php if ($flash['error']): ?>
|
||||||
<div class="flash-error" role="alert"><?= htmlspecialchars($flashError) ?></div>
|
<div class="flash-error" role="alert"><?= htmlspecialchars($flash['error']) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<h2>Pages statiques</h2>
|
<h2>Pages statiques</h2>
|
||||||
|
|||||||
26
app/templates/partials/form/form-help-block.php
Normal file
26
app/templates/partials/form/form-help-block.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Renders a single form help block as HTML.
|
||||||
|
*
|
||||||
|
* Variables consumed:
|
||||||
|
* string $helpContent — raw Markdown string from the DB (may be empty).
|
||||||
|
*
|
||||||
|
* Outputs nothing when $helpContent is empty or whitespace-only.
|
||||||
|
* Parsedown must already be autoloaded (it is, via bootstrap → APP_ROOT/src/).
|
||||||
|
*/
|
||||||
|
|
||||||
|
$helpContent = trim($helpContent ?? '');
|
||||||
|
if ($helpContent === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once APP_ROOT . '/src/Parsedown.php';
|
||||||
|
$pd = new Parsedown();
|
||||||
|
$pd->setSafeMode(true);
|
||||||
|
$html = $pd->text($helpContent);
|
||||||
|
?>
|
||||||
|
<div class="form-help-block">
|
||||||
|
<?= $html ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
unset($helpContent, $pd, $html);
|
||||||
Reference in New Issue
Block a user