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 # TODO
- [x] Make all heading font sizes the same (slightly smaller than current h1) in common.css - [x] Add fixed top gradient on partage main element, mirror of bottom gradient
- [x] Remove individual font-size overrides from other CSS files so they inherit - [x] Remove bottom border on `.thesis-add-header`
- [x] Standardise header nav structure: admin uses nav-left/nav-right like public - [x] Add subtitle "Formulaire pour XAMXAM" below partage header, with Ductus link to /
- [x] Unify font-size for all nav links (logo + nav links all use var(--step--1)) - [x] Switch `.thesis-add-header` to grid layout
- [x] Clean up redundant CSS rules (.nav-logo, .nav-left-links) - [ ] Create `admin/operation.php` — unified add/edit page
- [x] Update admin.css selectors to match new header structure - [ ] Wire up route: `?id=` → edit mode, no id → add mode
- [x] Bump nav font-size to var(--step-0) - [ ] Update all references: `add.php``operation.php`, `edit.php``operation.php?id=`
- [x] Add small inverted top gradient to admin body - [ ] Keep old `add.php` and `edit.php` as redirect stubs
- [x] Commit - [ ] Keep action endpoints (`actions/formulaire.php`, `actions/edit.php`) unchanged
- [x] Cap home page cards grid to max 3 columns (was auto-fill, now repeat(3, 1fr) with 2→1 column breakpoints) - [ ] Test both flows
- [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

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 <?php
/** /**
* HTMX handler: persist the new drag-and-drop sort order for form help blocks. * Legacy endpoint — no longer used (blocks are now static, non-sortable).
* * Returns 204 No Content for backwards compatibility.
* 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.
*/ */
require_once __DIR__ . '/../../../bootstrap.php'; require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php'; require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin(); 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); http_response_code(204);
exit; exit;

View File

@@ -27,7 +27,7 @@ $autofocusField = App::consumeAutofocus();
$siteSettings = Database::getInstance()->getAllSettings(); $siteSettings = Database::getInstance()->getAllSettings();
// Form help blocks // Form help blocks
$helpBlocks = Database::getInstance()->getAllFormHelpBlocks(); $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 { function withAutofocus(string $fieldName, array $attrs = []): array {
global $autofocusField; global $autofocusField;

View File

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

View File

@@ -19,7 +19,7 @@ $autofocusField = App::consumeAutofocus();
// Form help blocks for editable généralités // Form help blocks for editable généralités
$helpBlocks = Database::getInstance()->getAllFormHelpBlocks(); $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 = "") { function old($key, $default = "") {
global $formData; 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; white-space: nowrap;
scrollbar-width: thin; scrollbar-width: thin;
} }
.admin-body header nav .nav-left,
.admin-body header nav .nav-right-links { .admin-body header nav .nav-right-links {
flex-shrink: 0; 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 { .fhb-hint {
@@ -1489,225 +1488,250 @@
margin-bottom: var(--space-m); margin-bottom: var(--space-m);
} }
.fhb-layout { /* ── Structure container ───────────────────────────────────────────────────── */
.fhb-structure {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr;
gap: var(--space-m); gap: var(--space-xs);
align-items: start; max-width: 100%;
margin-top: var(--space-m); margin-top: var(--space-m);
} }
@media (max-width: 800px) { /* ── Fieldset cards (static reference) ─────────────────────────────────────── */
.fhb-layout {
grid-template-columns: 1fr;
}
}
/* ── Panels ─────────────────────────────────────────────────────────────── */ .fhb-fieldset-card {
.fhb-sortable-panel,
.fhb-form-preview-panel {
border: 1px solid var(--border-primary); border: 1px solid var(--border-primary);
border-radius: var(--radius); border-radius: var(--radius);
padding: var(--space-s); padding: var(--space-xs) var(--space-s);
background: var(--bg-secondary); background: var(--bg-secondary);
} }
.fhb-panel-title { .fhb-fieldset-card-legend {
font-size: var(--step-0); font-size: var(--step-0);
font-weight: 600; 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); color: var(--text-primary);
margin-bottom: var(--space-3xs); margin-bottom: var(--space-3xs);
padding-bottom: var(--space-3xs); padding-bottom: var(--space-3xs);
border-bottom: 1px solid var(--border-primary); border-bottom: 1px solid var(--border-primary);
} }
.fhb-fieldset-inputs { .fhb-fieldset-card-inputs {
margin: 0; margin: 0;
padding: 0 0 0 var(--space-s); 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); font-size: var(--step--2);
color: var(--text-secondary); color: var(--text-secondary);
line-height: 1.6; line-height: 1.6;
} }
.fhb-anchor { .fhb-fieldset-card-inputs li::before {
display: flex; content: '· ';
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 {
color: var(--text-tertiary); color: var(--text-tertiary);
} }
.fhb-anchor-icon { /* ── Help block wrapper ────────────────────────────────────────────────────── */
flex-shrink: 0;
font-style: normal; .fhb-block-wrapper {
/* container for the fhb-inline, one per help block */
} }
.fhb-anchor-label { /* ── Inline help block (collapsed state) ───────────────────────────────────── */
flex: 1;
}
.fhb-anchor-pos { .fhb-inline {
font-size: var(--step--2); display: grid;
font-weight: 600; grid-template-columns: 1fr auto;
color: var(--accent-primary); gap: var(--space-s);
background: var(--accent-muted); align-items: center;
border: 1px solid var(--border-primary);
border-left: 4px solid var(--accent-primary);
border-radius: var(--radius); 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);
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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'; $adminMode = ($_POST['admin_mode'] ?? '0') === '1';
$editMode = ($_POST['edit_mode'] ?? '0') === '1';
$hasSiteWeb = $siteWebId && in_array($siteWebId, $selectedFormats, true); $hasSiteWeb = $siteWebId && in_array($siteWebId, $selectedFormats, true);
$hasVideo = $videoId && in_array($videoId, $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"> <div id="format-fichiers-block">
<input type="hidden" name="admin_mode" value="<?= $adminMode ? '1' : '0' ?>"> <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) ═══════════════════ --> <!-- ═══════════════════ Format(s) ═══════════════════ -->
<fieldset> <fieldset>
@@ -81,7 +86,7 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
hx-post="<?= htmlspecialchars($hxPost) ?>" hx-post="<?= htmlspecialchars($hxPost) ?>"
hx-target="#format-fichiers-block" hx-target="#format-fichiers-block"
hx-trigger="change" 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"> hx-swap="outerHTML">
<legend class="sr-only">Format(s) du TFE</legend> <legend class="sr-only">Format(s) du TFE</legend>
<ul> <ul>
@@ -108,13 +113,26 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
<!-- ── 1. Couverture (always) ── --> <!-- ── 1. Couverture (always) ── -->
<div> <div>
<?php <?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'; $name = 'couverture';
$label = 'Image de couverture (optionnel) :'; $label = 'Image de couverture (optionnel) :';
$accept = 'image/jpeg,image/png,image/webp'; $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; $required = false;
$id = 'couverture'; $id = 'couverture';
include APP_ROOT . '/templates/partials/form/file-field.php'; include APP_ROOT . '/templates/partials/form/file-field.php';
unset($_cover);
?> ?>
</div> </div>
@@ -162,7 +180,7 @@ $hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragm
hx-post="<?= htmlspecialchars($hxPost) ?>" hx-post="<?= htmlspecialchars($hxPost) ?>"
hx-target="#format-fichiers-block" hx-target="#format-fichiers-block"
hx-trigger="change" 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"> hx-swap="outerHTML">
Ce TFE comporte des annexes Ce TFE comporte des annexes
</label> </label>

View File

@@ -42,6 +42,13 @@ if ($slug === 'fichiers-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') {
exit; 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 // Special route: /partage/recapitulatif?id=N
if ($slug === 'recapitulatif' || $slug === 'recapitulatif.php') { if ($slug === 'recapitulatif' || $slug === 'recapitulatif.php') {
App::boot(); App::boot();
@@ -267,7 +274,7 @@ function renderShareLinkForm(string $slug, array $link): void
// Load all form help blocks in one query. // Load all form help blocks in one query.
$helpBlocks = Database::getInstance()->getAllFormHelpBlocks(); $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 ────────────────────────────────────────────── // ── Shared form variables ──────────────────────────────────────────────
$mode = 'partage'; $mode = 'partage';
@@ -356,6 +363,7 @@ function renderShareLinkForm(string $slug, array $link): void
<?php if ($isVerified): ?> <?php if ($isVerified): ?>
<span class="share-badge">🔓 Accès partagé</span> <span class="share-badge">🔓 Accès partagé</span>
<?php endif; ?> <?php endif; ?>
<p class="thesis-add-subtitle">Formulaire pour <a href="/">XAMXAM</a></p>
</div> </div>
<?php include APP_ROOT . '/templates/partials/form/form.php'; ?> <?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', 'partage_intro',
'fieldset_tfe_info', 'fieldset_tfe_info',
'fieldset_synopsis', 'fieldset_synopsis',
'fieldset_jury', 'fieldset_languages',
'fieldset_keywords',
'fieldset_academic', 'fieldset_academic',
'fieldset_jury',
'fieldset_files', 'fieldset_files',
'fieldset_metadata',
'fieldset_access', 'fieldset_access',
'fieldset_email', '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. * 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 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 public function getAllFormHelpBlocks(): array
{ {
$stmt = $this->pdo->query( $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(); $rows = $stmt->fetchAll();
$out = []; $out = [];
foreach ($rows as $r) { foreach ($rows as $r) {
$out[$r['key']] = [ $out[$r['key']] = [
'name' => $r['name'],
'content' => $r['content'], 'content' => $r['content'],
'enabled' => (int)$r['enabled'],
'updated_at' => $r['updated_at'], 'updated_at' => $r['updated_at'],
'sort_order' => (int)$r['sort_order'],
]; ];
} }
return $out; 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 // SINGLETON PATTERN ENFORCEMENT
// ======================================================================== // ========================================================================

View File

@@ -38,157 +38,97 @@
</table> </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> <h2 id="form-help-blocks" style="margin-top:2rem;">Structure 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>
<p class="fhb-hint"> <p class="fhb-hint">
<strong>Glissez</strong> les blocs d'aide (cartes violettes) pour les réorganiser dans le formulaire. Chaque <strong>bloc d'aide</strong> s'affiche au-dessus de sa section dans le formulaire de soumission.
Cliquez sur <strong>Éditer</strong> pour modifier le contenu d'un bloc. Le <strong>bouton rond</strong> active/désactive l'affichage.
L'ordre est sauvegardé automatiquement après chaque déplacement.
</p> </p>
<?php <?php
// Build an ordered flat list of all blocks for the sortable form. $blocks = $formHelpBlocks;
// $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,
]);
}
// Static form structure: each item is either a 'fieldset' (visual container) // ── Student form structure each help block above its fieldset ───────────
// or an 'anchor' for a specific block key showing where it sits in the form. // Pairs: [help_key, fieldset_name, fieldset_inputs]
// We also need a mapping from block key → where it currently sits in the sorted list. $pairs = [
// The entire sorted order is what matters; we render the form structure as a visual // Top of form
// reference alongside the sortable list. ['partage_intro', null, null],
$formStructure = [
['type' => 'anchor', 'key' => 'partage_intro', 'position' => 'before-form', 'label' => 'Avant le formulaire (introduction)'], // Informations du TFE
['type' => 'fieldset', 'name' => 'Informations du TFE', 'inputs' => ['Titre', 'Sous-titre', 'Auteur·ice(s)', 'Contact', 'Synopsis']], ['fieldset_tfe_info', 'Informations du TFE',
['type' => 'anchor', 'key' => 'fieldset_tfe_info', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Informations du TFE »'], ['Titre', 'Sous-titre', 'Auteur·ice(s)', 'Contact visible', 'Synopsis']],
['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)']], // Langue(s)
['type' => 'anchor', 'key' => 'fieldset_jury', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Jury »'], ['fieldset_languages', 'Langue(s)',
['type' => 'fieldset', 'name' => 'Cadre académique', 'inputs' => ['Année', 'Orientation', 'AP', 'Finalité', 'Langues', 'Formats', 'Mots-clés']], ['Langues du TFE (cases à cocher)', 'Autre(s) langue(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']], // Mots-clés
['type' => 'anchor', 'key' => 'fieldset_files', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Fichiers »'], ['fieldset_keywords', 'Mots-clés',
['type' => 'fieldset', 'name' => 'Visibilité / Accès', 'inputs' => ["Type d'accès", 'Licence']], ['Mots-clés (max 10), séparés par des virgules']],
['type' => 'anchor', 'key' => 'fieldset_access', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Visibilité »'],
['type' => 'fieldset', 'name' => 'E-mail de confirmation', 'inputs' => ['Adresse e-mail']], // Cadre académique
['type' => 'anchor', 'key' => 'fieldset_email', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « E-mail »'], ['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"> <div class="fhb-structure">
<?php foreach ($pairs as [$helpKey, $fieldsetName, $inputs]):
<!-- Left: sortable ordered list of help blocks --> // Help block
<div class="fhb-sortable-panel"> $b = $blocks[$helpKey] ?? ['content' => '', 'name' => '', 'enabled' => 0];
<h3 class="fhb-panel-title">Ordre des blocs</h3> $title = $b['name'] ?: ($fieldsetName ?? $helpKey);
<p class="fhb-panel-desc">Glissez pour réorganiser</p> ?>
<div class="fhb-block-wrapper" data-key="<?= htmlspecialchars($helpKey) ?>">
<form class="fhb-sortable sortable" <div class="fhb-inline"
hx-post="/admin/actions/form-help-reorder.php" hx-get="/admin/form-help-inline-fragment.php?key=<?= urlencode($helpKey) ?>"
hx-trigger="end" hx-trigger="load"
hx-include="this" hx-swap="outerHTML">
hx-swap="none"> <div class="fhb-inline-name"><?= htmlspecialchars($title) ?></div>
<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> </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-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>
<?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): ?>
<li><?= htmlspecialchars($inp) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div> </div>
</div>
</div><!-- /.fhb-layout --> <?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>
</main> </main>
<script> <script>
(function () { (function () {
var script = document.createElement('script'); var otScript = document.createElement('script');
script.src = '<?= App::assetV('/assets/js/sortable.min.js') ?>'; otScript.src = '<?= App::assetV('/assets/js/overtype.min.js') ?>';
script.onload = initSortable; document.head.appendChild(otScript);
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]);
}
});
}
})(); })();
</script> </script>

View File

@@ -12,11 +12,11 @@ $_thesisId = $_GET['id'] ?? null;
<?php if ($_isAdmin): ?> <?php if ($_isAdmin): ?>
<nav aria-label="Navigation admin"> <nav aria-label="Navigation admin">
<div class="nav-left"> <ul class="nav-left-links">
<a href="/" target="_blank" rel="noopener noreferrer" class="nav-logo"> <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> <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> </a></li>
</div> </ul>
<ul class="nav-right-links"> <ul class="nav-right-links">
<li><a href="/admin/" <?= $_currentPage === 'index.php' ? 'aria-current="page"' : '' ?>>Liste des TFE</a></li> <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> <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" /> <input class="menu-btn" type="checkbox" id="menu-btn" name="menu-btn" />
<nav aria-label="Navigation principale"> <nav aria-label="Navigation principale">
<div class="nav-top-row"> <div class="nav-top-row">
<div class="nav-left"> <ul class="nav-left-links">
<a href="/" class="nav-logo">Xamxam</a> <li><a href="/" class="nav-logo">Xamxam</a></li>
<ul class="nav-left-links"> <li>
<li> <a href="/repertoire"
<a href="/repertoire" <?= ($_navCurrent === 'repertoire') ? 'aria-current="page"' : '' ?>>Répertoire</a>
<?= ($_navCurrent === 'repertoire') ? 'aria-current="page"' : '' ?>>Répertoire</a> </li>
</li> </ul>
</ul>
</div>
<ul class="nav-right-links"> <ul class="nav-right-links">
<li> <li>
<a href="/licence" <a href="/licence"

View File

@@ -30,32 +30,19 @@ $adminMode = $adminMode ?? false;
<fieldset class="licence-explanation"> <fieldset class="licence-explanation">
<legend>Degrés d'ouverture et licences</legend> <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 --> <!-- Degré d'ouverture -->
<div class="licence-choice"> <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 $selectedAccess = $formData['access_type_id'] ?? (string)$defaultAccessTypeId; ?>
<?php if ($libreEnabled): ?> <?php if ($libreEnabled): ?>
<div class="licence-degree"> <div class="licence-degree">
<label class="admin-checkbox-label"> <label class="admin-checkbox-label">
<input type="radio" name="access_type_id" value="1" <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' ?>> <?= $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. <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> </label>
@@ -66,6 +53,10 @@ $adminMode = $adminMode ?? false;
<div class="licence-degree"> <div class="licence-degree">
<label class="admin-checkbox-label"> <label class="admin-checkbox-label">
<input type="radio" name="access_type_id" value="2" <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' ?>> <?= $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. <strong>🔒 Interne</strong> — Mon TFE n'est accessible que sur place en physique. Une note descriptive est disponible sur le site.
</label> </label>
@@ -76,6 +67,10 @@ $adminMode = $adminMode ?? false;
<div class="licence-degree"> <div class="licence-degree">
<label class="admin-checkbox-label"> <label class="admin-checkbox-label">
<input type="radio" name="access_type_id" value="3" <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' ?>> <?= $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. <strong>🚫 Interdit</strong> — Mon TFE n'est pas disponible en physique ni sur le site. Une note descriptive est disponible sur le site.
</label> </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> <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> </div>
<!-- Licence --> <!-- Licence — swapped via htmx when radio changes -->
<div class="licence-license-choice"> <div class="licence-license-choice"
<h3>Licence du TFE</h3> hx-post="<?= $adminMode ? '/admin/licence-fragment.php' : '/partage/licence-fragment' ?>"
<?php hx-trigger="load"
$name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes; hx-include="closest fieldset"
$selected = $formData['license_id'] ?? ''; $placeholder = '— Sélectionner —'; $required = !$adminMode; hx-swap="outerHTML">
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>
</div> </div>
</fieldset> </fieldset>

View File

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

View File

@@ -102,6 +102,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<?php if ($showIntroHelp && isset($helpFn)): ?> <?php if ($showIntroHelp && isset($helpFn)): ?>
<?php <?php
$helpContent = $helpFn("partage_intro"); $helpContent = $helpFn("partage_intro");
$helpKey = 'partage_intro';
include APP_ROOT . "/templates/partials/form/form-help-block.php"; include APP_ROOT . "/templates/partials/form/form-help-block.php";
?> ?>
<?php endif; ?> <?php endif; ?>
@@ -146,6 +147,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<?php <?php
if ($mode === "partage" && isset($helpFn)) { if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_tfe_info"); $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/form-help-block.php";
} }
include APP_ROOT . "/templates/partials/form/fieldset-tfe-info.php"; include APP_ROOT . "/templates/partials/form/fieldset-tfe-info.php";
@@ -170,6 +172,13 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<?php endif; ?> <?php endif; ?>
<!-- ═══════════════════ Langue(s) ═══════════════════ --> <!-- ═══════════════════ 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"> <fieldset id="languages-fieldset">
<legend>Langue(s)</legend> <legend>Langue(s)</legend>
<?php <?php
@@ -205,6 +214,13 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
</fieldset> </fieldset>
<!-- ═══════════════════ Mots-clés ═══════════════════ --> <!-- ═══════════════════ 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> <fieldset>
<legend>Mots-clés</legend> <legend>Mots-clés</legend>
<?php <?php
@@ -219,13 +235,20 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
</fieldset> </fieldset>
<!-- ═══════════════════ Cadre académique ═══════════════════ --> <!-- ═══════════════════ 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"; ?> "/templates/partials/form/fieldset-academic.php"; ?>
<!-- ═══════════════════ Composition du jury ═══════════════════ --> <!-- ═══════════════════ Composition du jury ═══════════════════ -->
<?php <?php
if ($mode === "partage" && isset($helpFn)) { if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_jury"); $helpContent = $helpFn("fieldset_jury");
$helpKey = 'fieldset_jury';
include APP_ROOT . "/templates/partials/form/form-help-block.php"; include APP_ROOT . "/templates/partials/form/form-help-block.php";
} }
require APP_ROOT . "/templates/partials/form/jury-fieldset.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. // Synthesise POST-like data so fichiers-fragment.php can render the initial state.
if ($mode === "partage" && isset($helpFn)) { if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_files"); $helpContent = $helpFn("fieldset_files");
$helpKey = 'fieldset_files';
include APP_ROOT . "/templates/partials/form/form-help-block.php"; include APP_ROOT . "/templates/partials/form/form-help-block.php";
} }
// Temporarily populate $_POST so the fragment can read formats/website/annexes values. // Temporarily populate $_POST so the fragment can read formats/website/annexes values.
@@ -260,6 +284,8 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
$_POST['website_url'] = $existingWebsiteUrl; $_POST['website_url'] = $existingWebsiteUrl;
$_POST['website_label'] = $existingWebsiteLabel; $_POST['website_label'] = $existingWebsiteLabel;
$_POST['admin_mode'] = $adminMode ? '1' : '0'; $_POST['admin_mode'] = $adminMode ? '1' : '0';
$_POST['edit_mode'] = '1';
$_POST['_cover'] = $currentCover['file_path'] ?? null;
$_POST['has_annexes'] = $formData['has_annexes'] ?? null; $_POST['has_annexes'] = $formData['has_annexes'] ?? null;
include APP_ROOT . '/public/partage/fichiers-fragment.php'; include APP_ROOT . '/public/partage/fichiers-fragment.php';
$_POST = $_savedPost; $_POST = $_savedPost;
@@ -268,29 +294,6 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<!-- Edit-only: existing files management --> <!-- Edit-only: existing files management -->
<div id="edit-existing-files-block"> <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 --> <!-- Existing thesis files — sortable, with labels -->
<?php $thesisFilesList = array_values( <?php $thesisFilesList = array_values(
array_filter( array_filter(
@@ -398,13 +401,20 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<?php endif; ?> <?php endif; ?>
<!-- ═══════════════════ Métadonnées complémentaires ═══════════════════ --> <!-- ═══════════════════ 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"; ?> "/templates/partials/form/fieldset-metadata.php"; ?>
<!-- ═══════════════════ Degrés d'ouverture et licences ═══════════════════ --> <!-- ═══════════════════ Degrés d'ouverture et licences ═══════════════════ -->
<?php <?php
if ($mode === "partage" && isset($helpFn)) { if ($mode === "partage" && isset($helpFn)) {
$helpContent = $helpFn("fieldset_access"); $helpContent = $helpFn("fieldset_access");
$helpKey = 'fieldset_access';
include APP_ROOT . "/templates/partials/form/form-help-block.php"; include APP_ROOT . "/templates/partials/form/form-help-block.php";
} }
include APP_ROOT . include APP_ROOT .
@@ -509,6 +519,7 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
<?php if ($mode === "partage" && isset($helpFn)): ?> <?php if ($mode === "partage" && isset($helpFn)): ?>
<?php <?php
$helpContent = $helpFn("fieldset_email"); $helpContent = $helpFn("fieldset_email");
$helpKey = 'fieldset_email';
include APP_ROOT . "/templates/partials/form/form-help-block.php"; include APP_ROOT . "/templates/partials/form/form-help-block.php";
?> ?>
<?php endif; ?> <?php endif; ?>