From e74f9210c5d6ca0a092c0732ee57fc13e98d01b6 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Wed, 24 Jun 2026 12:56:09 +0200 Subject: [PATCH] #gzip #extract-inline-js enable gzip in nginx + move ~730 lines of inline JS to 15 external files --- TODO.md | 4 +- .../assets/js/app/admin-acces-sharelink.js | 46 ++++ app/public/assets/js/app/admin-acces.js | 105 +++++++++ .../assets/js/app/admin-contacts-form.js | 91 ++++++++ .../assets/js/app/admin-contenus-langues.js | 117 ++++++++++ .../assets/js/app/admin-contenus-motscles.js | 117 ++++++++++ app/public/assets/js/app/admin-file-access.js | 14 ++ app/public/assets/js/app/admin-index-bulk.js | 113 ++++++++++ app/public/assets/js/app/admin-tags.js | 80 +++++++ app/public/assets/js/app/admin-toc.js | 63 ++++++ .../assets/js/app/form-duration-toggle.js | 51 +++++ app/public/assets/js/app/form-jury-fields.js | 66 ++++++ .../assets/js/app/form-language-asterisk.js | 32 +++ app/public/assets/js/app/htmx-global-setup.js | 39 ++++ .../assets/js/app/repertoire-accordion.js | 89 ++++++++ .../js/app/repertoire-student-popover.js | 57 +++++ .../assets/js/app/sidebar-links-editor.js | 45 ++++ app/public/assets/js/app/smtp-error-focus.js | 13 ++ app/templates/admin/acces-etudiante.php | 35 +-- app/templates/admin/acces.php | 74 +------ app/templates/admin/apropos-groups-form.php | 85 +------- app/templates/admin/contenus-edit.php | 42 +--- app/templates/admin/contenus.php | 202 +----------------- app/templates/admin/file-access.php | 12 +- app/templates/admin/footer.php | 31 +-- app/templates/admin/index-table.php | 3 - app/templates/admin/index.php | 13 +- app/templates/admin/parametres.php | 13 +- app/templates/admin/partials/admin-toc.php | 44 +--- app/templates/admin/tags.php | 87 ++------ app/templates/partials/form/form.php | 48 +---- app/templates/partials/form/jury-fieldset.php | 51 +---- .../partials/form/language-search.php | 22 +- app/templates/public/repertoire.php | 119 +---------- nginx/xamxam.conf | 18 ++ 35 files changed, 1198 insertions(+), 843 deletions(-) create mode 100644 app/public/assets/js/app/admin-acces-sharelink.js create mode 100644 app/public/assets/js/app/admin-acces.js create mode 100644 app/public/assets/js/app/admin-contacts-form.js create mode 100644 app/public/assets/js/app/admin-contenus-langues.js create mode 100644 app/public/assets/js/app/admin-contenus-motscles.js create mode 100644 app/public/assets/js/app/admin-file-access.js create mode 100644 app/public/assets/js/app/admin-index-bulk.js create mode 100644 app/public/assets/js/app/admin-tags.js create mode 100644 app/public/assets/js/app/admin-toc.js create mode 100644 app/public/assets/js/app/form-duration-toggle.js create mode 100644 app/public/assets/js/app/form-jury-fields.js create mode 100644 app/public/assets/js/app/form-language-asterisk.js create mode 100644 app/public/assets/js/app/htmx-global-setup.js create mode 100644 app/public/assets/js/app/repertoire-accordion.js create mode 100644 app/public/assets/js/app/repertoire-student-popover.js create mode 100644 app/public/assets/js/app/sidebar-links-editor.js create mode 100644 app/public/assets/js/app/smtp-error-focus.js diff --git a/TODO.md b/TODO.md index f656f06..c7a3cde 100644 --- a/TODO.md +++ b/TODO.md @@ -4,13 +4,13 @@ > Context: Inline JS/CSS + gzip analysis (see docs/ANALYSIS_INLINE_JS_CSS_MINIFY.md) ## Pending -- [ ] #gzip-nginx Enable gzip compression in nginx config `(nginx/xamxam.conf)` -- [ ] #extract-inline-js Move inline JS to external files across 17 templates `(app/templates/**/*.php)` - [ ] #rep-student-touch Replace hover student popover with tap-to-open drawer for mobile `(repertoire.php, repertoire.css)` - [ ] #rep-polish Polish: scroll-position memory on HTMX swap, animation tuning `(repertoire.css)` - [ ] #icon-color-verify Verify icon colors render correctly across all pages (header, admin tables, forms, dialogs, cleanup modal) ## Completed +- [x] #gzip-nginx Enable gzip compression in nginx config `(nginx/xamxam.conf)` ✓ +- [x] #extract-inline-js Move inline JS to external files across 17 templates → 15 new JS files created `(app/public/assets/js/app/*.js)` ✓ - [x] #inline-icon-helper Create `icon()` PHP helper + auto-load in bootstrap `(src/icon.php, bootstrap.php)` ✓ - [x] #icon-fill-currentcolor Ensure all 36 icon SVGs use fill="currentColor" or stroke="currentColor" `(assets/icons/*.svg)` ✓ - [x] #migrate-all-img-icons Replace all remaining `` with `` across 26 template files ✓ diff --git a/app/public/assets/js/app/admin-acces-sharelink.js b/app/public/assets/js/app/admin-acces-sharelink.js new file mode 100644 index 0000000..7c96068 --- /dev/null +++ b/app/public/assets/js/app/admin-acces-sharelink.js @@ -0,0 +1,46 @@ +/** + * admin-acces-sharelink.js — Share link management for acces-etudiante.php. + * + * Provides: dialog openers, clipboard copy, password dialog. + */ +(function () { + var createBtn = document.getElementById('open-create-dialog'); + if (createBtn) { + createBtn.addEventListener('click', function () { + document.getElementById('create-dialog').showModal(); + }); + } +})(); + +var _pendingDeleteLinkId = null; + +function openDeleteLinkDialog(id) { + _pendingDeleteLinkId = id; + document.getElementById('delete-link-dialog').showModal(); +} + +function _executeDeleteLink() { + var form = document.getElementById('delete-link-form-' + _pendingDeleteLinkId); + if (form) form.submit(); +} + +function copyUrl(id) { + var input = document.getElementById('url-' + id); + navigator.clipboard.writeText(input.value).then(function () { + var btn = event.target.closest('button'); + var orig = btn.textContent; + btn.textContent = '✓ Copié'; + setTimeout(function () { + btn.textContent = orig; + }, 1200); + }); +} + +function openPasswordDialog(id, hasPassword) { + document.getElementById('password-link-id').value = id; + var info = document.getElementById('password-current-info'); + info.textContent = hasPassword + ? 'Un mot de passe est actuellement configuré. Entrez-en un nouveau ou laissez vide pour le supprimer.' + : 'Aucun mot de passe configuré.'; + document.getElementById('password-dialog').showModal(); +} diff --git a/app/public/assets/js/app/admin-acces.js b/app/public/assets/js/app/admin-acces.js new file mode 100644 index 0000000..f4f3a3b --- /dev/null +++ b/app/public/assets/js/app/admin-acces.js @@ -0,0 +1,105 @@ +/** + * admin-acces.js — Share link + file access management for acces.php. + * + * Reads PHP-injected data from tags: + * + * + * + */ +(function () { + var baseUrlMeta = document.querySelector('meta[name="acces-base-url"]'); + var passwordMeta = document.querySelector('meta[name="acces-new-link-password"]'); + var slugMeta = document.querySelector('meta[name="acces-new-link-slug"]'); + + // Show result dialog after redirect (new link created) + if (passwordMeta && slugMeta && passwordMeta.content && slugMeta.content) { + document.getElementById('create-result-password').value = passwordMeta.content; + document.getElementById('create-result-url').value = + (baseUrlMeta ? baseUrlMeta.content : '') + '/partage/' + slugMeta.content; + document.getElementById('create-result-dialog').showModal(); + } + + // Create dialog opener + var createBtn = document.getElementById('open-create-dialog'); + if (createBtn) { + createBtn.addEventListener('click', function () { + document.getElementById('create-dialog').showModal(); + }); + } +})(); + +function copyUrl(id) { + var input = document.getElementById('url-' + id); + navigator.clipboard.writeText(input.value).then(function () { + var btn = event.target.closest('button'); + if (btn) { + var orig = btn.getAttribute('title') || ''; + btn.setAttribute('title', '✓ Copié'); + setTimeout(function () { + btn.setAttribute('title', orig); + }, 1200); + } + }); +} + +function copyUrlFrom(el) { + navigator.clipboard.writeText(el.value).then(function () { + var btn = el.nextElementSibling; + if (btn) { + var orig = btn.textContent; + btn.textContent = '✓ Copié'; + setTimeout(function () { + btn.textContent = orig; + }, 1200); + } + }); +} + +function copyTextToClipboard(text) { + navigator.clipboard.writeText(text) + .then(function () { + var btn = event && event.target ? event.target.closest('button') : null; + if (btn) { + var orig = btn.getAttribute('title') || ''; + btn.setAttribute('title', '✓ Copié'); + setTimeout(function () { + btn.setAttribute('title', orig); + }, 1200); + } + }) + .catch(function () {}); +} + +function openEditDialog(id, name, hasPassword, expiresVal) { + document.getElementById('edit-link-id').value = id; + document.getElementById('edit-name').value = name || ''; + document.getElementById('edit-expires').value = expiresVal || ''; + document.getElementById('edit-dialog').showModal(); +} + +function openApproveDialog(requestId) { + document.getElementById('approve-request-id').value = requestId; + document.getElementById('approve-dialog').showModal(); +} + +function openRejectDialog(requestId) { + document.getElementById('reject-request-id').value = requestId; + document.getElementById('reject-dialog').showModal(); +} + +var _pendingArchiveLinkId = null; + +function openArchiveLinkDialog(id) { + _pendingArchiveLinkId = id; + document.getElementById('archive-link-dialog').showModal(); +} + +function _executeArchiveLink() { + var form = document.getElementById('archive-link-form-' + _pendingArchiveLinkId); + if (form) form.submit(); +} + +function openDeleteArchivedLinkDialog(id) { + document.getElementById('delete-archived-link-id').value = id; + document.getElementById('delete-archived-link-dialog').showModal(); +} diff --git a/app/public/assets/js/app/admin-contacts-form.js b/app/public/assets/js/app/admin-contacts-form.js new file mode 100644 index 0000000..1216b60 --- /dev/null +++ b/app/public/assets/js/app/admin-contacts-form.js @@ -0,0 +1,91 @@ +/** + * admin-contacts-form.js — Dynamic contact group/entry management. + * + * Reads `data-apropos-key` and `data-apropos-group-count` from the form element. + * Expects templates with ids `entry-template-f-{key}` and `group-template-f-{key}`. + */ +(function () { + var form = document.querySelector('form[data-apropos-key]'); + if (!form) return; + + var key = form.dataset.aproposKey; + var groupCount = parseInt(form.dataset.aproposGroupCount, 10) || 0; + var entryTpl = document.getElementById('entry-template-f-' + key).innerHTML; + var groupTpl = document.getElementById('group-template-f-' + key).innerHTML; + + function reindexGroups() { + var fieldsets = form.querySelectorAll('fieldset.apropos-group'); + groupCount = fieldsets.length; + fieldsets.forEach(function (fs, i) { + var newIdx = i; + var legend = fs.querySelector('legend'); + if (legend) legend.textContent = 'Contact ' + (newIdx + 1); + + fs.querySelectorAll('input').forEach(function (inp) { + if (inp.name) { + inp.name = inp.name.replace(/groups\[\d+\]/, 'groups[' + newIdx + ']'); + } + if (inp.id) { + inp.id = inp.id.replace( + /(group_f_contacts_|entry_f_contacts_)\d+/, + '$1' + newIdx + ); + } + }); + + fs.querySelectorAll('label[for]').forEach(function (lbl) { + lbl.setAttribute( + 'for', + lbl.getAttribute('for').replace( + /(group_f_contacts_|entry_f_contacts_)\d+/, + '$1' + newIdx + ) + ); + }); + + var addBtn = fs.querySelector('.add-entry-btn-f'); + if (addBtn) addBtn.dataset.group = newIdx; + }); + } + + function bindAddEntry(btn) { + btn.addEventListener('click', function () { + var gi = parseInt(this.dataset.group); + var fieldset = this.closest('fieldset'); + var entryCount = fieldset.querySelectorAll('.apropos-entry').length; + var html = entryTpl.replaceAll('{{gi}}', gi).replaceAll('{{ei}}', entryCount); + this.insertAdjacentHTML('beforebegin', html); + }); + } + + function bindDeleteGroup(btn) { + btn.addEventListener('click', function () { + var fieldset = this.closest('fieldset'); + if (fieldset) { + fieldset.remove(); + reindexGroups(); + } + }); + } + + // Bind existing buttons + form.querySelectorAll('.add-entry-btn-f').forEach(bindAddEntry); + form.querySelectorAll('.delete-group-btn-f').forEach(bindDeleteGroup); + + form.querySelector('.add-group-btn-f').addEventListener('click', function () { + groupCount++; + var html = groupTpl.replaceAll('{{gi}}', groupCount); + this.insertAdjacentHTML('beforebegin', html); + + var newGroup = this.previousElementSibling; + if (newGroup && newGroup.classList.contains('apropos-group')) { + var addBtn = newGroup.querySelector('.add-entry-btn-f'); + if (addBtn) { + addBtn.dataset.group = groupCount; + bindAddEntry(addBtn); + } + var delBtn = newGroup.querySelector('.delete-group-btn-f'); + if (delBtn) bindDeleteGroup(delBtn); + } + }); +})(); diff --git a/app/public/assets/js/app/admin-contenus-langues.js b/app/public/assets/js/app/admin-contenus-langues.js new file mode 100644 index 0000000..3187afc --- /dev/null +++ b/app/public/assets/js/app/admin-contenus-langues.js @@ -0,0 +1,117 @@ +/** + * admin-contenus-langues.js — Langues management on contenus.php. + * Bulk merge, delete, toggle-all, sticky bar height tracking. + * + * Inline-rename functions (languesStartRename, languesCancelRename) remain in + * the template because they embed PHP-generated icon SVGs. + */ +var _languesPendingForm = null; + +function languesConfirmDelete(btn, name) { + _languesPendingForm = btn.closest('form'); + document.getElementById('langues-delete-name').textContent = name; + document.getElementById('langues-delete-dialog').showModal(); +} + +function languesSubmitPending() { + if (_languesPendingForm) _languesPendingForm.submit(); +} + +function languesToggleAll(src) { + document.querySelectorAll('input[name="selected_langs[]"]').forEach(function (cb) { + cb.checked = src.checked; + }); + languesUpdateBulk(); +} + +function languesUpdateBulk() { + var n = document.querySelectorAll('input[name="selected_langs[]"]:checked').length; + document.getElementById('langues-selected-count').textContent = n; + var bar = document.getElementById('langues-bulk-actions'); + var wrap = document.getElementById('langues-table-wrap'); + var visible = n > 1; + bar.style.display = visible ? 'flex' : 'none'; + if (visible) { + requestAnimationFrame(function () { + wrap.style.setProperty('--sticky-top', bar.offsetHeight + 'px'); + }); + } else { + wrap.style.setProperty('--sticky-top', '0px'); + } +} + +function languesCancelSelection() { + document.querySelectorAll('input[name="selected_langs[]"]').forEach(function (cb) { + cb.checked = false; + }); + languesUpdateBulk(); +} + +function languesConfirmBulkDelete() { + var checked = document.querySelectorAll('input[name="selected_langs[]"]:checked'); + if (checked.length < 1) return; + document.getElementById('langues-bulk-delete-count').textContent = checked.length; + document.getElementById('langues-bulk-delete-dialog').showModal(); +} + +function languesExecBulkDelete() { + var container = document.getElementById('langues-bulk-checkboxes'); + container.innerHTML = ''; + document.querySelectorAll('input[name="selected_langs[]"]:checked').forEach(function (cb) { + var inp = document.createElement('input'); + inp.type = 'hidden'; + inp.name = 'selected_langs[]'; + inp.value = cb.value; + container.appendChild(inp); + }); + document.getElementById('langues-bulk-form').querySelector('input[name="action"]').value = + 'delete_bulk'; + document.getElementById('langues-bulk-delete-dialog').close(); + document.getElementById('langues-bulk-form').submit(); +} + +function languesConfirmBulkMerge() { + var checked = document.querySelectorAll('input[name="selected_langs[]"]:checked'); + if (checked.length < 2) return; + document.getElementById('langues-bulk-merge-count').textContent = checked.length; + var sel = document.getElementById('langues-bulk-merge-target-select'); + sel.innerHTML = ''; + checked.forEach(function (cb) { + var tr = cb.closest('tr'); + sel.innerHTML += + ''; + }); + document.getElementById('langues-bulk-merge-dialog').showModal(); +} + +function languesExecBulkMerge() { + var targetId = document.getElementById('langues-bulk-merge-target-select').value; + if (!targetId) return; + document.getElementById('langues-bulk-target').value = targetId; + var container = document.getElementById('langues-bulk-checkboxes'); + container.innerHTML = ''; + document.querySelectorAll('input[name="selected_langs[]"]:checked').forEach(function (cb) { + var inp = document.createElement('input'); + inp.type = 'hidden'; + inp.name = 'selected_langs[]'; + inp.value = cb.value; + container.appendChild(inp); + }); + document.getElementById('langues-bulk-merge-dialog').close(); + document.getElementById('langues-bulk-form').submit(); +} + +document.addEventListener('htmx:afterSwap', function (evt) { + if (evt.target.id === 'langues-table-wrap') { + document + .querySelectorAll('input[name="selected_langs[]"]') + .forEach(function (cb) { + cb.addEventListener('change', languesUpdateBulk); + }); + languesUpdateBulk(); + } +}); diff --git a/app/public/assets/js/app/admin-contenus-motscles.js b/app/public/assets/js/app/admin-contenus-motscles.js new file mode 100644 index 0000000..f3a2c44 --- /dev/null +++ b/app/public/assets/js/app/admin-contenus-motscles.js @@ -0,0 +1,117 @@ +/** + * admin-contenus-motscles.js — Mots-clés management on contenus.php. + * Bulk merge, delete, toggle-all, sticky bar height tracking. + * + * Inline-rename functions (motsclesStartRename, motsclesCancelRename) remain + * in the template because they embed PHP-generated icon SVGs. + */ +var _motsclesPendingForm = null; + +function motsclesConfirmDelete(btn, name) { + _motsclesPendingForm = btn.closest('form'); + document.getElementById('motscles-delete-name').textContent = name; + document.getElementById('motscles-delete-dialog').showModal(); +} + +function motsclesSubmitPending() { + if (_motsclesPendingForm) _motsclesPendingForm.submit(); +} + +function motsclesToggleAll(src) { + document.querySelectorAll('input[name="selected_tags[]"]').forEach(function (cb) { + cb.checked = src.checked; + }); + motsclesUpdateBulk(); +} + +function motsclesUpdateBulk() { + var n = document.querySelectorAll('input[name="selected_tags[]"]:checked').length; + document.getElementById('motscles-selected-count').textContent = n; + var bar = document.getElementById('motscles-bulk-actions'); + var wrap = document.getElementById('motscles-table-wrap'); + var visible = n > 1; + bar.style.display = visible ? 'flex' : 'none'; + if (visible) { + requestAnimationFrame(function () { + wrap.style.setProperty('--sticky-top', bar.offsetHeight + 'px'); + }); + } else { + wrap.style.setProperty('--sticky-top', '0px'); + } +} + +function motsclesConfirmBulkMerge() { + var checked = document.querySelectorAll('input[name="selected_tags[]"]:checked'); + if (checked.length < 2) return; + document.getElementById('motscles-bulk-merge-count').textContent = checked.length; + var sel = document.getElementById('motscles-bulk-merge-target-select'); + sel.innerHTML = ''; + checked.forEach(function (cb) { + var tr = cb.closest('tr'); + sel.innerHTML += + ''; + }); + document.getElementById('motscles-bulk-merge-dialog').showModal(); +} + +function motsclesExecBulkMerge() { + var targetId = document.getElementById('motscles-bulk-merge-target-select').value; + if (!targetId) return; + document.getElementById('motscles-bulk-target').value = targetId; + var container = document.getElementById('motscles-bulk-checkboxes'); + container.innerHTML = ''; + document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach(function (cb) { + var inp = document.createElement('input'); + inp.type = 'hidden'; + inp.name = 'selected_tags[]'; + inp.value = cb.value; + container.appendChild(inp); + }); + document.getElementById('motscles-bulk-merge-dialog').close(); + document.getElementById('motscles-bulk-form').submit(); +} + +function motsclesCancelSelection() { + document.querySelectorAll('input[name="selected_tags[]"]').forEach(function (cb) { + cb.checked = false; + }); + motsclesUpdateBulk(); +} + +function motsclesConfirmBulkDelete() { + var checked = document.querySelectorAll('input[name="selected_tags[]"]:checked'); + if (checked.length < 1) return; + document.getElementById('motscles-bulk-delete-count').textContent = checked.length; + document.getElementById('motscles-bulk-delete-dialog').showModal(); +} + +function motsclesExecBulkDelete() { + var container = document.getElementById('motscles-bulk-checkboxes'); + container.innerHTML = ''; + document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach(function (cb) { + var inp = document.createElement('input'); + inp.type = 'hidden'; + inp.name = 'selected_tags[]'; + inp.value = cb.value; + container.appendChild(inp); + }); + document.getElementById('motscles-bulk-form').querySelector('input[name="action"]').value = + 'delete_bulk'; + document.getElementById('motscles-bulk-delete-dialog').close(); + document.getElementById('motscles-bulk-form').submit(); +} + +document.addEventListener('htmx:afterSwap', function (evt) { + if (evt.target.id === 'motscles-table-wrap') { + document + .querySelectorAll('input[name="selected_tags[]"]') + .forEach(function (cb) { + cb.addEventListener('change', motsclesUpdateBulk); + }); + motsclesUpdateBulk(); + } +}); diff --git a/app/public/assets/js/app/admin-file-access.js b/app/public/assets/js/app/admin-file-access.js new file mode 100644 index 0000000..87e558a --- /dev/null +++ b/app/public/assets/js/app/admin-file-access.js @@ -0,0 +1,14 @@ +/** + * admin-file-access.js — File access request management dialogs. + * + * Provides: openApproveDialog, openRejectDialog. + */ +function openApproveDialog(requestId) { + document.getElementById('approve-request-id').value = requestId; + document.getElementById('approve-dialog').showModal(); +} + +function openRejectDialog(requestId) { + document.getElementById('reject-request-id').value = requestId; + document.getElementById('reject-dialog').showModal(); +} diff --git a/app/public/assets/js/app/admin-index-bulk.js b/app/public/assets/js/app/admin-index-bulk.js new file mode 100644 index 0000000..d05886f --- /dev/null +++ b/app/public/assets/js/app/admin-index-bulk.js @@ -0,0 +1,113 @@ +/** + * admin-index-bulk.js — Bulk selection and actions for the thesis list page. + * + * Provides: toggleAll, updateBulk, getSelectedIds, confirmBulk, execBulk, + * confirmExport, confirmExportFiles, confirmDelete. + */ +(function () { + function toggleAll(src) { + document.querySelectorAll('input[name="selected_theses[]"]').forEach(function (cb) { + cb.checked = src.checked; + }); + updateBulk(); + } + + function updateBulk() { + var n = document.querySelectorAll('input[name="selected_theses[]"]:checked').length; + var b = document.getElementById('bulk-actions'); + document.getElementById('selected-count').textContent = n; + b.style.display = n > 0 ? 'flex' : 'none'; + document.getElementById('admin-table-wrap').style.setProperty( + '--sticky-top', + n > 0 ? b.offsetHeight + 'px' : '0px' + ); + } + + function getSelectedIds() { + return Array.from( + document.querySelectorAll('input[name="selected_theses[]"]:checked') + ).map(function (cb) { + return cb.value; + }); + } + + function confirmBulk(act) { + var ids = getSelectedIds(); + if (!ids.length) { + document.getElementById('no-selection-dialog').showModal(); + return; + } + var n = ids.length; + document.getElementById('bulk-action-input').value = act; + if (act === 'delete') { + document.getElementById('bulk-delete-count').textContent = n; + document.getElementById('bulk-delete-dialog').showModal(); + } else { + document.getElementById('bulk-confirm-word').textContent = + act === 'publish' ? 'Publier' : 'Dépublier'; + document.getElementById('bulk-confirm-count').textContent = n; + document.getElementById('bulk-confirm-dialog').showModal(); + } + } + + function execBulk() { + var a = document.getElementById('bulk-action-input').value; + var f = document.getElementById('bulk-form'); + f.action = a === 'delete' ? 'actions/delete.php' : 'actions/publish.php'; + var c = document.getElementById('bulk-checkboxes'); + c.innerHTML = ''; + getSelectedIds().forEach(function (id) { + var inp = document.createElement('input'); + inp.type = 'hidden'; + inp.name = 'selected_theses[]'; + inp.value = id; + c.appendChild(inp); + }); + f.submit(); + } + + function confirmExport() { + var ids = getSelectedIds(); + if (!ids.length) { + document.getElementById('no-selection-dialog').showModal(); + return; + } + window.location.href = '/admin/actions/export.php?csv=1&ids=' + ids.join(','); + } + + function confirmExportFiles() { + var ids = getSelectedIds(); + if (!ids.length) { + document.getElementById('no-selection-dialog').showModal(); + return; + } + window.location.href = '/admin/actions/export.php?files=1&ids=' + ids.join(','); + } + + function confirmDelete(id, title) { + document.getElementById('delete-thesis-title').textContent = title; + document.getElementById('delete-thesis-dialog').showModal(); + document.getElementById('delete-dialog-confirm').onclick = function () { + document.getElementById('delete-form-' + id).submit(); + }; + } + + function reattachListeners() { + document.querySelectorAll('input[name="selected_theses[]"]').forEach(function (cb) { + cb.addEventListener('change', updateBulk); + }); + updateBulk(); + } + + document.addEventListener('DOMContentLoaded', reattachListeners); + document.addEventListener('htmx:afterSwap', reattachListeners); + + // Export to global scope for onclick handlers in HTML + window.toggleAll = toggleAll; + window.updateBulk = updateBulk; + window.confirmBulk = confirmBulk; + window.execBulk = execBulk; + window.confirmExport = confirmExport; + window.confirmExportFiles = confirmExportFiles; + window.confirmDelete = confirmDelete; +})(); diff --git a/app/public/assets/js/app/admin-tags.js b/app/public/assets/js/app/admin-tags.js new file mode 100644 index 0000000..03f2a70 --- /dev/null +++ b/app/public/assets/js/app/admin-tags.js @@ -0,0 +1,80 @@ +/** + * admin-tags.js — Tags/mots-clés admin page: bulk selection, merge, delete, and + * HTMX-reloaded attachment. + * + * Exported globals: tagsToggleAll, tagsUpdateBulk, tagsConfirmBulkMerge, + * tagsExecBulkMerge, tagsConfirmDelete, _submitPendingTagForm. + * + * Inline-rename functions (tagsStartRename, tagsCancelRename) remain in the + * template because they embed PHP-generated icon SVGs. + */ +var _pendingTagForm = null; + +function tagsToggleAll(src) { + document.querySelectorAll('input[name="selected_tags[]"]').forEach(function (cb) { + cb.checked = src.checked; + }); + tagsUpdateBulk(); +} + +function tagsUpdateBulk() { + var n = document.querySelectorAll('input[name="selected_tags[]"]:checked').length; + document.getElementById('tags-selected-count').textContent = n; + document.getElementById('tags-bulk-actions').style.display = n > 1 ? 'flex' : 'none'; +} + +function tagsConfirmBulkMerge() { + var checked = document.querySelectorAll('input[name="selected_tags[]"]:checked'); + if (checked.length < 2) return; + document.getElementById('bulk-merge-count').textContent = checked.length; + var sel = document.getElementById('bulk-merge-target-select'); + sel.innerHTML = ''; + checked.forEach(function (cb) { + var tr = cb.closest('tr'); + sel.innerHTML += + ''; + }); + document.getElementById('bulk-merge-dialog').showModal(); +} + +function tagsExecBulkMerge() { + var targetId = document.getElementById('bulk-merge-target-select').value; + if (!targetId) return; + document.getElementById('tags-bulk-target').value = targetId; + var container = document.getElementById('tags-bulk-checkboxes'); + container.innerHTML = ''; + document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach(function (cb) { + var inp = document.createElement('input'); + inp.type = 'hidden'; + inp.name = 'selected_tags[]'; + inp.value = cb.value; + container.appendChild(inp); + }); + document.getElementById('bulk-merge-dialog').close(); + document.getElementById('tags-bulk-form').submit(); +} + +function tagsConfirmDelete(btn, name) { + _pendingTagForm = btn.closest('form'); + document.getElementById('delete-tag-name').textContent = name; + document.getElementById('delete-tag-dialog').showModal(); +} + +function _submitPendingTagForm() { + if (_pendingTagForm) _pendingTagForm.submit(); +} + +document.addEventListener('htmx:afterSwap', function (evt) { + if (evt.target.id === 'tags-table-wrap') { + document + .querySelectorAll('input[name="selected_tags[]"]') + .forEach(function (cb) { + cb.addEventListener('change', tagsUpdateBulk); + }); + tagsUpdateBulk(); + } +}); diff --git a/app/public/assets/js/app/admin-toc.js b/app/public/assets/js/app/admin-toc.js new file mode 100644 index 0000000..b50ab2f --- /dev/null +++ b/app/public/assets/js/app/admin-toc.js @@ -0,0 +1,63 @@ +/** + * admin-toc.js — Sticky table-of-contents with IntersectionObserver active-section + * highlighting. + * + * Renders nav links from section[aria-labelledby] headings inside #main-content. + * Hides the TOC aside if fewer than 2 sections exist. + */ +(function () { + function build() { + var main = document.getElementById('main-content'); + var nav = document.getElementById('admin-toc-list'); + var aside = document.getElementById('admin-toc'); + if (!main || !nav || !aside) return; + + var sections = main.querySelectorAll('section[aria-labelledby]'); + if (sections.length < 2) { + aside.hidden = true; + return; + } + + var items = []; + sections.forEach(function (sec) { + var headingId = sec.getAttribute('aria-labelledby'); + var heading = document.getElementById(headingId); + if (!heading) return; + if (!sec.id) sec.id = headingId; + + var a = document.createElement('a'); + a.href = '#' + sec.id; + a.textContent = heading.textContent.trim(); + a.style.display = 'block'; + nav.appendChild(a); + items.push({ section: sec, link: a }); + }); + + var observer = new IntersectionObserver( + function (entries) { + var best = null, + bestRatio = 0; + entries.forEach(function (e) { + if (e.intersectionRatio > bestRatio) { + bestRatio = e.intersectionRatio; + best = e.target; + } + }); + items.forEach(function (item) { + item.link.classList.toggle('admin-toc-active', item.section === best); + }); + }, + { rootMargin: '-10% 0px -70% 0px', threshold: [0, 0.25, 0.5, 0.75, 1] } + ); + + items.forEach(function (item) { + observer.observe(item.section); + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', build); + } else { + build(); + } +})(); diff --git a/app/public/assets/js/app/form-duration-toggle.js b/app/public/assets/js/app/form-duration-toggle.js new file mode 100644 index 0000000..3502eac --- /dev/null +++ b/app/public/assets/js/app/form-duration-toggle.js @@ -0,0 +1,51 @@ +/** + * form-duration-toggle.js — Duration unit toggle on TFE form. + * + * Switches between integer input (pages/mo) and h/m/s time inputs (durée). + * Updates hidden #duration_value on change. + */ +(function () { + var unit = document.getElementById('duration_unit'); + var hidden = document.getElementById('duration_value'); + var intWrap = document.getElementById('duration-value-integer'); + var intInput = document.getElementById('duration_value_int'); + var intLabel = document.getElementById('duration-value-label'); + var timeWrap = document.getElementById('duration-value-time'); + var hInput = document.getElementById('duration_h'); + var mInput = document.getElementById('duration_m'); + var sInput = document.getElementById('duration_s'); + + var LABELS = { pages: 'Nombre :', mo: 'Taille :', durée: 'Durée :' }; + if (!unit || !hidden) return; + + function updateHidden() { + if (unit.value === 'durée') { + var h = parseInt(hInput.value, 10) || 0; + var m = parseInt(mInput.value, 10) || 0; + var s = parseInt(sInput.value, 10) || 0; + var total = h + m / 60 + s / 3600; + hidden.value = total > 0 ? total.toFixed(6) : ''; + } else { + hidden.value = intInput.value; + } + } + + function toggleFields() { + if (unit.value === 'durée') { + intWrap.style.display = 'none'; + timeWrap.style.display = ''; + } else { + timeWrap.style.display = 'none'; + intWrap.style.display = ''; + if (intLabel) intLabel.textContent = LABELS[unit.value] || 'Valeur :'; + } + updateHidden(); + } + + unit.addEventListener('change', toggleFields); + if (intInput) intInput.addEventListener('input', updateHidden); + if (hInput) hInput.addEventListener('input', updateHidden); + if (mInput) mInput.addEventListener('input', updateHidden); + if (sInput) sInput.addEventListener('input', updateHidden); + toggleFields(); +})(); diff --git a/app/public/assets/js/app/form-jury-fields.js b/app/public/assets/js/app/form-jury-fields.js new file mode 100644 index 0000000..92e0239 --- /dev/null +++ b/app/public/assets/js/app/form-jury-fields.js @@ -0,0 +1,66 @@ +/** + * form-jury-fields.js — Jury fieldset helpers: dynamic row add/remove and ULB + * promoteur conditional toggle (based on finality selection). + * + * Reads `data-admin-mode` (0/1) from the jury
. + */ +(function () { + // ── Dynamic row add/remove ────────────────────────────────────────────── + window.addJuryRow = function (listId, inputName, roleLabel) { + var list = document.getElementById(listId); + if (!list) return; + var n = list.querySelectorAll('.admin-jury-entry').length + 1; + var div = document.createElement('div'); + div.className = 'admin-jury-entry'; + div.innerHTML = + '' + + ''; + list.appendChild(div); + }; + + window.removeJuryRow = function (btn) { + var entry = btn.closest('.admin-jury-entry'); + if (entry) entry.remove(); + }; + + // ── ULB toggle ───────────────────────────────────────────────────────── + var juryFieldset = document.querySelector('fieldset[data-admin-mode]'); + var adminMode = juryFieldset ? juryFieldset.getAttribute('data-admin-mode') === '1' : false; + + try { + var finalitySelect = document.querySelector('select[name="finality"]'); + var ulbRow = document.getElementById('jury-promoteur-ulb-row'); + if (!finalitySelect || !ulbRow) return; + var ulbAsterisk = document.getElementById('jury-ulb-asterisk'); + + function isApprofondiSelected() { + var opt = finalitySelect.options[finalitySelect.selectedIndex]; + if (!opt) return false; + return (opt.textContent || opt.text || '').toLowerCase().includes('approfondi'); + } + + function toggleUlb() { + var show = isApprofondiSelected(); + ulbRow.style.display = show ? '' : 'none'; + if (ulbAsterisk) ulbAsterisk.style.display = show ? '' : 'none'; + var inputs = ulbRow.querySelectorAll('input[name="jury_promoteur_ulb_name[]"]'); + inputs.forEach(function (inp, idx) { + inp.required = adminMode ? false : show && idx === 0; + inp.disabled = !show; + if (!show) inp.value = ''; + }); + } + + finalitySelect.addEventListener('change', toggleUlb); + toggleUlb(); + } catch (e) { + console.error('jury ULB toggle:', e); + } +})(); diff --git a/app/public/assets/js/app/form-language-asterisk.js b/app/public/assets/js/app/form-language-asterisk.js new file mode 100644 index 0000000..e88665d --- /dev/null +++ b/app/public/assets/js/app/form-language-asterisk.js @@ -0,0 +1,32 @@ +/** + * form-language-asterisk.js — Toggle the required asterisk on the languages + * fieldset based on whether language pills or checkbox selections exist. + * + * Reads the container id from data-search-container-id on the pill-search div. + */ +(function () { + var pillDiv = document.querySelector('[data-search-container-id]'); + if (!pillDiv) return; + var containerId = pillDiv.getAttribute('data-search-container-id'); + var container = document.getElementById(containerId); + if (!container) return; + var pills = container.querySelector('.tag-search-pills'); + if (!pills) return; + + function check() { + var asteriskEl = document.getElementById('languages-required-asterisk'); + if (!asteriskEl) return; + var n = pills.querySelectorAll('.tag-pill').length; + var checkboxes = document.querySelectorAll( + '#languages-fieldset input[type="checkbox"]:checked' + ); + asteriskEl.innerHTML = + n === 0 && checkboxes.length === 0 + ? ' *' + : ''; + } + + var observer = new MutationObserver(check); + observer.observe(pills, { childList: true }); + check(); +})(); diff --git a/app/public/assets/js/app/htmx-global-setup.js b/app/public/assets/js/app/htmx-global-setup.js new file mode 100644 index 0000000..7537b9d --- /dev/null +++ b/app/public/assets/js/app/htmx-global-setup.js @@ -0,0 +1,39 @@ +/** + * htmx-global-setup.js — Global HTMX event listeners for admin pages. + * + * - Toast accessibility: focuses warning toasts after swap. + * - Markdown cheatsheet dialog: removes stale instances before request, closes on + * backdrop click. + * + * Loaded on all admin pages via footer.php. + */ +(function () { + // Toast accessibility — auto-focus warning toasts + document.body.addEventListener('htmx:afterSettle', function (e) { + if (e.target && e.target.id === 'toast-region') { + var warn = e.target.querySelector('.toast--warning'); + if (warn) { + warn.setAttribute('tabindex', '-1'); + warn.focus(); + } + } + }); + + // Markdown cheatsheet: remove stale dialogs before a new one arrives + document.body.addEventListener('htmx:beforeRequest', function (e) { + if ( + e.detail.requestConfig && + e.detail.requestConfig.path === '/admin/markdown-cheatsheet-fragment.php' + ) { + var old = document.getElementById('md-cheatsheet-dialog'); + if (old) old.remove(); + } + }); + + // Markdown cheatsheet: close on backdrop (dialog element) click + document.body.addEventListener('click', function (e) { + if (e.target.tagName === 'DIALOG' && e.target.id === 'md-cheatsheet-dialog') { + e.target.close(); + } + }); +})(); diff --git a/app/public/assets/js/app/repertoire-accordion.js b/app/public/assets/js/app/repertoire-accordion.js new file mode 100644 index 0000000..f5f369d --- /dev/null +++ b/app/public/assets/js/app/repertoire-accordion.js @@ -0,0 +1,89 @@ +/** + * repertoire-accordion.js — Mobile accordion for repertoire index columns. + * + * Single-open behavior on mobile (≤ 1025px). Re-initializes after HTMX swaps. + * On desktop, all panels close when crossing the breakpoint. + */ +(function () { + var INDEX_SEL = '#repertoire-index'; + var ACCORDION_SEL = '.rep-accordion'; + var TOGGLE_SEL = '.rep-accordion__toggle'; + var PANEL_SEL = '.rep-accordion__panel'; + + function isMobile() { + return window.matchMedia('(max-width: 1025px)').matches; + } + + function initAccordions(root) { + if (!isMobile()) return; + var toggles = root.querySelectorAll(TOGGLE_SEL); + toggles.forEach(function (btn) { + // Skip students column — always visible, not an accordion + if (btn.closest('[data-col="students"]')) return; + if (btn._accordionBound) return; + btn._accordionBound = true; + btn.addEventListener('click', function () { + var section = btn.closest(ACCORDION_SEL); + var panel = section.querySelector(PANEL_SEL); + var isOpen = btn.getAttribute('aria-expanded') === 'true'; + + // Close all others (except students column) + root.querySelectorAll(ACCORDION_SEL).forEach(function (s) { + if (s.dataset.col === 'students') return; + var p = s.querySelector(PANEL_SEL); + var t = s.querySelector(TOGGLE_SEL); + if (s !== section) { + t.setAttribute('aria-expanded', 'false'); + p.classList.remove('is-open'); + } + }); + + // Toggle this one + var nowOpen = !isOpen; + btn.setAttribute('aria-expanded', nowOpen ? 'true' : 'false'); + if (nowOpen) { + panel.classList.add('is-open'); + } else { + panel.classList.remove('is-open'); + } + }); + }); + } + + // Initial bind + initAccordions(document); + + // Re-bind after HTMX swaps (use live DOM since e.detail.target may be detached) + document.body.addEventListener('htmx:afterSwap', function (e) { + if ( + e.detail.target && + e.detail.target.matches && + e.detail.target.matches(INDEX_SEL) + ) { + var liveIndex = document.querySelector(INDEX_SEL); + if (liveIndex) initAccordions(liveIndex); + } + }); + + // Re-bind on resize crossing the breakpoint + var wasMobile = isMobile(); + window.addEventListener('resize', function () { + var nowMobile = isMobile(); + if (nowMobile !== wasMobile) { + wasMobile = nowMobile; + if (!nowMobile) { + // Switching to desktop — close all panels + document + .querySelectorAll(INDEX_SEL + ' ' + TOGGLE_SEL) + .forEach(function (btn) { + btn.setAttribute('aria-expanded', 'false'); + }); + document + .querySelectorAll(INDEX_SEL + ' ' + PANEL_SEL) + .forEach(function (p) { + p.classList.remove('is-open'); + }); + } + } + }); +})(); diff --git a/app/public/assets/js/app/repertoire-student-popover.js b/app/public/assets/js/app/repertoire-student-popover.js new file mode 100644 index 0000000..011e6d6 --- /dev/null +++ b/app/public/assets/js/app/repertoire-student-popover.js @@ -0,0 +1,57 @@ +/** + * repertoire-student-popover.js — Student name popover on repertoire page. + * + * Shows a popover with HTMX-fetched student details on hover over links + * with `data-student-name` attribute. + */ +(function () { + var popover = document.getElementById('student-popover'); + var currentAnchor = null; + + function position(anchor) { + var r = anchor.getBoundingClientRect(); + var left = r.right + window.scrollX + 12; + var top = r.top + window.scrollY; + if (left + 300 > window.innerWidth + window.scrollX) { + left = r.left + window.scrollX - 312; + } + popover.style.left = left + 'px'; + popover.style.top = top + 'px'; + } + + document.body.addEventListener( + 'mouseenter', + function (e) { + var a = e.target.closest('[data-student-name]'); + if (!a) return; + currentAnchor = a; + }, + true + ); + + document.body.addEventListener('htmx:afterSwap', function (e) { + if (e.detail.target !== popover) return; + if (currentAnchor) position(currentAnchor); + popover.hidden = false; + }); + + document.body.addEventListener( + 'mouseleave', + function (e) { + if ( + !e.target.closest('[data-student-name]') && + !e.target.closest('#student-popover') + ) + return; + setTimeout(function () { + if ( + !document.querySelector('[data-student-name]:hover') && + !document.querySelector('#student-popover:hover') + ) { + popover.hidden = true; + } + }, 120); + }, + true + ); +})(); diff --git a/app/public/assets/js/app/sidebar-links-editor.js b/app/public/assets/js/app/sidebar-links-editor.js new file mode 100644 index 0000000..ca4fb50 --- /dev/null +++ b/app/public/assets/js/app/sidebar-links-editor.js @@ -0,0 +1,45 @@ +/** + * sidebar-links-editor.js — Dynamic sidebar link row add/remove on contenus-edit.php. + * + * Operates on #sidebar-links-form and #sidebar-link-tpl. + */ +(function () { + var form = document.getElementById('sidebar-links-form'); + var tpl = document.getElementById('sidebar-link-tpl'); + if (!form || !tpl) return; + var tplHtml = tpl.innerHTML; + + function reindexLinks() { + var rows = form.querySelectorAll('.sidebar-link-row'); + rows.forEach(function (row, i) { + row.querySelectorAll('input').forEach(function (inp) { + if (inp.name) { + inp.name = inp.name.replace(/links\[\d+\]/, 'links[' + i + ']'); + } + if (inp.id) { + inp.id = inp.id.replace(/sl_\d+/, 'sl_' + i); + } + }); + row.querySelectorAll('label[for]').forEach(function (lbl) { + lbl.setAttribute('for', lbl.getAttribute('for').replace(/sl_\d+/, 'sl_' + i)); + }); + }); + } + + // Event delegation for remove buttons + form.addEventListener('click', function (e) { + if (!e.target.closest('.remove-sidebar-link-btn')) return; + e.preventDefault(); + e.target.closest('.sidebar-link-row').remove(); + reindexLinks(); + }); + + // Add button + var addBtn = document.getElementById('add-sidebar-link-btn'); + addBtn.addEventListener('click', function () { + var rows = form.querySelectorAll('.sidebar-link-row'); + var idx = rows.length; + var html = tplHtml.split('{{li}}').join(idx); + this.insertAdjacentHTML('beforebegin', html); + }); +})(); diff --git a/app/public/assets/js/app/smtp-error-focus.js b/app/public/assets/js/app/smtp-error-focus.js new file mode 100644 index 0000000..dc7745d --- /dev/null +++ b/app/public/assets/js/app/smtp-error-focus.js @@ -0,0 +1,13 @@ +/** + * smtp-error-focus.js — Scrolls to and focuses the SMTP field that caused a probe + * error. Reads the field id from data-smtp-error-field on the SMTP form. + */ +(function () { + var form = document.querySelector('form[data-smtp-error-field]'); + if (!form) return; + var fieldId = form.getAttribute('data-smtp-error-field'); + var el = fieldId ? document.getElementById(fieldId) : null; + if (!el) return; + el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + el.focus(); +})(); diff --git a/app/templates/admin/acces-etudiante.php b/app/templates/admin/acces-etudiante.php index 018c9e3..cd34cf4 100644 --- a/app/templates/admin/acces-etudiante.php +++ b/app/templates/admin/acces-etudiante.php @@ -179,40 +179,7 @@ - + diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php index 1aaeb97..71d9ebb 100644 --- a/app/templates/admin/acces.php +++ b/app/templates/admin/acces.php @@ -583,76 +583,10 @@ - + + + + diff --git a/app/templates/admin/apropos-groups-form.php b/app/templates/admin/apropos-groups-form.php index f38fcea..e3c16d8 100644 --- a/app/templates/admin/apropos-groups-form.php +++ b/app/templates/admin/apropos-groups-form.php @@ -10,7 +10,9 @@ hx-post="/admin/actions/apropos.php" hx-trigger="change delay:1500ms, input delay:1500ms" hx-swap="none" - hx-on::after-request="handleAutosaveResponse(event)"> + hx-on::after-request="handleAutosaveResponse(event)" + data-apropos-key="" + data-apropos-group-count=""> @@ -93,83 +95,4 @@
- - + diff --git a/app/templates/admin/contenus-edit.php b/app/templates/admin/contenus-edit.php index c6b5ffa..fe46f0a 100644 --- a/app/templates/admin/contenus-edit.php +++ b/app/templates/admin/contenus-edit.php @@ -92,47 +92,7 @@ - +
diff --git a/app/templates/admin/contenus.php b/app/templates/admin/contenus.php index 3381811..ea50248 100644 --- a/app/templates/admin/contenus.php +++ b/app/templates/admin/contenus.php @@ -289,16 +289,9 @@ + diff --git a/app/templates/admin/parametres.php b/app/templates/admin/parametres.php index bfb707b..8898b3a 100644 --- a/app/templates/admin/parametres.php +++ b/app/templates/admin/parametres.php @@ -481,18 +481,7 @@ - + diff --git a/app/templates/admin/partials/admin-toc.php b/app/templates/admin/partials/admin-toc.php index ec7ef05..c57d9d7 100644 --- a/app/templates/admin/partials/admin-toc.php +++ b/app/templates/admin/partials/admin-toc.php @@ -12,46 +12,4 @@ - + diff --git a/app/templates/admin/tags.php b/app/templates/admin/tags.php index 6322857..651ed3b 100644 --- a/app/templates/admin/tags.php +++ b/app/templates/admin/tags.php @@ -1,89 +1,30 @@ +
diff --git a/app/templates/partials/form/form.php b/app/templates/partials/form/form.php index c3b2aeb..e2aeed0 100644 --- a/app/templates/partials/form/form.php +++ b/app/templates/partials/form/form.php @@ -642,50 +642,4 @@ if ($filesMode === 'add'): ?> - + diff --git a/app/templates/partials/form/jury-fieldset.php b/app/templates/partials/form/jury-fieldset.php index 3a7454c..753e5d1 100644 --- a/app/templates/partials/form/jury-fieldset.php +++ b/app/templates/partials/form/jury-fieldset.php @@ -63,7 +63,7 @@ if ($addMode) { } } ?> -
+
Composition du jury @@ -141,36 +141,6 @@ if ($addMode) { + Ajouter un·e promoteur·ice ULB
- - - @@ -235,21 +205,4 @@ if ($addMode) {
- + diff --git a/app/templates/partials/form/language-search.php b/app/templates/partials/form/language-search.php index 737f108..c0fdfe7 100644 --- a/app/templates/partials/form/language-search.php +++ b/app/templates/partials/form/language-search.php @@ -31,7 +31,7 @@ $maxLanguages = $maxLanguages ?? 10; $required = $required ?? false; $langCount = count($selectedLanguages); ?> -
+
*' : '' ?>
@@ -80,24 +80,6 @@ $langCount = count($selectedLanguages);
- +
- - - + + diff --git a/nginx/xamxam.conf b/nginx/xamxam.conf index 1184ae5..c58dda7 100644 --- a/nginx/xamxam.conf +++ b/nginx/xamxam.conf @@ -35,6 +35,24 @@ server { # Add index.php to the list index index.php index.html index.htm; + # ── Compression ───────────────────────────────────────────────────── + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_min_length 256; + gzip_types + text/plain + text/css + text/javascript + application/javascript + application/json + application/xml + text/xml + image/svg+xml + application/x-font-ttf + font/opentype; + # Security headers add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload;" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';" always;