diff --git a/TODO.md b/TODO.md
index a9e1efc..9392d2a 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,5 +1,9 @@
# TODO
+- [x] Remove delete-all TFE from parametres (template, dialog, controller, DB method, logger)
+- [x] Move Formulaire + Types de travaux from parametres to contenus under Paramètres du Formulaire h2
+- [x] Restructure contenus Formulaire: sub-headings for Restrictions, Degré d'ouverture, Types de travaux, Structure
+- [x] Copy mots-clé htmx system (dropdown, pills, create) to Autre Langue input
- [x] Languages: store lowercase, display with ucfirst (getOrCreateLanguage, CSV import, getAllLanguages, v_theses_full, schema seed data, migration 025)
- [x] CSV importer: add AP aliases for D&P du multiple, PACS variants, Narraion typo
- [x] Move default semantic form element styles (checkbox, radio, select) from admin.css/form.css into common.css
diff --git a/app/public/admin/actions/delete.php b/app/public/admin/actions/delete.php
index 8f02a2c..e1e53d9 100644
--- a/app/public/admin/actions/delete.php
+++ b/app/public/admin/actions/delete.php
@@ -16,20 +16,14 @@ if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
exit;
}
-$isBulk = !empty($_POST['bulk']);
-$isDeleteAll = !empty($_POST['delete_all']);
+$isBulk = !empty($_POST['bulk']);
try {
$db = new Database();
$logger = AdminLogger::make();
- if ($isDeleteAll) {
- $count = $db->deleteAllTheses();
- $logger->logDeleteAllTheses($count);
- App::flash('success', "$count TFE(s) supprimé(s) avec succès.");
-
- } elseif ($isBulk) {
+ if ($isBulk) {
$ids = array_filter(array_map('intval', $_POST['selected_theses'] ?? []), fn($id) => $id > 0);
if (empty($ids)) {
diff --git a/app/public/admin/contenus.php b/app/public/admin/contenus.php
index d5b139c..251280a 100644
--- a/app/public/admin/contenus.php
+++ b/app/public/admin/contenus.php
@@ -18,6 +18,7 @@ try {
$pages = array_values(array_filter($allPages, fn($p) => in_array($p['slug'], $allowedPageSlugs, true)));
$aproposKeys = $db->getAllAproposContents();
$formHelpBlocks = $db->getAllFormHelpBlocks();
+ $siteSettings = $db->getAllSettings();
} catch (Exception $e) {
error_log("Error loading contenus: " . $e->getMessage());
die("Erreur lors du chargement des contenus.");
diff --git a/app/public/admin/index.php b/app/public/admin/index.php
index 54f00d8..f64826e 100644
--- a/app/public/admin/index.php
+++ b/app/public/admin/index.php
@@ -349,18 +349,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
}
}
if (!empty($languageRaw)) {
- $langName = strtolower(trim($languageRaw));
- // Lookup case-insensitively; insert if missing (stored lowercase).
- $s = $importPdo->prepare("SELECT id FROM languages WHERE LOWER(name) = LOWER(?)");
- $s->execute([$langName]);
- $r = $s->fetch();
- $langId = $r ? (int)$r['id'] : null;
- if ($langId === null) {
- $importPdo->prepare("INSERT INTO languages (name) VALUES (?)")->execute([$langName]);
- $langId = (int)$importPdo->lastInsertId();
+ foreach (array_map('trim', explode(',', $languageRaw)) as $langName) {
+ $langName = strtolower($langName);
+ if ($langName === '') continue;
+ // Lookup case-insensitively; insert if missing (stored lowercase).
+ $s = $importPdo->prepare("SELECT id FROM languages WHERE LOWER(name) = LOWER(?)");
+ $s->execute([$langName]);
+ $r = $s->fetch();
+ $langId = $r ? (int)$r['id'] : null;
+ if ($langId === null) {
+ $importPdo->prepare("INSERT INTO languages (name) VALUES (?)")->execute([$langName]);
+ $langId = (int)$importPdo->lastInsertId();
+ }
+ $s2 = $importPdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?,?)");
+ $s2->execute([$thesisId, $langId]);
}
- $s2 = $importPdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?,?)");
- $s2->execute([$thesisId, $langId]);
}
if (!empty($formatsRaw)) {
foreach (array_map('trim', explode(',', $formatsRaw)) as $fmt) {
diff --git a/app/public/admin/language-search-fragment.php b/app/public/admin/language-search-fragment.php
new file mode 100644
index 0000000..91a91fc
--- /dev/null
+++ b/app/public/admin/language-search-fragment.php
@@ -0,0 +1,13 @@
+getAllSettings();
$peerTubeSettings = PeerTubeService::getSettings($db);
$peerTubeEnabled = PeerTubeService::isEnabled($db);
$peerTubeConfigured = PeerTubeService::isConfigured($db);
-$stats = $db->getThesesStats();
$smtpSettings = SmtpRelay::getSettings($db);
$smtpConfigured = SmtpRelay::isConfigured($db);
$smtpErrorField = $_SESSION['_flash_smtp_field'] ?? null;
diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css
index b64c37e..538dce7 100644
--- a/app/public/assets/css/admin.css
+++ b/app/public/assets/css/admin.css
@@ -379,6 +379,11 @@ th.admin-ap-col {
.admin-body table .thesis-title {
font-weight: 500;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ word-break: break-word;
}
.admin-body table .thesis-subtitle {
@@ -1943,3 +1948,26 @@ th.admin-ap-col {
margin: 0;
}
}
+
+/* ── Active search loading indicator ───────────────────────────────────── */
+.admin-search-indicator {
+ display: block;
+ height: 2px;
+ background: var(--accent-primary);
+ opacity: 0;
+ transition: opacity 0.15s;
+ pointer-events: none;
+ margin-top: var(--space-2xs);
+}
+
+.admin-search-indicator.htmx-request {
+ opacity: 1;
+ animation: admin-search-progress 1.2s ease-in-out infinite;
+}
+
+@keyframes admin-search-progress {
+ 0% { transform: scaleX(0); transform-origin: left; }
+ 50% { transform: scaleX(1); transform-origin: left; }
+ 50.01% { transform: scaleX(1); transform-origin: right; }
+ 100% { transform: scaleX(0); transform-origin: right; }
+}
diff --git a/app/public/partage/index.php b/app/public/partage/index.php
index 3f4d488..b4137a0 100644
--- a/app/public/partage/index.php
+++ b/app/public/partage/index.php
@@ -56,6 +56,13 @@ if ($slug === 'tag-search-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') {
exit;
}
+// Special route: /partage/language-search-fragment (HTMX fragment — interactive language search)
+if ($slug === 'language-search-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') {
+ App::boot();
+ require_once __DIR__ . '/language-search-fragment.php';
+ exit;
+}
+
// Special route: /partage/recapitulatif?id=N
if ($slug === 'recapitulatif' || $slug === 'recapitulatif.php') {
App::boot();
diff --git a/app/public/partage/language-autre-fragment.php b/app/public/partage/language-autre-fragment.php
index 1f5fb3b..26f6e84 100644
--- a/app/public/partage/language-autre-fragment.php
+++ b/app/public/partage/language-autre-fragment.php
@@ -2,9 +2,9 @@
/**
* language-autre-fragment.php
*
- * Shared HTMX fragment include: returns the "Autre(s) langue(s)" input row
- * when no standard language checkbox is checked, or the plain (non-required)
- * variant when at least one is checked.
+ * Shared HTMX fragment include: returns the requirement asterisk for the
+ * "Autre(s) langue(s)" label. When no standard language checkbox is checked,
+ * an asterisk is rendered to signal the field is required (server-side validated).
*
* Included by:
* - /admin/language-autre-fragment.php (AdminAuth gated)
@@ -12,25 +12,11 @@
*
* Expected POST:
* languages[] — selected language IDs (may be absent)
- * language_autre — current free-text value (for repopulation)
*/
$selectedIds = isset($_POST['languages']) && is_array($_POST['languages'])
? $_POST['languages']
: [];
-$currentValue = htmlspecialchars(trim($_POST['language_autre'] ?? ''));
$anyChecked = !empty($selectedIds);
?>
-
-
-
-
- >
- Si votre TFE contient une langue absente de la liste, précisez-la ici.
-
-
-
+= !$anyChecked ? ' *' : '' ?>
diff --git a/app/public/partage/language-search-fragment.php b/app/public/partage/language-search-fragment.php
new file mode 100644
index 0000000..3b46b1f
--- /dev/null
+++ b/app/public/partage/language-search-fragment.php
@@ -0,0 +1,107 @@
+getConnection()->prepare(
+ "SELECT l.id, UPPER(SUBSTR(l.name,1,1)) || SUBSTR(l.name,2) as name,
+ COUNT(DISTINCT tl.thesis_id) as thesis_count
+ FROM languages l
+ LEFT JOIN thesis_languages tl ON l.id = tl.language_id
+ WHERE LOWER(l.name) LIKE ?
+ AND LOWER(l.name) NOT IN ($placeholders)
+ GROUP BY l.id
+ ORDER BY LOWER(l.name) = ? DESC, thesis_count DESC, LOWER(l.name)
+ LIMIT 10"
+ );
+ $stmt->execute(array_merge([$query . '%'], $predefined, [$query]));
+} else {
+ $placeholders = implode(',', array_fill(0, count($predefined), '?'));
+ $stmt = $db->getConnection()->prepare(
+ "SELECT l.id, UPPER(SUBSTR(l.name,1,1)) || SUBSTR(l.name,2) as name,
+ COUNT(DISTINCT tl.thesis_id) as thesis_count
+ FROM languages l
+ LEFT JOIN thesis_languages tl ON l.id = tl.language_id
+ WHERE LOWER(l.name) NOT IN ($placeholders)
+ GROUP BY l.id
+ ORDER BY thesis_count DESC, LOWER(l.name)
+ LIMIT 10"
+ );
+ $stmt->execute($predefined);
+}
+
+$results = $stmt->fetchAll();
+
+// Deduplicate results by lowercase name
+$seen = [];
+$results = array_values(array_filter($results, function($lang) use (&$seen) {
+ $key = strtolower($lang['name']);
+ if (isset($seen[$key])) return false;
+ $seen[$key] = true;
+ return true;
+}));
+
+// Filter out already-selected languages (case-insensitive)
+$results = array_values(array_filter($results, function($lang) use ($currentLanguages) {
+ return !in_array(strtolower($lang['name']), $currentLanguages, true);
+}));
+
+// Check if query exactly matches an existing language (case-insensitive)
+// Also check against predefined languages to avoid suggesting creation of a checkbox language
+$exactExists = false;
+foreach ($results as $lang) {
+ if (strcasecmp($lang['name'], $query) === 0) {
+ $exactExists = true;
+ break;
+ }
+}
+if (!$exactExists && $query !== '') {
+ $normalisedQuery = strtolower($query);
+ $normalisedPredefined = array_map('strtolower', $predefined);
+ if (in_array($normalisedQuery, $normalisedPredefined, true)) {
+ $exactExists = true;
+ }
+}
+
+// If no exact match and query non-empty, suggest creation
+$canCreate = ($query !== '' && !$exactExists && !in_array($query, $currentLanguages, true));
+?>
+
+Aucune langue trouvée.
+
+
+
+
+
+
+
+
+
diff --git a/app/src/AdminLogger.php b/app/src/AdminLogger.php
index cfa82ea..7abd489 100644
--- a/app/src/AdminLogger.php
+++ b/app/src/AdminLogger.php
@@ -194,12 +194,6 @@ class AdminLogger
]);
}
- /** Parametres: delete all TFEs */
- public function logDeleteAllTheses(int $count): void
- {
- $this->write('system', 'delete_all_theses', 'success', ['count' => $count]);
- }
-
/** Parametres: formulaire section toggles */
public function logFormSettingsUpdate(array $newValues): void
{
diff --git a/app/src/Controllers/ThesisCreateController.php b/app/src/Controllers/ThesisCreateController.php
index 778b9f7..f68ba9e 100644
--- a/app/src/Controllers/ThesisCreateController.php
+++ b/app/src/Controllers/ThesisCreateController.php
@@ -114,7 +114,7 @@ class ThesisCreateController
'orientations' => $this->db->getAllOrientations(),
'apPrograms' => $this->db->getAllAPPrograms(),
'finalityTypes' => $this->db->getAllFinalityTypes(),
- 'languages' => $this->db->getAllLanguages(),
+ 'languages' => $this->db->getPredefinedLanguages(),
'formatTypes' => $this->db->getAllFormatTypes(),
'licenseTypes' => $this->db->getAllLicenseTypes(),
'enabledAccessTypes' => $this->db->getEnabledFormAccessTypes(),
@@ -467,8 +467,16 @@ class ThesisCreateController
$languageIds = isset($post['languages']) && is_array($post['languages'])
? array_map('intval', $post['languages'])
: [];
- $autreRaw = trim($post['language_autre'] ?? '');
- if ($autreRaw !== '') {
+ // language_autre: pill-based component sends an array; also handle legacy comma-separated string
+ $autreRaw = $post['language_autre'] ?? '';
+ if (is_array($autreRaw)) {
+ foreach ($autreRaw as $langName) {
+ $langName = trim($langName);
+ if ($langName !== '') {
+ $languageIds[] = $this->db->getOrCreateLanguage($langName);
+ }
+ }
+ } elseif (is_string($autreRaw) && trim($autreRaw) !== '') {
foreach (array_map('trim', explode(',', $autreRaw)) as $langName) {
if ($langName !== '') {
$languageIds[] = $this->db->getOrCreateLanguage($langName);
diff --git a/app/src/Controllers/ThesisEditController.php b/app/src/Controllers/ThesisEditController.php
index 58193ec..677d176 100644
--- a/app/src/Controllers/ThesisEditController.php
+++ b/app/src/Controllers/ThesisEditController.php
@@ -95,7 +95,7 @@ class ThesisEditController
$orientations = $this->db->getAllOrientations();
$apPrograms = $this->db->getAllAPPrograms();
$finalityTypes = $this->db->getAllFinalityTypes();
- $languages = $this->db->getAllLanguages();
+ $languages = $this->db->getPredefinedLanguages();
$formatTypes = $this->db->getAllFormatTypes();
$licenseTypes = $this->db->getAllLicenseTypes();
$enabledAccessTypes = $this->db->getEnabledFormAccessTypes();
@@ -247,8 +247,16 @@ class ThesisEditController
$langIds = isset($post['languages']) && is_array($post['languages'])
? $post['languages']
: [];
- $autreRaw = trim($post['language_autre'] ?? '');
- if ($autreRaw !== '') {
+ // language_autre: pill-based component sends an array; also handle legacy comma-separated string
+ $autreRaw = $post['language_autre'] ?? '';
+ if (is_array($autreRaw)) {
+ foreach ($autreRaw as $langName) {
+ $langName = trim($langName);
+ if ($langName !== '') {
+ $langIds[] = (string)$this->db->getOrCreateLanguage($langName);
+ }
+ }
+ } elseif (is_string($autreRaw) && trim($autreRaw) !== '') {
foreach (array_map('trim', explode(',', $autreRaw)) as $langName) {
if ($langName !== '') {
$langIds[] = (string)$this->db->getOrCreateLanguage($langName);
diff --git a/app/src/Database.php b/app/src/Database.php
index a3c6757..12983e9 100644
--- a/app/src/Database.php
+++ b/app/src/Database.php
@@ -752,6 +752,21 @@ class Database
return $stmt->fetchAll();
}
+ /**
+ * Return only the predefined / hardcoded languages used as checkboxes
+ * in the form. All other languages go into the "Autre langue" input.
+ */
+ public function getPredefinedLanguages(): array
+ {
+ $stmt = $this->pdo->query(
+ "SELECT id, UPPER(SUBSTR(name,1,1)) || SUBSTR(name,2) as name, created_at
+ FROM languages
+ WHERE LOWER(name) IN ('français', 'anglais', 'néerlandais', 'francais', 'neerlandais')
+ ORDER BY name"
+ );
+ return $stmt->fetchAll();
+ }
+
// ========================================================================
// ADMIN LIST METHOD
// ========================================================================
@@ -1940,17 +1955,6 @@ class Database
/**
* Delete every thesis in the database.
*/
- public function deleteAllTheses(): int
- {
- $ids = $this->pdo->query('SELECT id FROM theses')->fetchAll(\PDO::FETCH_COLUMN);
- if (empty($ids)) {
- return 0;
- }
- $count = count($ids);
- $this->bulkDeleteTheses($ids);
- return $count;
- }
-
/**
* Insert a thesis file record.
* sort_order defaults to (max existing sort_order + 1) for the thesis.
diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php
index 0ee4e2c..dc9c906 100644
--- a/app/templates/admin/acces.php
+++ b/app/templates/admin/acces.php
@@ -100,6 +100,19 @@
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+\\\\\\\ to: sttrwkly ec5606f5 "CSV importer: boolean and ap variants/typos" (rebased revision)
++ $linkName = $link['name'] ?? '';
+++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
+%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: sttrwkly ec5606f5 "CSV importer: boolean and ap variants/typos" (rebased revision)
+\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
+- $linkName = $link['name'] ?? '';
+- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
+%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
+\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: qxuprqpt da941497 "Add language-search component for Autre Langue input + active search in lists" (rebased revision)
+ $linkName = $link['name'] ?? '';
+ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
+ $linkLockedYear = $link['locked_year'] ?? null;
++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
++\\\\\\\ to: qxuprqpt a1b3064d "Add language-search component for Autre Langue input + active search in lists" (rebased revision)
+++ $linkName = $link['name'] ?? '';
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
?>
diff --git a/app/templates/admin/contenus.php b/app/templates/admin/contenus.php
index d830c97..300639d 100644
--- a/app/templates/admin/contenus.php
+++ b/app/templates/admin/contenus.php
@@ -11,6 +11,116 @@
= htmlspecialchars($flash['error']) ?>
+
+
+
+
+
+
+
+
+
+ Options de visibilité disponibles dans le formulaire d'ajout de TFE.
+ L'option Libre ne sera activée qu'à partir de l'année académique prochaine.
+
+
+
+
+
+
+
+ Active ou désactive les types de travaux dans les formulaires et la consultation. Un type désactivé ne peut plus être soumis ni affiché sur le site.
+ Le type TFE est toujours actif et ne peut pas être désactivé.
+
+
+
+
+
Pages statiques
-
-
-
- Chaque bloc d'aide s'affiche au-dessus de sa section dans le formulaire de soumission.
- Le bouton rond active/désactive l'affichage.
-
+
+
+
+
+ Chaque bloc d'aide s'affiche au-dessus de sa section dans le formulaire de soumission.
+ Le bouton rond active/désactive l'affichage.
+
+
diff --git a/app/templates/admin/edit.php b/app/templates/admin/edit.php
index c8d47a9..216d03b 100644
--- a/app/templates/admin/edit.php
+++ b/app/templates/admin/edit.php
@@ -106,6 +106,26 @@
// Languages — either from flash repopulation or current thesis data
$formData['languages'] = $formData['languages'] ?? $currentLanguages ?? [];
+ // Compute "other" languages (those not in the predefined checkbox list)
+ $predefinedLangIds = array_column($languages, 'id');
+ $otherLangIds = array_diff($currentLanguages ?? [], $predefinedLangIds);
+ $selectedOtherLanguages = [];
+ if (!empty($otherLangIds)) {
+ $allLangs = Database::getInstance()->getAllLanguages();
+ $allLangMap = [];
+ foreach ($allLangs as $al) {
+ $allLangMap[(int)$al['id']] = $al['name'];
+ }
+ foreach ($otherLangIds as $lid) {
+ $lid = (int)$lid;
+ if (isset($allLangMap[$lid])) {
+ $selectedOtherLanguages[] = $allLangMap[$lid];
+ }
+ }
+ // Sort alphabetically
+ sort($selectedOtherLanguages, SORT_NATURAL | SORT_FLAG_CASE);
+ }
+
// Tags — either from flash repopulation or current thesis data
$keywordsStr = $thesis['keywords'] ?? '';
$currentTags = $keywordsStr !== '' ? array_map('trim', explode(',', $keywordsStr)) : [];
diff --git a/app/templates/admin/index-table.php b/app/templates/admin/index-table.php
index 79f4f71..8ca1a2e 100644
--- a/app/templates/admin/index-table.php
+++ b/app/templates/admin/index-table.php
@@ -21,6 +21,10 @@ $sortArrow = function(string $col) use ($sortCol, $sortDir): string {
+
+
+
+
0 TFE(s) sélectionné(s)
@@ -42,13 +46,13 @@ $sortArrow = function(string $col) use ($sortCol, $sortDir): string {
|
- ID= $sortArrow('identifier') ?> |
- Titre= $sortArrow('title') ?> |
+ ID= $sortArrow('identifier') ?> |
+ Titre= $sortArrow('title') ?> |
Auteur(s) |
- Année= $sortArrow('year') ?> |
- Orientation= $sortArrow('orientation') ?> |
- AP= $sortArrow('ap_program') ?> |
- Publié= $sortArrow('is_published') ?> |
+ Année= $sortArrow('year') ?> |
+ Orientation= $sortArrow('orientation') ?> |
+ AP= $sortArrow('ap_program') ?> |
+ Publié= $sortArrow('is_published') ?> |
Accès |
Actions |
@@ -64,7 +68,7 @@ $sortArrow = function(string $col) use ($sortCol, $sortDir): string {
|
= htmlspecialchars($thesis['identifier'] ?? $thesis['id']) ?> |
- = htmlspecialchars($thesis['title']) ?>
+ = htmlspecialchars($thesis['title']) ?>
|
= htmlspecialchars($thesis['authors'] ?? 'N/A') ?> |
= $thesis['year'] ?> |
diff --git a/app/templates/admin/index.php b/app/templates/admin/index.php
index ffcae82..84a4949 100644
--- a/app/templates/admin/index.php
+++ b/app/templates/admin/index.php
@@ -43,7 +43,15 @@ document.addEventListener('htmx:afterSwap',()=>{document.querySelectorAll('input
-
+
+
+
diff --git a/app/templates/admin/parametres.php b/app/templates/admin/parametres.php
index 98bb50a..b34cfc6 100644
--- a/app/templates/admin/parametres.php
+++ b/app/templates/admin/parametres.php
@@ -31,129 +31,6 @@
-
-
-
-
-
-
-
-
- Options de visibilité disponibles dans le formulaire d'ajout de TFE.
- L'option Libre ne sera activée qu'à partir de l'année académique prochaine.
-
-
-
-
-
-
- Types de travaux
- Active ou désactive les types de travaux dans les formulaires et la consultation. Un type désactivé ne peut plus être soumis ni affiché sur le site.
- Le type TFE est toujours actif et ne peut pas être désactivé.
-
-
-
+
diff --git a/app/templates/partials/form/form.php b/app/templates/partials/form/form.php
index aa9553c..cec0b30 100644
--- a/app/templates/partials/form/form.php
+++ b/app/templates/partials/form/form.php
@@ -186,29 +186,45 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
$checked = $formData["languages"] ?? [];
$required = !$adminMode;
$hxPost = $mode === 'partage' ? "/partage/language-autre-fragment" : "/admin/language-autre-fragment.php";
- $hxTarget = "#language-autre-row";
+ $hxTarget = "#language-autre-required";
$hxSwap = "outerHTML";
include APP_ROOT . "/templates/partials/form/checkbox-list.php";
unset($hxSwap);
?>
trim($_l)];
+ }
+ }
+ } elseif (!empty($selectedOtherLanguages) && is_array($selectedOtherLanguages)) {
+ $_selectedOtherLangs = array_map(fn($n) => ['name' => $n], $selectedOtherLanguages);
+ } else {
+ $_langRaw = $formData["language_autre"] ?? '';
+ if (is_string($_langRaw) && $_langRaw !== '') {
+ foreach (array_map('trim', explode(',', $_langRaw)) as $_l) {
+ if ($_l !== '') {
+ $_selectedOtherLangs[] = ['name' => $_l];
+ }
+ }
+ }
+ }
+ ?>
+
-
-
-
-
- >
- Si votre TFE contient une langue absente de la liste, précisez-la ici.
-
-
-
-
diff --git a/app/templates/partials/form/language-search.php b/app/templates/partials/form/language-search.php
new file mode 100644
index 0000000..858bb01
--- /dev/null
+++ b/app/templates/partials/form/language-search.php
@@ -0,0 +1,242 @@
+ int|null, 'name' => string] for pre-filled languages
+ * string|null $id — override the id attribute prefix
+ * int $maxLanguages — maximum number of languages (default 10)
+ * bool $required — whether at least one "other language" is required (default false)
+ */
+
+$name = $name ?? 'language_autre';
+$label = $label ?? 'Autre(s) langue(s)';
+$placeholder = $placeholder ?? 'Rechercher une langue…';
+$hint = $hint ?? null;
+$hxPost = $hxPost ?? '/admin/language-search-fragment.php';
+$selectedLanguages = $selectedLanguages ?? [];
+$id = $id ?? $name;
+$maxLanguages = $maxLanguages ?? 10;
+$required = $required ?? false;
+$langCount = count($selectedLanguages);
+?>
+
+
= htmlspecialchars($label) ?>= $required ? ' *' : '' ?>
+
+
+
= htmlspecialchars($hint) ?>
+
+
+
+
+
+
+
+ = htmlspecialchars($lang['name']) ?>
+
+
+
+
+
+
+
>
+ = $langCount ?>/= (int)$maxLanguages ?>
+ = $maxLanguages): ?>
+ Maximum de langues atteint
+
+
+
+
+
= $maxLanguages ? ' style="display:none"' : '' ?>>
+
+
+
+
+
+
+
+
+
+
+
+