mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
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:
137
TODO.md
137
TODO.md
@@ -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
|
|
||||||
|
|||||||
16
app/migrations/applied/022_form_help_blocks_name_enabled.sql
Normal file
16
app/migrations/applied/022_form_help_blocks_name_enabled.sql
Normal 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';
|
||||||
8
app/migrations/applied/023_add_missing_help_blocks.sql
Normal file
8
app/migrations/applied/023_add_missing_help_blocks.sql
Normal 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);
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
196
app/public/admin/form-help-inline-fragment.php
Normal file
196
app/public/admin/form-help-inline-fragment.php
Normal 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,'<').replace(/>/g,'>') + '</textarea>';
|
||||||
|
var ta = ed.querySelector('textarea');
|
||||||
|
ta.addEventListener('input', function() {
|
||||||
|
hidden.value = this.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
46
app/public/admin/licence-fragment.php
Normal file
46
app/public/admin/licence-fragment.php
Normal 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>
|
||||||
@@ -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
126
app/public/assets/js/overtype-webcomponent.min.js
vendored
Normal file
126
app/public/assets/js/overtype-webcomponent.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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>
|
||||||
|
|||||||
@@ -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'; ?>
|
||||||
|
|||||||
47
app/public/partage/licence-fragment.php
Normal file
47
app/public/partage/licence-fragment.php
Normal 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>
|
||||||
@@ -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
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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; ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user