Mode clair / sombre et thèmes de saison

This commit is contained in:
Yasser Boulayoune 2026-10-08 16:26:00 +02:00
parent da05adede1
commit 17cf2c8380
19 changed files with 3563 additions and 0 deletions

291
functions.php Normal file
View file

@ -0,0 +1,291 @@
<?php
/**
* yb2026 : thème portfolio « sombre & premium »
* v3 : porté depuis handoff/reference/portfolio-site-v3.html (+ theme-assets/)
*/
/* ── Assets ─────────────────────────────────────────────────────────── */
function yb2026_assets()
{
$v = '3.0';
$uri = get_template_directory_uri();
/* Polices hébergées sur le serveur (plus d'appel à Google Fonts : pas de transfert d'IP des visiteurs) */
wp_enqueue_style('yb-fonts', $uri . '/assets/fonts/fonts.css', array(), '1');
wp_enqueue_style('yb-effects', $uri . '/assets/css/effects.css', array(), $v);
wp_enqueue_style('yb-style', $uri . '/assets/css/style.css', array('yb-effects'), $v);
/* Ordre : cursor + orb définissent YBCursor/YBOrb, effects.js les initialise */
wp_enqueue_script('yb-cursor', $uri . '/assets/js/cursor.js', array(), $v, true);
if (is_front_page()) {
wp_enqueue_script('yb-orb', $uri . '/assets/js/orb.js', array(), $v, true);
}
wp_enqueue_script('yb-effects', $uri . '/assets/js/effects.js', array('yb-cursor'), $v, true);
if (is_page('qui-suis-je')) {
wp_enqueue_script('yb-about', $uri . '/assets/js/about.js', array('yb-effects'), $v . '.1', true);
}
if (is_page('competences')) {
wp_enqueue_script('yb-competences', $uri . '/assets/js/competences.js', array('yb-effects'), $v . '.1', true);
}
}
add_action('wp_enqueue_scripts', 'yb2026_assets');
require_once get_template_directory() . '/inc/profile.php';
require_once get_template_directory() . '/inc/proofs.php';
require_once get_template_directory() . '/inc/seasons.php';
require_once get_template_directory() . '/inc/errors.php';
add_theme_support('title-tag');
add_theme_support('post-thumbnails');
add_theme_support('html5', array('style', 'script'));
/* ── Custom Post Types (repris du thème précédent, mêmes rewrites) ──── */
function yb2026_post_types()
{
register_post_type('realisation', array(
'labels' => array(
'name' => 'Réalisations',
'singular_name' => 'Réalisation',
'add_new_item' => 'Ajouter une réalisation',
'edit_item' => 'Modifier la réalisation',
),
'public' => true,
'has_archive' => false,
'supports' => array('title', 'editor', 'thumbnail', 'excerpt'),
'menu_icon' => 'dashicons-portfolio',
'show_in_rest' => true,
'rewrite' => array('slug' => 'realisations'),
));
register_post_type('service', array(
'labels' => array(
'name' => 'Services',
'singular_name' => 'Service',
'add_new_item' => 'Ajouter un service',
'edit_item' => 'Modifier le service',
),
'public' => true,
'has_archive' => false,
'supports' => array('title', 'editor', 'thumbnail', 'excerpt'),
'menu_icon' => 'dashicons-portfolio',
'show_in_rest' => true,
'rewrite' => array('slug' => 'services'),
));
}
add_action('init', 'yb2026_post_types');
function yb2026_flush()
{
yb2026_post_types();
flush_rewrite_rules();
}
add_action('after_switch_theme', 'yb2026_flush');
/* ── Meta-box réalisation : champs de l'étude de cas ────────────────── */
function yb2026_add_metaboxes()
{
add_meta_box('yb_real_meta', 'Étude de cas (design v2)', 'yb2026_render_real_metabox', 'realisation', 'normal', 'high');
}
add_action('add_meta_boxes', 'yb2026_add_metaboxes');
function yb2026_render_real_metabox($post)
{
wp_nonce_field('yb_real_nonce_action', 'yb_real_nonce');
$f = function ($k) use ($post) { return esc_attr(get_post_meta($post->ID, $k, true)); };
$sections = get_post_meta($post->ID, '_yb_sections', true);
$sections_txt = '';
if ($sections) {
$arr = json_decode($sections, true);
if (is_array($arr)) {
foreach ($arr as $s) {
$sections_txt .= '/ ' . $s['h'] . "\n" . $s['p'] . "\n\n";
}
}
}
?>
<style>.yb-mb label{font-weight:600;display:block;margin:12px 0 4px}.yb-mb input,.yb-mb textarea{width:100%}</style>
<div class="yb-mb">
<label for="yb_order">Ordre (01–09, utilisé pour la numérotation et le tri)</label>
<input type="number" id="yb_order" name="yb_order" value="<?php echo $f('_yb_order'); ?>" min="1" max="99">
<label for="yb_tag">Stack / tag (affiché en mono lime, ex. « DEBIAN 12 · DOCKER · WIREGUARD »)</label>
<input type="text" id="yb_tag" name="yb_tag" value="<?php echo $f('_yb_tag'); ?>">
<label for="yb_year">Année (ex. « 2024–2026 »)</label>
<input type="text" id="yb_year" name="yb_year" value="<?php echo $f('_yb_year'); ?>">
<label for="yb_role">Rôle (ex. « Admin Sys & Réseaux »)</label>
<input type="text" id="yb_role" name="yb_role" value="<?php echo $f('_yb_role'); ?>">
<label for="yb_short">Description courte (cartes)</label>
<input type="text" id="yb_short" name="yb_short" value="<?php echo $f('_yb_short'); ?>">
<label for="yb_desc">Description longue (intro de l'étude de cas)</label>
<textarea id="yb_desc" name="yb_desc" rows="3"><?php echo esc_textarea(get_post_meta($post->ID, '_yb_desc', true)); ?></textarea>
<label for="yb_note">Note terminal (bordure lime, ex. « $ uptime · en production depuis 2 ans »)</label>
<input type="text" id="yb_note" name="yb_note" value="<?php echo $f('_yb_note'); ?>">
<label for="yb_sections">Sections de l'étude de cas : format : ligne « / Titre » puis paragraphe, sections séparées par une ligne vide. Laisser vide pour afficher le contenu de l'éditeur à la place.</label>
<textarea id="yb_sections" name="yb_sections" rows="12" placeholder="/ Le point de départ&#10;Texte du paragraphe…&#10;&#10;/ L'architecture&#10;Texte…"><?php echo esc_textarea($sections_txt); ?></textarea>
</div>
<?php
}
function yb2026_save_metaboxes($post_id)
{
if (
!isset($_POST['yb_real_nonce']) ||
!wp_verify_nonce($_POST['yb_real_nonce'], 'yb_real_nonce_action') ||
!current_user_can('edit_post', $post_id)
) return;
foreach (array('yb_order' => '_yb_order', 'yb_tag' => '_yb_tag', 'yb_year' => '_yb_year', 'yb_role' => '_yb_role', 'yb_short' => '_yb_short', 'yb_note' => '_yb_note') as $field => $key) {
if (isset($_POST[$field])) {
update_post_meta($post_id, $key, sanitize_text_field($_POST[$field]));
}
}
if (isset($_POST['yb_desc'])) {
update_post_meta($post_id, '_yb_desc', sanitize_textarea_field($_POST['yb_desc']));
}
if (isset($_POST['yb_sections'])) {
$raw = str_replace("\r\n", "\n", (string) $_POST['yb_sections']);
$out = array();
foreach (preg_split('/\n\s*\n/', trim($raw)) as $block) {
$lines = explode("\n", trim($block));
if (count($lines) < 2 || strpos($lines[0], '/') !== 0) continue;
$out[] = array(
'h' => sanitize_text_field(ltrim(substr($lines[0], 1))),
'p' => sanitize_textarea_field(implode(' ', array_slice($lines, 1))),
);
}
update_post_meta($post_id, '_yb_sections', $out ? wp_json_encode($out, JSON_UNESCAPED_UNICODE) : '');
}
}
add_action('save_post_realisation', 'yb2026_save_metaboxes');
/* ── Helpers ─────────────────────────────────────────────────────────── */
/** Toutes les réalisations triées par _yb_order (fallback date). */
function yb2026_get_projects($limit = -1)
{
$q = new WP_Query(array(
'post_type' => 'realisation',
'posts_per_page' => $limit,
'meta_key' => '_yb_order',
'orderby' => array('meta_value_num' => 'ASC', 'date' => 'DESC'),
'no_found_rows' => true,
));
return $q->posts;
}
/** Numéro « 01 » d'une réalisation. */
function yb2026_num($post_id)
{
$o = (int) get_post_meta($post_id, '_yb_order', true);
return str_pad($o ? $o : 1, 2, '0', STR_PAD_LEFT);
}
/** Nom court d'un projet : partie avant le « : » du titre WP (ancien séparateur « : » toléré). */
function yb2026_short_title($p)
{
$t = get_the_title($p);
$parts = preg_split('/\s+[:\x{2014}]\s+/u', $t, 2);
return $parts[0];
}
/** Carte projet (piste des travaux sur l'accueil + grille réalisations).
Classes .card / .card-img attendues par effects.css et effects.js (compteur, hover N&B). */
function yb2026_project_card($p, $home = false)
{
$tag = get_post_meta($p->ID, '_yb_tag', true);
if (!$tag) $tag = strtoupper((string) get_post_meta($p->ID, '_real_technologies', true));
$short = get_post_meta($p->ID, '_yb_short', true);
if (!$short) $short = get_the_excerpt($p);
$year = get_post_meta($p->ID, '_yb_year', true);
$name = yb2026_short_title($p);
?>
<a class="card<?php echo $home ? ' card--home' : ''; ?>" href="<?php echo esc_url(get_permalink($p)); ?>" data-cursor-label="VOIR LE PROJET" data-wipe="<?php echo esc_attr(mb_strtoupper($name)); ?>"<?php echo $home ? '' : ' data-reveal'; ?>>
<span class="card-img">
<?php echo get_the_post_thumbnail($p, 'large', array('loading' => 'lazy', 'alt' => esc_attr($name))); ?>
<span class="card-num"><?php echo esc_html(yb2026_num($p->ID)); ?></span>
<?php if ($home && $year) : ?><span class="card-year"><?php echo esc_html($year); ?></span><?php endif; ?>
</span>
<span class="card-body">
<span class="card-head"><span class="card-name"><?php echo esc_html($name); ?></span><span class="card-arrow">→</span></span>
<span class="card-tag"><?php echo esc_html($tag); ?></span>
<span class="card-short"><?php echo esc_html($short); ?></span>
</span>
</a>
<?php
}
/* ── Noindex partout sauf l'accueil (demande du 06/10/2026) ──────────
Seule la page d'accueil reste indexable ; pages, réalisations, articles,
archives, recherche, 404… sont en noindex. */
add_filter('wp_robots', function ($robots) {
if (is_front_page()) {
return $robots;
}
unset($robots['max-image-preview']);
$robots['noindex'] = true;
$robots['nofollow'] = true;
return $robots;
}, 99);
/* Même consigne pour les réponses non-HTML générées par WordPress (flux RSS, REST…) */
add_action('send_headers', function () {
if (!is_admin() && !is_front_page()) {
header('X-Robots-Tag: noindex, nofollow', true);
}
});
/* ── Anti-doublon : retire du contenu l'image déjà en mise-en-avant ──── */
add_filter('the_content', function ($content) {
if (!is_singular('realisation') || !has_post_thumbnail()) {
return $content;
}
$thumb_url = wp_get_attachment_image_url(get_post_thumbnail_id(), 'full');
if (!$thumb_url) return $content;
$basename = pathinfo(parse_url($thumb_url, PHP_URL_PATH), PATHINFO_FILENAME);
$basename = preg_quote($basename, '#');
$content = preg_replace('#<figure\b[^>]*>(?:(?!</figure>).)*?' . $basename . '(?:(?!</figure>).)*?</figure>#is', '', $content);
$content = preg_replace('#<img\b[^>]*?' . $basename . '[^>]*?/?>#i', '', $content);
$content = preg_replace('#<p>\s*</p>#i', '', $content);
return $content;
}, 20);
/* ── Options pages ACF (shim) ────────────────────────────────────────── */
add_action('acf/init', function () {
if (function_exists('acf_add_options_page')) {
acf_add_options_page(array(
'page_title' => 'Theme General Settings',
'menu_title' => 'Theme Settings',
'menu_slug' => 'theme-general-settings',
'capability' => 'edit_posts',
'redirect' => false,
));
acf_add_options_sub_page(array('page_title' => 'Theme Header Settings', 'menu_title' => 'Header', 'parent_slug' => 'theme-general-settings'));
acf_add_options_sub_page(array('page_title' => 'Theme Social Settings', 'menu_title' => 'Social', 'parent_slug' => 'theme-general-settings'));
acf_add_options_sub_page(array('page_title' => 'Theme Coordonnée Settings', 'menu_title' => 'Coordonnée', 'parent_slug' => 'theme-general-settings'));
}
});
/* Autoptimize : ne pas agréger le script inline anti-flash du wipe
(il doit s'exécuter immédiatement après l'élément #yb-wipe, avant le paint). */
add_filter('autoptimize_filter_js_exclude', function ($exclude) {
return $exclude . ', ybWipe, ybTheme';
});
/* Shortcode hérité (présent dans d'anciens contenus) */
add_shortcode('tavu', function () {
return "<h1>T'a vue ?</h1>";
});
/* Ancienne adresse de la page Compétences : /services/ → /competences/ (301).
Les contenus du CPT « service » gardent leurs adresses /services/<slug>/. */
add_action('template_redirect', function () {
$path = trim((string) parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH), '/');
if ($path === 'services') {
wp_redirect(home_url('/competences/'), 301);
exit;
}
}, 1);
/* ── Ressources tierces : rien n'est chargé depuis un CDN externe côté visiteur ── */
add_action('wp_enqueue_scripts', function () {
wp_dequeue_style('acf-shim-fontawesome'); // icônes Font Awesome de l'admin ACF, inutiles sur le site
}, 100);