style: normalize headers, overtype editor rounded corners, remove duplicate cover preview, thesis-add-header grid layout, subtitle below header with top gradient

This commit is contained in:
Pontoporeia
2026-05-08 19:24:24 +02:00
parent 7ccadbb224
commit 21c2b55bfb
23 changed files with 1855 additions and 1531 deletions

137
TODO.md
View File

@@ -1,129 +1,12 @@
# XAMXAM TODO
## Completed
- [x] PeerTube integration — two parallel systems (backup direct upload + PeerTube API)
- [x] `PeerTubeService.php` — credentials CRUD + OAuth2 password grant + multipart upload to `/api/v1/videos/upload`
- [x] Migration `021_peertube_settings.sql``peertube_settings` table (singleton) + `peertube_upload_enabled` feature flag (default 0 = disabled)
- [x] `actions/settings.php``peertube` section handler (toggle + credential save)
- [x] `admin/parametres.php` — PeerTube section UI (instance URL, username, password, channel ID, privacy)
- [x] `templates/admin/parametres.php` — PeerTube settings form between SMTP and admin account sections
- [x] `admin/partage/fichiers-fragment.php` — shows `<input type="file">` for video/audio when enabled, keeps TODO notice when disabled
- [x] `ThesisCreateController``handlePeerTubeUpload()` uploads video/audio to PeerTube, stores watch URL as `thesis_files` row
- [x] `ThesisEditController` — same `handlePeerTubeUpload()` method for edit workflow
- [x] `templates/public/tfe.php` — renders PeerTube iframe embed for files whose path contains `/videos/watch/`
- [x] `AdminLogger``logPeerTubeUpdate()` audit method
- [x] Direct file upload fallback: when `peertube_upload_enabled = 0`, standard `<input type="file">` + local storage works unchanged
- [x] Backoffice fieldset reorder — Note contextuelle merged in, Lien BAIU added, removed from Métadonnées
- [x] Backoffice order: Note contextuelle → Points du jury → Remarques → Lien BAIU → Exemplaire BAIU → Exemplaire ERG → Contact interne
- [x] Removed standalone "Note contextuelle" fieldset (now inside Backoffice)
- [x] Lien BAIU moved from Métadonnées complémentaires into Backoffice
- [x] Métadonnées fieldset now: pages, minutes, annexes only
- [x] Form fixes batch
- [x] bentopdf link clearer: "PDFs trop lourds ? https://bentopdf.com/" (full URL visible)
- [x] Multiple promoteurices: interne and ULB fields now dynamic (add/remove rows, same as lecteurs)
- [x] Contact visibility duplication removed from admin forms (`showContact = false`; `mail` field in fieldset-tfe-info covers it)
- [x] Asterisk corrections in files section: note_intention, website URL, video, audio all show red asterisk + `required` when non-admin
- [x] ULB promoteurice asterisk + required when finality=Approfondi (JS toggles `<span class="asterisk">*</span>` + `required` on first ULB input)
- [x] Controllers handle `jury_promoteur` and `jury_promoteur_ulb_name` as both scalar and array (backwards compat)
- [x] Fix `just serve` — justfile shebang recipes (`deploy-env`, `reencrypt-password`) used space indentation instead of tabs, causing "extra leading whitespace" parse error
- [x] PDF 100 MB limit + bentopdf mention
- [x] `ThesisCreateController`: `MAX_PDF_SIZE = 100 MB`; PDFs checked against it, other files still 500 MB
- [x] `ThesisEditController`: same per-PDF limit applied
- [x] `fichiers-fragment.php`: note d'intention and TFE hints mention 100 MB PDF limit + bentopdf.com link
- [x] `form.php` edit-mode new-files hint updated
- [x] `file-field.php`: added `$hintRaw` flag to allow HTML in hints
- [x] Format types: reorder, rename, add Image/Écriture
- [x] Migration 019: add Écriture
- [x] Migration 020: add `sort_order` column, rename Autre → Etc. / Autre, add Image, set display order (Écriture · Image · Audio · Vidéo · Site web · Performance · Objet éditorial · Installation · Etc. / Autre)
- [x] `Database.php` format_types query uses `ORDER BY sort_order, id`
- [x] `fichiers-fragment.php` uses `ORDER BY sort_order, id`; Image/Vidéo/Audio IDs resolved via name map
- [x] TODO: Vidéo + Audio — PeerTube API upload (notice shown in form for now)
- [x] Combined Format + Fichiers into HTMX-swappable block
- [x] `partage/fichiers-fragment.php` — new combined fragment: format checkboxes + fichiers fieldset that adapts based on selected formats (upload inputs / URL fields / both)
- [x] Route `/partage/fichiers-fragment` added to `partage/index.php`
- [x] `admin/fichiers-fragment.php` — admin-gated wrapper for the same fragment (sets `admin_mode=1`)
- [x] `admin/format-website-fragment.php` — admin-gated fragment for edit-mode website URL fieldset toggle
- [x] `form.php` — add/partage mode: replaced separate Format + Fichiers + website-url-fieldset with single `#format-fichiers-block` server-rendered via shared fragment
- [x] `form.php` — edit mode: Format checkboxes wire to `admin/format-website-fragment.php``#edit-website-url-fieldset` (existing-file management untouched)
- [x] `checkbox-list.php` — added `$hxInclude` variable (defaults to `'this, #website-url-fieldset'`) so callers can customise included fields
- [x] TDD analysis + new test suites
- [x] **Bug fixed**: `SearchController::handleSearch()``$coverMap` undefined variable + never populated for search results
- [x] `ShareLinkTest` (13 tests) — `generateSlug`, all `validateLink` branches, `verifyPassword`, `incrementUsage`, `objet_restriction`
- [x] `PureLogicTest` (31 tests) — `TfeController` helpers (meta, OG image, jury split, captions), `ThesisCreateController` helpers (autofocus, detectFileType, authorSlug), `ThesisEditController::buildFileSizeInfo`, `ExportController` CSV column consistency, `SearchController` coverMap regression
- [x] Private helpers promoted to `protected` in `TfeController`, `ThesisCreateController`, `ThesisEditController` to enable subclass-based testing without reflection
- [x] Form save audit + TDD
- [x] `createThesis()` missing `duration_pages`/`duration_minutes` columns — fixed
- [x] `ThesisCreateController` not passing raw page/minute values to `createThesis()` — fixed (`durationPages`, `durationMinutes` extracted and passed)
- [x] `FormSaveTest.php` — 14 red-green tests covering create+edit round-trips for all fields
- [x] Language form improvements
- [x] Add Néerlandais as default language option (schema + migration 017)
- [x] `language_autre` conditionally required via HTMX fragment (replaced custom JS)
- [x] `language_autre` saved via `getOrCreateLanguage()` in both create and edit controllers
- [x] `formData['languages']` wired in edit.php so checkboxes are pre-checked
- [x] `duration_pages`/`duration_minutes` saved in `updateThesis()` and read back in `getThesisRawFields()`
- [x] `beforeunload-guard` applied to add and partage forms too
- [x] Audit + fix direct PHP URL references blocked by nginx catch-all `deny all`
- [x] `/request-access.php` fetch in `tfe.php``/request-access`
- [x] `/media.php?path=` in `form.php` (×2) and `admin/recapitulatif.php``/media?path=`
- [x] Fix 403 on `/language-autre-fragment.php` from `edit.php`
- [x] Root cause: standalone root-level PHP file blocked by nginx catch-all `deny all`
- [x] Moved logic to `partage/language-autre-fragment.php` (shared include)
- [x] Added route `/partage/language-autre-fragment` in `partage/index.php`
- [x] Added `admin/language-autre-fragment.php` (AdminAuth gated, includes shared logic)
- [x] `form.php` picks URL based on `$mode` (`partage` vs admin)
- [x] Deleted `public/language-autre-fragment.php`; nginx unchanged
- [x] Merge banner images into cover images
- [x] Migration 016: copy `storage/banners/*``storage/covers/`, insert `thesis_files` cover records, clear `banner_path`, remove banners dir
- [x] Remove banner fieldset from edit form (`form.php`)
- [x] Remove banner fieldset from student submission form (`fieldset-files.php`: rename to couverture)
- [x] Update `ThesisEditController::save()` — remove banner upload/removal logic
- [x] Update `ThesisCreateController::submit()` — remove `handleBannerUpload` call
- [x] Update `Database::handleCoverUpload()` — add webp support, raise limit to 20 MB
- [x] Remove `Database::setBannerPath()`, `handleBannerUpload()`, `getThesisBannerPath()`
- [x] Update `Database::deleteThesis()` / `bulkDeleteTheses()` — remove banner file cleanup
- [x] `HomeController`: batch-load covers for all items, remove banner_path fallback
- [x] `SearchController::handleSearch()`: batch-load covers, pass `$coverMap` to view
- [x] `SearchController::handleStudentPreview()`: load covers, pass `$coverMap` to partial
- [x] `TfeController::resolveOgImage()`: use cover file_type instead of banner_path
- [x] `home.php`: use only `$coverMap` (no banner_path fallback)
- [x] `search.php`: show cover thumbnail on result cards
- [x] `student-preview.php`: use `$coverMap` instead of `banner_path`
- [x] Migration applied and file moved to `applied/`
- [x] Remove `required` from all form inputs in admin add/edit
- [x] Introduced `$adminMode` flag in `form.php` (true when `$mode` is `'add'` or `'edit'`)
- [x] Hidden "champs obligatoires" note in admin mode
- [x] All `$required = true` callers in `form.php`, `fieldset-tfe-info.php`, `fieldset-academic.php`, `fieldset-licence-explanation.php`, `fieldset-files.php` changed to `!$adminMode`
- [x] Hardcoded `required` HTML attributes in `fieldset-tfe-info.php` (synopsis, objet radios), `fieldset-licence-explanation.php` (access type radios), `jury-fieldset.php` (promoteur, lecteurs interne/externe) gated on `!$adminMode`
- [x] Dynamic JS `ulbInput.required` in jury fieldset also gated
# TODO
- [x] Make all heading font sizes the same (slightly smaller than current h1) in common.css
- [x] Remove individual font-size overrides from other CSS files so they inherit
- [x] Standardise header nav structure: admin uses nav-left/nav-right like public
- [x] Unify font-size for all nav links (logo + nav links all use var(--step--1))
- [x] Clean up redundant CSS rules (.nav-logo, .nav-left-links)
- [x] Update admin.css selectors to match new header structure
- [x] Bump nav font-size to var(--step-0)
- [x] Add small inverted top gradient to admin body
- [x] Commit
- [x] Cap home page cards grid to max 3 columns (was auto-fill, now repeat(3, 1fr) with 2→1 column breakpoints)
- [x] Remove Modifier link from admin header when on edit page
- [x] Move admin nav links to right side, keep only logo on left
- [x] Remove Mots-clés from admin header, add as button in dashboard toolbar; use grid layout (title|stats, search|buttons)
- [x] Group admin toolbar buttons: + Ajouter + Mots-clés stacked above Import/Export
- [x] Stack admin filters vertically: search+button row above dropdowns row
- [x] Standardise form inputs/selects/textareas in common.css: padding, --radius var, 2px accent border on focus
- [x] Add fixed top gradient on partage main element, mirror of bottom gradient
- [x] Remove bottom border on `.thesis-add-header`
- [x] Add subtitle "Formulaire pour XAMXAM" below partage header, with Ductus link to /
- [x] Switch `.thesis-add-header` to grid layout
- [ ] Create `admin/operation.php` — unified add/edit page
- [ ] Wire up route: `?id=` → edit mode, no id → add mode
- [ ] Update all references: `add.php``operation.php`, `edit.php``operation.php?id=`
- [ ] Keep old `add.php` and `edit.php` as redirect stubs
- [ ] Keep action endpoints (`actions/formulaire.php`, `actions/edit.php`) unchanged
- [ ] Test both flows

1
app/.env Normal file
View File

@@ -0,0 +1 @@
APP_KEY=M+4xc/c9/b4H+ScjO4cU82FzcZRO+xp5pzYNvJapUOQ=

View File

@@ -0,0 +1,16 @@
-- Add name (human-readable title) and enabled (show/hide toggle) columns
-- to form_help_blocks. Drop sort_order — blocks are now fixed-position,
-- one per fieldset, no reordering.
ALTER TABLE form_help_blocks ADD COLUMN name TEXT NOT NULL DEFAULT '';
ALTER TABLE form_help_blocks ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
-- Backfill names from the FORM_HELP_LABELS mapping.
UPDATE form_help_blocks SET name = 'Introduction' WHERE key = 'partage_intro';
UPDATE form_help_blocks SET name = 'Informations du TFE' WHERE key = 'fieldset_tfe_info';
UPDATE form_help_blocks SET name = 'Note Synopsis' WHERE key = 'fieldset_synopsis';
UPDATE form_help_blocks SET name = 'Composition du jury' WHERE key = 'fieldset_jury';
UPDATE form_help_blocks SET name = 'Cadre académique' WHERE key = 'fieldset_academic';
UPDATE form_help_blocks SET name = 'Fichiers' WHERE key = 'fieldset_files';
UPDATE form_help_blocks SET name = 'Visibilité / Accès' WHERE key = 'fieldset_access';
UPDATE form_help_blocks SET name = 'E-mail de confirmation' WHERE key = 'fieldset_email';

View File

@@ -0,0 +1,8 @@
-- Add missing form help block keys for all student-form fieldsets.
-- fieldset_synopsis is already seeded but injected inside Informations du TFE via $synopsisExtra.
-- fieldset_academic already exists but was never wired in form.php for partage mode.
INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled) VALUES
('fieldset_languages', 'Langue(s)', '', 1),
('fieldset_keywords', 'Mots-clés', '', 1),
('fieldset_metadata', 'Métadonnées complémentaires', '', 1);

View File

@@ -1,47 +1,11 @@
<?php
/**
* HTMX handler: persist the new drag-and-drop sort order for form help blocks.
*
* Expects POST fields:
* csrf_token — standard admin CSRF token
* block[] — ordered list of block keys (one hidden input per block, submitted by
* Sortable+htmx via the form's `end` event trigger)
*
* Returns a 204 No Content on success (htmx will not swap anything).
* On error, returns a 400 with a plain-text message.
* Legacy endpoint — no longer used (blocks are now static, non-sortable).
* Returns 204 No Content for backwards compatibility.
*/
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'])) {
http_response_code(403);
echo 'Token invalide.';
exit;
}
$keys = $_POST['block'] ?? [];
if (!is_array($keys) || empty($keys)) {
http_response_code(400);
echo 'Paramètre block[] manquant.';
exit;
}
// Sanitise: keep only scalar strings, deduplicate.
$keys = array_values(array_unique(array_filter($keys, 'is_string')));
require_once APP_ROOT . '/src/Database.php';
$db = new Database();
try {
$db->reorderFormHelpBlocks($keys);
} catch (Exception $e) {
error_log('form-help-reorder error: ' . $e->getMessage());
http_response_code(500);
echo 'Erreur lors de la sauvegarde.';
exit;
}
http_response_code(204);
exit;

View File

