210 lines
13 KiB
PHP
210 lines
13 KiB
PHP
<?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;
|
|
});
|