commit 7784ed070a869e15fde6d8887e84f69b39308eb7 Author: Valentin Le Moign Date: Tue May 12 23:32:48 2026 +0200 Initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..55be4e5 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Identifiants MySQL — utilisés par les conteneurs db et wordpress. +# Copier ce fichier en .env et remplir avec de vraies valeurs avant `docker compose up`. + +MYSQL_ROOT_PASSWORD=changeme-root +MYSQL_DATABASE=thalim +MYSQL_USER=thalim +MYSQL_PASSWORD=changeme diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00db0d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# --- Secrets --- +.env +.env.local +.env.*.local + +# --- WordPress install --- +wp-data/ + +# --- Outils locaux --- +.claude/ +.idea/ +.vscode/ + +# --- Archives / dumps --- +*.tar.gz +*.tgz +*.zip +*.sql +*.sql.gz + +# --- OS / éditeur --- +.DS_Store +Thumbs.db +*.swp +*~ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..522b6d2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,209 @@ +# CLAUDE.md + +Guidance pour Claude Code sur ce dépôt. + +## Project overview + +Site WordPress du laboratoire THALIM (Théorie et Histoire des Arts et des Littératures de la Modernité). Stack Docker + thème Timber/Twig customisé + deux plugins maison (HAL importer, Newsletter). Le site est bilingue FR/EN via un système maison (Polylang a été retiré). + +## Dev environment + +```bash +docker-compose up -d # WordPress, MySQL, phpMyAdmin +docker-compose down +``` + +- WordPress : http://localhost:8020 +- phpMyAdmin : http://localhost:8021 +- MySQL : `localhost:3307` → `db:3306` +- Identifiants dans `.env` +- WordPress monté depuis `./wp-data/` + +## Architecture + +### Thème : `wp-data/wp-content/themes/thalim/` + +Timber/Twig. Chaque `*.php` charge un Twig de `templates/`. `base.twig` est le layout, les autres l'étendent. + +- `functions.php` — gros (≈1400 lignes) : setup, i18n, contexte Twig, AJAX, filtres de requête, customisations admin +- `inc/` — helpers PHP par contexte (voir détails plus bas) +- `templates/` + `templates/partials/` — Twig +- `scss/` → `css/` — SASS compilé **manuellement** par l'utilisateur (CSS commité) +- `js/` — scripts frontend et `adminDashboardMods.js` (admin) +- `vendor/` — Composer (Timber 2.x). `composer install` après clone + +### Plugins maison + +- **`thalim-hal-importer/`** — import publications HAL (structure 254015). Admin : Outils → HAL Import. Voir le README du plugin pour le mapping doc types → catégories (10 types : ART, COUV, OUV, COMM, ISSUE, PROCEEDINGS, THESE, HDR, SON, VIDEO). +- **`thalim-newsletter/`** — composition et export HTML des digests mensuels. Admin : Outils → Newsletter. Voir le README du plugin pour les constantes de catégories. + +### Plugins WP requis + +- **Pods** — types de contenu + champs personnalisés (tout est en Pods) +- **Members** — gestion fine des rôles/capacités +- **Simple Local Avatars** — avatars uploadés (vs Gravatar) +- **Relevanssi** — recherche + +## Multilingue (système maison) + +Polylang a été **remplacé** par un système custom. Toute la logique vit dans `functions.php`. + +- **Détection** : `thalim_current_language()` retourne `'en'` si `THALIM_ORIGINAL_URI` commence par `/en/`, sinon `'fr'`. Le préfixe `/en/` est strippé via `do_parse_request` avant la résolution d'URL WP, et `redirect_canonical` est désactivé sur les URLs `/en/` pour éviter que WP ne supprime le préfixe +- **Champs texte bilingues** : convention `"FR // EN"` dans un même champ. `thalim_bilingual($value, $lang)` (exposé comme filtre Twig `|bilingual`) renvoie la bonne moitié. Utilisé partout : titres de posts, noms de termes de taxonomie, sous-titres, lieux, fonctions, types, etc. +- **Champs longs séparés** : pour le corps des posts, c'est un champ Pods `body_en` à part. Idem pour `biographie_en`, `presentation_en`, `recherches_en_cours_en`, `resume_de_la_these_en`, `autres_domaines_de_recherches_en` sur les utilisateurs, et `presentation_en` / `presentation_detail_en` sur le post `contenu_general` +- **URLs préfixées** : `thalim_en_url($url)` ajoute `/en` aux URLs internes en mode EN, attaché à `term_link`, `post_link`, `page_link`, `post_type_link`, `author_link`. Filtre Twig `|en_url` aussi disponible. No-op en admin (URI commence par `/wp-admin/`) +- **Noms de catégories** : `thalim_cat_name($cat)` (filtre Twig `|cat_name`) lit le term meta `titre_anglais` en mode EN. Aussi appliqué au title de l'onglet navigateur via `document_title_parts` +- **Menus** : slugs `Navigation` / `Navigation-en` et `Footer` / `Footer-en` (pas `menu-principal` — c'est un legacy obsolète) +- **Contenu général** : un seul post de type `contenu_general` fournit les blocs UMR/thalim/siècles/présentation au layout via `gc` dans le contexte Twig +- **Sélecteur de langue** : `thalim_language_switcher()` retourne `['fr' => ['slug','url','current_lang'], 'en' => …]` — remplace `pll_the_languages()` +- **Override pour AJAX** : poser `$GLOBALS['thalim_lang_override'] = $lang` avant `thalim_current_language()` (utilisé par les handlers `load_more_posts` / `load_more_agenda` qui reçoivent la langue en POST) + +## Dates et tri par événement + +Les annonces ont une `date_de_debut` / `date_de_fin` (champ date Pods) ou un `datetime` (communications). Les listes et l'agenda doivent **prioriser ces dates sur `post_date`**. + +- **Activation** : ajouter `'thalim_event_date_order' => true` aux args d'un `WP_Query` (ou Timber) +- **Filtre de plage** : `'thalim_event_date_filter' => ['from' => 'YYYY-MM-DD', 'to' => 'YYYY-MM-DD']` +- **Implémentation** : filtres `posts_join` + `posts_orderby` + `posts_where` dans `functions.php`. LEFT JOIN sur `postmeta` puis `CASE WHEN date_de_debut ELSE datetime ELSE post_date END DESC`. Les valeurs `0000-00-00...` sont traitées comme NULL +- **Format d'affichage** : `thalim_format_date($raw, $lang)` dans `inc/single-helpers.php` renvoie `date_i18n('j F Y', $ts)`. Les abréviations de mois (3-lettres) pour les vignettes agenda sont codées en dur dans `functions.php` (`thalim_get_agenda_card_data()`) et dans `inc/single-helpers.php` (séances) +- **Construction du `date_label`** : la logique « Le X de H1 à H2 / Du X au Y / Jusqu'au X / X à H » vit dans `thalim_get_agenda_card_data()` — à dupliquer prudemment si besoin ailleurs + +## Helpers PHP (`inc/`) + +| Fichier | Rôle | +|---|---| +| `single-helpers.php` | `thalim_get_single_data($post_id)` résout tous les champs Pods d'un post en tableau prêt pour Twig : dates formatées, images vs documents (split par mime-type), membres résolus en `{name, url}`, taxonomies (axes, étiquettes, programmes), séances triées en `seances_a_venir` / `seances_passees`, hiérarchie de catégorie, `type_label` et `fonction_label` dérivés des champs `type_*` / `fonction_*` ou (legacy) du `_pods_categorie`, liens externes (1–3), Canal-U / YouTube embeds. Inclut aussi `thalim_format_date()` (partagée). | +| `author-helpers.php` | `thalim_get_author_data($user_id)` (profil membre) et `thalim_get_author_posts_by_category($user_id)` (posts liés au membre, groupés par catégorie primaire, plus un groupe spécial « séances de séminaire » via cat 12). Tri inter-groupes par `ordre_profil` (term meta) puis par nombre de posts | +| `membres-helpers.php` | Page `/membres` : `thalim_get_membres_groups()` regroupe par slugs de rôle taxonomy (mapping codé en dur dans `$group_definitions` — voir « Page membres » plus bas) | +| `post-card-helpers.php` | `thalim_get_card_data($post_id)` / `thalim_get_cards_data($posts)` : données pour `partials/post-card.twig`. Inclut la résolution catégorie parente pour le code couleur (`parent_slug`), la première image (medium), la `card_event_date`, et la redirection `#seance-{ID}` pour les séances de séminaire | +| `pods-conditional-required.php` | Patch : Pods n'évalue pas sa propre logique conditionnelle côté serveur lors de la validation des champs *required*. On la rejoue dans `pods_api_pre_save_pod_item_post` et on désactive `required` sur les champs masqués | +| `pods-save-error-handler.php` | Quand Pods déclenche un `wp_die()` à la sauvegarde admin, intercepter (`pods_error_die`), stocker tous les `pods_meta_*` dans un transient, rediriger vers `post.php?action=edit`, annuler le statut si le post passait à `publish`, et restaurer les champs côté React via `get_post_metadata` + JS dans `admin_footer`. **Mécanisme partagé** avec : | +| `post-title-required.php` | Force un titre non-vide à la sauvegarde, réutilise le même mécanisme transient/restore | +| `admin-users-filter.php` | Dropdown « Statut » sur `users.php`, filtre via meta `role_1`/`role_2`/`role_3` | + +### Avatars (chaîne de fallback) + +`thalim_get_user_avatar_url($user_id)` dans `functions.php` : + +1. Simple Local Avatar (`simple_local_avatar` user meta) — résolu via `media_id` quand dispo (pour survivre aux changements de domaine), sinon URL réécrite +2. Gravatar (HEAD request avec `d=404`) — résultat (positif ou négatif) caché 1 semaine en transient `thalim_gravatar_{ID}` +3. Chaîne vide → templates fallback initiales + +## Pages spécifiques + +### Page Membres (`page-membres.php`) + +Pilotée par `inc/membres-helpers.php`. + +- Groupes basés sur les **slugs** de la taxonomy `role` (pas les term IDs — survit aux migrations) +- Groupe « Direction » en tête, ordre fixe : `directeur` puis `directeur_adjoint`, lus depuis les post meta de la page `le-laboratoire` +- Tri intra-groupe alphabétique sur le nom de famille via `Collator('fr_FR')` à `PRIMARY` (insensible accents/casse). Pour les noms composés (« Duclaux de l'Estoile »), `thalim_get_sort_key()` prend le premier mot du meta `last_name` +- Cas spécial « Personnel d'accompagnement à la recherche » : les statuts contenant « Gestion et pilotage » remontent en tête +- Chaque `` porte `data-name`, `data-status`, `data-affiliation`, `data-roles` (séparés par `|`), `data-avatar`, `data-domaines`, `data-autres-domaines` — consommés par `membresFilters.js` (filtre rôle + recherche texte sur name/status/affiliation, tri colonne), `membresPopover.js` (popover au hover), `seanceToggle.js` (collapse de groupe) +- **Zébrage** : classe `.is-even-row` posée en JS après chaque filtre/tri — pas `nth-of-type`, qui compte les lignes masquées et casse l'alternance + +### Single post + +`single-helpers.php` fait quasi tout le travail. À noter : + +- Hiérarchie de catégorie : `wp_get_post_categories()` → exclut cat 12 (séance) et 31, prend les ancêtres pour `parent_slug` (color theme) et `parent_name` +- Posts en catégorie racine sans sous-catégorie → leur lien vers la liste devient `/category/{slug}/autres/` (rewrite rule dédiée) +- `documents_joints` (Pods relationship file) est splitté par mime-type en `images` (taille `large`, marqué `portrait` si `h > w`) et `documents`. Les captions et titres passent par `|bilingual` +- Toggle « afficher le titre des images en légende » : meta `afficher_le_titre_des_images_en_legende` +- Séances de séminaire (`seances` meta = array de post IDs) sont **redirigées via `template_redirect`** vers le parent séminaire avec ancre `#seance-{ID}`. Les helpers post-card et agenda-card font la même substitution pour les liens + +### Front page + +`is_front_page()` charge : logo animé (`animatedLogo.js`, aussi sur 404), hero avec mots colorés (`coloredWordsHero.js`), Swiper d'annonces (`annoncesSwiper.js`), `messageLabo.js`, nuage de mots-clés (`keywordCloud.js`, exclut les tags marqués `ne_pas_afficher_dans_le_nuage`), `quickLinks.js`. Tags localisés dans `thalimTags` (window) via `wp_localize_script`. + +### Catégorie / archives / agenda + +`is_category()` charge Swiper + `agendaView.js` (slider d'événements à venir, scroll horizontal centré sur aujourd'hui via `today_offset` renvoyé par l'AJAX). + +Toutes les pages d'archives (catégorie, taxonomie, tag, `/annonces`, search) chargent : +- `infiniteScroll.js` — AJAX paginé via `wp_ajax_load_more_posts`. Params POST : `page`, `category`, `axe`, `date_from`/`date_to`, `taxonomy`/`term`, `filter_cat`/`filter_autres`, `exclude_cats`, `search`, `lang`. Recherche passe par Relevanssi (`'relevanssi' => true`, `orderby => 'relevance'`) +- `categoryFilters.js` — UI filtres + +Sur les pages de catégorie, les **posts épinglés** sont sortis du flux principal (`epingler_dans_la_categorie` = `1` ET (`date_de_fin_depinglage` vide OU ≥ aujourd'hui)) — affichés en tête, exclus de la pagination AJAX via `post__not_in`. + +Sur les pages de taxonomie/tag, les **séances de séminaire (cat 12)** sont exclues : on liste les posts taggés, pas leurs séances. + +## Restrictions / sécurité contenus + +- **Vie du labo (cat 9)** : exclue des requêtes pour les non-connectés via `pre_get_posts`, redirige vers `/` si on visite la page de catégorie, retirée du menu via `wp_nav_menu_objects` +- **Contributeurs** : + - Liste admin filtrée : posts qu'ils ont écrits **OU** où ils sont dans `membres`/`autre_membres` (filtre `pre_get_posts` + override de `wp_count_posts` pour des compteurs cohérents) + - Peuvent **éditer** les posts où ils figurent comme membre (filtre `user_has_cap` — accorde les caps primitives le temps du check, sans modifier la table des rôles). Gère le flow save : capture `post_ID` depuis `$_POST` quand WP teste `edit_others_posts` sans `$args[2]` + - Login redirige vers `edit.php` (pas le dashboard). Dashboard et menu Outils masqués pour les non-admins +- **Filtre catégorie admin** : par défaut WP inclut les sous-catégories, on force `include_children => false` (`thalim_exact_category_filter`) +- **Axes filtres** : pour les contributeurs, restreints au premier groupe (le plus récent) via `thalim_get_axes_filter_groups()` slice + +## Customisations admin + +`js/adminDashboardMods.js` (≈900 lignes) + `css/admin.css`. L'admin est très modifié. + +### Patterns transversaux + +- **Cacher des éléments** : `css/admin.css` avec `display: none !important`. Pour les boutons TinyMCE, **sélecteurs stables uniquement** (`aria-label`, `.mce-i-bold`, `.mce-i-italic`) — jamais `#mceu_*` qui dépendent du nombre d'éditeurs sur la page +- **Force du mode visuel** : filtre PHP `user_can_richedit` → `true` (sinon les users avec « désactiver l'éditeur visuel » dans leur profil sont bloqués en mode code, car nos CSS masquent les onglets Visual/Code). `ensureVisualMode(editorId)` (JS) retry jusqu'à 15× pour passer de `html-active` à `tmce-active` +- **TinyMCE sur champs conditionnels** : un éditeur initialisé dans un élément `display:none` a son iframe cassée (vide, non interactive). `reinitEditor(editorId)` détruit et recrée l'instance quand le champ devient visible. Utilisé pour `body_en` (au premier clic onglet EN) et pour `reference_bibliographique` (via MutationObserver sur le `style` de la ligne Pods) +- **Rename « Article » → « Annonce »** : `renameArticlesToAnnonces()` parcourt les nœuds texte des éléments connus (menu, h1, notices, submitbox, screen options, search) plus l'admin bar (côté PHP). Réplique de plusieurs casses + +### Page d'édition de post + +`initPostEditPage()` orchestre : + +- Désactivation d'options de catégorie (`1`, `12`, `5`, `20`) dans le select Pods +- Réordonnancement des metaboxes : `type-dannonce` en haut du `#side-sortables`, `affichage-sur-laccueil` et `thematique` en bas, `champs-contextuels` en haut du `#normal-sortables`, `documents-joints` juste après, `submitdiv` en bas +- **Onglets FR/EN sur le corps** : barre de tabs injectée avant `#postdivrich`. La metabox `#pods-meta-body-en` est déplacée juste après l'éditeur natif (le déplacement DOM casse son iframe TinyMCE — réparé par `reinitEditor()` au premier clic EN). Phase 1 = DOM avant `t=100ms`, Phase 2 = wiring après +- **Groupement des axes thématiques** : la liste des cases à cocher est regroupée par période (label = `annee_debut – annee_fin` ou « Axes antérieurs »), via `groupAxesCheckboxes()`. Données passées en `thalimAxesGroups` via `wp_localize_script`. Re-grouping déclenché si la ligne devient visible (MutationObserver) +- **Visibilité conditionnelle des Pods boxes** : `updatePostboxVisibility()` cache une `.postbox` Pods si tous ses `` ont `display:none` (sauf `body-en` qui est piloté par les onglets). Re-déclenché au changement de catégorie +- **Séparateur dans la grille Membres** : `` injecté en fin de tbody, affiché uniquement si une ligne `autre_*` est visible +- **Popover date picker Gutenberg** : le composant Popover de Gutenberg se ferme sur outside-click via `focusout`, mais si le focus n'y entre jamais, il reste ouvert. `initDatePickerPopoverFix()` force le focus dès que `.components-popover__content` apparaît dans le DOM +- **Info-popovers** (`INFO_TIPS` array) : boutons d'aide attachés à des sélecteurs spécifiques. Type `info` (cercle, neutre) ou `translate` (globe, vert) pour rappeler la convention `FR // EN`. Champ `page` (`post` | `user` | `taxonomy`) limite le scope + +### Page de profil utilisateur + +`initProfileEditors()` : + +- Réordonne les sections natives WP en lignes deux-colonnes (`PROFILE_ORDER`) +- Masque le `

` « À propos du compte » +- Renomme le label `Rôle` en `Rôle sur le site` +- Force le mode visuel sur tous les WYSIWYG Pods (`load` du window) +- `do_action('show_user_profile', new WP_User(0))` sur `user-new.php` pour que les champs Pods s'affichent à la création d'utilisateur. `user_register` rejoue `personal_options_update` pour sauver ces champs avec le nouvel ID +- Auto-sync `display_name` ← `first_name + last_name` à chaque save (`profile_update`, `user_register`, et hook Pods) + +### Pods modal (création de séance via popup) + +Quand l'URL contient `pods_modal`, la catégorie est verrouillée sur `12` (séance de séminaire) et le select est désactivé pour les autres valeurs. + +### Colonnes admin custom + +- Taxonomy `programme_de_recherche` : remplace « Description » par « Type de programme » (term meta `type_de_programme`). Select de filtre injecté côté JS dans la search-form, filtré côté serveur via `pre_get_terms` +- Taxonomy `post_tag` : remplace « Description » par « Exclure du nuage » (term meta `ne_pas_afficher_dans_le_nuage`) + +### Autres tweaks admin + +- Champ `etiquettes` (post_tag pick) : autocomplete déplafonnée (filtre `pods_form_ui_field_pick_autocomplete_limit` retourne `-1`) +- Admin bar : retire `wp-logo`, `customize`, `wpforms-menu`, et rename « article » → « annonce » dans tous les nodes + +## Search panel (`templates/partials/search-panel.twig`) + +Réutilisée dans `search.twig`. L'icône loupe `iconoir-search` est un `