@@ -27,7 +27,7 @@ $autofocusField = App::consumeAutofocus();
$siteSettings = Database::getInstance()->getAllSettings();
// Form help blocks
$helpBlocks = Database::getInstance()->getAllFormHelpBlocks();
$helpFn = fn(string $key) => $helpBlocks[$key]['content'] ?? '';
$helpFn = fn(string $key) => empty($helpBlocks[$key]['enabled']) ? '' : ($helpBlocks[$key]['content'] ?? '');
function withAutofocus(string $fieldName, array $attrs = []): array {
global $autofocusField;

View File

@@ -50,7 +50,7 @@ try {
} elseif ($formHelpKey) {
$editType = "form_help";
$formHelpContent = $db->getFormHelpBlock($formHelpKey);
$editTitle = Database::FORM_HELP_LABELS[$formHelpKey] ?? $formHelpKey;
$editTitle = $formHelpKey;
} else {
$editType = "apropos";
$value = $db->getAproposContent($aproposKey);

View File

@@ -19,7 +19,7 @@ $autofocusField = App::consumeAutofocus();
// Form help blocks for editable généralités
$helpBlocks = Database::getInstance()->getAllFormHelpBlocks();
$helpFn = fn(string $key) => $helpBlocks[$key]['content'] ?? '';
$helpFn = fn(string $key) => empty($helpBlocks[$key]['enabled']) ? '' : ($helpBlocks[$key]['content'] ?? '');
function old($key, $default = "") {
global $formData;

View File

@@ -0,0 +1,196 @@
<?php
/**
* HTMX fragment: expand/collapse/editor for a single form help block.
*
* GET ?key=xxx → render collapsed chip: 3/4 left (name + MD preview), 1/4 right (edit + toggle dot)
* GET ?key=xxx&edit=1 → render inline OverType editor + name field
* POST → save content + name, return collapsed view
* POST _action=toggle → toggle enabled, return collapsed view
*/
require_once __DIR__ . '/../../bootstrap.php';
require_once __DIR__ . '/../../src/AdminAuth.php';
AdminAuth::requireLogin();
require_once __DIR__ . '/../../src/Database.php';
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$key = $_GET['key'] ?? '';
if (!in_array($key, Database::FORM_HELP_KEYS, true)) {
http_response_code(400);
echo 'Clé invalide.';
exit;
}
// ── POST ─────────────────────────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['_action'] ?? 'save';
$db = new Database();
// Toggle doesn't need CSRF — low-risk action behind admin auth
if ($action === 'toggle') {
$db->toggleFormHelpBlock($key);
renderCollapsed($db, $key);
exit;
}
// Save requires CSRF
if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
http_response_code(403);
echo 'Token invalide.';
exit;
}
// save
$content = $_POST['content'] ?? '';
$name = trim($_POST['name'] ?? '');
try {
$db->setFormHelpBlock($key, $content);
if ($name !== '') {
$db->setFormHelpBlockName($key, $name);
}
require_once __DIR__ . '/../../src/AdminLogger.php';
AdminLogger::make()->logFormStructureEdit($key);
} catch (Exception $e) {
error_log('form-help-inline save error: ' . $e->getMessage());
http_response_code(500);
echo 'Erreur lors de la sauvegarde.';
exit;
}
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
renderCollapsed($db, $key);
exit;
}
// ── GET ──────────────────────────────────────────────────────────────────────
$db = new Database();
$editMode = ($_GET['edit'] ?? '') === '1';
if ($editMode) {
renderEditor($db, $key);
} else {
renderCollapsed($db, $key);
}
// ═══════════════════════════════════════════════════════════════════════════════
// RENDER HELPERS
// ═══════════════════════════════════════════════════════════════════════════════
function renderCollapsed(Database $db, string $key): void
{
$blocks = $db->getAllFormHelpBlocks();
$b = $blocks[$key] ?? ['content' => '', 'name' => '', 'enabled' => 0];
$name = $b['name'] ?: $key;
$content = $b['content'] ?? '';
$enabled = (int)($b['enabled'] ?? 1);
$hasContent = trim($content) !== '';
$mdHtml = '';
if ($hasContent) {
require_once APP_ROOT . '/src/Parsedown.php';
$pd = new Parsedown();
$pd->setSafeMode(true);
$mdHtml = $pd->text($content);
}
?>
<div class="fhb-inline <?= $enabled ? '' : 'fhb-inline--disabled' ?>" data-key="<?= htmlspecialchars($key) ?>">
<!-- Left side: name + content (content hidden when disabled) -->
<div class="fhb-inline-body">
<div class="fhb-inline-name"><?= htmlspecialchars($name) ?></div>
<?php if ($enabled && $hasContent): ?>
<div class="fhb-md-preview"><?= $mdHtml ?></div>
<?php elseif ($enabled): ?>
<div class="fhb-inline-empty">— vide —</div>
<?php endif; ?>
</div>
<!-- Right side: dot above edit button (edit hidden when disabled) -->
<div class="fhb-inline-actions">
<form hx-post="/admin/form-help-inline-fragment.php?key=<?= urlencode($key) ?>"
hx-target="closest .fhb-inline"
hx-swap="outerHTML"
class="fhb-toggle-form">
<input type="hidden" name="_action" value="toggle">
<button type="submit"
class="fhb-dot <?= $enabled ? 'fhb-dot--on' : 'fhb-dot--off' ?>"
title="<?= $enabled ? 'Désactiver ce bloc' : 'Activer ce bloc' ?>"
aria-label="<?= $enabled ? 'Désactiver ce bloc' : 'Activer ce bloc' ?>"></button>
</form>
<?php if ($enabled): ?>
<button type="button" class="btn btn--primary btn--sm"
hx-get="/admin/form-help-inline-fragment.php?key=<?= urlencode($key) ?>&edit=1"
hx-target="closest .fhb-inline"
hx-swap="outerHTML">Éditer</button>
<?php endif; ?>
</div>
</div>
<?php
}
function renderEditor(Database $db, string $key): void
{
$blocks = $db->getAllFormHelpBlocks();
$b = $blocks[$key] ?? ['content' => '', 'name' => ''];
$name = $b['name'] ?: $key;
$content = $b['content'] ?? '';
?>
<div class="fhb-inline fhb-inline--editing" data-key="<?= htmlspecialchars($key) ?>">
<form hx-post="/admin/form-help-inline-fragment.php?key=<?= urlencode($key) ?>"
hx-swap="outerHTML"
hx-target="closest .fhb-inline"
class="fhb-inline-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<div class="fhb-edit-name-row">
<label for="fhb-name-<?= htmlspecialchars($key) ?>" class="fhb-edit-label">Nom du bloc :</label>
<input type="text" id="fhb-name-<?= htmlspecialchars($key) ?>" name="name"
value="<?= htmlspecialchars($name) ?>"
class="fhb-name-input"
placeholder="Nom du bloc d'aide">
</div>
<label for="fhb-ed-<?= htmlspecialchars($key) ?>" class="fhb-edit-label">Contenu (Markdown) :</label>
<input type="hidden" id="fhb-content-<?= htmlspecialchars($key) ?>" name="content"
value="<?= htmlspecialchars($content) ?>">
<div id="fhb-editor-<?= htmlspecialchars($key) ?>" class="fhb-overtype-editor"></div>
<div class="fhb-edit-buttons">
<button type="submit" class="btn btn--primary btn--sm">Enregistrer</button>
<button type="button" class="btn btn--secondary btn--sm"
hx-get="/admin/form-help-inline-fragment.php?key=<?= urlencode($key) ?>"
hx-target="closest .fhb-inline"
hx-swap="outerHTML">Annuler</button>
</div>
</form>
<script>
(function(){
var hidden = document.getElementById('fhb-content-<?= htmlspecialchars($key) ?>');
var ed = document.getElementById('fhb-editor-<?= htmlspecialchars($key) ?>');
if (hidden && ed && typeof OverType !== 'undefined') {
var OT = OverType.default || OverType;
new OT(ed, {
value: hidden.value,
minHeight: '400px',
spellcheck: false,
onChange: function(v) { hidden.value = v; }
});
} else {
ed.innerHTML = '<textarea name="content" style="width:100%;min-height:400px;font-family:monospace">'
+ hidden.value.replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</textarea>';
var ta = ed.querySelector('textarea');
ta.addEventListener('input', function() {
hidden.value = this.value;
});
}
})();
</script>
</div>
<?php
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* HTMX fragment (admin): renders the licence section with conditional required states.
*
* POST: access_type_id, license_id, license_custom, cc2r, admin_mode
*
* Admin mode: never required.
*/
require_once __DIR__ . '/../../bootstrap.php';
require_once __DIR__ . '/../../src/AdminAuth.php';
AdminAuth::requireLogin();
$accessTypeId = $_POST['access_type_id'] ?? '2';
$licenseId = $_POST['license_id'] ?? '';
$licenseCustom = $_POST['license_custom'] ?? '';
$cc2r = !empty($_POST['cc2r']);
$adminMode = true;
require_once APP_ROOT . '/src/Database.php';
$db = Database::getInstance();
$licenseTypes = $db->getAllLicenseTypes();
?>
<div class="licence-license-choice">
<?php
$name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes;
$selected = $licenseId; $placeholder = '— Sélectionner —'; $required = false;
include APP_ROOT . '/templates/partials/form/select-field.php';
?>
<?php
$name = 'license_custom'; $label = 'Ou précisez une autre licence :';
$value = htmlspecialchars($licenseCustom);
$hint = 'Ex: CC BY-NC 4.0, Tous droits réservés...';
include APP_ROOT . '/templates/partials/form/text-field.php';
?>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="cc2r" value="1"
<?= $cc2r ? 'checked' : '' ?>>
J'accepte les Conditions Collectives de Réutilisation (CC2r)
</label>
<small><a href="https://cc2r.net/" target="_blank" rel="noopener">En savoir plus sur la CC2r ↗</a></small>
</div>
</div>

View File

@@ -16,7 +16,6 @@
white-space: nowrap;
scrollbar-width: thin;
}
.admin-body header nav .nav-left,
.admin-body header nav .nav-right-links {
flex-shrink: 0;
}
@@ -1480,7 +1479,7 @@
}
/* ═══════════════════════════════════════════════════════════════════════════
Form Help Blocks — drag-and-drop builder (contenus.php)
Form Help Blocks — static structure view (contenus.php)
═══════════════════════════════════════════════════════════════════════════ */
.fhb-hint {
@@ -1489,225 +1488,250 @@
margin-bottom: var(--space-m);
}
.fhb-layout {
/* ── Structure container ───────────────────────────────────────────────────── */
.fhb-structure {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-m);
align-items: start;
grid-template-columns: 1fr;
gap: var(--space-xs);
max-width: 100%;
margin-top: var(--space-m);
}
@media (max-width: 800px) {
.fhb-layout {
grid-template-columns: 1fr;
}
}
/* ── Fieldset cards (static reference) ─────────────────────────────────────── */
/* ── Panels ─────────────────────────────────────────────────────────────── */
.fhb-sortable-panel,
.fhb-form-preview-panel {
.fhb-fieldset-card {
border: 1px solid var(--border-primary);
border-radius: var(--radius);
padding: var(--space-s);
padding: var(--space-xs) var(--space-s);
background: var(--bg-secondary);
}
.fhb-panel-title {
.fhb-fieldset-card-legend {
font-size: var(--step-0);
font-weight: 600;
margin: 0 0 var(--space-3xs) 0;
letter-spacing: 0.03em;
}
.fhb-panel-desc {
font-size: var(--step--2);
color: var(--text-secondary);
margin: 0 0 var(--space-xs) 0;
}
/* ── Saving indicator ─────────────────────────────────────────────────────── */
.fhb-saving {
display: none;
align-items: center;
gap: var(--space-2xs);
font-size: var(--step--1);
color: var(--accent-primary);
padding: var(--space-2xs) 0;
}
.fhb-saving.htmx-request {
display: flex;
}
/* ── Draggable block cards ─────────────────────────────────────────────────── */
.fhb-sortable {
display: flex;
flex-direction: column;
gap: var(--space-2xs);
padding: 0;
margin: 0;
}
.fhb-block-card {
display: flex;
align-items: center;
gap: var(--space-xs);
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-left: 4px solid var(--accent-primary);
border-radius: var(--radius);
padding: var(--space-2xs) var(--space-xs);
cursor: default;
transition: box-shadow 0.15s, border-color 0.15s;
}
.fhb-block-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border-color: var(--accent-primary);
}
.fhb-drag-handle {
font-size: 1.2em;
color: var(--text-tertiary);
cursor: grab;
flex-shrink: 0;
line-height: 1;
user-select: none;
padding: 2px 4px;
}
.fhb-drag-handle:active {
cursor: grabbing;
}
.fhb-block-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.fhb-block-label {
font-size: var(--step--1);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fhb-block-preview {
font-size: var(--step--2);
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fhb-block-empty {
font-size: var(--step--2);
color: var(--text-tertiary);
font-style: italic;
}
.fhb-edit-btn {
flex-shrink: 0;
font-size: var(--step--2) !important;
padding: 2px var(--space-xs) !important;
}
/* ── SortableJS state classes ─────────────────────────────────────────────── */
.fhb-ghost {
opacity: 0.35;
background: var(--accent-muted);
border-color: var(--accent-primary);
}
.fhb-chosen {
box-shadow: 0 4px 16px rgba(149, 87, 181, 0.25);
border-color: var(--accent-primary);
}
.fhb-dragging {
opacity: 0.9;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
/* ── Form structure preview (right panel) ─────────────────────────────────── */
.fhb-form-preview {
display: flex;
flex-direction: column;
gap: var(--space-2xs);
}
.fhb-fieldset-preview {
border: 1px solid var(--border-secondary);
border-radius: var(--radius);
padding: var(--space-xs);
background: var(--bg-primary);
}
.fhb-fieldset-legend {
font-size: var(--step--1);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--space-3xs);
padding-bottom: var(--space-3xs);
border-bottom: 1px solid var(--border-primary);
}
.fhb-fieldset-inputs {
.fhb-fieldset-card-inputs {
margin: 0;
padding: 0 0 0 var(--space-s);
list-style: disc;
list-style: none;
display: flex;
flex-wrap: wrap;
gap: var(--space-3xs) var(--space-m);
}
.fhb-fieldset-inputs li {
.fhb-fieldset-card-inputs li {
font-size: var(--step--2);
color: var(--text-secondary);
line-height: 1.6;
}
.fhb-anchor {
display: flex;
align-items: center;
gap: var(--space-2xs);
border-radius: var(--radius);
padding: var(--space-3xs) var(--space-xs);
font-size: var(--step--2);
border: 1px dashed var(--border-primary);
background: transparent;
}
.fhb-anchor--filled {
border-color: var(--accent-primary);
background: var(--accent-muted);
color: var(--accent-secondary);
}
.fhb-anchor--empty {
.fhb-fieldset-card-inputs li::before {
content: '· ';
color: var(--text-tertiary);
}
.fhb-anchor-icon {
flex-shrink: 0;
font-style: normal;
/* ── Help block wrapper ────────────────────────────────────────────────────── */
.fhb-block-wrapper {
/* container for the fhb-inline, one per help block */
}
.fhb-anchor-label {
flex: 1;
}
/* ── Inline help block (collapsed state) ───────────────────────────────────── */
.fhb-anchor-pos {
font-size: var(--step--2);
font-weight: 600;
color: var(--accent-primary);
background: var(--accent-muted);
.fhb-inline {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--space-s);
align-items: center;
border: 1px solid var(--border-primary);
border-left: 4px solid var(--accent-primary);
border-radius: var(--radius);
padding: 0 4px;
padding: var(--space-xs) var(--space-s);
background: var(--bg-primary);
transition: opacity 0.15s;
}
.fhb-inline:hover {
opacity: 0.85;
}
/* Disabled state — one line only, content hidden */
.fhb-inline--disabled {
border-left-color: var(--text-tertiary);
opacity: 0.55;
}
.fhb-inline--disabled:hover {
opacity: 0.75;
}
.fhb-inline--disabled .fhb-md-preview,
.fhb-inline--disabled .fhb-inline-empty {
display: none;
}
/* Editing state */
.fhb-inline--editing {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
border-color: var(--accent-primary);
box-shadow: 0 4px 16px rgba(149, 87, 181, 0.15);
padding: var(--space-s);
min-height: 50vh;
}
.fhb-inline--editing .fhb-inline-form {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto auto 1fr auto;
height: 100%;
}
/* ── Left side: name + content ─────────────────────────────────────────────── */
.fhb-inline-body {
min-width: 0;
}
.fhb-inline-name {
font-size: var(--step--1);
font-weight: 600;
color: var(--accent-secondary);
margin-bottom: var(--space-3xs);
}
/* ── Small rendered Markdown preview ──────────────────────────────────────── */
.fhb-md-preview {
font-size: var(--step--2);
color: var(--text-secondary);
line-height: 1.45;
max-height: 6em;
overflow: hidden;
}
.fhb-md-preview p {
margin: 0;
}
.fhb-md-preview p + p {
margin-top: var(--space-3xs);
}
.fhb-md-preview ul,
.fhb-md-preview ol {
margin: var(--space-3xs) 0;
padding-left: var(--space-s);
}
.fhb-md-preview li {
margin-bottom: 0;
}
.fhb-md-preview strong { font-weight: 600; }
.fhb-md-preview em { font-style: italic; }
.fhb-md-preview code {
font-size: 0.9em;
background: var(--bg-secondary);
padding: 0 var(--space-4xs);
border-radius: 3px;
}
.fhb-inline-empty {
font-size: var(--step--2);
color: var(--text-tertiary);
font-style: italic;
}
/* ── Right side: edit button + live dot ───────────────────────────────────── */
.fhb-inline-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--space-2xs);
flex-shrink: 0;
}
.fhb-toggle-form {
margin: 0;
line-height: 0;
}
/* Live dot — green when on, red when off */
.fhb-dot {
display: block;
width: 18px;
height: 18px;
border-radius: 50%;
border: none;
cursor: pointer;
padding: 0;
transition: opacity 0.15s;
}
.fhb-dot:hover {
opacity: 0.7;
}
.fhb-dot--on {
background: #2d6a4f;
}
.fhb-dot--off {
background: #c0392b;
}
/* ── Editor form ──────────────────────────────────────────────────────────── */
.fhb-edit-name-row {
margin-bottom: var(--space-xs);
}
.fhb-edit-label {
display: block;
font-size: var(--step--2);
font-weight: 500;
color: var(--text-primary);
margin-bottom: var(--space-3xs);
}
.fhb-name-input {
width: 100%;
max-width: 400px;
padding: var(--space-2xs) var(--space-xs);
border: 1px solid var(--border-primary);
border-radius: var(--radius);
font-size: var(--step--1);
font-family: var(--font-body);
}
.fhb-name-input:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 2px var(--accent-muted);
}
.fhb-overtype-editor .--type-container {
border-radius: var(--radius);
}
.fhb-edit-buttons {
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
gap: var(--space-xs);
margin-top: var(--space-xs);
}
.fhb-inline-form {
margin-top: var(--space-xs);
}

View File

@@ -33,7 +33,9 @@ a {
}
a:hover {
text-decoration: none;
text-decoration-line: underline;
text-decoration-style: wavy;
text-decoration-thickness: 1px;
}
header {
@@ -49,13 +51,6 @@ header {
.nav-logo {
text-decoration: none;
font-size: var(--step--1);
}
.nav-left {
display: flex;
align-items: center;
gap: var(--space-l);
}
.nav-left-links,
@@ -112,7 +107,7 @@ header {
padding-bottom: 1px;
}
/* nav-top-row: transparent wrapper at desktop children become
/* nav-top-row: transparent wrapper at desktop - children become
direct flex items of nav, preserving the existing layout */
.nav-top-row {
display: contents;
@@ -125,33 +120,33 @@ header {
}
/* ============================================================
HAMBURGER MENU public nav (pure CSS, checkbox trick)
HAMBURGER MENU - public nav (pure CSS, checkbox trick)
DOM order inside <header> (public only):
input.menu-btn ← off-screen checkbox
nav
div.nav-top-row ← always-visible row (logo + burger)
div.nav-left ← logo + desktop link list
ul.nav-right-links ← desktop right links
div.nav-top-row ← always-visible row
ul.nav-left-links ← logo + Répertoire
ul.nav-right-links ← Licences, À Propos
label.menu-icon ← burger icon trigger
ul.nav-mobile-links ← full dropdown (hidden by default)
At desktop: .menu-icon and .nav-mobile-links are display:none.
.nav-top-row is display:contents so its children
participate directly in navs flex row.
participate directly in nav's flex row.
At mobile: nav becomes a flex column. .nav-top-row is a real
flex row (logo | burger). .nav-mobile-links expands
via max-height on checkbox:checked.
============================================================ */
/* Off-screen checkbox triggered by its label */
/* Off-screen checkbox - triggered by its label */
.menu-btn {
position: absolute;
top: -9999px;
left: -9999px;
}
/* Burger label takes no space at desktop */
/* Burger label - takes no space at desktop */
.menu-icon {
display: none;
cursor: pointer;
@@ -207,11 +202,19 @@ header {
padding: var(--space-s);
}
/* Hide desktop link lists inside the top row */
header nav[aria-label="Navigation principale"] .nav-left-links,
/* Hide desktop link lists inside the top row, but keep the logo visible */
header nav[aria-label="Navigation principale"] .nav-right-links {
display: none;
}
header nav[aria-label="Navigation principale"] .nav-left-links {
display: flex;
gap: 0;
}
header nav[aria-label="Navigation principale"]
.nav-left-links
li:not(:first-child) {
display: none;
}
/* Reveal the hamburger icon */
.menu-icon {
@@ -286,7 +289,7 @@ main * {
}
/* ============================================================
HEADINGS global scale, shared by admin + public pages
HEADINGS - global scale, shared by admin + public pages
All headings use the same font size (slightly smaller than
the previous h1). Individual page overrides for size have
been removed so everything inherits from here.
@@ -385,6 +388,8 @@ main * {
:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--accent-primary);
border-radius: var(--radius);
padding: var(--space-3xs) var(--space-xs);
}
/* Respect user motion preferences */
@@ -398,7 +403,7 @@ main * {
}
/* ============================================================
FORM ELEMENTS base input / select / textarea / button
FORM ELEMENTS - base input / select / textarea / button
============================================================ */
label {
@@ -406,7 +411,11 @@ label {
margin-bottom: var(--space-3xs);
}
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="color"]),
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not(
[type="hidden"]
):not([type="submit"]):not([type="button"]):not([type="reset"]):not(
[type="color"]
),
select,
textarea {
font-family: inherit;
@@ -419,7 +428,11 @@ textarea {
transition: border-color 0.15s;
}
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="color"]):focus,
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not(
[type="hidden"]
):not([type="submit"]):not([type="button"]):not([type="reset"]):not(
[type="color"]
):focus,
select:focus,
textarea:focus {
outline: none;
@@ -449,7 +462,7 @@ select {
}
/* ============================================================
BUTTONS shared .btn base class
BUTTONS - shared .btn base class
Targets both <a> and <button>, always has a background.
border-radius: var(--radius); padding: var(--space-xs)
============================================================ */
@@ -469,11 +482,7 @@ select {
text-decoration: none;
line-height: 1.3;
border: none;
transition:
background 0.15s,
opacity 0.15s,
box-shadow 0.15s,
filter 0.15s;
transition: background 0.15s, opacity 0.15s, box-shadow 0.15s, filter 0.15s;
width: fit-content;
}
@@ -619,14 +628,14 @@ select {
}
/* ============================================================
SEMANTIC HTML ELEMENTS baseline styling shared everywhere
SEMANTIC HTML ELEMENTS - baseline styling shared everywhere
============================================================ */
fieldset {
/*background: var(--bg-secondary);*/
border: 1px solid var(--border-primary);
border-radius: var(--radius);
padding: var(--space-m);
padding: 0 var(--space-m) var(--space-m) var(--space-m);
margin: 0;
}

View File

@@ -11,7 +11,7 @@
gap: 0;
}
.admin-form > div:not(.admin-form-footer) {
.admin-form > div:not(.admin-form-footer):not(.student-help-block) {
display: grid;
grid-template-columns: 260px 1fr;
align-items: start;
@@ -82,7 +82,11 @@
.admin-form
div:has(
input:required:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"])) > label,
input:required:not([type="checkbox"]):not([type="radio"]):not(
[type="file"]
):not([type="hidden"])
)
> label,
.admin-form div:has(select:required) > label,
.admin-form div:has(textarea:required) > label {
font-weight: 600;
@@ -243,12 +247,11 @@
/* ── Add / edit page header ─────────────────────────────────────────────── */
.thesis-add-header {
display: flex;
display: grid;
grid-template-columns: 1fr auto;
align-items: baseline;
justify-content: space-between;
gap: var(--space-m);
margin-bottom: var(--space-m);
border-bottom: 1px solid var(--border-primary);
gap: var(--space-2xs) var(--space-m);
margin-bottom: var(--space-xs);
padding-bottom: var(--space-xs);
}
@@ -256,6 +259,19 @@
margin: 0;
}
.thesis-add-subtitle {
grid-column: 1 / -1;
margin: 0;
font-family: var(--font-body);
font-size: var(--step--1);
color: var(--text-secondary);
}
.thesis-add-subtitle a {
font-family: var(--font-display);
color: var(--accent-primary);
}
.mode-toggle {
font-size: var(--step--1);
color: var(--text-secondary);
@@ -314,8 +330,8 @@
}
.flash-warning {
background: var(--warning-muted-bg, rgba(251,202,81,.12));
border-color: var(--warning-muted-border, rgba(251,202,81,.35));
background: var(--warning-muted-bg, rgba(251, 202, 81, 0.12));
border-color: var(--warning-muted-border, rgba(251, 202, 81, 0.35));
color: var(--text-primary);
white-space: pre-line;
}
@@ -393,6 +409,24 @@
.student-body main {
padding: var(--space-l) var(--space-l) var(--space-2xl);
width: 100%;
position: relative;
}
/* Fixed top gradient, mirror of body's bottom gradient */
.student-body main::before {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
height: 8vh;
background: linear-gradient(
180deg,
rgba(149, 87, 181, 1) 0%,
rgba(0, 0, 0, 0) 100%
);
pointer-events: none;
z-index: 1;
}
/* ── Share-link error page ──────────────────────────────────────────────── */
@@ -900,28 +934,40 @@ a.recap-file-name:hover {
/* deprecated alias for .btn--primary .btn--lg; kept for backward-compat */
}
/* ── Form help blocks ────────────────────────────────────────────────────── */
.form-help-block {
.student-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);
padding: var(--space-m);
margin: var(--space-s) 0;
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; }
.student-help-block > *:first-child {
margin-top: 0;
}
.student-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); }
.student-help-block p {
margin: 0 0 var(--space-xs);
}
.student-help-block ul,
.student-help-block ol {
margin: 0 0 var(--space-xs);
padding-left: var(--space-m);
}
.student-help-block li {
margin-bottom: var(--space-3xs);
}
.student-help-block a {
color: var(--accent-primary);
}
/* ── E-mail retry page ───────────────────────────────────────────────────── */

File diff suppressed because one or more lines are too long

View File

@@ -50,6 +50,7 @@ $selectedFormats = isset($_POST['formats']) && is_array($_POST['formats'])
: [];
$adminMode = ($_POST['admin_mode'] ?? '0') === '1';
$editMode = ($_POST['edit_mode'] ?? '0') === '1';
$hasSiteWeb = $siteWebId && in_array($siteWebId, $selectedFormats, true);
$hasVideo = $videoId && in_array($videoId, $selectedFormats, true);
@@ -70,6 +71,10 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
?>
<div id="format-fichiers-block">
<input type="hidden" name="admin_mode" value="<?= $adminMode ? '1' : '0' ?>">
<input type="hidden" name="edit_mode" value="<?= $editMode ? '1' : '0' ?>">
<?php if ($editMode && ($_POST['_cover'] ?? null)): ?>
<input type="hidden" name="_cover" value="<?= htmlspecialchars($_POST['_cover']) ?>">
<?php endif; ?>
<!-- ═══════════════════ Format(s) ═══════════════════ -->
<fieldset>
@@ -81,7 +86,7 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
hx-post="<?= htmlspecialchars($hxPost) ?>"
hx-target="#format-fichiers-block"
hx-trigger="change"
hx-include="this, [name='website_url'], [name='website_label'], [name='admin_mode']"
hx-include="this, [name='website_url'], [name='website_label'], [name='admin_mode'], [name='edit_mode'], [name='_cover']"
hx-swap="outerHTML">
<legend class="sr-only">Format(s) du TFE</legend>
<ul>
@@ -108,13 +113,26 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
<!-- ── 1. Couverture (always) ── -->
<div>
<?php
$_cover = $_POST['_cover'] ?? null;
if ($editMode && $_cover): ?>
<div class="admin-banner-preview">
<img src="/media?path=<?= urlencode($_cover) ?>"
alt="Couverture actuelle" style="max-height:180px;">
<label class="admin-checkbox-label">
<input type="checkbox" name="remove_cover" value="1"> Supprimer la couverture
</label>
</div>
<?php endif;
$name = 'couverture';
$label = 'Image de couverture (optionnel) :';
$accept = 'image/jpeg,image/png,image/webp';
$hint = 'JPG, PNG ou WEBP. Format 4:3 recommandé. Max 20 MB.';
$hint = ($editMode && $_cover)
? 'Laisser vide pour conserver la couverture actuelle. JPG, PNG ou WEBP. Max 20 MB.'
: 'JPG, PNG ou WEBP. Format 4:3 recommandé. Max 20 MB.';
$required = false;
$id = 'couverture';
include APP_ROOT . '/templates/partials/form/file-field.php';
unset($_cover);
?>
</div>
@@ -162,7 +180,7 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
hx-post="<?= htmlspecialchars($hxPost) ?>"
hx-target="#format-fichiers-block"
hx-trigger="change"
hx-include="[name='formats[]'], [name='website_url'], [name='website_label'], [name='admin_mode'], [name='has_annexes']"
hx-include="[name='formats[]'], [name='website_url'], [name='website_label'], [name='admin_mode'], [name='edit_mode'], [name='_cover'], [name='has_annexes']"
hx-swap="outerHTML">
Ce TFE comporte des annexes
</label>

View File

@@ -42,6 +42,13 @@ if ($slug === 'fichiers-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') {
exit;
}
// Special route: /partage/licence-fragment (HTMX fragment — licence section with conditional required)
if ($slug === 'licence-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') {
App::boot();
require_once __DIR__ . '/licence-fragment.php';
exit;
}
// Special route: /partage/recapitulatif?id=N
if ($slug === 'recapitulatif' || $slug === 'recapitulatif.php') {
App::boot();
@@ -267,7 +274,7 @@ function renderShareLinkForm(string $slug, array $link): void
// Load all form help blocks in one query.
$helpBlocks = Database::getInstance()->getAllFormHelpBlocks();
$helpFn = fn(string $key) => $helpBlocks[$key]['content'] ?? '';
$helpFn = fn(string $key) => empty($helpBlocks[$key]['enabled']) ? '' : ($helpBlocks[$key]['content'] ?? '');
// ── Shared form variables ──────────────────────────────────────────────
$mode = 'partage';
@@ -356,6 +363,7 @@ function renderShareLinkForm(string $slug, array $link): void
<?php if ($isVerified): ?>
<span class="share-badge">🔓 Accès partagé</span>
<?php endif; ?>
<p class="thesis-add-subtitle">Formulaire pour <a href="/">XAMXAM</a></p>
</div>
<?php include APP_ROOT . '/templates/partials/form/form.php'; ?>

View File

@@ -0,0 +1,47 @@
<?php
/**
* HTMX fragment: renders the licence section with conditional required states.
*
* POST: access_type_id, license_id, license_custom, cc2r, admin_mode
*
* When access_type_id=1 (Libre): licence fields are required.
* When access_type_id=2|3 (Interne/Interdit): licence fields are optional.
*/
require_once __DIR__ . '/../../bootstrap.php';
$accessTypeId = $_POST['access_type_id'] ?? '2';
$licenseId = $_POST['license_id'] ?? '';
$licenseCustom = $_POST['license_custom'] ?? '';
$cc2r = !empty($_POST['cc2r']);
$adminMode = ($_POST['admin_mode'] ?? '0') === '1';
$required = !$adminMode && $accessTypeId === '1';
require_once APP_ROOT . '/src/Database.php';
$db = Database::getInstance();
$licenseTypes = $db->getAllLicenseTypes();
?>
<div class="licence-license-choice">
<?php
$name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes;
$selected = $licenseId; $placeholder = '— Sélectionner —';
include APP_ROOT . '/templates/partials/form/select-field.php';
?>
<?php
$name = 'license_custom'; $label = 'Ou précisez une autre licence :';
$value = htmlspecialchars($licenseCustom);
$hint = 'Ex: CC BY-NC 4.0, Tous droits réservés...';
include APP_ROOT . '/templates/partials/form/text-field.php';
?>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="cc2r" value="1"
<?= $cc2r ? 'checked' : '' ?>>
J'accepte les Conditions Collectives de Réutilisation (CC2r)
</label>
<small><a href="https://cc2r.net/" target="_blank" rel="noopener">En savoir plus sur la CC2r ↗</a></small>
</div>
</div>

View File

@@ -2209,27 +2209,16 @@ class Database
'partage_intro',
'fieldset_tfe_info',
'fieldset_synopsis',
'fieldset_jury',
'fieldset_languages',
'fieldset_keywords',
'fieldset_academic',
'fieldset_jury',
'fieldset_files',
'fieldset_metadata',
'fieldset_access',
'fieldset_email',
];
/**
* Human-readable labels for each block key (used in the admin UI).
*/
public const FORM_HELP_LABELS = [
'partage_intro' => 'Introduction du formulaire',
'fieldset_tfe_info' => 'Informations du TFE — note d\'introduction',
'fieldset_synopsis' => 'Synopsis — explication',
'fieldset_jury' => 'Composition du jury — note',
'fieldset_academic' => 'Cadre académique — note',
'fieldset_files' => 'Fichiers — note',
'fieldset_access' => 'Visibilité / Accès — explication',
'fieldset_email' => 'E-mail de confirmation — note',
];
/**
* Get a single form help block by key. Returns '' when missing.
*/
@@ -2244,7 +2233,7 @@ class Database
}
/**
* Upsert a form help block.
* Upsert a form help block (content only).
*/
public function setFormHelpBlock(string $key, string $content): void
{
@@ -2260,41 +2249,57 @@ class Database
}
/**
* Return all form help blocks ordered by sort_order, as [ key => ['content' => ..., 'updated_at' => ..., 'sort_order' => ...] ].
* Update a form help block's name.
*/
public function setFormHelpBlockName(string $key, string $name): void
{
if (!in_array($key, self::FORM_HELP_KEYS, true)) {
throw new Exception("Unknown form help block key: $key");
}
$this->pdo->prepare(
'UPDATE form_help_blocks SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?'
)->execute([$name, $key]);
}
/**
* Toggle a form help block's enabled state. Returns the new state (0 or 1).
*/
public function toggleFormHelpBlock(string $key): int
{
if (!in_array($key, self::FORM_HELP_KEYS, true)) {
throw new Exception("Unknown form help block key: $key");
}
$this->pdo->prepare(
'UPDATE form_help_blocks SET enabled = CASE enabled WHEN 1 THEN 0 ELSE 1 END,
updated_at = CURRENT_TIMESTAMP
WHERE key = ?'
)->execute([$key]);
$stmt = $this->pdo->prepare('SELECT enabled FROM form_help_blocks WHERE key = ?');
$stmt->execute([$key]);
return (int)$stmt->fetchColumn();
}
/**
* Return all form help blocks, keyed by their key.
*/
public function getAllFormHelpBlocks(): array
{
$stmt = $this->pdo->query(
'SELECT key, content, updated_at, sort_order FROM form_help_blocks ORDER BY sort_order, key'
'SELECT key, name, content, enabled, updated_at FROM form_help_blocks'
);
$rows = $stmt->fetchAll();
$out = [];
foreach ($rows as $r) {
$out[$r['key']] = [
'name' => $r['name'],
'content' => $r['content'],
'enabled' => (int)$r['enabled'],
'updated_at' => $r['updated_at'],
'sort_order' => (int)$r['sort_order'],
];
}
return $out;
}
/**
* Persist a new sort order for all form help blocks.
* $keys must be an ordered array of known block keys.
*/
public function reorderFormHelpBlocks(array $keys): void
{
$stmt = $this->pdo->prepare(
'UPDATE form_help_blocks SET sort_order = ? WHERE key = ?'
);
foreach ($keys as $i => $key) {
if (in_array($key, self::FORM_HELP_KEYS, true)) {
$stmt->execute([$i, $key]);
}
}
}
// ========================================================================
// SINGLETON PATTERN ENFORCEMENT
// ========================================================================

View File

@@ -38,157 +38,97 @@
</table>
<!-- ═══════════════════════════════════════════════════════════════════
Blocs d'aide du formulaire étudiant·e
Structure du formulaire étudiant·e
═══════════════════════════════════════════════════════════════════ -->
<h2 id="form-help-blocks" style="margin-top:2rem;">Blocs d'aide du formulaire étudiant·e</h2>
<p>Ces textes apparaissent dans le formulaire de soumission accessible via les liens de partage.
Ils permettent d'expliquer aux étudiant·es comment remplir chaque section. Supporte le Markdown.</p>
<h2 id="form-help-blocks" style="margin-top:2rem;">Structure du formulaire étudiant·e</h2>
<p class="fhb-hint">
<strong>Glissez</strong> les blocs d'aide (cartes violettes) pour les réorganiser dans le formulaire.
Cliquez sur <strong>Éditer</strong> pour modifier le contenu d'un bloc.
L'ordre est sauvegardé automatiquement après chaque déplacement.
Chaque <strong>bloc d'aide</strong> s'affiche au-dessus de sa section dans le formulaire de soumission.
Le <strong>bouton rond</strong> active/désactive l'affichage.
</p>
<?php
// Build an ordered flat list of all blocks for the sortable form.
// $formHelpBlocks is keyed by block key, already sorted by sort_order.
$orderedBlocks = [];
foreach ($formHelpBlocks as $key => $block) {
$orderedBlocks[] = array_merge($block, [
'key' => $key,
'label' => Database::FORM_HELP_LABELS[$key] ?? $key,
]);
}
$blocks = $formHelpBlocks;
// Static form structure: each item is either a 'fieldset' (visual container)
// or an 'anchor' for a specific block key showing where it sits in the form.
// We also need a mapping from block key → where it currently sits in the sorted list.
// The entire sorted order is what matters; we render the form structure as a visual
// reference alongside the sortable list.
$formStructure = [
['type' => 'anchor', 'key' => 'partage_intro', 'position' => 'before-form', 'label' => 'Avant le formulaire (introduction)'],
['type' => 'fieldset', 'name' => 'Informations du TFE', 'inputs' => ['Titre', 'Sous-titre', 'Auteur·ice(s)', 'Contact', 'Synopsis']],
['type' => 'anchor', 'key' => 'fieldset_tfe_info', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Informations du TFE »'],
['type' => 'anchor', 'key' => 'fieldset_synopsis', 'position' => 'intro-fieldset', 'label' => 'Note sous le champ Synopsis'],
['type' => 'fieldset', 'name' => 'Composition du jury', 'inputs' => ['Président·e', 'Promoteur·ice', 'Lecteur·ices (×4)']],
['type' => 'anchor', 'key' => 'fieldset_jury', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Jury »'],
['type' => 'fieldset', 'name' => 'Cadre académique', 'inputs' => ['Année', 'Orientation', 'AP', 'Finalité', 'Langues', 'Formats', 'Mots-clés']],
['type' => 'anchor', 'key' => 'fieldset_academic', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Cadre académique »'],
['type' => 'fieldset', 'name' => 'Fichiers', 'inputs' => ['Fichier principal (PDF)', 'Annexes']],
['type' => 'anchor', 'key' => 'fieldset_files', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Fichiers »'],
['type' => 'fieldset', 'name' => 'Visibilité / Accès', 'inputs' => ["Type d'accès", 'Licence']],
['type' => 'anchor', 'key' => 'fieldset_access', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Visibilité »'],
['type' => 'fieldset', 'name' => 'E-mail de confirmation', 'inputs' => ['Adresse e-mail']],
['type' => 'anchor', 'key' => 'fieldset_email', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « E-mail »'],
// ── Student form structure each help block above its fieldset ───────────
// Pairs: [help_key, fieldset_name, fieldset_inputs]
$pairs = [
// Top of form
['partage_intro', null, null],
// Informations du TFE
['fieldset_tfe_info', 'Informations du TFE',
['Titre', 'Sous-titre', 'Auteur·ice(s)', 'Contact visible', 'Synopsis']],
// Langue(s)
['fieldset_languages', 'Langue(s)',
['Langues du TFE (cases à cocher)', 'Autre(s) langue(s)']],
// Mots-clés
['fieldset_keywords', 'Mots-clés',
['Mots-clés (max 10), séparés par des virgules']],
// Cadre académique
['fieldset_academic', 'Cadre académique',
['Année', 'Orientation', 'AP', 'Finalité']],
// Composition du jury
['fieldset_jury', 'Composition du jury',
['Président·e', 'Promoteur·ice(s)', 'Lecteur·ices']],
// Format(s) + Fichiers
['fieldset_files', 'Format(s) + Fichiers',
['Formats (PDF, vidéo, audio, site web…)', 'Couverture', 'Note d\'intention', 'Fichier principal', 'Annexes']],
// Métadonnées complémentaires
['fieldset_metadata', 'Métadonnées complémentaires',
['Nombre de pages', 'Durée (minutes)']],
// Degrés d'ouverture et licences
['fieldset_access', 'Degrés d\'ouverture et licences',
['Généralités', 'Degré (libre/interne/interdit)', 'Licence', 'CC2r']],
// E-mail de confirmation
['fieldset_email', 'E-mail de confirmation',
['Adresse e-mail']],
];
?>
<div class="fhb-layout">
<!-- Left: sortable ordered list of help blocks -->
<div class="fhb-sortable-panel">
<h3 class="fhb-panel-title">Ordre des blocs</h3>
<p class="fhb-panel-desc">Glissez pour réorganiser</p>
<form class="fhb-sortable sortable"
hx-post="/admin/actions/form-help-reorder.php"
hx-trigger="end"
hx-include="this"
hx-swap="none">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<div class="htmx-indicator fhb-saving">Sauvegarde…</div>
<?php foreach ($orderedBlocks as $b): ?>
<div class="fhb-block-card" data-key="<?= htmlspecialchars($b['key']) ?>">
<input type="hidden" name="block[]" value="<?= htmlspecialchars($b['key']) ?>">
<span class="fhb-drag-handle" aria-hidden="true">⠿</span>
<div class="fhb-block-info">
<span class="fhb-block-label"><?= htmlspecialchars($b['label']) ?></span>
<?php if (trim($b['content']) !== ''): ?>
<span class="fhb-block-preview"><?= htmlspecialchars(strlen($preview = trim($b['content'])) > 60 ? substr($preview, 0, 60) . '…' : $preview) ?></span>
<?php else: ?>
<span class="fhb-block-empty">— vide —</span>
<?php endif; ?>
</div>
<a href="/admin/contenus-edit.php?form_block=<?= urlencode($b['key']) ?>"
class="btn btn--primary btn--sm fhb-edit-btn">Éditer</a>
</div>
<?php endforeach; ?>
</form>
</div>
<!-- Right: static form structure reference -->
<div class="fhb-form-preview-panel">
<h3 class="fhb-panel-title">Structure du formulaire</h3>
<p class="fhb-panel-desc">Référence visuelle — non modifiable</p>
<div class="fhb-form-preview">
<?php foreach ($formStructure as $item): ?>
<?php if ($item['type'] === 'anchor'): ?>
<?php
$bData = $formHelpBlocks[$item['key']] ?? ['content' => '', 'sort_order' => 99];
$hasContent = trim($bData['content'] ?? '') !== '';
<div class="fhb-structure">
<?php foreach ($pairs as [$helpKey, $fieldsetName, $inputs]):
// Help block
$b = $blocks[$helpKey] ?? ['content' => '', 'name' => '', 'enabled' => 0];
$title = $b['name'] ?: ($fieldsetName ?? $helpKey);
?>
<div class="fhb-anchor <?= $hasContent ? 'fhb-anchor--filled' : 'fhb-anchor--empty' ?>">
<span class="fhb-anchor-icon"><?= $hasContent ? '✎' : '○' ?></span>
<span class="fhb-anchor-label"><?= htmlspecialchars(Database::FORM_HELP_LABELS[$item['key']] ?? $item['key']) ?></span>
<?php if ($hasContent): ?>
<span class="fhb-anchor-pos">#<?= (int)$bData['sort_order'] + 1 ?></span>
<?php endif; ?>
<div class="fhb-block-wrapper" data-key="<?= htmlspecialchars($helpKey) ?>">
<div class="fhb-inline"
hx-get="/admin/form-help-inline-fragment.php?key=<?= urlencode($helpKey) ?>"
hx-trigger="load"
hx-swap="outerHTML">
<div class="fhb-inline-name"><?= htmlspecialchars($title) ?></div>
</div>
<?php elseif ($item['type'] === 'fieldset'): ?>
<div class="fhb-fieldset-preview">
<div class="fhb-fieldset-legend"><?= htmlspecialchars($item['name']) ?></div>
<ul class="fhb-fieldset-inputs">
<?php foreach ($item['inputs'] as $inp): ?>
</div>
<?php if ($fieldsetName !== null): ?>
<div class="fhb-fieldset-card">
<div class="fhb-fieldset-card-legend"><?= htmlspecialchars($fieldsetName) ?></div>
<?php if ($inputs): ?>
<ul class="fhb-fieldset-card-inputs">
<?php foreach ($inputs as $inp): ?>
<li><?= htmlspecialchars($inp) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
</div><!-- /.fhb-layout -->
</main>
<script>
(function () {
var script = document.createElement('script');
script.src = '<?= App::assetV('/assets/js/sortable.min.js') ?>';
script.onload = initSortable;
document.head.appendChild(script);
function initSortable() {
htmx.onLoad(function (content) {
var sortables = content.querySelectorAll('.sortable');
for (var i = 0; i < sortables.length; i++) {
(function (sortable) {
var sortableInstance = new Sortable(sortable, {
animation: 150,
handle: '.fhb-drag-handle',
ghostClass: 'fhb-ghost',
chosenClass: 'fhb-chosen',
dragClass: 'fhb-dragging',
filter: '.htmx-indicator',
onMove: function (evt) {
return evt.related.className.indexOf('htmx-indicator') === -1;
},
onEnd: function () {
this.option('disabled', true);
}
});
sortable.addEventListener('htmx:afterRequest', function () {
sortableInstance.option('disabled', false);
});
})(sortables[i]);
}
});
}
var otScript = document.createElement('script');
otScript.src = '<?= App::assetV('/assets/js/overtype.min.js') ?>';
document.head.appendChild(otScript);
})();
</script>

View File

@@ -12,11 +12,11 @@ $_thesisId = $_GET['id'] ?? null;
<?php if ($_isAdmin): ?>
<nav aria-label="Navigation admin">
<div class="nav-left">
<a href="/" target="_blank" rel="noopener noreferrer" class="nav-logo">
<ul class="nav-left-links">
<li><a href="/" target="_blank" rel="noopener noreferrer" class="nav-logo">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M208,72H128V32a8,8,0,0,0-13.66-5.66l-96,96a8,8,0,0,0,0,11.32l96,96A8,8,0,0,0,128,224V184h80a16,16,0,0,0,16-16V88A16,16,0,0,0,208,72Zm0,96H120a8,8,0,0,0-8,8v28.69L35.31,128,112,51.31V80a8,8,0,0,0,8,8h88Z"></path></svg>XAMXAM<span class="sr-only"> (site public, nouvel onglet)</span>
</a>
</div>
</a></li>
</ul>
<ul class="nav-right-links">
<li><a href="/admin/" <?= $_currentPage === 'index.php' ? 'aria-current="page"' : '' ?>>Liste des TFE</a></li>
<li><a href="/admin/contenus.php" <?= in_array($_currentPage, ['contenus.php', 'contenus-edit.php']) ? 'aria-current="page"' : '' ?>>Contenus</a></li>
@@ -41,15 +41,13 @@ $_thesisId = $_GET['id'] ?? null;
<input class="menu-btn" type="checkbox" id="menu-btn" name="menu-btn" />
<nav aria-label="Navigation principale">
<div class="nav-top-row">
<div class="nav-left">
<a href="/" class="nav-logo">Xamxam</a>
<ul class="nav-left-links">
<li><a href="/" class="nav-logo">Xamxam</a></li>
<li>
<a href="/repertoire"
<?= ($_navCurrent === 'repertoire') ? 'aria-current="page"' : '' ?>>Répertoire</a>
</li>
</ul>
</div>
<ul class="nav-right-links">
<li>
<a href="/licence"

View File

@@ -30,32 +30,19 @@ $adminMode = $adminMode ?? false;
<fieldset class="licence-explanation">
<legend>Degrés d'ouverture et licences</legend>
<!-- Généralités -->
<div class="licence-generalites">
<h3>Généralités</h3>
<?php if ($generalitiesHtml): ?>
<div class="form-help-editable"><?= $generalitiesHtml ?></div>
<?php else: ?>
<ul>
<li>L'auteur·ice peut décider entre trois degrés de partage de son travail : <strong>libre</strong>, <strong>interne</strong>, <strong>interdit</strong>.</li>
<li>L'auteur·ice peut, à tout moment, décider de <strong>restreindre</strong> le degré d'accès à son travail. Il ne peut néanmoins pas l'ouvrir davantage.</li>
<li>Le choix effectué dans ce formulaire sera d'application <strong>une semaine après la soutenance orale</strong> de l'auteur·ice. Celui-ci peut donc décider de restreindre ce choix avant sa publication (mais pas l'ouvrir).</li>
<li>L'erg se réserve le droit de restreindre le degré d'ouverture du TFE ce en accord avec le règlement.</li>
<li>Dans tous les cas, l'auteur·ice garde les droits d'auteurs, de diffusion, d'utilisation, etc. de son travail sauf si la licence choisie restreindrait ses droits.</li>
<li>La diffusion « xamxam » est indépendante de la diffusion à la BAIU.</li>
</ul>
<?php endif; ?>
</div>
<!-- Degré d'ouverture -->
<div class="licence-choice">
<h3>J'autorise l'erg à archiver mon TFE de la manière suivante :</h3>
<p class="licence-prompt">J'autorise l'erg à archiver mon TFE de la manière suivante :</p>
<?php $selectedAccess = $formData['access_type_id'] ?? (string)$defaultAccessTypeId; ?>
<?php if ($libreEnabled): ?>
<div class="licence-degree">
<label class="admin-checkbox-label">
<input type="radio" name="access_type_id" value="1"
hx-post="<?= $adminMode ? '/admin/licence-fragment.php' : '/partage/licence-fragment' ?>"
hx-target=".licence-license-choice"
hx-swap="outerHTML"
hx-include="closest fieldset"
<?= $selectedAccess === '1' ? 'checked' : '' ?> <?= $adminMode ? '' : 'required' ?>>
<strong>🔓 Libre</strong> — Mon TFE est en libre accès à tout le monde sur la plateforme des TFE ainsi que dans la bibliothèque de l'erg.
</label>
@@ -66,6 +53,10 @@ $adminMode = $adminMode ?? false;
<div class="licence-degree">
<label class="admin-checkbox-label">
<input type="radio" name="access_type_id" value="2"
hx-post="<?= $adminMode ? '/admin/licence-fragment.php' : '/partage/licence-fragment' ?>"
hx-target=".licence-license-choice"
hx-swap="outerHTML"
hx-include="closest fieldset"
<?= $selectedAccess === '2' ? 'checked' : '' ?> <?= $adminMode ? '' : 'required' ?>>
<strong>🔒 Interne</strong> — Mon TFE n'est accessible que sur place en physique. Une note descriptive est disponible sur le site.
</label>
@@ -76,6 +67,10 @@ $adminMode = $adminMode ?? false;
<div class="licence-degree">
<label class="admin-checkbox-label">
<input type="radio" name="access_type_id" value="3"
hx-post="<?= $adminMode ? '/admin/licence-fragment.php' : '/partage/licence-fragment' ?>"
hx-target=".licence-license-choice"
hx-swap="outerHTML"
hx-include="closest fieldset"
<?= $selectedAccess === '3' ? 'checked' : '' ?> <?= $adminMode ? '' : 'required' ?>>
<strong>🚫 Interdit</strong> — Mon TFE n'est pas disponible en physique ni sur le site. Une note descriptive est disponible sur le site.
</label>
@@ -84,29 +79,11 @@ $adminMode = $adminMode ?? false;
<p class="licence-note"><em>L'auteur·ice peut, à tout moment, décider de restreindre son propre choix. Iel ne peut par contre pas l'ouvrir.</em></p>
</div>
<!-- Licence -->
<div class="licence-license-choice">
<h3>Licence du TFE</h3>
<?php
$name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes;
$selected = $formData['license_id'] ?? ''; $placeholder = '— Sélectionner —'; $required = !$adminMode;
include APP_ROOT . '/templates/partials/form/select-field.php';
?>
<?php
$name = 'license_custom'; $label = 'Ou précisez une autre licence :';
$value = htmlspecialchars($formData['license_custom'] ?? '');
$hint = 'Ex: CC BY-NC 4.0, Tous droits réservés...';
include APP_ROOT . '/templates/partials/form/text-field.php';
?>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="cc2r" value="1"
<?= !empty($formData['cc2r']) ? 'checked' : '' ?>>
J'accepte les Conditions Collectives de Réutilisation (CC2r)
</label>
<small><a href="https://cc2r.net/" target="_blank" rel="noopener">En savoir plus sur la CC2r ↗</a></small>
</div>
<!-- Licence — swapped via htmx when radio changes -->
<div class="licence-license-choice"
hx-post="<?= $adminMode ? '/admin/licence-fragment.php' : '/partage/licence-fragment' ?>"
hx-trigger="load"
hx-include="closest fieldset"
hx-swap="outerHTML">
</div>
</fieldset>

View File

@@ -4,6 +4,7 @@
*
* Variables consumed:
* string $helpContent — raw Markdown string from the DB (may be empty).
* string $helpKey — block key, used as element id.
*
* Outputs nothing when $helpContent is empty or whitespace-only.
* Parsedown must already be autoloaded (it is, via bootstrap → APP_ROOT/src/).
@@ -19,7 +20,7 @@ $pd = new Parsedown();
$pd->setSafeMode(true);
$html = $pd->text($helpContent);
?>
<div class="form-help-block">
<div class="student-help-block" id="help-<?= htmlspecialchars($helpKey ?? 'unknown') ?>">
<?= $html ?>
</div>
<?php

View File

@@ -102,6 +102,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<?php if ($showIntroHelp && isset($helpFn)): ?>
<?php
$helpContent = $helpFn("partage_intro");
$helpKey = 'partage_intro';
include APP_ROOT . "/templates/partials/form/form-help-block.php";
?>
<?php endif; ?>
@@ -146,6 +147,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<?php
if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_tfe_info");
$helpKey = 'fieldset_tfe_info';
include APP_ROOT . "/templates/partials/form/form-help-block.php";
}
include APP_ROOT . "/templates/partials/form/fieldset-tfe-info.php";
@@ -170,6 +172,13 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<?php endif; ?>
<!-- ═══════════════════ Langue(s) ═══════════════════ -->
<?php
if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_languages");
$helpKey = 'fieldset_languages';
include APP_ROOT . "/templates/partials/form/form-help-block.php";
}
?>
<fieldset id="languages-fieldset">
<legend>Langue(s)</legend>
<?php
@@ -205,6 +214,13 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
</fieldset>
<!-- ═══════════════════ Mots-clés ═══════════════════ -->
<?php
if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_keywords");
$helpKey = 'fieldset_keywords';
include APP_ROOT . "/templates/partials/form/form-help-block.php";
}
?>
<fieldset>
<legend>Mots-clés</legend>
<?php
@@ -219,13 +235,20 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
</fieldset>
<!-- ═══════════════════ Cadre académique ═══════════════════ -->
<?php include APP_ROOT .
<?php
if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_academic");
$helpKey = 'fieldset_academic';
include APP_ROOT . "/templates/partials/form/form-help-block.php";
}
include APP_ROOT .
"/templates/partials/form/fieldset-academic.php"; ?>
<!-- ═══════════════════ Composition du jury ═══════════════════ -->
<?php
if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_jury");
$helpKey = 'fieldset_jury';
include APP_ROOT . "/templates/partials/form/form-help-block.php";
}
require APP_ROOT . "/templates/partials/form/jury-fieldset.php";
@@ -238,6 +261,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
// Synthesise POST-like data so fichiers-fragment.php can render the initial state.
if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_files");
$helpKey = 'fieldset_files';
include APP_ROOT . "/templates/partials/form/form-help-block.php";
}
// Temporarily populate $_POST so the fragment can read formats/website/annexes values.
@@ -260,6 +284,8 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
$_POST['website_url'] = $existingWebsiteUrl;
$_POST['website_label'] = $existingWebsiteLabel;
$_POST['admin_mode'] = $adminMode ? '1' : '0';
$_POST['edit_mode'] = '1';
$_POST['_cover'] = $currentCover['file_path'] ?? null;
$_POST['has_annexes'] = $formData['has_annexes'] ?? null;
include APP_ROOT . '/public/partage/fichiers-fragment.php';
$_POST = $_savedPost;
@@ -268,29 +294,6 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<!-- Edit-only: existing files management -->
<div id="edit-existing-files-block">
<!-- Cover image -->
<div class="admin-form-group">
<label>Image de couverture :</label>
<div class="admin-file-input">
<?php if (!empty($currentCover)): ?>
<div class="admin-banner-preview">
<img src="/media?path=<?= urlencode(
$currentCover["file_path"],
) ?>"
alt="Couverture actuelle" style="max-height:180px;">
<label class="admin-checkbox-label">
<input type="checkbox" name="remove_cover" value="1"> Supprimer la couverture
</label>
</div>
<?php endif; ?>
<input type="file" id="couverture" name="couverture" accept="image/jpeg,image/png,image/webp" data-preview="fp-couverture">
<div id="fp-couverture" class="file-preview-list" aria-live="polite"></div>
<small><?= empty($currentCover)
? "JPG, PNG ou WEBP. Format 4:3 recommandé. Max 20 MB."
: "Laisser vide pour conserver la couverture actuelle. JPG, PNG ou WEBP. Max 20 MB." ?></small>
</div>
</div>
<!-- Existing thesis files — sortable, with labels -->
<?php $thesisFilesList = array_values(
array_filter(
@@ -398,13 +401,20 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<?php endif; ?>
<!-- ═══════════════════ Métadonnées complémentaires ═══════════════════ -->
<?php include APP_ROOT .
<?php
if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_metadata");
$helpKey = 'fieldset_metadata';
include APP_ROOT . "/templates/partials/form/form-help-block.php";
}
include APP_ROOT .
"/templates/partials/form/fieldset-metadata.php"; ?>
<!-- ═══════════════════ Degrés d'ouverture et licences ═══════════════════ -->
<?php
if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_access");
$helpKey = 'fieldset_access';
include APP_ROOT . "/templates/partials/form/form-help-block.php";
}
include APP_ROOT .
@@ -509,6 +519,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<?php if ($mode === "partage" && isset($helpFn)): ?>
<?php
$helpContent = $helpFn("fieldset_email");
$helpKey = 'fieldset_email';
include APP_ROOT . "/templates/partials/form/form-help-block.php";
?>
<?php endif; ?>