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
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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue