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
15
README.md
Normal file
15
README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# yb2026
|
||||||
|
|
||||||
|
Thème WordPress sur mesure du portfolio : sans page builder, meta-boxes natives, thèmes de saison et mode clair / sombre.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- PHP (thème WordPress classique, sans constructeur de pages)
|
||||||
|
- JavaScript vanilla (orbe en canvas, transitions, curseur)
|
||||||
|
- CSS à jetons pour le mode clair / sombre
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
Copier le dossier dans `wp-content/themes/`, puis l'activer dans Apparence → Thèmes.
|
||||||
|
```
|
||||||
109
front-page.php
Normal file
109
front-page.php
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
<?php
|
||||||
|
get_header();
|
||||||
|
$projects = yb2026_get_projects();
|
||||||
|
$home_projects = array_slice($projects, 0, 6);
|
||||||
|
$years = array_filter(array_map(function ($p) { return get_post_meta($p->ID, '_yb_year', true); }, $projects));
|
||||||
|
preg_match_all('/\d{4}/', implode(' ', $years), $yy);
|
||||||
|
$span = $yy[0] ? min($yy[0]) . ' / ' . max($yy[0]) : date('Y');
|
||||||
|
$sub = mb_strtoupper(yb2026_id('casquettes') . ' · ' . yb2026_id('localisation'));
|
||||||
|
$kicker = mb_strtoupper(yb2026_id('poste') . ' @ ' . yb2026_id('entreprise'));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Orbe persistante : canvas fixe derrière toute la page, morphée par le scroll (orb.js) -->
|
||||||
|
<canvas id="orb-canvas" class="orb-canvas" aria-hidden="true"></canvas>
|
||||||
|
|
||||||
|
<!-- Rail de navigation vertical -->
|
||||||
|
<nav class="section-rail" aria-label="Sections de la page">
|
||||||
|
<a href="#top" data-rail-target="#top">TOP</a>
|
||||||
|
<a href="#travaux" data-rail-target="#travaux">TRAVAUX</a>
|
||||||
|
<a href="#apropos" data-rail-target="#apropos">PROFIL</a>
|
||||||
|
<a href="#cta" data-rail-target="#cta">CONTACT</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- HERO -->
|
||||||
|
<section class="hero" id="top">
|
||||||
|
<div class="hero__vertical-label">PORTFOLIO · ÉDITION <?php echo esc_html(date('Y')); ?></div>
|
||||||
|
<div class="hero__content">
|
||||||
|
<div class="line-mask hero__kicker-mask"><div class="hero__kicker"><?php echo esc_html($kicker); ?></div></div>
|
||||||
|
<h1 class="hero__title">
|
||||||
|
<span class="line-mask"><span>YASSER</span></span>
|
||||||
|
<span class="line-mask"><span class="outline-lime">BOULAYOUNE</span></span>
|
||||||
|
</h1>
|
||||||
|
<div class="line-mask hero__sub-mask"><div class="hero__sub"><?php echo esc_html($sub); ?></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="hero__aside">
|
||||||
|
<p><?php echo esc_html(yb2026_id('hero_texte')); ?></p>
|
||||||
|
<a class="scroll-hint" href="#travaux">SCROLL TO EXPLORE<span class="scroll-hint__arrow">→</span></a>
|
||||||
|
</div>
|
||||||
|
<div class="hero__marquee marquee" aria-hidden="true">
|
||||||
|
<div class="marquee-track">
|
||||||
|
<?php for ($k = 0; $k < 2; $k++) : ?>
|
||||||
|
<span>PHP <span class="lime">✦</span> WORDPRESS <span class="lime">✦</span> SYMFONY <span class="lime">✦</span> JAVASCRIPT <span class="lime">✦</span> PYTHON <span class="lime">✦</span> DOCKER <span class="lime">✦</span> LINUX <span class="lime">✦</span> </span>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- TRAVAUX : scroll horizontal + skew (desktop), scroll-snap natif (mobile) -->
|
||||||
|
<section class="works" id="travaux">
|
||||||
|
<div class="works-sticky">
|
||||||
|
<div class="works-head">
|
||||||
|
<div data-reveal>
|
||||||
|
<h2 class="works-title">TRAVAUX<span class="lime">.</span></h2>
|
||||||
|
<div class="works-sub">SELECTED WORKS · <?php echo esc_html($span); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="works-meta">
|
||||||
|
<div class="works-hint">( SCROLL · LA SECTION GLISSE → )</div>
|
||||||
|
<div class="works-count">01 / <?php echo esc_html(str_pad(count($home_projects), 2, '0', STR_PAD_LEFT)); ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="works-track">
|
||||||
|
<?php foreach ($home_projects as $p) yb2026_project_card($p, true); ?>
|
||||||
|
<a class="works-end" href="<?php echo esc_url(home_url('/realisation/')); ?>" data-wipe="RÉALISATIONS">
|
||||||
|
<span>TOUT VOIR<br>( <?php echo esc_html(str_pad(count($projects), 2, '0', STR_PAD_LEFT)); ?> ) →</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- À PROPOS : blob + blend-mode -->
|
||||||
|
<section class="about" id="apropos">
|
||||||
|
<div class="about-blob" aria-hidden="true"></div>
|
||||||
|
<div class="about__title-wrap">
|
||||||
|
<h2 class="about__title" data-reveal>CODE.<br>SERVEURS.<br>RÉSEAUX.</h2>
|
||||||
|
</div>
|
||||||
|
<div class="about__cols">
|
||||||
|
<p class="about__text" data-reveal><?php echo yb2026_lime_marks(yb2026_id('apropos')); ?></p>
|
||||||
|
<div class="about__list" data-reveal>
|
||||||
|
<div><span class="lime">▸</span> WEB · PHP, WordPress, Symfony, JS</div>
|
||||||
|
<div><span class="lime">▸</span> SYS · Debian, Docker, virtualisation</div>
|
||||||
|
<div><span class="lime">▸</span> RÉSEAU · VLAN, DNS, DHCP, WireGuard</div>
|
||||||
|
<div><span class="lime">▸</span> OPS · supervision, backups, TLS</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="about__more">
|
||||||
|
<a class="link-underline" href="<?php echo esc_url(home_url('/qui-suis-je/')); ?>" data-wipe="QUI SUIS-JE">EN SAVOIR PLUS SUR MOI →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- MARQUEES compétences -->
|
||||||
|
<section class="skills-mq" aria-hidden="true">
|
||||||
|
<div class="marquee">
|
||||||
|
<div class="marquee-track skills-mq__track">
|
||||||
|
<?php for ($k = 0; $k < 2; $k++) : ?><span>PHP / WORDPRESS / SYMFONY / JAVASCRIPT / PYTHON / NODE / DOCKER / GIT / </span><?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="marquee">
|
||||||
|
<div class="marquee-track marquee-track--reverse skills-mq__track skills-mq__track--outline">
|
||||||
|
<?php for ($k = 0; $k < 2; $k++) : ?><span>DEBIAN / NGINX / WIREGUARD / VLAN / DNS / ACTIVE DIRECTORY / SQL / SUPERVISION / </span><?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA contact -->
|
||||||
|
<section class="cta" id="cta">
|
||||||
|
<div class="kicker" data-reveal><?php echo esc_html(mb_strtoupper(yb2026_id('accroche'))); ?></div>
|
||||||
|
<a class="cta__title" href="<?php echo esc_url(home_url('/contact/')); ?>" data-wipe="CONTACT" data-cursor-label="M'ÉCRIRE" data-reveal>TRAVAILLONS<br><span class="outline-lime">ENSEMBLE →</span></a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php get_footer(); ?>
|
||||||
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;
|
||||||
|
});
|
||||||
173
page-competences.php
Normal file
173
page-competences.php
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
<?php
|
||||||
|
/* Page « Compétences » (/competences/, ancienne adresse /services/ redirigée en 301) */
|
||||||
|
get_header();
|
||||||
|
$services = yb2026_profile('yb_services');
|
||||||
|
|
||||||
|
/* Chiffres calculés en direct */
|
||||||
|
$nb_real = (int) wp_count_posts('realisation')->publish;
|
||||||
|
$nb_posts = (int) wp_count_posts('post')->publish;
|
||||||
|
|
||||||
|
/* Boîte à outils : uniquement ce que prouvent les réalisations, articles et le poste */
|
||||||
|
$toolbox = array(
|
||||||
|
array('t' => 'Développement web', 'i' => array('PHP 8', 'Symfony', 'Twig Components', 'Doctrine', 'WordPress sur mesure', 'PHP/MVC sans framework', 'JavaScript vanilla', 'HTML / CSS responsive')),
|
||||||
|
array('t' => 'Données & outillage', 'i' => array('MariaDB / MySQL', 'PostgreSQL', 'SQL', 'API HTTP / JSON', 'Python 3', 'Node.js', 'JS ESM', 'FFmpeg', 'librsvg')),
|
||||||
|
array('t' => 'Systèmes & conteneurs', 'i' => array('Debian 12', 'Docker / Compose', 'systemd', 'Apache', 'Nginx Proxy Manager', 'TLS Let\'s Encrypt', 'Réglage mémoire JVM')),
|
||||||
|
array('t' => 'Réseau & sécurité', 'i' => array('VLAN', 'DNS', 'DHCP', 'WireGuard', 'Pi-hole', 'Active Directory', 'Reverse proxy', 'Services privés par défaut')),
|
||||||
|
array('t' => 'Support & MCO', 'i' => array('Diagnostic d\'incidents', 'Lecture de logs', 'Accompagnement utilisateurs', 'Procédures documentées', 'Supervision Uptime Kuma', 'Sauvegardes planifiées')),
|
||||||
|
array('t' => 'Outils du quotidien', 'i' => array('Git', 'Forgejo / Gitea', 'GitHub', 'VS Code / code-server', 'Terminal & SSH')),
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Méthode : tirée de mes retours d'expérience (OOM killer, faux bug, servyass) */
|
||||||
|
$method = array(
|
||||||
|
array('t' => 'Observer', 'd' => 'Lire les journaux en entier, reproduire le symptôme, noter ce qui change et ce qui ne change pas.'),
|
||||||
|
array('t' => 'Isoler', 'd' => 'Réduire le périmètre : environnement, ressources, configuration, code. Vérifier aussi l\'outil de mesure.'),
|
||||||
|
array('t' => 'Corriger', 'd' => 'Formuler une hypothèse, la tester, appliquer le correctif le plus simple qui traite la vraie cause.'),
|
||||||
|
array('t' => 'Documenter', 'd' => 'Écrire la procédure, pour que la même panne se règle en cinq minutes la prochaine fois.'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Preuves : gérées dans l'admin (Profil → Compétences), voir inc/proofs.php */
|
||||||
|
$proofs = yb2026_get_proofs();
|
||||||
|
|
||||||
|
$containers = array('npm', 'jellyfin', 'forgejo', 'code-server', 'wordpress + mariadb', 'passbolt + mariadb', 'poste.io', 'uptime-kuma', 'filebrowser · alist', 'wireguard');
|
||||||
|
$infra_points = array(
|
||||||
|
array('t' => 'Pas un lab jetable', 'd' => 'De la prod perso avec de vrais utilisateurs, de vraies pannes et de vrais correctifs. Si Jellyfin tombe, on me le fait savoir.'),
|
||||||
|
array('t' => 'Incidents gérés', 'd' => 'OOM, conflits de ports, migration complète de matériel, watchdog réseau, certificats TLS : diagnostiqués et résolus seul.'),
|
||||||
|
array('t' => 'Méthode', 'd' => 'Supervision Uptime Kuma, sauvegardes planifiées, mises à jour de sécurité régulières, procédures documentées.'),
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="cp">
|
||||||
|
|
||||||
|
<!-- ── Ouverture : titre + phrase manifeste aux chiffres animés ─────── -->
|
||||||
|
<section class="cp-open page-wrap">
|
||||||
|
<div class="kicker">CE QUE JE SAIS FAIRE</div>
|
||||||
|
<h1 class="cp-title" data-reveal>COMPÉ<span class="outline-lime">TENCES</span></h1>
|
||||||
|
<p class="cp-manifest" data-reveal>
|
||||||
|
J'ai livré <span class="cp-num" data-count="<?php echo (int) $nb_real; ?>" data-pad="2"><?php echo esc_html(str_pad($nb_real, 2, '0', STR_PAD_LEFT)); ?></span> projets,
|
||||||
|
écrit <span class="cp-num" data-count="<?php echo (int) $nb_posts; ?>" data-pad="2"><?php echo esc_html(str_pad($nb_posts, 2, '0', STR_PAD_LEFT)); ?></span> articles techniques,
|
||||||
|
et je fais tourner <span class="cp-num" data-count="37">37</span> services en production
|
||||||
|
depuis <span class="cp-num" data-count="2" data-suffix=" ans">2 ans</span>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── Services : lignes éditoriales, rideau lime au survol ─────────── -->
|
||||||
|
<section class="cp-svc" aria-label="Services">
|
||||||
|
<?php foreach ($services as $i => $sv) : ?>
|
||||||
|
<div class="cp-svc__row" data-reveal>
|
||||||
|
<span class="cp-svc__n"><?php echo esc_html(str_pad($i + 1, 2, '0', STR_PAD_LEFT)); ?></span>
|
||||||
|
<h2 class="cp-svc__t"><?php echo esc_html($sv['titre']); ?></h2>
|
||||||
|
<p class="cp-svc__d"><?php echo esc_html($sv['desc']); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── Boîte à outils : un bandeau défilant géant par domaine ───────── -->
|
||||||
|
<section class="cp-tools" aria-label="Boîte à outils">
|
||||||
|
<div class="page-wrap cp-head">
|
||||||
|
<div class="kicker">BOÎTE À OUTILS</div>
|
||||||
|
<h2 class="cp-h" data-reveal>CE QUE <span class="outline-lime">J'UTILISE</span></h2>
|
||||||
|
</div>
|
||||||
|
<?php foreach ($toolbox as $i => $tb) : ?>
|
||||||
|
<div class="cp-tools__row<?php echo $i % 2 ? ' is-alt' : ''; ?>">
|
||||||
|
<div class="cp-tools__label"><span class="lime"><?php echo esc_html(str_pad($i + 1, 2, '0', STR_PAD_LEFT)); ?></span> <?php echo esc_html(mb_strtoupper($tb['t'])); ?></div>
|
||||||
|
<div class="marquee cp-tools__mq" aria-label="<?php echo esc_attr(implode(', ', $tb['i'])); ?>">
|
||||||
|
<div class="marquee-track<?php echo $i % 2 ? ' marquee-track--reverse' : ''; ?>" style="--dur:<?php echo 26 + $i * 3; ?>s" aria-hidden="true">
|
||||||
|
<?php for ($k = 0; $k < 2; $k++) : ?>
|
||||||
|
<span><?php foreach ($tb['i'] as $j => $tool) : ?><b class="<?php echo ($j + $i) % 2 ? 'is-outline' : ''; ?>"><?php echo esc_html(mb_strtoupper($tool)); ?></b><i class="lime">✦</i><?php endforeach; ?></span>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── Méthode : numéro géant fixé, étapes qui défilent ─────────────── -->
|
||||||
|
<section class="cp-method page-wrap" aria-label="Ma méthode">
|
||||||
|
<div class="cp-method__aside">
|
||||||
|
<div class="kicker">MA MÉTHODE</div>
|
||||||
|
<h2 class="cp-h">QUAND ÇA <span class="outline-lime">CASSE</span></h2>
|
||||||
|
<div class="cp-method__big" aria-hidden="true">
|
||||||
|
<?php foreach ($method as $i => $m) : ?>
|
||||||
|
<span class="cp-method__num<?php echo $i === 0 ? ' is-on' : ''; ?>" data-step="<?php echo $i; ?>"><?php echo esc_html(str_pad($i + 1, 2, '0', STR_PAD_LEFT)); ?></span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ol class="cp-method__steps">
|
||||||
|
<?php foreach ($method as $i => $m) : ?>
|
||||||
|
<li class="cp-method__step<?php echo $i === 0 ? ' is-on' : ''; ?>" data-step="<?php echo $i; ?>">
|
||||||
|
<span class="cp-method__k"><?php echo esc_html(str_pad($i + 1, 2, '0', STR_PAD_LEFT)); ?> / <?php echo esc_html(str_pad(count($method), 2, '0', STR_PAD_LEFT)); ?></span>
|
||||||
|
<span class="cp-method__t"><?php echo esc_html(mb_strtoupper($m['t'])); ?></span>
|
||||||
|
<span class="cp-method__d"><?php echo esc_html($m['d']); ?></span>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── Infra servyass (terminal du design d'origine) ────────────────── -->
|
||||||
|
<section class="page-wrap cp-infra">
|
||||||
|
<div class="kicker">$ ssh yass@servyass</div>
|
||||||
|
<h2 class="infra-title" data-reveal>L'INFRA <span class="lime">SERVYASS</span> · EN PROD DEPUIS 2 ANS</h2>
|
||||||
|
<p class="infra-lead">Je ne me contente pas de lister des compétences : 2 ans d'infrastructure Linux/Docker auto-hébergée, administrée seul, en production réelle. Debian 12 · Docker Engine · Ryzen 3 3200G · 13 GB RAM.</p>
|
||||||
|
<div class="infra-cols">
|
||||||
|
<div class="term" data-reveal>
|
||||||
|
<div class="term-bar">
|
||||||
|
<span class="term-dot term-dot--r"></span><span class="term-dot term-dot--y"></span><span class="term-dot term-dot--g"></span>
|
||||||
|
<span class="term-title">yass@servyass: docker ps</span>
|
||||||
|
</div>
|
||||||
|
<div class="term-body">
|
||||||
|
<div class="term-muted">$ docker ps --format table · NAME · STATUS</div>
|
||||||
|
<?php foreach ($containers as $c) : ?>
|
||||||
|
<div class="term-row"><span><?php echo esc_html($c); ?></span><span class="lime">Up (healthy)</span></div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<div class="term-muted term-foot">$ uptime · ▲ en production depuis 2 ans, maintenu en autonomie</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="infra-points">
|
||||||
|
<?php foreach ($infra_points as $ip) : ?>
|
||||||
|
<div class="ipoint" data-reveal>
|
||||||
|
<div class="ipoint-title"><?php echo esc_html($ip['t']); ?></div>
|
||||||
|
<div class="ipoint-desc"><?php echo esc_html($ip['d']); ?></div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="infra-strip">FLUX EN DIRECT · 16+ CONTENEURS · 1 SEUL ADMIN · SUPERVISION UPTIME-KUMA · BACKUPS PLANIFIÉS · MÀJ SÉCU HEBDO</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── Preuves : survol d'une compétence = vignette du projet qui suit la souris ── -->
|
||||||
|
<?php if ($proofs) : ?>
|
||||||
|
<section class="cp-proofs" aria-label="Preuves">
|
||||||
|
<div class="page-wrap cp-head">
|
||||||
|
<div class="kicker">PREUVES À L'APPUI · PROJETS & ARTICLES</div>
|
||||||
|
<h2 class="cp-h" data-reveal>CHAQUE COMPÉTENCE <span class="outline-lime">A SON PROJET</span></h2>
|
||||||
|
</div>
|
||||||
|
<ul class="cp-proofs__list">
|
||||||
|
<?php foreach ($proofs as $skill => $links) :
|
||||||
|
$first = $links[0]; ?>
|
||||||
|
<li class="cp-proof" data-thumb="<?php echo esc_url($first['thumb']); ?>" data-name="<?php echo esc_attr($first['name']); ?>" data-cursor-label="<?php echo count($links) > 1 ? 'OUVRIR LA ROUE' : ($first['kind'] === 'article' ? 'LIRE L\'ARTICLE' : 'VOIR LE PROJET'); ?>">
|
||||||
|
<span class="cp-proof__skill"><?php echo esc_html($skill); ?></span>
|
||||||
|
<span class="cp-proof__links">
|
||||||
|
<?php foreach ($links as $l) : ?>
|
||||||
|
<a href="<?php echo esc_url(get_permalink($l['post'])); ?>" class="is-<?php echo esc_attr($l['kind']); ?>" data-wipe="<?php echo esc_attr($l['wipe']); ?>" data-cursor-label="<?php echo $l['kind'] === 'article' ? 'LIRE L\'ARTICLE' : 'VOIR LE PROJET'; ?>" data-thumb="<?php echo esc_url($l['thumb']); ?>" data-name="<?php echo esc_attr($l['name']); ?>" data-kind="<?php echo esc_attr($l['kind']); ?>" data-tag="<?php echo esc_attr($l['tag']); ?>"><?php if ($l['kind'] === 'article') : ?><span class="cp-proof__kind">ARTICLE</span> <?php endif; ?><?php echo esc_html($l['name']); ?> <span class="lime">→</span></a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<div class="cp-preview" aria-hidden="true"></div>
|
||||||
|
<div class="cp-wheel" hidden role="dialog" aria-label="Choisir un projet">
|
||||||
|
<div class="cp-wheel__svg" aria-hidden="true"></div>
|
||||||
|
<div class="cp-wheel__center"><span class="cp-wheel__skill"></span><span class="cp-wheel__name"></span><span class="cp-wheel__hint"></span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- ── Fin : appel à l'échange, comme sur l'accueil ─────────────────── -->
|
||||||
|
<section class="cta">
|
||||||
|
<div class="kicker" data-reveal><?php echo esc_html(mb_strtoupper(yb2026_id('accroche'))); ?></div>
|
||||||
|
<a class="cta__title" href="<?php echo esc_url(home_url('/contact/')); ?>" data-wipe="CONTACT" data-cursor-label="M'ÉCRIRE" data-reveal>TRAVAILLONS<br><span class="outline-lime">ENSEMBLE →</span></a>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php get_footer(); ?>
|
||||||
17
page-realisation.php
Normal file
17
page-realisation.php
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
/* Page « Réalisations » (slug /realisation/) : grille des projets */
|
||||||
|
get_header();
|
||||||
|
$projects = yb2026_get_projects();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-wrap">
|
||||||
|
<div class="kicker">PROJETS · FORMATION & CONTEXTE PRO</div>
|
||||||
|
<h1 class="page-title" data-reveal>MES<br><span class="outline-lime">RÉALISATIONS</span></h1>
|
||||||
|
<p class="page-lead">Du développement web à l'administration d'infrastructures. Cliquer sur un projet ouvre l'étude de cas.</p>
|
||||||
|
|
||||||
|
<div class="real-grid grid-auto">
|
||||||
|
<?php foreach ($projects as $p) yb2026_project_card($p); ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php get_footer(); ?>
|
||||||
50
single.php
Normal file
50
single.php
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
/* Article de blog */
|
||||||
|
get_header();
|
||||||
|
the_post();
|
||||||
|
|
||||||
|
$next = get_next_post();
|
||||||
|
if (!$next) $next = get_previous_post();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<article class="case">
|
||||||
|
<?php if (has_post_thumbnail()) : ?>
|
||||||
|
<div class="case-hero">
|
||||||
|
<?php the_post_thumbnail('full', array('class' => 'case-hero-img')); ?>
|
||||||
|
<div class="case-hero-fade"></div>
|
||||||
|
<div class="case-hero-text">
|
||||||
|
<div class="kicker rise rise-1">JOURNAL TECHNIQUE · <?php echo esc_html(strtoupper(get_the_date('j M Y'))); ?></div>
|
||||||
|
<h1 class="case-title case-title--post rise rise-2"><?php the_title(); ?></h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<div class="page-wrap page-wrap--tight">
|
||||||
|
<div class="kicker">JOURNAL TECHNIQUE · <?php echo esc_html(strtoupper(get_the_date('j M Y'))); ?></div>
|
||||||
|
<h1 class="page-title" data-reveal><?php the_title(); ?></h1>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="case-cols">
|
||||||
|
<aside class="case-meta article__meta">
|
||||||
|
<div><div class="case-meta-k">PUBLIÉ</div><div><?php echo esc_html(get_the_date('j F Y')); ?></div></div>
|
||||||
|
<?php $cats = get_the_category(); if ($cats) : ?>
|
||||||
|
<div><div class="case-meta-k">CATÉGORIE</div><div class="lime"><?php echo esc_html(strtoupper($cats[0]->name)); ?></div></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</aside>
|
||||||
|
<div class="case-body">
|
||||||
|
<div class="prose"><?php the_content(); ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($next) : ?>
|
||||||
|
<a class="case-next" href="<?php echo esc_url(get_permalink($next)); ?>" data-cursor-label="LIRE ↗" data-wipe="LECTURE">
|
||||||
|
<div>
|
||||||
|
<div class="case-next-k mono">ARTICLE SUIVANT</div>
|
||||||
|
<div class="case-next-name"><?php echo esc_html(get_the_title($next)); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="case-next-arrow mono">→</div>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<?php get_footer(); ?>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue