Initial commit

This commit is contained in:
2026-05-12 23:33:46 +02:00
commit ccf32dcece
104 changed files with 17439 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
# --- Dépendances Composer ---
vendor/
# --- Artefacts SASS ---
.sass-cache/
*.css.map
# --- Node ---
node_modules/
# --- OS / éditeur ---
.DS_Store
Thumbs.db
*.swp
*~
.idea/
.vscode/

3
404.php Normal file
View File

@@ -0,0 +1,3 @@
<?php
$context = Timber::context();
Timber::render('404.twig', $context);

86
README.md Normal file
View File

@@ -0,0 +1,86 @@
# thalim-theme
Thème WordPress personnalisé du laboratoire **THALIM** (UMR 7172). Basé sur [Timber 2.x](https://timber.github.io/docs/) (templating Twig pour WordPress), avec un système maison de bilinguisme FR/EN, une admin très customisée, et une logique de tri par date d'événement pour les annonces.
- **Version :** 1.0.0
- **Requires WordPress :** 6.0+
- **Requires PHP :** 7.4+
## Installation
Le thème est conçu pour être cloné dans le `wp-content/themes/` d'une installation WordPress. Dans le cadre du projet THALIM, le clonage est automatisé par le script `bootstrap.sh` du repo [`thalim-stack`](https://figureslibres.io/valentin_le_moign/thalim-stack).
Manuellement :
```bash
cd wp-content/themes
git clone gitea@figureslibres.io:valentin_le_moign/thalim-theme.git thalim
cd thalim
composer install
```
Puis activer le thème depuis l'admin WordPress.
## Plugins requis
- [Pods](https://wordpress.org/plugins/pods/) — types de contenu et champs personnalisés (toute la structure éditoriale du site est en Pods)
- [Members](https://wordpress.org/plugins/members/) — gestion fine des rôles et capacités
- [Simple Local Avatars](https://wordpress.org/plugins/simple-local-avatars/) — avatars uploadés (utilisés en priorité, Gravatar en fallback)
- [Relevanssi](https://wordpress.org/plugins/relevanssi/) — moteur de recherche
## Fonctionnalités notables
### Bilinguisme FR/EN maison
- Détection de la langue via le préfixe d'URL `/en/` (`thalim_current_language()`)
- Convention `"FR // EN"` dans un même champ pour les textes courts (titres, lieux, sous-titres, options Pods pick…) — split par le filtre Twig `|bilingual`
- Champs Pods séparés `*_en` pour les textes longs (`body_en`, `biographie_en`, `presentation_en`…)
- Auto-préfixage des URLs internes en mode EN via les filtres `term_link`, `post_link`, `page_link`, `post_type_link`, `author_link`
- Sélecteur de langue : `thalim_language_switcher()`
- Menus : `Navigation` / `Navigation-en` et `Footer` / `Footer-en`
### Tri par date d'événement
Les annonces avec `date_de_debut` / `datetime` (champs Pods) sont triées sur cette date plutôt que `post_date`, via `'thalim_event_date_order' => true` sur les args de `WP_Query`. Filtre de plage : `'thalim_event_date_filter' => ['from' => …, 'to' => …]`.
### Admin très customisée
- Onglets FR/EN sur l'éditeur de corps de post (`body` natif WP + `body_en` Pods)
- Renommage « Article » → « Annonce » dans toute l'UI
- Visibilité conditionnelle des metaboxes Pods + groupement des axes thématiques par période
- Restauration automatique des champs en cas d'erreur de validation Pods (transient + JS)
- Restrictions des contributeurs (édition limitée aux posts où ils figurent comme membre)
- Dashboard et menu Outils masqués pour les non-admins
- Voir `CLAUDE.md` côté stack pour le détail
### AJAX et infinite scroll
Les pages d'archives utilisent un système d'infinite scroll AJAX (`wp_ajax_load_more_posts`) avec recherche Relevanssi, filtres par axe / date / taxonomie / catégorie, et override de langue côté serveur.
## Compilation des styles
Les fichiers SCSS dans `scss/` sont compilés **manuellement** vers `css/`.
## Structure
```
.
├── functions.php # setup, i18n, contexte Twig, AJAX, filtres de requête, mods admin (≈1400 lignes)
├── index.php, single.php … # templates PHP qui chargent les Twig correspondants
├── templates/ # templates Twig (base.twig = layout, autres l'étendent)
│ └── partials/ # header, footer, post-card, agenda-card, search-panel…
├── scss/ → css/ # sources SASS → CSS compilé (commité)
├── js/ # scripts frontend + adminDashboardMods.js
├── inc/ # helpers PHP par contexte :
│ ├── single-helpers.php # résolution champs Pods d'un post
│ ├── author-helpers.php # profil membre + posts liés
│ ├── membres-helpers.php # page /membres (groupes par rôle)
│ ├── post-card-helpers.php # données pour les cards
│ ├── pods-conditional-required.php # patch validation Pods
│ ├── pods-save-error-handler.php # restauration des champs en cas d'erreur
│ ├── post-title-required.php # titre obligatoire
│ └── admin-users-filter.php # filtre Statut sur /wp-admin/users.php
├── assets/ # fonts, images, logo-shapes (SVG)
├── composer.json # dépendance : timber/timber ^2.3
└── vendor/ # Composer (gitignoré, à reconstruire après clone)
```
## Architecture détaillée
Voir le fichier `CLAUDE.md` à la racine du repo [`thalim-stack`](https://figureslibres.io/valentin_le_moign/thalim-stack) pour la documentation exhaustive (conventions Pods, customisations admin, restrictions de contenu, helpers, rewrite rules, etc.).

Binary file not shown.

Binary file not shown.

BIN
assets/images/cnrs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
assets/images/ens.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/images/sorbonne.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="170.98988mm"
height="129.68549mm"
viewBox="0 0 170.98988 129.68549"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="logo-thalim-inkscape.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.30295176"
inkscape:cx="-523.18561"
inkscape:cy="2751.2631"
inkscape:window-width="1728"
inkscape:window-height="1108"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"><inkscape:page
x="0"
y="-1.517571e-14"
width="170.98988"
height="129.68549"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1"><linearGradient
id="swatch1"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop1" /></linearGradient></defs><g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="Layer 4"
transform="translate(476.37288,-54.015086)"><path
d="m -351.91056,165.85995 h -52.50622 V 67.507728 c 0,0 8.8076,-12.737588 26.73354,-12.960949 16.61076,-0.206875 25.06867,12.315648 25.06867,12.315648 l 0.70401,98.997523"
fill="url(#fill-0-render-5)"
id="path9"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.05833;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-filename="/home/val/code/test_thalim_hero_header/logos/thalim-logo-bolder-bolder.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" /><path
d="m -426.30884,183.17725 -8.34311,-120.774255 63.88462,7.799753 9.05832,103.284002 -64.59983,9.6905"
fill="url(#fill-0-render-3)"
id="path10"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.05833;stroke-dasharray:none;stroke-opacity:1" /><path
d="m -317.73563,85.200571 -4.02352,77.834909 -154.0598,-8.0928 3.78594,-78.549023 154.29738,8.806914"
fill="url(#fill-0-render-2)"
id="path11"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.05833;stroke-dasharray:none;stroke-opacity:1" /><ellipse
cx="-406.18973"
cy="132.90717"
rx="71.197098"
ry="8.8586254"
transform="rotate(-3.0000013)"
fill="url(#fill-0-render-1)"
id="ellipse11"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.05833;stroke-dasharray:none;stroke-opacity:1" /><path
d="M -460.51897,146.56124 V 86.474302 l 154.60354,-19.54642 -0.475,86.631298 -154.12854,-6.99794"
fill="url(#fill-0-render-4)"
id="path12"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.05833;stroke-dasharray:none;stroke-opacity:1" /><path
id="path13"
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.05833;stroke-dasharray:none;stroke-opacity:1"
d="m -321.37051,153.07232 3.49911,-67.684754 -91.43175,-5.218878"
sodipodi:nodetypes="ccc" /><path
style="font-size:172px;font-family:'News Cycle';-inkscape-font-specification:'News Cycle';white-space:pre;stroke-width:2"
d="m 804.12695,-143.04834 v -9.40625 h 14.94922 v -32.33399 h 11.08594 v 32.33399 h 20.32422 v 9.40625 h -20.32422 v 56.185543 q 0,8.482422 2.60351,11.589844 2.60352,3.107422 8.56641,3.107422 2.43555,0 5.79492,-0.419922 3.35938,-0.419922 3.35938,-0.503906 v 8.314453 q -5.54297,2.015625 -10.83399,2.015625 -5.20703,0 -7.13867,-0.251953 -1.93164,-0.251953 -4.70312,-1.595703 -2.77149,-1.34375 -4.53516,-3.779297 -4.19922,-5.878906 -4.19922,-18.476563 v -56.185543 z m 58.70508,79.197262 V -191.08741 h 11.08594 v 53.16211 q 12.42969,-11.84179 20.4082,-14.27734 4.11524,-1.17578 8.56641,-1.17578 9.6582,0 15.87304,5.8789 6.21485,5.87891 6.21485,17.63672 v 66.011722 H 913.89453 V -129.8628 q 0,-6.29882 -4.53516,-10.16211 -4.45117,-3.94726 -9.6582,-3.94726 -12.42969,0 -25.7832,14.5293 v 65.591792 z m 77.43359,-21.583984 q 0,-6.046875 1.51172,-10.498047 1.51172,-4.535161 4.70313,-7.978511 3.27539,-3.52735 7.39062,-6.21485 4.11524,-2.6875 10.2461,-4.87109 11.16992,-3.94727 27.29492,-7.13867 0,-13.26954 -5.12305,-17.72071 -4.53515,-3.94726 -13.18554,-3.94726 -10.58204,0 -18.22461,8.0625 -3.27539,3.27539 -4.53516,5.8789 l -7.97852,-5.29101 q 9.40625,-18.05664 31.99805,-18.05664 20.24024,0 25.86722,15.53711 2.2675,6.29882 2.2675,15.28515 v 46.107425 q 0,2.435547 0.9239,6.634765 0.9238,4.199219 1.6797,5.794922 h -11.16996 q -2.51953,-1.259765 -2.51953,-14.109375 -11.8418,15.201172 -26.11914,15.201172 -15.78906,0 -21.91992,-11.085937 -3.10743,-5.626954 -3.10743,-11.589844 z m 13.18555,-9.658203 q -2.09961,3.863281 -2.09961,10.078125 0,6.214843 4.36719,9.574218 4.36719,3.275391 9.6582,3.275391 5.375,0 9.40625,-1.595703 4.11524,-1.595703 7.47461,-4.115234 6.13086,-4.619141 9.1543,-8.230469 v -26.623043 q -15.95703,4.11523 -18.89649,5.03906 -14.78125,4.78711 -19.06445,12.597655 z m 69.53903,31.410156 V -191.08741 h 11.086 v 127.404301 z m 34.2657,-99.941411 v -12.59765 h 12.5976 v 12.59765 z m 0.7558,99.773442 v -88.603512 h 11.086 v 88.603512 z m 34.2656,0 v -88.603512 h 11.086 v 14.61328 q 11.5898,-11.58985 20.4082,-14.27735 4.1152,-1.25976 10.3301,-1.25976 6.2148,0 11.9257,4.03125 5.795,4.03125 7.6426,12.17773 12.1778,-12.17773 20.9121,-14.86523 4.1153,-1.34375 8.7344,-1.34375 9.6582,0 15.8731,5.8789 6.2148,5.87891 6.2148,17.63672 v 66.011722 h -11.0859 V -129.8628 q 0,-6.29882 -4.5352,-10.16211 -4.4512,-3.94726 -9.6582,-3.94726 -12.3457,0 -25.6992,14.5293 v 65.591792 h -11.086 V -129.8628 q 0,-6.29882 -4.5351,-10.16211 -4.4512,-3.94726 -9.6582,-3.94726 -12.4297,0 -25.7832,14.5293 v 65.591792 z"
id="text14"
transform="matrix(0.22608231,0,0,0.22608231,-618.79063,142.94722)"
aria-label="thalim" /></g></svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="142.78297mm" height="72.903015mm" viewBox="0 0 142.78297 72.903015" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(421.12105,-208.44674)">
<path d="m 3501.8495,-1061.6392 -33.7838,4.271 v 39.53 l 91.6323,4.1605 2.3019,-44.5285 z" transform="matrix(1.520032,0,0,1.520032,-5692.6919,1822.1723)" fill="#ffffffee" stroke="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 424 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="53.564522mm" height="112.37409mm" viewBox="0 0 53.564522 112.37409" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(448.53132,-700.6753)">
<g transform="matrix(1.0501243,0,0,1.0501243,-4112.9365,1981.3086)">
<path d="m 3540.0005,-1113.0001 h -50 v -93.6577 c 0,0 8.3872,-12.1296 25.4575,-12.3423 15.8179,-0.197 23.8721,11.7278 23.8721,11.7278 l 0.6704,94.2722" fill="none" stroke="#000000" stroke-width="1.428"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 525 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="143.26076mm" height="20.26207mm" viewBox="0 0 143.26076 20.26207" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(377.25504,-763.4427)">
<g transform="matrix(0.65619448,0,0,0.65619448,-376.82181,764.71516)">
<ellipse cx="108.5" cy="13.5" rx="108.5" ry="13.5" transform="rotate(-3.0000011,108.50006,13.499027)" fill="none" stroke="#000000" stroke-width="2.286"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 474 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="159.16571mm" height="87.756729mm" viewBox="0 0 159.16571 87.756729" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(220.90156,-725.07326)">
<g transform="matrix(1.520032,0,0,1.520032,-5476.6183,2342.946)">
<path d="M 3562,-1058.2061 3559.353,-1007 3458,-1012.3241 3460.4907,-1064 3562,-1058.2061" fill="none" stroke="#000000" stroke-width="0.987"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="74.08564mm" height="121.90051mm" viewBox="0 0 74.08564 121.90051" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(27.64622,-702.0151)">
<g transform="matrix(1.1957859,0,0,1.1957859,-4339.0775,1997.654)">
<path d="m 3612.9766,-982 -6.9771,-100.9999 53.4248,6.5227 7.5752,86.37332 -54.0229,8.10388" fill="none" stroke="#000000" stroke-width="1.254"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="155.66518mm" height="87.785599mm" viewBox="0 0 155.66518 87.785599" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-78.011869,-718.14361)">
<g transform="matrix(1.3327892,0,0,1.3327892,-4692.8443,2295.4339)">
<path d="m 3580,-1123.2506 v -45.0836 l 116,-14.6658 -0.3564,65 -115.6436,-5.2506" fill="none" stroke="#000000" stroke-width="1.125"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 457 B

16
author.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
$context = Timber::context();
$user = get_queried_object();
if (!($user instanceof WP_User)) {
global $wp_query;
$wp_query->set_404();
status_header(404);
return;
}
$context['author'] = thalim_get_author_data($user->ID);
$context['author_posts'] = thalim_get_author_posts_by_category($user->ID);
$context['author_edit_link'] = current_user_can('edit_user', $user->ID) ? get_edit_user_link($user->ID) : '';
Timber::render('author.twig', $context);

272
category.php Normal file
View File

@@ -0,0 +1,272 @@
<?php
$context = Timber::context();
$category = get_queried_object();
$context['category'] = Timber::get_term($category);
$context['cards'] = [];
$excluded_ids = [12, 31]; // Séance de séminaire, Non classé
if ( ! is_user_logged_in() ) $excluded_ids[] = 9; // Vie du labo
// Parent category slug for color theming
if ($category->parent) {
$parent_cat = get_category($category->parent);
$context['parent_slug'] = $parent_cat->slug;
$context['active_rubrique'] = $parent_cat->term_id;
} else {
$context['parent_slug'] = $category->slug;
$context['active_rubrique'] = $category->term_id;
}
// Read filter query params
$active_axe = isset($_GET['axe']) ? intval($_GET['axe']) : 0;
$active_date_from = isset($_GET['date_from']) ? sanitize_text_field($_GET['date_from']) : '';
$active_date_to = isset($_GET['date_to']) ? sanitize_text_field($_GET['date_to']) : '';
$context['active_axe'] = $active_axe;
$context['active_date_from'] = $active_date_from;
$context['active_date_to'] = $active_date_to;
// Build query param string to preserve across filter links
$filter_query = http_build_query(array_filter([
'axe' => $active_axe ?: null,
'date_from' => $active_date_from ?: null,
'date_to' => $active_date_to ?: null,
]));
$context['filter_query'] = $filter_query;
// Build extra query args for axe/date filtering
$extra_query_args = [];
if ($active_axe) {
$extra_query_args['meta_query'] = [[
'key' => 'axes_thematiques',
'value' => $active_axe,
'type' => 'NUMERIC',
]];
}
if ($active_date_from || $active_date_to) {
$extra_query_args['thalim_event_date_filter'] = ['from' => $active_date_from, 'to' => $active_date_to];
}
// Build parent categories for filter bar (with links)
$all_cats = get_categories([
'taxonomy' => 'category',
'hide_empty' => false,
'exclude' => $excluded_ids,
]);
$filter_parents = [];
foreach ($all_cats as $cat) {
if ($cat->parent == 0) {
$link = get_category_link($cat->term_id);
if ($filter_query) $link .= '?' . $filter_query;
$filter_parents[] = [
'id' => $cat->term_id,
'name' => thalim_cat_name($cat),
'slug' => $cat->slug,
'link' => $link,
];
}
}
$context['filter_parents'] = $filter_parents;
// Children of active rubrique for catégorie filter (with links)
$active_rubrique_id = $context['active_rubrique'];
$is_direct = (bool) get_query_var('thalim_direct_posts');
$lang = thalim_current_language();
$filter_categories = [];
foreach ($all_cats as $cat) {
if ($cat->parent == $active_rubrique_id) {
$link = get_category_link($cat->term_id);
if ($filter_query) $link .= '?' . $filter_query;
$filter_categories[] = [
'id' => $cat->term_id,
'name' => thalim_cat_name($cat),
'slug' => $cat->slug,
'link' => $link,
];
}
}
// Add "Autres" entry if the active rubrique has posts directly assigned to it
if ($is_direct) {
$has_direct_posts = true;
} else {
$direct_check = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 1,
'fields' => 'ids',
'no_found_rows' => true,
'lang' => '',
'tax_query' => [[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [$active_rubrique_id],
'include_children' => false,
]],
]);
$has_direct_posts = $direct_check->have_posts();
}
if ($has_direct_posts && !empty($filter_categories)) {
$autres_link = trailingslashit(get_category_link($active_rubrique_id)) . 'autres/';
if ($filter_query) $autres_link .= '?' . $filter_query;
$filter_categories[] = [
'id' => 'autres',
'name' => $lang === 'en' ? 'Other' : 'Autres',
'slug' => 'autres',
'link' => $autres_link,
];
}
$context['filter_categories'] = $filter_categories;
$context['active_category_id'] = $is_direct ? 'autres' : $category->term_id;
// Axes thématiques for filter dropdown
$axes_groups = thalim_get_axes_filter_groups();
$current_axes = $axes_groups[0]['terms'] ?? [];
$context['filter_axes'] = $current_axes;
$context['axe_stay_on_page'] = true;
// Fetch posts for initial display
$children = get_categories([
'parent' => $category->term_id,
'taxonomy' => 'category',
'hide_empty' => true,
'exclude' => $excluded_ids,
]);
// Ordre personnalisé des sous-catégories (term_id => position).
// Les termes absents du tableau sont placés en dernier (position 999).
$subcategory_order = [
// Publications et productions (parent: 4)
15 => 0, // Ouvrages
16 => 1, // Articles
65 => 2, // Revues et collections
17 => 3, // Multimédia
// Activités (parent: 3)
11 => 0, // Séminaires
10 => 1, // Colloques et journées d'études
13 => 2, // Communications
14 => 3, // Soutenances
];
usort($children, function($a, $b) use ($subcategory_order) {
$pos_a = $subcategory_order[$a->term_id] ?? 999;
$pos_b = $subcategory_order[$b->term_id] ?? 999;
return $pos_a - $pos_b;
});
$context['category_id'] = $category->term_id;
$context['agenda_include_children'] = ( ! $is_direct && ! empty( $children ) ) ? 1 : 0;
// Helper: move pinned posts to the front (same logic as homepage diaporamas)
$sort_with_pinned = function ( $posts ) {
$today = date( 'Y-m-d' );
$pinned = [];
$normal = [];
foreach ( $posts as $post ) {
$epingle = get_post_meta( $post->ID, 'epingler_dans_la_categorie', true );
$fin = get_post_meta( $post->ID, 'date_de_fin_depinglage', true );
$active = $epingle == '1' && ( empty( $fin ) || $fin === '0000-00-00' || $fin >= $today );
if ( $active ) { $pinned[] = $post; } else { $normal[] = $post; }
}
return array_merge( $pinned, $normal );
};
if (!$is_direct && !empty($children)) {
$context['is_parent'] = true;
$context['subcategories'] = [];
foreach ($children as $child) {
$query_args = array_merge([
'post_type' => 'post',
'tax_query' => [[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [$child->term_id],
'include_children' => false,
]],
'posts_per_page' => 3,
'orderby' => 'date',
'order' => 'DESC',
'lang' => '',
'thalim_event_date_order' => true,
], $extra_query_args);
$posts = $sort_with_pinned( Timber::get_posts($query_args) );
$context['cards'] += thalim_get_cards_data($posts);
$context['subcategories'][] = [
'term' => Timber::get_term($child),
'posts' => $posts,
];
}
// Fetch posts directly in the parent category (no child category assigned)
if ($has_direct_posts) {
$direct_query_args = array_merge([
'post_type' => 'post',
'tax_query' => [[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [$category->term_id],
'include_children' => false,
]],
'posts_per_page' => 3,
'orderby' => 'date',
'order' => 'DESC',
'lang' => '',
'thalim_event_date_order' => true,
], $extra_query_args);
$direct_posts = $sort_with_pinned( Timber::get_posts($direct_query_args) );
if (!empty($direct_posts)) {
$context['cards'] += thalim_get_cards_data($direct_posts);
$context['direct_posts'] = $direct_posts;
$autres_link = trailingslashit(get_category_link($category->term_id)) . 'autres/';
if ($filter_query) $autres_link .= '?' . $filter_query;
$context['autres_link'] = $autres_link;
}
}
} else {
$context['is_parent'] = false;
$context['is_direct'] = $is_direct;
$query_args = array_merge([
'post_type' => 'post',
'tax_query' => [[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [$category->term_id],
'include_children' => false,
]],
'posts_per_page' => 12,
'orderby' => 'date',
'order' => 'DESC',
'lang' => '',
'thalim_event_date_order' => true,
], $extra_query_args);
$posts = $sort_with_pinned( Timber::get_posts($query_args) );
$context['cards'] = thalim_get_cards_data($posts);
$context['posts'] = $posts;
}
// View mode toggle (?view=agenda)
$view_mode = ( isset( $_GET['view'] ) && $_GET['view'] === 'agenda' ) ? 'agenda' : 'grid';
$context['view_mode'] = $view_mode;
// Toggle URL (used as href fallback on the button)
$toggle_base = get_category_link( $category->term_id );
$toggle_params = array_filter([
'axe' => $active_axe ?: null,
'date_from' => $active_date_from ?: null,
'date_to' => $active_date_to ?: null,
]);
if ( $view_mode === 'grid' ) {
$toggle_params['view'] = 'agenda';
}
// When toggling back to grid we omit ?view entirely
$context['agenda_toggle_url'] = add_query_arg( $toggle_params, $toggle_base );
// Custom Pods presentation fields
$cat_lang = thalim_current_language();
$pres_fr = get_term_meta( $category->term_id, 'presentation', true ) ?: '';
$pres_en = get_term_meta( $category->term_id, 'presentation_en', true ) ?: '';
$context['term_presentation'] = wpautop( ( $cat_lang === 'en' && $pres_en ) ? $pres_en : $pres_fr );
Timber::render('category.twig', $context);

5
composer.json Executable file
View File

@@ -0,0 +1,5 @@
{
"require": {
"timber/timber": "^2.3"
}
}

434
composer.lock generated Executable file
View File

@@ -0,0 +1,434 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "977b0c3760f52f4b6ad4694d7ac704ba",
"packages": [
{
"name": "symfony/deprecation-contracts",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-12-23T08:48:59+00:00"
},
{
"name": "timber/timber",
"version": "v2.3.3",
"source": {
"type": "git",
"url": "https://github.com/timber/timber.git",
"reference": "7a87ac27c0b9deedffe419388b63a0c95d8798ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/timber/timber/zipball/7a87ac27c0b9deedffe419388b63a0c95d8798ca",
"reference": "7a87ac27c0b9deedffe419388b63a0c95d8798ca",
"shasum": ""
},
"require": {
"php": "^8.1",
"twig/twig": "^3.19"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.28",
"php-parallel-lint/php-parallel-lint": "^1.3",
"php-stubs/wp-cli-stubs": "^2.0",
"phpro/grumphp": "^2.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^2",
"phpunit/phpunit": "^9.0",
"rector/rector": "^2.0",
"squizlabs/php_codesniffer": "^3.0",
"symplify/easy-coding-standard": "^12",
"szepeviktor/phpstan-wordpress": "^2",
"twig/cache-extra": "^3.17",
"wpackagist-plugin/advanced-custom-fields": "^6.0",
"wpackagist-plugin/co-authors-plus": "^3.6",
"yoast/wp-test-utils": "^1.2"
},
"suggest": {
"php-coveralls/php-coveralls": "^2.0 for code coverage",
"twig/cache-extra": "For using the cache tag in Twig"
},
"type": "library",
"autoload": {
"psr-4": {
"Timber\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Erik van der Bas",
"email": "erik@basedonline.nl",
"homepage": "https://basedonline.nl"
},
{
"name": "Lukas Gächter",
"email": "lukas.gaechter@mind.ch",
"homepage": "https://www.mind.ch"
},
{
"name": "Nicolas Lemoine",
"email": "nico@n5s.dev",
"homepage": "https://n5s.dev"
},
{
"name": "Jared Novack",
"email": "jared@upstatement.com",
"homepage": "https://upstatement.com"
},
{
"name": "Timber Community",
"homepage": "https://github.com/timber/timber"
}
],
"description": "Create WordPress themes with beautiful OOP code and the Twig Template Engine",
"homepage": "https://timber.upstatement.com",
"keywords": [
"templating",
"themes",
"timber",
"twig",
"wordpress"
],
"support": {
"docs": "https://timber.github.io/docs/",
"issues": "https://github.com/timber/timber/issues",
"source": "https://github.com/timber/timber"
},
"funding": [
{
"url": "https://github.com/timber",
"type": "github"
},
{
"url": "https://opencollective.com/timber",
"type": "open_collective"
}
],
"time": "2025-09-24T14:07:33+00:00"
},
{
"name": "twig/twig",
"version": "v3.22.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "4509984193026de413baf4ba80f68590a7f2c51d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/4509984193026de413baf4ba80f68590a7f2c51d",
"reference": "4509984193026de413baf4ba80f68590a7f2c51d",
"shasum": ""
},
"require": {
"php": ">=8.1.0",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
"type": "library",
"autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": {
"Twig\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
},
{
"name": "Twig Team",
"role": "Contributors"
},
{
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"role": "Project Founder"
}
],
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "https://twig.symfony.com",
"keywords": [
"templating"
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.22.0"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2025-10-29T15:56:47+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.9.0"
}

923
css/admin.css Normal file
View File

@@ -0,0 +1,923 @@
/* Fade in transition for post edit and profile pages */
body.post-php #wpbody,
body.post-new-php #wpbody,
body.profile-php #wpbody,
body.user-edit-php #wpbody,
body.user-new-php #wpbody {
opacity: 0;
transition: opacity 0.3s ease;
}
body.post-php.admin-mods-ready #wpbody,
body.post-new-php.admin-mods-ready #wpbody,
body.profile-php.admin-mods-ready #wpbody,
body.user-edit-php.admin-mods-ready #wpbody,
body.user-new-php.admin-mods-ready #wpbody {
opacity: 1;
}
/* Post edit pages: hide elements */
body.post-php #advanced-sortables,
body.post-new-php #advanced-sortables,
body.post-php #preview-action,
body.post-new-php #preview-action,
body.post-php #visibility,
body.post-new-php #visibility,
body.post-php #add_pod_button,
body.post-new-php #add_pod_button,
body.post-php #wp-content-editor-tools > .wp-editor-tabs,
body.post-new-php #wp-content-editor-tools > .wp-editor-tabs,
body.post-php #mceu_0,
body.post-new-php #mceu_0,
body.post-php #mceu_6,
body.post-new-php #mceu_6,
body.post-php #mceu_7,
body.post-new-php #mceu_7,
body.post-php #mceu_8,
body.post-new-php #mceu_8,
body.post-php #mceu_10,
body.post-new-php #mceu_10,
body.post-php #mceu_11,
body.post-new-php #mceu_11,
body.post-php #postexcerpt,
body.post-new-php #postexcerpt,
body.post-php #postcustom,
body.post-new-php #postcustom,
body.post-php #slugdiv,
body.post-new-php #slugdiv,
body.post-php #authordiv,
body.post-new-php #authordiv,
body.post-php #screen-meta-links,
body.post-new-php #screen-meta-links,
body.post-php .handle-order-higher,
body.post-new-php .handle-order-higher,
body.post-php .handle-order-lower,
body.post-new-php .handle-order-lower,
body.post-php #pods-meta-seances-seminaire .pods-field-wrapper > div:first-of-type,
body.post-new-php #pods-meta-seances-seminaire .pods-field-wrapper > div:first-of-type,
body.post-php #tagsdiv-post_tag,
body.post-new-php #tagsdiv-post_tag,
body.post-php #wp-content-media-buttons,
body.post-new-php #wp-content-media-buttons,
body.post-php #members-cp,
body.post-new-php #members-cp {
display: none !important;
}
body.post-php .postbox-header .hndle,
body.post-new-php .postbox-header .hndle {
pointer-events: none;
cursor: default;
overflow: visible;
justify-content: start;
}
body.post-php #pods-form-ui-pods-meta-categorie option[value="31"],
body.post-new-php #pods-form-ui-pods-meta-categorie option[value="31"] {
display: none;
}
body.post-php #pods-form-ui-pods-meta-categorie option:disabled,
body.post-new-php #pods-form-ui-pods-meta-categorie option:disabled {
color: #a7aaad;
}
/* Documents joints: margin-top to separate it from the body editor section */
body.post-php #pods-meta-documents-joints,
body.post-new-php #pods-meta-documents-joints {
margin-top: 20px;
}
/* Pods iframe modal: hide elements */
body.pods-modal-window #ml_box,
body.pods-modal-window #pods-meta-liens-externes,
body.pods-modal-window #pods-meta-documents-joints {
display: none;
}
/* Axes thématiques checkbox group headers */
body.post-php .pods-form-ui-row-name-axes-thematiques li.axes-group-label,
body.post-new-php .pods-form-ui-row-name-axes-thematiques li.axes-group-label {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #646970;
background-color: #f0f0f1;
padding: 0.4rem 0.5rem;
cursor: default;
pointer-events: none;
border-top: 1px solid #dcdcde;
margin-top: 0.4rem;
width: 100%;
}
body.post-php .pods-form-ui-row-name-axes-thematiques li.axes-group-label:first-child,
body.post-new-php .pods-form-ui-row-name-axes-thematiques li.axes-group-label:first-child {
border-top: none;
margin-top: 0;
}
/* Remove WP's JS-injected padding-top on the editor wrap (compensation for sticky toolbar) */
body.post-php #wp-content-editor-tools,
body.post-new-php #wp-content-editor-tools {
padding-top: 0 !important;
}
body.post-php .pods-dfv-container-wysiwyg,
body.post-new-php .pods-dfv-container-wysiwyg {
max-width: unset !important;
}
body.post-php .pods-tinymce-reinit,
body.post-new-php .pods-tinymce-reinit {
display: none;
}
/* Keep statusbar in layout so TinyMCE's reposition() can read its getBoundingClientRect().
display:none would return top=0, making mceStatusbarTop=windowHeight → editorHeight<0 → toolbar always hidden. */
body.post-php .pods-dfv-container-wysiwyg .mce-statusbar,
body.post-new-php .pods-dfv-container-wysiwyg .mce-statusbar {
opacity: 0;
pointer-events: none;
user-select: none;
}
body.post-php #pods-form-ui-pods-meta-body-en_ifr,
body.post-new-php #pods-form-ui-pods-meta-body-en_ifr {
min-height: 334px !important;
}
body.post-php #content_ifr,
body.post-new-php #content_ifr,
body.post-php #pods-form-ui-pods-meta-body-en_ifr,
body.post-new-php #pods-form-ui-pods-meta-body-en_ifr {
cursor: text;
}
/* Pods body-en: hide Visuel/Code tabs and same toolbar buttons removed from native editor
(paragraph format, alignment ×3, "Lire la suite", toggle-advanced-toolbar).
Uses aria-label selectors — more stable than MCE numeric IDs used for the native editor. */
body.post-php #pods-meta-body-en .wp-editor-tabs,
body.post-new-php #pods-meta-body-en .wp-editor-tabs,
body.post-php #pods-meta-body-en .mce-listbox.mce-fixed-width,
body.post-new-php #pods-meta-body-en .mce-listbox.mce-fixed-width,
body.post-php #pods-meta-body-en [aria-label*="Aligner"],
body.post-new-php #pods-meta-body-en [aria-label*="Aligner"],
body.post-php #pods-meta-body-en [aria-label*="Centrer"],
body.post-new-php #pods-meta-body-en [aria-label*="Centrer"],
body.post-php #pods-meta-body-en [aria-label*="Lire la suite"],
body.post-new-php #pods-meta-body-en [aria-label*="Lire la suite"],
body.post-php #pods-meta-body-en [aria-label*="Permuter la barre"],
body.post-new-php #pods-meta-body-en [aria-label*="Permuter la barre"] {
display: none !important;
}
/* Référence bibliographique: hide tabs, media buttons, status bar,
and all toolbar buttons except Italic (only formatting allowed). */
#wp-pods-form-ui-pods-meta-reference-bibliographique-wrap .wp-editor-tabs,
#wp-pods-form-ui-pods-meta-reference-bibliographique-wrap .wp-media-buttons,
#wp-pods-form-ui-pods-meta-reference-bibliographique-wrap .mce-statusbar {
display: none !important;
}
#wp-pods-form-ui-pods-meta-reference-bibliographique-wrap .mce-toolbar .mce-btn {
display: none !important;
}
#wp-pods-form-ui-pods-meta-reference-bibliographique-wrap .mce-toolbar .mce-btn:has(.mce-i-italic),
#wp-pods-form-ui-pods-meta-reference-bibliographique-wrap .mce-toolbar .mce-btn:has(.mce-i-bold) {
display: inline-block !important;
}
/* Profile / user-edit: fixed submit button */
body.profile-php #your-profile p.submit::before,
body.user-edit-php #your-profile p.submit::before {
content: "Pensez à sauvegarder les modifications";
display: block;
font-size: 11px;
color: #646970;
margin-bottom: 6px;
}
body.profile-php #your-profile p.submit,
body.user-edit-php #your-profile p.submit {
position: fixed;
bottom: 0;
right: 0;
margin: 0;
padding: 12px 16px;
background: #f0f0f1;
border-top: 1px solid #c3c4c7;
border-left: 1px solid #c3c4c7;
z-index: 100;
}
/* Profile / user-edit / user-new / taxonomy term pages: TinyMCE editor mods (mirrors post-edit treatment) */
body.profile-php .wp-editor-tabs,
body.user-edit-php .wp-editor-tabs,
body.user-new-php .wp-editor-tabs,
body.edit-tags-php .wp-editor-tabs,
body.term-php .wp-editor-tabs,
body.profile-php .mce-listbox.mce-fixed-width,
body.user-edit-php .mce-listbox.mce-fixed-width,
body.user-new-php .mce-listbox.mce-fixed-width,
body.edit-tags-php .mce-listbox.mce-fixed-width,
body.term-php .mce-listbox.mce-fixed-width,
body.profile-php [aria-label*="Aligner"],
body.user-edit-php [aria-label*="Aligner"],
body.user-new-php [aria-label*="Aligner"],
body.edit-tags-php [aria-label*="Aligner"],
body.term-php [aria-label*="Aligner"],
body.profile-php [aria-label*="Centrer"],
body.user-edit-php [aria-label*="Centrer"],
body.user-new-php [aria-label*="Centrer"],
body.edit-tags-php [aria-label*="Centrer"],
body.term-php [aria-label*="Centrer"],
body.profile-php [aria-label*="Lire la suite"],
body.user-edit-php [aria-label*="Lire la suite"],
body.user-new-php [aria-label*="Lire la suite"],
body.edit-tags-php [aria-label*="Lire la suite"],
body.term-php [aria-label*="Lire la suite"],
body.profile-php [aria-label*="Permuter la barre"],
body.user-edit-php [aria-label*="Permuter la barre"],
body.user-new-php [aria-label*="Permuter la barre"],
body.edit-tags-php [aria-label*="Permuter la barre"],
body.term-php [aria-label*="Permuter la barre"],
body.profile-php .wp-media-buttons,
body.user-edit-php .wp-media-buttons,
body.user-new-php .wp-media-buttons,
body.edit-tags-php .wp-media-buttons,
body.term-php .wp-media-buttons,
body.profile-php .pods-tinymce-reinit,
body.user-edit-php .pods-tinymce-reinit,
body.user-new-php .pods-tinymce-reinit,
body.edit-tags-php .pods-tinymce-reinit,
body.term-php .pods-tinymce-reinit {
display: none !important;
}
body.profile-php .wp-editor-tools,
body.user-edit-php .wp-editor-tools,
body.user-new-php .wp-editor-tools,
body.edit-tags-php .wp-editor-tools,
body.term-php .wp-editor-tools {
padding-top: 0 !important;
}
body.profile-php .pods-dfv-container-wysiwyg,
body.user-edit-php .pods-dfv-container-wysiwyg,
body.user-new-php .pods-dfv-container-wysiwyg,
body.edit-tags-php .pods-dfv-container-wysiwyg,
body.term-php .pods-dfv-container-wysiwyg {
max-width: unset !important;
}
body.profile-php .wp-editor-container iframe,
body.user-edit-php .wp-editor-container iframe,
body.user-new-php .wp-editor-container iframe,
body.edit-tags-php .wp-editor-container iframe,
body.term-php .wp-editor-container iframe {
cursor: text;
}
/* Fade-in transition for tab panels (opacity is set to 0 by JS before reveal, then cleared) */
body.post-php #postdivrich,
body.post-new-php #postdivrich,
body.post-php #pods-meta-body-en,
body.post-new-php #pods-meta-body-en {
transition: opacity 0.15s ease;
}
/* Body FR/EN language tabs */
body.post-php .body-lang-tabs,
body.post-new-php .body-lang-tabs {
display: flex;
gap: 0;
margin-bottom: -1px;
position: relative;
z-index: 1;
}
body.post-php .body-lang-tab,
body.post-new-php .body-lang-tab {
padding: 5px 14px;
background: #f0f0f1;
border: 1px solid #c3c4c7;
border-bottom-color: #c3c4c7;
cursor: pointer;
font-size: 13px;
line-height: 1.5;
color: #50575e;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
body.post-php .body-lang-tab.is-active,
body.post-new-php .body-lang-tab.is-active {
background: #fff;
border-bottom-color: #fff;
color: #1d2327;
font-weight: 600;
}
/* Hide postbox headers for both body editors — tabs replace them */
body.post-php #postdivrich .postbox-header,
body.post-new-php #postdivrich .postbox-header,
body.post-php #pods-meta-body-en .postbox-header,
body.post-new-php #pods-meta-body-en .postbox-header {
display: none !important;
}
/* Strip Pods table layout from body-en — make it full width like native editor */
body.post-php #pods-meta-body-en .inside,
body.post-new-php #pods-meta-body-en .inside {
margin: 0;
padding: 0;
}
body.post-php #pods-meta-body-en .form-table,
body.post-new-php #pods-meta-body-en .form-table,
body.post-php #pods-meta-body-en .form-table tbody,
body.post-new-php #pods-meta-body-en .form-table tbody,
body.post-php #pods-meta-body-en .form-table tr,
body.post-new-php #pods-meta-body-en .form-table tr,
body.post-php #pods-meta-body-en .form-table td,
body.post-new-php #pods-meta-body-en .form-table td {
display: block;
width: 100%;
padding: 0;
margin: 0;
}
body.post-php #pods-meta-body-en .form-table th,
body.post-new-php #pods-meta-body-en .form-table th {
display: none;
}
body.post-php #pods-meta-body-en .pods-submittable-fields,
body.post-new-php #pods-meta-body-en .pods-submittable-fields,
body.post-php #pods-meta-body-en .pods-dfv-container,
body.post-new-php #pods-meta-body-en .pods-dfv-container {
padding: 0;
}
/* Allow TinyMCE floating panels (link popover, inline toolbar) to escape the Pods
container chain — any overflow:hidden in Pods DFV elements would clip them */
body.post-php #pods-meta-body-en .inside,
body.post-new-php #pods-meta-body-en .inside,
body.post-php #pods-meta-body-en .pods-submittable-fields,
body.post-new-php #pods-meta-body-en .pods-submittable-fields,
body.post-php #pods-meta-body-en .pods-dfv-field,
body.post-new-php #pods-meta-body-en .pods-dfv-field,
body.post-php #pods-meta-body-en .pods-field-option,
body.post-new-php #pods-meta-body-en .pods-field-option,
body.post-php #pods-meta-body-en .pods-field-option__field,
body.post-new-php #pods-meta-body-en .pods-field-option__field,
body.post-php #pods-meta-body-en .pods-dfv-container-wysiwyg,
body.post-new-php #pods-meta-body-en .pods-dfv-container-wysiwyg,
body.post-php #pods-meta-body-en .pods-field-wrapper,
body.post-new-php #pods-meta-body-en .pods-field-wrapper,
body.post-php #pods-meta-body-en .wp-editor-wrap,
body.post-new-php #pods-meta-body-en .wp-editor-wrap,
body.post-php #pods-meta-body-en .wp-editor-container,
body.post-new-php #pods-meta-body-en .wp-editor-container {
overflow: visible !important;
}
/* Info popovers */
.thalim-info-wrapper {
position: relative;
display: inline-flex;
align-items: center;
margin-left: 6px;
vertical-align: middle;
}
.thalim-info-btn {
display: inline-flex;
align-items: center;
justify-content: center;
background: none;
border: none;
padding: 2px;
cursor: pointer;
color: #646970;
line-height: 1;
border-radius: 50%;
transition: color 0.15s, background 0.15s;
pointer-events: auto;
}
.thalim-info-btn:hover,
.thalim-info-btn:focus {
color: #2271b1;
background: #f0f6fc;
outline: none;
}
.thalim-info-popover {
display: none;
position: fixed;
transform: translateX(-50%);
z-index: 9999;
background: #fff;
border: 1px solid #c3c4c7;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
padding: 10px 12px;
min-width: 220px;
max-width: 320px;
font-size: 12px;
font-weight: normal;
line-height: 1.5;
color: #3c434a;
white-space: normal;
text-align: left;
pointer-events: auto;
}
.thalim-info-popover.is-open {
display: block;
}
.thalim-info-popover p {
margin: 0 0 4px;
}
.thalim-info-popover p:last-child {
margin-bottom: 0;
}
/* Translate popovers */
.thalim-translate-btn {
display: inline-flex;
align-items: center;
justify-content: center;
background: none;
border: none;
padding: 2px;
cursor: pointer;
color: #2a7a4f;
line-height: 1;
border-radius: 50%;
transition: color 0.15s, background 0.15s;
pointer-events: auto;
}
.thalim-translate-btn:hover,
.thalim-translate-btn:focus {
color: #1d5c3a;
background: #edfaf3;
outline: none;
}
.thalim-translate-popover {
border-color: #b2dfcc;
}
/* Required field indicator */
abbr.required::after {
content: " Champ nécessaire";
font-size: 0.72rem;
font-weight: normal;
font-style: normal;
color: #646970;
text-decoration: none;
}
/* Profile pages: hide sections */
body.user-new-php #simple-local-avatar-section,
body.profile-php .user-admin-color-wrap,
body.user-edit-php .user-admin-color-wrap,
body.user-new-php .user-admin-color-wrap,
body.profile-php .user-admin-bar-front-wrap,
body.user-edit-php .user-admin-bar-front-wrap,
body.user-new-php .user-admin-bar-front-wrap,
body.profile-php .user-nickname-wrap,
body.user-edit-php .user-nickname-wrap,
body.user-new-php .user-nickname-wrap,
body.profile-php .user-display-name-wrap,
body.user-edit-php .user-display-name-wrap,
body.user-new-php .user-display-name-wrap,
body.profile-php .user-profile-picture,
body.user-edit-php .user-profile-picture,
body.user-new-php .user-profile-picture,
body.profile-php .ratings-row,
body.user-edit-php .ratings-row,
body.user-new-php .ratings-row,
body.profile-php .application-passwords,
body.user-edit-php .application-passwords,
body.user-new-php .application-passwords,
body.profile-php .user-comment-shortcuts-wrap,
body.user-edit-php .user-comment-shortcuts-wrap,
body.user-new-php .user-comment-shortcuts-wrap,
body.profile-php .form-table:has(.user-description-wrap),
body.user-edit-php .form-table:has(.user-description-wrap),
body.user-new-php .form-table:has(.user-description-wrap) {
display: none !important;
}
body.profile-php .form-table,
body.user-edit-php .form-table,
body.user-new-php .form-table {
display: block;
}
body.profile-php .form-table tbody,
body.user-edit-php .form-table tbody,
body.user-new-php .form-table tbody {
display: grid;
grid-template-columns: 1fr;
background: #fff;
border: 1px solid #c3c4c7;
padding: 8px 16px;
margin-bottom: 16px;
}
body.profile-php .form-table tbody tr,
body.user-edit-php .form-table tbody tr,
body.user-new-php .form-table tbody tr {
display: flex;
flex-direction: column;
grid-column: 1 / -1;
}
body.profile-php .form-table tbody tr th,
body.user-edit-php .form-table tbody tr th,
body.user-new-php .form-table tbody tr th,
body.profile-php .form-table tbody tr td,
body.user-edit-php .form-table tbody tr td,
body.user-new-php .form-table tbody tr td {
display: block;
width: 100%;
padding: 0 0 4px;
}
@media (min-width: 768px) {
/* Side-by-side section pairs */
body.profile-php .profile-section-row,
body.user-edit-php .profile-section-row,
body.user-new-php .profile-section-row {
display: flex;
gap: 24px;
align-items: flex-start;
}
body.profile-php .profile-section-col,
body.user-edit-php .profile-section-col,
body.user-new-php .profile-section-col {
flex: 1;
min-width: 0;
}
/* Tables inside a col are already at 50% width — keep fields single-column */
body.profile-php .profile-section-col .form-table tbody,
body.user-edit-php .profile-section-col .form-table tbody,
body.user-new-php .profile-section-col .form-table tbody {
grid-template-columns: 1fr;
}
body.profile-php .form-table tbody,
body.user-edit-php .form-table tbody,
body.user-new-php .form-table tbody {
grid-template-columns: 1fr 1fr;
gap: 0 15px;
&:has(.pods-form-ui-row-name-role-1) {
grid-template-columns: 1fr 1fr 1fr;
}
}
body.profile-php .form-table tbody .user-user-login-wrap,
body.user-edit-php .form-table tbody .user-user-login-wrap,
body.user-new-php .form-table tbody .user-user-login-wrap,
body.profile-php .form-table tbody .user-first-name-wrap,
body.user-edit-php .form-table tbody .user-first-name-wrap,
body.user-new-php .form-table tbody .user-first-name-wrap,
body.profile-php .form-table tbody .user-email-wrap,
body.user-edit-php .form-table tbody .user-email-wrap,
body.user-new-php .form-table tbody .user-email-wrap,
body.profile-php .form-table tbody .pods-form-ui-row-name-lien-externe-1,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-lien-externe-1,
body.user-new-php .form-table tbody .pods-form-ui-row-name-lien-externe-1,
body.profile-php .form-table tbody .pods-form-ui-row-name-lien-externe-2,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-lien-externe-2,
body.user-new-php .form-table tbody .pods-form-ui-row-name-lien-externe-2,
body.profile-php .form-table tbody .pods-form-ui-row-name-lien-externe-3,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-lien-externe-3,
body.user-new-php .form-table tbody .pods-form-ui-row-name-lien-externe-3,
body.profile-php .form-table tbody .pods-form-ui-row-name-role-1,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-role-1,
body.user-new-php .form-table tbody .pods-form-ui-row-name-role-1,
body.profile-php .form-table tbody .pods-form-ui-row-name-role-2,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-role-2,
body.user-new-php .form-table tbody .pods-form-ui-row-name-role-2,
body.profile-php .form-table tbody .pods-form-ui-row-name-role-3,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-role-3,
body.user-new-php .form-table tbody .pods-form-ui-row-name-role-3,
body.profile-php .form-table tbody .pods-form-ui-row-name-affiliation,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-affiliation,
body.user-new-php .form-table tbody .pods-form-ui-row-name-affiliation,
body.profile-php .form-table tbody .pods-form-ui-row-name-titre-de-these,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-titre-de-these,
body.user-new-php .form-table tbody .pods-form-ui-row-name-titre-de-these,
body.profile-php .form-table tbody .pods-form-ui-row-name-directeur-de-these-thalim,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-directeur-de-these-thalim,
body.user-new-php .form-table tbody .pods-form-ui-row-name-directeur-de-these-thalim,
body.profile-php .form-table tbody .pods-form-ui-row-name-recherches-en-cours,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-recherches-en-cours,
body.user-new-php .form-table tbody .pods-form-ui-row-name-recherches-en-cours,
body.profile-php .form-table tbody .pods-form-ui-row-name-autres-domaines-de-recherches,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-autres-domaines-de-recherches,
body.user-new-php .form-table tbody .pods-form-ui-row-name-autres-domaines-de-recherches,
body.profile-php .form-table tbody .pods-form-ui-row-name-resume-de-la-these,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-resume-de-la-these,
body.user-new-php .form-table tbody .pods-form-ui-row-name-resume-de-la-these,
body.profile-php .form-table tbody .pods-form-ui-row-name-biographie,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-biographie,
body.user-new-php .form-table tbody .pods-form-ui-row-name-biographie {
grid-column: 1;
}
body.profile-php .form-table tbody .user-role-wrap,
body.user-edit-php .form-table tbody .user-role-wrap,
body.user-new-php .form-table tbody .user-role-wrap,
body.profile-php .form-table tbody .user-last-name-wrap,
body.user-edit-php .form-table tbody .user-last-name-wrap,
body.user-new-php .form-table tbody .user-last-name-wrap,
body.profile-php .form-table tbody .user-url-wrap,
body.user-edit-php .form-table tbody .user-url-wrap,
body.user-new-php .form-table tbody .user-url-wrap,
body.profile-php .form-table tbody .pods-form-ui-row-name-titre-du-lien-1,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-titre-du-lien-1,
body.user-new-php .form-table tbody .pods-form-ui-row-name-titre-du-lien-1,
body.profile-php .form-table tbody .pods-form-ui-row-name-titre-du-lien-2,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-titre-du-lien-2,
body.user-new-php .form-table tbody .pods-form-ui-row-name-titre-du-lien-2,
body.profile-php .form-table tbody .pods-form-ui-row-name-titre-du-lien-3,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-titre-du-lien-3,
body.user-new-php .form-table tbody .pods-form-ui-row-name-titre-du-lien-3,
body.profile-php .form-table tbody .pods-form-ui-row-name-complement-de-role-1,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-complement-de-role-1,
body.user-new-php .form-table tbody .pods-form-ui-row-name-complement-de-role-1,
body.profile-php .form-table tbody .pods-form-ui-row-name-complement-de-role-2,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-complement-de-role-2,
body.user-new-php .form-table tbody .pods-form-ui-row-name-complement-de-role-2,
body.profile-php .form-table tbody .pods-form-ui-row-name-complement-de-role-3,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-complement-de-role-3,
body.user-new-php .form-table tbody .pods-form-ui-row-name-complement-de-role-3,
body.profile-php .form-table tbody .pods-form-ui-row-name-affiliation-autre,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-affiliation-autre,
body.user-new-php .form-table tbody .pods-form-ui-row-name-affiliation-autre,
body.profile-php .form-table tbody .pods-form-ui-row-name-autre-directeur-de-these,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-autre-directeur-de-these,
body.user-new-php .form-table tbody .pods-form-ui-row-name-autre-directeur-de-these,
body.profile-php .form-table tbody .pods-form-ui-row-name-date-de-soutenance,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-date-de-soutenance,
body.user-new-php .form-table tbody .pods-form-ui-row-name-date-de-soutenance,
body.profile-php .form-table tbody .pods-form-ui-row-name-recherches-en-cours-en,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-recherches-en-cours-en,
body.user-new-php .form-table tbody .pods-form-ui-row-name-recherches-en-cours-en,
body.profile-php .form-table tbody .pods-form-ui-row-name-autres-domaines-de-recherches-en,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-autres-domaines-de-recherches-en,
body.user-new-php .form-table tbody .pods-form-ui-row-name-autres-domaines-de-recherches-en,
body.profile-php .form-table tbody .pods-form-ui-row-name-resume-de-la-these-en,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-resume-de-la-these-en,
body.user-new-php .form-table tbody .pods-form-ui-row-name-resume-de-la-these-en,
body.profile-php .form-table tbody .pods-form-ui-row-name-biographie-en,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-biographie-en,
body.user-new-php .form-table tbody .pods-form-ui-row-name-biographie-en {
grid-column: 2;
}
body.profile-php .form-table tbody .pods-form-ui-row-name-affichage-du-statut-1,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-affichage-du-statut-1,
body.user-new-php .form-table tbody .pods-form-ui-row-name-affichage-du-statut-1,
body.profile-php .form-table tbody .pods-form-ui-row-name-affichage-du-statut-2,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-affichage-du-statut-2,
body.user-new-php .form-table tbody .pods-form-ui-row-name-affichage-du-statut-2,
body.profile-php .form-table tbody .pods-form-ui-row-name-affichage-du-statut-3,
body.user-edit-php .form-table tbody .pods-form-ui-row-name-affichage-du-statut-3,
body.user-new-php .form-table tbody .pods-form-ui-row-name-affichage-du-statut-3 {
grid-column: 3;
}
body.profile-php .user-user-login-wrap span.description,
body.user-edit-php .user-user-login-wrap span.description,
body.user-new-php .user-user-login-wrap span.description {
display: block;
}
}
/* Profile FR/EN language overlay tabs */
body.profile-php .profile-lang-tabs,
body.user-edit-php .profile-lang-tabs,
body.user-new-php .profile-lang-tabs {
display: flex;
gap: 0;
margin-bottom: -1px;
position: relative;
z-index: 1;
}
body.profile-php .profile-lang-tab,
body.user-edit-php .profile-lang-tab,
body.user-new-php .profile-lang-tab {
padding: 5px 14px;
background: #f0f0f1;
border: 1px solid #c3c4c7;
cursor: pointer;
font-size: 13px;
line-height: 1.5;
color: #50575e;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
body.profile-php .profile-lang-tab.is-active,
body.user-edit-php .profile-lang-tab.is-active,
body.user-new-php .profile-lang-tab.is-active {
background: #fff;
border-bottom-color: #fff;
color: #1d2327;
font-weight: 600;
}
body.profile-php .form-table tbody .profile-lang-tabs-row td,
body.user-edit-php .form-table tbody .profile-lang-tabs-row td,
body.user-new-php .form-table tbody .profile-lang-tabs-row td {
padding: 0;
grid-column: 1 / -1;
}
/* Membres: 2-column explicit grid
Row 1: Fonction (col 1 only)
Row 2: Membres | Autrepersonnes
Row 3: full-width separator (injected <tr class="membres-grid-separator">)
Row 4: Autre fonction (col 1 only)
Row 5: Autre membres | Autre autrepersonnes
*/
body.post-php #pods-meta-membres .form-table,
body.post-new-php #pods-meta-membres .form-table {
display: block;
}
body.post-php #pods-meta-membres .form-table tbody,
body.post-new-php #pods-meta-membres .form-table tbody {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 16px;
}
body.post-php #pods-meta-membres .form-table tr,
body.post-new-php #pods-meta-membres .form-table tr {
display: flex;
flex-direction: column;
padding: 6px 0;
}
body.post-php #pods-meta-membres .form-table th,
body.post-new-php #pods-meta-membres .form-table th,
body.post-php #pods-meta-membres .form-table td,
body.post-new-php #pods-meta-membres .form-table td {
display: block;
width: 100%;
padding: 0 0 4px;
}
body.post-php #pods-meta-membres .form-table input[type="text"],
body.post-new-php #pods-meta-membres .form-table input[type="text"] {
width: 100%;
box-sizing: border-box;
max-width: none;
}
/* Row 1: Fonction — left column only */
body.post-php #pods-meta-membres [class*="pods-form-ui-row-name-fonction-"],
body.post-new-php #pods-meta-membres [class*="pods-form-ui-row-name-fonction-"] {
grid-column: 1;
grid-row: 1;
}
/* Row 2: Membres (left) | Autrepersonnes (right) */
body.post-php #pods-meta-membres .pods-form-ui-row-name-membres,
body.post-new-php #pods-meta-membres .pods-form-ui-row-name-membres {
grid-column: 1;
grid-row: 2;
}
body.post-php #pods-meta-membres .pods-form-ui-row-name-autrepersonnes,
body.post-new-php #pods-meta-membres .pods-form-ui-row-name-autrepersonnes {
grid-column: 2;
grid-row: 2;
}
/* Row 3: full-width separator */
body.post-php #pods-meta-membres .membres-grid-separator,
body.post-new-php #pods-meta-membres .membres-grid-separator {
grid-column: 1 / -1;
grid-row: 3;
border-top: 1px solid #dcdcde;
height: 0;
padding: 0;
margin: 4px 0;
}
/* Row 4: Autre fonction — left column only */
body.post-php #pods-meta-membres [class*="pods-form-ui-row-name-autre-fonction-"],
body.post-new-php #pods-meta-membres [class*="pods-form-ui-row-name-autre-fonction-"] {
grid-column: 1;
grid-row: 4;
}
/* Row 5: Autre membres (left) | Autre autrepersonnes (right) */
body.post-php #pods-meta-membres .pods-form-ui-row-name-autre-membres,
body.post-new-php #pods-meta-membres .pods-form-ui-row-name-autre-membres {
grid-column: 1;
grid-row: 5;
}
body.post-php #pods-meta-membres .pods-form-ui-row-name-autre-autrepersonnes,
body.post-new-php #pods-meta-membres .pods-form-ui-row-name-autre-autrepersonnes {
grid-column: 2;
grid-row: 5;
}
/* Liens externes: two-column layout — URL field and title field side by side per link */
body.post-php #pods-meta-liens-externes .form-table,
body.post-new-php #pods-meta-liens-externes .form-table {
display: block;
}
body.post-php #pods-meta-liens-externes .form-table tbody,
body.post-new-php #pods-meta-liens-externes .form-table tbody {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 16px;
}
body.post-php #pods-meta-liens-externes .form-table tr,
body.post-new-php #pods-meta-liens-externes .form-table tr {
display: flex;
flex-direction: column;
padding: 6px 0;
}
body.post-php #pods-meta-liens-externes .form-table th,
body.post-new-php #pods-meta-liens-externes .form-table th,
body.post-php #pods-meta-liens-externes .form-table td,
body.post-new-php #pods-meta-liens-externes .form-table td {
display: block;
width: 100%;
padding: 0 0 4px;
}
body.post-php #pods-meta-liens-externes .form-table input[type="text"],
body.post-new-php #pods-meta-liens-externes .form-table input[type="text"] {
width: 100%;
box-sizing: border-box;
max-width: none;
}
/* Visual separator between link pairs 2 and 3 */
body.post-php #pods-meta-liens-externes .pods-form-ui-row-name-lien-externe-2,
body.post-new-php #pods-meta-liens-externes .pods-form-ui-row-name-lien-externe-2,
body.post-php #pods-meta-liens-externes .pods-form-ui-row-name-titre-du-lien-externe-2,
body.post-new-php #pods-meta-liens-externes .pods-form-ui-row-name-titre-du-lien-externe-2,
body.post-php #pods-meta-liens-externes .pods-form-ui-row-name-lien-externe-3,
body.post-new-php #pods-meta-liens-externes .pods-form-ui-row-name-lien-externe-3,
body.post-php #pods-meta-liens-externes .pods-form-ui-row-name-titre-du-lien-externe-3,
body.post-new-php #pods-meta-liens-externes .pods-form-ui-row-name-titre-du-lien-externe-3 {
border-top: 1px solid #dcdcde;
padding-top: 8px;
}
/* Dates: date de début and date de fin side by side */
body.post-php #pods-meta-dates .form-table,
body.post-new-php #pods-meta-dates .form-table {
display: block;
}
body.post-php #pods-meta-dates .form-table tbody,
body.post-new-php #pods-meta-dates .form-table tbody {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 16px;
}
body.post-php #pods-meta-dates .form-table tr,
body.post-new-php #pods-meta-dates .form-table tr {
display: flex;
flex-direction: column;
padding: 6px 0;
}
body.post-php #pods-meta-dates .form-table th,
body.post-new-php #pods-meta-dates .form-table th,
body.post-php #pods-meta-dates .form-table td,
body.post-new-php #pods-meta-dates .form-table td {
display: block;
width: 100%;
padding: 0 0 4px;
}
/* Taxonomy pages — hide unused description field */
.edit-tags-php .term-description-wrap,
.term-php .term-description-wrap {
display: none;
}

2901
css/style.css Executable file

File diff suppressed because it is too large Load Diff

1375
functions.php Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
<?php
/**
* Add a "Statut" filter dropdown to /wp-admin/users.php.
* Filters by the custom "role" taxonomy stored in user meta keys role_1/role_2/role_3.
*/
add_action( 'restrict_manage_users', function() {
$terms = get_terms( [ 'taxonomy' => 'role', 'hide_empty' => true, 'orderby' => 'name' ] );
if ( is_wp_error( $terms ) || empty( $terms ) ) return;
$selected = isset( $_GET['thalim_statut'] ) ? intval( $_GET['thalim_statut'] ) : 0;
?>
<select id="thalim_statut_filter">
<option value=""><?php esc_html_e( 'Tous les statuts', 'thalim' ); ?></option>
<?php foreach ( $terms as $term ) : ?>
<option value="<?php echo esc_attr( $term->term_id ); ?>"<?php selected( $selected, $term->term_id ); ?>>
<?php echo esc_html( $term->name ); ?>
</option>
<?php endforeach; ?>
</select>
<button type="button" class="button" id="thalim_statut_go"><?php esc_html_e( 'Filtrer', 'thalim' ); ?></button>
<script>
document.getElementById('thalim_statut_go').addEventListener('click', function() {
var v = document.getElementById('thalim_statut_filter').value;
var url = new URL(window.location.href);
url.searchParams.delete('paged');
if (v) url.searchParams.set('thalim_statut', v);
else url.searchParams.delete('thalim_statut');
window.location.href = url.toString();
});
</script>
<?php
} );
add_action( 'pre_get_users', function( WP_User_Query $query ) {
if ( ! is_admin() ) return;
$term_id = isset( $_GET['thalim_statut'] ) ? intval( $_GET['thalim_statut'] ) : 0;
if ( ! $term_id ) return;
$query->set( 'meta_query', [
'relation' => 'OR',
[ 'key' => 'role_1', 'value' => $term_id, 'compare' => '=' ],
[ 'key' => 'role_2', 'value' => $term_id, 'compare' => '=' ],
[ 'key' => 'role_3', 'value' => $term_id, 'compare' => '=' ],
] );
} );

250
inc/author-helpers.php Normal file
View File

@@ -0,0 +1,250 @@
<?php
/**
* Resolve all profile data for a member/author into a display-ready array.
*/
function thalim_get_author_data($user_id) {
$user = get_userdata($user_id);
if (!$user) return [];
$lang = thalim_current_language();
// --- Avatar (Simple Local Avatar with Gravatar fallback) ---
$avatar_url = thalim_get_user_avatar_url( $user_id );
// --- Role (taxonomy 'role') ---
$role_id = get_user_meta($user_id, 'role_1', true);
$role_label = '';
if ($role_id) {
$role_term = get_term(intval($role_id), 'role');
if ($role_term && !is_wp_error($role_term)) {
$override = thalim_bilingual(get_user_meta($user_id, 'affichage_du_statut_1', true) ?: '', $lang);
$role_label = $override ?: $role_term->name;
}
}
// --- Direction title (read from "Le laboratoire" page) ---
$labo_page = get_page_by_path('le-laboratoire');
$directeur_id = $labo_page ? intval(get_post_meta($labo_page->ID, 'directeur', true)) : 0;
$adjoint_id = $labo_page ? intval(get_post_meta($labo_page->ID, 'directeur_adjoint', true)) : 0;
if ($user_id === $directeur_id) {
$role_label = 'Directeur' . ($role_label ? ', ' . $role_label : '');
} elseif ($user_id === $adjoint_id) {
$role_label = 'Directeur adjoint' . ($role_label ? ', ' . $role_label : '');
}
// --- Domaines de recherches (multiple usermeta rows with post_tag IDs) ---
$domaine_ids = get_user_meta($user_id, 'domaines_de_recherches', false);
$domaines_tags = [];
foreach ($domaine_ids as $tag_id) {
if (!$tag_id) continue;
$term = get_term(intval($tag_id), 'post_tag');
if ($term && !is_wp_error($term)) {
$link = get_term_link($term);
if (!is_wp_error($link)) {
$domaines_tags[] = ['name' => thalim_bilingual($term->name, $lang), 'url' => $link];
}
}
}
// --- Axes thématiques (multiple usermeta rows) ---
$axe_ids = get_user_meta($user_id, 'axes_thematiques', false);
$axes = [];
foreach ($axe_ids as $axe_id) {
$term = get_term(intval($axe_id), 'axe_thematique');
if ($term && !is_wp_error($term)) {
$axes[] = [
'name' => thalim_bilingual($term->name, $lang),
'url' => get_term_link($term),
];
}
}
// --- External links (up to 4) ---
$liens_externes = [];
for ($i = 1; $i <= 4; $i++) {
$url = get_user_meta($user_id, 'lien_externe_' . $i, true);
if ($url) {
$titre = thalim_bilingual(get_user_meta($user_id, 'titre_du_lien_' . $i, true) ?: '', $lang);
if (!$titre) {
$host = parse_url($url, PHP_URL_HOST) ?: $url;
$parts = explode('.', $host);
$titre = count($parts) >= 2 ? implode('.', array_slice($parts, -2)) : $host;
}
$liens_externes[] = ['url' => $url, 'titre' => $titre];
}
}
// --- Documents (multiple usermeta rows with attachment IDs) ---
$doc_ids = get_user_meta($user_id, 'documents', false);
$documents = [];
foreach ($doc_ids as $doc_id) {
$url = wp_get_attachment_url(intval($doc_id));
if ($url) {
$documents[] = [
'url' => $url,
'title' => get_the_title($doc_id) ?: basename(get_attached_file($doc_id)),
];
}
}
// --- Thesis director (THALIM member — stored as user ID) ---
$directeur_id = get_user_meta($user_id, 'directeur_de_these_thalim', true);
$directeur_thalim = null;
if ($directeur_id) {
$dir_user = get_userdata(intval($directeur_id));
if ($dir_user) {
$directeur_thalim = [
'name' => $dir_user->display_name,
'url' => get_author_posts_url(intval($directeur_id)),
];
}
}
// --- Email visibility ---
$is_ancien = isset($role_term) && $role_term && $role_term->slug === 'anciens-membres';
$show_email = !$is_ancien && get_user_meta($user_id, 'afficher_ladresse_mail_sur_le_profil', true);
return [
'display_name' => $user->display_name,
'avatar_url' => $avatar_url,
'role_label' => $role_label,
'role_complement' => thalim_bilingual(get_user_meta($user_id, 'complement_de_role_1', true) ?: '', $lang),
'affiliation' => (function() use ($user_id, $lang) {
$v = get_user_meta($user_id, 'affiliation', true) ?: '';
return strtolower($v) === 'autre'
? thalim_bilingual(get_user_meta($user_id, 'affiliation_autre', true) ?: '', $lang)
: $v;
})(),
'bio' => wpautop( make_clickable( get_user_meta($user_id, 'biographie', true) ?: '' ) ),
'bio_en' => wpautop( make_clickable( get_user_meta($user_id, 'biographie_en', true) ?: '' ) ),
'domaines_tags' => $domaines_tags,
'domaines' => wpautop( make_clickable( get_user_meta($user_id, 'autres_domaines_de_recherches', true) ?: '' ) ),
'domaines_en' => wpautop( make_clickable( get_user_meta($user_id, 'autres_domaines_de_recherches_en', true) ?: '' ) ),
'recherches' => wpautop( get_user_meta($user_id, 'recherches_en_cours', true) ?: '' ),
'recherches_en' => wpautop( get_user_meta($user_id, 'recherches_en_cours_en', true) ?: '' ),
'axes' => $axes,
'titre_these' => thalim_bilingual(get_user_meta($user_id, 'titre_de_these', true) ?: '', $lang),
'date_soutenance' => get_user_meta($user_id, 'date_de_soutenance', true) ?: '',
'directeur_thalim'=> $directeur_thalim,
'autre_directeur' => get_user_meta($user_id, 'autre_directeur_de_these', true) ?: '',
'resume_these' => wpautop( get_user_meta($user_id, 'resume_de_la_these', true) ?: '' ),
'resume_these_en' => wpautop( get_user_meta($user_id, 'resume_de_la_these_en', true) ?: '' ),
'email' => $show_email ? $user->user_email : '',
'liens_externes' => $liens_externes,
'documents' => $documents,
'hal_publications_url' => (function() use ($user_id) {
$hal_id = get_user_meta($user_id, 'identifiant_hal', true) ?: '';
return $hal_id
? 'https://hal.science/search/index/?qa[authIdHal_s][]=' . rawurlencode($hal_id) . '&sort=publicationDate_tdate+desc'
: '';
})(),
'user_since' => date_i18n('d/m/Y', strtotime($user->user_registered)),
];
}
/**
* Query all posts linked to a member and group them by primary category.
* Returns an array sorted by post count (descending).
*/
function thalim_get_author_posts_by_category($user_id) {
$excluded_cats = [12, 31]; // séances de séminaire, etc.
$lang = thalim_current_language();
$posts = Timber::get_posts([
'post_type' => 'post',
'posts_per_page' => -1,
'meta_query' => [
'relation' => 'OR',
[
'key' => 'membres',
'value' => $user_id,
],
[
'key' => 'autre_membres',
'value' => $user_id,
],
],
'thalim_event_date_order' => true,
'lang' => '',
]);
$groups = [];
foreach ($posts as $post) {
$categories = wp_get_post_categories($post->ID, ['fields' => 'all']);
$primary_cat = null;
foreach ($categories as $cat) {
if (in_array($cat->term_id, $excluded_cats)) continue;
$primary_cat = $cat;
break;
}
if (!$primary_cat) continue;
$cat_id = $primary_cat->term_id;
if (!isset($groups[$cat_id])) {
// A top-level category with subcategories → these posts are "Autres"
$is_autres = false;
if ($primary_cat->parent == 0) {
$subcats = get_categories(['parent' => $cat_id, 'hide_empty' => true, 'exclude' => $excluded_cats]);
$is_autres = !empty($subcats);
}
$groups[$cat_id] = [
'cat_id' => $cat_id,
'cat_name' => $is_autres
? ($lang === 'en' ? 'Other ' : 'Autres ') . thalim_cat_name($primary_cat, $lang)
: thalim_cat_name($primary_cat, $lang),
'cat_url' => $is_autres
? trailingslashit(get_category_link($cat_id)) . 'autres/'
: get_category_link($cat_id),
'posts' => [],
];
}
$groups[$cat_id]['posts'][] = $post;
}
// Séances de séminaire — dedicated group. Posts in cat 12 where the member
// is listed in `membres`/`autre_membres`. Cards use the parent séminaire
// permalink with a #seance-{ID} hash (see thalim_get_card_data).
$seances = Timber::get_posts([
'post_type' => 'post',
'posts_per_page' => -1,
'category__in' => [12],
'meta_query' => [
'relation' => 'OR',
[ 'key' => 'membres', 'value' => $user_id ],
[ 'key' => 'autre_membres', 'value' => $user_id ],
],
'thalim_event_date_order' => true,
'lang' => '',
]);
if (count($seances) > 0) {
$seance_cat = get_term(12, 'category');
$groups[12] = [
'cat_id' => 12,
'cat_name' => $seance_cat && !is_wp_error($seance_cat)
? thalim_cat_name($seance_cat, $lang)
: ($lang === 'en' ? 'Seminar sessions' : 'Séances de séminaire'),
'cat_url' => get_category_link(12),
'posts' => $seances,
];
}
// Resolve card data and sort by count descending
foreach ($groups as &$group) {
$group['cards'] = thalim_get_cards_data($group['posts']);
}
unset($group);
uasort($groups, function($a, $b) {
$oa = (int) get_term_meta($a['cat_id'], 'ordre_profil', true) ?: 999;
$ob = (int) get_term_meta($b['cat_id'], 'ordre_profil', true) ?: 999;
return $oa !== $ob
? $oa <=> $ob
: count($b['posts']) <=> count($a['posts']);
});
return array_values($groups);
}

244
inc/membres-helpers.php Normal file
View File

@@ -0,0 +1,244 @@
<?php
/**
* Normalise a free-text research-domain field for use in a data-* attribute:
* converts <br> variants to \n and strips any remaining HTML tags.
*/
function thalim_sanitize_domaines( $raw ) {
// Normalise all <br> variants (including \r before them) to a newline
$text = preg_replace( '/\r?<br\s*\/?>/i', "\n", $raw );
// Strip any remaining HTML tags
$text = strip_tags( $text );
// Clean up excess blank lines / whitespace
$text = preg_replace( "/\n{3,}/", "\n\n", trim( $text ) );
return $text;
}
/**
* Build the display data array for a single user.
*/
function thalim_build_membre_data( $user ) {
$lang = thalim_current_language();
$status_parts = [];
$role_names = [];
for ( $n = 1; $n <= 3; $n++ ) {
$role_id = get_user_meta( $user->ID, 'role_' . $n, true );
if ( ! $role_id ) continue;
$term = get_term( intval( $role_id ), 'role' );
if ( ! $term || is_wp_error( $term ) ) continue;
$role_names[] = $term->name;
$override = thalim_bilingual( get_user_meta( $user->ID, 'affichage_du_statut_' . $n, true ) ?: '', $lang );
if ( $override ) {
$status_parts[] = $override;
} else {
$entry = $term->name;
$complement = thalim_bilingual( get_user_meta( $user->ID, 'complement_de_role_' . $n, true ) ?: '', $lang );
if ( $complement ) $entry .= ' ' . $complement;
$status_parts[] = $entry;
}
}
// Avatar (Simple Local Avatar with Gravatar fallback)
$avatar_url = thalim_get_user_avatar_url( $user->ID );
// Domaines de recherches: multiple usermeta rows, each is a post_tag term ID
$domaine_ids = get_user_meta( $user->ID, 'domaines_de_recherches', false );
$domaines = [];
foreach ( $domaine_ids as $term_id ) {
$term = get_term( intval( $term_id ), 'post_tag' );
if ( $term && ! is_wp_error( $term ) ) {
$domaines[] = $term->name;
}
}
return [
'display_name' => $user->display_name,
'sort_key' => thalim_get_sort_key( $user->ID, $user->display_name ),
'url' => get_author_posts_url( $user->ID ),
'status' => implode( ', ', $status_parts ),
'affiliation' => (function() use ($user) {
$v = get_user_meta( $user->ID, 'affiliation', true ) ?: '';
return strtolower( $v ) === 'autre'
? ( get_user_meta( $user->ID, 'affiliation_autre', true ) ?: '' )
: $v;
})(),
'role_names' => $role_names,
'avatar_url' => $avatar_url,
'domaines' => $domaines,
'autres_domaines' => thalim_sanitize_domaines( get_user_meta( $user->ID, 'autres_domaines_de_recherches', true ) ?: '' ),
];
}
/**
* Return all role taxonomy terms that are in use, sorted by name.
*/
function thalim_get_role_terms() {
$terms = get_terms( [ 'taxonomy' => 'role', 'hide_empty' => true, 'orderby' => 'name' ] );
if ( is_wp_error( $terms ) ) return [];
return array_values( array_map(
fn( $t ) => [ 'id' => $t->term_id, 'name' => $t->name ],
array_filter( $terms, fn( $t ) => ! in_array( mb_strtolower( $t->name, 'UTF-8' ), [ 'archive', 'à ranger' ], true ) )
) );
}
/**
* Return all role term_ids set for a user (role_1, role_2, role_3).
*/
function thalim_get_user_role_ids( $user_id ) {
$ids = [];
for ( $n = 1; $n <= 3; $n++ ) {
$role_id = get_user_meta( $user_id, 'role_' . $n, true );
if ( $role_id ) $ids[] = intval( $role_id );
}
return $ids;
}
/**
* Sort key: first word of last_name user meta (handles compound last names like
* "Duclaux de l'Estoile" → "Duclaux"). Falls back to last word of display_name.
*/
function thalim_get_sort_key( $user_id, $display_name ) {
$last = get_user_meta( $user_id, 'last_name', true );
if ( $last ) {
$parts = explode( ' ', trim( $last ) );
return $parts[0];
}
$parts = explode( ' ', trim( $display_name ) );
return end( $parts );
}
/**
* Return all member groups for the /membres page.
* Each group: ['title' => string, 'members' => array of member data arrays].
* Empty groups are omitted.
*/
function thalim_get_membres_groups() {
// Fetch all users that have role_1 set
$users = get_users( [
'meta_key' => 'role_1',
'number' => -1,
] );
// Direction: read directeur and directeur_adjoint from "Le laboratoire" page
$labo_page = get_page_by_path( 'le-laboratoire' );
$directeur_id = $labo_page ? intval( get_post_meta( $labo_page->ID, 'directeur', true ) ) : 0;
$adjoint_id = $labo_page ? intval( get_post_meta( $labo_page->ID, 'directeur_adjoint', true ) ) : 0;
$direction_users = [];
foreach ( [ $directeur_id, $adjoint_id ] as $uid ) {
if ( $uid ) {
$u = get_userdata( $uid );
if ( $u ) $direction_users[] = $u;
}
}
// Pre-build member data for all relevant users (cache by ID)
$member_cache = [];
$all_users = array_merge( $users, $direction_users );
foreach ( $all_users as $user ) {
if ( ! isset( $member_cache[ $user->ID ] ) ) {
$member_cache[ $user->ID ] = thalim_build_membre_data( $user );
}
}
// Prepend direction title to status for director / deputy director
if ( $directeur_id && isset( $member_cache[ $directeur_id ] ) ) {
$existing = $member_cache[ $directeur_id ]['status'];
$member_cache[ $directeur_id ]['status'] = 'Directeur' . ( $existing ? ', ' . $existing : '' );
}
if ( $adjoint_id && isset( $member_cache[ $adjoint_id ] ) ) {
$existing = $member_cache[ $adjoint_id ]['status'];
$member_cache[ $adjoint_id ]['status'] = 'Directeur adjoint' . ( $existing ? ', ' . $existing : '' );
}
// Build a slug→ID map for the 'role' taxonomy so group definitions survive
// database migrations where auto-incremented term IDs change.
$slug_to_id = [];
foreach ( get_terms( [ 'taxonomy' => 'role', 'hide_empty' => false ] ) as $term ) {
$slug_to_id[ $term->slug ] = $term->term_id;
}
$by_slug = fn( ...$slugs ) => array_values(
array_filter( array_map( fn( $s ) => $slug_to_id[ $s ] ?? null, $slugs ) )
);
// Group definitions (title => role slugs that qualify a user for membership)
$group_definitions = [
'Chercheuses et chercheurs CNRS' => $by_slug( 'directeur-de-recherche', 'charge-de-recherche' ),
'Enseignantes-chercheuses et enseignants-chercheurs' => $by_slug( 'professeur', 'maitre-de-conferences' ),
'Doctorantes et doctorants' => $by_slug( 'doctorant' ),
'Docteures et docteurs' => $by_slug( 'docteur' ),
'Postdoctorantes et postdoctorants' => $by_slug( 'postdoctorant' ),
'Personnel contractuel' => $by_slug( 'personnel-contractuel' ),
"Personnel d'accompagnement à la recherche" => $by_slug( 'personnel-technique' ),
'Membres associées et membres associés' => $by_slug( 'membre-associe' ),
'Anciennes et anciens membres' => $by_slug( 'anciens-membres' ),
];
$groups = [];
// Direction group first: directeur before directeur adjoint
$direction_members = [];
if ( $directeur_id && isset( $member_cache[ $directeur_id ] ) ) {
$direction_members[] = $member_cache[ $directeur_id ];
}
if ( $adjoint_id && isset( $member_cache[ $adjoint_id ] ) ) {
$direction_members[] = $member_cache[ $adjoint_id ];
}
if ( ! empty( $direction_members ) ) {
$groups[] = [
'title' => 'Direction',
'members' => $direction_members,
'fixed_order' => true,
];
}
// Role-based groups (a user appears in every group that matches any of their roles)
foreach ( $group_definitions as $title => $term_ids ) {
$group_members = [];
foreach ( $users as $user ) {
$user_role_ids = thalim_get_user_role_ids( $user->ID );
if ( array_intersect( $term_ids, $user_role_ids ) ) {
$group_members[] = $member_cache[ $user->ID ];
}
}
if ( empty( $group_members ) ) continue;
// Sort alphabetically by last name, accent- and case-insensitive (fr locale)
static $collator = null;
if ( $collator === null ) {
$collator = class_exists( 'Collator' ) ? new Collator( 'fr_FR' ) : false;
if ( $collator ) $collator->setStrength( Collator::PRIMARY );
}
usort( $group_members, function( $a, $b ) use ( $collator ) {
$la = $a['sort_key'];
$lb = $b['sort_key'];
if ( $collator ) return $collator->compare( $la, $lb );
return strcmp( mb_strtolower( $la, 'UTF-8' ), mb_strtolower( $lb, 'UTF-8' ) );
} );
// In "Personnel d'accompagnement", place "Gestion et pilotage" first
$fixed = false;
if ( $title === "Personnel d'accompagnement à la recherche" ) {
$priority = [];
$rest = [];
foreach ( $group_members as $m ) {
if ( stripos( $m['status'], 'Gestion et pilotage' ) !== false ) {
$priority[] = $m;
} else {
$rest[] = $m;
}
}
$group_members = array_merge( $priority, $rest );
$fixed = true;
}
$groups[] = [
'title' => $title,
'members' => $group_members,
'fixed_order' => $fixed,
];
}
return $groups;
}

View File

@@ -0,0 +1,94 @@
<?php
/**
* Désactiver le required des champs masqués par la logique conditionnelle de Pods.
*
* Pods ne prend pas en compte sa propre logique conditionnelle lors de la
* validation serveur des champs requis. Ce filtre corrige ce comportement
* pour le pod "post".
*/
add_filter( 'pods_api_pre_save_pod_item_post', 'thalim_skip_required_for_hidden_fields', 10, 3 );
function thalim_skip_required_for_hidden_fields( $pieces, $is_new_item, $id ) {
if ( empty( $pieces['fields'] ) ) {
return $pieces;
}
// Récupérer les valeurs actuelles des champs pour évaluer la logique conditionnelle
$field_values = [];
foreach ( $pieces['fields'] as $name => $field ) {
$field_values[ $name ] = isset( $field['value'] ) ? $field['value'] : '';
}
// Pour un post existant, si un champ n'a pas été soumis explicitement via
// pods_meta_* (ex. Pods DFV React en éditeur classique), remplir sa valeur
// depuis la BDD. Cela corrige à la fois :
// - la validation du champ lui-même (pieces['fields']['value'])
// - l'évaluation de la logique conditionnelle des autres champs ($field_values)
if ( ! $is_new_item && $id ) {
foreach ( $pieces['fields'] as $name => $field ) {
// Skip pick/file/avatar fields: their value format in $pieces is complex
// and get_post_meta returns a raw value that corrupts Pods' pick processing.
// These fields are always submitted via POST by Pods DFV React.
$field_type = pods_v( 'type', $field, '' );
if ( in_array( $field_type, [ 'pick', 'file', 'avatar' ], true ) ) {
continue;
}
$current_val = isset( $field['value'] ) ? $field['value'] : '';
if ( ( '' === $current_val || null === $current_val ) && ! isset( $_POST[ 'pods_meta_' . $name ] ) ) {
$db_val = get_post_meta( (int) $id, $name, true );
if ( '' !== $db_val && null !== $db_val ) {
$pieces['fields'][ $name ]['value'] = $db_val;
$field_values[ $name ] = $db_val;
}
}
}
}
foreach ( $pieces['fields'] as $field_name => $field_data ) {
// Ne traiter que les champs required
$required = is_object( $field_data ) && method_exists( $field_data, 'get_field_object' )
? (int) $field_data->get_field_object()->get_arg( 'required', 0 )
: (int) pods_v( 'required', $field_data, 0 );
if ( 1 !== $required ) {
continue;
}
// Récupérer la logique conditionnelle
$conditional_logic = null;
if ( is_object( $field_data ) && method_exists( $field_data, 'get_field_object' ) ) {
$conditional_logic = $field_data->get_field_object()->get_conditional_logic();
}
// Fallback : charger le champ via l'API Pods
if ( ! $conditional_logic ) {
$field_obj = pods_api()->load_field( [
'name' => $field_name,
'pod' => 'post',
] );
if ( $field_obj && method_exists( $field_obj, 'get_conditional_logic' ) ) {
$conditional_logic = $field_obj->get_conditional_logic();
}
}
if ( ! $conditional_logic ) {
continue;
}
// Évaluer si le champ est visible avec les valeurs actuelles
if ( ! $conditional_logic->is_visible( $field_values ) ) {
// Le champ est masqué → désactiver le required
if ( is_object( $field_data ) && method_exists( $field_data, 'get_field_object' ) ) {
$field_data->get_field_object()->set_arg( 'required', 0 );
}
$pieces['fields'][ $field_name ]['required'] = 0;
if ( isset( $pieces['fields'][ $field_name ]['options'] ) ) {
$pieces['fields'][ $field_name ]['options']['required'] = 0;
}
}
}
return $pieces;
}

View File

@@ -0,0 +1,195 @@
<?php
/**
* Intercepte les erreurs de validation Pods lors du save d'un post admin :
* - Empêche wp_die() (qui publiait quand même le post sans les champs Pods)
* - Annule le changement de statut si le post n'était pas encore publié
* - Redirige vers la page d'édition avec les champs restaurés via transient
*/
// Capture l'ID du post en cours de sauvegarde (avant Pods, qui peut le créer pour les nouveaux posts)
add_action( 'save_post', function ( $post_id ) {
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
$GLOBALS['thalim_saving_post_id'] = $post_id;
}, 1 );
// Intercepte wp_die() de Pods pendant un save admin
add_filter( 'pods_error_die', function ( $die, $error ) {
if ( ! is_admin() || ! isset( $_REQUEST['action'] ) || $_REQUEST['action'] !== 'editpost' ) {
return $die;
}
$post_id = $GLOBALS['thalim_saving_post_id'] ?? intval( $_POST['post_ID'] ?? 0 );
if ( ! $post_id ) {
return $die;
}
$user_id = get_current_user_id();
// Collecter les valeurs Pods soumises
$restore = [];
foreach ( $_POST as $key => $val ) {
if ( str_starts_with( $key, 'pods_meta_' ) ) {
$restore[ $key ] = is_array( $val ) ? array_map( 'wp_unslash', $val ) : wp_unslash( $val );
}
}
$error_text = is_wp_error( $error ) ? $error->get_error_message() : (string) $error;
$restore['_msg'] = wp_strip_all_tags( $error_text );
$restore['_title'] = sanitize_text_field( wp_unslash( $_POST['post_title'] ?? '' ) );
set_transient( 'thalim_pods_restore_' . $post_id . '_' . $user_id, $restore, 10 * MINUTE_IN_SECONDS );
$GLOBALS['thalim_pods_error_post_id'] = $post_id;
return false; // empêche wp_die()
}, 10, 2 );
// Après le save : rediriger vers la page d'édition + annuler le statut si besoin
add_filter( 'redirect_post_location', function ( $location ) {
$post_id = $GLOBALS['thalim_pods_error_post_id'] ?? 0;
if ( ! $post_id ) {
return $location;
}
// Annuler le changement de statut vers publish si le post n'était pas encore publié
$original = isset( $_POST['original_post_status'] ) ? sanitize_key( $_POST['original_post_status'] ) : '';
$post = get_post( $post_id );
if (
$post &&
in_array( $post->post_status, [ 'publish', 'future', 'pending' ], true ) &&
! in_array( $original, [ 'publish', 'future', 'pending' ], true )
) {
global $wpdb;
$wpdb->update(
$wpdb->posts,
[ 'post_status' => $original ?: 'draft' ],
[ 'ID' => $post_id ],
[ '%s' ],
[ '%d' ]
);
clean_post_cache( $post_id );
}
return admin_url( 'post.php?post=' . $post_id . '&action=edit' );
}, 10 );
// Sur la page d'édition (GET) : lire le transient une seule fois → global → supprimer
add_action( 'current_screen', function ( $screen ) {
if ( $screen->base !== 'post' ) {
return;
}
$post_id = isset( $_GET['post'] ) ? intval( $_GET['post'] ) : 0;
if ( ! $post_id ) {
return;
}
$user_id = get_current_user_id();
$key = 'thalim_pods_restore_' . $post_id . '_' . $user_id;
$data = get_transient( $key );
if ( ! $data ) {
return;
}
$GLOBALS['thalim_pods_restore'] = [
'post_id' => $post_id,
'data' => $data,
];
delete_transient( $key );
} );
// Injecter les valeurs dans get_post_meta → Pods DFV les embarque dans son JSON React
add_filter( 'get_post_metadata', function ( $value, $object_id, $meta_key, $single ) {
$restore = $GLOBALS['thalim_pods_restore'] ?? null;
if ( ! $restore || $restore['post_id'] !== (int) $object_id ) {
return $value;
}
$pods_key = 'pods_meta_' . $meta_key;
if ( ! isset( $restore['data'][ $pods_key ] ) ) {
return $value;
}
$val = $restore['data'][ $pods_key ];
return $single ? $val : [ $val ];
}, 10, 4 );
// Restauration JS : titre + champs Pods select/pick via PodsDFV (même pattern que les modales)
add_action( 'admin_footer', function () {
$restore = $GLOBALS['thalim_pods_restore'] ?? null;
if ( ! $restore ) {
return;
}
$screen = get_current_screen();
if ( ! $screen || $screen->base !== 'post' ) {
return;
}
$post_id = intval( $restore['post_id'] );
$title = $restore['data']['_title'] ?? '';
// Construire le map fieldName => value pour les champs Pods
$fields = [];
foreach ( $restore['data'] as $key => $val ) {
if ( str_starts_with( $key, 'pods_meta_' ) ) {
$fields[ substr( $key, 10 ) ] = $val;
}
}
?>
<script>
(function ($) {
var postId = <?php echo $post_id; ?>;
var fields = <?php echo wp_json_encode( $fields ); ?>;
var title = <?php echo wp_json_encode( $title ); ?>;
function doRestore() {
// Titre WordPress (non géré par get_post_metadata)
var $titleInput = $('#title');
if ($titleInput.length && !$titleInput.val() && title) {
$titleInput.val(title).trigger('input');
}
// Champs Pods : DOM direct + PodsDFV pour les selects/picks
Object.keys(fields).forEach(function (fieldName) {
var value = fields[fieldName];
var $el = $('[name="pods_meta_' + fieldName + '"]');
if ($el.length) {
$el.val(value).trigger('change');
}
if (window.PodsDFV && postId) {
try {
window.PodsDFV.setFieldValue('post', postId, fieldName, value, 0);
} catch (e) {}
}
});
}
$(window).on('load', function () {
setTimeout(doRestore, 300);
});
}(jQuery));
</script>
<?php
} );
// Afficher le message d'erreur en admin notice
add_action( 'admin_notices', function () {
$restore = $GLOBALS['thalim_pods_restore'] ?? null;
if ( ! $restore ) {
return;
}
$screen = get_current_screen();
if ( ! $screen || $screen->base !== 'post' ) {
return;
}
$msg = esc_html( $restore['data']['_msg'] ?? '' );
if ( $msg ) {
echo '<div class="notice notice-error is-dismissible"><p>' . $msg . '</p></div>';
}
echo '<div class="notice notice-info is-dismissible"><p>Votre contenu a &eacute;t&eacute; restaur&eacute;. V&eacute;rifiez les champs obligatoires avant de republier.</p></div>';
} );

172
inc/post-card-helpers.php Normal file
View File

@@ -0,0 +1,172 @@
<?php
/**
* Build card display data for a single post.
* Resolves Pods relationship fields (stored as multiple meta rows) into
* ready-to-display values so Twig templates don't need to call PHP functions.
*
* Returns an associative array with resolved card fields.
*/
function thalim_get_card_data($post_id) {
$data = [
'card_image' => null,
'card_membres' => [],
'card_axes' => [],
'card_etiquettes' => [],
'parent_slug' => '',
'card_category_name' => '',
'card_category_url' => '',
'card_type' => '',
'card_event_date' => '',
'card_event_date_iso' => '',
'card_link' => '',
];
// Event date — date_de_debut (events), fallback to datetime (communications)
// Used for display instead of post_date when set
foreach (['date_de_debut', 'datetime'] as $date_key) {
$event_raw = get_post_meta($post_id, $date_key, true) ?: '';
if ($event_raw && !str_starts_with($event_raw, '0000-00-00')) {
$ts = strtotime($event_raw);
if ($ts) {
$data['card_event_date'] = date_i18n('d/m/Y', $ts);
$data['card_event_date_iso'] = date('Y-m-d', $ts);
break;
}
}
}
// Resolve top-level parent category slug for color theming and direct category name for display
$categories = wp_get_post_categories($post_id, ['fields' => 'all']);
$excluded_ids = [12, 31];
$is_seance = false;
foreach ($categories as $cat) {
if ($cat->term_id === 12) { $is_seance = true; }
}
foreach ($categories as $cat) {
if (in_array($cat->term_id, $excluded_ids)) continue;
$ancestor_ids = get_ancestors($cat->term_id, 'category');
if (!empty($ancestor_ids)) {
$root = get_category(end($ancestor_ids));
} else {
$root = $cat;
}
$data['parent_slug'] = $root->slug;
$data['card_category_name'] = thalim_cat_name($cat);
$data['card_category_url'] = get_category_link($cat->term_id);
break;
}
// Séances de séminaire: link to parent séminaire with hash, derive color from parent's categories
if ($is_seance) {
// Always show the category label for séances even though cat 12 is excluded from color resolution
if (!$data['card_category_name']) {
$seance_cat = get_category(12);
if ($seance_cat) {
$data['card_category_name'] = thalim_cat_name($seance_cat);
$data['card_category_url'] = get_category_link(12);
}
}
global $wpdb;
$parent_id = $wpdb->get_var($wpdb->prepare(
"SELECT pm.post_id FROM {$wpdb->postmeta} pm
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = 'seances' AND pm.meta_value = %s
AND p.post_status = 'publish'
LIMIT 1",
(string) $post_id
));
if ($parent_id) {
$data['card_link'] = get_permalink((int) $parent_id) . '#seance-' . $post_id;
// Derive color from parent séminaire's categories if not already set
if (!$data['parent_slug']) {
foreach (wp_get_post_categories((int) $parent_id, ['fields' => 'all']) as $cat) {
if (in_array($cat->term_id, $excluded_ids)) continue;
$ancestor_ids = get_ancestors($cat->term_id, 'category');
$root = !empty($ancestor_ids) ? get_category(end($ancestor_ids)) : $cat;
$data['parent_slug'] = $root->slug;
break;
}
}
}
}
// Type label (first non-empty type_* field)
$type_fields = [
'type_colloque_journee_d_etude',
'type_soutenance',
'type_evenement_culturel',
'type_media',
'type_captation',
'type_revue_collection',
'type_autre',
];
foreach ($type_fields as $field) {
$val = get_post_meta($post_id, $field, true);
if ($val) {
$data['card_type'] = $val;
break;
}
}
// First image from documents_joints
$doc_ids = get_post_meta($post_id, 'documents_joints', false);
foreach ($doc_ids as $doc_id) {
$mime = get_post_mime_type($doc_id);
if ($mime && str_starts_with($mime, 'image/')) {
$src = wp_get_attachment_image_url($doc_id, 'medium');
if ($src) {
$data['card_image'] = $src;
break;
}
}
}
// Members (user IDs → display names + profile URLs)
// Falls back to autre_membres if membres is empty
$membre_ids = get_post_meta($post_id, 'membres', false);
if (empty($membre_ids)) {
$membre_ids = get_post_meta($post_id, 'autre_membres', false);
}
foreach ($membre_ids as $uid) {
$user = get_userdata($uid);
if ($user) {
$data['card_membres'][] = [
'name' => $user->display_name,
'url' => get_author_posts_url($uid),
];
}
}
// Axes thématiques (post IDs → titles)
$axe_ids = get_post_meta($post_id, 'axes_thematiques', false);
foreach ($axe_ids as $axe_id) {
$axe = get_post($axe_id);
if ($axe) {
$data['card_axes'][] = $axe->post_title;
}
}
// Etiquettes (post IDs → titles)
$tag_ids = get_post_meta($post_id, 'etiquettes', false);
foreach ($tag_ids as $tag_id) {
$tag_post = get_post($tag_id);
if ($tag_post) {
$data['card_etiquettes'][] = $tag_post->post_title;
}
}
return $data;
}
/**
* Build card data map for a collection of posts.
* Returns an array keyed by post ID.
*/
function thalim_get_cards_data($posts) {
$cards = [];
foreach ($posts as $post) {
$cards[$post->ID] = thalim_get_card_data($post->ID);
}
return $cards;
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* Require a non-empty title when saving a post from the admin.
*
* Uses the same transient / redirect / restore mechanism as pods-save-error-handler.php
* so content is never lost: the post saves (with empty title), the status is reverted
* to draft if needed, the editor reopens with all fields restored and an error notice.
*/
add_action( 'save_post', 'thalim_check_post_title_required', 5 );
function thalim_check_post_title_required( $post_id ) {
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( ! is_admin() ) {
return;
}
if ( ( $_POST['action'] ?? '' ) !== 'editpost' ) {
return;
}
$post = get_post( $post_id );
if ( ! $post || ! post_type_supports( $post->post_type, 'title' ) ) {
return;
}
// Title was provided — nothing to do.
if ( trim( wp_unslash( $_POST['post_title'] ?? '' ) ) !== '' ) {
return;
}
// Title is empty: store restore transient and signal the redirect handler
// (same keys as pods-save-error-handler.php so everything is shared).
$user_id = get_current_user_id();
$restore = [];
foreach ( $_POST as $key => $val ) {
if ( str_starts_with( $key, 'pods_meta_' ) ) {
$restore[ $key ] = is_array( $val )
? array_map( 'wp_unslash', $val )
: wp_unslash( $val );
}
}
$restore['_msg'] = __( 'Le champ Titre est obligatoire.', 'thalim' );
$restore['_title'] = ''; // intentionally empty — user must fill it
set_transient(
'thalim_pods_restore_' . $post_id . '_' . $user_id,
$restore,
10 * MINUTE_IN_SECONDS
);
// Signal redirect_post_location (defined in pods-save-error-handler.php):
// it will revert the post status if needed and redirect to the edit screen.
$GLOBALS['thalim_pods_error_post_id'] = $post_id;
}

447
inc/single-helpers.php Normal file
View File

@@ -0,0 +1,447 @@
<?php
/**
* Format a Pods datetime string (e.g. "2026-01-17 00:00:00") into natural French/English.
* Shows time only if not midnight.
*/
function thalim_format_date($raw, $lang = 'fr') {
if (!$raw || str_starts_with($raw, '0000-00-00')) return '';
$ts = strtotime($raw);
if ($ts === false || $ts < 0) return '';
return date_i18n('j F Y', $ts);
}
/**
* Resolve all Pods custom fields for a single post into a display-ready array.
*/
function thalim_get_single_data($post_id) {
$lang = thalim_current_language();
$data = [
// Text fields
'sous_titre' => thalim_bilingual( get_post_meta($post_id, 'sous-titre', true) ?: '', $lang ),
'reference_bibliographique' => get_post_meta($post_id, 'reference_bibliographique', true) ?: '',
'editeur' => get_post_meta($post_id, 'editeur', true) ?: '',
'journal' => get_post_meta($post_id, 'journal', true) ?: '',
'lieu' => thalim_bilingual( get_post_meta($post_id, 'lieu', true) ?: '', $lang ),
'adresse' => get_post_meta($post_id, 'adresse', true) ?: '',
'autrepersonnes' => get_post_meta($post_id, 'autrepersonnes', true) ?: '',
'autre_autrepersonnes' => get_post_meta($post_id, 'autre_autrepersonnes', true) ?: '',
'body_en' => apply_filters( 'the_content', get_post_meta($post_id, 'body_en', true) ?: '' ),
// Dates (formatted for display)
'datetime' => thalim_format_date(get_post_meta($post_id, 'datetime', true), $lang),
'date_de_debut' => '',
'date_de_fin' => '',
'date_debut_ymd' => '',
'date_fin_ymd' => '',
'heure_de_debut' => substr( get_post_meta($post_id, 'heure_de_debut', true) ?: '', 0, 5 ),
'heure_de_fin' => substr( get_post_meta($post_id, 'heure_de_fin', true) ?: '', 0, 5 ),
// URLs
'hal_url' => get_post_meta($post_id, 'hal_url', true) ?: '',
'hal_file' => get_post_meta($post_id, 'hal_file', true) ?: '',
'canal_u' => array_values( array_filter( array_map( function( $url ) {
if ( preg_match( '/(\d+)\/?$/', trim( $url ), $m ) ) {
return 'https://www.canal-u.tv/embed/' . $m[1] . '?t=0';
}
return '';
}, get_post_meta( $post_id, 'lien_canal_u', false ) ) ) ),
'youtube' => array_values( array_filter( array_map( function( $url ) {
$url = trim( $url );
// youtu.be/ID or youtube.com/embed/ID or youtube.com/watch?v=ID
if ( preg_match( '/(?:youtu\.be\/|youtube\.com\/(?:embed\/|watch\?.*v=|shorts\/))([A-Za-z0-9_-]{11})/', $url, $m ) ) {
return 'https://www.youtube-nocookie.com/embed/' . $m[1];
}
return '';
}, get_post_meta( $post_id, 'lien_youtube', false ) ) ) ),
// Resolved below
'liens_externes' => [],
'membres' => [],
'autre_membres' => [],
'autre_fonction_label' => '',
'axes' => [],
'etiquettes' => [],
'programmes' => [],
'annonces_liees' => [],
'seances_a_venir' => [],
'seances_passees' => [],
'show_image_titles' => (bool) get_post_meta($post_id, 'afficher_le_titre_des_images_en_legende', true),
'images' => [],
'documents' => [],
'type_label' => '',
'fonction_label' => '',
'parent_slug' => '',
'parent_name' => '',
'parent_link' => '',
'category_name' => '',
'category_link' => '',
];
// --- Dates ---
$raw_debut = get_post_meta($post_id, 'date_de_debut', true);
$raw_fin = get_post_meta($post_id, 'date_de_fin', true);
$ts_debut = ($raw_debut && !str_starts_with($raw_debut, '0000-00-00')) ? strtotime($raw_debut) : 0;
$ts_fin = ($raw_fin && !str_starts_with($raw_fin, '0000-00-00')) ? strtotime($raw_fin) : 0;
$data['date_de_debut'] = thalim_format_date($raw_debut, $lang);
$data['date_de_fin'] = thalim_format_date($raw_fin, $lang);
if ($ts_debut) $data['date_debut_ymd'] = date('Y-m-d', $ts_debut);
if ($ts_fin) $data['date_fin_ymd'] = date('Y-m-d', $ts_fin);
// --- External links (up to 3) ---
for ($i = 1; $i <= 3; $i++) {
$url = get_post_meta($post_id, 'lien_externe_' . $i, true);
if ($url) {
$titre = thalim_bilingual( get_post_meta($post_id, 'titre_du_lien_externe_' . $i, true) ?: '', $lang );
if (!$titre) {
$host = parse_url($url, PHP_URL_HOST) ?: $url;
$parts = explode('.', $host);
$titre = count($parts) >= 2 ? implode('.', array_slice($parts, -2)) : $host;
}
$data['liens_externes'][] = [
'url' => $url,
'titre' => $titre,
];
}
}
// --- Category hierarchy for breadcrumb and color ---
$categories = wp_get_post_categories($post_id, ['fields' => 'all']);
$excluded_ids = [12, 31];
foreach ($categories as $cat) {
if (in_array($cat->term_id, $excluded_ids)) continue;
$ancestor_ids = get_ancestors($cat->term_id, 'category');
if (!empty($ancestor_ids)) {
$root = get_category(end($ancestor_ids));
$data['parent_slug'] = $root->slug;
$data['parent_name'] = $root->name;
$data['parent_link'] = get_category_link($root->term_id);
$data['category_name'] = $cat->name;
} else {
$data['parent_slug'] = $cat->slug;
$data['parent_name'] = $cat->name;
$data['parent_link'] = get_category_link($cat->term_id);
$data['category_name'] = $lang === 'en' ? 'Other' : 'Autre';
}
// category_link: for direct posts (no ancestors), point to the /autres index
$data['category_link'] = empty($ancestor_ids)
? trailingslashit(get_category_link($cat->term_id)) . 'autres/'
: get_category_link($cat->term_id);
break;
}
// --- Documents joints: split images vs files ---
$doc_ids = get_post_meta($post_id, 'documents_joints', false);
foreach ($doc_ids as $doc_id) {
$mime = get_post_mime_type($doc_id);
if (!$mime) continue;
if (str_starts_with($mime, 'image/')) {
$src = wp_get_attachment_image_url($doc_id, 'large');
if ($src) {
$meta = wp_get_attachment_metadata($doc_id);
$w = isset($meta['width']) ? $meta['width'] : 0;
$h = isset($meta['height']) ? $meta['height'] : 0;
$data['images'][] = [
'url' => $src,
'alt' => get_post_meta($doc_id, '_wp_attachment_image_alt', true) ?: '',
'caption' => thalim_bilingual(wp_get_attachment_caption($doc_id) ?: '', $lang),
'title' => thalim_bilingual(get_the_title($doc_id) ?: '', $lang),
'portrait' => ($h > $w),
];
}
} else {
$data['documents'][] = [
'url' => wp_get_attachment_url($doc_id),
'title' => thalim_bilingual(get_the_title($doc_id) ?: '', $lang) ?: basename(get_attached_file($doc_id)),
];
}
}
// --- Members (user IDs → name + profile URL) ---
foreach (get_post_meta($post_id, 'membres', false) as $uid) {
$user = get_userdata($uid);
if ($user) {
$data['membres'][] = [
'name' => $user->display_name,
'url' => get_author_posts_url($uid),
];
}
}
// --- Autre membres (user IDs → name + profile URL) ---
foreach (get_post_meta($post_id, 'autre_membres', false) as $uid) {
$user = get_userdata($uid);
if ($user) {
$data['autre_membres'][] = [
'name' => $user->display_name,
'url' => get_author_posts_url($uid),
];
}
}
// --- Axes thématiques (taxonomy term IDs) ---
$axe_ids = get_post_meta($post_id, 'axes_thematiques', false);
foreach ($axe_ids as $axe_id) {
$term = get_term(intval($axe_id), 'axe_thematique');
if ($term && !is_wp_error($term)) {
$data['axes'][] = [
'id' => $term->term_id,
'name' => thalim_bilingual($term->name, $lang),
'url' => get_term_link($term),
];
}
}
// --- Étiquettes (taxonomy term IDs) ---
$tag_ids = get_post_meta($post_id, 'etiquettes', false);
foreach ($tag_ids as $tag_id) {
$term = get_term(intval($tag_id), 'post_tag');
if ($term && !is_wp_error($term)) {
$data['etiquettes'][] = [
'id' => $term->term_id,
'name' => thalim_bilingual($term->name, $lang),
'url' => get_term_link($term),
];
}
}
// --- Programmes de recherche (taxonomy term IDs) ---
$prog_ids = get_post_meta($post_id, 'programmes_de_recherche', false);
foreach ($prog_ids as $prog_id) {
$term = get_term(intval($prog_id), 'programme_de_recherche');
if ($term && !is_wp_error($term)) {
$data['programmes'][] = [
'id' => $term->term_id,
'name' => thalim_bilingual($term->name, $lang),
'url' => get_term_link($term),
];
}
}
// --- Annonces liées (related posts) ---
$related_ids = get_post_meta($post_id, 'annonces_liees', false);
if (!empty($related_ids)) {
$data['annonces_liees'] = Timber::get_posts([
'post_type' => 'post',
'post__in' => array_map('intval', $related_ids),
'posts_per_page' => -1,
'lang' => '',
]);
}
// --- Séances (session posts) — split into upcoming / past ---
$seance_ids = get_post_meta($post_id, 'seances', false);
$data['seances_a_venir'] = [];
$data['seances_passees'] = [];
if (!empty($seance_ids)) {
$seance_posts = Timber::get_posts([
'post_type' => 'post',
'post__in' => array_map('intval', $seance_ids),
'posts_per_page' => -1,
'orderby' => 'meta_value',
'meta_key' => 'date_de_debut',
'order' => 'ASC',
'lang' => '',
'post_status' => ['publish', 'future'],
]);
$now = time();
$current_year = date('Y');
$months_fr = ['jan.', 'fév.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.'];
$months_en = ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.'];
foreach ($seance_posts as $seance) {
$raw_date = get_post_meta($seance->ID, 'date_de_debut', true);
$ts = $raw_date ? strtotime($raw_date) : strtotime($seance->post_date);
// Only expose date_fin when it's a different day than date_de_debut
$raw_fin = get_post_meta($seance->ID, 'date_de_fin', true);
$ts_fin = $raw_fin && !str_starts_with($raw_fin, '0000-00-00') ? strtotime($raw_fin) : false;
$date_fin_display = ($ts_fin && date('Y-m-d', $ts_fin) !== date('Y-m-d', $ts))
? thalim_format_date($raw_fin, $lang)
: '';
$month_idx = intval(date('n', $ts)) - 1;
$seance_data = [
'post' => $seance,
'day' => date('d', $ts),
'month' => ($lang === 'en') ? $months_en[$month_idx] : $months_fr[$month_idx],
'year' => (date('Y', $ts) !== $current_year) ? date('Y', $ts) : '',
'date_full' => thalim_format_date($raw_date, $lang),
'date_fin' => $date_fin_display,
'heure_de_debut' => substr( get_post_meta($seance->ID, 'heure_de_debut', true) ?: '', 0, 5 ),
'heure_de_fin' => substr( get_post_meta($seance->ID, 'heure_de_fin', true) ?: '', 0, 5 ),
'lieu' => thalim_bilingual( get_post_meta($seance->ID, 'lieu', true) ?: '', $lang ),
'adresse' => get_post_meta($seance->ID, 'adresse', true) ?: '',
'body_en' => apply_filters( 'the_content', get_post_meta($seance->ID, 'body_en', true) ?: '' ),
'intervenants' => [],
'images' => [],
'documents' => [],
'liens_externes' => [],
'annonces_liees' => [],
];
// Resolve intervenants (membres or autrepersonnes)
$m_ids = get_post_meta($seance->ID, 'membres', false);
if (empty($m_ids)) {
$m_ids = get_post_meta($seance->ID, 'autre_membres', false);
}
foreach ($m_ids as $uid) {
$user = get_userdata($uid);
if ($user) {
$seance_data['intervenants'][] = [
'name' => $user->display_name,
'url' => get_author_posts_url($uid),
];
}
}
$seance_data['autrepersonnes'] = get_post_meta($seance->ID, 'autrepersonnes', true) ?: '';
$seance_data['show_image_titles'] = (bool) get_post_meta($seance->ID, 'afficher_le_titre_des_images_en_legende', true);
// Documents joints: images and files
$s_doc_ids = get_post_meta($seance->ID, 'documents_joints', false);
foreach ($s_doc_ids as $doc_id) {
$mime = get_post_mime_type($doc_id);
if (!$mime) continue;
if (str_starts_with($mime, 'image/')) {
$src = wp_get_attachment_image_url($doc_id, 'large');
if ($src) {
$seance_data['images'][] = [
'url' => $src,
'alt' => get_post_meta($doc_id, '_wp_attachment_image_alt', true) ?: '',
'caption' => thalim_bilingual(wp_get_attachment_caption($doc_id) ?: '', $lang),
'title' => thalim_bilingual(get_the_title($doc_id) ?: '', $lang),
];
}
} else {
$seance_data['documents'][] = [
'url' => wp_get_attachment_url($doc_id),
'title' => thalim_bilingual(get_the_title($doc_id) ?: '', $lang) ?: basename(get_attached_file($doc_id)),
];
}
}
// External links (up to 3)
for ($i = 1; $i <= 3; $i++) {
$url = get_post_meta($seance->ID, 'lien_externe_' . $i, true);
if ($url) {
$titre = thalim_bilingual( get_post_meta($seance->ID, 'titre_du_lien_externe_' . $i, true) ?: '', $lang );
if (!$titre) {
$host = parse_url($url, PHP_URL_HOST) ?: $url;
$parts = explode('.', $host);
$titre = count($parts) >= 2 ? implode('.', array_slice($parts, -2)) : $host;
}
$seance_data['liens_externes'][] = ['url' => $url, 'titre' => $titre];
}
}
// Annonces liées
$s_related_ids = get_post_meta($seance->ID, 'annonces_liees', false);
if (!empty($s_related_ids)) {
$seance_data['annonces_liees'] = Timber::get_posts([
'post_type' => 'post',
'post__in' => array_map('intval', $s_related_ids),
'posts_per_page' => -1,
'lang' => '',
]);
}
if ($ts >= $now) {
$data['seances_a_venir'][] = $seance_data;
} else {
$data['seances_passees'][] = $seance_data;
}
}
// Past séances: most recent first
$data['seances_passees'] = array_reverse($data['seances_passees']);
}
// --- Type label (category-conditional type fields) ---
$type_fields = [
'type_colloque_journee_d_etude',
'type_soutenance',
'type_evenement_culturel',
'type_media',
'type_captation',
'type_revue_collection',
'type_autre',
];
foreach ($type_fields as $field) {
$val = get_post_meta($post_id, $field, true);
if ($val) {
$data['type_label'] = thalim_bilingual( $val, $lang );
break;
}
}
// --- Fonction label (first non-empty fonction_* field) ---
$fonction_fields = [
'fonction_auteur',
'fonction_organisation',
'fonction_intervention',
'fonction_redaction',
'fonction_realisation',
'fonction_dirige',
'fonction_responsable',
'fonction_candidat',
];
foreach ($fonction_fields as $field) {
$val = get_post_meta($post_id, $field, true);
if ($val) {
$data['fonction_label'] = thalim_bilingual( $val, $lang );
break;
}
}
// --- Autre fonction label (first non-empty autre_fonction_* field) ---
$autre_fonction_fields = [
'autre_fonction_autre',
'autre_fonction_concerne',
'autre_fonction_directeur',
'autre_fonction_direction_d_ouvrage',
'autre_fonction_intervenant',
'autre_fonction_participants',
];
foreach ($autre_fonction_fields as $field) {
$val = get_post_meta($post_id, $field, true);
if ($val) {
$data['autre_fonction_label'] = thalim_bilingual( $val, $lang );
break;
}
}
// --- Fallback: derive labels from Pods categorie ID for older posts ---
if (!$data['fonction_label'] || !$data['autre_fonction_label']) {
$pods_cat = get_post_meta($post_id, '_pods_categorie', true);
$cat_id = (is_array($pods_cat) && !empty($pods_cat)) ? intval($pods_cat[0]) : 0;
// Pods categorie ID → fonction label (main membres)
$cat_to_fonction = [
3 => 'Organisation', 4 => 'Auteur', 6 => 'Responsable',
8 => 'Organisation', 9 => 'Responsable', 10 => 'Organisation',
11 => 'Organisation', 12 => 'Intervention', 13 => 'Intervention',
14 => 'Candidat', 15 => 'Auteur', 16 => 'Auteur',
17 => 'Responsable', 18 => 'Organisation', 19 => 'Intervention',
21 => 'Rédaction', 22 => 'Réalisation', 23 => 'Intervention',
24 => 'Responsable', 25 => 'Responsable', 65 => 'Dirigé par',
];
// Pods categorie ID → autre_fonction label (autre membres)
$cat_to_autre_fonction = [
3 => 'Participants', 4 => "Direction d'ouvrage",
10 => 'Participants', 14 => 'Directeur de thèse',
15 => "Direction d'ouvrage", 16 => "Direction d'ouvrage",
19 => 'Membre concerné', 22 => 'Intervenant',
];
if (!$data['fonction_label'] && isset($cat_to_fonction[$cat_id])) {
$data['fonction_label'] = $cat_to_fonction[$cat_id];
}
if (!$data['autre_fonction_label'] && isset($cat_to_autre_fonction[$cat_id])) {
$data['autre_fonction_label'] = $cat_to_autre_fonction[$cat_id];
}
}
return $data;
}

263
index.php Executable file
View File

@@ -0,0 +1,263 @@
<?php
$context = Timber::context();
// Helper: split posts into active-pinned / normal and merge (pinned first, max_normal non-pinned)
$sort_with_pinned = function ($posts, string $pin_field, int $max_normal = PHP_INT_MAX) {
$today = date('Y-m-d');
$pinned = [];
$normal = [];
foreach ($posts as $post) {
$epingle = get_post_meta($post->ID, $pin_field, true);
$fin = get_post_meta($post->ID, 'date_de_fin_depinglage', true);
$active = $epingle == '1' && (empty($fin) || $fin === '0000-00-00' || $fin >= $today);
if ($active) { $pinned[] = $post; } else { $normal[] = $post; }
}
return array_merge($pinned, array_slice($normal, 0, $max_normal));
};
// --- Nombre d'items des diaporamas (depuis la page Le Laboratoire) ---
$labo_page = get_page_by_path('le-laboratoire');
$max_swiper = $labo_page ? intval(get_post_meta($labo_page->ID, 'nombres_ditems_des_diaporamas', true)) : 0;
if ($max_swiper < 1) $max_swiper = 10;
// --- Annonces diaporama ---
$annonces_raw = Timber::get_posts([
'post_type' => 'post',
'posts_per_page' => -1,
'meta_query' => [[
'key' => 'afficher_dans_le_diaporama_dannonces_page_daccueil',
'value' => '1',
]],
'orderby' => 'date',
'order' => 'DESC',
'lang' => '',
'thalim_event_date_order' => true,
]);
$context['annonces'] = $sort_with_pinned($annonces_raw, 'epingler_dans_le_diaporama_dannonces', $max_swiper);
$context['annonces_cards'] = thalim_get_cards_data($annonces_raw);
// --- Publications et productions diaporama ---
$publications_raw = Timber::get_posts([
'post_type' => 'post',
'posts_per_page' => 30,
'orderby' => 'date',
'order' => 'DESC',
'lang' => '',
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [4],
'operator' => 'IN',
'include_children' => true,
],
[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [16],
'operator' => 'NOT IN',
],
],
'meta_query' => [
'relation' => 'OR',
[
'key' => 'type_autre',
'value' => "Chapitre d'ouvrage",
'compare' => '!=',
],
[
'key' => 'type_autre',
'compare' => 'NOT EXISTS',
],
],
]);
$context['publications'] = $sort_with_pinned($publications_raw, 'epingler_dans_le_diaporama_des_publications_et_productions', $max_swiper);
$context['publications_cards'] = thalim_get_cards_data($publications_raw);
$context['publications_link'] = thalim_en_url( get_category_link(4) );
$context['annonces_link'] = thalim_en_url( get_permalink(29100) );
// --- Message du laboratoire ---
$messages_labo = Timber::get_posts([
'post_type' => 'post',
'posts_per_page' => 5,
'cat' => 268,
'orderby' => 'date',
'order' => 'DESC',
'lang' => '',
]);
$context['messages_labo'] = $messages_labo ?: [];
$context['message_labo_link'] = thalim_en_url( get_category_link(268) );
// --- Agenda (médiation scientifique + séances de séminaire à venir) ---
$agenda_lang = thalim_current_language();
$mediation_cat_ids = [5, 18, 19, 20, 21, 22, 23];
$months_fr = ['jan.', 'fév.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.'];
$months_en = ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.'];
$agenda_type_fields = [
'type_colloque_journee_d_etude', 'type_soutenance', 'type_evenement_culturel',
'type_media', 'type_captation', 'type_revue_collection', 'type_autre',
];
$now_str = date('Y-m-d H:i:s');
$make_agenda_item = function ($post, $raw_date, $type_label, $lieu, $link) use ($agenda_lang, $months_fr, $months_en) {
$ts = strtotime($raw_date);
if (!$ts) return null;
$month_idx = intval(date('n', $ts)) - 1;
return [
'post' => $post,
'ts' => $ts,
'day' => intval(date('j', $ts)),
'month' => $agenda_lang === 'en' ? $months_en[$month_idx] : $months_fr[$month_idx],
'type_label' => $type_label,
'lieu' => $lieu,
'link' => $link,
];
};
$agenda_items = [];
// 1. Upcoming médiation scientifique posts
$mediation_upcoming = Timber::get_posts([
'post_type' => 'post',
'posts_per_page' => 8,
'category__in' => $mediation_cat_ids,
'orderby' => ['date_clause' => 'ASC'],
'lang' => '',
'meta_query' => [
'date_clause' => [
'key' => 'date_de_debut',
'value' => $now_str,
'compare' => '>=',
'type' => 'DATETIME',
],
],
]);
foreach ($mediation_upcoming as $mpost) {
$raw_date = get_post_meta($mpost->ID, 'date_de_debut', true);
if (!$raw_date) continue;
$type_label = '';
foreach ($agenda_type_fields as $field) {
$val = get_post_meta($mpost->ID, $field, true);
if ($val) { $type_label = $val; break; }
}
if (!$type_label) {
foreach (get_the_category($mpost->ID) as $cat) {
if (in_array($cat->term_id, $mediation_cat_ids)) { $type_label = thalim_cat_name($cat); break; }
}
}
$item = $make_agenda_item($mpost, $raw_date, $type_label, get_post_meta($mpost->ID, 'lieu', true) ?: '', get_permalink($mpost->ID));
if ($item) $agenda_items[] = $item;
}
// 2. Upcoming séances de séminaire (cat 12)
$seances_upcoming = Timber::get_posts([
'post_type' => 'post',
'posts_per_page' => 8,
'category__in' => [12],
'orderby' => ['date_clause' => 'ASC'],
'lang' => '',
'meta_query' => [
'date_clause' => [
'key' => 'date_de_debut',
'value' => $now_str,
'compare' => '>=',
'type' => 'DATETIME',
],
],
]);
foreach ($seances_upcoming as $seance) {
$raw_date = get_post_meta($seance->ID, 'date_de_debut', true);
if (!$raw_date) continue;
// Direct DB lookup — bypasses Polylang and other hook filters
global $wpdb;
$parent_id = $wpdb->get_var($wpdb->prepare(
"SELECT pm.post_id FROM {$wpdb->postmeta} pm
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = 'seances' AND pm.meta_value = %s
AND p.post_status = 'publish'
LIMIT 1",
(string) $seance->ID
));
$link = $parent_id
? get_permalink((int) $parent_id) . '#seance-' . $seance->ID
: get_permalink($seance->ID);
$label = $agenda_lang === 'en' ? 'Seminar session' : 'Séance de séminaire';
$item = $make_agenda_item($seance, $raw_date, $label, get_post_meta($seance->ID, 'lieu', true) ?: '', $link);
if ($item) $agenda_items[] = $item;
}
// Sort merged list by date, keep 5 soonest
usort($agenda_items, fn($a, $b) => $a['ts'] <=> $b['ts']);
$agenda_items = array_slice($agenda_items, 0, 5);
// Fallback: if no upcoming events, show 5 most recent mediation events
if (empty($agenda_items)) {
$fallback = Timber::get_posts([
'post_type' => 'post',
'posts_per_page' => 5,
'category__in' => $mediation_cat_ids,
'orderby' => ['date_clause' => 'DESC'],
'lang' => '',
'meta_query' => [
'date_clause' => ['key' => 'date_de_debut', 'type' => 'DATETIME'],
],
]);
foreach ($fallback as $fpost) {
$raw_date = get_post_meta($fpost->ID, 'date_de_debut', true);
if (!$raw_date) continue;
$type_label = '';
foreach ($agenda_type_fields as $field) {
$val = get_post_meta($fpost->ID, $field, true);
if ($val) { $type_label = $val; break; }
}
if (!$type_label) {
foreach (get_the_category($fpost->ID) as $cat) {
if (in_array($cat->term_id, $mediation_cat_ids)) { $type_label = thalim_cat_name($cat); break; }
}
}
$item = $make_agenda_item($fpost, $raw_date, $type_label, get_post_meta($fpost->ID, 'lieu', true) ?: '', get_permalink($fpost->ID));
if ($item) $agenda_items[] = $item;
}
}
$context['agenda_items'] = $agenda_items;
$context['manifestations_link'] = thalim_en_url( add_query_arg( 'view', 'agenda', get_category_link(3) ) );
// --- Quick links ---
$newsletter_cat = get_category_by_slug('newsletter');
$newsletter_url = '';
if ($newsletter_cat) {
$nl_posts = get_posts([
'post_type' => 'post',
'posts_per_page' => 1,
'category__in' => [ $newsletter_cat->term_id ],
'include_children' => false,
'orderby' => 'date',
'order' => 'DESC',
'suppress_filters' => true,
]);
if ( ! empty( $nl_posts ) ) {
$newsletter_url = thalim_en_url( get_permalink( $nl_posts[0]->ID ) );
}
}
if ( ! $newsletter_url ) {
$newsletter_url = thalim_en_url(
$newsletter_cat ? get_category_link( $newsletter_cat->term_id ) : home_url( '/category/le-laboratoire/newsletter/' )
);
}
$context['quick_links'] = [
'agenda' => thalim_en_url(add_query_arg('view', 'agenda', get_category_link(3))),
'contacts' => thalim_en_url(home_url('/contacts/')),
'newsletter' => $newsletter_url,
];
// --- Tags (étiquettes) pour le nuage de mots-clés ---
$context['has_tags'] = !empty(get_terms([
'taxonomy' => 'post_tag',
'hide_empty' => true,
'number' => 1,
'lang' => '',
]));
Timber::render('index.twig', $context);

883
js/adminDashboardMods.js Normal file
View File

@@ -0,0 +1,883 @@
(function($) {
'use strict';
function isPostEditPage() {
return window.pagenow === 'post'
|| window.pagenow === 'post-new'
// On CPTs, pagenow is the post_type slug — also catch them via the
// body classes WP sets for any post.php / post-new.php screen.
|| document.body.classList.contains('post-php')
|| document.body.classList.contains('post-new-php');
}
function isProfileEditPage() {
return window.pagenow === 'profile' || window.pagenow === 'user-edit' || window.pagenow === 'user-new';
}
function getProfileForm() {
return document.querySelector('#your-profile, #createuser');
}
function isPodsModal() {
return new URLSearchParams(window.location.search).has('pods_modal');
}
function renameArticlesToAnnonces() {
const replacements = [
[/Tous les articles/g, 'Toutes les annonces'],
[/Ajouter un article/g, 'Ajouter une annonce'],
[/Modifier l.article/g, "Modifier l'annonce"],
[/Pr\u00e9visualiser l.article/g, "Pr\u00e9visualiser l'annonce"],
[/Afficher l.article/g, "Afficher l'annonce"],
[/Voir l.article/g, "Voir l'annonce"],
[/Article publi\u00e9/g, 'Annonce publi\u00e9e'],
[/Article mis \u00e0 jour/g, 'Annonce mise \u00e0 jour'],
[/Article planifi\u00e9/g, 'Annonce planifi\u00e9e'],
[/Articles par page/g, 'Annonces par page'],
[/Articles/g, 'Annonces'],
[/Article/g, 'Annonce'],
[/Rechercher des articles/g, 'Rechercher des annonces'],
];
function applyReplacements(text) {
return replacements.reduce((t, [s, r]) => t.replace(s, r), text);
}
function replaceInTextNodes(el) {
if (!el) return;
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
const nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
nodes.forEach(function(node) {
const replaced = applyReplacements(node.textContent);
if (replaced !== node.textContent) node.textContent = replaced;
});
}
// Menu latéral
replaceInTextNodes(document.querySelector('#menu-posts'));
// Titre de page (h1) et bouton d'ajout
document.querySelectorAll('.wp-heading-inline, .page-title-action').forEach(replaceInTextNodes);
// Notifications après sauvegarde (Article publié, mis à jour…)
document.querySelectorAll('#message, .notice').forEach(replaceInTextNodes);
// Boîte de publication — lien "Voir l'article"
replaceInTextNodes(document.querySelector('.submitbox'));
// Options d'écran — "Articles par page"
replaceInTextNodes(document.querySelector('#screen-options-wrap'));
// Bouton de recherche (attribut value + aria-label)
var searchSubmit = document.querySelector('#search-submit');
if (searchSubmit) {
if (searchSubmit.value) {
searchSubmit.value = applyReplacements(searchSubmit.value);
}
var ariaLabel = searchSubmit.getAttribute('aria-label');
if (ariaLabel) {
searchSubmit.setAttribute('aria-label', applyReplacements(ariaLabel));
}
}
// Titre de l'onglet du navigateur
document.title = applyReplacements(document.title);
}
function updatePostboxVisibility() {
document.querySelectorAll('.postbox').forEach((postBox) => {
if (postBox.id.startsWith('pods')) {
// body-en is controlled by language tabs — never auto-hide it
if (postBox.id === 'pods-meta-body-en') return;
const fields = postBox.querySelectorAll('tr');
const hasVisibleFields = Array.from(fields).some(field => field.style.display !== 'none');
postBox.style.display = hasVisibleFields ? 'block' : 'none';
}
});
}
// Force Visual (TinyMCE) mode on page load.
// WP stores the last-used editor mode in localStorage and restores it at document.ready.
// When Code mode is restored, TinyMCE is never initialised — tinymce.get() returns null.
// Instead, check the wrapper's CSS class:
// tmce-active = Visual mode (fine)
// html-active = Code mode (switch to Visual)
function ensureVisualMode(editorId, attempt) {
attempt = attempt || 0;
if (attempt > 15) return;
var wrap = document.getElementById('wp-' + editorId + '-wrap');
if (!wrap) {
setTimeout(function() { ensureVisualMode(editorId, attempt + 1); }, 200);
return;
}
if (wrap.classList.contains('html-active')) {
var ed = window.tinymce && tinymce.get(editorId);
if (!ed || !ed.initialized) {
// TinyMCE not ready yet — retry rather than calling switchEditors.go() prematurely
setTimeout(function() { ensureVisualMode(editorId, attempt + 1); }, 200);
return;
}
if (typeof switchEditors !== 'undefined') {
switchEditors.go(editorId, 'tmce');
}
return;
}
if (!wrap.classList.contains('tmce-active')) {
// Mode not yet determined — retry
setTimeout(function() { ensureVisualMode(editorId, attempt + 1); }, 200);
}
}
// Phase 1: insert the tab bar and relocate #pods-meta-body-en.
// The DOM move breaks TinyMCE's iframe (browsers reset iframe content on detach),
// so we leave the container visible here and let Pods/TinyMCE initialise normally.
// The broken iframe is repaired by reinitEditor() on first EN tab open.
function setupBodyTabsDom() {
var nativeEditor = document.getElementById('postdivrich') || document.getElementById('postdiv');
var bodyEnBox = document.getElementById('pods-meta-body-en');
if (!nativeEditor || !bodyEnBox) return;
var tabBar = document.createElement('div');
tabBar.className = 'body-lang-tabs';
tabBar.innerHTML =
'<button type="button" class="body-lang-tab is-active" data-panel="fr">Fran\u00e7ais</button>' +
'<button type="button" class="body-lang-tab" data-panel="en">English</button>';
nativeEditor.parentNode.insertBefore(tabBar, nativeEditor);
// Move EN metabox to sit right after the native editor for correct visual layout.
// Do NOT hide it yet — Pods must init TinyMCE with the container visible so the
// iframe can measure its dimensions. Page is still opacity:0 so no flash.
nativeEditor.parentNode.insertBefore(bodyEnBox, nativeEditor.nextSibling);
}
// Rebuild a TinyMCE editor whose iframe is broken (empty/non-interactive).
// This happens when TinyMCE is initialised on a hidden (display:none) element:
// the iframe can't measure dimensions and its document body stays empty.
//
// We reinit from tinyMCEPreInit.mceInit — first trying the editor's own config
// (registered by Pods server-side), falling back to 'content' (the native WP editor).
//
// Inline toolbar positioning fix:
// TinyMCE's 'wordpress' plugin captures document.getElementById(id+'_ifr') during
// 'preinit' — before the iframe is created — so mceIframe is always null.
// Fix: intercept getElementById during preinit so the 'wordpress' plugin captures
// a proxyIframe instead of null. After init, proxy delegates to the real iframe.
function reinitEditor(editorId) {
var ed = window.tinymce && tinymce.get(editorId);
// Preserve existing content before destroying the instance
var savedContent = '';
if (ed) {
try { savedContent = ed.getContent(); } catch (e) {}
ed.remove();
}
if (!savedContent) {
var ta = document.getElementById(editorId);
if (ta) savedContent = ta.value || '';
}
if (!window.tinyMCEPreInit || !window.tinymce) return;
// Use the editor's own server-side config if available, else clone from 'content'
var baseInit = (tinyMCEPreInit.mceInit && tinyMCEPreInit.mceInit[editorId])
|| (tinyMCEPreInit.mceInit && tinyMCEPreInit.mceInit['content']);
if (!baseInit) return;
// Proxy iframe: getBoundingClientRect() falls back to the editor wrap
var wrapId = 'wp-' + editorId + '-wrap';
var proxyIframe = {
getBoundingClientRect: function() {
var el = document.getElementById(wrapId);
return el ? el.getBoundingClientRect()
: { top: 0, left: 0, right: window.innerWidth,
bottom: window.innerHeight, width: window.innerWidth,
height: window.innerHeight };
}
};
var savedGetById = document.getElementById;
var origSetup = baseInit.setup;
var content = savedContent;
tinymce.init($.extend({}, baseInit, {
selector: '#' + editorId,
setup: function(editor) {
if (typeof origSetup === 'function') origSetup(editor);
editor.on('focus', function() {
window.wpActiveEditor = editorId;
});
editor.on('preinit', function() {
document.getElementById = function(id) {
if (id === editorId + '_ifr') return proxyIframe;
return savedGetById.call(document, id);
};
setTimeout(function() {
document.getElementById = savedGetById;
}, 0);
});
editor.on('init', function() {
// Point proxy to real iframe
var realIframe = savedGetById.call(document, editorId + '_ifr');
if (realIframe) {
proxyIframe.getBoundingClientRect = function() {
return realIframe.getBoundingClientRect();
};
}
// Restore content that was in the textarea
if (content) {
editor.setContent(content);
}
});
}
}));
}
// Phase 2: wire tab click handlers — runs at t=100ms after metabox reordering.
function initBodyLanguageTabs() {
var nativeEditor = document.getElementById('postdivrich') || document.getElementById('postdiv');
var bodyEnBox = document.getElementById('pods-meta-body-en');
var tabBar = document.querySelector('.body-lang-tabs');
if (!nativeEditor || !bodyEnBox || !tabBar) {
// body_en not available (e.g. contributor role) — still force visual mode on main editor
if (nativeEditor) ensureVisualMode('content');
return;
}
var enEditorId = 'pods-form-ui-pods-meta-body-en';
var enTmceReady = false;
// Hide EN panel — page is still opacity:0, user won't see the switch
bodyEnBox.style.display = 'none';
tabBar.querySelectorAll('.body-lang-tab').forEach(function(btn) {
btn.addEventListener('click', function() {
tabBar.querySelectorAll('.body-lang-tab').forEach(function(b) {
b.classList.remove('is-active');
});
btn.classList.add('is-active');
var revealedPanel;
if (btn.dataset.panel === 'fr') {
bodyEnBox.style.display = 'none';
nativeEditor.style.opacity = '0';
nativeEditor.style.display = '';
revealedPanel = nativeEditor;
} else {
nativeEditor.style.display = 'none';
bodyEnBox.style.opacity = '0';
bodyEnBox.style.display = 'block';
revealedPanel = bodyEnBox;
if (!enTmceReady) {
enTmceReady = true;
// Reinit while container is visible so TinyMCE can measure dimensions
reinitEditor(enEditorId);
}
}
// Notify TinyMCE to reflow, then fade in once layout is correct
setTimeout(function() {
window.dispatchEvent(new Event('resize'));
requestAnimationFrame(function() {
requestAnimationFrame(function() {
revealedPanel.style.opacity = '';
});
});
}, 50);
});
});
// Ensure both editors start in Visual (not Code) mode
ensureVisualMode('content');
ensureVisualMode(enEditorId);
}
function groupAxesCheckboxes() {
if (!window.thalimAxesGroups || !thalimAxesGroups.length) return;
var row = document.querySelector('.pods-form-ui-row-name-axes-thematiques');
if (!row) return;
var list = row.querySelector('ul');
if (!list) return;
// Already grouped — nothing to do
if (list.querySelector('.axes-group-label')) return;
// Map existing <li> by checkbox value; preserve "add new" button
var liMap = {};
var addNewItem = null;
list.querySelectorAll('li').forEach(function(li) {
if (li.classList.contains('pods-pick-add-new')) { addNewItem = li; return; }
var cb = li.querySelector('input[type="checkbox"]');
if (cb) liMap[cb.value] = li;
});
// Rebuild list in group order
list.innerHTML = '';
thalimAxesGroups.forEach(function(group) {
var labelLi = document.createElement('li');
labelLi.className = 'axes-group-label';
labelLi.textContent = group.label;
list.appendChild(labelLi);
group.terms.forEach(function(term) {
var li = liMap[String(term.id)];
if (li) list.appendChild(li);
});
});
if (addNewItem) list.appendChild(addNewItem);
}
var REF_BIB_EDITOR_ID = 'pods-form-ui-pods-meta-reference-bibliographique';
var refBibReinited = false;
// Reinit the référence bibliographique TinyMCE editor.
// Called at page load (if the field is already visible) and by the
// MutationObserver (when the field becomes visible after a category change).
function initRefBibEditor() {
if (refBibReinited) return;
var row = document.querySelector('.pods-form-ui-row-name-reference-bibliographique');
if (!row || row.style.display === 'none') return;
refBibReinited = true;
reinitEditor(REF_BIB_EDITOR_ID);
ensureVisualMode(REF_BIB_EDITOR_ID);
}
function initAxesGroupObserver() {
// Pods shows/hides conditional rows by removing inline style="display:none"
// Watch the entire Pods meta form for style changes on the axes row
var podsForm = document.querySelector('.pods-pick-values, #pods-meta-champs-contextuels, form#post');
if (!podsForm) podsForm = document.body;
var observer = new MutationObserver(function(mutations) {
for (var i = 0; i < mutations.length; i++) {
var target = mutations[i].target;
if (target.classList && target.classList.contains('pods-form-ui-row-name-axes-thematiques')) {
if (target.style.display !== 'none') {
setTimeout(groupAxesCheckboxes, 50);
}
}
// Reinit TinyMCE on the référence bibliographique field when its
// row becomes visible — Pods hides it with display:none which breaks
// the TinyMCE iframe. Only reinit once per page load.
if (!refBibReinited && target.classList &&
target.classList.contains('pods-form-ui-row-name-reference-bibliographique')) {
if (target.style.display !== 'none') {
setTimeout(initRefBibEditor, 100);
}
}
}
});
observer.observe(podsForm, { attributes: true, attributeFilter: ['style'], subtree: true });
}
function initPostEditPage() {
// Disable category options (CSS handles the color)
const categorieSelect = document.querySelector('#pods-form-ui-pods-meta-categorie');
if (categorieSelect) {
const categoriesToDisable = ['1', '12', '5', '20'];
categorieSelect.querySelectorAll('option').forEach(option => {
if (categoriesToDisable.includes(option.value)) {
option.disabled = true;
}
});
}
// Reorder meta boxes
const sideSortables = document.querySelector('#side-sortables');
if (sideSortables) {
const typeDannonce = document.querySelector('#pods-meta-type-dannonce');
const affichageAccueil = document.querySelector('#pods-meta-affichage-sur-laccueil');
const thematique = document.querySelector('#pods-meta-thematique');
if (typeDannonce) sideSortables.prepend(typeDannonce);
if (affichageAccueil) sideSortables.appendChild(affichageAccueil);
if (thematique) sideSortables.appendChild(thematique);
}
const submitDiv = document.querySelector('#submitdiv');
if (submitDiv && submitDiv.parentNode) {
submitDiv.parentNode.appendChild(submitDiv);
}
const champsContextuels = document.querySelector('#pods-meta-champs-contextuels');
if (champsContextuels && champsContextuels.parentNode) {
champsContextuels.parentNode.prepend(champsContextuels);
}
initBodyLanguageTabs();
initRefBibEditor();
groupAxesCheckboxes();
initAxesGroupObserver();
updatePostboxVisibility();
initDatePickerPopoverFix();
initInfoPopovers();
// Place #pods-meta-documents-joints in #normal-sortables, right after
// #pods-meta-champs-contextuels. This keeps it out of #post-body-content
// (the body editor section) regardless of whether champsContextuels is
// currently visible. When champsContextuels is hidden it takes no space,
// so documentsJoints simply appears first in #normal-sortables.
const documentsJoints = document.querySelector('#pods-meta-documents-joints');
if (documentsJoints) {
if (champsContextuels && champsContextuels.parentNode) {
champsContextuels.parentNode.insertBefore(documentsJoints, champsContextuels.nextSibling);
} else {
const normalSortables = document.querySelector('#normal-sortables');
if (normalSortables) normalSortables.prepend(documentsJoints);
}
}
// Inject separator row for the Membres grid layout
var membresTbody = document.querySelector('#pods-meta-membres .form-table tbody');
if (membresTbody && !membresTbody.querySelector('.membres-grid-separator')) {
var sep = document.createElement('tr');
sep.className = 'membres-grid-separator';
membresTbody.appendChild(sep);
}
updateMembresGridSeparator();
}
var INFO_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 11v6"/><path d="M12 8v.01" stroke-width="2"/></svg>';
var TRANSLATE_ICON = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';
var TRANSLATE_LINES = [
'Traduction en anglais apr\u00e8s //',
'ex\u00a0: Texte en fran\u00e7ais // English text'
];
// Tips without a `page` key default to 'post'.
// type: 'translate' uses the globe icon + green button style.
var INFO_TIPS = [
// --- post edit page: info ---
{
selector: '.wp-heading-inline',
lines: [
'Saisir le titre anglais apr\u00e8s //',
'ex\u00a0: Titre de l\u2019annonce // Title of the announcement'
]
},
{
selector: '#pods-meta-documents-joints .postbox-header h2',
lines: [
'Ajouter les images dans les documents.',
'Ajouter les l\u00e9gendes comme titre du document.'
]
},
{
selector: '#pods-meta-membres .postbox-header h2',
lines: [
'Le champ fonction change le libell\u00e9 de la liste de personnes cit\u00e9es.',
'Le champ membre permet de lister les membres de Thalim li\u00e9s \u00e0 l\u2019annonce.',
'Le champ autre personnes permet de lister des personnes ext\u00e9rieures \u00e0 Thalim.'
]
},
{
selector: '#pods-meta-dates .postbox-header h2',
lines: [
'Pour entrer une date sans l\u2019heure, r\u00e9gler l\u2019heure sur 00\u202f:00.'
]
},
{
selector: '#pods-meta-affichage-sur-laccueil .postbox-header h2',
lines: [
'\u00c9pingler l\u2019annonce dans le diaporama la fait s\u2019afficher avant les autres.'
]
},
{
selector: '#pods-meta-medias .postbox-header h2',
lines: [
'Pour ajouter un m\u00e9dia Canal\u00a0U, copier le lien depuis \u00ab\u00a0Citer cette ressource\u00a0\u00bb.',
'ex\u00a0: https://www.canal-u.tv/166564'
]
},
// --- post edit page: translate ---
{ type: 'translate', selector: '#pods-meta-documents-joints .postbox-header h2', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-sous-titre th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-lieu th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-titre-du-lien-externe-1 th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-titre-du-lien-externe-2 th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-titre-du-lien-externe-3 th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-organisation th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-intervention th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-candidat th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-realisation th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-dirige th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-redaction th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-auteur th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-fonction-responsable th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-autre-fonction-autre th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-autre-fonction-concerne th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-autre-fonction-directeur th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-autre-fonction-direction-d-ouvrage th',lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-autre-fonction-intervenant th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-autre-fonction-participants th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-type-autre th', lines: TRANSLATE_LINES },
// --- contenu_general edit page: translate ---
{ type: 'translate', selector: '.pods-form-ui-row-name-umr th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-thalim th', lines: TRANSLATE_LINES },
{ type: 'translate', selector: '.pods-form-ui-row-name-siecles th', lines: TRANSLATE_LINES },
// --- user/profile edit page: translate ---
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-titre-du-lien-1 th', lines: TRANSLATE_LINES },
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-titre-du-lien-2 th', lines: TRANSLATE_LINES },
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-titre-du-lien-3 th', lines: TRANSLATE_LINES },
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-titre-du-lien-4 th', lines: TRANSLATE_LINES },
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-complement-de-role-1 th', lines: TRANSLATE_LINES },
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-complement-de-role-2 th', lines: TRANSLATE_LINES },
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-complement-de-role-3 th', lines: TRANSLATE_LINES },
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-affichage-du-statut-1 th', lines: TRANSLATE_LINES },
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-affichage-du-statut-2 th', lines: TRANSLATE_LINES },
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-affichage-du-statut-3 th', lines: TRANSLATE_LINES },
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-affiliation-autre th', lines: TRANSLATE_LINES },
{ type: 'translate', page: 'user', selector: '.pods-form-ui-row-name-titre-de-these th', lines: TRANSLATE_LINES },
// --- taxonomy edit pages: translate ---
{ type: 'translate', page: 'taxonomy', selector: 'label[for="name"]', lines: TRANSLATE_LINES },
// --- user/profile edit page: info ---
{
page: 'user',
selector: '.pods-form-ui-label-pods-meta-identifiant-hal',
lines: [
'Renseigner votre idHAL (en lettres), pas votre PersonId (en chiffres).'
]
},
{
page: 'user',
selector: '.pods-form-ui-label-pods-meta-affichage-du-statut-1',
lines: [
'Texte de statut affiché sur le profil publique.'
]
}
];
var _popoverCloseHandlerRegistered = false;
function initInfoPopovers(currentPage) {
currentPage = currentPage || 'post';
INFO_TIPS.forEach(function(tip) {
if ((tip.page || 'post') !== currentPage) return;
var el = document.querySelector(tip.selector);
if (!el) return;
var isTranslate = tip.type === 'translate';
var btn = document.createElement('button');
btn.type = 'button';
btn.className = isTranslate ? 'thalim-translate-btn' : 'thalim-info-btn';
btn.setAttribute('aria-label', isTranslate ? 'Traduction bilingue' : 'Informations');
btn.innerHTML = isTranslate ? TRANSLATE_ICON : INFO_ICON;
var popover = document.createElement('div');
popover.className = 'thalim-info-popover' + (isTranslate ? ' thalim-translate-popover' : '');
popover.innerHTML = tip.lines.map(function(line) {
return '<p>' + line + '</p>';
}).join('');
var wrapper = document.createElement('span');
wrapper.className = 'thalim-info-wrapper';
wrapper.appendChild(btn);
wrapper.appendChild(popover);
el.appendChild(wrapper);
btn.addEventListener('click', function(e) {
e.stopPropagation();
var isOpen = popover.classList.contains('is-open');
document.querySelectorAll('.thalim-info-popover.is-open').forEach(function(p) {
p.classList.remove('is-open');
});
if (!isOpen) {
var rect = btn.getBoundingClientRect();
popover.style.top = (rect.bottom + 6) + 'px';
popover.style.left = (rect.left + rect.width / 2) + 'px';
popover.classList.add('is-open');
}
});
popover.addEventListener('click', function(e) {
e.stopPropagation();
});
});
if (!_popoverCloseHandlerRegistered) {
_popoverCloseHandlerRegistered = true;
document.addEventListener('click', function() {
document.querySelectorAll('.thalim-info-popover.is-open').forEach(function(p) {
p.classList.remove('is-open');
});
});
}
}
// Only native WP field sections — never touch Pods tables (they may contain TinyMCE editors)
var PROFILE_SECTION_KEYS = [
'user-language-wrap',
'user-first-name-wrap',
'user-email-wrap',
'user-pass1-wrap',
'upload-avatar-row',
];
// Desired order. Groups of 2 are wrapped in a flex row and displayed side by side.
var PROFILE_ORDER = [
['user-first-name-wrap', 'upload-avatar-row'],
['user-email-wrap'],
['user-language-wrap', 'user-pass1-wrap'],
];
function reorderProfileSections() {
var form = getProfileForm();
if (!form) return;
var pairMap = {};
form.querySelectorAll('table.form-table').forEach(function(table) {
PROFILE_SECTION_KEYS.forEach(function(key) {
if (pairMap[key] || !table.querySelector('.' + key)) return;
// Find the associated heading: first try preceding sibling in same parent,
// then look for an h2/h3 inside the same wrapper element.
var h2 = null;
var el = table.previousElementSibling;
while (el) {
if (el.tagName === 'H2' || el.tagName === 'H3') { h2 = el; break; }
if (el.tagName === 'TABLE') break;
el = el.previousElementSibling;
}
if (!h2 && table.parentElement !== form) {
h2 = table.parentElement.querySelector('h2, h3');
}
// The unit to move: if h2 and table share a non-form wrapper, move the wrapper.
var wrapper = null;
if (h2 && h2.parentElement !== form && h2.parentElement === table.parentElement) {
wrapper = h2.parentElement;
}
pairMap[key] = { h2: h2, table: table, wrapper: wrapper };
});
});
// Remove all matched units from DOM (dedup by actual element)
var removed = new Set();
function removeEl(el) {
if (el && !removed.has(el)) { removed.add(el); el.remove(); }
}
Object.values(pairMap).forEach(function(unit) {
if (unit.wrapper) { removeEl(unit.wrapper); }
else { removeEl(unit.h2); removeEl(unit.table); }
});
// Re-insert in declared order before the submit button
var submitAnchor = form.querySelector('p.submit');
function append(el) {
if (submitAnchor && submitAnchor.parentNode) form.insertBefore(el, submitAnchor);
else form.appendChild(el);
}
function appendUnit(unit) {
if (unit.wrapper) { append(unit.wrapper); }
else { if (unit.h2) append(unit.h2); append(unit.table); }
}
PROFILE_ORDER.forEach(function(group) {
var available = group.filter(function(key) { return !!pairMap[key]; });
if (!available.length) return;
// Dedup: two keys may resolve to the same table/wrapper
var seen = new Set();
var units = [];
available.forEach(function(key) {
var unit = pairMap[key];
var id = unit.wrapper || unit.table;
if (!seen.has(id)) { seen.add(id); units.push(unit); }
});
if (units.length === 1) {
appendUnit(units[0]);
} else {
var row = document.createElement('div');
row.className = 'profile-section-row';
units.forEach(function(unit) {
var col = document.createElement('div');
col.className = 'profile-section-col';
if (unit.wrapper) { col.appendChild(unit.wrapper); }
else { if (unit.h2) col.appendChild(unit.h2); col.appendChild(unit.table); }
row.appendChild(col);
});
append(row);
}
});
}
function initProfileEditors() {
reorderProfileSections();
initInfoPopovers('user');
// Hide the "À propos du compte" section heading
document.querySelectorAll('#your-profile h2, #adduser h2, #createuser h2').forEach(function(h2) {
if (h2.textContent.trim() === '\u00c0 propos du compte') {
h2.style.display = 'none';
}
});
// Rename "Rôle" label to "Rôle sur le site"
var roleLabel = document.querySelector('label[for="role"]');
if (roleLabel && roleLabel.textContent.trim() === 'R\u00f4le') {
roleLabel.textContent = 'R\u00f4le sur le site';
}
}
// Gutenberg's Popover component closes on outside click via focusout detection.
// But if focus never enters the popover, focusout never fires and clicking outside
// does nothing. Fix: focus the popover container as soon as it appears in the DOM.
function initDatePickerPopoverFix() {
var observer = new MutationObserver(function(mutations) {
for (var i = 0; i < mutations.length; i++) {
var added = mutations[i].addedNodes;
for (var j = 0; j < added.length; j++) {
var node = added[j];
if (node.nodeType !== 1) continue;
var content = node.classList.contains('components-popover__content')
? node
: node.querySelector && node.querySelector('.components-popover__content');
if (content) {
var c = content;
requestAnimationFrame(function() {
if (!c.hasAttribute('tabindex')) c.setAttribute('tabindex', '-1');
c.focus();
});
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function updateMembresGridSeparator() {
var sep = document.querySelector('#pods-meta-membres .membres-grid-separator');
if (!sep) return;
var autreRows = document.querySelectorAll('#pods-meta-membres [class*="pods-form-ui-row-name-autre-"]');
var anyVisible = Array.from(autreRows).some(function(row) {
return row.style.display !== 'none';
});
sep.style.display = anyVisible ? '' : 'none';
}
// Inject a "Type de programme" filter select into the taxonomy search form.
// The form already has hidden taxonomy/post_type fields so the select value
// is submitted with them and picked up by pre_get_terms server-side.
function initProgrammeFilter() {
var form = document.querySelector('form.search-form');
if (!form) return;
var types = [
'Programme subventionné',
'Autre programme',
'Ancien programme'
];
// Read current filter value from the URL.
var params = new URLSearchParams(window.location.search);
var current = params.get('type_de_programme') || '';
var select = document.createElement('select');
select.name = 'type_de_programme';
select.id = 'filter-type-de-programme';
select.style.cssText = 'margin-right:6px;';
var blank = document.createElement('option');
blank.value = '';
blank.textContent = 'Tous les types';
select.appendChild(blank);
types.forEach(function(type) {
var opt = document.createElement('option');
opt.value = type;
opt.textContent = type;
if (type === current) opt.selected = true;
select.appendChild(opt);
});
// Insert before the first <p> (search-box) inside the form.
var searchBox = form.querySelector('p.search-box');
form.insertBefore(select, searchBox || null);
}
$(document).ready(function() {
renameArticlesToAnnonces();
if (isPostEditPage()) {
setupBodyTabsDom();
}
setTimeout(() => {
if (isPostEditPage()) {
initPostEditPage();
}
if (isProfileEditPage()) {
initProfileEditors();
}
var TRANSLATE_TAXONOMIES = ['axe_thematique', 'programme_de_recherche', 'post_tag'];
var isTaxonomyListPage = TRANSLATE_TAXONOMIES.some(function(tax) {
return window.location.search.indexOf('taxonomy=' + tax) !== -1;
});
if (isTaxonomyListPage) {
initInfoPopovers('taxonomy');
}
if (window.location.search.indexOf('taxonomy=programme_de_recherche') !== -1) {
initProgrammeFilter();
}
document.body.classList.add('admin-mods-ready');
}, 100);
// Fallback: force reveal after 2s in case the 100ms path failed (e.g. JS error mid-init)
setTimeout(() => {
document.body.classList.add('admin-mods-ready');
}, 2000);
$('#pods-form-ui-pods-meta-categorie').change(function() {
setTimeout(function() {
updatePostboxVisibility();
updateMembresGridSeparator();
}, 10);
});
if (isProfileEditPage() || window.pagenow === 'edit-tags' || window.pagenow === 'term') {
$(window).on('load', function() {
var scope = getProfileForm() || document;
scope.querySelectorAll('.pods-dfv-container-wysiwyg textarea').forEach(function(ta) {
if (!ta.id) return;
ensureVisualMode(ta.id);
});
});
}
if (isPodsModal()) {
$(window).on('load', function() {
var itemId = $('#post_ID').val();
if (window.PodsDFV && itemId) {
window.PodsDFV.setFieldValue('post', itemId, 'categorie', '12', 0);
}
// Lock category select to 12 in iframe — delay to run after Pods React re-render
setTimeout(function() {
var $select = $('#pods-form-ui-pods-meta-categorie');
if ($select.length) {
$select.find('option').each(function() {
this.disabled = this.value !== '12';
});
$select.val('12');
}
updatePostboxVisibility();
}, 200);
});
}
});
})(jQuery);

119
js/adminFormRestore.js Normal file
View File

@@ -0,0 +1,119 @@
(function($) {
'use strict';
var STORAGE_PREFIX = 'thalim_form_restore_';
var MAX_AGE_MS = 10 * 60 * 1000; // 10 min
function getPostId() {
return $('#post_ID').val() || 'new';
}
function getStorageKey() {
return STORAGE_PREFIX + getPostId();
}
function saveFormData() {
var data = { timestamp: Date.now() };
var $title = $('#title');
if ($title.length) data.title = $title.val();
var $content = $('#content');
if ($content.length) {
if (typeof tinyMCE !== 'undefined' && tinyMCE.get('content')) {
data.content = tinyMCE.get('content').getContent();
} else {
data.content = $content.val();
}
}
$('[name^="pods_meta_"]').each(function() {
data[this.name] = $(this).val();
});
try {
sessionStorage.setItem(getStorageKey(), JSON.stringify(data));
} catch (e) {}
}
function restoreFormData() {
var navEntries = performance.getEntriesByType('navigation');
if (!navEntries.length || navEntries[0].type !== 'back_forward') return;
var key = getStorageKey();
var stored, data;
try {
stored = sessionStorage.getItem(key);
if (!stored) return;
data = JSON.parse(stored);
} catch (e) { return; }
if (!data.timestamp || Date.now() - data.timestamp > MAX_AGE_MS) {
try { sessionStorage.removeItem(key); } catch (e) {}
return;
}
// Restaurer titre
if (data.title !== undefined) $('#title').val(data.title);
// Restaurer contenu (TinyMCE ou textarea brut)
if (data.content !== undefined) {
$('#content').val(data.content);
if (typeof tinyMCE !== 'undefined') {
var ed = tinyMCE.get('content');
if (ed) {
ed.setContent(data.content);
} else {
tinyMCE.on('AddEditor', function(e) {
if (e.editor.id === 'content') {
e.editor.on('init', function() {
e.editor.setContent(data.content);
});
}
});
}
}
}
// Restaurer champs Pods après rendu React
var restorePods = function() {
setTimeout(function() {
var postId = getPostId();
Object.keys(data).forEach(function(name) {
if (!name.startsWith('pods_meta_')) return;
var fieldName = name.replace(/^pods_meta_/, '');
var value = data[name];
var $el = $('[name="' + name + '"]');
if ($el.length) $el.val(value).trigger('change');
if (window.PodsDFV && postId) {
try { window.PodsDFV.setFieldValue('post', postId, fieldName, value, 0); } catch (e) {}
}
});
}, 300);
};
if (document.readyState === 'complete') {
restorePods();
} else {
$(window).on('load', restorePods);
}
// Notice informative
var $notice = $('<div class="notice notice-info is-dismissible"><p>Votre contenu a \u00e9t\u00e9 restaur\u00e9 suite \u00e0 une erreur de validation. V\u00e9rifiez les champs obligatoires avant de publier.</p></div>');
$('#wpbody-content').find('.wrap').first().find('h1').after($notice);
}
$(document).ready(function() {
if (window.pagenow !== 'post' && window.pagenow !== 'post-new') return;
// Nettoyage si sauvegarde réussie
if ($('.notice-success, #message.updated').length) {
try { sessionStorage.removeItem(getStorageKey()); } catch (e) {}
return;
}
$('#post').on('submit', saveFormData);
restoreFormData();
});
})(jQuery);

146
js/agendaView.js Normal file
View File

@@ -0,0 +1,146 @@
(function () {
'use strict';
document.addEventListener('DOMContentLoaded', function () {
var toggleBtn = document.querySelector('.agenda-toggle-btn');
var gridSections = document.getElementById('grid-sections');
var agendaEl = document.getElementById('agenda-view');
if (!toggleBtn || !agendaEl) return;
var swiper = null;
var agendaPage = 0;
var agendaDone = false;
var agendaLoading = false;
var swiperWrapper = document.getElementById('agenda-swiper-wrapper');
var agendaSpinner = document.getElementById('agenda-spinner');
var categoryId = agendaEl.dataset.category || '';
var includeChildren = agendaEl.dataset.includeChildren || '0';
var axe = agendaEl.dataset.axe || '';
var dateFrom = agendaEl.dataset.dateFrom || '';
var dateTo = agendaEl.dataset.dateTo || '';
function showAgenda() {
if (gridSections) gridSections.style.display = 'none';
agendaEl.classList.add('is-active');
toggleBtn.innerHTML =
'<i class="iconoir-view-grid"></i>' +
(agendaViewData.lang === 'en' ? 'Switch to grid view' : 'Passer à la vue grille');
if (!swiper) {
loadMoreAgenda(function (data) {
var todayOffset = Math.max(0, ((data && data.today_offset) || 0) - 1);
var anchorPage = Math.ceil((todayOffset + 1) / 12);
if (anchorPage > 1) {
chainLoadPages(2, anchorPage, function () {
initSwiper(todayOffset);
});
} else {
initSwiper(todayOffset);
}
});
}
}
function showGrid() {
agendaEl.classList.remove('is-active');
if (gridSections) gridSections.style.display = '';
toggleBtn.innerHTML =
'<i class="iconoir-calendar"></i>' +
(agendaViewData.lang === 'en' ? 'Switch to agenda view' : 'Passer à la vue agenda');
}
function initSwiper(initialSlide) {
swiper = new Swiper(agendaEl.querySelector('.agenda-swiper'), {
slidesPerView: 1.2,
spaceBetween: 20,
initialSlide: initialSlide || 0,
navigation: {
nextEl: agendaEl.querySelector('.agenda-swiper-prev'),
prevEl: agendaEl.querySelector('.agenda-swiper-next'),
},
breakpoints: {
640: { slidesPerView: 2, spaceBetween: 24 },
1024: { slidesPerView: 3, spaceBetween: 32 },
},
on: {
reachEnd: function () {
if (!agendaDone) loadMoreAgenda();
},
},
});
swiper.changeLanguageDirection('rtl');
}
function chainLoadPages(fromPage, toPage, callback) {
if (fromPage > toPage) { callback(); return; }
loadMoreAgenda(function () {
chainLoadPages(fromPage + 1, toPage, callback);
});
}
function loadMoreAgenda(callback) {
if (agendaLoading || agendaDone) return;
agendaLoading = true;
agendaPage++;
agendaSpinner.style.display = 'flex';
var data = new FormData();
data.append('action', 'load_more_agenda');
data.append('page', agendaPage);
data.append('nonce', agendaViewData.nonce);
data.append('lang', agendaViewData.lang);
if (categoryId) data.append('category', categoryId);
if (includeChildren==='1') data.append('include_children', '1');
if (axe) data.append('axe', axe);
if (dateFrom) data.append('date_from', dateFrom);
if (dateTo) data.append('date_to', dateTo);
fetch(agendaViewData.ajaxUrl, { method: 'POST', body: data })
.then(function (r) { return r.json(); })
.then(function (result) {
agendaSpinner.style.display = 'none';
agendaLoading = false;
if (result.success && result.data.html) {
var tmp = document.createElement('div');
tmp.innerHTML = result.data.html;
Array.from(tmp.children).forEach(function (slide) {
if (swiper) {
swiper.appendSlide(slide.outerHTML);
} else {
swiperWrapper.appendChild(slide);
}
});
if (swiper) swiper.update();
if (callback) callback(result.data);
} else {
agendaDone = true;
if (callback) callback(result.data);
}
})
.catch(function () {
agendaSpinner.style.display = 'none';
agendaLoading = false;
});
}
toggleBtn.addEventListener('click', function (e) {
e.preventDefault();
if (agendaEl.classList.contains('is-active')) {
var url = new URL(window.location.href);
url.searchParams.delete('view');
history.pushState({}, '', url.toString());
showGrid();
} else {
var url = new URL(window.location.href);
url.searchParams.set('view', 'agenda');
history.pushState({}, '', url.toString());
showAgenda();
}
});
// Initial state driven by server-side class
if (agendaEl.classList.contains('is-active')) {
showAgenda();
}
});
}());

577
js/animatedLogo.js Normal file
View File

@@ -0,0 +1,577 @@
const CURSOR_INFLUENCE_INNER = 150;
const CURSOR_INFLUENCE_OUTER = 300;
const EASING_FACTOR = 0.03;
const COLORS = [
'#e0775d',
'#7cc0c6',
'#e05680',
'#46ae51',
'#bb8dd9',
'#f7ff29',
];
class FloatingShape {
constructor(element, originalX, originalY, width, height) {
this.element = element;
this.width = width;
this.height = height;
this.originalX = originalX;
this.originalY = originalY;
this.posX = originalX;
this.posY = originalY;
this.targetX = originalX;
this.targetY = originalY;
}
update(mouseX, mouseY) {
const shapeCenterX = this.originalX + this.width / 2;
const shapeCenterY = this.originalY + this.height / 2;
const dx = mouseX - shapeCenterX;
const dy = mouseY - shapeCenterY;
const distance = Math.sqrt(dx * dx + dy * dy);
const innerRadius = CURSOR_INFLUENCE_INNER / 2;
const outerRadius = CURSOR_INFLUENCE_OUTER / 2;
if (distance < outerRadius && distance > 0) {
const dirX = dx / distance;
const dirY = dy / distance;
let strength, maxDisplacement;
if (distance < innerRadius) {
strength = (innerRadius - distance) / innerRadius;
maxDisplacement = innerRadius;
} else {
const outerZoneProgress = (outerRadius - distance) / (outerRadius - innerRadius);
strength = outerZoneProgress * 0.5;
maxDisplacement = innerRadius * 0.6;
}
this.targetX = this.originalX - dirX * strength * maxDisplacement;
this.targetY = this.originalY - dirY * strength * maxDisplacement;
} else {
this.targetX = this.originalX;
this.targetY = this.originalY;
}
this.posX += (this.targetX - this.posX) * EASING_FACTOR;
this.posY += (this.targetY - this.posY) * EASING_FACTOR;
}
render() {
this.element.style.transform = `translate3d(${this.posX}px, ${this.posY}px, 0)`;
}
}
class FloatingShapesManager {
constructor(containerId) {
this.container = document.getElementById(containerId);
if (!this.container) {
console.error(`Container #${containerId} not found`);
return;
}
this.shapes = [];
this.strokePaths = []; // Store all stroke paths for fill control
this.fillShape = null; // Reference to the filled shape SVG for fade control
this.fillShapeReady = false; // Track if fillshape animation has completed
this.thalimText = null; // Reference to the THALIM text element
this.textReady = false; // Track if text animation has completed
this.mouseX = 0;
this.mouseY = 0;
this.animationId = null;
this.isTouching = false; // Track if currently in a touch interaction
this.init();
}
init() {
const containerRect = this.container.getBoundingClientRect();
const containerWidth = containerRect.width;
const containerHeight = containerRect.height;
const shapeConfigs = [
{
id: 'shape-1',
svgPath: `${themeDirURI}/assets/logo-shapes/shape1.svg`,
baseWidth: 53.564522,
baseHeight: 112.37409,
scale: 1.5,
posX: this.isMobile() ? 20: 35,
posY: -20,
strokeWidth: 2,
animationDuration: 1.9,
gradientStart: COLORS[0], // orange
gradientEnd: '#e0b7ad'
},
{
id: 'shape-2',
svgPath: `${themeDirURI}/assets/logo-shapes/shape4.svg`,
baseWidth: 74.08564,
baseHeight: 121.90051,
scale: 1.5,
posX: 0,
posY: this.isMobile() ? -8 : -5,
strokeWidth: 2,
animationDuration: 2.5,
gradientStart: '#aec4c6',
gradientEnd: COLORS[1] // blue
},
{
id: 'shape-3',
svgPath: `${themeDirURI}/assets/logo-shapes/shape3.svg`,
baseWidth: 159.16571,
baseHeight: 87.756729,
scale: 1.5,
posX: 0,
posY: -10,
strokeWidth: 2,
animationDuration: 3.1,
gradientStart: COLORS[2], // pink
gradientEnd: '#e0b0be'
},
{
id: 'shape-4',
svgPath: `${themeDirURI}/assets/logo-shapes/shape2.svg`,
baseWidth: 143.26076,
baseHeight: 20.26207,
scale: 1.5,
posX: 0,
posY: this.isMobile() ? 20 : 45,
strokeWidth: 2,
animationDuration: 3.7,
gradientStart: '#8bc491',
gradientEnd: COLORS[3] // green
},
{
id: 'shape-5',
svgPath: `${themeDirURI}/assets/logo-shapes/shape5.svg`,
baseWidth: 155.66518,
baseHeight: 87.785599,
scale: 1.5,
posX: this.isMobile() ? 13 : 19.5,
posY: this.isMobile() ? -18 : -23.5,
strokeWidth: 2,
animationDuration: 4.3,
gradientStart: COLORS[4], // purple
gradientEnd: '#c9b0d9'
},
{
id: 'fillshape',
svgPath: `${themeDirURI}/assets/logo-shapes/fillshape.svg`,
baseWidth: 142.78297,
baseHeight: 72.903015,
scale: this.isMobile() ? 0.78 : 1.47,
posX: this.isMobile() ? 7.5 : 11,
posY: this.isMobile() ? -13 : -14.5,
isFilled: true,
targetOpacity: 1,
animationDuration: 0.9,
animationDelay: 3.7 // Start after longest stroke animation
}
];
// Create shapes with center-based positioning
const centerX = containerWidth / 2;
const centerY = containerHeight / 2;
// Apply mobile scale on smaller viewports
const mobileScale = 0.8;
shapeConfigs.forEach((config) => {
const scale = config.id === 'fillshape' ? config.scale : this.isMobile() ? mobileScale : config.scale;
const scaledWidth = config.baseWidth * scale;
const scaledHeight = config.baseHeight * scale;
// Calculate position from center with offset, minus half dimensions to center the shape
const x = centerX + (config.posX || 0) - scaledWidth / 2;
const y = centerY + (config.posY || 0) - scaledHeight / 2;
// Create animated stroke element
this.createAnimatedStrokeElement(config, scaledWidth, scaledHeight, x, y);
});
// Create THALIM text overlay
this.createThalimText(centerX, centerY);
this.container.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.container.addEventListener('mouseleave', this.handleMouseLeave.bind(this));
this.container.addEventListener('touchstart', this.handleTouchStart.bind(this));
this.container.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
this.container.addEventListener('touchend', this.handleTouchEnd.bind(this));
window.addEventListener('resize', this.handleResize.bind(this));
// Set sketch margin on mobile based on hero-logos height
this.setSketchMarginOnMobile();
this.animate();
}
isMobile() {
return window.innerWidth < 768; // tablet breakpoint from scss/_variables.scss
}
setSketchMarginOnMobile() {
if (this.isMobile()) {
const heroLogos = document.querySelector('.hero-logos');
if (heroLogos) {
const logoHeight = heroLogos.offsetHeight;
this.container.style.marginTop = `${logoHeight}px`;
}
} else {
// Reset margin-top on desktop (handled by CSS)
this.container.style.marginTop = '';
}
}
async createAnimatedStrokeElement(config, width, height, x, y) {
try {
const response = await fetch(config.svgPath);
const svgText = await response.text();
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgText, 'image/svg+xml');
const svgElement = svgDoc.querySelector('svg');
if (!svgElement) {
console.error(`Failed to parse SVG for ${config.id}`);
return;
}
// Create shape div for physics
const shapeDiv = document.createElement('div');
shapeDiv.className = 'floating-shape';
shapeDiv.id = config.id;
shapeDiv.style.zIndex = config.isFilled ? '10' : '1'; // Fillshape above strokes
// Set SVG dimensions
svgElement.setAttribute('width', width);
svgElement.setAttribute('height', height);
if (config.isFilled) {
// Handle filled shapes (fade-in animation)
svgElement.style.opacity = '0';
svgElement.style.transition = 'opacity 0.3s ease-out'; // Smooth fade on mouse interaction
const duration = config.animationDuration || 1.5;
const delay = config.animationDelay || 0;
const targetOpacity = config.targetOpacity || 0.5;
svgElement.style.setProperty('--target-opacity', targetOpacity);
svgElement.style.animation = `fadeIn ${duration}s ease-in-out ${delay}s forwards`;
// Remove animation after it completes so we can control opacity manually
setTimeout(() => {
svgElement.style.animation = 'none';
svgElement.style.opacity = targetOpacity;
this.fillShapeReady = true; // Mark fillshape as ready for interaction
}, (delay + duration) * 1000);
// Store reference for mouse interaction
this.fillShape = svgElement;
this.fillShapeOpacity = targetOpacity;
// Position the fillshape div (since it's not in the physics system)
shapeDiv.style.transform = `translate3d(${x}px, ${y}px, 0)`;
} else {
// Handle stroked shapes (gradient + stroke drawing animation)
// Create gradient for stroke
const gradientId = `gradient-${config.id}`;
// Create defs element if it doesn't exist
let defs = svgElement.querySelector('defs');
if (!defs) {
defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
svgElement.insertBefore(defs, svgElement.firstChild);
}
// Create linear gradient
const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gradient.setAttribute('id', gradientId);
gradient.setAttribute('x1', '0%');
gradient.setAttribute('y1', '0%');
gradient.setAttribute('x2', '100%');
gradient.setAttribute('y2', '100%');
// Create gradient stops
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop1.setAttribute('offset', '0%');
stop1.setAttribute('stop-color', config.gradientStart);
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop2.setAttribute('offset', '100%');
stop2.setAttribute('stop-color', config.gradientEnd);
gradient.appendChild(stop1);
gradient.appendChild(stop2);
defs.appendChild(gradient);
// Apply stroke styling and animation to all paths in the SVG
const paths = svgElement.querySelectorAll('path, polyline, polygon, line, circle, ellipse, rect');
paths.forEach(path => {
// Set stroke properties
path.style.fill = 'white';
path.style.fillOpacity = '0'; // Start invisible
path.style.transition = 'fill-opacity 0.5s ease-in-out'; // Smooth fill changes
path.style.stroke = `url(#${gradientId})`;
// Don't override stroke-width - preserve SVG's compensated values
path.style.strokeLinecap = 'round';
path.style.strokeLinejoin = 'round';
// Calculate path length for animation
const pathLength = path.getTotalLength ? path.getTotalLength() : 1000;
// Set CSS variable for path length
path.style.setProperty('--path-length', pathLength);
// Set up stroke dash animation
path.style.strokeDasharray = pathLength;
path.style.strokeDashoffset = pathLength;
// Create CSS animation (plays once and stays completed)
const duration = config.animationDuration || 3;
path.style.animation = `drawStroke ${duration}s ease-in-out forwards`;
// Store path reference for fill control
this.strokePaths.push(path);
});
// Fade in white fill when text starts appearing (at 4.3s)
setTimeout(() => {
paths.forEach(path => {
path.style.fillOpacity = '0.7';
});
}, 4300);
}
// Append SVG to shape div
shapeDiv.appendChild(svgElement);
this.container.appendChild(shapeDiv);
// Create FloatingShape instance (only for shapes with physics, not filled shapes)
if (!config.isFilled) {
const floatingShape = new FloatingShape(shapeDiv, x, y, width, height);
this.shapes.push(floatingShape);
}
} catch (error) {
console.error(`Error loading SVG for ${config.id}:`, error);
}
}
createThalimText(centerX, centerY) {
// Create container for the text
const textContainer = document.createElement('div');
textContainer.className = 'thalim-text';
textContainer.style.top = '-20px';
textContainer.style.left = '10px';
textContainer.style.transform = `translate3d(${centerX}px, ${centerY}px, 0) translate(-50%, -50%)`; // Center the text
textContainer.style.color = '#000000cc';
// Create individual letter spans
const letters = 'thalim'.split('');
letters.forEach((letter, index) => {
const span = document.createElement('span');
span.textContent = letter;
span.style.opacity = '0';
span.style.animation = `letterAppear 0.8s ease-in-out ${4.2 + index * 0.1}s forwards`;
textContainer.appendChild(span);
});
// Mark text as ready after last letter finishes animating
// Last letter: 4.2s + 0.5s (6th letter) + 0.8s (duration) = 5.5s
setTimeout(() => {
this.textReady = true;
}, 5500);
this.container.appendChild(textContainer);
this.thalimText = textContainer;
}
handleMouseMove(e) {
// Ignore mouse events during touch interactions (prevents interference from synthetic mouse events)
if (this.isTouching) {
return;
}
const rect = this.container.getBoundingClientRect();
this.mouseX = e.clientX - rect.left;
this.mouseY = e.clientY - rect.top;
// Fade out fillshape and text on mouse move (only if they've finished appearing)
if (this.fillShape && this.fillShapeReady) {
this.fillShape.style.opacity = '0';
}
if (this.thalimText && this.textReady) {
this.thalimText.style.opacity = '0';
}
// Fade out white fills on stroke shapes
this.strokePaths.forEach(path => {
path.style.fillOpacity = '0';
});
}
handleMouseLeave() {
// Move mouse far away to trigger return animation
this.mouseX = -10000;
this.mouseY = -10000;
// Fade fillshape and text back in after delay (let strokes settle first)
// Only if they have completed their initial animations
if (this.fillShape && this.fillShapeReady) {
setTimeout(() => {
this.fillShape.style.opacity = this.fillShapeOpacity;
}, 800); // Delay to allow strokes to return to position
}
if (this.thalimText && this.textReady) {
setTimeout(() => {
this.thalimText.style.opacity = '1';
}, 800);
}
// Fade white fills back in on stroke shapes
setTimeout(() => {
this.strokePaths.forEach(path => {
path.style.fillOpacity = '0.7';
});
}, 800);
}
handleTouchStart() {
// Mark that we're in a touch interaction to prevent mouse event interference
this.isTouching = true;
}
handleTouchMove(e) {
e.preventDefault(); // Prevent scrolling while interacting with sketch
const rect = this.container.getBoundingClientRect();
const touch = e.touches[0];
this.mouseX = touch.clientX - rect.left;
this.mouseY = touch.clientY - rect.top;
// Fade out fillshape and text on touch move (only if they've finished appearing)
if (this.fillShape && this.fillShapeReady) {
this.fillShape.style.opacity = '0';
}
if (this.thalimText && this.textReady) {
this.thalimText.style.opacity = '0';
}
// Fade out white fills on stroke shapes
this.strokePaths.forEach(path => {
path.style.fillOpacity = '0';
});
}
handleTouchEnd() {
// Reset shapes to original position
this.handleMouseLeave();
// Clear touch flag after a delay to ignore synthetic mouse events
setTimeout(() => {
this.isTouching = false;
}, 500);
}
handleResize() {
// Don't refresh on mobile (prevents refresh during scroll when browser bar appears/disappears)
if (this.isMobile()) {
// Still update margin-top on mobile if needed
this.setSketchMarginOnMobile();
return;
}
// Debounced resize: only recreate after user stops resizing for 250ms
// This prevents page crashes from rapid resize events
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
}
this.resizeTimeout = setTimeout(() => {
// Stop animation loop before destroying
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
// Clear shapes and container
this.shapes = [];
this.container.innerHTML = '';
// Reinitialize
this.init();
}, 250);
}
animate() {
// Update all shapes
this.shapes.forEach(shape => {
shape.update(this.mouseX, this.mouseY);
shape.render();
});
// Continue loop
this.animationId = requestAnimationFrame(this.animate.bind(this));
}
destroy() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
}
this.container.removeEventListener('mousemove', this.handleMouseMove);
this.container.removeEventListener('mouseleave', this.handleMouseLeave);
this.container.removeEventListener('touchstart', this.handleTouchStart);
this.container.removeEventListener('touchmove', this.handleTouchMove);
this.container.removeEventListener('touchend', this.handleTouchEnd);
window.removeEventListener('resize', this.handleResize);
}
}
// Inject CSS for stroke and fade animations
const style = document.createElement('style');
style.textContent = `
@keyframes drawStroke {
0% {
stroke-dashoffset: var(--path-length);
opacity: 0;
}
10% {
opacity: 1;
}
100% {
stroke-dashoffset: 0;
opacity: 1;
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: var(--target-opacity);
}
}
@keyframes letterAppear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
`;
document.head.appendChild(style);
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
window.floatingShapes = new FloatingShapesManager('sketch');
});
} else {
window.floatingShapes = new FloatingShapesManager('sketch');
}

28
js/annoncesSwiper.js Normal file
View File

@@ -0,0 +1,28 @@
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-swiper]').forEach(function (section) {
new Swiper(section.querySelector('.swiper'), {
navigation: {
addIcons: false,
nextEl: section.querySelector('.swiper-button-next'),
prevEl: section.querySelector('.swiper-button-prev'),
},
autoplay: {
delay: 5000,
disableOnInteraction: false,
pauseOnMouseEnter: true,
},
slidesPerView: 1,
spaceBetween: 30,
breakpoints: {
768: {
slidesPerView: 2,
spaceBetween: 40,
},
1024: {
slidesPerView: 3,
spaceBetween: 50,
},
},
});
});
});

138
js/categoryFilters.js Normal file
View File

@@ -0,0 +1,138 @@
document.addEventListener('DOMContentLoaded', function () {
// ── Filters toggle ────────────────────────────────────────
var toggleBtn = document.getElementById('category-filters-toggle');
var filtersEl = document.getElementById('category-filters');
if (toggleBtn && filtersEl) {
toggleBtn.addEventListener('click', function () {
var isOpen = filtersEl.classList.toggle('is-open');
toggleBtn.classList.toggle('is-open', isOpen);
toggleBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
}
var dateBtn = document.getElementById('filter-date-btn');
var datePopover = document.getElementById('filter-date-popover');
var dateFrom = document.getElementById('filter-date-from');
var dateTo = document.getElementById('filter-date-to');
var dateApply = document.getElementById('filter-date-apply');
var axeBtn = document.getElementById('filter-axe-btn');
var axePopover = document.getElementById('filter-axe-popover');
if (!dateBtn) return;
// Build URL with updated query params
function buildUrl(params) {
var url = new URL(window.location.href);
Object.keys(params).forEach(function (key) {
if (params[key]) {
url.searchParams.set(key, params[key]);
} else {
url.searchParams.delete(key);
}
});
return url.toString();
}
function formatDate(d) {
var y = d.getFullYear();
var m = String(d.getMonth() + 1).padStart(2, '0');
var day = String(d.getDate()).padStart(2, '0');
return y + '-' + m + '-' + day;
}
// Dropdown toggle
function openDropdown(popover) {
popover.style.display = '';
popover.closest('.filter-dd').classList.add('is-open');
}
function closeDropdown(popover) {
popover.style.display = 'none';
popover.closest('.filter-dd').classList.remove('is-open');
}
function toggleDropdown(popover) {
if (popover.style.display === 'none') {
openDropdown(popover);
} else {
closeDropdown(popover);
}
}
// Close dropdowns on outside click
document.addEventListener('click', function (e) {
if (!e.target.closest('#filter-date-dd')) {
closeDropdown(datePopover);
}
if (axePopover && !e.target.closest('#filter-axe-dd')) {
closeDropdown(axePopover);
}
});
// --- Date dropdown ---
dateBtn.addEventListener('click', function (e) {
e.stopPropagation();
if (axePopover) closeDropdown(axePopover);
toggleDropdown(datePopover);
});
// Date presets
datePopover.addEventListener('click', function (e) {
var preset = e.target.dataset.preset;
if (!preset) return;
var now = new Date();
var from, to;
if (preset === 'week') {
var day = now.getDay();
var diff = day === 0 ? 6 : day - 1;
from = new Date(now);
from.setDate(now.getDate() - diff);
to = new Date(from);
to.setDate(from.getDate() + 6);
} else if (preset === 'month') {
from = new Date(now.getFullYear(), now.getMonth(), 1);
to = new Date(now.getFullYear(), now.getMonth() + 1, 0);
} else if (preset === 'lastmonth') {
from = new Date(now.getFullYear(), now.getMonth() - 1, 1);
to = new Date(now.getFullYear(), now.getMonth(), 0);
} else if (preset === 'upcoming') {
from = now;
to = null;
}
window.location.href = buildUrl({
date_from: formatDate(from),
date_to: to ? formatDate(to) : ''
});
});
// Date apply → navigate
dateApply.addEventListener('click', function () {
window.location.href = buildUrl({
date_from: dateFrom.value,
date_to: dateTo.value
});
});
// --- Axe dropdown ---
if (axeBtn && axePopover) {
axeBtn.addEventListener('click', function (e) {
e.stopPropagation();
closeDropdown(datePopover);
toggleDropdown(axePopover);
});
axePopover.addEventListener('click', function (e) {
var li = e.target.closest('[data-axe-id]');
if (!li) return;
if (li.dataset.axeHref) {
window.location.href = li.dataset.axeHref;
} else {
window.location.href = buildUrl({ axe: li.dataset.axeId });
}
});
}
});

22
js/coloredWordsHero.js Normal file
View File

@@ -0,0 +1,22 @@
document.addEventListener('DOMContentLoaded', function() {
if (document.querySelector('body').classList.contains('home')) {
const colors = ['#e0775d', '#7cc0c6', '#e05680', '#46ae51', '#bb8dd9'];
const timeouts = new Map();
// Color changing on hover
document.querySelectorAll('.color-changer').forEach(element => {
element.addEventListener('mouseenter', (e) => {
if (timeouts.has(e.target)) {
clearTimeout(timeouts.get(e.target));
}
e.target.style.color = colors[Math.floor(Math.random() * colors.length)];
const timeoutId = setTimeout(() => {
e.target.style.color = 'black';
timeouts.delete(e.target);
}, 2000);
timeouts.set(e.target, timeoutId);
});
});
}
});

43
js/fitPostCardTitle.js Normal file
View File

@@ -0,0 +1,43 @@
document.addEventListener('DOMContentLoaded', function () {
function fitTitles() {
var cards = document.querySelectorAll('.post-card');
cards.forEach(function (card) {
var h2 = card.querySelector('.gradient-container h2');
if (!h2) return;
var container = h2.closest('.gradient-container');
var maxHeight = container.clientHeight;
var fontSize = parseFloat(window.getComputedStyle(h2).fontSize);
var minSize = 12;
h2.style.fontSize = '';
fontSize = parseFloat(window.getComputedStyle(h2).fontSize);
while (h2.scrollHeight > maxHeight && fontSize > minSize) {
fontSize -= 1;
h2.style.fontSize = fontSize + 'px';
}
});
}
window.fitPostCardTitles = fitTitles;
fitTitles();
var resizeTimer;
window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(fitTitles, 200);
});
// Re-fit after infinite scroll loads new cards
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (m) {
if (m.addedNodes.length) fitTitles();
});
});
var grid = document.getElementById('post-grid');
if (grid) {
observer.observe(grid, { childList: true });
}
});

37
js/frenchTypography.js Normal file
View File

@@ -0,0 +1,37 @@
/**
* French typography: replaces normal spaces with non-breaking spaces
* where French typographic rules require them.
*/
function applyFrenchTypography(elements) {
elements.forEach(function (el) {
var html = el.innerHTML;
// Collapse multiple spaces into one (outside HTML tags)
html = html.replace(/([^<>]) {2,}(?=[^<>])/g, '$1 ');
// Space before ? ! : ; »
html = html.replace(/ ([?!:;»])/g, '\u00A0$1');
// Space after «
html = html.replace(/(«) /g, '$1\u00A0');
// "p. 42" → non-breaking space after "p."
html = html.replace(/\bp\. /g, 'p.\u00A0');
// "n° 3" → non-breaking space after "n°"
html = html.replace(/\bn° /g, 'n°\u00A0');
el.innerHTML = html;
});
}
document.addEventListener('DOMContentLoaded', function () {
applyFrenchTypography(document.querySelectorAll('.post-card h2, .post-card .post-card__title'));
// Re-apply on any post-card dynamically added anywhere on the page
new MutationObserver(function (mutations) {
mutations.forEach(function (m) {
m.addedNodes.forEach(function (node) {
if (node.nodeType !== 1) return;
var targets = node.classList && node.classList.contains('post-card')
? node.querySelectorAll('h2, .post-card__title')
: node.querySelectorAll('.post-card h2, .post-card .post-card__title');
applyFrenchTypography(targets);
});
});
}).observe(document.body, { childList: true, subtree: true });
});

16
js/imageSwiper.js Normal file
View File

@@ -0,0 +1,16 @@
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-image-swiper]').forEach(function (container) {
new Swiper(container.querySelector('.swiper'), {
slidesPerView: 1,
loop: true,
navigation: {
nextEl: container.querySelector('.swiper-button-next'),
prevEl: container.querySelector('.swiper-button-prev'),
},
pagination: {
el: container.querySelector('.swiper-pagination'),
clickable: true,
},
});
});
});

71
js/infiniteScroll.js Normal file
View File

@@ -0,0 +1,71 @@
document.addEventListener('DOMContentLoaded', function () {
var sentinel = document.getElementById('scroll-sentinel');
var grid = document.getElementById('post-grid');
var spinner = document.getElementById('scroll-spinner');
if (!sentinel || !grid) return;
var page = 1;
var loading = false;
var done = false;
// Read filter params from sentinel data attributes (set server-side)
var categoryId = sentinel.dataset.category || '';
var axe = sentinel.dataset.axe || '';
var dateFrom = sentinel.dataset.dateFrom || '';
var dateTo = sentinel.dataset.dateTo || '';
var taxonomy = sentinel.dataset.taxonomy || '';
var termId = sentinel.dataset.term || '';
var catFilter = sentinel.dataset.filterCat || '';
var filterAutres = sentinel.dataset.filterAutres || '';
var excludeCats = sentinel.dataset.excludeCats || '';
var searchQuery = sentinel.dataset.search || '';
function fetchPosts() {
if (loading || done) return;
loading = true;
page++;
spinner.style.display = 'flex';
var data = new FormData();
data.append('action', 'load_more_posts');
data.append('page', page);
data.append('nonce', infiniteScrollData.nonce);
data.append('lang', infiniteScrollData.lang || 'fr');
if (categoryId) data.append('category', categoryId);
if (axe) data.append('axe', axe);
if (dateFrom) data.append('date_from', dateFrom);
if (dateTo) data.append('date_to', dateTo);
if (taxonomy) data.append('taxonomy', taxonomy);
if (termId) data.append('term', termId);
if (catFilter) data.append('filter_cat', catFilter);
if (filterAutres) data.append('filter_autres', filterAutres);
if (excludeCats) data.append('exclude_cats', excludeCats);
if (searchQuery) data.append('search', searchQuery);
fetch(infiniteScrollData.ajaxUrl, { method: 'POST', body: data })
.then(function (response) { return response.json(); })
.then(function (result) {
spinner.style.display = 'none';
if (result.success && result.data.html) {
grid.insertAdjacentHTML('beforeend', result.data.html);
loading = false;
} else {
done = true;
observer.disconnect();
}
})
.catch(function () {
spinner.style.display = 'none';
loading = false;
});
}
var observer = new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) {
fetchPosts();
}
}, { rootMargin: '200px' });
observer.observe(sentinel);
});

249
js/keywordCloud.js Normal file
View File

@@ -0,0 +1,249 @@
(function () {
'use strict';
document.addEventListener('DOMContentLoaded', function () {
var container = document.getElementById('keyword-container');
var tags = window.thalimTags;
if (!container || !tags || !tags.length) return;
// — init —
var GAP = 14;
var COLORS = ['#e0775d', '#7cc0c6', '#e05680', '#46ae51', '#bb8dd9'];
var colorTimeouts = new Map();
// Mélange aléatoire pour varier la disposition à chaque chargement
var shuffled = tags.slice().sort(function () { return Math.random() - 0.5; });
// Crée les éléments une seule fois (réutilisés à chaque layout)
var items = shuffled.map(function (tag) {
var el = document.createElement('a');
el.className = 'keyword';
el.href = tag.url;
el.textContent = tag.name;
el.addEventListener('mouseenter', function () {
if (colorTimeouts.has(el)) clearTimeout(colorTimeouts.get(el));
el.style.color = COLORS[Math.floor(Math.random() * COLORS.length)];
colorTimeouts.set(el, setTimeout(function () {
el.style.color = '';
colorTimeouts.delete(el);
}, 2000));
});
container.appendChild(el);
return { el: el, w: 0, h: 0 };
});
var gradOverlay = null;
var lastLayoutWidth = 0;
function layoutCloud() {
var cw = container.offsetWidth;
if (cw === lastLayoutWidth) return;
lastLayoutWidth = cw;
var cx = cw / 2;
var isMobile = cw < 768;
var a = cx - GAP; // demi-grand axe horizontal
var b = Math.round(cw * (isMobile ? 0.45 : 0.20)); // demi-petit axe vertical (plus haut sur mobile)
var R_V = Math.round(b * (isMobile ? 0.45 : 0.70)); // demi-axe vertical de la zone d'exclusion (plus petit sur mobile)
var R_H = Math.round(b * (isMobile ? 0.70 : 1.15)); // demi-axe horizontal (plus petit sur mobile)
// Re-mesurer les éléments (la taille peut changer avec le viewport)
// +1 compense les arrondis sub-pixel sur écrans haute densité
items.forEach(function (item) {
var rect = item.el.getBoundingClientRect();
item.w = Math.ceil(rect.width) + 1;
item.h = Math.ceil(rect.height) + 1;
});
var placed = [];
function hasOverlap(x, y, w, h) {
for (var i = 0; i < placed.length; i++) {
var p = placed[i];
if (x < p.x + p.w + GAP &&
x + w + GAP > p.x &&
y < p.y + p.h + GAP &&
y + h + GAP > p.y) return true;
}
return false;
}
// Placement contraint à l'anneau elliptique :
// { intérieur ellipse (a, b) } ∩ { extérieur ellipse d'exclusion (R_H, R_V) }
// Les candidats sont triés par distance à l'ellipse d'exclusion (croissante) :
// les mots s'accumulent au plus près du centre avant de s'étendre.
function findPos(w, h) {
var candidates = [];
for (var x = 0; x <= cw - w; x += 8) {
var dx = (x + w / 2) - cx;
var ratio = dx / a;
if (Math.abs(ratio) >= 1) continue;
// Limite verticale du centre du mot imposée par l'ellipse externe (avec marge h/2)
var maxPcy = b * Math.sqrt(1 - ratio * ratio);
if (maxPcy < h / 2) continue;
var maxAbsY = maxPcy - h / 2;
// Limite verticale minimale imposée par l'ellipse d'exclusion (R_H, R_V)
var minAbsY = (Math.abs(dx) >= R_H) ? 0
: R_V * Math.sqrt(Math.max(0, 1 - (dx * dx) / (R_H * R_H)));
if (minAbsY > maxAbsY) continue;
for (var absY = minAbsY; absY <= maxAbsY; absY += 4) {
// Distance normalisée à l'ellipse d'exclusion (0 = sur le bord)
var nx = dx / R_H, ny = absY / R_V;
var dist = Math.sqrt(nx * nx + ny * ny) - 1;
var yB = Math.round(absY - h / 2);
if (absY > 0) {
candidates.push({ x: x, y: Math.round(-absY - h / 2), dist: dist });
}
candidates.push({ x: x, y: yB, dist: dist });
}
}
// Trier par distance à l'ellipse d'exclusion croissante → attraction vers le centre
candidates.sort(function (ca, cb) { return ca.dist - cb.dist; });
for (var i = 0; i < candidates.length; i++) {
var c = candidates[i];
if (!hasOverlap(c.x, c.y, w, h)) return { x: c.x, y: c.y };
}
return null; // aucune position dans l'anneau
}
// Place les mots dans l'anneau elliptique, collecte les débordements
var overflow = [];
items.forEach(function (item) {
var pos = findPos(item.w, item.h);
if (pos) {
item.pos = { x: pos.x, y: pos.y, w: item.w, h: item.h };
placed.push(item.pos);
item.el.style.left = pos.x + 'px';
item.el.style.top = pos.y + 'px';
} else {
overflow.push(item);
}
});
// Placement des débordements en lignes centrées (style flex-wrap center)
if (overflow.length) {
var startY = placed.reduce(function (m, p) { return Math.max(m, p.y + p.h); }, 0) + GAP;
var rows = [];
var currentRow = [];
var currentRowW = 0;
overflow.forEach(function (item) {
var needed = currentRowW > 0 ? item.w + GAP : item.w;
if (currentRowW + needed > cw && currentRow.length > 0) {
rows.push(currentRow);
currentRow = [];
currentRowW = 0;
}
currentRow.push(item);
currentRowW += (currentRowW > 0 ? GAP : 0) + item.w;
});
if (currentRow.length) rows.push(currentRow);
var curY = startY;
rows.forEach(function (row) {
var rowW = row.reduce(function (s, item) { return s + item.w; }, 0) + (row.length - 1) * GAP;
var rowH = row.reduce(function (m, item) { return Math.max(m, item.h); }, 0);
var offsetX = Math.round((cw - rowW) / 2);
row.forEach(function (item) {
item.pos = { x: offsetX, y: curY, w: item.w, h: item.h };
placed.push(item.pos);
item.el.style.left = offsetX + 'px';
item.el.style.top = curY + 'px';
offsetX += item.w + GAP;
});
curY += rowH + GAP;
});
}
// Normalisation Y : cy=0 → shift px depuis le haut du conteneur
var minY = items.reduce(function (m, item) { return Math.min(m, item.pos.y); }, Infinity);
var shift = Math.max(0, GAP - minY);
items.forEach(function (item) {
item.pos.y += shift;
item.el.style.top = item.pos.y + 'px';
});
// Hauteur basée sur le contenu réel (évite le débordement sur mobile)
var maxPlacedY = items.reduce(function (m, item) { return Math.max(m, item.pos.y + item.pos.h); }, 0);
container.style.height = (maxPlacedY + GAP) + 'px';
// Dégradé sur un overlay séparé pour permettre l'animation scale au survol
if (gradOverlay) {
container.removeChild(gradOverlay);
}
gradOverlay = document.createElement('div');
gradOverlay.style.cssText =
'position:absolute;inset:0;pointer-events:none;' +
'background:radial-gradient(circle ' + Math.round(R_V * 2) + 'px at ' + cx + 'px ' + shift + 'px,' +
'rgba(247,255,41,1) 0%,rgba(247,255,41,0.6) 16%,rgba(247,255,41,0.15) 55%,transparent 70%);' +
'transform-origin:' + cx + 'px ' + shift + 'px;' +
'transition:transform 0.4s ease;';
container.insertBefore(gradOverlay, container.firstChild);
}
// Premier layout
layoutCloud();
// Événements du dégradé (référencent gradOverlay via closure)
container.addEventListener('mouseenter', function () {
if (!gradOverlay) return;
gradOverlay.style.transition = 'transform 0.4s ease';
gradOverlay.style.transform = 'scale(0.82)';
});
container.addEventListener('mousemove', function (e) {
if (!gradOverlay) return;
var cw = container.offsetWidth;
var cx = cw / 2;
var rect = container.getBoundingClientRect();
var dx = (e.clientX - rect.left) - cx;
var dy = (e.clientY - rect.top) - parseFloat(gradOverlay.style.transformOrigin.split(' ')[1]);
var tx = (-dx * 0.09).toFixed(2);
var ty = (-dy * 0.09).toFixed(2);
gradOverlay.style.transition = 'transform 0.15s ease-out';
gradOverlay.style.transform = 'translate(' + tx + 'px,' + ty + 'px) scale(0.82)';
});
container.addEventListener('mouseleave', function () {
if (!gradOverlay) return;
gradOverlay.style.transition = 'transform 0.9s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
gradOverlay.style.transform = 'scale(1)';
});
// Resize avec debounce
var resizeTimer;
window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
lastLayoutWidth = 0; // forcer le recalcul
layoutCloud();
}, 250);
});
// Animation d'apparition au scroll
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
items.forEach(function (item, i) {
item.el.style.animationDelay = (i * 0.03) + 's';
item.el.classList.add('keyword--visible');
});
observer.unobserve(container);
}
});
}, { threshold: 0.05 });
observer.observe(container);
});
})();

229
js/membresFilters.js Normal file
View File

@@ -0,0 +1,229 @@
document.addEventListener('DOMContentLoaded', function () {
// ── Filters toggle ────────────────────────────────────────
var membresToggleBtn = document.getElementById('membres-filters-toggle');
var membresFiltersEl = document.getElementById('membres-filters');
if (membresToggleBtn && membresFiltersEl) {
membresToggleBtn.addEventListener('click', function () {
var isOpen = membresFiltersEl.classList.toggle('is-open');
membresToggleBtn.classList.toggle('is-open', isOpen);
membresToggleBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
if (isOpen) {
var searchInput = document.getElementById('membres-search');
if (searchInput) searchInput.focus();
}
});
}
var activeRole = '';
var searchQuery = '';
// ── Dropdown (role filter) ────────────────────────────────
var roleBtn = document.getElementById('filter-role-btn');
var rolePopover = document.getElementById('filter-role-popover');
var roleLabel = document.getElementById('filter-role-label');
var roleReset = document.getElementById('role-reset');
var roleDd = document.getElementById('filter-role-dd');
function openRole() { rolePopover.style.display = ''; roleDd.classList.add('is-open'); }
function closeRole() { rolePopover.style.display = 'none'; roleDd.classList.remove('is-open'); }
roleBtn.addEventListener('click', function () {
rolePopover.style.display === 'none' ? openRole() : closeRole();
});
rolePopover.querySelectorAll('[data-role]').forEach(function (item) {
item.addEventListener('click', function () {
activeRole = item.dataset.role;
roleLabel.textContent = item.textContent.trim();
roleReset.style.display = activeRole ? '' : 'none';
roleDd.classList.toggle('is-active', !!activeRole);
closeRole();
updateChips();
applyFilters();
});
});
roleReset.addEventListener('click', function (e) {
e.preventDefault();
activeRole = '';
roleLabel.textContent = rolePopover.querySelector('[data-role=""]').textContent.trim();
roleReset.style.display = 'none';
roleDd.classList.remove('is-active');
updateChips();
applyFilters();
});
document.addEventListener('click', function (e) {
if (!roleDd.contains(e.target)) closeRole();
});
// ── Search input ──────────────────────────────────────────
var searchInput = document.getElementById('membres-search');
var searchReset = document.getElementById('search-reset');
searchInput.addEventListener('input', function () {
searchQuery = searchInput.value.trim().toLowerCase();
searchReset.style.display = searchQuery ? '' : 'none';
searchInput.classList.toggle('is-active', !!searchQuery);
updateChips();
applyFilters();
});
searchReset.addEventListener('click', function (e) {
e.preventDefault();
searchQuery = '';
searchInput.value = '';
searchReset.style.display = 'none';
searchInput.classList.remove('is-active');
updateChips();
applyFilters();
});
// ── Active chips ──────────────────────────────────────────
var chipsContainer = document.getElementById('membres-active-chips');
function makeChip(label, onRemove) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'filter-chip';
btn.innerHTML = label + '<i class="iconoir-xmark"></i>';
btn.addEventListener('click', onRemove);
return btn;
}
function updateChips() {
if (!chipsContainer) return;
chipsContainer.innerHTML = '';
if (activeRole) {
chipsContainer.appendChild(makeChip(activeRole, function () {
activeRole = '';
roleLabel.textContent = rolePopover.querySelector('[data-role=""]').textContent.trim();
roleReset.style.display = 'none';
roleDd.classList.remove('is-active');
updateChips();
applyFilters();
}));
}
if (searchQuery) {
chipsContainer.appendChild(makeChip(searchInput.value, function () {
searchQuery = '';
searchInput.value = '';
searchReset.style.display = 'none';
searchInput.classList.remove('is-active');
updateChips();
applyFilters();
}));
}
}
// ── Column sort ───────────────────────────────────────────
var sortKey = 'nom';
var sortDir = 'asc';
function getSortValue(row, key) {
if (key === 'nom') {
return (row.dataset.sortName || '').trim().toLowerCase();
}
if (key === 'statut') return (row.dataset.status || '').toLowerCase();
if (key === 'affiliation') return (row.dataset.affiliation || '').toLowerCase();
return '';
}
function applySortToTable(tbody) {
var rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort(function (a, b) {
var va = getSortValue(a, sortKey);
var vb = getSortValue(b, sortKey);
var cmp = va.localeCompare(vb, 'fr', { sensitivity: 'base' });
return sortDir === 'asc' ? cmp : -cmp;
});
rows.forEach(function (row) { tbody.appendChild(row); });
}
function applySort() {
document.querySelectorAll('.membres-table tbody').forEach(function (tbody) {
if (!tbody.closest('table').hasAttribute('data-fixed-order')) {
applySortToTable(tbody);
}
restripe(tbody);
});
document.querySelectorAll('.membres-table th[data-sort]').forEach(function (th) {
th.classList.remove('sort-asc', 'sort-desc');
if (th.dataset.sort === sortKey) {
th.classList.add('sort-' + sortDir);
}
});
}
document.querySelectorAll('.membres-table th[data-sort]').forEach(function (th) {
th.addEventListener('click', function () {
var key = th.dataset.sort;
if (sortKey === key) {
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
} else {
sortKey = key;
sortDir = 'asc';
}
applySort();
});
});
applySort();
// ── Row restriping (fixes alternating colors after filter) ──
function restripe(tbody) {
var n = 0;
tbody.querySelectorAll('tr').forEach(function (row) {
if (row.style.display === 'none') return;
row.classList.toggle('is-even-row', n % 2 === 1);
n++;
});
}
// ── Apply both filters ────────────────────────────────────
function applyFilters() {
var isFiltering = activeRole || searchQuery;
document.querySelectorAll('.membres-item').forEach(function (item) {
// Filter rows by role and/or name
var rows = item.querySelectorAll('tbody tr');
var visible = 0;
rows.forEach(function (row) {
var name = (row.dataset.name || '').toLowerCase();
var status = (row.dataset.status || '').toLowerCase();
var affiliation = (row.dataset.affiliation || '').toLowerCase();
var roles = (row.dataset.roles || '').split('|').map(function (r) { return r.trim().toLowerCase(); });
var matchesRole = !activeRole || roles.includes(activeRole.toLowerCase());
var matchesName = !searchQuery || name.includes(searchQuery)
|| status.includes(searchQuery)
|| affiliation.includes(searchQuery);
var show = matchesRole && matchesName;
row.style.display = show ? '' : 'none';
if (show) visible++;
});
item.querySelectorAll('tbody').forEach(restripe);
// Hide group if no rows are visible
if (isFiltering && visible === 0) {
item.style.display = 'none';
return;
}
item.style.display = '';
var content = item.querySelector('.membres-content');
if (isFiltering) {
// Auto-expand when a filter is active
content.style.display = '';
item.classList.add('is-open');
} else {
// Collapse back when all filters are cleared
content.style.display = 'none';
item.classList.remove('is-open');
}
});
}
});

81
js/membresPopover.js Normal file
View File

@@ -0,0 +1,81 @@
document.addEventListener('DOMContentLoaded', function () {
var section = document.querySelector('.membres-section');
if (!section) return;
// Build the popover element once and append to body
var popover = document.createElement('div');
popover.id = 'membre-popover';
popover.innerHTML =
'<div class="membre-popover-inner">' +
'<img class="membre-popover-pic" src="" alt="">' +
'<div class="membre-popover-info">' +
'<p class="membre-popover-name"></p>' +
'<p class="membre-popover-status"></p>' +
'<p class="membre-popover-domaines"></p>' +
'<p class="membre-popover-autres"></p>' +
'</div>' +
'</div>';
document.body.appendChild(popover);
var pic = popover.querySelector('.membre-popover-pic');
var elName = popover.querySelector('.membre-popover-name');
var elStat = popover.querySelector('.membre-popover-status');
var elDom = popover.querySelector('.membre-popover-domaines');
var elAut = popover.querySelector('.membre-popover-autres');
var visible = false;
var currentRow = null;
// ── Show/hide via event delegation on the section ────────
section.addEventListener('mouseover', function (e) {
var row = e.target.closest('tbody tr');
if (!row || row === currentRow) return;
currentRow = row;
var avatar = row.dataset.avatar || '';
if (avatar) {
pic.src = avatar;
pic.style.display = '';
} else {
pic.src = '';
pic.style.display = 'none';
}
elName.textContent = row.dataset.name || '';
elStat.textContent = row.dataset.status || '';
var domVal = row.dataset.domaines || '';
elDom.textContent = domVal;
elDom.style.display = domVal ? '' : 'none';
var autVal = row.dataset.autresDomaines || '';
elAut.innerHTML = autVal.replace(/\n/g, '<br>');
elAut.style.display = autVal ? '' : 'none';
popover.classList.add('is-visible');
visible = true;
});
section.addEventListener('mouseout', function (e) {
var row = e.target.closest('tbody tr');
if (!row) return;
// only hide when leaving the row entirely (not moving to a child)
if (e.relatedTarget && row.contains(e.relatedTarget)) return;
currentRow = null;
popover.classList.remove('is-visible');
visible = false;
});
// ── Follow the cursor ─────────────────────────────────────
document.addEventListener('mousemove', function (e) {
if (!visible) return;
var x = e.clientX + 18;
var y = e.clientY + 18;
var pw = popover.offsetWidth;
var ph = popover.offsetHeight;
if (x + pw > window.innerWidth) x = e.clientX - pw - 8;
if (y + ph > window.innerHeight) y = e.clientY - ph - 8;
popover.style.left = x + 'px';
popover.style.top = y + 'px';
});
});

57
js/messageLabo.js Normal file
View File

@@ -0,0 +1,57 @@
document.addEventListener('DOMContentLoaded', function () {
var messageList = document.querySelector('.messages-list');
var agendaContent = document.querySelector('.agenda-content');
var sectionTitle = document.querySelector('.message-du-labo .section-title');
var buttonMessages = document.querySelector('.button-messages');
if (!messageList || !agendaContent) return;
var items = Array.from(messageList.querySelectorAll('.message-item'));
function sync() {
items.forEach(function (item) {
item.style.display = '';
var content = item.querySelector('.message-content');
if (content) { content.style.maxHeight = ''; content.classList.remove('is-overflowing'); }
});
if (window.innerWidth < 768) {
// Mobile : afficher uniquement le premier message
items.forEach(function (item, i) { item.style.display = i === 0 ? '' : 'none'; });
return;
}
var budget = agendaContent.offsetHeight
- (sectionTitle ? sectionTitle.offsetHeight : 0)
- (buttonMessages ? buttonMessages.offsetHeight : 0);
var used = 0;
for (var i = 0; i < items.length; i++) {
var item = items[i];
var itemHeight = item.offsetHeight;
if (used + itemHeight <= budget) {
used += itemHeight;
} else {
var remaining = budget - used;
var content = item.querySelector('.message-content');
if (content && remaining > 100) {
var contentBudget = remaining - (itemHeight - content.offsetHeight);
if (contentBudget > 60) {
content.style.maxHeight = contentBudget + 'px';
content.classList.add('is-overflowing');
} else {
item.style.display = 'none';
}
} else {
item.style.display = 'none';
}
for (var j = i + 1; j < items.length; j++) {
items[j].style.display = 'none';
}
break;
}
}
}
sync();
window.addEventListener('resize', sync);
});

32
js/navAxesToggle.js Normal file
View File

@@ -0,0 +1,32 @@
document.addEventListener('DOMContentLoaded', function () {
var item = document.querySelector('.nav-axes-item');
if (!item) return;
var trigger = item.querySelector('.nav-axes-trigger');
var mainMenu = document.querySelector('.main-menu');
trigger.addEventListener('click', function (e) {
e.stopPropagation();
var isOpen = item.classList.toggle('is-open');
trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
// Close when clicking outside
document.addEventListener('click', function (e) {
if (!item.contains(e.target)) {
item.classList.remove('is-open');
trigger.setAttribute('aria-expanded', 'false');
}
});
// Reset when main menu closes
if (mainMenu) {
var observer = new MutationObserver(function () {
if (!mainMenu.classList.contains('active')) {
item.classList.remove('is-open');
trigger.setAttribute('aria-expanded', 'false');
}
});
observer.observe(mainMenu, { attributes: true, attributeFilter: ['class'] });
}
});

133
js/overlay.js Normal file
View File

@@ -0,0 +1,133 @@
document.addEventListener('DOMContentLoaded', function() {
const header = document.querySelector('header');
const body = document.querySelector('body');
const menuToggle = document.querySelector('.menu-toggle');
const mainMenu = document.querySelector('.main-menu');
const menuOverlay = document.querySelector('.overlay');
const menuIcon = document.querySelector('.menu-toggle i');
const wpAdminBar = document.querySelector('#wpadminbar');
const stickyHeaderMobile = document.querySelector('.header-right');
const searchButton = document.querySelector('.search-button');
const searchPanel = document.querySelector('.search-panel');
const searchIcon = document.querySelector('.search-button i');
const searchInput = document.querySelector('.search-panel__input');
const breakpointTablet = 768;
mainMenu.style.top = `${mainMenu.offsetHeight * -1}px`;
searchPanel.style.top = `${searchPanel.offsetHeight * -1}px`;
// Compute the pixel offset at which panels should appear (just below the header)
function getHeaderBottom() {
const adminBarHeight = wpAdminBar ? wpAdminBar.offsetHeight : 0;
if (window.innerWidth < breakpointTablet) {
if (window.scrollY > header.getBoundingClientRect().bottom) {
return stickyHeaderMobile.getBoundingClientRect().bottom + window.scrollY;
}
return header.getBoundingClientRect().bottom + window.scrollY;
}
return header.offsetHeight + adminBarHeight;
}
function updateOverlay() {
const anyOpen = mainMenu.classList.contains('active') || searchPanel.classList.contains('active');
menuOverlay.classList.toggle('active', anyOpen);
if (anyOpen) {
body.style.overflow = 'hidden';
} else {
body.style.removeProperty('overflow');
}
}
// --- Menu ---
function openMenu() {
mainMenu.scrollTo(0, 0);
if (window.innerWidth < breakpointTablet) {
const adminBarHeight = wpAdminBar ? wpAdminBar.offsetHeight : 0;
if (window.scrollY > header.getBoundingClientRect().bottom) {
mainMenu.style.height = `${window.innerHeight - adminBarHeight - stickyHeaderMobile.offsetHeight}px`;
} else {
mainMenu.style.height = `${window.innerHeight - header.getBoundingClientRect().bottom}px`;
}
} else {
mainMenu.style.removeProperty('height');
}
mainMenu.style.top = `${getHeaderBottom()}px`;
mainMenu.classList.add('active');
menuIcon.classList.remove('iconoir-menu');
menuIcon.classList.add('iconoir-xmark');
}
function closeMenu() {
mainMenu.style.top = `${mainMenu.offsetHeight * -1}px`;
mainMenu.classList.remove('active');
menuIcon.classList.remove('iconoir-xmark');
menuIcon.classList.add('iconoir-menu');
}
function toggleMenu() {
if (searchPanel.classList.contains('active')) closeSearch();
if (mainMenu.classList.contains('active')) {
closeMenu();
} else {
openMenu();
}
updateOverlay();
}
// --- Search ---
function openSearch() {
searchPanel.style.top = `${getHeaderBottom()}px`;
searchPanel.classList.add('active');
searchIcon.classList.remove('iconoir-search');
searchIcon.classList.add('iconoir-xmark');
setTimeout(function() { if (searchInput) searchInput.focus(); }, 400);
}
function closeSearch() {
searchPanel.style.top = `${searchPanel.offsetHeight * -1}px`;
searchPanel.classList.remove('active');
searchIcon.classList.remove('iconoir-xmark');
searchIcon.classList.add('iconoir-search');
}
function toggleSearch() {
if (mainMenu.classList.contains('active')) closeMenu();
if (searchPanel.classList.contains('active')) {
closeSearch();
} else {
openSearch();
}
updateOverlay();
}
// --- Event listeners ---
menuToggle.addEventListener('click', toggleMenu);
searchButton.addEventListener('click', toggleSearch);
menuOverlay.addEventListener('click', function() {
if (mainMenu.classList.contains('active')) closeMenu();
if (searchPanel.classList.contains('active')) closeSearch();
updateOverlay();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
if (mainMenu.classList.contains('active')) closeMenu();
if (searchPanel.classList.contains('active')) closeSearch();
updateOverlay();
}
});
let resizeTimeout;
window.addEventListener('resize', function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function() {
if (mainMenu.classList.contains('active')) closeMenu();
if (searchPanel.classList.contains('active')) closeSearch();
updateOverlay();
mainMenu.style.top = `${mainMenu.offsetHeight * -1}px`;
searchPanel.style.top = `${searchPanel.offsetHeight * -1}px`;
}, 150);
});
});

24
js/quickLinks.js Normal file
View File

@@ -0,0 +1,24 @@
document.addEventListener('DOMContentLoaded', function () {
var quickLinks = document.querySelector('.quick-links');
if (!quickLinks) return;
// Last section: keyword cloud if present, otherwise last swiper section
var lastSection = document.querySelector('.keyword-cloud');
if (!lastSection) {
var swiperSections = document.querySelectorAll('.swiper-section');
lastSection = swiperSections[swiperSections.length - 1] || null;
}
if (!lastSection) return;
var initialTop = quickLinks.getBoundingClientRect().top + window.scrollY;
var quickLinksHeight = quickLinks.offsetHeight;
window.addEventListener('scroll', function () {
var sectionBottom = lastSection.getBoundingClientRect().bottom;
if (initialTop - sectionBottom > 0) {
quickLinks.style.top = (initialTop - (initialTop - sectionBottom) - quickLinksHeight) + 'px';
} else {
quickLinks.style.top = initialTop + 'px';
}
});
});

39
js/seanceToggle.js Normal file
View File

@@ -0,0 +1,39 @@
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-seance-toggle]').forEach(function (header) {
header.addEventListener('click', function (e) {
// Don't toggle when clicking a link
if (e.target.closest('a')) return;
var item = header.closest('.seance-item, .membres-item, .author-posts-item, .labo-dropdown-item');
var content = item.querySelector('.seance-content, .membres-content, .author-posts-content, .labo-dropdown-content');
var isOpen = item.classList.contains('is-open');
if (isOpen) {
content.style.display = 'none';
item.classList.remove('is-open');
} else {
content.style.display = '';
item.classList.add('is-open');
if (window.fitPostCardTitles) window.fitPostCardTitles();
}
});
});
// Auto-expand and scroll to a séance targeted by URL hash (#seance-{ID})
var hash = window.location.hash;
if (hash && hash.startsWith('#seance-')) {
var target = document.querySelector(hash + '.seance-item');
if (target) {
var content = target.querySelector('.seance-content');
if (content) {
content.style.display = '';
target.classList.add('is-open');
if (window.fitPostCardTitles) window.fitPostCardTitles();
setTimeout(function () {
var top = target.getBoundingClientRect().top + window.scrollY - 150;
window.scrollTo({ top: top, behavior: 'smooth' });
}, 150);
}
}
}
});

91
js/stickyHeader.js Normal file
View File

@@ -0,0 +1,91 @@
document.addEventListener('DOMContentLoaded', function() {
const header = document.querySelector('header');
const wpAdminBar = document.querySelector('#wpadminbar');
const stickyHeaderMobile = document.querySelector('.header-right');
const relativeHeaderMobile = document.querySelector('.header-left');
const mainLogo = document.querySelector('.main-logo');
const description = document.querySelector('.description');
const burgerContainer = document.querySelector('.menu-toggle > div');
const menuIconContainer = document.querySelector('.menu-toggle > div > div');
const menuText = document.querySelector('.menu-toggle > div > p');
const breakpointTablet = 768;
function checkMobile() {
if (window.innerWidth < breakpointTablet) {
stickyHeaderMobile.style.top = wpAdminBar ? `${wpAdminBar.offsetHeight}px` : '0px';
} else {
stickyHeaderMobile.style.top = 'unset';
}
}
function resetStyles() {
header.style.removeProperty("height");
mainLogo.style.removeProperty("padding");
description.style.removeProperty("opacity");
burgerContainer.style.removeProperty("padding");
burgerContainer.style.removeProperty("justify-content");
menuIconContainer.style.removeProperty("font-size");
menuText.style.removeProperty("display");
}
window.addEventListener('scroll', () => {
const isScrolledTop = window.scrollY === 0;
if (window.innerWidth < breakpointTablet) {
// mobile
if (window.scrollY > header.getBoundingClientRect().bottom) {
// déployer petit logo à gauche
if(!stickyHeaderMobile.classList.contains('scrolled')) {
stickyHeaderMobile.classList.add('scrolled')
}
} else {
// rétracter petit logo à gauche
if(stickyHeaderMobile.classList.contains('scrolled')) {
stickyHeaderMobile.classList.remove('scrolled');
}
}
} else {
// desktop
header.style.height = isScrolledTop ? '12vh' : '6vh';
header.style.minHeight = isScrolledTop ? '100px' : 'unset';
mainLogo.style.padding = isScrolledTop ? '1.5rem 2rem' : '0.2rem 0.4rem';
description.style.opacity = isScrolledTop ? '1' : '0';
burgerContainer.style.padding = isScrolledTop ? '2rem' : '0.6rem 1rem';
burgerContainer.style.justifyContent = isScrolledTop ? 'space-between' : 'center';
menuIconContainer.style.fontSize = isScrolledTop ? '2rem' : '1.5rem';
menuText.style.display = isScrolledTop ? '' : 'none';
if (window.scrollY === 0) {
// agrandir le header
} else {
// diminuer le header
}
}
});
let resizeTimeout;
let previousWidth = window.innerWidth;
window.addEventListener('resize', () => {
let currentWidth = window.innerWidth;
if (currentWidth !== previousWidth) {
window.scrollTo(0, 0);
previousWidth = currentWidth;
}
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function() {
resetStyles();
checkMobile();
}, 150);
});
checkMobile();
});

165
page-annonces.php Normal file
View File

@@ -0,0 +1,165 @@
<?php
$context = Timber::context();
$excluded_cat_ids = [12, 31]; // Séance de séminaire, Non classé
if ( ! is_user_logged_in() ) $excluded_cat_ids[] = 9; // Vie du labo
// Read filter query params
$active_axe = isset($_GET['axe']) ? intval($_GET['axe']) : 0;
$active_date_from = isset($_GET['date_from']) ? sanitize_text_field($_GET['date_from']) : '';
$active_date_to = isset($_GET['date_to']) ? sanitize_text_field($_GET['date_to']) : '';
$active_cat_id = isset($_GET['filter_cat']) ? intval($_GET['filter_cat']) : 0;
$filter_autres = isset($_GET['filter_autres']) ? 1 : 0;
$context['active_axe'] = $active_axe;
$context['active_date_from'] = $active_date_from;
$context['active_date_to'] = $active_date_to;
$context['active_category_id'] = $filter_autres ? 'autres' : $active_cat_id;
$context['active_cat_id'] = $active_cat_id;
$context['filter_autres'] = $filter_autres;
// Redirect ?filter_cat=X to the actual category page (preserving axe/date params)
if ( $active_cat_id && ! $filter_autres ) {
$cat_obj = get_category( $active_cat_id );
if ( $cat_obj && ! is_wp_error( $cat_obj ) ) {
$redir_params = array_filter([
'axe' => $active_axe ?: null,
'date_from' => $active_date_from ?: null,
'date_to' => $active_date_to ?: null,
]);
$redir_url = $redir_params
? add_query_arg( $redir_params, get_category_link( $cat_obj->term_id ) )
: get_category_link( $cat_obj->term_id );
wp_redirect( $redir_url, 301 );
exit;
}
}
// Determine active rubrique
$active_rubrique_id = 0;
if ($active_cat_id) {
$active_cat_obj = get_category($active_cat_id);
$active_rubrique_id = ($active_cat_obj && $active_cat_obj->parent)
? $active_cat_obj->parent
: $active_cat_id;
}
$context['active_rubrique'] = $active_rubrique_id;
// Base filter params preserved across all filter links
$base_filter_params = array_filter([
'axe' => $active_axe ?: null,
'date_from' => $active_date_from ?: null,
'date_to' => $active_date_to ?: null,
]);
// Build tax_query: exclude séances + optional category filter
$tax_query = [
'relation' => 'AND',
[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => $excluded_cat_ids,
'operator' => 'NOT IN',
],
];
if ($active_cat_id) {
$tax_query[] = [
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [$active_cat_id],
'include_children' => !$filter_autres,
];
}
$query_args = [
'post_type' => 'post',
'posts_per_page' => 12,
'orderby' => 'date',
'order' => 'DESC',
'lang' => '',
'tax_query' => $tax_query,
'thalim_event_date_order' => true,
];
if ($active_axe) {
$query_args['meta_query'] = [[
'key' => 'axes_thematiques',
'value' => $active_axe,
'type' => 'NUMERIC',
]];
}
if ($active_date_from || $active_date_to) {
$query_args['thalim_event_date_filter'] = ['from' => $active_date_from, 'to' => $active_date_to];
}
// Axes thématiques for filter dropdown
$axes_groups = thalim_get_axes_filter_groups();
$current_axes = $axes_groups[0]['terms'] ?? [];
$context['filter_axes'] = $current_axes;
// Rubrique/catégorie filter links (stay on this page with filter_cat param)
$page_url = get_permalink();
$all_cats = get_categories(['taxonomy' => 'category', 'hide_empty' => false, 'exclude' => $excluded_cat_ids]);
$filter_parents = [];
foreach ($all_cats as $cat) {
if ($cat->parent == 0) {
$filter_parents[] = [
'id' => $cat->term_id,
'name' => thalim_cat_name($cat),
'slug' => $cat->slug,
'link' => $base_filter_params
? add_query_arg($base_filter_params, get_category_link($cat->term_id))
: get_category_link($cat->term_id),
];
}
}
$context['filter_parents'] = $filter_parents;
$filter_categories = [];
if ($active_rubrique_id) {
foreach ($all_cats as $cat) {
if ($cat->parent == $active_rubrique_id) {
$filter_categories[] = [
'id' => $cat->term_id,
'name' => thalim_cat_name($cat),
'slug' => $cat->slug,
'link' => $base_filter_params
? add_query_arg($base_filter_params, get_category_link($cat->term_id))
: get_category_link($cat->term_id),
];
}
}
}
// Add "Autres" entry if active rubrique has posts directly assigned to it
if ($active_rubrique_id && !empty($filter_categories)) {
$lang = thalim_current_language();
$direct_check = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 1,
'fields' => 'ids',
'no_found_rows' => true,
'lang' => '',
'tax_query' => [[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [$active_rubrique_id],
'include_children' => false,
]],
]);
if ($direct_check->have_posts()) {
$params = array_filter(array_merge($base_filter_params, ['filter_cat' => $active_rubrique_id, 'filter_autres' => 1]));
$filter_categories[] = [
'id' => 'autres',
'name' => $lang === 'en' ? 'Other' : 'Autres',
'slug' => 'autres',
'link' => add_query_arg($params, $page_url),
];
}
}
$context['filter_categories'] = $filter_categories;
$posts = Timber::get_posts($query_args);
$context['cards'] = thalim_get_cards_data($posts);
$context['posts'] = $posts;
Timber::render('page-annonces.twig', $context);

2
page-announcements.php Normal file
View File

@@ -0,0 +1,2 @@
<?php
require __DIR__ . '/page-annonces.php';

110
page-le-laboratoire.php Normal file
View File

@@ -0,0 +1,110 @@
<?php
$context = Timber::context();
$post = Timber::get_post();
$context['post'] = $post;
$page_id = $post->ID;
// ── Liens (internal page links) ──────────────────────────────
$lien_ids = get_post_meta( $page_id, 'liens', false );
$liens = [];
foreach ( $lien_ids as $lid ) {
$lid = intval( $lid );
if ( ! $lid ) continue;
$liens[] = [
'title' => get_the_title( $lid ),
'url' => get_permalink( $lid ),
];
}
$context['liens'] = $liens;
$labo_lang = thalim_current_language();
// ── Images (two side-by-side slots) ──────────────────────────
$images = [];
foreach ( [ 'image_labo_1', 'image_labo_2' ] as $field ) {
$img_id = intval( get_post_meta( $page_id, $field, true ) );
if ( ! $img_id ) continue;
$src = wp_get_attachment_image_url( $img_id, 'large' );
if ( ! $src ) continue;
$images[] = [
'url' => $src,
'alt' => get_post_meta( $img_id, '_wp_attachment_image_alt', true ) ?: '',
'title' => thalim_bilingual( get_the_title( $img_id ), $labo_lang ),
];
}
$context['images'] = $images;
// ── Axes thématiques grouped by period ───────────────────────
$terms = get_terms( [ 'taxonomy' => 'axe_thematique', 'hide_empty' => false ] );
$axes_map = [];
$label_prefix = $labo_lang === 'en' ? 'Research areas ' : 'Axes thématiques ';
$label_passes = $labo_lang === 'en' ? 'Past research areas' : 'Axes thématiques antérieurs';
foreach ( $terms as $term ) {
$debut = trim( get_term_meta( $term->term_id, 'annee_debut', true ) );
$fin = trim( get_term_meta( $term->term_id, 'annee_fin', true ) );
if ( $debut && $fin ) {
$key = $debut . '-' . $fin;
$label = $label_prefix . $debut . ' ' . $fin;
} else {
$key = 'passes';
$label = $label_passes;
}
if ( ! isset( $axes_map[ $key ] ) ) {
$axes_map[ $key ] = [
'label' => $label,
'debut' => intval( $debut ),
'terms' => [],
];
}
$ordre = trim( get_term_meta( $term->term_id, 'ordre_daffichage', true ) );
$axes_map[ $key ]['terms'][] = [
'name' => $term->name,
'url' => get_term_link( $term ),
'ordre' => $ordre !== '' ? intval( $ordre ) : null,
];
}
// Sort: newest first by annee_debut, 'passes' always last (debut === 0)
uasort( $axes_map, function ( $a, $b ) {
if ( $a['debut'] === 0 ) return 1;
if ( $b['debut'] === 0 ) return -1;
return $b['debut'] - $a['debut'];
} );
// Within each group: numbered items first (ascending), then unnumbered alphabetically
foreach ( $axes_map as &$group ) {
usort( $group['terms'], function ( $a, $b ) {
$a_has = $a['ordre'] !== null;
$b_has = $b['ordre'] !== null;
if ( $a_has && $b_has ) return $a['ordre'] - $b['ordre'];
if ( $a_has ) return -1;
if ( $b_has ) return 1;
return strcmp( $a['name'], $b['name'] );
} );
}
unset( $group );
$context['axes_groups'] = array_values( $axes_map );
// ── Body (English override) ──────────────────────────────────
$context['body_en'] = apply_filters( 'the_content', get_post_meta( $page_id, 'body_en', true ) ?: '' );
// ── WYSIWYG fields ────────────────────────────────────────────
$context['partenaires_internationaux'] = wpautop( ( $labo_lang === 'en' && get_post_meta( $page_id, 'partenaires_internationaux_en', true ) )
? get_post_meta( $page_id, 'partenaires_internationaux_en', true )
: ( get_post_meta( $page_id, 'partenaires_internationaux', true ) ?: '' ) );
$context['partenaires_nationaux'] = wpautop( ( $labo_lang === 'en' && get_post_meta( $page_id, 'partenaires_nationaux_en', true ) )
? get_post_meta( $page_id, 'partenaires_nationaux_en', true )
: ( get_post_meta( $page_id, 'partenaires_nationaux', true ) ?: '' ) );
$context['bibliotheques'] = wpautop( ( $labo_lang === 'en' && get_post_meta( $page_id, 'bibliotheques_en', true ) )
? get_post_meta( $page_id, 'bibliotheques_en', true )
: ( get_post_meta( $page_id, 'bibliotheques', true ) ?: '' ) );
// ── Edit link ─────────────────────────────────────────────────
$context['page_edit_link'] = current_user_can( 'edit_page', $page_id ) ? get_edit_post_link( $page_id ) : '';
Timber::render( 'page-le-laboratoire.twig', $context );

8
page-membres.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
require_once __DIR__ . '/inc/membres-helpers.php';
$context = Timber::context();
$context['groups'] = thalim_get_membres_groups();
$context['filter_roles'] = thalim_get_role_terms();
Timber::render( 'page-membres.twig', $context );

View File

@@ -0,0 +1,48 @@
<?php
$context = Timber::context();
$post = Timber::get_post();
$context['post'] = $post;
$terms = get_terms( [ 'taxonomy' => 'programme_de_recherche', 'hide_empty' => false ] );
$sections = [
'subventionne' => [ 'label' => 'Programmes subventionnés', 'items' => [] ],
'autre' => [ 'label' => 'Autres programmes', 'items' => [] ],
'ancien' => [ 'label' => 'Anciens programmes', 'items' => [] ],
];
foreach ( $terms as $term ) {
$type = get_term_meta( $term->term_id, 'type_de_programme', true );
$item = [
'name' => $term->name,
'description' => wpautop( $term->description ),
'url' => get_term_link( $term ),
'annee_fin' => (int) get_term_meta( $term->term_id, 'annee_fin', true ),
];
if ( $type === 'Programme subventionné' ) {
$sections['subventionne']['items'][] = $item;
} elseif ( $type === 'Ancien programme' ) {
$sections['ancien']['items'][] = $item;
} else {
// "Autre programme" or no type set
$sections['autre']['items'][] = $item;
}
}
// Sort by annee_fin descending (most recent end year first); items without a year go last
foreach ( $sections as &$section ) {
usort( $section['items'], function( $a, $b ) {
if ( $a['annee_fin'] === $b['annee_fin'] ) return strcmp( $a['name'], $b['name'] );
if ( ! $a['annee_fin'] ) return 1;
if ( ! $b['annee_fin'] ) return -1;
return $b['annee_fin'] - $a['annee_fin'];
} );
}
unset( $section );
$context['sections'] = array_values( $sections );
$context['page_edit_link'] = current_user_can( 'edit_page', $post->ID ) ? get_edit_post_link( $post->ID ) : '';
Timber::render( 'page-programmes-de-recherche.twig', $context );

8
page.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
$context = Timber::context();
$post = Timber::get_post();
$context['post'] = $post;
$context['page_edit_link'] = current_user_can( 'edit_page', $post->ID ) ? get_edit_post_link( $post->ID ) : '';
Timber::render( 'page.twig', $context );

164
scss/_author.scss Normal file
View File

@@ -0,0 +1,164 @@
// ====================================
// AUTHOR PROFILE PAGE
// ====================================
.author-header {
display: flex;
gap: 2rem;
align-items: flex-start;
margin: 2rem 0;
}
.author-avatar {
flex-shrink: 0;
img {
width: 140px;
height: 140px;
object-fit: cover;
@media ($tablet) {
width: 180px;
height: 180px;
}
}
}
.author-identity {
h2 {
margin-top: 0;
}
& + .author-bio {
margin-top: 2rem;
}
}
.author-role {
font-family: $font-primary;
margin-top: 0.4rem;
opacity: 0.85;
line-height: 1.4;
}
.author-bio {
margin-bottom: 1.5rem;
line-height: 1.6;
> p {
margin: 0.8rem 0;
}
hr {
display: none;
}
h3 {
font-family: $font-primary;
text-transform: uppercase;
}
}
.author-resume-these {
margin-top: 1rem;
line-height: 1.6;
> p {
margin: 0.8rem 0;
}
}
.author-cat-footer {
display: flex;
justify-content: center;
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid $light-gray;
}
.author-titre-these {
font-family: $font-heading;
font-size: 1.4rem;
line-height: 1.3 !important;
margin-bottom: 0.8rem;
}
.these-inline-title {
text-transform: uppercase;
}
// WYSIWYG article-fields (domaines de recherches, recherches en cours):
// the wpautop'd <p> follows the inline title — give it a small breathing space.
.domaines-autres,
.recherches-en-cours {
> p:first-of-type {
margin-top: 0.4rem;
}
}
// ── Author posts dropdowns ────────────────────────────────────
.author-posts-section {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 5rem;
}
.author-posts-header {
display: flex;
align-items: center;
gap: 1.2rem;
padding: 0.8rem;
background-color: $light-gray;
cursor: pointer;
transition: background-color 0.15s;
font-family: $font-primary;
text-transform: uppercase;
&:hover {
background-color: $less-light-gray;
}
}
.author-posts-chevron {
font-size: 1.2rem;
transition: transform 0.2s;
flex-shrink: 0;
margin-left: auto;
.author-posts-item.is-open & {
transform: rotate(180deg);
}
}
.author-posts-content {
padding: 1rem;
background-color: $light-light-gray;
position: relative;
padding-bottom: 3rem;
&::after {
@include yellow-gradient-after;
}
article a {
text-decoration: none !important;
h2 {
margin-top: 0 !important;
}
}
}
.author-post-grid {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
width: 100%;
h2::after {
display: none;
}
@media ($tablet) {
grid-template-columns: repeat(2, 1fr);
}
}

3
scss/_base.scss Normal file
View File

@@ -0,0 +1,3 @@
#wpadminbar {
position: fixed !important;
}

557
scss/_category.scss Normal file
View File

@@ -0,0 +1,557 @@
// Category archive pages
.category-archive {
margin-top: 0;
.category-header-top {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: start;
.breadcrumb {
font-size: 0.85rem;
margin-bottom: 1.5rem;
text-transform: uppercase;
line-height: 1.3;
&__separator {
margin: 0 0.4rem;
}
}
@media ($tablet) {
flex-direction: row;
}
}
h1 {
font-family: Gelasio;
font-weight: normal;
font-size: 1.8rem;
position: relative;
margin-top: 2rem;
display: inline-block;
&::after {
content: '';
display: block;
position: absolute;
height: 6px;
width: 100%;
bottom: -1.2rem;
left: 0;
z-index: 2;
}
@media ($tablet) {
font-size: 2.6rem;
&::after {
bottom: -0.4rem;
}
}
}
.category--le-laboratoire h1::after {
background: linear-gradient(to bottom, transparent 0%, $laboratoire 50%);
}
.category--manifestations-scientifiques h1::after {
background: linear-gradient(to bottom, transparent 0%, $manifestations 50%);
}
.category--publications-et-productions h1::after {
background: linear-gradient(to bottom, transparent 0%, $publications 50%);
}
.category--mediation-scientifique h1::after {
background: linear-gradient(to bottom, transparent 0%, $mediations 50%);
}
.category--ressources h1::after {
background: linear-gradient(to bottom, transparent 0%, $ressources 50%);
}
}
// Breadcrumb: non-category links (Accueil, etc.) underlined in muted gray
.breadcrumb a:not(.breadcrumb__cat) { text-decoration: underline; text-decoration-color: $less-light-gray; text-underline-offset: 3px; }
// Breadcrumb category color underlines — links and current page indicator
.category--le-laboratoire .breadcrumb__cat,
.category--le-laboratoire .breadcrumb__current { text-decoration: underline; text-decoration-color: $laboratoire; text-underline-offset: 3px; }
.category--manifestations-scientifiques .breadcrumb__cat,
.category--manifestations-scientifiques .breadcrumb__current { text-decoration: underline; text-decoration-color: $manifestations; text-underline-offset: 3px; }
.category--publications-et-productions .breadcrumb__cat,
.category--publications-et-productions .breadcrumb__current { text-decoration: underline; text-decoration-color: $publications; text-underline-offset: 3px; }
.category--mediation-scientifique .breadcrumb__cat,
.category--mediation-scientifique .breadcrumb__current { text-decoration: underline; text-decoration-color: $mediations; text-underline-offset: 3px; }
.category--ressources .breadcrumb__cat,
.category--ressources .breadcrumb__current { text-decoration: underline; text-decoration-color: $ressources; text-underline-offset: 3px; }
.category-archive {
.taxonomy-description {
font-family: $font-primary;
margin-top: 2rem;
line-height: 1.6;
max-width: 70ch;
a {
text-decoration: underline;
}
p {
margin-bottom: 1rem;
line-height: 1.6;
strong {
font-weight: bold;
}
em {
font-style: italic;
}
&:first-child {
margin-top: 0 !important;
}
}
ul, ol {
line-height: 1.6;
padding-left: 0.8rem;
}
ul {
list-style: inside "· ";
}
ol {
list-style: inside decimal;
}
blockquote{
padding-left: 1rem;
margin-left: 1.5rem;
border-left: solid 1px $light-gray;
}
}
.subcategory-section {
margin-top: 5rem;
&:first-of-type {
margin-top: 2rem;
}
.subcategory-section__title {
font-family: NewsCycle;
text-transform: uppercase;
position: relative;
display: inline-block;
margin-bottom: 2rem;
font-size: 1.2rem;
&::after {
@include yellow-gradient-after(10px);
bottom: -10px;
}
}
.post-grid {
display: grid;
align-items: start;
grid-template-columns: 1fr;
gap: 2rem;
width: 100%;
.post-card {
min-width: 0;
}
@media ($tablet) {
grid-template-columns: repeat(2, 1fr);
}
@media ($desktop) {
grid-template-columns: repeat(3, 1fr);
}
}
.category-section-footer {
margin-top: 3rem;
display: flex;
justify-content: center;
}
}
}
/*
.category-header {
width: 100%;
margin-bottom: 3vh;
&__back {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.9rem;
text-decoration: none;
margin-bottom: 1rem;
i {
font-size: 1.1rem;
}
}
&__title {
font-size: 2rem;
margin-bottom: 0.5rem;
@media ($tablet) {
font-size: 2.5rem;
}
}
&__description {
color: $less-dark-gray;
}
}
// Sub-category sections on parent category page
.subcategory-section {
width: 100%;
margin-bottom: 4vh;
&__header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 2vh;
border-bottom: 2px solid $publications;
padding-bottom: 0.5rem;
}
&__title {
font-size: 1.5rem;
a {
text-decoration: none;
}
}
&__see-all {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.9rem;
text-decoration: none;
white-space: nowrap;
}
&__empty {
color: $less-less-light-gray;
font-style: italic;
}
}
// Post grid
.post-grid {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
width: 100%;
@media ($tablet) {
grid-template-columns: repeat(2, 1fr);
}
@media ($desktop) {
grid-template-columns: repeat(3, 1fr);
}
}
// Post card
.post-card {
background-color: white;
display: flex;
flex-direction: column;
overflow: hidden;
&__image {
width: 100%;
aspect-ratio: 4 / 3;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
&__content {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
&__title {
font-size: 1.1rem;
line-height: 1.3;
a {
text-decoration: none;
}
}
&__subtitle {
font-size: 0.9rem;
color: $less-dark-gray;
font-style: italic;
}
&__date {
font-size: 0.8rem;
color: $less-less-light-gray;
}
&__authors {
font-size: 0.9rem;
display: flex;
flex-wrap: wrap;
gap: 0.2rem;
.post-card__author {
&:not(:last-child)::after {
content: ',';
}
}
&--external {
color: $less-dark-gray;
}
}
&__role,
&__publisher,
&__journal {
font-size: 0.85rem;
color: $less-dark-gray;
}
&__axes,
&__tags {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
}
&__axe,
&__tag {
font-size: 0.75rem;
background-color: $light-gray;
padding: 0.15rem 0.5rem;
}
&__axe {
border-left: 2px solid $publications;
}
&__links {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.3rem;
}
&__link {
font-size: 0.8rem;
&--hal {
color: $publications;
}
}
}
// Infinite scroll spinner
.scroll-spinner {
display: flex;
justify-content: center;
align-items: center;
gap: 0.4rem;
padding: 2rem 0;
&__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: $publications;
animation: scroll-spinner-bounce 1.2s infinite ease-in-out;
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
@keyframes scroll-spinner-bounce {
0%, 80%, 100% {
transform: scale(0.6);
opacity: 0.4;
}
40% {
transform: scale(1);
opacity: 1;
}
}
*/
// ── Agenda slider view ────────────────────────────────────────────────────────
.agenda-view-container {
display: none;
margin-top: 3rem;
&.is-active { display: block; }
}
.agenda-view-title {
font-family: NewsCycle;
text-transform: uppercase;
position: relative;
display: inline-block;
margin-bottom: 2rem;
font-size: 1.2rem;
&::after {
@include yellow-gradient-after(10px);
bottom: -10px;
}
}
.agenda-swiper-wrap {
display: flex;
align-items: center;
gap: 0.8rem;
margin-top: 2rem;
}
.agenda-swiper {
flex: 1;
overflow: hidden;
}
.agenda-swiper-prev,
.agenda-swiper-next {
flex-shrink: 0;
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
padding: 0.4rem;
color: $dark-gray;
line-height: 1;
&:hover { color: black; }
&.swiper-button-disabled {
opacity: 0.25;
pointer-events: none;
}
}
.agenda-card {
display: flex;
flex-direction: column;
text-decoration: none;
color: inherit;
padding-bottom: 1.5rem;
border-bottom: 1px solid $light-gray;
height: 100%;
position: relative;
transition: transform 0.2s ease-out;
margin-right: 0 !important;
// Timeline line running through the center of the date boxes
&::before {
content: '';
position: absolute;
top: 1.7rem;
left: 0;
right: 0;
height: 1px;
background-color: $less-light-gray;
z-index: 0;
width: calc(100% + 20px);
@media ($tablet) {
width: calc(100% + 24px);
}
@media ($desktop) {
width: calc(100% + 32px);
}
}
&:hover { transform: scale(0.98); }
&__dates {
display: flex;
align-items: flex-start;
gap: 0.5rem;
margin-bottom: 1rem;
}
&__body { flex: 1; }
&__meta {
display: flex;
flex-wrap: wrap;
gap: 0.3rem 0.8rem;
font-family: $font-primary;
font-size: 0.75rem;
text-transform: uppercase;
opacity: 0.65;
margin-bottom: 0.5rem;
}
&__title {
font-family: $font-heading;
font-size: 1rem;
line-height: 1.35;
}
}
// Category-specific accent color on agenda card border + date box gradient
.category--manifestations-scientifiques .agenda-card { border-bottom-color: $manifestations; }
.category--le-laboratoire .agenda-card { border-bottom-color: $laboratoire; }
.category--publications-et-productions .agenda-card { border-bottom-color: $publications; }
.category--mediation-scientifique .agenda-card { border-bottom-color: $mediations; }
.category--ressources .agenda-card { border-bottom-color: $ressources; }
.category--manifestations-scientifiques .agenda-date-box { @include category-gradient($manifestations); }
.category--le-laboratoire .agenda-date-box { @include category-gradient($laboratoire); }
.category--publications-et-productions .agenda-date-box { @include category-gradient($publications); }
.category--mediation-scientifique .agenda-date-box { @include category-gradient($mediations); }
.category--ressources .agenda-date-box { @include category-gradient($ressources); }
.agenda-date-box {
position: relative;
z-index: 1;
background-color: $light-gray;
font-family: $font-primary;
text-transform: uppercase;
text-align: center;
padding: 0.4rem 0.55rem;
flex-shrink: 0;
min-width: 2.6rem;
.agenda-date-day { display: block; font-size: 1.3rem; line-height: 1; }
.agenda-date-month { display: block; font-size: 0.65rem; margin-top: 2px; }
.agenda-date-year { display: block; font-size: 0.6rem; opacity: 0.65; }
sup {
font-size: 0.7rem;
}
}
.agenda-date-arrow {
font-size: 0.85rem;
opacity: 0.5;
}

344
scss/_filters.scss Normal file
View File

@@ -0,0 +1,344 @@
.filters-bar {
margin-top: 2.5rem;
display: flex;
align-items: center;
gap: 0.8rem;
flex-wrap: wrap;
position: relative;
padding-bottom: 0.6rem;
&::after {
content: '';
position: absolute;
bottom: 0;
left: -5vw;
width: calc(100% + 10vw);
height: 1px;
background-color: $light-gray;
}
@media ($tablet) {
&::after {
left: -3vw;
width: calc(100% + 6vw);
}
}
}
.filters-toggle-btn {
display: flex;
align-items: center;
gap: 0.5rem;
font-family: $font-primary;
font-size: 0.9rem;
text-transform: uppercase;
background-color: $light-gray;
border: unset;
cursor: pointer;
padding: 0.4rem 0.8rem;
color: $dark-gray;
flex-shrink: 0;
.filters-chevron {
transition: transform 0.2s;
}
&.is-open .filters-chevron {
transform: rotate(180deg);
}
&:hover {
background-color: $less-light-gray;
}
}
.filters-active-chips {
display: flex;
align-items: center;
gap: 0.4rem;
flex-wrap: wrap;
}
.filter-chip {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-family: $font-primary;
font-size: 0.8rem;
text-transform: uppercase;
background-color: $light-gray;
padding: 0.3rem 0.5rem;
text-decoration: none;
color: $dark-gray;
border: none;
cursor: pointer;
i {
font-size: 0.7rem;
opacity: 0.5;
}
&:hover {
background-color: $less-light-gray;
i { opacity: 1; }
}
}
.category-filters {
font-size: 0.9rem;
font-family: $font-primary;
margin-top: 0;
display: none;
padding: 2rem 5vw;
padding-bottom: 0.5rem !important;
margin-left: -5vw;
width: calc(100% + 10vw);
justify-content: space-between;
background-color: $light-light-gray;
border-top: solid 1px $light-gray;
border-bottom: solid 1px $light-gray;
flex-direction: column;
&.is-open {
display: flex;
}
> div {
margin-top: 1.2rem;
margin-bottom: 1.2rem;
flex: auto;
&.filtre-rubrique {
flex: 2;
}
&.filtre-categorie {
flex: 1;
}
&.filtre-date {
flex: 1;
}
&.filtre-axe {
flex: 1;
}
&:first-of-type {
margin-top: 0;
padding-left: 0;
}
&:last-of-type {
margin-bottom: 0;
}
&:not(:last-of-type) {
padding-bottom: 1.8rem;
border-bottom: solid 1px $less-light-gray;
}
.filter-section-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 2rem;
}
.section-title {
text-transform: uppercase;
position: relative;
display: inline-block;
&::after {
@include yellow-gradient-after(10px);
bottom: -10px;
}
}
ul {
display: flex;
justify-content: start;
flex-wrap: wrap;
align-items: start;
gap: 0.5rem 0.2rem;
list-style: none;
margin: 0;
padding: 0;
li {
font-size: 1rem;
cursor: pointer;
padding: 0.4rem 0.6rem;
a {
text-decoration: none;
}
&:hover,
&.is-active {
background-color: $less-light-gray;
}
}
}
}
.filter-dd {
position: relative;
margin-top: 0;
display: inline-block;
.dd-title {
padding: 0.3rem 0.5rem;
display: flex;
align-items: center;
gap: 0.4rem;
cursor: pointer;
> p,
> i {
font-size: 0.9rem;
}
> i {
transition: transform 0.2s;
}
}
&.is-open .dd-title > i {
transform: rotate(180deg);
}
&.is-active .dd-title {
background-color: $less-light-gray;
}
.dd-content {
position: absolute;
padding: 0;
z-index: 5;
background: white;
border: 1px solid $less-light-gray;
min-width: 100%;
ul {
gap: 0.3rem;
padding: 0.5rem;
li {
padding: 0.4rem;
white-space: nowrap;
background-color: $light-gray;
}
}
&#filter-axe-popover {
right: auto;
left: 0;
min-width: 280px;
max-width: min(420px, 90vw);
ul {
flex-direction: column;
padding: 0;
gap: 0;
li {
width: 100%;
margin: 0;
white-space: normal;
&:nth-of-type(odd) {
background-color: white;
}
&:first-of-type {
background-color: $less-light-gray;
}
&.dd-axe-group-label {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: $less-dark-gray;
background-color: $light-gray !important;
padding: 0.5rem 0.4rem;
cursor: default;
pointer-events: none;
border-top: solid 1px $less-light-gray;
&:first-child {
margin-top: 0;
}
}
}
}
}
}
.dd-date-fields {
padding: 0.6rem;
padding-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
border-top: 1px solid $less-light-gray;
label {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.85rem;
text-transform: uppercase;
}
input[type="date"] {
padding: 0.2rem 0.4rem;
border: 1px solid $less-light-gray;
font-size: 0.85rem;
font-family: $font-primary;
}
}
.dd-date-apply {
margin-top: 0.6rem;
width: 100%;
padding: 0.3rem;
border: none;
font-size: 0.85rem;
font-family: $font-primary;
text-transform: uppercase;
cursor: pointer;
}
}
.membres-search-input.is-active {
background-color: $less-light-gray;
}
.date-reset-link {
display: block;
font-size: 0.75rem;
text-transform: uppercase;
text-decoration: none;
color: inherit;
opacity: 0.6;
padding: 0 0.5rem 0.4rem;
&:hover {
opacity: 1;
}
}
@media ($tablet) {
padding: 2rem 3vw;
margin-left: -3vw;
width: calc(100% + 6vw);
flex-direction: row;
> div {
margin-top: 0;
margin-bottom: 0;
padding-left: 1.5rem;
&:not(:last-of-type) {
padding-right: 0.8rem;
border-bottom: unset;
border-right: solid 1px $less-light-gray;
}
}
}
}

55
scss/_footer.scss Normal file
View File

@@ -0,0 +1,55 @@
footer {
background-color: white;
width: 100%;
padding: 1rem 2rem;
margin-top: auto;
}
.footer-content {
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
gap: 2rem;
@media ($tablet) {
gap: unset;
flex-direction: row;
}
}
.footer-nav {
ul#menu-footer,
ul#menu-footer-en {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.5rem 2rem;
@media ($tablet) {
justify-content: start;
}
li {
a {
text-transform: uppercase;
font-size: 0.75rem;
}
}
}
}
.footer-logos {
display: inline-flex;
height: 3rem;
gap: 1rem;
padding: 0.5rem;
align-items: start;
> a {
height: 100%;
> img {
height: 100%;
}
}
}

330
scss/_header.scss Normal file
View File

@@ -0,0 +1,330 @@
body >
header {
display: flex;
flex-direction: column-reverse;
justify-content: space-between;
transition: height 0.3s ease-out;
background-color: white;
width: 100%;
position: relative;
z-index: 5;
@media ($tablet) {
position: fixed;
height: 12vh;
min-height: 100px;
max-height: 130px;
flex-direction: row;
}
}
.header-left {
display: flex;
height: 6rem;
margin-top: 3rem;
@media ($tablet) {
margin-top: unset;
height: 100%;
}
}
.main-logo-container {
display: inline-block;
background-color: $light_gray;
height: 100%;
@media ($tablet) {
flex-direction: row;
}
}
.main-logo {
display: inline-block;
padding: 0.3rem 0.6rem;
height: 100%;
@include hover-gradient-background;
@media ($tablet) {
padding: 1.5rem 2rem;
}
> img,
> a > img {
height: 100%;
transform: scale(1);
transition: transform 0.2s ease-out;
@media ($tablet) {
}
}
&:hover > img,
&:hover > a > img {
transform: scale(1.05);
}
}
.description {
display: flex;
flex-direction: column;
align-items: start;
justify-content: center;
gap: 0.3rem;
margin-left: 1.2rem;
opacity: 1;
transition: opacity 0.2s ease-out;
> div:first-of-type {
text-transform: uppercase;
font-size: 0.8rem;
background-color: $light_gray;
padding: 0.2rem;
}
> div:last-of-type {
font-size: 0.8rem;
> sup {
font-size: 0.6rem;
vertical-align: super;
}
}
}
.header-right {
display: flex;
align-items: center;
gap: 2rem;
justify-content: space-between;
position: fixed;
width: 100%;
background-color: white;
height: 3rem;
@media ($tablet) {
padding-left: unset;
height: unset;
position: relative;
width: unset;
}
&.scrolled {
.secondary-logo-container {
max-width: 40vw;
.main-logo:hover {
background-position: unset;
img {
transform: unset;
}
}
}
}
}
.secondary-logo-container {
height: 100%;
max-width: 0;
overflow: hidden;
transition: max-width 0.6s ease-out;
@media ($tablet) {
display: none;
}
}
.lang-switch {
text-transform: uppercase;
font-size: 0.8rem;
transition: font-size 0.2s ease-out;
> ul {
display: flex;
gap: 1rem;
> li:not(.active) {
> a {
text-decoration: none;
}
}
}
}
.search-button {
margin-left: auto;
> div {
background-color: $light_gray;
padding: 0.5rem;
border-radius: 5rem;
min-width: 2rem;
aspect-ratio: 1 / 1;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.3s ease-out;
cursor: pointer;
&:hover {
background-color: $less_light-gray;
}
}
}
.menu-toggle {
display: inline-block;
background-color: $light_gray;
width: fit-content;
height: 100%;
> div {
height: 100%;
display: flex;
padding: 0.5rem;
flex-direction: column;
align-items: center;
justify-content: space-between;
gap: 0.3rem;
@include hover-gradient-background;
@media ($tablet) {
gap: unset;
padding: 2rem;
}
> div {
font-size: 1rem;
transform: scale(1);
transition: transform 0.2s ease-out, font-size 0.2s ease-out;
@media ($tablet) {
font-size: 2rem;
}
}
> p {
font-family: NewsCycle;
text-transform: uppercase;
transform: scale(1);
font-size: 0.8rem;
transition: transform 0.2s ease-out, font-size 0.2s ease-out;
@media ($tablet) {
font-size: 1rem;
}
}
}
&:hover > div > div,
&:hover > div > p {
transform: scale(0.9);
}
}
.search-panel {
position: absolute;
right: 0;
width: 100%;
background-color: white;
z-index: 4;
border-top: 2px solid $light-gray;
transition: top 0.4s ease-out, opacity 0.2s ease-out;
opacity: 0;
pointer-events: none;
@media ($tablet) {
position: fixed;
width: 33.333%;
}
&.active {
opacity: 1;
pointer-events: all;
}
&__inner {
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
&__title {
font-family: $font-primary;
font-size: 1.1rem;
text-transform: uppercase;
display: inline-block;
align-self: start;
position: relative;
line-height: 1.6;
&::after {
@include yellow-gradient-after;
bottom: -10px;
}
}
&__desc {
font-family: $font-primary;
color: $less-dark-gray;
}
&__input-wrap {
position: relative;
}
&__icon-btn {
position: absolute;
right: 0.8rem;
top: 50%;
transform: translateY(-50%);
color: $less-dark-gray;
background: none;
border: none;
padding: 0;
cursor: pointer;
line-height: 1;
&:hover {
color: $dark-gray;
}
}
&__input {
width: 100%;
border: none;
padding: 0.6rem 2.5rem 0.6rem 0.8rem;
font-family: $font-primary;
font-size: 0.85rem;
outline: none;
background-color: $light-gray;
&::placeholder {
color: $less-dark-gray;
text-transform: uppercase;
}
&:focus {
border-color: $less-light-gray;
}
}
&__submit {
display: block;
margin-left: auto;
background-color: $light-gray;
padding: 0.5rem 1rem;
font-family: $font-primary;
font-size: 0.85rem;
text-transform: uppercase;
cursor: pointer;
margin-top: 1rem;
border: none;
&:hover {
background-color: $less-light-gray;
}
}
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: $less_light-gray;
opacity: 0;
z-index: 3;
pointer-events: none;
transition: opacity 0.3s ease-out;
&.active {
opacity: 0.6;
pointer-events: all;
}
}

562
scss/_index.scss Normal file
View File

@@ -0,0 +1,562 @@
.hero-header {
display: flex;
flex-direction: column-reverse;
position: relative;
@media ($tablet) {
flex-direction: row;
}
}
.hero-logos {
position: absolute;
top: 2.5vh;
display: inline-flex;
height: 3rem;
gap: 1rem;
padding: 0.5rem;
align-items: start;
background-color: $light_gray;
@media ($tablet) {
position: unset;
top: unset;
}
> a {
height: 100%;
> img {
height: 100%;
}
&:nth-of-type(2) {
mix-blend-mode: darken;
}
}
}
.color-changer {
transition: color 0.3s ease-out;
}
.hero-presentation {
font-family: $font-heading;
font-size: 1.6rem;
line-height: 1.1;
@media ($tablet) {
margin-top: 2rem;
font-size: 2.6rem;
}
}
.hero-presentation-detail {
margin-top: 1.5rem;
width: 90%;
}
.hero-content > .link-button {
margin-top: 2.5rem;
}
.hero-content {
@media ($tablet) {
width: 75%;
}
}
// ====================================
// SKETCH
// ====================================
#sketch {
z-index: 0;
position: relative;
display: block;
height: 200px;
// background-color: white;
@media ($tablet) {
height: unset;
width: 25%;
margin-top: 0 !important;
}
}
// Floating shapes (DOM-based implementation)
.floating-shape {
position: absolute;
top: 0;
left: 0;
transform-origin: top left;
will-change: transform;
pointer-events: none;
svg {
overflow: visible;
}
path, polyline, polygon, line, circle, ellipse, rect {
transition: fill-opacity 0.5s ease-in-out;
}
}
.thalim-text {
position: absolute;
pointer-events: none;
transition: opacity 0.3s ease-out;
display: flex;
gap: 0;
z-index: 20;
font-family: 'NewsCycle', sans-serif;
font-size: 26px;
@media ($tablet) {
font-size: 48px;
}
}
// ====================================
// END SKETCH
// ====================================
// ====================================
// SWIPER SECTIONS (annonces, publications…)
// ====================================
.swiper-section {
margin-top: 3rem;
position: relative;
.section-title {
font-family: NewsCycle;
text-transform: uppercase;
position: relative;
display: inline-block;
margin-bottom: 2rem;
&::after {
@include yellow-gradient-after(10px);
bottom: -10px;
}
}
.swiper_content_controls {
display: flex;
gap: 0.8rem;
align-items: center;
margin-top: 2rem;
.swiper {
overflow: hidden;
flex: 1;
}
.swiper-button-prev,
.swiper-button-next {
position: static;
width: 2.4rem;
height: 2.4rem;
aspect-ratio: 1;
margin: 0;
background-color: $light-gray;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: $dark-gray;
transition: background-color 0.2s ease-out;
cursor: pointer;
flex-shrink: 0;
&::after {
display: none; // hide Swiper default arrow
}
&:hover {
background-color: $less-light-gray;
}
i {
font-size: 1.1rem;
line-height: 1;
}
}
}
.button-annonces {
display: flex;
justify-content: center;
margin-top: 1.2rem;
}
}
// ====================================
// END SWIPER SECTIONS
// ====================================
// ====================================
// MESSAGE DU LABORATOIRE + AGENDA
// ====================================
.message-agenda-section {
display: flex;
flex-direction: column;
width: 100%;
gap: 2rem;
margin-top: 3rem;
@media ($tablet) {
flex-direction: row;
}
}
.message-du-labo,
.agenda {
position: relative;
padding: 2.5vh 5vw;
padding-bottom: 6vh;
background-color: white;
display: flex;
flex-direction: column;
align-items: flex-start;
&::after {
@include yellow-gradient-after;
}
.section-title {
font-family: $font-primary;
text-transform: uppercase;
position: relative;
display: inline-block;
margin-bottom: 3rem;
&::after {
@include yellow-gradient-after(10px);
bottom: -10px;
}
}
@media ($tablet) {
padding: 3vh 3vw;
padding-bottom: 8vh;
}
}
.message-du-labo {
@media ($tablet) {
flex: 4;
}
}
.messages-list {
width: 100%;
@media ($tablet) {
flex: 1;
overflow: hidden;
}
}
.agenda {
@media ($tablet) {
flex: 3;
// border-left: 1px solid $light-gray;
}
}
.message-date {
font-size: 0.75rem;
color: $less-dark-gray;
display: block;
margin-bottom: 0.3rem;
}
.message-item {
& + .message-item {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid $light-gray;
}
}
.message-content {
margin-bottom: 1.5rem;
padding-right: 2rem;
position: relative;
> p {
margin: 0.7rem 0;
}
p {
margin-bottom: 1rem;
line-height: 1.6;
strong {
font-weight: bold;
}
em {
font-style: italic;
}
}
ul, ol {
line-height: 1.6;
padding-left: 0.8rem;
}
ul {
list-style: inside "· ";
}
ol {
list-style: inside decimal;
}
blockquote{
padding-left: 1rem;
margin-left: 1.5rem;
border-left: solid 1px $light-gray;
}
@media ($tablet) {
overflow: hidden;
&.is-overflowing::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 8rem;
background: linear-gradient(to bottom, transparent 0%, white 70%);
pointer-events: none;
}
}
}
.button-messages,
.button-agenda {
align-self: center;
margin-top: auto;
}
.message-read-more {
display: none;
position: absolute;
bottom: 1rem;
left: 0;
z-index: 1;
font-size: 0.85rem;
text-decoration: none;
font-family: $font-primary;
text-transform: uppercase;
.is-overflowing & {
display: inline-block;
}
}
.agenda-content {
width: 100%;
.agenda-item {
display: flex;
align-items: center;
gap: 1.5rem;
transform: scale(1);
transition: transform 0.2s ease-out;
margin-bottom: 2rem;
text-decoration: none;
color: inherit;
&:hover {
transform: scale(0.97);
}
}
.date-container {
background-color: $light-gray;
text-transform: uppercase;
font-family: $font-primary;
text-align: center;
padding: 0.5rem;
position: relative;
flex-shrink: 0;
min-width: 3rem;
> p {
position: relative;
z-index: 1;
&:first-of-type {
font-size: 1.3rem;
}
&:last-of-type {
margin-bottom: 3px;
}
}
&::after {
content: '';
display: block;
position: absolute;
height: 10px;
width: 100%;
bottom: 0;
left: 0;
background: linear-gradient(to bottom, $light-gray 0%, $manifestations 100%);
z-index: 0;
}
}
.event-content {
padding-bottom: 1rem;
border-bottom: 1px solid $manifestations;
flex: 1;
.meta {
font-family: $font-primary;
display: flex;
gap: 1rem;
padding-bottom: 0.5rem;
text-transform: uppercase;
font-size: 0.8rem;
flex-wrap: wrap;
opacity: 0.7;
}
.event-title {
font-family: $font-heading;
font-size: 1.1rem;
}
}
}
// ====================================
// END MESSAGE DU LABORATOIRE + AGENDA
// ====================================
// ====================================
// NUAGE DE MOTS-CLÉS
// ====================================
.keyword-cloud {
margin-top: 4rem;
margin-bottom: 3rem;
width: 100%;
@media ($tablet) {
margin-bottom: 0;
}
.section-title {
font-family: $font-primary;
text-transform: uppercase;
position: relative;
display: inline-block;
margin-bottom: 2rem;
&::after {
@include yellow-gradient-after(10px);
bottom: -10px;
}
}
}
#keyword-container {
position: relative;
width: 100%;
min-height: 120px;
}
.keyword {
position: absolute;
font-family: $font-primary;
font-size: 0.7rem;
text-transform: uppercase;
white-space: nowrap;
text-decoration: none;
color: $dark-gray;
opacity: 0;
cursor: pointer;
@media ($tablet) {
font-size: 0.95rem;
}
&.keyword--visible {
animation: keywordFadeIn 0.7s ease-out forwards;
}
}
@keyframes keywordFadeIn {
from {
opacity: 0;
transform: scale(0.92);
}
to {
opacity: 1;
transform: scale(1);
}
}
// ====================================
// END NUAGE DE MOTS-CLÉS
// ====================================
// Quick links widget
.quick-links {
position: fixed;
display: block;
background-color: $light-light-gray;
right: 0;
top: 35vh;
z-index: 3;
font-family: $font-primary;
max-width: 2.2rem;
overflow: hidden;
transition: max-width 0.9s ease-out, top 0.2s ease;
text-decoration: none;
z-index: 10;
@media ($tablet) {
right: 2vw;
}
@media ($desktop) {
right: 4vw;
}
> ul {
display: flex;
flex-direction: column;
align-items: end;
gap: 0.8rem;
padding: 0.8rem 0.6rem;
> li > a {
display: flex;
align-items: center;
gap: 0.8rem;
text-decoration: none;
white-space: nowrap;
&:hover {
font-weight: bold;
}
}
}
&:hover {
max-width: 40vw;
}
&::after {
content: "";
display: block;
position: absolute;
height: 10px;
width: 100%;
bottom: 0;
left: 0;
background: linear-gradient(to bottom, transparent 0%, $yellow 100%);
z-index: 2;
}
}

44
scss/_layout.scss Normal file
View File

@@ -0,0 +1,44 @@
body {
display: flex;
flex-direction: column;
height: 100vh;
background-color: $light_gray;
}
main {
width: 100vw;
display: flex;
flex-direction: column;
align-items: center;
@media ($tablet) {
margin-top: 12vh;
}
}
.container {
display: flex;
align-items: center;
justify-content: start;
flex-direction: column;
position: relative;
z-index: 1;
padding: 4vh 5vw;
max-width: 1640px;
width: 100vw;
}
.full-block {
width: 100%;
background-color: white;
padding: 2.5vh 5vw;
position: relative;
padding-bottom: 6vh;
@media ($tablet) {
padding: 3vh 3vw;
padding-bottom: 8vh;
}
&::after {
@include yellow-gradient-after;
}
}

211
scss/_membres.scss Normal file
View File

@@ -0,0 +1,211 @@
.membres-section {
margin-top: 4rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.membres-item.is-open {
position: relative;
padding-bottom: 1rem;
&::after {
@include yellow-gradient-after;
}
}
.membres-header {
display: flex;
align-items: center;
gap: 1.2rem;
padding: 0.8rem;
background-color: $light-gray;
cursor: pointer;
transition: background-color 0.15s;
font-family: $font-primary;
text-transform: uppercase;
&:hover {
background-color: $less-light-gray;
}
}
.membres-chevron {
font-size: 1.2rem;
transition: transform 0.2s;
flex-shrink: 0;
margin-left: auto;
.membres-item.is-open & {
transform: rotate(180deg);
}
}
.membres-sort-chevron {
font-size: 0.8rem;
opacity: 0.3;
transition: transform 0.2s, opacity 0.15s;
vertical-align: middle;
margin-left: 0.3rem;
}
.membres-content {
padding: 1rem;
background-color: $light-light-gray;
font-family: $font-primary;
font-size: 0.9rem;
line-height: 1.6;
}
.membres-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
font-size: 0.9rem;
th {
width: 33.333%;
text-align: left;
text-transform: uppercase;
font-family: $font-primary;
font-weight: normal;
font-size: 0.8rem;
padding: 0.5rem 1.5rem 0.5rem 0.5rem;
background-color: $light-gray;
cursor: pointer;
user-select: none;
white-space: nowrap;
&:hover .membres-sort-chevron {
opacity: 0.7;
}
&.sort-asc .membres-sort-chevron {
opacity: 1;
transform: rotate(180deg);
}
&.sort-desc .membres-sort-chevron {
opacity: 1;
transform: rotate(0deg);
}
}
tbody tr {
cursor: pointer;
transition: background-color 0.15s;
background-color: white;
&.is-even-row {
background-color: $light-gray;
}
&:last-child {
border-bottom: none;
}
&:hover {
background-color: $less-light-gray;
}
}
td {
padding: 0.6rem 1.5rem 0.6rem 0.5rem;
vertical-align: top;
line-height: 1.4;
// Name column
&:first-child {
white-space: nowrap;
}
a {
text-decoration: none;
}
}
}
// Member hover popover
#membre-popover {
position: fixed;
z-index: 9999;
background: white;
pointer-events: none;
max-width: 380px;
min-width: 180px;
padding: 1rem;
font-family: $font-primary;
font-size: 0.85rem;
opacity: 0;
transition: opacity 0.1s;
border: solid 1px $light-gray;
&.is-visible {
opacity: 1;
}
.membre-popover-inner {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.membre-popover-pic {
width: 80px;
height: 80px;
object-fit: cover;
object-position: top;
flex-shrink: 0;
}
.membre-popover-name {
font-size: 1.1rem;
font-weight: normal;
margin: 0 0 0.3rem;
}
.membre-popover-status {
text-transform: uppercase;
color: $less-dark-gray;
margin: 0 0 0.6rem;
}
.membre-popover-domaines {
font-size: 0.8rem;
margin: 0 0 0.3rem;
line-height: 1.5;
}
.membre-popover-autres {
font-size: 0.8rem;
margin: 0;
line-height: 1.5;
color: $less-dark-gray;
}
}
// Filter bar sizing
.filtre-role { flex: 2; }
.filtre-recherche { flex: 1; }
// Member search input
.membres-search-input {
width: 100%;
border: none;
padding: 0.6rem 0.8rem;
font-family: $font-primary;
font-size: 0.85rem;
background-color: $light-gray;
outline: none;
&::placeholder {
color: $less-dark-gray;
}
&:focus {
background-color: $less-light-gray;
}
@media ($desktop) {
width: 50%;
}
}

32
scss/_mixins.scss Normal file
View File

@@ -0,0 +1,32 @@
@mixin hover-gradient-background {
background: linear-gradient(to bottom, $light_gray 60%, $yellow 100%);
background-position: bottom 0px left 0px;
background-repeat: no-repeat;
cursor: pointer;
transition: background 0.3s ease-out, padding 0.2s ease-out;
&:hover {
background-position: bottom -10px left 0px;
}
}
@mixin yellow-gradient {
background: linear-gradient(to bottom, transparent 50%, $yellow 100%);
}
@mixin yellow-gradient-after($height: 30px) {
content: '';
display: block;
position: absolute;
height: $height;
width: 100%;
bottom: 0;
left: 0;
@include yellow-gradient;
z-index: 2;
pointer-events: none;
}
@mixin category-gradient($color) {
background: linear-gradient(to bottom, $light_gray 60%, $color);
}

205
scss/_navigation.scss Normal file
View File

@@ -0,0 +1,205 @@
.main-menu {
overflow-y: scroll;
left: 0;
width: 100%;
background-color: white;
z-index: 4;
padding-bottom: 4vh;
transition: top 0.4s ease-out, opacity 0.2s ease-out;
border-top: 2px solid $light_gray;
position: absolute;
opacity: 0;
@media ($tablet) {
overflow-y: unset;
position: fixed;
}
&::after {
@include yellow-gradient-after();
opacity: 0;
transition: opacity 0.2s ease-out 0.3s;
@media ($tablet) {
opacity: 1;
}
}
&.active {
opacity: 1;
&::after {
position: fixed;
opacity: 1;
@media ($tablet) {
position: absolute;
}
}
}
}
.menu-navigation-container,
.menu-navigation-en-container {
> ul {
display: grid;
gap: 3rem;
padding: 2rem 1.5rem;
max-width: 1400px;
margin: 0 auto;
@media ($tablet) {
padding: 3rem 4rem;
grid-template-columns: repeat(3, auto);
}
@media ($desktop) {
grid-template-columns: repeat(5, auto);
}
> li {
display: flex;
flex-direction: column;
gap: 0.8rem;
align-items: start;
> a {
text-transform: uppercase;
text-decoration: none;
position: relative;
padding-bottom: 0.8rem;
margin-bottom: 0.5rem;
&::after {
content: '';
display: block;
position: absolute;
height: 8px;
width: 100%;
bottom: 0;
left: 0;
z-index: 2;
}
}
&:nth-of-type(1) {
a:hover {
color: $laboratoire;
}
> a::after {
background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, $laboratoire 100%);
}
> ul.sub-menu > li:first-of-type {
border-bottom: 1px solid $laboratoire;
padding-bottom: 0.8rem;
}
}
&:nth-of-type(2) {
a:hover {
color: $manifestations;
}
> a::after {
background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, $manifestations 100%);
}
> ul.sub-menu > li:first-of-type {
border-bottom: 1px solid $manifestations;
padding-bottom: 0.8rem;
}
}
&:nth-of-type(3) {
a:hover {
color: $publications;
}
> a::after {
background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, $publications 100%);
}
> ul.sub-menu > li:first-of-type {
border-bottom: 1px solid $publications;
padding-bottom: 0.8rem;
}
}
&:nth-of-type(4) {
a:hover {
color: $mediations;
}
> a::after {
background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, $mediations 100%);
}
> ul.sub-menu > li:first-of-type {
border-bottom: 1px solid $mediations;
padding-bottom: 0.8rem;
}
}
&:nth-of-type(5) {
a:hover {
color: $ressources;
}
> a::after {
background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, $ressources 100%);
}
> ul.sub-menu > li:first-of-type {
border-bottom: 1px solid $ressources;
padding-bottom: 0.8rem;
}
}
> ul.sub-menu {
display: flex;
flex-direction: column;
gap: 0.8rem;
> li {
> a {
text-decoration: none;
}
}
}
}
}
}
// Axes thématiques dropdown inside nav first column
.nav-axes-item {
.nav-axes-trigger {
background: none;
border: none;
padding: 0;
cursor: pointer;
font-family: $font-primary;
font-size: inherit;
color: inherit;
display: flex;
align-items: center;
gap: 0.3rem;
i {
transition: transform 0.2s ease;
}
}
&.is-open .nav-axes-trigger i {
transform: rotate(180deg);
}
.nav-axes-list {
display: none;
flex-direction: column;
gap: 0.8rem;
padding-top: 0.8rem;
li a {
font-size: 0.8rem;
padding-left: 0.5rem;
text-decoration: none;
}
}
&.is-open .nav-axes-list {
display: flex;
}
}
.menu-navigation-container,
.menu-navigation-en-container {
> ul > li:nth-of-type(1) {
.nav-axes-trigger:hover {
color: $laboratoire;
}
}
}

183
scss/_page-laboratoire.scss Normal file
View File

@@ -0,0 +1,183 @@
// ====================================
// PAGE LE LABORATOIRE
// ====================================
// ── Images ────────────────────────────────────────────────────
.labo-images {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
margin-bottom: 3rem;
}
.labo-image {
flex: 0 0 auto;
width: 100%;
margin-top: 2rem;
img {
width: 100%;
height: auto;
display: block;
}
figcaption {
font-family: $font-primary;
font-size: 0.85rem;
color: $less-dark-gray;
margin-top: 0.5rem;
}
@media ($tablet) {
width: calc(50% - 0.75rem);
}
}
// ── Section titles ────────────────────────────────────────────
.labo-section {
margin-top: 5rem;
> h3 {
font-family: $font-primary;
text-transform: uppercase;
position: relative;
display: inline-block;
margin-bottom: 2rem;
&::after {
@include yellow-gradient-after(10px);
bottom: -10px;
}
}
}
// ── Dropdown wrapper ──────────────────────────────────────────
.labo-dropdowns {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 5rem;
.labo-section & {
margin-top: 0;
}
}
// ── Dropdown item ─────────────────────────────────────────────
.labo-dropdown-item.is-open {
position: relative;
padding-bottom: 1rem;
&::after {
@include yellow-gradient-after;
}
}
.labo-dropdown-header {
display: flex;
align-items: center;
gap: 1.2rem;
padding: 0.8rem;
background-color: $light-gray;
cursor: pointer;
transition: background-color 0.15s;
font-family: $font-primary;
font-size: inherit;
font-weight: normal;
text-transform: uppercase;
margin: 0;
&:hover {
background-color: $less-light-gray;
}
}
.labo-dropdown-chevron {
font-size: 1.2rem;
transition: transform 0.2s;
flex-shrink: 0;
margin-left: auto;
.labo-dropdown-item.is-open & {
transform: rotate(180deg);
}
}
.labo-dropdown-content {
padding: 1rem 1rem 1rem 1rem;
background-color: $light-light-gray;
font-family: $font-primary;
font-size: 0.9rem;
line-height: 1.6;
p {
margin-bottom: 0.6rem;
}
p + ul {
margin-top: -1rem;
}
ul, ol {
padding-left: 1.2rem;
margin-bottom: 0.6rem;
}
}
// ── Axes list ─────────────────────────────────────────────────
.labo-axes-list {
list-style: none;
padding: 0;
margin: 0;
li {
padding: 0.5rem 0;
border-bottom: 1px solid $light-gray;
&:last-child {
border-bottom: none;
}
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
// ── Bibliothèques ─────────────────────────────────────────────
.labo-bibliotheques {
font-family: $font-primary;
font-size: 0.9rem;
line-height: 1.6;
p {
margin-bottom: 0.8rem;
}
p + ul {
margin-top: -1rem;
margin-bottom: 0.6rem;
}
a {
text-decoration: underline;
}
}
// ── Programme de recherche ────────────────────────────────────
.programme-description {
margin-bottom: 1.5rem;
p { margin-bottom: 0.6rem; }
ul, ol { padding-left: 1.2rem; }
a { text-decoration: underline; }
}
.programme-link {
padding-top: 0.5rem;
border-top: 1px solid $light-gray;
}

114
scss/_postcard.scss Normal file
View File

@@ -0,0 +1,114 @@
.post-card {
padding-bottom: 0.8rem;
border-bottom: solid 1px;
// Category-specific gradients
&.gradient--le-laboratoire {
.gradient-container {
@include category-gradient($laboratoire);
}
border-color: $laboratoire
}
&.gradient--manifestations-scientifiques {
.gradient-container {
@include category-gradient($manifestations);
}
border-color: $manifestations;
}
&.gradient--publications-et-productions {
.gradient-container {
@include category-gradient($publications);
}
border-color: $publications;
}
&.gradient--mediation-scientifique {
.gradient-container {
@include category-gradient($mediations);
}
border-color: $mediations
}
&.gradient--ressources {
.gradient-container {
@include category-gradient($ressources);
}
border-color: $ressources
}
&:hover {
.gradient-container {
img, h2 {
transform: scale(0.98);
}
}
}
.gradient-container {
height: 25vh;
padding: 0.7rem;
display: flex;
align-items: center;
justify-content: center;
text-decoration: unset;
img {
max-height: 100%;
transition: transform 0.2s ease-out;
transform: scale(1);
max-width: 100%;
}
h2 {
font-family: Gelasio;
font-size: 1.7rem;
line-height: 1.1;
padding: 1.5rem;
transition: transform 0.2s ease-out;
transform: scale(1);
text-decoration: unset;
}
&.text-only {
font-family: Gelasio;
font-size: 1.7rem;
padding: 1.5rem;
p {
transition: transform 0.2s ease-out;
transform: scale(1);
}
}
}
.contextual-infos {
text-transform: uppercase;
font-size: 0.8rem;
display: flex;
justify-content: space-between;
margin-top: 0.6rem;
line-height: 1.3;
.authors {
a {
text-decoration: none;
}
> span:not(:last-of-type)::after {
content: ", ";
}
}
.date-category {
display: flex;
flex-direction: column;
align-items: flex-end;
text-align: right;
a {
text-decoration: none;
}
}
}
.title-bottom {
font-size: 1.2rem;
margin-top: 0.8rem;
line-height: 1.2;
a {
text-decoration: none;
}
}
}

46
scss/_reset.scss Normal file
View File

@@ -0,0 +1,46 @@
*, *:before, *:after{
box-sizing: border-box;
}
html, body, div, span, object, iframe, figure, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, code, em, img, small, strike, strong, sub, sup, tt, b, u, i, ol, ul, li, fieldset, form, label, table, caption, tbody, tfoot, thead, tr, th, td, main, canvas, embed, footer, header, nav, section, video{
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
text-size-adjust: none;
}
footer, header, nav, section, main{
display: block;
}
body{
line-height: 1;
}
ol, ul{
list-style: none;
}
blockquote, q{
quotes: none;
}
blockquote:before, blockquote:after, q:before, q:after{
content: '';
content: none;
}
table{
border-collapse: collapse;
border-spacing: 0;
}
input{
-webkit-appearance: none;
border-radius: 0;
}

153
scss/_search.scss Normal file
View File

@@ -0,0 +1,153 @@
.search-page-form {
margin-top: 2rem;
margin-bottom: 3rem;
@media ($desktop) {
width: 50%;
}
.search-panel__desc {
margin-bottom: 1.3rem;
}
}
.search-page-form + #category-filters {
margin-top: 0;
}
// Author search results
.author-results {
margin-bottom: 3rem;
&__title {
font-family: $font-primary;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1.2rem;
color: $less-dark-gray;
}
}
// Taxonomy search results (axes & programmes)
.taxonomy-results {
margin-bottom: 3rem;
&__title {
font-family: $font-primary;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1.2rem;
color: $less-dark-gray;
}
&__list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
}
&__link {
display: block;
padding: 0.5rem 1rem;
border: 1px solid $light-gray;
text-decoration: none;
transition: border-color 0.15s;
&:hover {
border-color: $dark-gray;
}
}
&__name {
font-family: $font-heading;
font-size: 0.95rem;
line-height: 1.3;
}
&__meta {
display: block;
font-family: $font-primary;
font-size: 0.7rem;
text-transform: uppercase;
color: $less-dark-gray;
margin-top: 0.15rem;
}
}
.author-cards-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
@media ($tablet) {
grid-template-columns: repeat(3, 1fr);
}
@media ($desktop) {
grid-template-columns: repeat(6, 1fr);
}
}
.author-card {
border-bottom: solid 1px $laboratoire;
padding-bottom: 0.4rem;
&__visual {
display: flex;
align-items: center;
justify-content: center;
height: 14vh;
padding: 0.7rem;
background-color: lighten($laboratoire, 28%);
overflow: hidden;
text-decoration: none;
img {
max-height: 100%;
max-width: 100%;
width: auto;
height: auto;
transition: transform 0.2s ease-out;
}
&:hover img {
transform: scale(0.98);
}
}
&__initials {
font-family: $font-heading;
font-size: 2rem;
color: $laboratoire;
user-select: none;
}
&__info {
padding-top: 0.5rem;
}
&__name {
font-family: $font-heading;
font-size: 1rem;
font-weight: normal;
line-height: 1.2;
margin-bottom: 0.3rem;
a {
text-decoration: none;
}
}
&__role,
&__affiliation {
font-family: $font-primary;
font-size: 0.75rem;
text-transform: uppercase;
line-height: 1.3;
color: $less-dark-gray;
margin: 0;
}
}

584
scss/_single.scss Normal file
View File

@@ -0,0 +1,584 @@
.article {
margin-top: 0;
width: 100%;
.category-header-top {
display: flex;
flex-direction: column;
align-items: start;
justify-content: space-between;
.breadcrumb {
font-size: 0.85rem;
margin-bottom: 1.5rem;
text-transform: uppercase;
line-height: 1.3;
&__separator {
margin: 0 0.4rem;
}
}
@media ($tablet) {
flex-direction: row;
}
}
h2 {
font-family: Gelasio;
font-weight: normal;
font-size: 1.8rem;
position: relative;
display: inline-block;
margin-top: 2rem;
margin-bottom: 2rem;
&::after {
content: '';
display: block;
position: absolute;
height: 5px;
width: 100%;
// bottom: -1.1rem;
left: 0;
z-index: 2;
}
p {
line-height: 1.3;
&:last-of-type {
margin-top: 0.3rem;
margin-bottom: 0.3rem;
}
}
p + p {
font-size: 1.6rem;
}
@media ($tablet) {
p {
font-size: 2.2rem !important;
}
p + p {
font-size: 1.6rem !important;
}
&::after {
bottom: -0.4rem;
}
}
}
// Category color gradients on h2::after
&.category--le-laboratoire h2::after {
background: linear-gradient(to bottom, transparent 0%, $laboratoire 30%);
}
&.category--manifestations-scientifiques h2::after {
background: linear-gradient(to bottom, transparent 0%, $manifestations 30%);
}
&.category--publications-et-productions h2::after {
background: linear-gradient(to bottom, transparent 0%, $publications 30%);
}
&.category--mediation-scientifique h2::after {
background: linear-gradient(to bottom, transparent 0%, $mediations 30%);
}
&.category--ressources h2::after {
background: linear-gradient(to bottom, transparent 0%, $ressources 30%);
}
.article-type {
display: inline-block;
margin-top: 2rem;
font-family: $font-primary;
font-size: 0.85rem;
text-transform: uppercase;
background-color: $light-gray;
padding: 0.2rem 0.6rem;
}
.maj {
font-family: $font-primary;
font-size: 0.85rem !important;
color: $less-dark-gray;
margin-top: 1rem;
margin-bottom: 2.5rem;
text-transform: uppercase;
}
.imgs {
width: 100%;
margin-bottom: 3rem;
figure {
width: 100%;
img {
width: 100%;
height: auto;
}
figcaption {
font-family: $font-primary;
font-size: 0.85rem;
color: $less-dark-gray;
margin-top: 0.5rem;
font-style: italic;
}
@media ($tablet) {
width: 50%;
}
}
&--swiper {
display: flex;
align-items: center;
gap: 0.5rem;
@media ($tablet) {
width: 50%;
}
.swiper {
flex: 1;
min-width: 0;
}
figure {
width: 100%;
}
.swiper-pagination {
position: static;
margin-top: 0.5rem;
text-align: center;
}
.swiper-pagination-bullet-active {
background-color: $less-dark-gray !important;
}
}
}
.article-content {
display: flex;
flex-direction: column-reverse;
gap: 3rem;
@media ($tablet) {
flex-direction: row;
}
}
.sidebar {
background-color: $light-light-gray;
@media ($tablet) {
width: 25%;
padding: 0.8rem;
flex-shrink: 0;
}
.sidebar-container {
position: sticky;
top: 6rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.sidebar-section {
display: flex;
flex-direction: column;
gap: 0.8rem;
line-height: 1.4;
p {
margin: 0;
}
&.reference-bibliographique {
display: inline-block;
em, i {
font-style: italic;
}
strong {
font-weight: bold;
}
}
}
p {
font-family: $font-primary;
}
.link-button {
width: fit-content;
}
.imgs--portrait {
@media ($tablet) {
width: calc(100% + 1.6rem);
margin-left: -0.8rem;
margin-right: -0.8rem;
}
.sidebar-portrait {
width: 100%;
}
}
.sidebar-portrait {
width: 100%;
img {
width: 100%;
height: auto;
}
figcaption {
font-family: $font-primary;
font-size: 0.85rem;
color: $less-dark-gray;
margin-top: 0.5rem;
font-style: italic;
}
}
}
.main-content-text {
margin-top: 1rem;
flex: 1;
min-height: unset;
/* Affichage posts newsletter */
&:has(table[role=presentation]) {
p:not(table[role=presentation] p):not(.maj) {
display: none;
}
table {
&[role=presentation] p {
margin: unset;
}
td {
vertical-align: top;
}
br {
display: none;
}
}
}
> *:not(.article-field) {
font-size: 1.25rem;
}
a {
text-decoration: underline;
}
p {
margin-bottom: 1rem;
line-height: 1.4;
strong {
font-weight: bold;
}
em {
font-style: italic;
}
&:first-child {
margin-top: 0 !important;
}
}
ul, ol {
line-height: 1.4;
padding-left: 0.8rem;
}
ul {
list-style: inside "· ";
}
ol {
list-style: inside decimal;
}
blockquote{
padding-left: 1rem;
margin-left: 1.5rem;
border-left: solid 1px $light-gray;
}
p:first-of-type + .mots-cles {
margin-top: 1rem;
}
.article-field {
font-size: 0.9rem !important;
line-height: 1.4 !important;
margin-bottom: 1.5rem !important;
i {
font-style: italic;
}
}
.mots-cles {
margin-top: 4rem; // style par défaut = style du premier
}
.mots-cles ~ .mots-cles {
margin-top: 1rem; // les 2e et 3e ont moins d'espace
}
.canal-u-embeds,
.youtube-embeds {
margin-top: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.canal-u-embed,
.video-embed {
position: relative;
width: 100%;
padding-bottom: 56.25%; // 16:9
height: 0;
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
}
}
.inline-title {
text-transform: uppercase;
position: relative;
&::after {
content: '';
width: 100%;
height: 10px;
bottom: -1px;
left: 0;
position: absolute;
@include yellow-gradient;
}
}
.related-posts,
.seances-section {
margin-top: 5rem;
h3 {
font-family: $font-primary;
text-transform: uppercase;
position: relative;
display: inline-block;
margin-bottom: 2rem;
&::after {
@include yellow-gradient-after(10px);
bottom: -10px;
}
}
}
.related-posts {
.post-grid {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
.post-card {
min-width: 0;
a {
text-decoration: none;
h2::after {
display: none;
}
}
}
@media ($tablet) {
grid-template-columns: repeat(2, 1fr);
}
}
}
.seances-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.seance-header {
display: flex;
align-items: stretch;
gap: 1.2rem;
padding: 0.1rem;
background-color: $light-gray;
cursor: pointer;
transition: background-color 0.15s;
&:hover {
background-color: $less-light-gray;
}
@media ($tablet) {
padding: 0.8rem;
}
}
.seance-date {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 3rem;
font-family: $font-primary;
text-transform: uppercase;
line-height: 1.2;
padding: 0.4rem 0;
background: linear-gradient(to bottom, $light_light_gray 60%, $manifestations);
&__day {
font-size: 1.4rem;
}
&__month {
font-size: 0.85rem;
}
&__year {
font-size: 0.75rem;
}
}
.seance-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.2rem;
.seance-title {
font-family: $font-heading;
font-size: 1.15rem;
line-height: 1.2;
text-decoration: none;
padding: 0.6rem 0;
@media ($tablet) {
padding: unset;
}
}
.seance-intervenants {
font-family: $font-primary;
font-size: 0.85rem;
margin-top: 0.1rem;
color: black;
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
.seance-chevron {
font-size: 1.2rem;
transition: transform 0.2s;
flex-shrink: 0;
align-self: center;
margin-right: 0.5rem;
}
.seance-item.is-open .seance-chevron {
transform: rotate(180deg);
}
.seance-content {
padding: 1rem 1rem 1rem 1rem;
background-color: $light-light-gray;
font-family: $font-primary;
font-size: 0.9rem;
line-height: 1.4;
.seance-content-infos {
display: flex;
width: 100%;
justify-content: space-between;
margin-bottom: 1rem;
> .seance-content-lieu {
text-align: right;
> p {
margin: 0;
}
}
}
p {
margin-bottom: 0.6rem;
}
.seance-images {
margin-top: 1.5rem;
margin-bottom: 0;
figure {
@media ($tablet) {
width: 50%;
}
}
}
.seance-extras {
display: flex;
flex-direction: column;
align-items: start;
gap: 0.8rem;
margin-top: 1.5rem;
}
.seance-related {
margin-top: 1.5rem;
h4 {
font-family: $font-primary;
text-transform: uppercase;
margin-bottom: 1rem;
}
.post-grid {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
.post-card {
min-width: 0;
a {
text-decoration: none;
h2::after {
display: none;
}
}
}
@media ($tablet) {
grid-template-columns: repeat(2, 1fr);
}
}
}
@media ($tablet) {
padding: 1rem 1rem 1rem 5.5rem;
}
}
}

69
scss/_typography.scss Normal file
View File

@@ -0,0 +1,69 @@
@font-face {
font-family: 'Gelasio';
src: url('../assets/fonts/Gelasio-Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'NewsCycle';
src: url('../assets/fonts/NewsCycle-Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
body {
font-family: $font-primary;
}
h1, h2, h3, h4, h5, h6 {
font-family: $font-heading;
}
a,
a:active {
color: $dark-gray;
transition: color 0.2s ease-out;
}
a:hover {
color: $less-dark-gray;
}
p {
line-height: 1.2;
}
.link-button {
display: inline-flex;
background-color: $light_gray;
color: $dark_gray;
padding: 0.6rem 0.7rem;
font-size: 0.9rem;
transition: background-color 0.3s ease-out;
text-decoration: none;
justify-content: center;
align-items: center;
word-break: break-all;
max-width: 100%;
> i {
margin-right: 0.6rem;
}
&:hover {
background-color: $less_light-gray;
}
@media ($tablet) {
font-size: unset;
padding: 0.6rem 1rem;
}
// Multi-word titles: break at word boundaries; only split a word mid-letter
// as a last resort when it overflows the container.
&--wrap-word {
word-break: normal;
overflow-wrap: break-word;
}
}

31
scss/_variables.scss Normal file
View File

@@ -0,0 +1,31 @@
// Neutral colors
$light-light-gray: #fcfcfc;
$light-gray: #eeeeee;
$less-light-gray: #cccccc;
$less-less-light-gray: #bbbbbb;
$yellow: #f7ff29;
$dark-gray: #1a1a1a;
$less-dark-gray: #3e3e3e;
// Theme colors
$laboratoire: #e0775d;
$manifestations: #7cc0c6;
$mediations: #e05680;
$publications: #46ae51;
$ressources: #bb8dd9;
// Fonts
$font-primary: 'NewsCycle', sans-serif;
$font-heading: 'Gelasio', serif;
// Breakpoints
$breakpoint-tablet: 768px;
$breakpoint-desktop: 1024px;
$breakpoint-large: 1440px;
// Media queries (mobile first)
$tablet: 'min-width: #{$breakpoint-tablet}';
$desktop: 'min-width: #{$breakpoint-desktop}';
$large: 'min-width: #{$breakpoint-large}';
// Fonts sizes

25
scss/style.scss Normal file
View File

@@ -0,0 +1,25 @@
@import 'reset';
@import 'variables';
@import 'mixins';
@import 'base';
@import 'typography';
@import 'layout';
@import 'header';
@import 'navigation';
@import 'footer';
@import 'index';
@import 'postcard';
@import 'category';
@import 'filters';
@import 'single';
@import 'author';
@import 'membres';
@import 'page-laboratoire';
@import 'search';
/*
Theme Name: Thalim
Author: Valentin Le Moign
Version: 1.0
*/

248
search.php Normal file
View File

@@ -0,0 +1,248 @@
<?php
$context = Timber::context();
// Séances de séminaire (cat 12) are included: post-card-helpers rewrites their
// link to the parent séminaire + #seance-{ID} hash.
$excluded_cat_ids = [31]; // Non classé
if ( ! is_user_logged_in() ) $excluded_cat_ids[] = 9; // Vie du labo
$search_query = get_search_query();
// Read filter query params
$active_axe = isset($_GET['axe']) ? intval($_GET['axe']) : 0;
$active_date_from = isset($_GET['date_from']) ? sanitize_text_field($_GET['date_from']) : '';
$active_date_to = isset($_GET['date_to']) ? sanitize_text_field($_GET['date_to']) : '';
$active_cat_id = isset($_GET['filter_cat']) ? intval($_GET['filter_cat']) : 0;
$filter_autres = isset($_GET['filter_autres']) ? 1 : 0;
$context['search_query'] = $search_query;
$context['active_axe'] = $active_axe;
$context['active_date_from'] = $active_date_from;
$context['active_date_to'] = $active_date_to;
$context['active_category_id'] = $filter_autres ? 'autres' : $active_cat_id;
$context['active_cat_id'] = $active_cat_id;
$context['filter_autres'] = $filter_autres;
// Determine active rubrique
$active_rubrique_id = 0;
if ($active_cat_id) {
$active_cat_obj = get_category($active_cat_id);
$active_rubrique_id = ($active_cat_obj && $active_cat_obj->parent)
? $active_cat_obj->parent
: $active_cat_id;
}
$context['active_rubrique'] = $active_rubrique_id;
// Base URL for search filter links (language-aware)
$search_base = thalim_en_url( home_url('/') );
// Override annonces_url: rubrique reset stays on search page (no filter_cat)
$context['annonces_url'] = add_query_arg(['s' => $search_query], $search_base);
// Base params preserved across filter links (preserves search term)
$base_filter_params = array_filter([
's' => $search_query,
'axe' => $active_axe ?: null,
'date_from' => $active_date_from ?: null,
'date_to' => $active_date_to ?: null,
]);
// Build tax_query
$tax_query = [
'relation' => 'AND',
[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => $excluded_cat_ids,
'operator' => 'NOT IN',
],
];
if ($active_cat_id) {
$tax_query[] = [
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [$active_cat_id],
'include_children' => !$filter_autres,
];
}
$query_args = [
'post_type' => 'post',
's' => $search_query,
'relevanssi' => true,
'posts_per_page' => 12,
'orderby' => 'relevance',
'order' => 'DESC',
'lang' => '',
'tax_query' => $tax_query,
];
if ($active_axe) {
$query_args['meta_query'] = [[
'key' => 'axes_thematiques',
'value' => $active_axe,
'type' => 'NUMERIC',
]];
}
if ($active_date_from || $active_date_to) {
$date_query = ['inclusive' => true];
if ($active_date_from) $date_query['after'] = $active_date_from;
if ($active_date_to) $date_query['before'] = $active_date_to;
$query_args['date_query'] = [$date_query];
}
// Axes thématiques for filter dropdown
$axes_groups = thalim_get_axes_filter_groups();
$current_axes = $axes_groups[0]['terms'] ?? [];
$context['filter_axes'] = $current_axes;
$context['axe_stay_on_page'] = true;
// Rubrique/catégorie filter links (all preserve search term)
$all_cats = get_categories(['taxonomy' => 'category', 'hide_empty' => false, 'exclude' => $excluded_cat_ids]);
$filter_parents = [];
foreach ($all_cats as $cat) {
if ($cat->parent == 0) {
$params = array_filter(array_merge($base_filter_params, ['filter_cat' => $cat->term_id]));
$filter_parents[] = [
'id' => $cat->term_id,
'name' => thalim_cat_name($cat),
'slug' => $cat->slug,
'link' => add_query_arg($params, $search_base),
];
}
}
$context['filter_parents'] = $filter_parents;
$filter_categories = [];
if ($active_rubrique_id) {
foreach ($all_cats as $cat) {
if ($cat->parent == $active_rubrique_id) {
$params = array_filter(array_merge($base_filter_params, ['filter_cat' => $cat->term_id]));
$filter_categories[] = [
'id' => $cat->term_id,
'name' => thalim_cat_name($cat),
'slug' => $cat->slug,
'link' => add_query_arg($params, $search_base),
];
}
}
}
// Add "Autres" entry if active rubrique has posts directly assigned to it
if ($active_rubrique_id && !empty($filter_categories)) {
$lang = thalim_current_language();
$direct_check = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 1,
'fields' => 'ids',
'no_found_rows' => true,
'lang' => '',
'tax_query' => [[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [$active_rubrique_id],
'include_children' => false,
]],
]);
if ($direct_check->have_posts()) {
$params = array_filter(array_merge($base_filter_params, ['filter_cat' => $active_rubrique_id, 'filter_autres' => 1]));
$filter_categories[] = [
'id' => 'autres',
'name' => $lang === 'en' ? 'Other' : 'Autres',
'slug' => 'autres',
'link' => add_query_arg($params, $search_base),
];
}
}
$context['filter_categories'] = $filter_categories;
$posts = Timber::get_posts($query_args);
$context['cards'] = thalim_get_cards_data($posts);
$context['posts'] = $posts;
// Search users (members) by display_name
$author_cards = [];
if ( $search_query ) {
$excluded_role_ids = [ 600, 598 ]; // "À ranger", "Archive"
$user_query = new WP_User_Query([
'search' => '*' . $search_query . '*',
'search_columns' => ['display_name'],
'number' => 6,
'orderby' => 'display_name',
'order' => 'ASC',
'meta_query' => [
[
'key' => 'role_1',
'value' => $excluded_role_ids,
'compare' => 'NOT IN',
],
],
]);
$lang = thalim_current_language();
// Direction IDs (same source as membres page and author page)
$labo_page = get_page_by_path( 'le-laboratoire' );
$labo_directeur_id = $labo_page ? intval( get_post_meta( $labo_page->ID, 'directeur', true ) ) : 0;
$labo_adjoint_id = $labo_page ? intval( get_post_meta( $labo_page->ID, 'directeur_adjoint', true ) ) : 0;
foreach ( $user_query->get_results() as $user ) {
$avatar_url = thalim_get_user_avatar_url( $user->ID );
$role_id = get_user_meta( $user->ID, 'role_1', true );
$role_label = '';
if ( $role_id ) {
$role_term = get_term( intval( $role_id ), 'role' );
if ( $role_term && ! is_wp_error( $role_term ) ) {
$override = thalim_bilingual( get_user_meta( $user->ID, 'affichage_du_statut_1', true ) ?: '', $lang );
$role_label = $override ?: $role_term->name;
}
}
if ( $user->ID === $labo_directeur_id ) {
$role_label = 'Directeur' . ( $role_label ? ', ' . $role_label : '' );
} elseif ( $user->ID === $labo_adjoint_id ) {
$role_label = 'Directeur adjoint' . ( $role_label ? ', ' . $role_label : '' );
}
$affiliation = get_user_meta( $user->ID, 'affiliation', true ) ?: '';
if ( strtolower( $affiliation ) === 'autre' ) {
$affiliation = thalim_bilingual( get_user_meta( $user->ID, 'affiliation_autre', true ) ?: '', $lang );
}
$words = preg_split( '/\s+/', trim( $user->display_name ) );
$initials = implode( '', array_map( fn( $w ) => mb_substr( $w, 0, 1 ), $words ) );
$author_cards[] = [
'id' => $user->ID,
'name' => $user->display_name,
'url' => get_author_posts_url( $user->ID ),
'avatar_url' => $avatar_url,
'initials' => mb_strtoupper( $initials ),
'role_label' => $role_label,
'affiliation' => $affiliation,
];
}
}
$context['author_cards'] = $author_cards;
// Search taxonomy terms (axes thématiques + programmes de recherche)
$taxonomy_cards = [];
if ( $search_query ) {
$matching_terms = get_terms([
'taxonomy' => [ 'axe_thematique', 'programme_de_recherche' ],
'hide_empty' => false,
'name__like' => $search_query,
]);
if ( ! is_wp_error( $matching_terms ) ) {
foreach ( $matching_terms as $term ) {
$tax_obj = get_taxonomy( $term->taxonomy );
$taxonomy_cards[] = [
'name' => $term->name,
'url' => get_term_link( $term ),
'taxonomy_label' => $tax_obj ? $tax_obj->labels->singular_name : $term->taxonomy,
'count' => $term->count,
];
}
}
}
$context['taxonomy_cards'] = $taxonomy_cards;
Timber::render('search.twig', $context);

21
single.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
$context = Timber::context();
$post = Timber::get_post();
$context['post'] = $post;
$context['article'] = thalim_get_single_data($post->ID);
// Card data for related posts (main + séances)
$related_cards = [];
if (!empty($context['article']['annonces_liees'])) {
$related_cards += thalim_get_cards_data($context['article']['annonces_liees']);
}
foreach (['seances_a_venir', 'seances_passees'] as $seance_group) {
foreach ($context['article'][$seance_group] as $s) {
if (!empty($s['annonces_liees'])) {
$related_cards += thalim_get_cards_data($s['annonces_liees']);
}
}
}
$context['related_cards'] = $related_cards;
Timber::render('single.twig', $context);

9
style.css Normal file
View File

@@ -0,0 +1,9 @@
/*
Theme Name: Thalim
Author: THALIM — Théorie et Histoire des Arts et des Littératures de la Modernité
Description: Thème personnalisé pour le laboratoire THALIM (UMR 7172). Basé sur Timber/Twig.
Version: 1.0.0
Requires at least: 6.0
Requires PHP: 7.4
Text Domain: thalim
*/

2
tag.php Normal file
View File

@@ -0,0 +1,2 @@
<?php
require __DIR__ . '/taxonomy.php';

186
taxonomy.php Normal file
View File

@@ -0,0 +1,186 @@
<?php
$context = Timber::context();
$term = get_queried_object();
$taxonomy = $term->taxonomy;
$context['term'] = Timber::get_term($term);
$context['taxonomy_slug'] = $taxonomy;
$context['term_id'] = $term->term_id;
$context['parent_slug'] = '';
$tax_object = get_taxonomy($taxonomy);
$context['taxonomy_label'] = $tax_object ? $tax_object->labels->singular_name : $taxonomy;
$excluded_ids = [12, 31]; // Séance de séminaire, Non classé
if ( ! is_user_logged_in() ) $excluded_ids[] = 9; // Vie du labo
// Read filter query params
$active_axe = isset($_GET['axe']) ? intval($_GET['axe']) : 0;
$active_date_from = isset($_GET['date_from']) ? sanitize_text_field($_GET['date_from']) : '';
$active_date_to = isset($_GET['date_to']) ? sanitize_text_field($_GET['date_to']) : '';
$active_cat_id = isset($_GET['filter_cat']) ? intval($_GET['filter_cat']) : 0;
$filter_autres = isset($_GET['filter_autres']) ? 1 : 0;
$context['active_axe'] = $active_axe;
$context['active_date_from'] = $active_date_from;
$context['active_date_to'] = $active_date_to;
$context['active_category_id'] = $filter_autres ? 'autres' : $active_cat_id;
$context['active_cat_id'] = $active_cat_id;
$context['filter_autres'] = $filter_autres;
// Determine active rubrique from active category (parent if subcategory, itself if top-level)
$active_rubrique_id = 0;
if ($active_cat_id) {
$active_cat_obj = get_category($active_cat_id);
$active_rubrique_id = ($active_cat_obj && $active_cat_obj->parent)
? $active_cat_obj->parent
: $active_cat_id;
}
$context['active_rubrique'] = $active_rubrique_id;
// Base params shared across all filter links (preserves active filters when navigating)
$base_filter_params = array_filter([
'axe' => $active_axe ?: null,
'date_from' => $active_date_from ?: null,
'date_to' => $active_date_to ?: null,
]);
// Build tax_query — combine all clauses with AND
$tax_query = [
'relation' => 'AND',
// Terme de la taxonomie courante
[
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => [$term->term_id],
],
// Exclure les séances de séminaire (catégorie 12)
[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [12],
'operator' => 'NOT IN',
],
];
if ($active_cat_id) {
$tax_query[] = [
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [$active_cat_id],
'include_children' => !$filter_autres,
];
}
// On axe_thematique pages, the current term IS the active axe (for display only — taxonomy query handles filtering)
$axe_taxonomy_mode = ($taxonomy === 'axe_thematique');
if ($axe_taxonomy_mode) {
$active_axe = $term->term_id;
$context['active_axe'] = $active_axe;
}
// Build remaining query args (meta/date)
$extra_query_args = [];
if ($active_axe && !$axe_taxonomy_mode) {
$extra_query_args['meta_query'] = [[
'key' => 'axes_thematiques',
'value' => $active_axe,
'type' => 'NUMERIC',
]];
}
if ($active_date_from || $active_date_to) {
$extra_query_args['thalim_event_date_filter'] = ['from' => $active_date_from, 'to' => $active_date_to];
}
// Axes thématiques filter
$axes_groups = thalim_get_axes_filter_groups();
$current_axes = $axes_groups[0]['terms'] ?? [];
$context['filter_axes'] = $current_axes;
$context['axe_taxonomy_mode'] = $axe_taxonomy_mode;
$context['axe_stay_on_page'] = !$axe_taxonomy_mode;
// Build rubrique/catégorie filter links pointing back to the current taxonomy URL
$current_term_url = get_term_link($term);
$all_cats = get_categories(['taxonomy' => 'category', 'hide_empty' => false, 'exclude' => $excluded_ids]);
$filter_parents = [];
foreach ($all_cats as $cat) {
if ($cat->parent == 0) {
$params = array_filter(array_merge($base_filter_params, ['filter_cat' => $cat->term_id]));
$filter_parents[] = [
'id' => $cat->term_id,
'name' => thalim_cat_name($cat),
'slug' => $cat->slug,
'link' => add_query_arg($params, $current_term_url),
];
}
}
$context['filter_parents'] = $filter_parents;
$filter_categories = [];
if ($active_rubrique_id) {
foreach ($all_cats as $cat) {
if ($cat->parent == $active_rubrique_id) {
$params = array_filter(array_merge($base_filter_params, ['filter_cat' => $cat->term_id]));
$filter_categories[] = [
'id' => $cat->term_id,
'name' => thalim_cat_name($cat),
'slug' => $cat->slug,
'link' => add_query_arg($params, $current_term_url),
];
}
}
}
// Add "Autres" entry if active rubrique has posts directly assigned to it
if ($active_rubrique_id && !empty($filter_categories)) {
$lang = thalim_current_language();
$direct_check = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 1,
'fields' => 'ids',
'no_found_rows' => true,
'lang' => '',
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => [$term->term_id],
],
[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [$active_rubrique_id],
'include_children' => false,
],
],
]);
if ($direct_check->have_posts()) {
$params = array_filter(array_merge($base_filter_params, ['filter_cat' => $active_rubrique_id, 'filter_autres' => 1]));
$filter_categories[] = [
'id' => 'autres',
'name' => $lang === 'en' ? 'Other' : 'Autres',
'slug' => 'autres',
'link' => add_query_arg($params, $current_term_url),
];
}
}
$context['filter_categories'] = $filter_categories;
$posts = Timber::get_posts(array_merge([
'post_type' => 'post',
'tax_query' => $tax_query,
'posts_per_page' => 12,
'orderby' => 'date',
'order' => 'DESC',
'lang' => '',
'thalim_event_date_order' => true,
], $extra_query_args));
$context['cards'] = thalim_get_cards_data($posts);
$context['posts'] = $posts;
// Custom Pods presentation fields (not the WP built-in description)
$tax_lang = thalim_current_language();
$pres_fr = get_term_meta($term->term_id, 'presentation', true) ?: '';
$pres_en = get_term_meta($term->term_id, 'presentation_en', true) ?: '';
$context['term_presentation'] = wpautop( ( $tax_lang === 'en' && $pres_en ) ? $pres_en : $pres_fr );
Timber::render('taxonomy.twig', $context);

21
templates/404.twig Normal file
View File

@@ -0,0 +1,21 @@
{% extends "base.twig" %}
{% block content %}
<div class="full-block hero-header" style="min-height: 300px;">
<div class="hero-content">
<p class="error-404__code">404</p>
<p class="error-404__title">
{{ current_language == 'en' ? 'Page not found' : 'Page introuvable' }}
</p>
<h1 style="font-size: 1.2rem; margin-top: 1rem;">
{{ current_language == 'en'
? 'The page you are looking for does not exist or has been moved.'
: "La page que vous cherchez n'existe pas ou a été déplacée." }}
</h1>
<a href="{{ function('home_url', '/') }}" class="link-button">
{{ current_language == 'en' ? '← Back to home' : "← Retour à l'accueil" }}
</a>
</div>
<div id="sketch"></div>
</div>
{% endblock %}

181
templates/author.twig Normal file
View File

@@ -0,0 +1,181 @@
{% extends "base.twig" %}
{% block content %}
<div class="article category--le-laboratoire">
<div class="full-block">
<div class="category-header-top">
<nav class="breadcrumb" aria-label="Fil d'Ariane">
<a href="{{ function('home_url', '/') }}">{{ current_language == 'en' ? 'Home' : 'Accueil' }}</a>
<span class="breadcrumb__separator">&rarr;</span>
<a class="breadcrumb__cat" href="{{ current_language == 'en' ? function('home_url', '/en/le-laboratoire/') : function('home_url', '/le-laboratoire/') }}">{{ current_language == 'en' ? 'The department' : 'Le laboratoire' }}</a>
<span class="breadcrumb__separator">&rarr;</span>
<a class="breadcrumb__cat" href="{{ current_language == 'en' ? function('home_url', '/en/membres/') : function('home_url', '/membres/') }}">{{ current_language == 'en' ? 'Lab members' : 'Membres du laboratoire' }}</a>
</nav>
{% if author_edit_link %}
<a href="{{ author_edit_link }}" class="link-button" target="_blank" rel="noopener">
<i class="iconoir-edit-pencil"></i>{{ current_language == 'en' ? 'Edit profile' : 'Éditer le profil' }}
</a>
{% endif %}
</div>
{% if author.avatar_url %}
<div class="author-header">
<div class="author-avatar">
<img src="{{ author.avatar_url }}" alt="{{ author.display_name }}">
</div>
<div class="author-identity">
<h2><p>{{ author.display_name }}</p></h2>
{% if author.role_label or author.role_complement or author.affiliation %}
<p class="author-role">
{{ author.role_label }}{% if author.role_complement %} {{ author.role_complement }}{% if author.affiliation %},{% endif %}{% endif %}{% if author.affiliation %} {{ author.affiliation }}{% endif %}
</p>
{% endif %}
<p class="maj">{{ current_language == 'en' ? 'Updated on' : 'Mis à jour le' }} {{ author.user_since }}</p>
</div>
</div>
{% endif %}
<div class="article-content">
{% if author.email or author.liens_externes or author.documents or author.hal_publications_url %}
<div class="sidebar">
<div class="sidebar-container">
<div class="sidebar-section">
{% if author.email %}
<a href="mailto:{{ author.email }}" class="link-button"><i class="iconoir-mail"></i>{{ author.email }}</a>
{% endif %}
{% for doc in author.documents %}
<a href="{{ doc.url }}" class="link-button{% if ' ' in doc.title %} link-button--wrap-word{% endif %}" target="_blank" rel="noopener"><i class="iconoir-page"></i>{{ doc.title }}</a>
{% endfor %}
{% for lien in author.liens_externes %}
<a href="{{ lien.url }}" class="link-button{% if ' ' in lien.titre %} link-button--wrap-word{% endif %}" target="_blank" rel="noopener"><i class="iconoir-open-new-window"></i>{{ lien.titre }}</a>
{% endfor %}
{% if author.hal_publications_url %}
<a href="{{ author.hal_publications_url }}" class="link-button link-button--wrap-word" target="_blank" rel="noopener"><i class="iconoir-open-new-window"></i>Publications HAL</a>
{% endif %}
</div>
</div>
</div>
{% endif %}
<div class="main-content-text">
{% if not author.avatar_url %}
<div class="author-identity">
<h2><p>{{ author.display_name }}</p></h2>
{% if author.role_label or author.role_complement or author.affiliation %}
<p class="author-role">
{{ author.role_label }}{% if author.role_complement %} {{ author.role_complement }}{% if author.affiliation %},{% endif %}{% endif %}{% if author.affiliation %} {{ author.affiliation }}{% endif %}
</p>
{% endif %}
<p class="maj">{{ current_language == 'en' ? 'Updated on' : 'Mis à jour le' }} {{ author.user_since }}</p>
</div>
{% endif %}
{% if current_language == 'en' and author.bio_en %}
<div class="author-bio">{{ author.bio_en|raw }}</div>
{% elseif author.bio %}
<div class="author-bio">{{ author.bio|raw }}</div>
{% endif %}
{% if author.domaines_tags %}
<p class="article-field mots-cles">
<span class="inline-title">{{ current_language == 'en' ? 'Research areas' : 'Domaines de recherches' }} :</span>
{% for tag in author.domaines_tags %}
<a href="{{ tag.url }}" class="keyword-link">{{ tag.name }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
</p>
{% endif %}
{% if current_language == 'en' and author.domaines_en %}
<div class="article-field domaines-autres">
{% if not author.domaines_tags %}<span class="inline-title">Research areas :</span>{% endif %}
{{ author.domaines_en|raw }}
</div>
{% elseif author.domaines %}
<div class="article-field domaines-autres">
{% if not author.domaines_tags %}<span class="inline-title">{{ current_language == 'en' ? 'Research areas' : 'Domaines de recherches' }} :</span>{% endif %}
{{ author.domaines|raw }}
</div>
{% endif %}
{% if current_language == 'en' and author.recherches_en %}
<div class="article-field recherches-en-cours">
<span class="inline-title">Current research :</span>
{{ author.recherches_en|raw }}
</div>
{% elseif author.recherches %}
<div class="article-field recherches-en-cours">
<span class="inline-title">{{ current_language == 'en' ? 'Current research' : 'Recherches en cours' }} :</span>
{{ author.recherches|raw }}
</div>
{% endif %}
{% if author.axes %}
<p class="article-field mots-cles">
<span class="inline-title">{{ current_language == 'en' ? 'Thematic axes' : 'Axes thématiques' }} :</span>
{% for axe in author.axes %}
<a href="{{ axe.url }}" class="keyword-link">{{ axe.name }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
</p>
{% endif %}
{% if author.titre_these or author.resume_these or author.resume_these_en %}
<section class="seances-section">
<h3>{{ current_language == 'en' ? 'Thesis' : 'Thèse' }}</h3>
{% if author.titre_these %}
<p class="author-titre-these">{{ author.titre_these }}</p>
{% endif %}
{% if author.date_soutenance %}
<p class="article-field">
<span class="these-inline-title">{{ current_language == 'en' ? 'Defended in' : 'Soutenue en' }} </span>
{{ author.date_soutenance }}
</p>
{% endif %}
{% if author.directeur_thalim or author.autre_directeur %}
<p class="article-field">
<span class="these-inline-title">{{ current_language == 'en' ? 'Supervisor' : 'Direction' }} :</span>
{% if author.directeur_thalim %}
<a href="{{ author.directeur_thalim.url }}">{{ author.directeur_thalim.name }}</a>{% if author.autre_directeur %}, {% endif %}
{% endif %}
{% if author.autre_directeur %}{{ author.autre_directeur }}{% endif %}
</p>
{% endif %}
{% if current_language == 'en' and author.resume_these_en %}
<div class="author-resume-these">{{ author.resume_these_en|raw }}</div>
{% elseif author.resume_these %}
<div class="author-resume-these">{{ author.resume_these|raw }}</div>
{% endif %}
</section>
{% endif %}
{% if author_posts %}
<div class="author-posts-section">
{% for group in author_posts %}
<div class="author-posts-item">
<h3 class="author-posts-header" data-seance-toggle>
{{ group.cat_name }}
<i class="iconoir-nav-arrow-down author-posts-chevron"></i>
</h3>
<div class="author-posts-content" style="display: none;">
<div class="author-post-grid">
{% for post in group.posts %}
{% include 'partials/post-card.twig' with {
post: post,
card: group.cards[post.ID],
show_category: false
} %}
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

21
templates/base.twig Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html {{ site.language_attributes }}>
<head>
<meta charset="{{ site.charset }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{ function('wp_head') }}
</head>
<body class="{{ body_class }}">
{% include 'partials/header.twig' %}
<main>
<div class="container">
{% block content %}{% endblock %}
</div>
</main>
{% include 'partials/footer.twig' %}
{{ function('wp_footer') }}
</body>
</html>

132
templates/category.twig Normal file
View File

@@ -0,0 +1,132 @@
{% extends "base.twig" %}
{% block content %}
<main class="category-archive">
<div class="container">
<div class="full-block category--{{ parent_slug }}">
<header class="category-header">
<div class="category-header-top">
<nav class="breadcrumb" aria-label="Fil d'Ariane">
<a href="{{ function('home_url', '/') }}">{{ current_language == 'en' ? 'Home' : 'Accueil' }}</a>
{% if not is_parent and category.parent %}
<span class="breadcrumb__separator">&rarr;</span>
<a class="breadcrumb__cat" href="{{ function('get_category_link', category.parent) }}">{{ function('thalim_cat_name', category.parent) }}</a>
<span class="breadcrumb__separator">&rarr;</span>
<a class="breadcrumb__cat" href="{{ function('get_category_link', category.term_id) }}"><span class="breadcrumb__current">{{ category|cat_name }}</span></a>
{% elseif is_direct %}
<span class="breadcrumb__separator">&rarr;</span>
<a class="breadcrumb__cat" href="{{ function('get_category_link', category.term_id) }}">{{ category|cat_name }}</a>
<span class="breadcrumb__separator">&rarr;</span>
<span class="breadcrumb__current">{{ current_language == 'en' ? 'Other' : 'Autre' }}</span>
{% else %}
<span class="breadcrumb__separator">&rarr;</span>
<a class="breadcrumb__cat" href="{{ function('get_category_link', category.term_id) }}"><span class="breadcrumb__current">{{ category|cat_name }}</span></a>
{% endif %}
</nav>
<a href="{{ agenda_toggle_url }}" class="link-button agenda-toggle-btn">
{% if view_mode == 'agenda' %}
<i class="iconoir-view-grid"></i>{{ current_language == 'en' ? 'Switch to grid view' : 'Passer à la vue grille' }}
{% else %}
<i class="iconoir-calendar"></i>{{ current_language == 'en' ? 'Switch to agenda view' : 'Passer à la vue agenda' }}
{% endif %}
</a>
</div>
<h1 class="category-header__title">
{%- if is_direct -%}
{{ current_language == 'en' ? 'Other' : 'Autres' }} {{ (category|cat_name)|lower }}
{%- else -%}
{{ category|cat_name }}
{%- endif -%}
</h1>
{% if term_presentation %}
<div class="taxonomy-description">{{ term_presentation|raw }}</div>
{% endif %}
</header>
{% include 'partials/category-filters.twig' %}
<div id="grid-sections"{% if view_mode == 'agenda' %} style="display:none"{% endif %}>
{% if is_parent %}
{% for subcategory in subcategories %}
{% if subcategory.posts is not empty %}
<section class="subcategory-section">
<h2 class="subcategory-section__title">{{ subcategory.term|cat_name }}</h2>
<div class="post-grid">
{% for post in subcategory.posts %}
{% include 'partials/post-card.twig' with { post: post, card: cards[post.ID], show_category: true, type_only: true } %}
{% endfor %}
</div>
<div class="category-section-footer">
{% if current_language == 'en' %}
<a href="{{ subcategory.term.link }}" class="link-button"><i class="iconoir-plus-circle"></i>See all {{ (subcategory.term|cat_name)|lower }}</a>
{% else %}
{% set is_fem = subcategory.term.description == 'f' %}
<a href="{{ subcategory.term.link }}" class="link-button"><i class="iconoir-plus-circle"></i>Voir {{ is_fem ? 'toutes les' : 'tous les' }} {{ (subcategory.term|cat_name)|lower }}</a>
{% endif %}
</div>
</section>
{% endif %}
{% endfor %}
{% if direct_posts is defined and direct_posts is not empty %}
<section class="subcategory-section">
<h2 class="subcategory-section__title">{{ current_language == 'en' ? 'Other' : 'Autres' }}</h2>
<div class="post-grid">
{% for post in direct_posts %}
{% include 'partials/post-card.twig' with { post: post, card: cards[post.ID], show_category: true, type_only: true } %}
{% endfor %}
</div>
<div class="category-section-footer">
{% if current_language == 'en' %}
<a href="{{ autres_link }}" class="link-button"><i class="iconoir-plus-circle"></i>See all other {{ (category|cat_name)|lower }}</a>
{% else %}
<a href="{{ autres_link }}" class="link-button"><i class="iconoir-plus-circle"></i>Voir toutes les autres {{ (category|cat_name)|lower }}</a>
{% endif %}
</div>
</section>
{% endif %}
{% else %}
<section class="subcategory-section">
<div class="post-grid" id="post-grid">
{% for post in posts %}
{% include 'partials/post-card.twig' with { post: post, card: cards[post.ID], show_category: true, type_only: true } %}
{% endfor %}
</div>
<div id="scroll-sentinel"
data-category="{{ category_id }}"
data-axe="{{ active_axe }}"
data-date-from="{{ active_date_from }}"
data-date-to="{{ active_date_to }}"></div>
<div id="scroll-spinner" class="scroll-spinner" style="display: none;">
<div class="scroll-spinner__dot"></div>
<div class="scroll-spinner__dot"></div>
<div class="scroll-spinner__dot"></div>
</div>
</section>
{% endif %}
</div>
{# Agenda view — shared by parent and leaf categories #}
<div class="agenda-view-container{% if view_mode == 'agenda' %} is-active{% endif %}"
id="agenda-view"
data-category="{{ category_id }}"
data-include-children="{{ agenda_include_children }}"
data-axe="{{ active_axe }}"
data-date-from="{{ active_date_from }}"
data-date-to="{{ active_date_to }}">
<h2 class="agenda-view-title">Agenda</h2>
<div class="agenda-swiper-wrap">
<button class="agenda-swiper-prev" aria-label="{{ current_language == 'en' ? 'Previous' : 'Précédent' }}"><i class="iconoir-arrow-left"></i></button>
<div class="swiper agenda-swiper">
<div class="swiper-wrapper" id="agenda-swiper-wrapper"></div>
</div>
<button class="agenda-swiper-next" aria-label="{{ current_language == 'en' ? 'Next' : 'Suivant' }}"><i class="iconoir-arrow-right"></i></button>
</div>
<div id="agenda-spinner" class="scroll-spinner" style="display:none;">
<div class="scroll-spinner__dot"></div>
<div class="scroll-spinner__dot"></div>
<div class="scroll-spinner__dot"></div>
</div>
</div>
</div>
</div>
</main>
{% endblock %}

135
templates/index.twig Normal file
View File

@@ -0,0 +1,135 @@
{% extends "base.twig" %}
{% block content %}
<div class="full-block hero-header">
<div class="hero-content">
<div class="hero-logos">
<a href="https://www.cnrs.fr/" target="_blank">
<img src="{{ theme.uri }}/assets/images/cnrs.png" alt="Logo CNRS">
</a>
<a href="https://www.sorbonne-nouvelle.fr/" target="_blank">
<img src="{{ theme.uri }}/assets/images/sorbonne.png" alt="Logo Sorbonne Nouvelle">
</a>
<a href="https://www.ens.psl.eu/" target="_blank">
<img src="{{ theme.uri }}/assets/images/ens.png" alt="Logo ENS">
</a>
</div>
<p class="hero-presentation">
{{ gc.presentation }}
</p>
<p class="hero-presentation-detail">
{{ gc.presentation_detail }}
</p>
<a href="{{ current_language == 'en' ? function('home_url', '/en/le-laboratoire/') : function('home_url', '/le-laboratoire/') }}" class="link-button">
{{ current_language == 'fr' ? 'En savoir plus' : 'Learn more' }}
</a>
</div>
<div id="sketch"></div>
</div>
{% include 'partials/swiper-section.twig' with {
section_posts: annonces,
section_cards: annonces_cards,
section_title: current_language == 'en' ? 'Announcements' : 'Annonces',
all_link: annonces_link,
all_label: current_language == 'en' ? 'All announcements' : 'Toutes les annonces'
} %}
{% if messages_labo or agenda_items %}
<section class="message-agenda-section">
{% if messages_labo %}
<div class="message-du-labo">
<div class="section-title">
<p>{{ current_language == 'en' ? 'Laboratory messages' : 'Messages du laboratoire' }}</p>
</div>
<div class="messages-list">
{% for message in messages_labo %}
<div class="message-item">
<time class="message-date" datetime="{{ message.date('Y-m-d') }}">{{ message.date('d/m/Y') }}</time>
<div class="message-content">
{{ message.content | raw }}
<a href="{{ message.link | en_url }}" class="message-read-more">
{{ current_language == 'en' ? 'Read more' : 'Lire la suite' }}
</a>
</div>
</div>
{% endfor %}
</div>
<div class="button-messages">
<a href="{{ message_labo_link }}" class="link-button">
{{ current_language == 'en' ? 'All laboratory messages' : 'Tous les messages du laboratoire' }}
</a>
</div>
</div>
{% endif %}
{% if agenda_items %}
<div class="agenda">
<div class="section-title">
<p>{{ current_language == 'en' ? 'Upcoming' : 'À venir' }}</p>
</div>
<div class="agenda-content">
{% for item in agenda_items %}
<a href="{{ item.link | en_url }}" class="agenda-item">
<div class="date-container">
<p>{% if item.day == 1 %}1<sup>{{ current_language == 'en' ? 'st' : 'er' }}</sup>{% else %}{{ item.day }}{% endif %}</p>
<p>{{ item.month }}</p>
</div>
<div class="event-content">
<div class="meta">
{% if item.type_label %}<p>{{ item.type_label }}</p>{% endif %}
{% if item.lieu %}<p>{{ item.lieu }}</p>{% endif %}
</div>
<div class="event-title">
<p>{{ item.post.title | bilingual(current_language) }}</p>
</div>
</div>
</a>
{% endfor %}
</div>
<div class="button-agenda">
<a href="{{ manifestations_link }}" class="link-button">
{{ current_language == 'en' ? 'All events' : 'Tous les événements' }}
</a>
</div>
</div>
{% endif %}
</section>
{% endif %}
{% include 'partials/swiper-section.twig' with {
section_posts: publications,
section_cards: publications_cards,
section_title: current_language == 'en' ? 'Books & journals' : 'Ouvrages et Revues',
all_link: publications_link,
all_label: current_language == 'en' ? 'All publications' : 'Toutes les publications'
} %}
{% if has_tags %}
<section class="keyword-cloud">
<div id="keyword-container"></div>
</section>
{% endif %}
<div class="quick-links">
<ul>
<li>
<a href="{{ quick_links.agenda }}">
<p>Agenda</p>
<i class="iconoir-calendar"></i>
</a>
</li>
<li>
<a href="{{ quick_links.contacts }}">
<p>Contacts</p>
<i class="iconoir-mail"></i>
</a>
</li>
<li>
<a href="{{ quick_links.newsletter }}">
<p>Newsletter</p>
<i class="iconoir-message-text"></i>
</a>
</li>
</ul>
</div>
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends "base.twig" %}
{% block content %}
<main class="category-archive">
<div class="container">
<div class="full-block">
<header class="category-header">
<div class="category-header-top">
<nav class="breadcrumb" aria-label="Fil d'Ariane">
<a href="{{ function('home_url', '/') }}">{{ current_language == 'en' ? 'Home' : 'Accueil' }}</a>
<span class="breadcrumb__separator">&rarr;</span>
<span class="breadcrumb__current">{{ current_language == 'en' ? 'Announcements' : 'Annonces' }}</span>
</nav>
</div>
<h1 class="category-header__title">{{ current_language == 'en' ? 'Announcements' : 'Annonces' }}</h1>
</header>
{% include 'partials/category-filters.twig' %}
<section class="subcategory-section">
<div class="post-grid" id="post-grid">
{% for post in posts %}
{% include 'partials/post-card.twig' with { post: post, card: cards[post.ID], show_category: true } %}
{% endfor %}
</div>
<div id="scroll-sentinel"
data-exclude-cats="12,31"
data-filter-cat="{{ active_cat_id }}"
data-filter-autres="{{ filter_autres }}"
data-axe="{{ active_axe }}"
data-date-from="{{ active_date_from }}"
data-date-to="{{ active_date_to }}"></div>
<div id="scroll-spinner" class="scroll-spinner" style="display: none;">
<div class="scroll-spinner__dot"></div>
<div class="scroll-spinner__dot"></div>
<div class="scroll-spinner__dot"></div>
</div>
</section>
</div>
</div>
</main>
{% endblock %}

View File

@@ -0,0 +1,123 @@
{% extends "base.twig" %}
{% block content %}
<div class="article category--le-laboratoire">
<div class="full-block">
<div class="category-header-top">
<nav class="breadcrumb" aria-label="Fil d'Ariane">
<a href="{{ function('home_url', '/') }}">{{ current_language == 'en' ? 'Home' : 'Accueil' }}</a>
<span class="breadcrumb__separator">&rarr;</span>
<a class="breadcrumb__cat" href="{{ current_language == 'en' ? function('home_url', '/en/le-laboratoire/') : function('home_url', '/le-laboratoire/') }}">{{ current_language == 'en' ? 'The department' : 'Le laboratoire' }}</a>
<span class="breadcrumb__separator">&rarr;</span>
<span class="breadcrumb__current">{{ post.title | bilingual(current_language) }}</span>
</nav>
{% if page_edit_link %}
<a href="{{ page_edit_link }}" class="link-button" target="_blank" rel="noopener">
<i class="iconoir-edit-pencil"></i>{{ current_language == 'en' ? 'Edit page' : 'Éditer la page' }}
</a>
{% endif %}
</div>
<h2><p>{{ post.title | bilingual(current_language) }}</p></h2>
{% if images %}
<div class="labo-images">
{% for img in images %}
<figure class="labo-image">
<img src="{{ img.url }}" alt="{{ img.alt }}" loading="lazy">
{% if img.title %}
<figcaption>{{ img.title }}</figcaption>
{% endif %}
</figure>
{% endfor %}
</div>
{% endif %}
<div class="article-content">
{% if liens %}
<div class="sidebar">
<div class="sidebar-container">
<div class="sidebar-section">
{% for lien in liens %}
<a href="{{ lien.url }}" class="link-button">{{ lien.title }}</a>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<div class="main-content-text">
{% if current_language == 'en' and body_en %}
{{ body_en|raw }}
{% else %}
{{ post.content }}
{% endif %}
{% if axes_groups %}
<div class="labo-dropdowns">
{% for group in axes_groups %}
<div class="labo-dropdown-item">
<h3 class="labo-dropdown-header" data-seance-toggle>
{{ group.label }}
<i class="iconoir-nav-arrow-down labo-dropdown-chevron"></i>
</h3>
<div class="labo-dropdown-content" style="display: none;">
<ul class="labo-axes-list">
{% for axe in group.terms %}
<li><a href="{{ axe.url }}">{{ axe.name | bilingual(current_language) }}</a></li>
{% endfor %}
</ul>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% if partenaires_internationaux or partenaires_nationaux %}
<section class="labo-section">
<h3>{{ current_language == 'en' ? 'Partner institutions' : 'Institutions partenaires' }}</h3>
<div class="labo-dropdowns">
{% if partenaires_internationaux %}
<div class="labo-dropdown-item">
<h4 class="labo-dropdown-header" data-seance-toggle>
{{ current_language == 'en' ? 'International partners' : 'Partenaires internationaux' }}
<i class="iconoir-nav-arrow-down labo-dropdown-chevron"></i>
</h4>
<div class="labo-dropdown-content" style="display: none;">
{{ partenaires_internationaux|raw }}
</div>
</div>
{% endif %}
{% if partenaires_nationaux %}
<div class="labo-dropdown-item">
<h4 class="labo-dropdown-header" data-seance-toggle>
{{ current_language == 'en' ? 'National partners' : 'Partenaires nationaux' }}
<i class="iconoir-nav-arrow-down labo-dropdown-chevron"></i>
</h4>
<div class="labo-dropdown-content" style="display: none;">
{{ partenaires_nationaux|raw }}
</div>
</div>
{% endif %}
</div>
</section>
{% endif %}
{% if bibliotheques %}
<section class="labo-section">
<h3>{{ current_language == 'en' ? 'Libraries' : 'Bibliothèques' }}</h3>
<div class="labo-bibliotheques">
{{ bibliotheques|raw }}
</div>
</section>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

107
templates/page-membres.twig Normal file
View File

@@ -0,0 +1,107 @@
{% extends "base.twig" %}
{% block content %}
<main class="category-archive">
<div class="container">
<div class="full-block category--le-laboratoire">
<header class="category-header">
<div class="category-header-top">
<nav class="breadcrumb" aria-label="Fil d'Ariane">
<a href="{{ function('home_url', '/') }}">{{ current_language == 'en' ? 'Home' : 'Accueil' }}</a>
<span class="breadcrumb__separator">&rarr;</span>
<a class="breadcrumb__cat" href="{{ current_language == 'en' ? function('home_url', '/en/le-laboratoire/') : function('home_url', '/le-laboratoire/') }}">{{ current_language == 'en' ? 'The department' : 'Le laboratoire' }}</a>
<span class="breadcrumb__separator">&rarr;</span>
<span class="breadcrumb__current">{{ current_language == 'en' ? 'Lab members' : 'Membres du laboratoire' }}</span>
</nav>
</div>
<h1 class="category-header__title">{{ current_language == 'en' ? 'Lab members' : 'Membres du laboratoire' }}</h1>
</header>
<div class="filters-bar">
<button class="filters-toggle-btn" id="membres-filters-toggle" aria-expanded="false">
{{ current_language == 'en' ? 'Filters' : 'Filtres' }}
<i class="iconoir-nav-arrow-down filters-chevron"></i>
</button>
<div class="filters-active-chips" id="membres-active-chips"></div>
</div>
<div class="category-filters" id="membres-filters">
<div class="filtre-role">
<div class="filter-section-header">
<p class="section-title">{{ current_language == 'en' ? 'Filter by status' : 'Filtrer par statut' }}</p>
<a href="#" class="date-reset-link" id="role-reset" style="display:none">
{{ current_language == 'en' ? 'Reset' : 'Réinitialiser' }}
</a>
</div>
<div class="filter-dd" id="filter-role-dd">
<div class="dd-title" id="filter-role-btn">
<p id="filter-role-label">{{ current_language == 'en' ? 'All statuses' : 'Tous les statuts' }}</p>
<i class="iconoir-nav-arrow-down"></i>
</div>
<div class="dd-content" id="filter-role-popover" style="display:none;">
<ul>
<li data-role="">{{ current_language == 'en' ? 'All statuses' : 'Tous les statuts' }}</li>
{% for role in filter_roles %}
<li data-role="{{ role.name }}">{{ role.name }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="filtre-recherche">
<div class="filter-section-header">
<p class="section-title">{{ current_language == 'en' ? 'Search a member' : 'Rechercher un membre' }}</p>
<a href="#" class="date-reset-link" id="search-reset" style="display:none">
{{ current_language == 'en' ? 'Reset' : 'Réinitialiser' }}
</a>
</div>
<input type="search" id="membres-search" class="membres-search-input"
placeholder="{{ current_language == 'en' ? 'Name…' : 'Nom…' }}"
autocomplete="off">
</div>
</div>
<section class="membres-section">
{% for group in groups %}
<div class="membres-item">
<h3 class="membres-header" data-seance-toggle>
{{ group.title }}
<i class="iconoir-nav-arrow-down membres-chevron"></i>
</h3>
<div class="membres-content" style="display: none;">
<table class="membres-table"{% if group.fixed_order %} data-fixed-order{% endif %}>
<thead>
<tr>
<th data-sort="nom">{{ current_language == 'en' ? 'Name' : 'Nom' }} <i class="iconoir-nav-arrow-down membres-sort-chevron"></i></th>
<th data-sort="statut">{{ current_language == 'en' ? 'Status' : 'Statut' }} <i class="iconoir-nav-arrow-down membres-sort-chevron"></i></th>
<th data-sort="affiliation">{{ current_language == 'en' ? 'Affiliation' : 'Affiliation' }} <i class="iconoir-nav-arrow-down membres-sort-chevron"></i></th>
</tr>
</thead>
<tbody>
{% for member in group.members %}
<tr onclick="window.location.href='{{ member.url }}'"
data-name="{{ member.display_name }}"
data-sort-name="{{ member.sort_key }}"
data-roles="{{ member.role_names|join('|') }}"
data-avatar="{{ member.avatar_url }}"
data-status="{{ member.status }}"
data-affiliation="{{ member.affiliation }}"
data-domaines="{{ member.domaines|join(', ') }}"
data-autres-domaines="{{ member.autres_domaines }}">
<td>{{ member.display_name }}</td>
<td>{{ member.status }}</td>
<td>{{ member.affiliation }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</section>
</div>
</div>
</main>
{% endblock %}

View File

@@ -0,0 +1,60 @@
{% extends "base.twig" %}
{% block content %}
<div class="article category--le-laboratoire">
<div class="full-block">
<div class="category-header-top">
<nav class="breadcrumb" aria-label="Fil d'Ariane">
<a href="{{ function('home_url', '/') }}">{{ current_language == 'en' ? 'Home' : 'Accueil' }}</a>
<span class="breadcrumb__separator">&rarr;</span>
<a class="breadcrumb__cat" href="{{ current_language == 'en' ? function('home_url', '/en/le-laboratoire/') : function('home_url', '/le-laboratoire/') }}">{{ current_language == 'en' ? 'The department' : 'Le laboratoire' }}</a>
<span class="breadcrumb__separator">&rarr;</span>
<span class="breadcrumb__current">{{ current_language == 'en' ? 'Research programs' : 'Programmes de recherche' }}</span>
</nav>
{% if page_edit_link %}
<a href="{{ page_edit_link }}" class="link-button" target="_blank" rel="noopener">
<i class="iconoir-edit-pencil"></i>{{ current_language == 'en' ? 'Edit page' : 'Éditer la page' }}
</a>
{% endif %}
</div>
<h2><p>{{ post.title | bilingual(current_language) }}</p></h2>
<div class="article-content">
<div class="main-content-text">
{% for section in sections %}
{% if section.items %}
<section class="labo-section">
<h3>{{ section.label }}</h3>
<div class="labo-dropdowns">
{% for programme in section.items %}
<div class="labo-dropdown-item">
<h4 class="labo-dropdown-header" data-seance-toggle>
{{ programme.name }}
<i class="iconoir-nav-arrow-down labo-dropdown-chevron"></i>
</h4>
<div class="labo-dropdown-content" style="display: none;">
{% if programme.description %}
<div class="programme-description">{{ programme.description|raw }}</div>
{% endif %}
<div class="programme-link">
<a href="{{ programme.url }}" class="link-button">
<i class="iconoir-open-new-window"></i>{{ current_language == 'en' ? 'View programme' : 'Voir le programme' }}
</a>
</div>
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}

30
templates/page.twig Normal file
View File

@@ -0,0 +1,30 @@
{% extends "base.twig" %}
{% block content %}
<div class="article category--le-laboratoire">
<div class="full-block">
<div class="category-header-top">
<nav class="breadcrumb" aria-label="Fil d'Ariane">
<a href="{{ function('home_url', '/') }}">{{ current_language == 'en' ? 'Home' : 'Accueil' }}</a>
<span class="breadcrumb__separator">&rarr;</span>
<a class="breadcrumb__cat" href="{{ current_language == 'en' ? function('home_url', '/en/le-laboratoire/') : function('home_url', '/le-laboratoire/') }}">{{ current_language == 'en' ? 'The department' : 'Le laboratoire' }}</a>
</nav>
{% if page_edit_link %}
<a href="{{ page_edit_link }}" class="link-button" target="_blank" rel="noopener">
<i class="iconoir-edit-pencil"></i>{{ current_language == 'en' ? 'Edit page' : 'Éditer la page' }}
</a>
{% endif %}
</div>
<h2><p>{{ post.title | bilingual(current_language) }}</p></h2>
<div class="article-content">
<div class="main-content-text">
{{ post.content }}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,25 @@
<a href="{{ link ?: post.link }}" class="swiper-slide agenda-card" dir="ltr">
<div class="agenda-card__dates">
<div class="agenda-date-box">
<span class="agenda-date-day">{{ day }}{% if day == 1 %}<sup>er</sup>{% endif %}</span>
<span class="agenda-date-month">{{ month }}</span>
<span class="agenda-date-year">{{ year }}</span>
</div>
{% if end_day %}
<span class="agenda-date-arrow">→</span>
<div class="agenda-date-box">
<span class="agenda-date-day">{{ end_day }}</span>
<span class="agenda-date-month">{{ end_month }}</span>
<span class="agenda-date-year">{{ end_year }}</span>
</div>
{% endif %}
</div>
<div class="agenda-card__body">
<div class="agenda-card__meta">
{% if date_label %}<span class="agenda-card__date-label">{{ date_label }}</span>{% endif %}
{% if type_label %}<span>{{ type_label }}</span>{% endif %}
{% if lieu %}<span>{{ lieu }}</span>{% endif %}
</div>
<p class="agenda-card__title">{{ post.title }}</p>
</div>
</a>

View File

@@ -0,0 +1,18 @@
<article class="author-card">
<a href="{{ author.url }}" class="author-card__visual" tabindex="-1" aria-hidden="true">
{% if author.avatar_url %}
<img src="{{ author.avatar_url }}" alt="{{ author.name }}" loading="lazy">
{% else %}
<span class="author-card__initials">{{ author.initials }}</span>
{% endif %}
</a>
<div class="author-card__info">
<h2 class="author-card__name"><a href="{{ author.url }}">{{ author.name }}</a></h2>
{% if author.role_label %}
<p class="author-card__role">{{ author.role_label }}</p>
{% endif %}
{% if author.affiliation %}
<p class="author-card__affiliation">{{ author.affiliation }}</p>
{% endif %}
</div>
</article>

View File

@@ -0,0 +1,171 @@
<div class="filters-bar">
<button class="filters-toggle-btn" id="category-filters-toggle" aria-expanded="false">
{{ current_language == 'en' ? 'Filters' : 'Filtres' }}
<i class="iconoir-nav-arrow-down filters-chevron"></i>
</button>
<div class="filters-active-chips">
{% if filter_parents is defined and active_rubrique %}
{% for parent in filter_parents %}
{% if parent.id == active_rubrique %}
<a href="{{ annonces_url }}" class="filter-chip">{{ parent.name }}<i class="iconoir-xmark"></i></a>
{% endif %}
{% endfor %}
{% endif %}
{% if filter_categories is defined and active_category_id and active_category_id != active_rubrique %}
{% for cat in filter_categories %}
{% if cat.id == active_category_id %}
{% set cat_reset_url = annonces_url %}
{% if filter_parents is defined %}
{% for parent in filter_parents %}
{% if parent.id == active_rubrique %}{% set cat_reset_url = parent.link %}{% endif %}
{% endfor %}
{% endif %}
<a href="{{ cat_reset_url }}" class="filter-chip">{{ cat.name }}<i class="iconoir-xmark"></i></a>
{% endif %}
{% endfor %}
{% endif %}
{% if active_date_from or active_date_to %}
<a href="{{ function('remove_query_arg', ['date_from', 'date_to'])|en_url }}" class="filter-chip">
{%- if active_date_from %}{{ active_date_from|date('d/m/Y') }}{% endif -%}
{{- active_date_from and active_date_to ? ' → ' : '' -}}
{%- if active_date_to %}{{ active_date_to|date('d/m/Y') }}{% endif -%}
<i class="iconoir-xmark"></i>
</a>
{% endif %}
{% if active_axe is defined and active_axe and filter_axes is defined %}
{% for axe in filter_axes %}
{% if axe.id == active_axe %}
<a href="{{ (axe_taxonomy_mode ? annonces_url : function('remove_query_arg', ['axe']))|en_url }}" class="filter-chip">{{ axe.name }}<i class="iconoir-xmark"></i></a>
{% endif %}
{% endfor %}
{% endif %}
</div>
</div>
<div class="category-filters" id="category-filters">
{% if filter_parents is defined and filter_parents %}
<div class="filtre-rubrique">
<div class="filter-section-header">
<p class="section-title">{{ current_language == 'en' ? 'Filter by section' : 'Filtrer par rubrique' }}</p>
{% if active_rubrique %}
<a href="{{ annonces_url }}" class="date-reset-link">
{{ current_language == 'en' ? 'Reset' : 'Réinitialiser' }}
</a>
{% endif %}
</div>
<ul>
{% for parent in filter_parents %}
<li{% if parent.id == active_rubrique %} class="is-active"{% endif %}>
<a href="{{ parent.link }}">{{ parent.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if filter_categories is defined and filter_categories %}
<div class="filtre-categorie">
<div class="filter-section-header">
<p class="section-title">{{ current_language == 'en' ? 'Filter by category' : 'Filtrer par catégorie' }}</p>
{% if active_category_id and active_category_id != active_rubrique %}
{% for parent in filter_parents %}
{% if parent.id == active_rubrique %}
<a href="{{ parent.link }}" class="date-reset-link">
{{ current_language == 'en' ? 'Reset' : 'Réinitialiser' }}
</a>
{% endif %}
{% endfor %}
{% endif %}
</div>
<ul>
{% for cat in filter_categories %}
<li{% if cat.id == active_category_id %} class="is-active"{% endif %}>
<a href="{{ cat.link }}">{{ cat.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="filtre-date">
<div class="filter-section-header">
<p class="section-title">{{ current_language == 'en' ? 'Filter by date' : 'Filtrer par date' }}</p>
{% if active_date_from or active_date_to %}
<a href="{{ function('remove_query_arg', ['date_from', 'date_to'])|en_url }}" class="date-reset-link">
{{ current_language == 'en' ? 'Reset' : 'Réinitialiser' }}
</a>
{% endif %}
</div>
<div class="filter-dd{% if active_date_from or active_date_to %} is-active{% endif %}" id="filter-date-dd">
<div class="dd-title" id="filter-date-btn">
<p id="filter-date-label">
{%- if active_date_from or active_date_to -%}
{%- if active_date_from -%}{{ active_date_from|date('d/m/Y') }}{%- endif -%}
{{- active_date_from and active_date_to ? ' → ' : '' -}}
{%- if active_date_to -%}{{ active_date_to|date('d/m/Y') }}{%- endif -%}
{%- else -%}
{{ current_language == 'en' ? 'Show all' : 'Tout afficher' }}
{%- endif -%}
</p>
<i class="iconoir-nav-arrow-down"></i>
</div>
<div class="dd-content" id="filter-date-popover" style="display: none;">
<ul>
<li data-preset="week">{{ current_language == 'en' ? 'This week' : 'Cette semaine' }}</li>
<li data-preset="month">{{ current_language == 'en' ? 'This month' : 'Ce mois-ci' }}</li>
<li data-preset="upcoming">{{ current_language == 'en' ? 'Upcoming' : 'À venir' }}</li>
<li data-preset="lastmonth">{{ current_language == 'en' ? 'Last month' : 'Le mois dernier' }}</li>
</ul>
<div class="dd-date-fields">
<label>
{{ current_language == 'en' ? 'From' : 'De' }}
<input type="date" id="filter-date-from" value="{{ active_date_from }}" lang="{{ current_language == 'en' ? 'en-GB' : 'fr-FR' }}">
</label>
<label>
{{ current_language == 'en' ? 'To' : 'À' }}
<input type="date" id="filter-date-to" value="{{ active_date_to }}" lang="{{ current_language == 'en' ? 'en-GB' : 'fr-FR' }}">
</label>
<button type="button" class="link-button dd-date-apply" id="filter-date-apply">
{{ current_language == 'en' ? 'Apply' : 'Appliquer' }}
</button>
</div>
</div>
</div>
</div>
{% if filter_axes is defined and filter_axes %}
<div class="filtre-axe">
<div class="filter-section-header">
<p class="section-title">{{ current_language == 'en' ? 'Filter by thematic axis' : 'Filtrer par axe thématique' }}</p>
{% if active_axe and not axe_taxonomy_mode %}
<a href="{{ function('remove_query_arg', ['axe'])|en_url }}" class="date-reset-link">
{{ current_language == 'en' ? 'Reset' : 'Réinitialiser' }}
</a>
{% endif %}
</div>
<div class="filter-dd{% if active_axe %} is-active{% endif %}" id="filter-axe-dd">
<div class="dd-title" id="filter-axe-btn">
<p id="filter-axe-label">
{%- set axe_label = current_language == 'en' ? 'All axes' : 'Tous les axes' -%}
{%- for axe in filter_axes -%}
{%- if axe.id == active_axe -%}
{%- set axe_label = axe.name -%}
{%- endif -%}
{%- endfor -%}
{{ axe_label }}
</p>
<i class="iconoir-nav-arrow-down"></i>
</div>
<div class="dd-content" id="filter-axe-popover" style="display: none;">
<ul>
{% if not axe_taxonomy_mode %}
<li data-axe-id="">{{ current_language == 'en' ? 'All axes' : 'Tous les axes' }}</li>
{% endif %}
{% for axe in filter_axes %}
<li data-axe-id="{{ axe.id }}"{% if not axe_stay_on_page %} data-axe-href="{{ axe.href }}"{% endif %}>{{ axe.name }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,24 @@
<footer>
<div class="footer-content">
<nav class="footer-nav">
<ul id="{{ current_language == 'en' ? 'menu-footer-en' : 'menu-footer' }}">
{% for item in footer_menu.items %}
<li class="{{ item.classes|join(' ') }}">
<a href="{{ item.url }}">{{ item.title | bilingual(current_language) }}</a>
</li>
{% endfor %}
</ul>
</nav>
<div class="footer-logos">
<a href="https://www.cnrs.fr/" target="_blank">
<img src="{{ theme.uri }}/assets/images/cnrs.png" alt="Logo CNRS">
</a>
<a href="https://www.sorbonne-nouvelle.fr/" target="_blank">
<img src="{{ theme.uri }}/assets/images/sorbonne.png" alt="Logo Sorbonne Nouvelle">
</a>
<a href="https://www.ens.psl.eu/" target="_blank">
<img src="{{ theme.uri }}/assets/images/ens.png" alt="Logo ENS">
</a>
</div>
</div>
</footer>

View File

@@ -0,0 +1,57 @@
<header>
<div class="header-left">
<div class="main-logo-container">
<div class="main-logo">
<a href="{{ current_language == 'en' ? function('home_url', '/en/') : site.url }}">
<img src="{{ theme.uri }}/assets/images/thalim-logo.svg" alt="Logo Thalim">
</a>
</div>
</div>
<div class="description">
<div>{{ gc.umr }}</div>
<div>{{ gc.thalim }}</div>
<div>{{ gc.siecles }}</div>
</div>
</div>
<div class="header-right">
<div class="secondary-logo-container">
<div class="main-logo">
<a href="{{ current_language == 'en' ? function('home_url', '/en/') : site.url }}">
<img src="{{ theme.uri }}/assets/images/thalim-logo.svg" alt="Logo Thalim">
</a>
</div>
</div>
<div class="lang-switch">
{% if languages %}
<ul class="language-switcher">
{% for lang in languages %}
<li class="{{ lang.current_lang ? 'active' : '' }}">
<a href="{{ lang.url }}">
{{ lang.slug == 'fr' ? 'Français' : 'English' }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="search-button">
<div>
<i class="iconoir-search"></i>
</div>
</div>
<div class="menu-toggle">
<div>
<div>
<i class="iconoir-menu"></i>
</div>
<p>Menu</p>
</div>
</div>
</div>
</header>
<div class="overlay"></div>
{% include 'partials/search-panel.twig' %}
{% include 'partials/navigation.twig' %}

View File

@@ -0,0 +1,34 @@
<nav class="main-menu">
<div class="{{ current_language == 'en' ? 'menu-navigation-en-container' : 'menu-navigation-container' }}">
<ul>
{% for item in menu.items %}
{% set is_first_col = loop.index == 1 %}
<li class="{{ item.classes|join(' ') }}">
<a href="{{ item.url }}">{{ item.title | bilingual(current_language) }}</a>
{% if item.children %}
<ul class="sub-menu">
{% for child in item.children %}
<li class="{{ child.classes|join(' ') }}">
<a href="{{ child.url }}">{{ child.title | bilingual(current_language) }}</a>
</li>
{% if is_first_col and loop.index == 2 and axes_courants %}
<li class="nav-axes-item">
<div class="nav-axes-trigger" aria-expanded="false">
{{ current_language == 'en' ? 'Thematic axes' : 'Axes thématiques' }}
<i class="iconoir-nav-arrow-down"></i>
</div>
<ul class="nav-axes-list">
{% for axe in axes_courants %}
<li><a href="{{ axe.link }}">{{ axe.name }}</a></li>
{% endfor %}
</ul>
</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</nav>

View File

@@ -0,0 +1,116 @@
<article class="post-card gradient--{{ card.parent_slug }}">
<a href="{{ card.card_link ?: post.link }}" class="gradient-container">
{% if card.card_image %}
<img src="{{ card.card_image }}" alt="{{ post.title|bilingual(current_language) }}" loading="lazy">
{% else %}
<h2>{{ post.title|bilingual(current_language) }}</h2>
{% endif %}
</a>
<div class="contextual-infos">
<div class="authors">
{% set autres = post.meta('autrepersonnes') %}
{% set autres_list = autres ? autres|split(', ') : [] %}
{% set membres_count = card.card_membres|length %}
{% set total = membres_count + autres_list|length %}
{% set slots_left = 3 - membres_count %}
{% for membre in card.card_membres|slice(0, 3) %}
<span><a href="{{ membre.url }}">{{ membre.name }}</a></span>
{% endfor %}
{% if slots_left > 0 and autres_list|length > 0 %}
<span>{{ autres_list|slice(0, slots_left)|join(', ') }}</span>
{% endif %}
{% if total > 3 %}{% endif %}
</div>
{% if show_category %}
<div class="date-category">
<time class="date" datetime="{{ card.card_event_date_iso ?: post.date('Y-m-d') }}">{{ card.card_event_date ?: post.date('d/m/Y') }}</time>
{% if card.card_type %}
<a class="card-type" href="{{ card.card_category_url }}">{{ card.card_type }}</a>
{% elseif card.card_category_name and not type_only %}
<a class="card-type" href="{{ card.card_category_url }}">{{ card.card_category_name }}</a>
{% endif %}
</div>
{% else %}
<time class="date" datetime="{{ card.card_event_date_iso ?: post.date('Y-m-d') }}">{{ card.card_event_date ?: post.date('d/m/Y') }}</time>
{% endif %}
</div>
{% if card.card_image %}
<h2 class="title-bottom">
<a href="{{ card.card_link ?: post.link }}">{{ post.title|bilingual(current_language) }}</a>
</h2>
{% endif %}
</article>
{#
<article class="post-card">
{% if card.card_image %}
<div class="post-card__image">
<img src="{{ card.card_image }}" alt="{{ post.title }}" loading="lazy">
</div>
{% endif %}
<div class="post-card__content">
<h3 class="post-card__title">
<a href="{{ post.link }}">{{ post.title }}</a>
</h3>
{% if post.meta('sous-titre') %}
<p class="post-card__subtitle">{{ post.meta('sous-titre') }}</p>
{% endif %}
<time class="post-card__date" datetime="{{ post.date('Y-m-d') }}">{{ post.date('d/m/Y') }}</time>
{% if card.card_membres is not empty or post.meta('autrepersonnes') %}
<div class="post-card__authors">
{% for name in card.card_membres %}
<span class="post-card__author">{{ name }}</span>
{% endfor %}
{% if post.meta('autrepersonnes') %}
<span class="post-card__author post-card__author--external">{{ post.meta('autrepersonnes') }}</span>
{% endif %}
</div>
{% endif %}
{% if post.meta('fonction_auteur') %}
<span class="post-card__role">{{ post.meta('fonction_auteur') }}</span>
{% endif %}
{% if post.meta('editeur') %}
<span class="post-card__publisher">{{ post.meta('editeur') }}</span>
{% endif %}
{% if post.meta('journal') %}
<span class="post-card__journal">{{ post.meta('journal') }}</span>
{% endif %}
{% if card.card_axes is not empty %}
<div class="post-card__axes">
{% for axe in card.card_axes %}
<span class="post-card__axe">{{ axe }}</span>
{% endfor %}
</div>
{% endif %}
{% if card.card_etiquettes is not empty %}
<div class="post-card__tags">
{% for tag in card.card_etiquettes %}
<span class="post-card__tag">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
<div class="post-card__links">
{% if post.meta('lien_externe_1') %}
<a href="{{ post.meta('lien_externe_1') }}" class="post-card__link" target="_blank" rel="noopener">
{{ post.meta('titre_du_lien_externe_1') ?: post.meta('lien_externe_1') }}
</a>
{% endif %}
{% if post.meta('hal_url') %}
<a href="{{ post.meta('hal_url') }}" class="post-card__link post-card__link--hal" target="_blank" rel="noopener">
HAL
</a>
{% endif %}
</div>
</div>
</article>
#}

View File

@@ -0,0 +1,17 @@
<div class="search-panel">
<div class="search-panel__inner">
<p class="search-panel__title">{{ current_language == 'en' ? 'Search the site' : 'Rechercher sur le site' }}</p>
<p class="search-panel__desc">{{ current_language == 'en' ? 'Search for an event, a lab member, a publication…' : 'Rechercher un événement, un membre du laboratoire, un ouvrage…' }}</p>
<form class="search-panel__form" role="search" method="get" action="{{ function('home_url', current_language == 'en' ? '/en/' : '/') }}">
<div class="search-panel__input-wrap">
<input type="search" name="s" class="search-panel__input"
placeholder="{{ current_language == 'en' ? 'Type your search…' : 'Écrire la recherche…' }}"
autocomplete="off">
<button type="submit" class="search-panel__icon-btn" aria-label="{{ current_language == 'en' ? 'Search' : 'Rechercher sur le site' }}"><i class="iconoir-search"></i></button>
</div>
<button type="submit" class="search-panel__submit">
{{ current_language == 'en' ? 'Search' : 'Rechercher sur le site' }}
</button>
</form>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More