Pages Compétences, Contact et Qui suis-je
This commit is contained in:
parent
e06ad42369
commit
da05adede1
10 changed files with 1242 additions and 0 deletions
72
inc/error-template.php
Normal file
72
inc/error-template.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
/* Généré par ~/yb-erreurs/build.py : ne pas modifier à la main. Variables : $yb_code, $yb_title, $yb_msg (HTML sûr). */
|
||||
?><!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="theme-color" content="#0a0a0a">
|
||||
<title><?php echo esc_html($yb_code . ' · ' . $yb_title); ?> · Yasser Boulayoune</title>
|
||||
<script>try{var t=localStorage.getItem('yb-theme');if(t==='light'||t==='dark')document.documentElement.setAttribute('data-theme',t)}catch(e){}</script>
|
||||
<style>@font-face{font-family:'Archivo';font-weight:100 900;font-stretch:62% 125%;font-display:swap;src:url(/wp-content/themes/yb2026/assets/fonts/Archivo-100-900-latin.woff2) format('woff2')}
|
||||
@font-face{font-family:'JetBrains Mono';font-weight:400 700;font-display:swap;src:url(/wp-content/themes/yb2026/assets/fonts/JetBrainsMono-400-latin.woff2) format('woff2')}
|
||||
:root{--bg:#0a0a0a;--ink:#f5f5f0;--lime:#d3fd50;--ink-rgb:245,245,240;--lime-rgb:211,253,80;--mut:rgba(245,245,240,.55);--b:rgba(245,245,240,.12);color-scheme:dark}
|
||||
@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){--bg:#f3f2ed;--ink:#111;--lime:#4a7300;--ink-rgb:17,17,17;--lime-rgb:74,115,0;--mut:rgba(17,17,17,.55);--b:rgba(17,17,17,.12);color-scheme:light}}
|
||||
:root[data-theme="light"]{--bg:#f3f2ed;--ink:#111;--lime:#4a7300;--ink-rgb:17,17,17;--lime-rgb:74,115,0;--mut:rgba(17,17,17,.55);--b:rgba(17,17,17,.12);color-scheme:light}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body{min-height:100%;background:var(--bg);overflow-x:clip}
|
||||
body{color:var(--ink);font:400 16px/1.6 'Archivo',system-ui,sans-serif;display:flex;flex-direction:column;min-height:100vh;padding:0 clamp(20px,4vw,44px);overflow-x:hidden}
|
||||
::selection{background:var(--lime);color:var(--bg)}
|
||||
a{color:inherit;text-decoration:none}
|
||||
.grain{position:fixed;inset:0;pointer-events:none;opacity:.05;z-index:5;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)'/%3E%3C/svg%3E")}
|
||||
.top{display:flex;justify-content:space-between;align-items:center;padding:22px 0;border-bottom:1px solid var(--b);font:700 14px 'JetBrains Mono',monospace;letter-spacing:.05em}
|
||||
.top b{color:var(--lime);font-weight:700}
|
||||
.top span{font-weight:500;font-size:11.5px;letter-spacing:.18em;color:var(--mut)}
|
||||
main{flex:1;display:flex;flex-direction:column;justify-content:center;gap:22px;padding:48px 0;position:relative;min-width:0}
|
||||
.ring{position:absolute;right:-6vw;top:50%;width:min(56vw,620px);aspect-ratio:1;transform:translateY(-50%);border-radius:50%;pointer-events:none;
|
||||
background:radial-gradient(circle,rgba(var(--lime-rgb),.10),transparent 62%);border:1px dashed rgba(var(--lime-rgb),.22);animation:spin 60s linear infinite}
|
||||
.ring::after{content:'';position:absolute;inset:16%;border-radius:50%;border:1px solid rgba(var(--ink-rgb),.08)}
|
||||
@keyframes spin{to{transform:translateY(-50%) rotate(360deg)}}
|
||||
.kick{font:700 12px 'JetBrains Mono',monospace;letter-spacing:.22em;color:var(--lime)}
|
||||
.code{font:900 clamp(110px,26vw,300px)/.82 'Archivo',sans-serif;font-stretch:118%;letter-spacing:-.05em;display:flex;gap:.04em;user-select:none}
|
||||
.code span:nth-child(odd){color:var(--ink)}
|
||||
.code span:nth-child(even){color:transparent;-webkit-text-stroke:2px var(--lime)}
|
||||
h1{font:800 clamp(28px,4.4vw,52px)/1.05 'Archivo',sans-serif;letter-spacing:-.025em;text-wrap:balance;max-width:16ch}
|
||||
p.msg{max-width:52ch;color:rgba(var(--ink-rgb),.78);font-size:17px}
|
||||
.req{font:500 12.5px 'JetBrains Mono',monospace;color:var(--mut);display:flex;gap:10px;flex-wrap:wrap;align-items:center;max-width:100%}
|
||||
.req code{font:inherit;color:var(--ink);background:rgba(var(--ink-rgb),.06);border:1px solid var(--b);padding:3px 8px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.req em{font-style:normal;color:var(--lime)}
|
||||
.actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:6px}
|
||||
.btn{font:700 12.5px 'JetBrains Mono',monospace;letter-spacing:.14em;padding:14px 20px;border-radius:99px;border:1px solid var(--b);transition:transform .25s cubic-bezier(.3,1.5,.5,1),border-color .2s,color .2s,background .2s;cursor:pointer;background:none;color:var(--ink)}
|
||||
.btn:hover{border-color:var(--lime);color:var(--lime);transform:translateY(-2px)}
|
||||
.btn.main{background:var(--lime);border-color:var(--lime);color:var(--bg)}
|
||||
.btn.main:hover{color:var(--bg)}
|
||||
.btn:focus-visible{outline:2px solid var(--lime);outline-offset:3px}
|
||||
footer{padding:20px 0 26px;border-top:1px solid var(--b);font:500 11px 'JetBrains Mono',monospace;letter-spacing:.16em;color:var(--mut);display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
||||
@media (max-width:640px){.ring{right:-30vw;width:90vw}.top span{display:none}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="grain" aria-hidden="true"></div>
|
||||
<header class="top"><a href="/">YB<b>©</b>2026</a><span>YASSER BOULAYOUNE · PORTFOLIO</span></header>
|
||||
<main>
|
||||
<div class="ring" aria-hidden="true"></div>
|
||||
<p class="kick">ERREUR <?php echo esc_html($yb_code); ?></p>
|
||||
<div class="code" aria-hidden="true"><?php foreach (str_split((string) $yb_code) as $d) echo '<span>' . esc_html($d) . '</span>'; ?></div>
|
||||
<h1><?php echo esc_html($yb_title); ?></h1>
|
||||
<p class="msg"><?php echo $yb_msg; ?></p>
|
||||
<p class="req"><span>REQUÊTE</span><code id="yb-path">/</code><em>→ <?php echo esc_html($yb_code); ?></em></p>
|
||||
<div class="actions">
|
||||
<a class="btn main" href="/">ACCUEIL →</a>
|
||||
<button class="btn" type="button" id="yb-back">← RETOUR</button>
|
||||
<a class="btn" href="/contact/">CONTACT</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer><span>© 2026 YASSER BOULAYOUNE</span><span>RÉGION LYONNAISE</span></footer>
|
||||
<script>
|
||||
(function(){var p=document.getElementById('yb-path');if(p)p.textContent=location.pathname+location.search;
|
||||
var b=document.getElementById('yb-back');if(b){if(history.length<2)b.hidden=true;b.addEventListener('click',function(){history.back()});}})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
51
inc/errors.php
Normal file
51
inc/errors.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
/**
|
||||
* Pages d'erreur WordPress au style du portfolio.
|
||||
* - wp_die() côté site public (l'admin, la page de connexion, AJAX/REST gardent l'affichage natif)
|
||||
* - wp-content/php-error.php (erreur fatale PHP) utilise le même modèle : inc/error-template.php
|
||||
* Le modèle est généré par ~/yb-erreurs/build.py (pages Apache et NPM identiques).
|
||||
*/
|
||||
|
||||
function yb2026_error_titles()
|
||||
{
|
||||
return array(400 => 'Requête invalide', 401 => 'Authentification requise', 403 => 'Accès interdit', 404 => 'Page introuvable',
|
||||
405 => 'Méthode non autorisée', 410 => 'Page supprimée', 429 => 'Trop de requêtes', 500 => 'Erreur interne', 503 => 'Maintenance en cours');
|
||||
}
|
||||
|
||||
/* Affiche la page d'erreur (code HTTP, titre, message HTML déjà sûr) puis s'arrête */
|
||||
function yb2026_render_error($code, $title, $msg_html)
|
||||
{
|
||||
$yb_code = (int) $code ?: 500;
|
||||
$yb_title = $title !== '' ? $title : (yb2026_error_titles()[$yb_code] ?? 'Erreur');
|
||||
$yb_msg = $msg_html;
|
||||
if (!headers_sent()) {
|
||||
status_header($yb_code);
|
||||
nocache_headers();
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
header('X-Robots-Tag: noindex, nofollow');
|
||||
}
|
||||
// on vide les tampons de sortie (dont celui d'Autoptimize) : la page sort à l'identique des pages Apache / NPM
|
||||
while (ob_get_level() > 0) ob_end_clean();
|
||||
include __DIR__ . '/error-template.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
add_filter('wp_die_handler', function ($handler) {
|
||||
if (is_admin() || wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST) || (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST)
|
||||
|| (isset($GLOBALS['pagenow']) && $GLOBALS['pagenow'] === 'wp-login.php')) {
|
||||
return $handler;
|
||||
}
|
||||
return function ($message, $title = '', $args = array()) {
|
||||
$args = wp_parse_args($args, array('response' => 500));
|
||||
if (is_wp_error($message)) {
|
||||
$data = $message->get_error_data();
|
||||
if (is_array($data) && !empty($data['status'])) $args['response'] = $data['status'];
|
||||
$message = implode('<br>', array_map('esc_html', $message->get_error_messages()));
|
||||
}
|
||||
$code = (int) $args['response'];
|
||||
// le titre WordPress par défaut (« Erreur WordPress ») n'apporte rien : on garde un titre lisible
|
||||
$t = is_string($title) && $title !== '' && stripos($title, 'WordPress') === false ? wp_strip_all_tags($title) : '';
|
||||
$message = trim(preg_replace('#</?p[^>]*>#i', ' ', (string) $message)); // le modèle fournit déjà le paragraphe
|
||||
yb2026_render_error($code, $t, wp_kses_post($message));
|
||||
};
|
||||
});
|
||||
323
inc/profile.php
Normal file
323
inc/profile.php
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
<?php
|
||||
/**
|
||||
* Page d'admin « Profil » : poste, coordonnées, textes clés, parcours,
|
||||
* compétences et services, modifiables sans toucher au code.
|
||||
*
|
||||
* ACF Pro n'est plus actif (licence perdue, voir plugin acf-pro-shim en lecture
|
||||
* seule) : les répéteurs sont donc faits maison, stockés dans 4 options WP :
|
||||
* yb_identity (tableau clé => texte), yb_parcours, yb_skills, yb_services (listes).
|
||||
*/
|
||||
|
||||
/** Valeurs par défaut (= contenu de référence du design v3). */
|
||||
function yb2026_profile_defaults()
|
||||
{
|
||||
return array(
|
||||
'yb_identity' => array(
|
||||
'poste' => 'Chargé support logiciel MCO',
|
||||
'entreprise' => 'SeipraScore',
|
||||
'casquettes' => 'Développeur web · Admin sys & réseaux',
|
||||
'localisation' => 'Région lyonnaise',
|
||||
'region' => 'Auvergne-Rhône-Alpes',
|
||||
'ville_schema' => 'Lyon',
|
||||
'email' => 'contact@exemple.fr',
|
||||
'telephone' => '06 00 00 00 00',
|
||||
'mobilite' => 'Permis B',
|
||||
'hero_texte' => 'Support logiciel et maintien en conditions opérationnelles chez SeipraScore. Développeur web et admin Linux à côté, en continu.',
|
||||
'apropos' => '',
|
||||
'intro' => '',
|
||||
'accroche' => 'Un projet ? Une question ? Un échange ?',
|
||||
),
|
||||
'yb_parcours' => array(),
|
||||
'yb_skills' => array(),
|
||||
'yb_services' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
/** Description des champs de chaque répéteur : clé => [libellé, type]. */
|
||||
function yb2026_profile_repeaters()
|
||||
{
|
||||
return array(
|
||||
'yb_parcours' => array(
|
||||
'title' => 'Parcours',
|
||||
'help' => 'Du plus récent au plus ancien. La date et le lieu s\'affichent en majuscules automatiquement.',
|
||||
'fields' => array('date' => array('Période', 'text'), 'titre' => array('Intitulé', 'text'), 'lieu' => array('Lieu / établissement', 'text'), 'desc' => array('Description', 'textarea')),
|
||||
),
|
||||
'yb_skills' => array(
|
||||
'title' => 'Compétences techniques',
|
||||
'help' => 'Barres de la page Qui suis-je. Pourcentage = remplissage de la barre (0 à 100).',
|
||||
'fields' => array('name' => array('Compétence', 'text'), 'level' => array('Niveau', 'text'), 'pct' => array('%', 'number')),
|
||||
),
|
||||
'yb_services' => array(
|
||||
'title' => 'Services (page Compétences)',
|
||||
'help' => 'Cartes numérotées ( 01 ), ( 02 )… dans l\'ordre de la liste.',
|
||||
'fields' => array('titre' => array('Titre', 'text'), 'desc' => array('Description', 'textarea')),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** Libellés des champs d'identité. */
|
||||
function yb2026_identity_fields()
|
||||
{
|
||||
return array(
|
||||
'poste' => array('Poste', 'text', 'Kicker du hero, fiche Qui suis-je, données structurées.'),
|
||||
'entreprise' => array('Entreprise', 'text', 'Badge de la nav (« @ ENTREPRISE »), hero, schema.org worksFor.'),
|
||||
'casquettes' => array('Casquettes', 'text', 'Sous-titre du hero, avant la localisation.'),
|
||||
'localisation' => array('Localisation', 'text', 'Volontairement large : pas de ville précise.'),
|
||||
'region' => array('Région', 'text', 'Ajoutée après la localisation dans le footer, le menu mobile et le contact.'),
|
||||
'ville_schema' => array('Ville (SEO uniquement)', 'text', 'Utilisée seulement dans les données structurées schema.org, jamais affichée.'),
|
||||
'email' => array('E-mail', 'text', ''),
|
||||
'telephone' => array('Téléphone', 'text', ''),
|
||||
'mobilite' => array('Mobilité', 'text', ''),
|
||||
'hero_texte' => array('Texte du hero (à droite)', 'textarea', 'Accueil, paragraphe au-dessus de « SCROLL TO EXPLORE ».'),
|
||||
'apropos' => array('Texte « À propos » de l\'accueil', 'textarea', 'Section CODE. SERVEURS. RÉSEAUX. Les mots entre *astérisques* passent en lime.'),
|
||||
'intro' => array('Intro de la page Qui suis-je', 'textarea', ''),
|
||||
'accroche' => array('Accroche du bloc contact', 'text', 'Au-dessus de « TRAVAILLONS ENSEMBLE ».'),
|
||||
);
|
||||
}
|
||||
|
||||
/** Lecture d'une option du profil, fusionnée avec les valeurs par défaut. */
|
||||
function yb2026_profile($option)
|
||||
{
|
||||
$defaults = yb2026_profile_defaults();
|
||||
$value = get_option($option, null);
|
||||
if ($option === 'yb_identity') {
|
||||
return array_merge($defaults['yb_identity'], is_array($value) ? array_filter($value, 'strlen') : array());
|
||||
}
|
||||
return is_array($value) ? $value : $defaults[$option];
|
||||
}
|
||||
|
||||
/** Raccourci : un champ d'identité. */
|
||||
function yb2026_id($key)
|
||||
{
|
||||
$id = yb2026_profile('yb_identity');
|
||||
return isset($id[$key]) ? $id[$key] : '';
|
||||
}
|
||||
|
||||
/** Localisation complète : « Région lyonnaise, Auvergne-Rhône-Alpes ». */
|
||||
function yb2026_location_long()
|
||||
{
|
||||
$loc = yb2026_id('localisation');
|
||||
$reg = yb2026_id('region');
|
||||
return $reg ? $loc . ', ' . $reg : $loc;
|
||||
}
|
||||
|
||||
/** « 06 00 00 00 00 » → « tel:+33600000000 ». */
|
||||
function yb2026_tel_href($tel)
|
||||
{
|
||||
$digits = preg_replace('/\D/', '', (string) $tel);
|
||||
return 'tel:' . (strpos($digits, '0') === 0 ? '+33' . substr($digits, 1) : '+' . $digits);
|
||||
}
|
||||
|
||||
/** Texte avec *mots* mis en lime (échappé). */
|
||||
function yb2026_lime_marks($text)
|
||||
{
|
||||
return preg_replace('/\*([^*]+)\*/u', '<span class="lime">$1</span>', esc_html($text));
|
||||
}
|
||||
|
||||
/* ── Admin ──────────────────────────────────────────────────────────── */
|
||||
|
||||
add_action('admin_menu', function () {
|
||||
add_menu_page('Profil', 'Profil', 'edit_pages', 'yb-profil', 'yb2026_profile_page', 'dashicons-id-alt', 3);
|
||||
});
|
||||
|
||||
function yb2026_profile_page()
|
||||
{
|
||||
$identity = yb2026_profile('yb_identity');
|
||||
?>
|
||||
<div class="wrap yb-profil">
|
||||
<h1>Profil</h1>
|
||||
<p>Ces informations alimentent tout le site : hero, nav, Qui suis-je, Compétences, Contact, footer et données SEO.</p>
|
||||
<?php if (isset($_GET['updated'])) : ?><div class="notice notice-success is-dismissible"><p>Profil enregistré.</p></div><?php endif; ?>
|
||||
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||
<input type="hidden" name="action" value="yb_save_profile">
|
||||
<?php wp_nonce_field('yb_save_profile'); ?>
|
||||
|
||||
<h2>Situation et coordonnées</h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<?php foreach (yb2026_identity_fields() as $key => $f) : ?>
|
||||
<tr>
|
||||
<th scope="row"><label for="yb-id-<?php echo esc_attr($key); ?>"><?php echo esc_html($f[0]); ?></label></th>
|
||||
<td>
|
||||
<?php if ($f[1] === 'textarea') : ?>
|
||||
<textarea class="large-text" rows="3" id="yb-id-<?php echo esc_attr($key); ?>" name="yb_identity[<?php echo esc_attr($key); ?>]"><?php echo esc_textarea($identity[$key]); ?></textarea>
|
||||
<?php else : ?>
|
||||
<input class="regular-text" type="text" id="yb-id-<?php echo esc_attr($key); ?>" name="yb_identity[<?php echo esc_attr($key); ?>]" value="<?php echo esc_attr($identity[$key]); ?>">
|
||||
<?php endif; ?>
|
||||
<?php if ($f[2]) : ?><p class="description"><?php echo esc_html($f[2]); ?></p><?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
|
||||
<?php foreach (yb2026_profile_repeaters() as $opt => $rep) :
|
||||
$rows = yb2026_profile($opt); ?>
|
||||
<h2><?php echo esc_html($rep['title']); ?></h2>
|
||||
<p class="description"><?php echo esc_html($rep['help']); ?></p>
|
||||
<table class="widefat striped yb-rep" data-opt="<?php echo esc_attr($opt); ?>">
|
||||
<thead><tr>
|
||||
<?php foreach ($rep['fields'] as $f) : ?><th><?php echo esc_html($f[0]); ?></th><?php endforeach; ?>
|
||||
<th class="yb-rep__actions"></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $i => $row) yb2026_profile_row($opt, $rep['fields'], $i, $row); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<template class="yb-rep__tpl" data-opt="<?php echo esc_attr($opt); ?>"><?php yb2026_profile_row($opt, $rep['fields'], '__i__', array()); ?></template>
|
||||
<p><button type="button" class="button yb-rep__add" data-opt="<?php echo esc_attr($opt); ?>">+ Ajouter une ligne</button></p>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php submit_button('Enregistrer le profil'); ?>
|
||||
</form>
|
||||
</div>
|
||||
<style>
|
||||
.yb-profil .yb-rep td { vertical-align: top; }
|
||||
.yb-profil .yb-rep input[type=text], .yb-profil .yb-rep textarea { width: 100%; }
|
||||
.yb-profil .yb-rep input[type=number] { width: 5em; }
|
||||
.yb-profil .yb-rep__actions { width: 7.5em; white-space: nowrap; }
|
||||
.yb-profil .yb-rep__actions .button { min-width: 2em; padding: 0 .4em; }
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
var n = Date.now();
|
||||
document.querySelectorAll('.yb-rep__add').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var opt = btn.dataset.opt;
|
||||
var tpl = document.querySelector('.yb-rep__tpl[data-opt="' + opt + '"]');
|
||||
var body = document.querySelector('.yb-rep[data-opt="' + opt + '"] tbody');
|
||||
body.insertAdjacentHTML('beforeend', tpl.innerHTML.replace(/__i__/g, 'n' + (n++)));
|
||||
});
|
||||
});
|
||||
document.addEventListener('click', function (e) {
|
||||
var b = e.target.closest('[data-row-act]');
|
||||
if (!b) return;
|
||||
var tr = b.closest('tr');
|
||||
var act = b.dataset.rowAct;
|
||||
if (act === 'del') tr.remove();
|
||||
if (act === 'up' && tr.previousElementSibling) tr.parentNode.insertBefore(tr, tr.previousElementSibling);
|
||||
if (act === 'down' && tr.nextElementSibling) tr.parentNode.insertBefore(tr.nextElementSibling, tr);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/** Une ligne de répéteur (aussi utilisée comme modèle <template>). */
|
||||
function yb2026_profile_row($opt, $fields, $i, $row)
|
||||
{
|
||||
echo '<tr>';
|
||||
foreach ($fields as $key => $f) {
|
||||
$name = $opt . '[' . $i . '][' . $key . ']';
|
||||
$val = isset($row[$key]) ? $row[$key] : '';
|
||||
echo '<td>';
|
||||
if ($f[1] === 'textarea') {
|
||||
echo '<textarea rows="2" name="' . esc_attr($name) . '" aria-label="' . esc_attr($f[0]) . '">' . esc_textarea($val) . '</textarea>';
|
||||
} else {
|
||||
$extra = $f[1] === 'number' ? ' min="0" max="100"' : '';
|
||||
echo '<input type="' . esc_attr($f[1]) . '"' . $extra . ' name="' . esc_attr($name) . '" value="' . esc_attr($val) . '" aria-label="' . esc_attr($f[0]) . '">';
|
||||
}
|
||||
echo '</td>';
|
||||
}
|
||||
echo '<td class="yb-rep__actions">'
|
||||
. '<button type="button" class="button" data-row-act="up" aria-label="Monter">↑</button> '
|
||||
. '<button type="button" class="button" data-row-act="down" aria-label="Descendre">↓</button> '
|
||||
. '<button type="button" class="button" data-row-act="del" aria-label="Supprimer">✕</button>'
|
||||
. '</td></tr>';
|
||||
}
|
||||
|
||||
add_action('admin_post_yb_save_profile', function () {
|
||||
if (!current_user_can('edit_pages')) wp_die('Accès refusé.');
|
||||
check_admin_referer('yb_save_profile');
|
||||
|
||||
$identity = array();
|
||||
$raw = isset($_POST['yb_identity']) ? (array) wp_unslash($_POST['yb_identity']) : array();
|
||||
foreach (yb2026_identity_fields() as $key => $f) {
|
||||
$val = isset($raw[$key]) ? $raw[$key] : '';
|
||||
$identity[$key] = $f[1] === 'textarea' ? sanitize_textarea_field($val) : sanitize_text_field($val);
|
||||
}
|
||||
update_option('yb_identity', $identity, false);
|
||||
|
||||
foreach (yb2026_profile_repeaters() as $opt => $rep) {
|
||||
$rows = isset($_POST[$opt]) ? (array) wp_unslash($_POST[$opt]) : array();
|
||||
$clean = array();
|
||||
foreach ($rows as $row) {
|
||||
$item = array();
|
||||
foreach ($rep['fields'] as $key => $f) {
|
||||
$val = isset($row[$key]) ? $row[$key] : '';
|
||||
if ($f[1] === 'number') $item[$key] = max(0, min(100, (int) $val));
|
||||
elseif ($f[1] === 'textarea') $item[$key] = sanitize_textarea_field($val);
|
||||
else $item[$key] = sanitize_text_field($val);
|
||||
}
|
||||
/* Ligne vide ignorée */
|
||||
if (implode('', array_map('strval', array_filter($item, 'is_string')))) $clean[] = $item;
|
||||
}
|
||||
update_option($opt, $clean, false);
|
||||
}
|
||||
|
||||
wp_safe_redirect(admin_url('admin.php?page=yb-profil&updated=1'));
|
||||
exit;
|
||||
});
|
||||
|
||||
/* ── Données structurées schema.org Person ─────────────────────────────
|
||||
SEOPress émet déjà un Person minimal avec le même @id : les deux nœuds
|
||||
fusionnent, celui-ci ajoute poste, employeur et zone géographique. */
|
||||
add_action('wp_head', function () {
|
||||
$id = yb2026_profile('yb_identity');
|
||||
$person = array(
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Person',
|
||||
'@id' => home_url('/#person'),
|
||||
'name' => 'Yasser Boulayoune',
|
||||
'url' => home_url('/'),
|
||||
'jobTitle' => $id['poste'],
|
||||
'worksFor' => array('@type' => 'Organization', 'name' => $id['entreprise']),
|
||||
'address' => array('@type' => 'PostalAddress', 'addressLocality' => $id['ville_schema'], 'addressRegion' => $id['region'], 'addressCountry' => 'FR'),
|
||||
'knowsAbout' => array('Support logiciel', 'MCO', 'Développement web', 'Administration systèmes et réseaux', 'Linux', 'Docker', 'WordPress', 'Symfony'),
|
||||
);
|
||||
echo '<script type="application/ld+json">' . wp_json_encode($person, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . '</script>' . "\n";
|
||||
}, 20);
|
||||
|
||||
/* ── Protection anti-collecte de l'email et du téléphone ───────────────
|
||||
Dans le HTML, ces valeurs n'apparaissent jamais en clair : elles sont
|
||||
encodées (ROT13 → inversion → base64) et décodées en JS au premier geste
|
||||
d'un visiteur humain (voir YBProtect dans effects.js). Un robot qui lit
|
||||
le HTML brut ne trouve ni « @ », ni « mailto: », ni numéro. */
|
||||
|
||||
/** Encode une chaîne (décodage JS : atob → inversion → ROT13). */
|
||||
function yb2026_obf($s)
|
||||
{
|
||||
return base64_encode(strrev(str_rot13((string) $s)));
|
||||
}
|
||||
|
||||
/** Texte protégé : affiché « •••• » tant que le JS ne l'a pas décodé. */
|
||||
function yb2026_pv($value)
|
||||
{
|
||||
return '<span class="yb-pv" data-pv="' . esc_attr(yb2026_obf($value)) . '">' . str_repeat('•', 8) . '</span>';
|
||||
}
|
||||
|
||||
/** Attributs d'un lien protégé (href décodé au premier geste). */
|
||||
function yb2026_pv_href($href)
|
||||
{
|
||||
return 'href="#" data-pv-href="' . esc_attr(yb2026_obf($href)) . '"';
|
||||
}
|
||||
|
||||
/** Contenu des pages (mentions légales…) : email et téléphone du profil protégés automatiquement. */
|
||||
add_filter('the_content', function ($html) {
|
||||
$email = yb2026_id('email');
|
||||
$tel = yb2026_id('telephone');
|
||||
if ($email) {
|
||||
/* <a href="mailto:x">x</a> → lien protégé */
|
||||
$html = preg_replace_callback('#<a\b([^>]*?)href=(["\'])mailto:' . preg_quote($email, '#') . '\2([^>]*)>(.*?)</a>#is', function ($m) use ($email) {
|
||||
$inner = str_ireplace($email, yb2026_pv($email), $m[4]);
|
||||
return '<a' . $m[1] . yb2026_pv_href('mailto:' . $email) . $m[3] . '>' . $inner . '</a>';
|
||||
}, $html);
|
||||
/* Email en texte brut restant (hors attributs) */
|
||||
$html = preg_replace_callback('#(>[^<]*?)' . preg_quote($email, '#') . '#i', function ($m) use ($email) { return $m[1] . yb2026_pv($email); }, $html);
|
||||
}
|
||||
if ($tel) {
|
||||
$digits = preg_replace('/\D/', '', $tel);
|
||||
$loose = implode('[\s.\-]?', str_split($digits)); // « 06 00 00 00 00 », « 06.00… », « 0600000000 »
|
||||
$html = preg_replace_callback('#(>[^<]*?)' . $loose . '#', function ($m) use ($tel) { return $m[1] . yb2026_pv($tel); }, $html);
|
||||
}
|
||||
return $html;
|
||||
}, 30);
|
||||
210
inc/proofs.php
Normal file
210
inc/proofs.php
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
<?php
|
||||
/**
|
||||
* Compétences de la page /competences/ (bloc « Chaque compétence a son projet »).
|
||||
*
|
||||
* Stockage : option `yb_proofs` = liste de [ 'skill' => nom, 'items' => [ID, ID…] ].
|
||||
* Les items sont des réalisations ou des articles ; le premier est la carte
|
||||
* de devant dans le paquet. Nom court d'un article sur les cartes : méta `_yb_short_label`.
|
||||
*
|
||||
* Admin : Profil → Compétences (créer / renommer / réordonner / supprimer
|
||||
* des compétences, y ajouter des réalisations et des articles).
|
||||
*/
|
||||
|
||||
/** Liste prête pour le gabarit : [ nom => [ [post, kind, name, tag, thumb, wipe], … ] ]. */
|
||||
function yb2026_get_proofs()
|
||||
{
|
||||
$rows = get_option('yb_proofs', array());
|
||||
$out = array();
|
||||
foreach ((array) $rows as $row) {
|
||||
$links = array();
|
||||
foreach ((array) ($row['items'] ?? array()) as $id) {
|
||||
$p = get_post((int) $id);
|
||||
if (!$p || $p->post_status !== 'publish' || !in_array($p->post_type, array('realisation', 'post'), true)) continue;
|
||||
$is_post = $p->post_type === 'post';
|
||||
$short = $is_post ? (string) get_post_meta($p->ID, '_yb_short_label', true) : '';
|
||||
$cat = $is_post ? get_the_category($p->ID) : array();
|
||||
$links[] = array(
|
||||
'post' => $p,
|
||||
'kind' => $is_post ? 'article' : 'projet',
|
||||
'name' => $short !== '' ? $short : yb2026_short_title($p),
|
||||
'tag' => $is_post ? 'ARTICLE · ' . mb_strtoupper($cat ? $cat[0]->name : 'BLOG') : (string) get_post_meta($p->ID, '_yb_tag', true),
|
||||
'thumb' => get_the_post_thumbnail_url($p, 'large'),
|
||||
'wipe' => $is_post ? 'LECTURE' : mb_strtoupper(yb2026_short_title($p)),
|
||||
);
|
||||
}
|
||||
if ($links && !empty($row['skill'])) $out[$row['skill']] = $links;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/* ── Admin ──────────────────────────────────────────────────────────── */
|
||||
|
||||
add_action('admin_menu', function () {
|
||||
add_submenu_page('yb-profil', 'Compétences', 'Compétences', 'edit_pages', 'yb-competences', 'yb2026_proofs_page');
|
||||
});
|
||||
|
||||
function yb2026_proofs_page()
|
||||
{
|
||||
$rows = (array) get_option('yb_proofs', array());
|
||||
$reals = get_posts(array('post_type' => 'realisation', 'post_status' => 'publish', 'numberposts' => -1, 'orderby' => 'title', 'order' => 'ASC'));
|
||||
$arts = get_posts(array('post_type' => 'post', 'post_status' => 'publish', 'numberposts' => -1));
|
||||
$label = function ($p) {
|
||||
$s = $p->post_type === 'post' ? (string) get_post_meta($p->ID, '_yb_short_label', true) : '';
|
||||
return ($p->post_type === 'post' ? 'Article · ' : 'Réalisation · ') . ($s !== '' ? $s : yb2026_short_title($p));
|
||||
};
|
||||
$all = array();
|
||||
foreach (array_merge($reals, $arts) as $p) $all[$p->ID] = $label($p);
|
||||
|
||||
/* Contenus rattachés à aucune compétence : signalés en haut de page */
|
||||
$used = array();
|
||||
foreach ($rows as $r) foreach ((array) ($r['items'] ?? array()) as $id) $used[(int) $id] = true;
|
||||
$orphans = array_diff_key($all, $used);
|
||||
|
||||
$options = function () use ($reals, $arts, $label) {
|
||||
$h = '<option value="">+ Ajouter une réalisation ou un article…</option><optgroup label="Réalisations">';
|
||||
foreach ($reals as $p) $h .= '<option value="' . (int) $p->ID . '">' . esc_html($label($p)) . '</option>';
|
||||
$h .= '</optgroup><optgroup label="Articles du blog">';
|
||||
foreach ($arts as $p) $h .= '<option value="' . (int) $p->ID . '">' . esc_html($label($p)) . '</option>';
|
||||
return $h . '</optgroup>';
|
||||
};
|
||||
$chip = function ($id, $row) use ($all) {
|
||||
return '<span class="ybp-chip" draggable="true"><input type="hidden" name="yb_proofs[' . esc_attr($row) . '][items][]" value="' . (int) $id . '">'
|
||||
. esc_html($all[$id] ?? ('#' . $id)) . ' <button type="button" class="ybp-chip__x" aria-label="Retirer">✕</button></span>';
|
||||
};
|
||||
?>
|
||||
<div class="wrap ybp">
|
||||
<h1>Compétences</h1>
|
||||
<p>Chaque compétence apparaît sur la page <a href="<?php echo esc_url(home_url('/competences/')); ?>" target="_blank">Compétences</a>, dans « Chaque compétence a son projet ». Le <strong>premier</strong> contenu d'une compétence est la carte de devant ; avec plusieurs contenus, un clic ouvre le menu circulaire.</p>
|
||||
<?php if (isset($_GET['updated'])) : ?><div class="notice notice-success is-dismissible"><p>Compétences enregistrées.</p></div><?php endif; ?>
|
||||
<?php if ($orphans) : ?>
|
||||
<div class="notice notice-warning"><p><strong>Pas encore rattachés :</strong> <?php echo esc_html(implode(', ', $orphans)); ?></p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||
<input type="hidden" name="action" value="yb_save_proofs">
|
||||
<?php wp_nonce_field('yb_save_proofs'); ?>
|
||||
|
||||
<div id="ybp-rows">
|
||||
<?php foreach ($rows as $i => $r) : ?>
|
||||
<div class="ybp-row">
|
||||
<div class="ybp-row__head">
|
||||
<input type="text" class="ybp-skill" name="yb_proofs[<?php echo (int) $i; ?>][skill]" value="<?php echo esc_attr($r['skill'] ?? ''); ?>" placeholder="Nom de la compétence">
|
||||
<button type="button" class="button" data-act="up" aria-label="Monter">↑</button>
|
||||
<button type="button" class="button" data-act="down" aria-label="Descendre">↓</button>
|
||||
<button type="button" class="button button-link-delete" data-act="del">Supprimer</button>
|
||||
</div>
|
||||
<div class="ybp-chips" data-row="<?php echo (int) $i; ?>"><?php foreach ((array) ($r['items'] ?? array()) as $id) echo $chip($id, $i); ?></div>
|
||||
<select class="ybp-add"><?php echo $options(); ?></select>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<template id="ybp-tpl">
|
||||
<div class="ybp-row">
|
||||
<div class="ybp-row__head">
|
||||
<input type="text" class="ybp-skill" name="yb_proofs[__i__][skill]" value="" placeholder="Nom de la compétence">
|
||||
<button type="button" class="button" data-act="up" aria-label="Monter">↑</button>
|
||||
<button type="button" class="button" data-act="down" aria-label="Descendre">↓</button>
|
||||
<button type="button" class="button button-link-delete" data-act="del">Supprimer</button>
|
||||
</div>
|
||||
<div class="ybp-chips" data-row="__i__"></div>
|
||||
<select class="ybp-add"><?php echo $options(); ?></select>
|
||||
</div>
|
||||
</template>
|
||||
<p><button type="button" class="button" id="ybp-new">+ Nouvelle compétence</button></p>
|
||||
|
||||
<h2>Noms courts des articles</h2>
|
||||
<p class="description">Affichés sur les cartes et dans le menu circulaire (les titres d'articles sont souvent trop longs). Laisser vide pour utiliser le début du titre.</p>
|
||||
<table class="widefat striped ybp-labels">
|
||||
<tbody>
|
||||
<?php foreach ($arts as $p) : ?>
|
||||
<tr>
|
||||
<td><?php echo esc_html(get_the_title($p)); ?></td>
|
||||
<td><input type="text" name="yb_short_label[<?php echo (int) $p->ID; ?>]" value="<?php echo esc_attr((string) get_post_meta($p->ID, '_yb_short_label', true)); ?>" placeholder="<?php echo esc_attr(yb2026_short_title($p)); ?>"></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php submit_button('Enregistrer les compétences'); ?>
|
||||
</form>
|
||||
</div>
|
||||
<style>
|
||||
.ybp-row { background: #fff; border: 1px solid #c3c4c7; border-radius: 6px; padding: 14px 16px; margin: 0 0 12px; max-width: 980px; }
|
||||
.ybp-row__head { display: flex; gap: 6px; align-items: center; margin-bottom: 10px; }
|
||||
.ybp-skill { flex: 1; font-size: 15px; font-weight: 600; }
|
||||
.ybp-chips { display: flex; flex-wrap: wrap; gap: 6px; min-height: 30px; margin-bottom: 8px; }
|
||||
.ybp-chips:empty::before { content: 'Aucun contenu : ajoute une réalisation ou un article ci-dessous.'; color: #8c8f94; font-style: italic; line-height: 30px; }
|
||||
.ybp-chip { display: inline-flex; align-items: center; gap: 6px; background: #f0f6fc; border: 1px solid #72aee6; border-radius: 99px; padding: 3px 6px 3px 12px; cursor: grab; }
|
||||
.ybp-chip:first-child { background: #edfaef; border-color: #00a32a; font-weight: 600; }
|
||||
.ybp-chip:first-child::before { content: 'devant'; font-size: 10px; text-transform: uppercase; color: #00a32a; }
|
||||
.ybp-chip.is-drag { opacity: .4; }
|
||||
.ybp-chip__x { border: 0; background: none; cursor: pointer; color: #b32d2e; padding: 0 4px; }
|
||||
.ybp-add { max-width: 420px; }
|
||||
.ybp-labels { max-width: 980px; }
|
||||
.ybp-labels td:last-child input { width: 100%; }
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
const rowsEl = document.getElementById('ybp-rows');
|
||||
let n = Date.now();
|
||||
const chipHTML = (id, row, text) => '<span class="ybp-chip" draggable="true"><input type="hidden" name="yb_proofs[' + row + '][items][]" value="' + id + '">'
|
||||
+ text.replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c])) + ' <button type="button" class="ybp-chip__x" aria-label="Retirer">✕</button></span>';
|
||||
document.getElementById('ybp-new').addEventListener('click', () => {
|
||||
rowsEl.insertAdjacentHTML('beforeend', document.getElementById('ybp-tpl').innerHTML.replace(/__i__/g, 'n' + (n++)));
|
||||
rowsEl.lastElementChild.querySelector('.ybp-skill').focus();
|
||||
});
|
||||
document.addEventListener('change', e => {
|
||||
const sel = e.target.closest('.ybp-add'); if (!sel || !sel.value) return;
|
||||
const chips = sel.parentNode.querySelector('.ybp-chips');
|
||||
if (!chips.querySelector('input[value="' + sel.value + '"]')) {
|
||||
chips.insertAdjacentHTML('beforeend', chipHTML(sel.value, chips.dataset.row, sel.options[sel.selectedIndex].text));
|
||||
}
|
||||
sel.value = '';
|
||||
});
|
||||
document.addEventListener('click', e => {
|
||||
const x = e.target.closest('.ybp-chip__x'); if (x) { x.closest('.ybp-chip').remove(); return; }
|
||||
const b = e.target.closest('[data-act]'); if (!b) return;
|
||||
const row = b.closest('.ybp-row');
|
||||
if (b.dataset.act === 'del' && confirm('Supprimer cette compétence ?')) row.remove();
|
||||
if (b.dataset.act === 'up' && row.previousElementSibling) row.parentNode.insertBefore(row, row.previousElementSibling);
|
||||
if (b.dataset.act === 'down' && row.nextElementSibling) row.parentNode.insertBefore(row.nextElementSibling, row);
|
||||
});
|
||||
/* Glisser-déposer des pastilles pour changer l'ordre (la première = carte de devant) */
|
||||
let drag = null;
|
||||
document.addEventListener('dragstart', e => { drag = e.target.closest('.ybp-chip'); if (drag) drag.classList.add('is-drag'); });
|
||||
document.addEventListener('dragend', () => { if (drag) drag.classList.remove('is-drag'); drag = null; });
|
||||
document.addEventListener('dragover', e => {
|
||||
if (!drag) return;
|
||||
const over = e.target.closest('.ybp-chip'), box = e.target.closest('.ybp-chips');
|
||||
if (!box || box !== drag.parentNode) return;
|
||||
e.preventDefault();
|
||||
if (over && over !== drag) {
|
||||
const r = over.getBoundingClientRect();
|
||||
box.insertBefore(drag, (e.clientX - r.left) > r.width / 2 ? over.nextSibling : over);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
add_action('admin_post_yb_save_proofs', function () {
|
||||
if (!current_user_can('edit_pages')) wp_die('Accès refusé.');
|
||||
check_admin_referer('yb_save_proofs');
|
||||
$clean = array();
|
||||
foreach ((array) wp_unslash($_POST['yb_proofs'] ?? array()) as $r) {
|
||||
$skill = sanitize_text_field($r['skill'] ?? '');
|
||||
$items = array_values(array_unique(array_filter(array_map('intval', (array) ($r['items'] ?? array())))));
|
||||
if ($skill === '' && !$items) continue;
|
||||
$clean[] = array('skill' => $skill !== '' ? $skill : 'Sans nom', 'items' => $items);
|
||||
}
|
||||
update_option('yb_proofs', $clean, false);
|
||||
foreach ((array) wp_unslash($_POST['yb_short_label'] ?? array()) as $id => $lbl) {
|
||||
$id = (int) $id;
|
||||
if (get_post_type($id) !== 'post') continue;
|
||||
$lbl = sanitize_text_field($lbl);
|
||||
$lbl === '' ? delete_post_meta($id, '_yb_short_label') : update_post_meta($id, '_yb_short_label', $lbl);
|
||||
}
|
||||
wp_safe_redirect(admin_url('admin.php?page=yb-competences&updated=1'));
|
||||
exit;
|
||||
});
|
||||
222
inc/seasons.php
Normal file
222
inc/seasons.php
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<?php
|
||||
/**
|
||||
* Thèmes saisonniers (fêtes). Réglage : Profil → Saisons (option yb_season : auto | off | <clé>).
|
||||
* Auto : voir yb2026_season_auto() (dates en heure de Paris).
|
||||
* Aperçu sans rien enregistrer : ?saison=<clé> | aucune
|
||||
* Le visiteur peut couper le décor (bouton dans la barre, mémorisé : localStorage yb-season=off).
|
||||
*/
|
||||
|
||||
function yb2026_seasons()
|
||||
{
|
||||
$ico = array(
|
||||
'pumpkin' => '<path d="M12 6.2c-1-.9-2.6-1.2-4-.6C4.6 7 3.4 10.6 4.2 14.2c.7 3.3 3.2 5.6 5.6 5.4.8 0 1.5-.4 2.2-.4s1.4.4 2.2.4c2.4.2 4.9-2.1 5.6-5.4.8-3.6-.4-7.2-3.8-8.6-1.4-.6-3-.3-4 .6z"/><path d="M12 6.2c0-1.4.6-2.6 1.8-3.2"/><path class="st-face" d="M8.3 11.2l1.5-1.6 1.1 1.6zM13.1 11.2l1.1-1.6 1.5 1.6zM8.6 14.6c1 .9 2.1 1.2 3.4 1.2s2.4-.3 3.4-1.2l-1.2.3-.8-.6-.7.7-.7-.7-.7.7-.8-.6z"/>',
|
||||
'flake' => '<path d="M12 2.5v19M3.8 7.25l16.4 9.5M3.8 16.75l16.4-9.5"/><path d="M9.6 3.9L12 6.2l2.4-2.3M9.6 20.1l2.4-2.3 2.4 2.3M3.9 10.1l3.2.9-.8 3.2M20.1 13.9l-3.2-.9.8-3.2M6.3 9.8l.8 3.2-3.2.9M17.7 14.2l-.8-3.2 3.2-.9"/>',
|
||||
'burst' => '<path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M5.6 18.4l2.8-2.8M15.6 8.4l2.8-2.8"/><circle class="st-face" cx="12" cy="12" r="1.6"/>',
|
||||
'clover' => '<path d="M12 12c-2.6-.4-4.6-2.2-4.2-4.3.3-1.5 2-2.2 3.2-1.3.1-1.6 1.8-2.6 3.1-1.8 1.7 1 1.4 4.7-2.1 7.4zM12 12c.4 2.6 2.2 4.6 4.3 4.2 1.5-.3 2.2-2 1.3-3.2 1.6-.1 2.6-1.8 1.8-3.1-1-1.7-4.7-1.4-7.4 2.1zM12 12c-2.6.4-4.6 2.2-4.2 4.3.3 1.5 2 2.2 3.2 1.3"/><path d="M12 12c1.4 3.2 1.3 6.3-.6 9"/>',
|
||||
'egg' => '<path d="M12 3c3.6 0 6.5 5.2 6.5 10.2 0 4.3-2.9 7.3-6.5 7.3s-6.5-3-6.5-7.3C5.5 8.2 8.4 3 12 3z"/><path d="M6.2 11.6l2.3 1.6 2.3-1.6 2.4 1.6 2.3-1.6 2.3 1.6"/>',
|
||||
'fish' => '<path d="M3 12c2.8-4 7-5.6 11-4.4 2 .6 3.6 2.4 4.6 4.4-1 2-2.6 3.8-4.6 4.4C10 17.6 5.8 16 3 12z"/><path d="M18.6 12l2.9-3.2v6.4z"/><circle class="st-face" cx="7.4" cy="11.2" r="1"/>',
|
||||
'note' => '<path d="M9 17.5V6.2l10-2.2v11.3"/><ellipse class="st-face" cx="6.8" cy="17.6" rx="2.4" ry="1.9"/><ellipse class="st-face" cx="16.8" cy="15.4" rx="2.4" ry="1.9"/>',
|
||||
'cake' => '<path d="M4 20.5h16M5 20.5v-7.2h14v7.2"/><path d="M5 16c1.6 1.2 3 1.2 4.6 0 1.6 1.2 3.2 1.2 4.8 0 1.6 1.2 3 1.2 4.6 0"/><path d="M12 13.3V9.5"/><path class="st-face" d="M12 4.2c1.2 1.4 1.5 2.4 1 3.3-.5.8-1.5.8-2 0-.5-.9-.2-1.9 1-3.3z"/>',
|
||||
'crescent' => '<path d="M14.6 4.2a8.2 8.2 0 1 0 0 15.6 6.6 6.6 0 1 1 0-15.6z"/><path class="st-face" d="M17.6 9.1l.9 2.1 2.3.1-1.8 1.4.6 2.2-1.9-1.3-1.9 1.3.6-2.2-1.8-1.4 2.3-.1z"/>',
|
||||
'torii' => '<path d="M3 6.2c3 .9 15 .9 18 0"/><path d="M5.2 9.4h13.6"/><path d="M7 7v13.5M17 7v13.5"/><path d="M12 7v2.4"/>',
|
||||
'prompt' => '<rect x="2.5" y="4" width="19" height="16" rx="2.5"/><path d="M6.5 9.5l3 2.5-3 2.5"/><path class="st-face" d="M11.5 14h6v1.8h-6z"/>',
|
||||
'tag' => '<path d="M3.5 12.6V4.5a1 1 0 0 1 1-1h8.1l8 8a1.4 1.4 0 0 1 0 2l-7.1 7.1a1.4 1.4 0 0 1-2 0z"/><circle class="st-face" cx="8" cy="8" r="1.5"/>',
|
||||
);
|
||||
$s = array(
|
||||
'nouvel-an' => array('Nouvel An', 'burst', 'du 28 décembre au 6 janvier'),
|
||||
'st-patrick' => array('Saint-Patrick', 'clover', 'du 14 au 17 mars'),
|
||||
'anniversaire'=> array('Anniversaire', 'cake', 'le 14 mai'),
|
||||
'algerie' => array('Indépendance de l\'Algérie', 'crescent', 'du 2 au 8 juillet'),
|
||||
'japon' => array('Japon', 'torii', 'du 20 mars au 10 avril (hanami)'),
|
||||
'hack' => array('Hack', 'prompt', 'le 30 novembre (journée de la sécurité informatique)'),
|
||||
'paques' => array('Pâques', 'egg', 'du jeudi au lundi de Pâques'),
|
||||
'poisson' => array('Poisson d\'avril', 'fish', 'le 1er avril'),
|
||||
'musique' => array('Fête de la musique', 'note', 'du 19 au 21 juin'),
|
||||
'juillet' => array('14 juillet', 'burst', 'du 12 au 14 juillet'),
|
||||
'halloween' => array('Halloween', 'pumpkin', 'du 1er octobre au 2 novembre'),
|
||||
'black-friday'=> array('Black Friday', 'tag', 'du Black Friday au Cyber Monday'),
|
||||
'noel' => array('Noël', 'flake', 'du 1er au 27 décembre'),
|
||||
);
|
||||
$out = array();
|
||||
foreach ($s as $k => $v) $out[$k] = array(
|
||||
'label' => $v[0], 'when' => $v[2],
|
||||
'off' => 'Couper le décor ' . $v[0], 'on' => 'Remettre le décor ' . $v[0],
|
||||
'icon' => '<svg viewBox="0 0 24 24" aria-hidden="true">' . $ico[$v[1]] . '</svg>',
|
||||
);
|
||||
return $out;
|
||||
}
|
||||
|
||||
/* Saison selon la date (heure de Paris). $ts : horodatage à tester (défaut : maintenant) */
|
||||
function yb2026_season_auto($ts = null)
|
||||
{
|
||||
$ts = $ts ?? time();
|
||||
$y = (int) wp_date('Y', $ts);
|
||||
$md = (int) wp_date('nd', $ts); // ex. 1031 = 31 octobre, 106 = 6 janvier
|
||||
$day = function ($m, $d, $yy) { return (int) wp_date('Ymd', mktime(12, 0, 0, $m, $d, $yy)); };
|
||||
$today = (int) wp_date('Ymd', $ts);
|
||||
|
||||
/* Pâques (calcul grégorien de Meeus) : du jeudi saint au lundi de Pâques */
|
||||
$a = $y % 19; $b = intdiv($y, 100); $c = $y % 100; $d = intdiv($b, 4); $e = $b % 4;
|
||||
$f = intdiv($b + 8, 25); $g = intdiv($b - $f + 1, 3); $h = (19 * $a + $b - $d - $g + 15) % 30;
|
||||
$i = intdiv($c, 4); $k = $c % 4; $l = (32 + 2 * $e + 2 * $i - $h - $k) % 7; $m = intdiv($a + 11 * $h + 22 * $l, 451);
|
||||
$em = intdiv($h + $l - 7 * $m + 114, 31); $ed = (($h + $l - 7 * $m + 114) % 31) + 1;
|
||||
/* Black Friday : lendemain du 4e jeudi de novembre, jusqu'au Cyber Monday */
|
||||
$thu = 1 + ((4 - (int) date('w', mktime(12, 0, 0, 11, 1, $y)) + 7) % 7) + 21;
|
||||
|
||||
if ($md === 401) return 'poisson';
|
||||
if ($today >= $day($em, $ed - 3, $y) && $today <= $day($em, $ed + 1, $y)) return 'paques';
|
||||
if ($md === 1130) return 'hack'; // Computer Security Day, passe devant le Cyber Monday
|
||||
if ($today >= $day(11, $thu + 1, $y) && $today <= $day(11, $thu + 4, $y)) return 'black-friday';
|
||||
if ($md >= 1228 || $md <= 106) return 'nouvel-an';
|
||||
if ($md >= 314 && $md <= 317) return 'st-patrick';
|
||||
if ($md === 514) return 'anniversaire';
|
||||
if ($md >= 702 && $md <= 708) return 'algerie'; // semaine autour du 5 juillet
|
||||
if ($md >= 320 && $md <= 410) return 'japon'; // hanami (après poisson d'avril et Pâques, qui passent devant)
|
||||
if ($md >= 619 && $md <= 621) return 'musique';
|
||||
if ($md >= 712 && $md <= 714) return 'juillet';
|
||||
if ($md >= 1001 && $md <= 1102) return 'halloween';
|
||||
if ($md >= 1201 && $md <= 1227) return 'noel';
|
||||
return '';
|
||||
}
|
||||
|
||||
/* Saison active pour cette requête ('' = aucune) */
|
||||
/* Pages sobres, jamais décorées (comme les pages d'erreur Apache / NPM, qui ne connaissent pas la saison) */
|
||||
function yb2026_season_blocked() { return is_404(); }
|
||||
|
||||
function yb2026_season()
|
||||
{
|
||||
static $s = null;
|
||||
if ($s !== null) return $s;
|
||||
if (yb2026_season_blocked()) return $s = '';
|
||||
$all = yb2026_seasons();
|
||||
if (isset($_GET['saison'])) {
|
||||
$q = sanitize_key($_GET['saison']);
|
||||
return $s = isset($all[$q]) ? $q : '';
|
||||
}
|
||||
$mode = (string) get_option('yb_season', 'auto');
|
||||
if ($mode === 'off') return $s = '';
|
||||
return $s = isset($all[$mode]) ? $mode : yb2026_season_auto();
|
||||
}
|
||||
|
||||
/* Quantité d'éléments animés par fête, en % (100 = normal). Option yb_season_density */
|
||||
function yb2026_season_density()
|
||||
{
|
||||
$d = (array) get_option('yb_season_density', array());
|
||||
$out = array();
|
||||
foreach (array_keys(yb2026_seasons()) as $k) if (isset($d[$k])) $out[$k] = max(0, min(300, (int) $d[$k]));
|
||||
return $out;
|
||||
}
|
||||
|
||||
/* Fêtes qui ont un CV assorti (wp-content/uploads/cv/saisons/<clé>/) */
|
||||
function yb2026_season_cvs()
|
||||
{
|
||||
$out = array();
|
||||
foreach (array_keys(yb2026_seasons()) as $k) if (file_exists(WP_CONTENT_DIR . '/uploads/cv/saisons/' . $k . '/CV_Yasser_Boulayoune.pdf')) $out[] = $k;
|
||||
return $out;
|
||||
}
|
||||
|
||||
/* Attribut de <html> : fête active */
|
||||
function yb2026_season_attrs()
|
||||
{
|
||||
if (yb2026_season_blocked()) return ' data-no-season';
|
||||
$a = '';
|
||||
if (yb2026_season()) $a .= ' data-season="' . esc_attr(yb2026_season()) . '"';
|
||||
return $a;
|
||||
}
|
||||
|
||||
add_action('wp_enqueue_scripts', function () {
|
||||
if (yb2026_season_blocked()) return;
|
||||
$dir = get_template_directory();
|
||||
$uri = get_template_directory_uri();
|
||||
wp_enqueue_style('yb-seasons', $uri . '/assets/css/seasons.css', array('yb-style'), filemtime($dir . '/assets/css/seasons.css'));
|
||||
wp_enqueue_script('yb-seasons', $uri . '/assets/js/seasons.js', array('yb-effects'), filemtime($dir . '/assets/js/seasons.js'), true);
|
||||
$list = array();
|
||||
/* accent (mode sombre) de chaque fête : sert à teinter la main du curseur dans la roue */
|
||||
$acc = array('nouvel-an' => '#f2c14e', 'st-patrick' => '#3ddc84', 'anniversaire' => '#2ee6c5', 'paques' => '#ff9fcb', 'poisson' => '#4fd1ff',
|
||||
'musique' => '#ff5cc8', 'juillet' => '#ff4a5a', 'halloween' => '#ff7b1c', 'black-friday' => '#ffd400', 'noel' => '#ff4a5a',
|
||||
'algerie' => '#2fbf71', 'japon' => '#ff5a3c', 'hack' => '#00ff66');
|
||||
foreach (yb2026_seasons() as $k => $t) $list[$k] = array('label' => $t['label'], 'when' => $t['when'], 'icon' => $t['icon'], 'accent' => $acc[$k] ?? '');
|
||||
wp_add_inline_script('yb-seasons', 'window.YB_SEASONS=' . wp_json_encode(array('list' => $list, 'auto' => yb2026_season_auto(), 'cv' => yb2026_season_cvs(), 'density' => (object) yb2026_season_density())) . ';', 'before');
|
||||
}, 20);
|
||||
|
||||
/* Bouton de la barre (icône de la fête) : coupe / remet le décor */
|
||||
function yb2026_season_toggle()
|
||||
{
|
||||
if (yb2026_season_blocked()) return;
|
||||
$s = yb2026_season();
|
||||
$t = $s ? yb2026_seasons()[$s] : array('label' => 'de fête', 'off' => 'Thème de fête', 'on' => 'Thème de fête', 'icon' => yb2026_seasons()['nouvel-an']['icon']);
|
||||
printf(
|
||||
'<button type="button" class="season-toggle' . ($s ? '' : ' season-toggle--latent') . '" data-season-toggle data-label-off="%1$s" data-label-on="%2$s" aria-label="%1$s" aria-pressed="true" title="%3$s">%4$s</button>',
|
||||
esc_attr($t['off']), esc_attr($t['on']), esc_attr('Décor ' . $t['label']), $t['icon']
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Admin : Profil → Saisons ───────────────────────────────────────── */
|
||||
|
||||
add_action('admin_menu', function () {
|
||||
add_submenu_page('yb-profil', 'Saisons', 'Saisons', 'edit_pages', 'yb-saisons', 'yb2026_seasons_page');
|
||||
});
|
||||
|
||||
function yb2026_seasons_page()
|
||||
{
|
||||
$mode = (string) get_option('yb_season', 'auto');
|
||||
$auto = yb2026_season_auto();
|
||||
$all = yb2026_seasons();
|
||||
$opts = array('auto' => 'Automatique' . ($auto ? ' (en ce moment : ' . $all[$auto]['label'] . ')' : ' (en ce moment : aucune fête)'));
|
||||
foreach ($all as $k => $t) $opts[$k] = 'Toujours ' . $t['label'] . ' (en auto : ' . $t['when'] . ')';
|
||||
$opts['off'] = 'Désactivé';
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Thèmes saisonniers</h1>
|
||||
<?php if (!empty($_GET['updated'])) : ?><div class="notice notice-success is-dismissible"><p>Réglage enregistré.</p></div><?php endif; ?>
|
||||
<p>Palette, décor animé et orbe de l'accueil changent selon la saison. Les visiteurs peuvent couper le décor avec le bouton de la barre de navigation.</p>
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||
<input type="hidden" name="action" value="yb_save_season">
|
||||
<?php wp_nonce_field('yb_save_season'); ?>
|
||||
<fieldset>
|
||||
<?php foreach ($opts as $k => $l) : ?>
|
||||
<p><label><input type="radio" name="yb_season" value="<?php echo esc_attr($k); ?>" <?php checked($mode, $k); ?>> <?php echo esc_html($l); ?></label></p>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
<h2>Quantité d'éléments animés</h2>
|
||||
<p>Combien d'objets apparaissent en fond (étiquettes, ballons, flocons, chauves-souris, feux d'artifice...). 100 % = réglage d'origine, 0 % = aucun.</p>
|
||||
<?php $dens = yb2026_season_density(); $what = array(
|
||||
'nouvel-an' => 'feux d\'artifice', 'st-patrick' => 'trèfles', 'anniversaire' => 'ballons et confettis', 'paques' => 'pétales',
|
||||
'poisson' => 'poissons et bulles', 'musique' => 'notes', 'juillet' => 'feux d\'artifice', 'halloween' => 'chauves-souris et braises',
|
||||
'black-friday' => 'étiquettes de prix', 'noel' => 'flocons', 'algerie' => 'feux d\'artifice et confettis', 'japon' => 'pétales de sakura', 'hack' => 'pluie de caractères'); ?>
|
||||
<table class="form-table" role="presentation"><tbody>
|
||||
<?php foreach ($all as $k => $t) : $v = $dens[$k] ?? 100; ?>
|
||||
<tr>
|
||||
<th scope="row"><label for="yb-dens-<?php echo esc_attr($k); ?>"><?php echo esc_html($t['label']); ?></label></th>
|
||||
<td>
|
||||
<input type="range" id="yb-dens-<?php echo esc_attr($k); ?>" name="yb_season_density[<?php echo esc_attr($k); ?>]" min="0" max="300" step="10" value="<?php echo (int) $v; ?>" style="width:260px;vertical-align:middle" oninput="this.nextElementSibling.textContent=this.value+' %'">
|
||||
<output style="display:inline-block;min-width:52px;margin-left:8px;font-weight:600"><?php echo (int) $v; ?> %</output>
|
||||
<span class="description"><?php echo esc_html($what[$k] ?? ''); ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody></table>
|
||||
<p>Aperçu sans changer le réglage :
|
||||
<?php foreach ($all as $k => $t) : ?><a href="<?php echo esc_url(add_query_arg('saison', $k, home_url('/'))); ?>" target="_blank"><?php echo esc_html($t['label']); ?></a> · <?php endforeach; ?>
|
||||
<a href="<?php echo esc_url(add_query_arg('saison', 'aucune', home_url('/'))); ?>" target="_blank">Sans décor</a>
|
||||
</p>
|
||||
<?php submit_button('Enregistrer'); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
add_action('admin_post_yb_save_season', function () {
|
||||
if (!current_user_can('edit_pages')) wp_die('Accès refusé.');
|
||||
check_admin_referer('yb_save_season');
|
||||
$v = sanitize_key($_POST['yb_season'] ?? 'auto');
|
||||
$dens = array();
|
||||
foreach ((array) wp_unslash($_POST['yb_season_density'] ?? array()) as $k => $v) {
|
||||
$k = sanitize_key($k);
|
||||
if (isset(yb2026_seasons()[$k])) $dens[$k] = max(0, min(300, (int) $v));
|
||||
}
|
||||
update_option('yb_season_density', $dens, false);
|
||||
update_option('yb_season', ($v === 'auto' || $v === 'off' || isset(yb2026_seasons()[$v])) ? $v : 'auto');
|
||||
wp_safe_redirect(admin_url('admin.php?page=yb-saisons&updated=1'));
|
||||
exit;
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue