Añadir mu-plugins y scripts de feadulta
This commit is contained in:
Executable
+75
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fe Adulta — Carta de la Semana
|
||||
* Description: Redirige las URLs de carta al archivo de categoría correspondiente.
|
||||
* Version: 1.8
|
||||
*/
|
||||
|
||||
// Redirigir las páginas custom a las categorías
|
||||
add_action("template_redirect", function() {
|
||||
if (is_page("carta-de-la-semana")) {
|
||||
wp_redirect(home_url("/category/cartasemana/"), 302);
|
||||
exit;
|
||||
}
|
||||
if (is_page("la-semana-pasada")) {
|
||||
wp_redirect(home_url("/category/carta-semana-pasada/"), 302);
|
||||
exit;
|
||||
}
|
||||
});
|
||||
|
||||
// Las categorías de carta actual/anterior siempre llevan al post traducido que
|
||||
// corresponde a la categoría española canónica. No dependemos del count ni de
|
||||
// las relaciones traducidas, que pueden quedar desfasadas durante una importación.
|
||||
add_action("template_redirect", function() {
|
||||
if (!is_category()) return;
|
||||
$cat = get_queried_object();
|
||||
if (!$cat || empty($cat->term_id)) return;
|
||||
|
||||
$source_cat_id = (int) $cat->term_id;
|
||||
if (function_exists('pll_get_term')) {
|
||||
$spanish_cat_id = (int) pll_get_term($source_cat_id, 'es');
|
||||
if ($spanish_cat_id) $source_cat_id = $spanish_cat_id;
|
||||
}
|
||||
if (!in_array($source_cat_id, [6, 22], true)) return;
|
||||
|
||||
global $wpdb;
|
||||
$source_post_id = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT p.ID
|
||||
FROM {$wpdb->posts} p
|
||||
INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
|
||||
INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
|
||||
WHERE tt.taxonomy = 'category' AND tt.term_id = %d
|
||||
AND p.post_type = 'post' AND p.post_status = 'publish'
|
||||
ORDER BY p.post_date DESC, p.ID DESC
|
||||
LIMIT 1",
|
||||
$source_cat_id
|
||||
));
|
||||
if (!$source_post_id) return;
|
||||
|
||||
$post_id = $source_post_id;
|
||||
if (function_exists('pll_current_language') && function_exists('pll_get_post')) {
|
||||
$lang = pll_current_language();
|
||||
$translated = $lang ? (int) pll_get_post($source_post_id, $lang) : 0;
|
||||
if ($translated) $post_id = $translated;
|
||||
}
|
||||
|
||||
$url = get_permalink($post_id);
|
||||
if (!$url) return;
|
||||
wp_safe_redirect($url, 302);
|
||||
exit;
|
||||
}, 9);
|
||||
|
||||
// Mostrar 50 artículos por página en los archivos de cartas
|
||||
add_action("pre_get_posts", function($query) {
|
||||
if (!$query->is_main_query() || is_admin()) return;
|
||||
if ($query->is_category([
|
||||
"cartasemana", "carta-semana-pasada", "cartas-de-otras-semanas",
|
||||
"letter-of-the-week", "lettre-de-la-semaine", "lettera-della-settimana", "carta-da-semana",
|
||||
"carta-semana-pasada-en", "carta-semana-pasada-fr",
|
||||
"carta-semana-pasada-it", "carta-semana-pasada-pt",
|
||||
"letters-from-other-weeks", "lettres-des-autres-semaines",
|
||||
"lettere-delle-altre-settimane", "cartas-de-outras-semanas",
|
||||
])) {
|
||||
$query->set("posts_per_page", 50);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fe Adulta Custom CSS
|
||||
* Description: Carga el CSS personalizado para el faldón
|
||||
*/
|
||||
|
||||
add_action("wp_enqueue_scripts", function() {
|
||||
wp_enqueue_style(
|
||||
"fa-custom-css",
|
||||
content_url("/uploads/astra-custom.css"),
|
||||
array(),
|
||||
time()
|
||||
);
|
||||
}, 999);
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fea Analytics — GA4 (#93)
|
||||
* Description: Inserta Google Analytics 4 (gtag.js) en el <head> del front-end.
|
||||
* Measurement ID portable: el cutover wp-nuevo.feadulta.com -> www.feadulta.com
|
||||
* no requiere tocar nada (el ID no cambia; la Stream URL es informativa).
|
||||
* NO se filtra tráfico interno (decisión Rafa: las IPs propias cuentan).
|
||||
* Nota RGPD: GA4 setea cookies; si se añade banner de consentimiento habrá
|
||||
* que condicionar este tag (Consent Mode) — pendiente, no implementado aquí.
|
||||
*/
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
define('FEA_GA4_MEASUREMENT_ID', 'G-6RT9ZRS4LW');
|
||||
|
||||
add_action('wp_head', function () {
|
||||
// wp_head no corre en wp-admin; guard extra por seguridad.
|
||||
if (is_admin()) return;
|
||||
$gid = FEA_GA4_MEASUREMENT_ID;
|
||||
?>
|
||||
<!-- Google tag (gtag.js) - GA4 #93 -->
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
// Consent Mode v2: por defecto DENEGADO hasta que el usuario acepte en el
|
||||
// banner (fea-cookie-consent.php hace el consent 'update'). HTML idéntico
|
||||
// para todos (cacheable por Cloudflare); el estado por usuario lo aplica el
|
||||
// banner en JS leyendo la cookie.
|
||||
gtag('consent', 'default', {
|
||||
'ad_storage': 'denied',
|
||||
'ad_user_data': 'denied',
|
||||
'ad_personalization': 'denied',
|
||||
'analytics_storage': 'denied',
|
||||
'wait_for_update': 500
|
||||
});
|
||||
gtag('js', new Date());
|
||||
gtag('config', '<?php echo esc_js($gid); ?>');
|
||||
</script>
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=<?php echo esc_attr($gid); ?>"></script>
|
||||
<?php
|
||||
}, 1);
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fea Audio Player (#76)
|
||||
* Description: Reproductor TTS compacto en la fila del autor (a la derecha) cuando
|
||||
* existe el meta fea_audio_url (voz Nico, MiniMax HD). Se inyecta como
|
||||
* último hijo del grupo flex del autor (template FSE) vía render_block.
|
||||
*/
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
/** Devuelve el HTML del reproductor para el post actual, o '' si no hay audio. */
|
||||
function fea_audio_player_html(): string {
|
||||
$url = get_post_meta(get_the_ID(), 'fea_audio_url', true);
|
||||
if (!$url) return '';
|
||||
return '<div class="fea-audio">'
|
||||
. '<span class="fea-audio-label">'
|
||||
. '<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false">'
|
||||
. '<path fill="currentColor" d="M3 10v4h4l5 5V5L7 10H3zm13.5 2a4.5 4.5 0 0 0-2.5-4.03v8.06A4.5 4.5 0 0 0 16.5 12zM14 3.23v2.06a7 7 0 0 1 0 13.42v2.06a9 9 0 0 0 0-17.54z"/>'
|
||||
. '</svg> Escucha</span>'
|
||||
. '<audio controls preload="none" src="' . esc_url($url) . '"></audio>'
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
// Inyecta el player como último hijo de la fila flex del autor (el grupo que
|
||||
// contiene el core/avatar en la cabecera FSE del single).
|
||||
add_filter('render_block', function ($html, $block) {
|
||||
if (!is_singular('post')) return $html;
|
||||
if (($block['blockName'] ?? '') !== 'core/group') return $html;
|
||||
$has_avatar = false;
|
||||
foreach ($block['innerBlocks'] ?? [] as $ib) {
|
||||
if (($ib['blockName'] ?? '') === 'core/avatar') { $has_avatar = true; break; }
|
||||
}
|
||||
if (!$has_avatar) return $html;
|
||||
$player = fea_audio_player_html();
|
||||
if (!$player) return $html;
|
||||
$pos = strrpos($html, '</div>');
|
||||
return $pos === false ? $html . $player : substr($html, 0, $pos) . $player . substr($html, $pos);
|
||||
}, 10, 2);
|
||||
|
||||
add_action('wp_head', function () {
|
||||
if (!is_singular('post')) return;
|
||||
if (!get_post_meta(get_queried_object_id(), 'fea_audio_url', true)) return;
|
||||
?>
|
||||
<style>
|
||||
/* El player se inyecta en el grupo flex del autor. En el single FSE ese
|
||||
contenedor es el wp-block-group (no .fea-byline, que solo existe en Astra),
|
||||
así que el wrap debe apuntar también a él o en móvil vertical el <audio>
|
||||
no encuentra ancho y no muestra controles (#142). */
|
||||
.fea-byline,
|
||||
.wp-block-group:has(> .wp-block-avatar):has(.wp-block-post-author-name){flex-wrap:wrap}
|
||||
.fea-audio{display:flex;align-items:center;gap:.5rem;margin-left:auto;
|
||||
padding:.3rem .55rem;background:#faf6f7;border:1px solid #e7d6da;
|
||||
border-left:3px solid #8b1a2e;border-radius:8px}
|
||||
.fea-audio-label{display:inline-flex;align-items:center;gap:.35rem;font-size:.78rem;
|
||||
font-weight:600;color:#8b1a2e;white-space:nowrap;line-height:1}
|
||||
.fea-audio audio{height:32px;width:230px;max-width:44vw}
|
||||
@media(max-width:600px){
|
||||
.fea-audio{margin-left:0;width:100%;margin-top:.5rem}
|
||||
.fea-audio audio{flex:1 1 auto;width:auto;max-width:none}
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fea Avatar Cache-bust (#81)
|
||||
* Description: Añade ?v=<mtime> a las URLs de avatar servidas desde
|
||||
* uploads/avatares/autores/autor-<uid>.png. Como al actualizar la foto se
|
||||
* reescribe el MISMO fichero, sin esto el navegador/Cloudflare siguen sirviendo
|
||||
* la versión cacheada. Corre tras el filtro de fea-homepage (prioridad 20).
|
||||
*/
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
add_filter('get_avatar_url', function ($url, $id_or_email, $args) {
|
||||
if (!is_string($url) || strpos($url, '/avatares/autores/autor-') === false) return $url;
|
||||
$rel = preg_replace('~\?.*$~', '', substr($url, strpos($url, '/avatares/')));
|
||||
$path = wp_get_upload_dir()['basedir'] . $rel;
|
||||
if (file_exists($path)) $url = add_query_arg('v', filemtime($path), $url);
|
||||
return $url;
|
||||
}, 20, 3);
|
||||
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fe Adulta — Feedback Beta
|
||||
* Description: Barra sutil de aviso "Beta" en todo el sitio + mini formulario (👍/👎 +
|
||||
* comentario opcional) que se abre a demanda, para que el público ayude a
|
||||
* encontrar errores. Guarda cada voto como "Beta Feedback" (CPT propio),
|
||||
* legible en wp-admin en una sola lista. No usa el sistema de comentarios.
|
||||
* Version: 1.1
|
||||
*
|
||||
* Ver issue rafa/feadulta#78.
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
const FEA_FB_CPT = 'fea_feedback';
|
||||
const FEA_FB_RATE_MAX = 12; // máximo de envíos por IP por hora
|
||||
const FEA_FB_COMMENT_MAX = 2000;
|
||||
|
||||
/* ── 1) CPT donde se guardan los votos (solo backend) ─────────────────────── */
|
||||
add_action('init', function () {
|
||||
register_post_type(FEA_FB_CPT, [
|
||||
'labels' => [
|
||||
'name' => 'Beta Feedback',
|
||||
'singular_name' => 'Feedback',
|
||||
'menu_name' => 'Beta Feedback',
|
||||
],
|
||||
'public' => false,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => true,
|
||||
'menu_icon' => 'dashicons-feedback',
|
||||
'menu_position' => 26,
|
||||
'capability_type' => 'post',
|
||||
'capabilities' => ['create_posts' => 'do_not_allow'], // solo se crean por API
|
||||
'map_meta_cap' => true,
|
||||
'supports' => ['title', 'editor'],
|
||||
'exclude_from_search' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
/* ── 2a) Endpoint de consulta de idioma Polylang (para publicabot / integración externa) ── */
|
||||
add_action('rest_api_init', function () {
|
||||
register_rest_route('fea/v1', '/lang/(?P<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'permission_callback' => '__return_true',
|
||||
'callback' => function (WP_REST_Request $req) {
|
||||
$id = (int) $req->get_param('id');
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== 'post') {
|
||||
return new WP_REST_Response(['error' => 'post not found'], 404);
|
||||
}
|
||||
$langs = wp_get_object_terms($id, 'language', ['fields' => 'slugs']);
|
||||
$lang = (!is_wp_error($langs) && !empty($langs)) ? $langs[0] : null;
|
||||
return new WP_REST_Response(['id' => $id, 'lang' => $lang], 200);
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
/* ── 2) Endpoint REST para recibir el voto ────────────────────────────────── */
|
||||
add_action('rest_api_init', function () {
|
||||
register_rest_route('fea/v1', '/feedback', [
|
||||
'methods' => 'POST',
|
||||
'permission_callback' => '__return_true', // público (Beta); protegido con honeypot + rate-limit
|
||||
'callback' => 'fea_feedback_submit',
|
||||
]);
|
||||
});
|
||||
|
||||
function fea_feedback_submit(WP_REST_Request $req) {
|
||||
// Honeypot: si el campo oculto viene relleno, es un bot.
|
||||
if (trim((string) $req->get_param('website')) !== '') {
|
||||
return new WP_REST_Response(['ok' => true], 200); // fingir éxito
|
||||
}
|
||||
|
||||
$vote = $req->get_param('vote') === 'up' ? 'up' : ($req->get_param('vote') === 'down' ? 'down' : '');
|
||||
if ($vote === '') {
|
||||
return new WP_REST_Response(['ok' => false, 'error' => 'voto inválido'], 400);
|
||||
}
|
||||
|
||||
// Rate-limit por IP.
|
||||
$ip = fea_feedback_client_ip();
|
||||
$key = 'fea_fb_rl_' . md5($ip);
|
||||
$n = (int) get_transient($key);
|
||||
if ($n >= FEA_FB_RATE_MAX) {
|
||||
return new WP_REST_Response(['ok' => false, 'error' => 'demasiados envíos'], 429);
|
||||
}
|
||||
set_transient($key, $n + 1, HOUR_IN_SECONDS);
|
||||
|
||||
$comment = trim((string) $req->get_param('comment'));
|
||||
$comment = mb_substr(wp_strip_all_tags($comment), 0, FEA_FB_COMMENT_MAX);
|
||||
$url = esc_url_raw((string) $req->get_param('url'));
|
||||
$src_id = (int) $req->get_param('post_id');
|
||||
$lang = preg_replace('/[^a-z]/', '', (string) $req->get_param('lang'));
|
||||
$title = (string) $req->get_param('title');
|
||||
|
||||
$emoji = $vote === 'up' ? '👍' : '👎';
|
||||
$post_id = wp_insert_post([
|
||||
'post_type' => FEA_FB_CPT,
|
||||
'post_status' => 'private',
|
||||
'post_title' => sprintf('%s %s', $emoji, $title ?: $url),
|
||||
'post_content' => $comment,
|
||||
], true);
|
||||
|
||||
if (is_wp_error($post_id)) {
|
||||
return new WP_REST_Response(['ok' => false, 'error' => 'no se pudo guardar'], 500);
|
||||
}
|
||||
|
||||
update_post_meta($post_id, '_fea_fb_vote', $vote);
|
||||
update_post_meta($post_id, '_fea_fb_url', $url);
|
||||
update_post_meta($post_id, '_fea_fb_source_id', $src_id);
|
||||
update_post_meta($post_id, '_fea_fb_lang', $lang);
|
||||
update_post_meta($post_id, '_fea_fb_ua', mb_substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255));
|
||||
update_post_meta($post_id, '_fea_fb_ip', md5($ip)); // hash, no IP en claro
|
||||
|
||||
return new WP_REST_Response(['ok' => true], 200);
|
||||
}
|
||||
|
||||
function fea_feedback_client_ip(): string {
|
||||
foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'] as $k) {
|
||||
if (!empty($_SERVER[$k])) return trim(explode(',', $_SERVER[$k])[0]);
|
||||
}
|
||||
return '0.0.0.0';
|
||||
}
|
||||
|
||||
/* ── 3) Columnas en el listado del wp-admin ───────────────────────────────── */
|
||||
add_filter('manage_' . FEA_FB_CPT . '_posts_columns', function ($cols) {
|
||||
return [
|
||||
'cb' => $cols['cb'] ?? '',
|
||||
'fb_vote' => 'Voto',
|
||||
'fb_url' => 'Página',
|
||||
'fb_lang' => 'Idioma',
|
||||
'fb_comment'=> 'Comentario',
|
||||
'date' => 'Fecha',
|
||||
];
|
||||
});
|
||||
add_action('manage_' . FEA_FB_CPT . '_posts_custom_column', function ($col, $post_id) {
|
||||
if ($col === 'fb_vote') {
|
||||
echo get_post_meta($post_id, '_fea_fb_vote', true) === 'up' ? '👍' : '👎';
|
||||
} elseif ($col === 'fb_url') {
|
||||
$u = get_post_meta($post_id, '_fea_fb_url', true);
|
||||
if ($u) echo '<a href="' . esc_url($u) . '" target="_blank" rel="noopener">' . esc_html(wp_parse_url($u, PHP_URL_PATH) ?: $u) . '</a>';
|
||||
} elseif ($col === 'fb_lang') {
|
||||
echo esc_html(strtoupper(get_post_meta($post_id, '_fea_fb_lang', true) ?: '—'));
|
||||
} elseif ($col === 'fb_comment') {
|
||||
echo esc_html(wp_trim_words(get_post_field('post_content', $post_id), 24));
|
||||
}
|
||||
}, 10, 2);
|
||||
|
||||
/* ── 4) Barra Beta sutil (persistente) + tarjeta de feedback (a demanda) ──── */
|
||||
/** Etiquetas del widget Beta por idioma (Polylang). */
|
||||
function fea_beta_labels(): array {
|
||||
$all = [
|
||||
'es' => ['region'=>'Aviso Beta','intro'=>'Estamos en','help'=>'¿Nos ayudas a mejorar FeAdulta?',
|
||||
'opinion'=>'Dar mi opinión','collab'=>'Colaborar','dismiss'=>'Cerrar aviso','fbregion'=>'Feedback de la página',
|
||||
'close'=>'Cerrar','q'=>'¿Se ve bien esta página?','up'=>'Sí, se ve bien','down'=>'No, hay algo mal',
|
||||
'ph'=>'¿Algo falla o se ve mal? Cuéntanoslo (opcional)','send'=>'Enviar','thanks'=>'¡Gracias por ayudar! 🙏'],
|
||||
'en' => ['region'=>'Beta notice','intro'=>'We are in','help'=>'Will you help us improve FeAdulta?',
|
||||
'opinion'=>'Give feedback','collab'=>'Collaborate','dismiss'=>'Close notice','fbregion'=>'Page feedback',
|
||||
'close'=>'Close','q'=>'Does this page look right?','up'=>'Yes, looks good','down'=>'No, something is wrong',
|
||||
'ph'=>'Something broken or off? Tell us (optional)','send'=>'Send','thanks'=>'Thanks for helping! 🙏'],
|
||||
'fr' => ['region'=>'Avis Bêta','intro'=>'Nous sommes en','help'=>'Voulez-vous nous aider à améliorer FeAdulta ?',
|
||||
'opinion'=>'Donner mon avis','collab'=>'Collaborer','dismiss'=>'Fermer l’avis','fbregion'=>'Retour sur la page',
|
||||
'close'=>'Fermer','q'=>'Cette page s’affiche-t-elle bien ?','up'=>'Oui, c’est bien','down'=>'Non, il y a un problème',
|
||||
'ph'=>'Un souci ou un affichage incorrect ? Dites-le-nous (facultatif)','send'=>'Envoyer','thanks'=>'Merci de votre aide ! 🙏'],
|
||||
'it' => ['region'=>'Avviso Beta','intro'=>'Siamo in','help'=>'Ci aiuti a migliorare FeAdulta?',
|
||||
'opinion'=>'Dai la tua opinione','collab'=>'Collabora','dismiss'=>'Chiudi avviso','fbregion'=>'Feedback della pagina',
|
||||
'close'=>'Chiudi','q'=>'Questa pagina si vede bene?','up'=>'Sì, si vede bene','down'=>'No, c’è qualcosa che non va',
|
||||
'ph'=>'Qualcosa non va o si vede male? Faccelo sapere (facoltativo)','send'=>'Invia','thanks'=>'Grazie per l’aiuto! 🙏'],
|
||||
'pt' => ['region'=>'Aviso Beta','intro'=>'Estamos em','help'=>'Ajuda-nos a melhorar a FeAdulta?',
|
||||
'opinion'=>'Dar a minha opinião','collab'=>'Colaborar','dismiss'=>'Fechar aviso','fbregion'=>'Feedback da página',
|
||||
'close'=>'Fechar','q'=>'Esta página vê-se bem?','up'=>'Sim, vê-se bem','down'=>'Não, há algo errado',
|
||||
'ph'=>'Algo falha ou vê-se mal? Conta-nos (opcional)','send'=>'Enviar','thanks'=>'Obrigado por ajudar! 🙏'],
|
||||
];
|
||||
$lang = function_exists('pll_current_language') ? (string) pll_current_language() : 'es';
|
||||
return $all[$lang] ?? $all['es'];
|
||||
}
|
||||
|
||||
add_action('wp_footer', function () {
|
||||
if (is_admin()) return;
|
||||
$rest = esc_url_raw(rest_url('fea/v1/feedback'));
|
||||
$t = fea_beta_labels();
|
||||
?>
|
||||
<style>
|
||||
/* Barra sutil de aviso Beta, abajo, full-width */
|
||||
#fea-beta-bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 99997;
|
||||
background: #faf6f2; border-top: 1px solid #e6ddd5; color: #4a3b34;
|
||||
font-family: inherit; font-size: .86rem; line-height: 1.3;
|
||||
padding: 8px 44px 8px 16px; text-align: center; }
|
||||
#fea-beta-bar strong { color: #8b1a2e; }
|
||||
#fea-beta-bar .fea-beta-open { margin-left: 10px; cursor: pointer; border: 1px solid #8b1a2e;
|
||||
background: #8b1a2e; color: #fff; border-radius: 6px; padding: 4px 12px; font-size: .82rem; font-weight: 600; }
|
||||
#fea-beta-bar .fea-beta-open:hover { background: #761526; }
|
||||
#fea-beta-bar .fea-beta-collab { margin-left: 8px; cursor: pointer; display: inline-block;
|
||||
border: 1px solid #1b7a34; background: #1b7a34; color: #fff; border-radius: 6px;
|
||||
padding: 4px 12px; font-size: .82rem; font-weight: 600; text-decoration: none; }
|
||||
#fea-beta-bar .fea-beta-collab:hover { background: #15642a; }
|
||||
#fea-beta-bar .fea-beta-dismiss { position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
|
||||
border: 0; background: none; font-size: 1.15rem; cursor: pointer; color: #8a7a72; padding: 2px 6px; line-height: 1; }
|
||||
#fea-beta-bar.hidden { display: none; }
|
||||
|
||||
/* Tarjeta de feedback: oculta hasta que el usuario la abre desde la barra */
|
||||
#fea-fb { position: fixed; right: 16px; bottom: 56px; z-index: 99998; font-family: inherit; max-width: 300px; }
|
||||
#fea-fb[hidden] { display: none; }
|
||||
#fea-fb .fea-fb-card { background:#fff; border:1px solid #e2e2e2; border-radius:12px;
|
||||
box-shadow:0 8px 28px rgba(0,0,0,.16); padding:12px 14px; font-size:.9rem; color:#222; position:relative; }
|
||||
#fea-fb .fea-fb-q { margin:0 0 8px; line-height:1.3; padding-right:16px; }
|
||||
#fea-fb .fea-fb-btns { display:flex; gap:8px; }
|
||||
#fea-fb button.fea-fb-vote { cursor:pointer; border:1px solid #ccc; background:#fafafa; border-radius:8px;
|
||||
padding:6px 12px; font-size:1.05rem; line-height:1; }
|
||||
#fea-fb button.fea-fb-vote:hover { background:#f0f0f0; }
|
||||
#fea-fb button.fea-fb-vote.sel { border-color:#8b1a2e; background:#f7e9ec; }
|
||||
#fea-fb textarea { width:100%; margin:9px 0 8px; border:1px solid #ccc; border-radius:8px; padding:7px;
|
||||
font:inherit; font-size:.85rem; resize:vertical; min-height:58px; box-sizing:border-box; }
|
||||
#fea-fb .fea-fb-send { background:#8b1a2e; color:#fff; border:1px solid #8b1a2e; border-radius:8px;
|
||||
padding:6px 12px; font-size:.85rem; width:100%; cursor:pointer; }
|
||||
#fea-fb .fea-fb-hp { position:absolute; left:-9999px; }
|
||||
#fea-fb .fea-fb-close { position:absolute; top:4px; right:8px; border:0; background:none; font-size:1rem; cursor:pointer; padding:2px 4px; line-height:1; }
|
||||
@media (max-width:600px){ #fea-fb{ right:10px; left:10px; max-width:none; } #fea-beta-bar{ font-size:.8rem; } }
|
||||
</style>
|
||||
|
||||
<div id="fea-beta-bar" class="hidden" role="region" aria-label="<?php echo esc_attr($t['region']); ?>">
|
||||
🌱 <?php echo esc_html($t['intro']); ?> <strong>Beta</strong>. <?php echo esc_html($t['help']); ?>
|
||||
<button type="button" class="fea-beta-open"><?php echo esc_html($t['opinion']); ?></button>
|
||||
<a class="fea-beta-collab" href="https://edicionesfeadulta.com/colabora/" target="_blank" rel="noopener"><?php echo esc_html($t['collab']); ?></a>
|
||||
<button type="button" class="fea-beta-dismiss" aria-label="<?php echo esc_attr($t['dismiss']); ?>">×</button>
|
||||
</div>
|
||||
|
||||
<div id="fea-fb" hidden role="complementary" aria-label="<?php echo esc_attr($t['fbregion']); ?>">
|
||||
<div class="fea-fb-card">
|
||||
<button type="button" class="fea-fb-close" aria-label="<?php echo esc_attr($t['close']); ?>">×</button>
|
||||
<p class="fea-fb-q"><?php echo esc_html($t['q']); ?></p>
|
||||
<div class="fea-fb-btns">
|
||||
<button type="button" class="fea-fb-vote" data-vote="up" aria-label="<?php echo esc_attr($t['up']); ?>">👍</button>
|
||||
<button type="button" class="fea-fb-vote" data-vote="down" aria-label="<?php echo esc_attr($t['down']); ?>">👎</button>
|
||||
</div>
|
||||
<div class="fea-fb-more" hidden>
|
||||
<input type="text" class="fea-fb-hp" tabindex="-1" autocomplete="off" aria-hidden="true" placeholder="No rellenar">
|
||||
<textarea placeholder="<?php echo esc_attr($t['ph']); ?>"></textarea>
|
||||
<button type="button" class="fea-fb-send"><?php echo esc_html($t['send']); ?></button>
|
||||
</div>
|
||||
<div class="fea-fb-thanks" hidden><?php echo esc_html($t['thanks']); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var bar = document.getElementById('fea-beta-bar');
|
||||
var box = document.getElementById('fea-fb');
|
||||
if(!bar || !box) return;
|
||||
var REST = <?php echo json_encode($rest); ?>;
|
||||
var pid = <?php echo (int) (is_singular() ? get_queried_object_id() : 0); ?>;
|
||||
var lang = <?php echo json_encode(function_exists('pll_current_language') ? (string) pll_current_language() : ''); ?>;
|
||||
var chosen = null;
|
||||
var moreEl = box.querySelector('.fea-fb-more');
|
||||
var votes = box.querySelectorAll('.fea-fb-vote');
|
||||
var thanks = box.querySelector('.fea-fb-thanks');
|
||||
|
||||
// Mostrar la barra salvo que el usuario la haya descartado antes.
|
||||
try { if (!localStorage.getItem('fea_beta_bar_off')) bar.classList.remove('hidden'); }
|
||||
catch(e){ bar.classList.remove('hidden'); }
|
||||
|
||||
function openCard(){ box.hidden = false; }
|
||||
function closeCard(){ box.hidden = true; }
|
||||
|
||||
bar.querySelector('.fea-beta-open').addEventListener('click', openCard);
|
||||
bar.querySelector('.fea-beta-dismiss').addEventListener('click', function(){
|
||||
bar.classList.add('hidden');
|
||||
try { localStorage.setItem('fea_beta_bar_off','1'); } catch(e){}
|
||||
});
|
||||
box.querySelector('.fea-fb-close').addEventListener('click', closeCard);
|
||||
|
||||
votes.forEach(function(b){ b.addEventListener('click', function(){
|
||||
chosen = b.getAttribute('data-vote');
|
||||
votes.forEach(function(x){ x.classList.toggle('sel', x===b); });
|
||||
moreEl.hidden = false;
|
||||
});});
|
||||
|
||||
box.querySelector('.fea-fb-send').addEventListener('click', function(){
|
||||
if(!chosen) return;
|
||||
var hp = box.querySelector('.fea-fb-hp').value;
|
||||
var comment = box.querySelector('textarea').value;
|
||||
fetch(REST, { method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ vote:chosen, comment:comment, url:location.href, post_id:pid,
|
||||
lang:lang, title:document.title, website:hp }) }).catch(function(){});
|
||||
box.querySelector('.fea-fb-btns').hidden = true;
|
||||
box.querySelector('.fea-fb-q').hidden = true;
|
||||
moreEl.hidden = true; thanks.hidden = false;
|
||||
setTimeout(closeCard, 2200);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}, 40);
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fe Adulta — Carta → Portada
|
||||
* Description: Parser de la carta semanal. Extrae los links de cada sección de la
|
||||
* carta y los expone para que los shortcodes de portada los rendericen.
|
||||
* Version: 1.0
|
||||
*
|
||||
* Modelo: cada carta semanal es un post HTML con secciones encabezadas
|
||||
* (Evangelio, Artículos, Eucaristía, Multimedia, EFFA). Los links DENTRO de
|
||||
* cada sección son lo que la portada debe mostrar en su shortcode equivalente.
|
||||
*
|
||||
* Ver issue rafa/feadulta#38.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Devuelve el post-carta vigente para un idioma (más reciente en cat 6).
|
||||
* Si Polylang está activo y hay traducción del idioma, devuelve la traducida.
|
||||
*/
|
||||
function fea_get_current_carta_id($lang = null) {
|
||||
static $cache = [];
|
||||
$lang = $lang ?: (function_exists('pll_current_language') ? pll_current_language() : 'es');
|
||||
if ($lang === false || $lang === null) $lang = 'es';
|
||||
if (isset($cache[$lang])) return $cache[$lang];
|
||||
|
||||
$cat_es = 6;
|
||||
$cat = function_exists('fea_cat') ? fea_cat($cat_es) : $cat_es;
|
||||
|
||||
$cartas = get_posts([
|
||||
'posts_per_page' => 1,
|
||||
'category__in' => [$cat],
|
||||
'post_status' => 'publish',
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
'suppress_filters' => false,
|
||||
]);
|
||||
if (!$cartas) return $cache[$lang] = 0;
|
||||
|
||||
$carta_id = (int) $cartas[0]->ID;
|
||||
|
||||
if ($lang !== 'es' && function_exists('pll_get_post')) {
|
||||
$trans = pll_get_post($carta_id, $lang);
|
||||
if ($trans) $carta_id = (int) $trans;
|
||||
}
|
||||
|
||||
return $cache[$lang] = $carta_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsea el HTML de la carta y devuelve los post_ids agrupados por sección.
|
||||
*/
|
||||
function fea_parse_carta_sections($carta_id) {
|
||||
static $mem = [];
|
||||
$carta_id = (int) $carta_id;
|
||||
if (!$carta_id) return [];
|
||||
if (isset($mem[$carta_id])) return $mem[$carta_id];
|
||||
|
||||
$tk = 'fea_carta_sections_' . $carta_id;
|
||||
$cached = get_transient($tk);
|
||||
if (is_array($cached)) return $mem[$carta_id] = $cached;
|
||||
|
||||
$post = get_post($carta_id);
|
||||
if (!$post) return $mem[$carta_id] = [];
|
||||
|
||||
$sections = fea_extract_sections_from_html($post->post_content);
|
||||
|
||||
set_transient($tk, $sections, 15 * MINUTE_IN_SECONDS);
|
||||
return $mem[$carta_id] = $sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae secciones del HTML. Pública para tests/CLI.
|
||||
*/
|
||||
function fea_extract_sections_from_html($html) {
|
||||
$section_patterns = [
|
||||
'evangelio' => '/Evangelio\s+y\s+comentarios\s+al\s+Evangelio/iu',
|
||||
'articulos' => '/Art[ií]culos\s+seleccionados\s+para\s+la\s+semana/iu',
|
||||
'eucaristia' => '/Para\s+unas\s+eucarist[ií]as\s+m[áa]s\s+participativas/iu',
|
||||
'multimedia' => '/Material\s+multimedia/iu',
|
||||
'effa' => '/Escuela\s+EFFA/iu',
|
||||
];
|
||||
|
||||
$positions = [];
|
||||
foreach ($section_patterns as $slug => $regex) {
|
||||
if (preg_match($regex, $html, $m, PREG_OFFSET_CAPTURE)) {
|
||||
$positions[$slug] = $m[0][1];
|
||||
}
|
||||
}
|
||||
if (empty($positions)) return [];
|
||||
asort($positions);
|
||||
|
||||
$slugs = array_keys($positions);
|
||||
$offsets = array_values($positions);
|
||||
$offsets[] = strlen($html);
|
||||
|
||||
$sections = [];
|
||||
for ($i = 0; $i < count($slugs); $i++) {
|
||||
$segment = substr($html, $offsets[$i], $offsets[$i+1] - $offsets[$i]);
|
||||
$ids = fea_resolve_links_in_html($segment);
|
||||
if ($ids) $sections[$slugs[$i]] = $ids;
|
||||
}
|
||||
return $sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae los href de un fragmento HTML y los resuelve a wp_posts.ID.
|
||||
*/
|
||||
function fea_resolve_links_in_html($html) {
|
||||
if (!preg_match_all('/href=["\']([^"\']+)["\']/i', $html, $m)) return [];
|
||||
$ids = [];
|
||||
$seen = [];
|
||||
foreach ($m[1] as $url) {
|
||||
$pid = fea_url_to_post_id($url);
|
||||
if ($pid && !isset($seen[$pid])) {
|
||||
$seen[$pid] = true;
|
||||
$ids[] = $pid;
|
||||
}
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resuelve una URL (WP o Joomla legacy) a wp_posts.ID o null.
|
||||
*/
|
||||
function fea_url_to_post_id($url) {
|
||||
global $wpdb;
|
||||
|
||||
// Joomla legacy: /item/<k2id>-...html
|
||||
if (preg_match('~/item/(\d+)-[^/"]+\.html~i', $url, $m)) {
|
||||
$k2 = (int) $m[1];
|
||||
if ($k2 > 0) {
|
||||
$pid = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key='_fgj2wp_old_k2_id' AND meta_value=%s LIMIT 1",
|
||||
(string) $k2
|
||||
));
|
||||
if ($pid) return (int) $pid;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Enlace interno WP: deriva el slug del path, relativo al home. Agnóstico al
|
||||
// entorno → funciona en local (home en .../fea) y en prod (home en la raíz).
|
||||
// No depende de un prefijo /fea/ hardcodeado (issue #91).
|
||||
$host = wp_parse_url($url, PHP_URL_HOST);
|
||||
$home_host = wp_parse_url(home_url(), PHP_URL_HOST);
|
||||
if ($host && $home_host && strcasecmp($host, $home_host) !== 0) {
|
||||
return null; // host externo → no es un artículo nuestro
|
||||
}
|
||||
|
||||
$path = (string) wp_parse_url($url, PHP_URL_PATH);
|
||||
if ($path === '') return null;
|
||||
$home_path = rtrim((string) wp_parse_url(home_url('/'), PHP_URL_PATH), '/');
|
||||
if ($home_path !== '' && strpos($path, $home_path . '/') === 0) {
|
||||
$path = substr($path, strlen($home_path));
|
||||
}
|
||||
$seg = explode('/', trim($path, '/'));
|
||||
$slug = $seg[0] ?? '';
|
||||
if ($slug === '' || in_array($slug, ['wp-admin','wp-content','category','tag','author','page','en','fr','it','pt'], true)) {
|
||||
return null;
|
||||
}
|
||||
$pid = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT ID FROM {$wpdb->posts}
|
||||
WHERE post_name=%s AND post_status='publish' AND post_type='post'
|
||||
ORDER BY post_date DESC LIMIT 1",
|
||||
$slug
|
||||
));
|
||||
return $pid ? (int) $pid : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve los WP_Post objects de una sección de la carta vigente,
|
||||
* o array vacío si no hay carta o no hay links resueltos en esa sección.
|
||||
*/
|
||||
function fea_carta_section_posts($section_slug, $lang = null) {
|
||||
$lang = $lang ?: (function_exists('fea_current_lang') ? fea_current_lang() : 'es');
|
||||
|
||||
// El parser de secciones reconoce las cabeceras SOLO en español
|
||||
// (fea_extract_sections_from_html). Las cartas traducidas tienen las
|
||||
// cabeceras en su idioma → 0 secciones. Por eso parseamos SIEMPRE la
|
||||
// carta ES y luego mapeamos cada link a su traducción del idioma destino.
|
||||
$carta_id = fea_get_current_carta_id($lang);
|
||||
if (!$carta_id) return [];
|
||||
$carta_es = $carta_id;
|
||||
if ($lang !== 'es' && function_exists('pll_get_post')) {
|
||||
$es = pll_get_post($carta_id, 'es');
|
||||
if ($es) $carta_es = (int) $es;
|
||||
}
|
||||
|
||||
$sections = fea_parse_carta_sections($carta_es);
|
||||
$ids = $sections[$section_slug] ?? [];
|
||||
if (!$ids) return [];
|
||||
$posts = [];
|
||||
foreach ($ids as $pid) {
|
||||
// Mapear el artículo ES a su traducción del idioma de la portada.
|
||||
// Si no hay traducción, se mantiene el ES (degradación elegante).
|
||||
if ($lang !== 'es' && function_exists('pll_get_post')) {
|
||||
$tr = pll_get_post((int) $pid, $lang);
|
||||
if ($tr) $pid = (int) $tr;
|
||||
}
|
||||
$p = get_post($pid);
|
||||
if ($p && $p->post_status === 'publish') $posts[] = $p;
|
||||
}
|
||||
return $posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalida transients de secciones al guardar/editar un post.
|
||||
*/
|
||||
add_action('save_post_post', function($post_id, $post, $update) {
|
||||
$cats = wp_get_post_categories($post_id);
|
||||
$watch = [6, 21, 22];
|
||||
if (array_intersect($cats, $watch)) {
|
||||
global $wpdb;
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_fea_carta_sections_%'");
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_fea_carta_sections_%'");
|
||||
}
|
||||
}, 10, 3);
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fe Adulta - compact entry spacing
|
||||
* Description: Ajusta el aire vertical de la navegacion de entradas y la paginacion de archivos.
|
||||
*/
|
||||
|
||||
add_action('wp_head', function() {
|
||||
if (is_admin()) return;
|
||||
if (!(is_single() || is_archive() || is_search() || is_home())) return;
|
||||
?>
|
||||
<style>
|
||||
/* Issue #67: compactar el cierre del single post sin tocar el template FSE. */
|
||||
body.single-post .wp-block-group.alignwide:has(> nav[aria-label="Navegación de entradas"]) {
|
||||
margin-top: 1rem !important;
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
body.single-post nav[aria-label="Navegación de entradas"] {
|
||||
padding-top: 0.75rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
gap: 1rem !important;
|
||||
}
|
||||
body.single-post .wp-block-post-navigation-link {
|
||||
line-height: 1.35;
|
||||
}
|
||||
body.single-post .wp-block-post-navigation-link a {
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
body.single-post .wp-block-group.alignwide:has(> .wp-block-heading + .wp-block-query) {
|
||||
padding-top: 1rem !important;
|
||||
padding-bottom: 1.25rem !important;
|
||||
}
|
||||
body.single-post .wp-block-group.alignwide:has(> .wp-block-heading + .wp-block-query) > .wp-block-heading {
|
||||
margin-bottom: 0.65rem !important;
|
||||
}
|
||||
body.single-post .wp-block-group.alignwide:has(> .wp-block-heading + .wp-block-query) .wp-block-post-template > .wp-block-post {
|
||||
margin-block-start: 0 !important;
|
||||
}
|
||||
|
||||
/* Issue #67: paginacion de archivos/categorias ("Mas entradas" / siguiente pagina). */
|
||||
body.archive .wp-block-query.alignwide > .wp-block-spacer,
|
||||
body.search .wp-block-query.alignwide > .wp-block-spacer,
|
||||
body.blog .wp-block-query.alignwide > .wp-block-spacer {
|
||||
height: 0.5rem !important;
|
||||
}
|
||||
body.archive .wp-block-query.alignwide > .wp-block-group.alignfull:has(> .wp-block-query-pagination),
|
||||
body.search .wp-block-query.alignwide > .wp-block-group.alignfull:has(> .wp-block-query-pagination),
|
||||
body.blog .wp-block-query.alignwide > .wp-block-group.alignfull:has(> .wp-block-query-pagination) {
|
||||
margin-top: 0.5rem !important;
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
body.archive .wp-block-query-pagination,
|
||||
body.search .wp-block-query-pagination,
|
||||
body.blog .wp-block-query-pagination {
|
||||
gap: 0.75rem 1.25rem !important;
|
||||
align-items: center;
|
||||
}
|
||||
body.archive .wp-block-query-pagination-numbers,
|
||||
body.search .wp-block-query-pagination-numbers,
|
||||
body.blog .wp-block-query-pagination-numbers {
|
||||
display: flex;
|
||||
gap: 0.45rem;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
}, 30);
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fea Cookie Consent — banner RGPD (#93)
|
||||
* Description: Banner de consentimiento de cookies (Consent Mode v2). Mientras no
|
||||
* haya consentimiento, GA4 (fea-analytics.php) arranca con analytics_storage
|
||||
* 'denied'. Al Aceptar, el banner hace gtag consent 'update' a 'granted' y guarda
|
||||
* cookie. Multiidioma por <html lang> (ES/EN/FR/IT/PT). HTML idéntico para todos
|
||||
* (cacheable); el estado por usuario se aplica en JS desde la cookie.
|
||||
* Cambiar preferencias: window.feaOpenCookiePrefs() (engánchalo a un enlace del pie).
|
||||
*/
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
// Página de política de privacidad/cookies. ID del post madre (ES); el enlace
|
||||
// se resuelve a la traducción del idioma actual vía Polylang.
|
||||
define('FEA_PRIVACY_PAGE_ID', 21946);
|
||||
|
||||
add_action('wp_footer', function () {
|
||||
if (is_admin()) return;
|
||||
// Resolver la política al idioma actual (Polylang); fallback al ID madre.
|
||||
$priv_id = FEA_PRIVACY_PAGE_ID;
|
||||
if (function_exists('pll_get_post') && function_exists('pll_current_language')) {
|
||||
$tr = pll_get_post($priv_id, pll_current_language());
|
||||
if ($tr) $priv_id = $tr;
|
||||
}
|
||||
$privacy = get_permalink($priv_id);
|
||||
$privacy = esc_url($privacy ? $privacy : '/');
|
||||
?>
|
||||
<style>
|
||||
#fea-cc{position:fixed;left:0;right:0;bottom:0;z-index:99999;display:none;
|
||||
background:#fafafa;color:#555;border-top:1px solid #e5e7eb;padding:12px 18px;
|
||||
box-shadow:0 -1px 6px rgba(0,0,0,.08);font-size:13px;line-height:1.4}
|
||||
#fea-cc .fea-cc-inner{max-width:1100px;margin:0 auto;display:flex;gap:14px;
|
||||
align-items:center;flex-wrap:wrap;justify-content:space-between}
|
||||
#fea-cc .fea-cc-text{flex:1 1 380px;min-width:240px}
|
||||
#fea-cc a{color:#555;text-decoration:underline}
|
||||
#fea-cc .fea-cc-btns{display:flex;gap:8px;flex-shrink:0}
|
||||
#fea-cc button{cursor:pointer;border:0;border-radius:5px;padding:8px 16px;
|
||||
font-size:13px;font-weight:500}
|
||||
#fea-cc .fea-cc-reject{background:transparent;color:#6b7280;border:1px solid #d1d5db}
|
||||
#fea-cc .fea-cc-accept{background:#6b7280;color:#fff;border:1px solid #6b7280}
|
||||
</style>
|
||||
<div id="fea-cc" role="dialog" aria-live="polite" aria-label="cookie consent">
|
||||
<div class="fea-cc-inner">
|
||||
<div class="fea-cc-text" id="fea-cc-text"></div>
|
||||
<div class="fea-cc-btns">
|
||||
<button type="button" class="fea-cc-reject" id="fea-cc-reject"></button>
|
||||
<button type="button" class="fea-cc-accept" id="fea-cc-accept"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
var PRIVACY = <?php echo json_encode($privacy); ?>;
|
||||
var I18N = {
|
||||
es:{t:"Usamos cookies de analítica (Google Analytics) para entender cómo se usa la web y mejorarla. ¿Nos das tu consentimiento?",a:"Aceptar",r:"Rechazar",m:"Más información"},
|
||||
en:{t:"We use analytics cookies (Google Analytics) to understand how the site is used and improve it. Do you consent?",a:"Accept",r:"Reject",m:"Learn more"},
|
||||
fr:{t:"Nous utilisons des cookies d'analyse (Google Analytics) pour comprendre l'usage du site et l'améliorer. Acceptez-vous ?",a:"Accepter",r:"Refuser",m:"En savoir plus"},
|
||||
it:{t:"Usiamo cookie di analisi (Google Analytics) per capire come viene usato il sito e migliorarlo. Acconsenti?",a:"Accetta",r:"Rifiuta",m:"Maggiori informazioni"},
|
||||
pt:{t:"Usamos cookies de análise (Google Analytics) para perceber como o site é usado e melhorá-lo. Dás o teu consentimento?",a:"Aceitar",r:"Rejeitar",m:"Saber mais"}
|
||||
};
|
||||
function lang(){
|
||||
var l=(document.documentElement.lang||"es").slice(0,2).toLowerCase();
|
||||
return I18N[l]?l:"es";
|
||||
}
|
||||
function getCookie(n){
|
||||
var m=document.cookie.match('(?:^|; )'+n+'=([^;]*)');
|
||||
return m?decodeURIComponent(m[1]):null;
|
||||
}
|
||||
function setCookie(n,v){
|
||||
var d=";domain=.feadulta.com";
|
||||
if(!/(^|\.)feadulta\.com$/.test(location.hostname)) d=""; // local: host-only
|
||||
var exp=new Date(Date.now()+180*864e5).toUTCString();
|
||||
document.cookie=n+"="+encodeURIComponent(v)+";path=/;expires="+exp+";SameSite=Lax"+d;
|
||||
}
|
||||
function grant(){
|
||||
if(typeof gtag==='function'){
|
||||
gtag('consent','update',{'analytics_storage':'granted'});
|
||||
}
|
||||
}
|
||||
function render(){
|
||||
var L=I18N[lang()];
|
||||
var box=document.getElementById('fea-cc');
|
||||
document.getElementById('fea-cc-text').innerHTML=
|
||||
L.t+' <a href="'+PRIVACY+'">'+L.m+'</a>';
|
||||
document.getElementById('fea-cc-accept').textContent=L.a;
|
||||
document.getElementById('fea-cc-reject').textContent=L.r;
|
||||
box.style.display='block';
|
||||
}
|
||||
function hide(){var b=document.getElementById('fea-cc');if(b)b.style.display='none';}
|
||||
function init(){
|
||||
var v=getCookie('fea_consent');
|
||||
if(v==='granted'){grant();return;}
|
||||
if(v==='denied'){return;}
|
||||
render();
|
||||
document.getElementById('fea-cc-accept').addEventListener('click',function(){
|
||||
setCookie('fea_consent','granted');grant();hide();
|
||||
});
|
||||
document.getElementById('fea-cc-reject').addEventListener('click',function(){
|
||||
setCookie('fea_consent','denied');hide();
|
||||
});
|
||||
}
|
||||
// Reabrir el banner para cambiar preferencias (enlace del pie, etc.)
|
||||
window.feaOpenCookiePrefs=function(){render();
|
||||
document.getElementById('fea-cc-accept').onclick=function(){setCookie('fea_consent','granted');grant();hide();};
|
||||
document.getElementById('fea-cc-reject').onclick=function(){setCookie('fea_consent','denied');hide();};
|
||||
};
|
||||
if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',init);}
|
||||
else{init();}
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}, 99);
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* fea-disable-comments — feadulta no usa comentarios.
|
||||
* Defensivo: aunque un post quede con comment_status=open por accidente,
|
||||
* el render trata comentarios y pings como cerrados y no muestra UI.
|
||||
*/
|
||||
|
||||
// Comentarios y pings siempre cerrados en el frontend.
|
||||
add_filter('comments_open', '__return_false', 20, 2);
|
||||
add_filter('pings_open', '__return_false', 20, 2);
|
||||
|
||||
// No devolver comentarios existentes al render.
|
||||
add_filter('comments_array', '__return_empty_array', 10, 2);
|
||||
|
||||
// Quitar el soporte de comentarios de los tipos de contenido.
|
||||
add_action('init', function () {
|
||||
remove_post_type_support('post', 'comments');
|
||||
remove_post_type_support('post', 'trackbacks');
|
||||
remove_post_type_support('page', 'comments');
|
||||
remove_post_type_support('page', 'trackbacks');
|
||||
});
|
||||
|
||||
// Quitar "Comentarios" de la barra de admin.
|
||||
add_action('wp_before_admin_bar_render', function () {
|
||||
if (is_admin_bar_showing()) {
|
||||
global $wp_admin_bar;
|
||||
$wp_admin_bar->remove_menu('comments');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fe Adulta - hide imported artifacts
|
||||
* Description: Oculta en frontend artefactos importados hasta decidir una limpieza definitiva.
|
||||
*/
|
||||
|
||||
function fea_current_request_path() {
|
||||
$path = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH);
|
||||
return is_string($path) ? trim($path, '/') : '';
|
||||
}
|
||||
|
||||
function fea_is_bad_imported_request_path() {
|
||||
$path = fea_current_request_path();
|
||||
return (bool)preg_match('~(^|/)tag/1/?$~', $path)
|
||||
|| (bool)preg_match('~(^|/)[0-9]{2}-[0-9]{2}-[0-9]{4}/?$~', $path);
|
||||
}
|
||||
|
||||
function fea_is_bad_imported_tag($term) {
|
||||
return $term
|
||||
&& isset($term->taxonomy, $term->slug, $term->name)
|
||||
&& $term->taxonomy === 'post_tag'
|
||||
&& ($term->slug === '1' || $term->name === '1');
|
||||
}
|
||||
|
||||
add_filter('get_the_terms', function($terms, $post_id, $taxonomy) {
|
||||
if (is_admin() || $taxonomy !== 'post_tag' || empty($terms) || is_wp_error($terms)) {
|
||||
return $terms;
|
||||
}
|
||||
|
||||
return array_values(array_filter($terms, function($term) {
|
||||
return !fea_is_bad_imported_tag($term);
|
||||
}));
|
||||
}, 10, 3);
|
||||
|
||||
add_filter('get_terms', function($terms, $taxonomies) {
|
||||
if (is_admin() || is_wp_error($terms) || empty($terms) || !in_array('post_tag', (array)$taxonomies, true)) {
|
||||
return $terms;
|
||||
}
|
||||
|
||||
return array_values(array_filter($terms, function($term) {
|
||||
return !fea_is_bad_imported_tag($term);
|
||||
}));
|
||||
}, 10, 2);
|
||||
|
||||
add_filter('redirect_canonical', function($redirect_url) {
|
||||
return fea_is_bad_imported_request_path() ? false : $redirect_url;
|
||||
}, 10);
|
||||
|
||||
add_filter('do_redirect_guess_404_permalink', function($do_redirect) {
|
||||
return fea_is_bad_imported_request_path() ? false : $do_redirect;
|
||||
}, 10);
|
||||
|
||||
add_filter('wp_redirect', function($location) {
|
||||
return fea_is_bad_imported_request_path() ? false : $location;
|
||||
}, 0);
|
||||
|
||||
add_action('template_redirect', function() {
|
||||
if (!is_tag('1') && !fea_is_bad_imported_request_path()) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wp_query;
|
||||
$wp_query->set_404();
|
||||
status_header(404);
|
||||
nocache_headers();
|
||||
}, 0);
|
||||
Executable
+2130
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
/**
|
||||
* fea-menu-i18n — Traducción de los menús FSE (issue #120).
|
||||
*
|
||||
* Polylang FREE no traduce los bloques `wp_navigation` (menús del header y pie).
|
||||
* Este plugin engancha `render_block` sobre cada `core/navigation-link` /
|
||||
* `core/navigation-submenu` y, cuando el idioma actual ≠ es:
|
||||
* 1) sustituye la ETIQUETA por su traducción (mapa de abajo, hecho a mano),
|
||||
* 2) remapea la URL al destino traducido si existe (post/página/categoría via
|
||||
* Polylang); si no hay traducción del destino, deja la URL ES (fallback).
|
||||
*
|
||||
* Las etiquetas son cortas y de contexto religioso → traducidas a mano para
|
||||
* máxima calidad (el contenido largo de #120 va por MiniMax + glosario).
|
||||
*/
|
||||
|
||||
// NOTA: los mu-plugins cargan antes que Polylang → NO comprobar pll_* a nivel de
|
||||
// fichero (abortaría). Se comprueba dentro del filtro, en render (ya cargado).
|
||||
|
||||
/** Mapa etiqueta ES => [en, fr, it, pt]. Claves normalizadas con trim. */
|
||||
function fea_menu_map(): array {
|
||||
static $m = null;
|
||||
if ($m !== null) return $m;
|
||||
$m = [
|
||||
// ── Menú principal (header) ──
|
||||
'PORTADA' => ['Home', 'Accueil', 'Home', 'Início'],
|
||||
'Quiénes somos' => ['About us', 'À propos', 'Chi siamo', 'Quem somos'],
|
||||
'Colaboradores' => ['Contributors', 'Collaborateurs', 'Collaboratori', 'Colaboradores'],
|
||||
'Este portal' => ['This portal', 'Ce portail', 'Questo portale', 'Este portal'],
|
||||
'Para poner al día la Fe' => ['Bringing faith up to date', 'Mettre la foi à jour', 'Aggiornare la fede', 'Atualizar a fé'],
|
||||
'Cartas' => ['Letters', 'Lettres', 'Lettere', 'Cartas'],
|
||||
'Esta semana' => ['This week', 'Cette semaine', 'Questa settimana', 'Esta semana'],
|
||||
'Semana pasada' => ['Last week', 'Semaine dernière', 'Settimana scorsa', 'Semana passada'],
|
||||
'Otras semanas' => ['Other weeks', 'Autres semaines', 'Altre settimane', 'Outras semanas'],
|
||||
'Acceso a webs anteriores:' => ['Previous websites:', 'Anciens sites :', 'Siti precedenti:', 'Sites anteriores:'],
|
||||
'Web V1 — FrontPage (2006-2012)' => ['Site V1 — FrontPage (2006-2012)', 'Site V1 — FrontPage (2006-2012)', 'Sito V1 — FrontPage (2006-2012)', 'Site V1 — FrontPage (2006-2012)'],
|
||||
'Web V2 — Joomla (2012-2026)' => ['Site V2 — Joomla (2012-2026)', 'Site V2 — Joomla (2012-2026)', 'Sito V2 — Joomla (2012-2026)', 'Site V2 — Joomla (2012-2026)'],
|
||||
'Nueva política de datos' => ['New data policy', 'Nouvelle politique de données', 'Nuova politica sui dati', 'Nova política de dados'],
|
||||
'Contactar' => ['Contact', 'Contact', 'Contatti', 'Contactar'],
|
||||
'Para contactar con nosotros' => ['Contact us', 'Nous contacter', 'Per contattarci', 'Para contactar-nos'],
|
||||
'Para recibir la carta de novedades' => ['Subscribe to the newsletter', 'Recevoir la newsletter', 'Ricevere la newsletter', 'Receber a newsletter'],
|
||||
'Para inscribirse en la Escuela' => ['Enrol in the School', 'S\'inscrire à l\'École', 'Iscriversi alla Scuola', 'Inscrever-se na Escola'],
|
||||
'🎓 Escuela' => ['🎓 School', '🎓 École', '🎓 Scuola', '🎓 Escola'],
|
||||
'📚 Librería 🛒' => ['📚 Bookshop 🛒', '📚 Librairie 🛒', '📚 Libreria 🛒', '📚 Livraria 🛒'],
|
||||
'Buscar' => ['Search', 'Rechercher', 'Cerca', 'Pesquisar'],
|
||||
// ── Menús del pie ──
|
||||
'Cartas que nos llegan' => ['Letters we receive', 'Lettres que nous recevons', 'Lettere che riceviamo', 'Cartas que recebemos'],
|
||||
'Tablón de anuncios' => ['Notice board', 'Tableau d\'annonces', 'Bacheca', 'Mural de avisos'],
|
||||
'Asociación FeAdulta' => ['FeAdulta Association', 'Association FeAdulta', 'Associazione FeAdulta', 'Associação FeAdulta'],
|
||||
'La suma de todos' => ['The sum of all', 'La somme de tous', 'La somma di tutti', 'A soma de todos'],
|
||||
'Comunidades cristianas' => ['Christian communities', 'Communautés chrétiennes', 'Comunità cristiane', 'Comunidades cristãs'],
|
||||
'El Evangelio de cada día' => ['The daily Gospel', 'L\'Évangile de chaque jour', 'Il Vangelo di ogni giorno', 'O Evangelho de cada dia'],
|
||||
'Índice cronológico' => ['Chronological index', 'Index chronologique', 'Indice cronologico', 'Índice cronológico'],
|
||||
'Índice cronológico' => ['Chronological index', 'Index chronologique', 'Indice cronologico', 'Índice cronológico'],
|
||||
'Evangelios y comentarios' => ['Gospels and commentaries', 'Évangiles et commentaires', 'Vangeli e commenti', 'Evangelhos e comentários'],
|
||||
'Oraciones eucarísticas' => ['Eucharistic prayers', 'Prières eucharistiques', 'Preghiere eucaristiche', 'Orações eucarísticas'],
|
||||
'A modo de salmos' => ['In the manner of psalms', 'À la manière de psaumes', 'A mo\' di salmi', 'À maneira de salmos'],
|
||||
'Preces y oraciones varias' => ['Prayers and various orations', 'Prières et oraisons diverses', 'Preci e orazioni varie', 'Preces e orações várias'],
|
||||
'Primeras lecturas' => ['First readings', 'Premières lectures', 'Prime letture', 'Primeiras leituras'],
|
||||
'Autores' => ['Authors', 'Auteurs', 'Autori', 'Autores'],
|
||||
'Temas' => ['Topics', 'Thèmes', 'Temi', 'Temas'],
|
||||
'Multimedia' => ['Multimedia', 'Multimédia', 'Multimedia', 'Multimédia'],
|
||||
'Índice de pensamientos' => ['Index of reflections', 'Index des pensées', 'Indice dei pensieri', 'Índice de pensamentos'],
|
||||
'Cantoral' => ['Hymnal', 'Recueil de chants', 'Canzoniere', 'Cancioneiro'],
|
||||
'Películas' => ['Films', 'Films', 'Film', 'Filmes'],
|
||||
'Reseñas de libros' => ['Book reviews', 'Critiques de livres', 'Recensioni di libri', 'Resenhas de livros'],
|
||||
'In memoriam' => ['In memoriam', 'In memoriam', 'In memoriam', 'In memoriam'],
|
||||
];
|
||||
return $m;
|
||||
}
|
||||
|
||||
/** Índice de columna por idioma. */
|
||||
function fea_menu_lang_col(string $lang): int {
|
||||
return ['en' => 0, 'fr' => 1, 'it' => 2, 'pt' => 3][$lang] ?? -1;
|
||||
}
|
||||
|
||||
function fea_menu_tr(string $label, int $col): ?string {
|
||||
$map = fea_menu_map();
|
||||
$key = trim($label);
|
||||
if (isset($map[$key][$col]) && $map[$key][$col] !== '') return $map[$key][$col];
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remapea una URL ES al destino traducido si existe (post/página/categoría).
|
||||
* Devuelve la URL original si no hay traducción o no se resuelve.
|
||||
*/
|
||||
function fea_menu_localize_url(string $url, string $lang): string {
|
||||
if ($url === '' || preg_match('~^(mailto:|tel:|javascript:|#)~i', $url)) return $url;
|
||||
// Solo enlaces internos de ESTE sitio (no Librería, no webs anteriores externas).
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
if ($host) {
|
||||
$site_host = parse_url(home_url('/'), PHP_URL_HOST);
|
||||
if ($host !== $site_host) return $url; // externo
|
||||
}
|
||||
$path = trim((string) parse_url($url, PHP_URL_PATH), '/');
|
||||
// quitar subcarpeta local (fea) y prefijo de idioma (es/en/…), con o sin barra final
|
||||
$path = preg_replace('#^fea(/|$)#', '', $path);
|
||||
$path = preg_replace('#^(es|en|fr|it|pt)(/|$)#', '', $path);
|
||||
if ($path === '') {
|
||||
// raíz del sitio → portada del idioma
|
||||
return function_exists('pll_home_url') ? pll_home_url($lang) : $url;
|
||||
}
|
||||
|
||||
// categoría: category/<slug>. El slug del menú es ES; buscamos SIN filtro de
|
||||
// idioma de Polylang (lang => '') porque el render va en otro idioma.
|
||||
if (preg_match('#^category/([^/]+)/?$#', $path, $mm)) {
|
||||
$terms = get_terms(['taxonomy' => 'category', 'slug' => $mm[1], 'hide_empty' => false, 'number' => 1, 'lang' => '']);
|
||||
$term = (!is_wp_error($terms) && $terms) ? $terms[0] : null;
|
||||
if ($term && function_exists('pll_get_term')) {
|
||||
$tr = pll_get_term($term->term_id, $lang);
|
||||
if ($tr) {
|
||||
$link = get_category_link($tr);
|
||||
if ($link && !is_wp_error($link)) return $link;
|
||||
}
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
// página/post por último segmento (lang => '' para no filtrar por idioma actual)
|
||||
$slug = basename($path);
|
||||
$q = get_posts(['name' => $slug, 'post_type' => ['post', 'page'], 'numberposts' => 1, 'post_status' => 'publish', 'lang' => '']);
|
||||
$page = $q ? $q[0] : null;
|
||||
if ($page && function_exists('pll_get_post')) {
|
||||
$tr = pll_get_post($page->ID, $lang);
|
||||
if ($tr) {
|
||||
$link = get_permalink($tr);
|
||||
if ($link) return $link;
|
||||
}
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
add_filter('render_block', function ($content, $block) {
|
||||
if (empty($block['blockName'])) return $content;
|
||||
if ($block['blockName'] !== 'core/navigation-link' && $block['blockName'] !== 'core/navigation-submenu') return $content;
|
||||
if (!function_exists('pll_current_language')) return $content;
|
||||
|
||||
$lang = pll_current_language();
|
||||
$col = fea_menu_lang_col((string) $lang);
|
||||
if ($col < 0) return $content; // es o desconocido → sin tocar
|
||||
|
||||
$label = isset($block['attrs']['label']) ? (string) $block['attrs']['label'] : '';
|
||||
$url = isset($block['attrs']['url']) ? (string) $block['attrs']['url'] : '';
|
||||
|
||||
// 1) etiqueta
|
||||
if ($label !== '') {
|
||||
$tr = fea_menu_tr($label, $col);
|
||||
if ($tr !== null && $tr !== $label) {
|
||||
$content = str_replace('>' . esc_html($label) . '<', '>' . esc_html($tr) . '<', $content);
|
||||
}
|
||||
}
|
||||
// 2) URL — localizar el href realmente renderizado (robusto frente a discrepancias attr/HTML)
|
||||
$content = preg_replace_callback('/href=("|\')([^"\']*)\1/i', function ($m) use ($lang) {
|
||||
$href = html_entity_decode($m[2], ENT_QUOTES);
|
||||
$loc = fea_menu_localize_url($href, (string) $lang);
|
||||
return 'href=' . $m[1] . esc_url($loc) . $m[1];
|
||||
}, $content, 1);
|
||||
return $content;
|
||||
}, 10, 2);
|
||||
@@ -0,0 +1,453 @@
|
||||
<?php
|
||||
/**
|
||||
* fea-pensamientos — Galerías Joomla y pausa aleatoria en artículos.
|
||||
*
|
||||
* Reutiliza /images de Joomla sin duplicar ficheros en WordPress.
|
||||
*/
|
||||
|
||||
if (!defined('FEA_JOOMLA_IMAGES_DIR')) {
|
||||
define('FEA_JOOMLA_IMAGES_DIR', file_exists('/web/images') ? '/web/images' : '/var/www/joomla-images');
|
||||
}
|
||||
|
||||
if (!defined('FEA_JOOMLA_IMAGES_URL')) {
|
||||
$fea_is_prod = (defined('ABSPATH') && strpos((string) ABSPATH, '/web/wp-nuevo/') === 0)
|
||||
|| (isset($_SERVER['HTTP_HOST']) && preg_match('/(^|\.)feadulta\.com$/', (string) $_SERVER['HTTP_HOST']))
|
||||
|| file_exists('/web/images');
|
||||
|
||||
define(
|
||||
'FEA_JOOMLA_IMAGES_URL',
|
||||
$fea_is_prod ? 'https://www.feadulta.com/images' : 'https://farmer.taild3aaf6.ts.net/joomla/images'
|
||||
);
|
||||
}
|
||||
|
||||
if (!defined('FEA_PENS_DIR')) {
|
||||
define('FEA_PENS_DIR', rtrim(FEA_JOOMLA_IMAGES_DIR, '/') . '/Pensamientos');
|
||||
}
|
||||
|
||||
if (!defined('FEA_PENS_URL')) {
|
||||
define('FEA_PENS_URL', rtrim(FEA_JOOMLA_IMAGES_URL, '/') . '/Pensamientos');
|
||||
}
|
||||
|
||||
if (!defined('FEA_GALLERY_PER_PAGE')) {
|
||||
define('FEA_GALLERY_PER_PAGE', 72);
|
||||
}
|
||||
|
||||
if (!defined('FEA_RANDOM_THOUGHT_EXCLUDED_CATS')) {
|
||||
// Categorías que NO muestran pensamiento aleatorio:
|
||||
// 1645 Lecturas bíblicas · 28 Evangelios y comentarios (textos del evangelio)
|
||||
// 20 Presentación colaboradores (fichas de colaboradores)
|
||||
// 1647 Comentarios al evangelio SÍ muestra pensamiento (decisión Rafa 2026-06-19).
|
||||
define('FEA_RANDOM_THOUGHT_EXCLUDED_CATS', '1645,28,20');
|
||||
}
|
||||
|
||||
if (!defined('FEA_RANDOM_THOUGHT_EXCLUDED_IDS')) {
|
||||
// Posts estructurales (páginas disfrazadas de post) que no deben llevar pensamiento.
|
||||
// 17563 = índice /colaboradores/ (está en «Sin categoría», no lo cubre la cat 20).
|
||||
define('FEA_RANDOM_THOUGHT_EXCLUDED_IDS', '17563');
|
||||
}
|
||||
|
||||
if (!defined('FEA_GALLERY_MANIFEST')) {
|
||||
define('FEA_GALLERY_MANIFEST', WP_CONTENT_DIR . '/uploads/fea-gallery-manifest.json');
|
||||
}
|
||||
|
||||
function fea_gallery_safe_dir(string $dir): string {
|
||||
$dir = trim($dir);
|
||||
$dir = trim($dir, "/\\ \t\n\r\0\x0B");
|
||||
return preg_replace('/[^A-Za-z0-9._-]/', '', $dir);
|
||||
}
|
||||
|
||||
function fea_gallery_base_dir(): string {
|
||||
return rtrim(FEA_JOOMLA_IMAGES_DIR, '/');
|
||||
}
|
||||
|
||||
function fea_gallery_base_url(): string {
|
||||
return rtrim(FEA_JOOMLA_IMAGES_URL, '/');
|
||||
}
|
||||
|
||||
function fea_gallery_manifest_files(string $dir, string $order = 'desc'): array {
|
||||
static $manifest = null;
|
||||
|
||||
if ($manifest === null) {
|
||||
$manifest = [];
|
||||
$path = (string) FEA_GALLERY_MANIFEST;
|
||||
if (is_readable($path)) {
|
||||
$decoded = json_decode((string) file_get_contents($path), true);
|
||||
if (is_array($decoded)) {
|
||||
$manifest = $decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($manifest[$dir]) || !is_array($manifest[$dir])) return [];
|
||||
|
||||
$files = array_values(array_filter(array_map('strval', $manifest[$dir]), function ($file) {
|
||||
return preg_match('/\.(jpe?g|png|gif|webp)$/i', $file);
|
||||
}));
|
||||
natsort($files);
|
||||
$files = array_values($files);
|
||||
if ($order === 'desc') {
|
||||
$files = array_reverse($files);
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
function fea_gallery_files(string $dir, string $order = 'desc'): array {
|
||||
$dir = fea_gallery_safe_dir($dir);
|
||||
if ($dir === '') return [];
|
||||
|
||||
$path = fea_gallery_base_dir() . '/' . $dir;
|
||||
if (!is_dir($path) || !is_readable($path)) {
|
||||
return fea_gallery_manifest_files($dir, $order);
|
||||
}
|
||||
|
||||
$mtime = (int) @filemtime($path);
|
||||
$cache_key = 'fea_gallery_' . md5($path . '|' . $mtime . '|' . $order);
|
||||
$cached = get_transient($cache_key);
|
||||
if (is_array($cached)) return $cached;
|
||||
|
||||
$files = [];
|
||||
$entries = @scandir($path);
|
||||
if (!is_array($entries)) return [];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry === '.' || $entry === '..') continue;
|
||||
if (!preg_match('/\.(jpe?g|png|gif|webp)$/i', $entry)) continue;
|
||||
if (!is_file($path . '/' . $entry)) continue;
|
||||
$files[] = $entry;
|
||||
}
|
||||
|
||||
natsort($files);
|
||||
$files = array_values($files);
|
||||
if ($order === 'desc') {
|
||||
$files = array_reverse($files);
|
||||
}
|
||||
|
||||
set_transient($cache_key, $files, 10 * MINUTE_IN_SECONDS);
|
||||
return $files;
|
||||
}
|
||||
|
||||
function fea_gallery_url(string $dir, string $file): string {
|
||||
return fea_gallery_base_url() . '/' . rawurlencode(fea_gallery_safe_dir($dir)) . '/' . rawurlencode($file);
|
||||
}
|
||||
|
||||
function fea_gallery_page_param(string $dir): string {
|
||||
return 'fea_gallery_' . substr(md5($dir), 0, 8);
|
||||
}
|
||||
|
||||
function fea_gallery_render(array $atts = []): string {
|
||||
$atts = shortcode_atts([
|
||||
'dir' => 'Pensamientos',
|
||||
'per_page' => (string) FEA_GALLERY_PER_PAGE,
|
||||
'order' => 'desc',
|
||||
], $atts, 'fea_galeria');
|
||||
|
||||
$dir = fea_gallery_safe_dir((string) $atts['dir']);
|
||||
if ($dir === '') return '';
|
||||
|
||||
$order = strtolower((string) $atts['order']) === 'asc' ? 'asc' : 'desc';
|
||||
$files = fea_gallery_files($dir, $order);
|
||||
if (!$files) {
|
||||
return '<p class="fea-gallery-empty">Galería no disponible.</p>';
|
||||
}
|
||||
|
||||
$per_page = max(12, min(144, (int) $atts['per_page']));
|
||||
$total = count($files);
|
||||
$pages = max(1, (int) ceil($total / $per_page));
|
||||
$param = fea_gallery_page_param($dir);
|
||||
$page = isset($_GET[$param]) ? max(1, (int) $_GET[$param]) : 1;
|
||||
$page = min($page, $pages);
|
||||
$offset = ($page - 1) * $per_page;
|
||||
$visible = array_slice($files, $offset, $per_page);
|
||||
|
||||
$html = '<div class="fea-gallery" data-fea-gallery="' . esc_attr($dir) . '">';
|
||||
$html .= '<div class="fea-gallery-grid">';
|
||||
foreach ($visible as $file) {
|
||||
$url = fea_gallery_url($dir, $file);
|
||||
$alt = preg_replace('/\.[^.]+$/', '', $file);
|
||||
$html .= '<a class="fea-gallery-item" href="' . esc_url($url) . '" data-fea-lightbox="1">';
|
||||
$html .= '<img src="' . esc_url($url) . '" alt="' . esc_attr($alt) . '" loading="lazy" decoding="async">';
|
||||
$html .= '</a>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
if ($pages > 1) {
|
||||
$html .= '<nav class="fea-gallery-pages" aria-label="Paginación de galería">';
|
||||
if ($page > 1) {
|
||||
$html .= '<a href="' . esc_url(add_query_arg($param, $page - 1)) . '">Anterior</a>';
|
||||
}
|
||||
$html .= '<span>Página ' . esc_html((string) $page) . ' de ' . esc_html((string) $pages) . '</span>';
|
||||
if ($page < $pages) {
|
||||
$html .= '<a href="' . esc_url(add_query_arg($param, $page + 1)) . '">Siguiente</a>';
|
||||
}
|
||||
$html .= '</nav>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
add_shortcode('fea_galeria', 'fea_gallery_render');
|
||||
|
||||
add_filter('the_content', function ($content) {
|
||||
if (is_admin() || stripos($content, '{gallery}') === false) return $content;
|
||||
|
||||
return preg_replace_callback(
|
||||
'/\{gallery\}\s*([^{}]+?)\s*\{\/gallery\}/i',
|
||||
function ($m) {
|
||||
return fea_gallery_render(['dir' => trim($m[1])]);
|
||||
},
|
||||
$content
|
||||
);
|
||||
}, 8);
|
||||
|
||||
function fea_random_thought_html(): string {
|
||||
$files = fea_gallery_files('Pensamientos', 'desc');
|
||||
if (!$files) return '';
|
||||
|
||||
$file = $files[array_rand($files)];
|
||||
$url = fea_gallery_url('Pensamientos', $file);
|
||||
|
||||
return '<aside class="fea-random-thought" aria-label="Una pausa">'
|
||||
. '<div class="fea-random-thought-title"><span></span><strong>Una pausa para el alma</strong><span></span></div>'
|
||||
. '<a href="' . esc_url($url) . '" data-fea-lightbox="1">'
|
||||
. '<img src="' . esc_url($url) . '" alt="Pensamiento aleatorio" loading="lazy" decoding="async">'
|
||||
. '</a></aside>';
|
||||
}
|
||||
|
||||
function fea_random_thought_excluded(): bool {
|
||||
if (!is_singular('post')) return true;
|
||||
|
||||
$post_id = get_the_ID();
|
||||
|
||||
$excluded_ids = array_filter(array_map('intval', explode(',', (string) FEA_RANDOM_THOUGHT_EXCLUDED_IDS)));
|
||||
if ($post_id && in_array((int) $post_id, $excluded_ids, true)) return true;
|
||||
|
||||
if ($post_id) {
|
||||
$raw = (string) get_post_field('post_content', $post_id);
|
||||
if (stripos($raw, '{gallery}') !== false || stripos($raw, '[fea_galeria') !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$ids = array_filter(array_map('intval', explode(',', (string) FEA_RANDOM_THOUGHT_EXCLUDED_CATS)));
|
||||
foreach ($ids as $id) {
|
||||
if ($id > 0 && has_category($id)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
add_shortcode('fea_reflexion_aleatoria', function () {
|
||||
return fea_random_thought_html();
|
||||
});
|
||||
|
||||
add_filter('the_content', function ($content) {
|
||||
if (is_admin() || !is_main_query() || !in_the_loop() || fea_random_thought_excluded()) {
|
||||
return $content;
|
||||
}
|
||||
if (strpos($content, 'fea-random-thought') !== false) return $content;
|
||||
|
||||
$thought = fea_random_thought_html();
|
||||
return $thought ? $content . $thought : $content;
|
||||
}, 18);
|
||||
|
||||
function fea_pensamientos_assets_needed(): bool {
|
||||
if (!is_singular()) return false;
|
||||
|
||||
$post_id = get_queried_object_id();
|
||||
$raw = $post_id ? (string) get_post_field('post_content', $post_id) : '';
|
||||
if (stripos($raw, '{gallery}') !== false || stripos($raw, '[fea_galeria') !== false || stripos($raw, '[fea_reflexion_aleatoria') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !fea_random_thought_excluded();
|
||||
}
|
||||
|
||||
add_action('wp_head', function () {
|
||||
if (!fea_pensamientos_assets_needed()) return;
|
||||
|
||||
?>
|
||||
<style>
|
||||
.fea-gallery {
|
||||
width: min(1180px, 100%);
|
||||
max-width: none;
|
||||
margin: 1.5rem auto 2rem;
|
||||
}
|
||||
.fea-gallery .fea-gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
|
||||
gap: 0.65rem !important;
|
||||
}
|
||||
.fea-gallery-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 275 / 160;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.07);
|
||||
}
|
||||
.fea-gallery-item img {
|
||||
width: 98.5%;
|
||||
height: 96.5%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
transition: transform 0.16s ease;
|
||||
}
|
||||
.fea-gallery-item:hover img { transform: scale(1.02); }
|
||||
.fea-gallery-pages {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1.25rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.fea-gallery-pages a {
|
||||
padding: 0.45rem 0.75rem;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.fea-random-thought {
|
||||
margin: 2.4rem auto 1.4rem;
|
||||
max-width: 720px;
|
||||
text-align: center;
|
||||
}
|
||||
.fea-random-thought-title {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #7f1d1d;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.fea-random-thought-title span {
|
||||
height: 1px;
|
||||
background: currentColor;
|
||||
opacity: 0.28;
|
||||
}
|
||||
.fea-random-thought a { display: inline-block; max-width: min(100%, 560px); }
|
||||
.fea-random-thought img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.14);
|
||||
}
|
||||
.fea-lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 99999;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 5rem;
|
||||
background: rgba(0,0,0,0.86);
|
||||
}
|
||||
.fea-lightbox.is-open { display: flex; }
|
||||
.fea-lightbox-frame {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.fea-lightbox-frame img {
|
||||
max-width: min(100%, 1100px);
|
||||
max-height: calc(100vh - 8rem);
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 18px 60px rgba(0,0,0,0.45);
|
||||
}
|
||||
.fea-lightbox button {
|
||||
position: absolute;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.92);
|
||||
color: #111;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.fea-lightbox-close {
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
.fea-lightbox-prev,
|
||||
.fea-lightbox-next {
|
||||
top: 50%;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
font-size: 2rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.fea-lightbox-prev { left: 1rem; }
|
||||
.fea-lightbox-next { right: 1rem; }
|
||||
@media (max-width: 700px) {
|
||||
.fea-gallery { width: min(100%, calc(100vw - 1rem)); }
|
||||
.fea-gallery .fea-gallery-grid { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
|
||||
.fea-lightbox { padding: 3.5rem 1rem; }
|
||||
.fea-lightbox-prev,
|
||||
.fea-lightbox-next {
|
||||
top: auto;
|
||||
bottom: 0.75rem;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var links = Array.prototype.slice.call(document.querySelectorAll('[data-fea-lightbox="1"]'));
|
||||
if (!links.length) return;
|
||||
|
||||
var box = document.createElement('div');
|
||||
box.className = 'fea-lightbox';
|
||||
box.innerHTML = '<button type="button" class="fea-lightbox-close" aria-label="Cerrar">×</button><button type="button" class="fea-lightbox-prev" aria-label="Anterior">‹</button><div class="fea-lightbox-frame"><img alt=""></div><button type="button" class="fea-lightbox-next" aria-label="Siguiente">›</button>';
|
||||
document.body.appendChild(box);
|
||||
|
||||
var img = box.querySelector('img');
|
||||
var closeButton = box.querySelector('.fea-lightbox-close');
|
||||
var prevButton = box.querySelector('.fea-lightbox-prev');
|
||||
var nextButton = box.querySelector('.fea-lightbox-next');
|
||||
var index = 0;
|
||||
|
||||
var show = function (nextIndex) {
|
||||
index = (nextIndex + links.length) % links.length;
|
||||
img.src = links[index].href;
|
||||
img.alt = links[index].querySelector('img') ? links[index].querySelector('img').alt : '';
|
||||
box.classList.add('is-open');
|
||||
};
|
||||
var close = function () {
|
||||
box.classList.remove('is-open');
|
||||
img.removeAttribute('src');
|
||||
};
|
||||
|
||||
box.addEventListener('click', function (event) {
|
||||
if (event.target === box) close();
|
||||
});
|
||||
closeButton.addEventListener('click', close);
|
||||
prevButton.addEventListener('click', function () { show(index - 1); });
|
||||
nextButton.addEventListener('click', function () { show(index + 1); });
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (!box.classList.contains('is-open')) return;
|
||||
if (event.key === 'Escape') close();
|
||||
if (event.key === 'ArrowLeft') show(index - 1);
|
||||
if (event.key === 'ArrowRight') show(index + 1);
|
||||
});
|
||||
links.forEach(function (link, linkIndex) {
|
||||
link.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
show(linkIndex);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}, 30);
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
/**
|
||||
* fea-recopilatorios — Listados dinámicos auto-actualizables (issues #96-#118).
|
||||
*
|
||||
* Sustituye las páginas-recopilatorio MANUALES de Joomla (tablas/listas de
|
||||
* enlaces mantenidas a mano) por un listado generado desde una categoría.
|
||||
* Así, cada carta/post nuevo que entre en la categoría aparece solo, sin
|
||||
* copiar y pegar.
|
||||
*
|
||||
* Uso:
|
||||
* [fea_recopilatorio cat="1648"] (por term_id)
|
||||
* [fea_recopilatorio cat="eucaristia"] (por slug)
|
||||
* [fea_recopilatorio cat="1648" per_page="150" group="year" order="desc"]
|
||||
*
|
||||
* - group="year" (def.): separadores por año. group="none": lista plana.
|
||||
* - Paginación propia (?recop=N) para no chocar con la paginación del tema.
|
||||
* - Títulos normalizados con fea_title() si existe (legacy en MAYÚSCULAS).
|
||||
*/
|
||||
|
||||
if (!defined('FEA_RECOP_DEFAULT_PER_PAGE')) {
|
||||
define('FEA_RECOP_DEFAULT_PER_PAGE', 200);
|
||||
}
|
||||
|
||||
function fea_recop_resolve_term($cat): int {
|
||||
$cat = trim((string) $cat);
|
||||
if ($cat === '') return 0;
|
||||
if (ctype_digit($cat)) return (int) $cat;
|
||||
$t = get_term_by('slug', $cat, 'category');
|
||||
return $t ? (int) $t->term_id : 0;
|
||||
}
|
||||
|
||||
function fea_recop_title(string $raw): string {
|
||||
return function_exists('fea_title') ? fea_title($raw) : $raw;
|
||||
}
|
||||
|
||||
function fea_recop_render(array $atts = []): string {
|
||||
$atts = shortcode_atts([
|
||||
'cat' => '',
|
||||
'per_page' => (string) FEA_RECOP_DEFAULT_PER_PAGE,
|
||||
'group' => 'year',
|
||||
'order' => 'desc',
|
||||
], $atts, 'fea_recopilatorio');
|
||||
|
||||
$term_id = fea_recop_resolve_term($atts['cat']);
|
||||
if (!$term_id) return '<p class="fea-recop-empty">Recopilatorio no disponible.</p>';
|
||||
|
||||
$per_page = max(20, min(500, (int) $atts['per_page']));
|
||||
$order = strtolower($atts['order']) === 'asc' ? 'ASC' : 'DESC';
|
||||
$paged = isset($_GET['recop']) ? max(1, (int) $_GET['recop']) : 1;
|
||||
|
||||
$q = new WP_Query([
|
||||
'post_type' => 'post',
|
||||
'post_status' => 'publish',
|
||||
'cat' => $term_id,
|
||||
'posts_per_page' => $per_page,
|
||||
'paged' => $paged,
|
||||
'orderby' => 'date',
|
||||
'order' => $order,
|
||||
'ignore_sticky_posts' => true,
|
||||
'no_found_rows' => false,
|
||||
]);
|
||||
|
||||
if (!$q->have_posts()) {
|
||||
wp_reset_postdata();
|
||||
return '<p class="fea-recop-empty">Todavía no hay entradas en esta sección.</p>';
|
||||
}
|
||||
|
||||
$by_year = ($atts['group'] === 'year');
|
||||
$html = '<div class="fea-recop">';
|
||||
$cur_year = null;
|
||||
$open = false;
|
||||
|
||||
while ($q->have_posts()) {
|
||||
$q->the_post();
|
||||
if ($by_year) {
|
||||
$y = get_the_date('Y');
|
||||
if ($y !== $cur_year) {
|
||||
if ($open) $html .= '</ul>';
|
||||
$html .= '<h3 class="fea-recop-year">' . esc_html($y) . '</h3><ul class="fea-recop-list">';
|
||||
$cur_year = $y; $open = true;
|
||||
}
|
||||
} elseif (!$open) {
|
||||
$html .= '<ul class="fea-recop-list">'; $open = true;
|
||||
}
|
||||
$title = fea_recop_title(get_the_title());
|
||||
$html .= '<li><a href="' . esc_url(get_permalink()) . '">' . esc_html($title) . '</a>'
|
||||
. ' <span class="fea-recop-date">' . esc_html(get_the_date('j M Y')) . '</span></li>';
|
||||
}
|
||||
if ($open) $html .= '</ul>';
|
||||
wp_reset_postdata();
|
||||
|
||||
// Paginación propia
|
||||
$total_pages = (int) $q->max_num_pages;
|
||||
if ($total_pages > 1) {
|
||||
$html .= '<nav class="fea-recop-pages" aria-label="Paginación del recopilatorio">';
|
||||
if ($paged > 1) {
|
||||
$html .= '<a href="' . esc_url(add_query_arg('recop', $paged - 1)) . '">Anterior</a>';
|
||||
}
|
||||
$html .= '<span>Página ' . $paged . ' de ' . $total_pages . '</span>';
|
||||
if ($paged < $total_pages) {
|
||||
$html .= '<a href="' . esc_url(add_query_arg('recop', $paged + 1)) . '">Siguiente</a>';
|
||||
}
|
||||
$html .= '</nav>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
add_shortcode('fea_recopilatorio', 'fea_recop_render');
|
||||
|
||||
// ── [fea_multimedia_indice] — galería visual de multimedia (issue #110) ──────
|
||||
// Sustituye la página intermedia /multimedia/ (4 enlaces) por la lista directa
|
||||
// de los artículos de las categorías multimedia, con preview visual (miniatura
|
||||
// de YouTube o primera imagen del contenido) + extracto, para invitar al clic.
|
||||
|
||||
/** Extrae una preview del contenido: ['type'=>'video'|'image'|'none','src'=>url]. */
|
||||
function fea_mm_preview(string $content): array {
|
||||
// 1) Vídeo de YouTube embebido → miniatura hqdefault
|
||||
if (preg_match('~(?:youtube(?:-nocookie)?\.com/(?:embed/|watch\?v=)|youtu\.be/)([A-Za-z0-9_-]{6,})~', $content, $m)) {
|
||||
return ['type' => 'video', 'src' => 'https://img.youtube.com/vi/' . $m[1] . '/hqdefault.jpg'];
|
||||
}
|
||||
// 2) Vimeo → sin thumbnail server-side fiable; marcar como vídeo sin src
|
||||
if (preg_match('~vimeo\.com/(?:video/)?(\d+)~', $content)) {
|
||||
return ['type' => 'video', 'src' => ''];
|
||||
}
|
||||
// 3) Primera imagen del contenido
|
||||
if (preg_match('~<img[^>]+src=["\']([^"\']+)["\']~i', $content, $m)) {
|
||||
return ['type' => 'image', 'src' => $m[1]];
|
||||
}
|
||||
return ['type' => 'none', 'src' => ''];
|
||||
}
|
||||
|
||||
function fea_mm_indice_render(array $atts = []): string {
|
||||
$atts = shortcode_atts([
|
||||
'cats' => '1649,26', // Multimedia + Índice multimedia
|
||||
'per_page' => '24',
|
||||
], $atts, 'fea_multimedia_indice');
|
||||
|
||||
$cats = array_filter(array_map('intval', explode(',', $atts['cats'])));
|
||||
if (!$cats) return '';
|
||||
$per_page = max(6, min(60, (int) $atts['per_page']));
|
||||
$paged = isset($_GET['mmpag']) ? max(1, (int) $_GET['mmpag']) : 1;
|
||||
|
||||
$q = new WP_Query([
|
||||
'post_type' => 'post',
|
||||
'post_status' => 'publish',
|
||||
'category__in' => $cats,
|
||||
'posts_per_page' => $per_page,
|
||||
'paged' => $paged,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
'ignore_sticky_posts' => true,
|
||||
]);
|
||||
|
||||
if (!$q->have_posts()) {
|
||||
wp_reset_postdata();
|
||||
return '<p class="fea-recop-empty">Todavía no hay multimedia disponible.</p>';
|
||||
}
|
||||
|
||||
$html = '<div class="fea-mm-wrap"><div class="fea-mm-grid">';
|
||||
while ($q->have_posts()) {
|
||||
$q->the_post();
|
||||
$content = get_the_content();
|
||||
$prev = fea_mm_preview($content);
|
||||
$title = fea_recop_title(get_the_title());
|
||||
$url = get_permalink();
|
||||
$excerpt = wp_trim_words(trim(preg_replace('/\s+/', ' ', wp_strip_all_tags($content))), 22, '…');
|
||||
|
||||
$thumb = '';
|
||||
if ($prev['src'] !== '') {
|
||||
$thumb = '<img class="fea-mm-img" src="' . esc_url($prev['src']) . '" alt="" loading="lazy" '
|
||||
. 'onerror="this.style.display=\'none\';this.parentNode.classList.add(\'fea-mm-noimg\');">';
|
||||
}
|
||||
$cls = 'fea-mm-thumb' . ($prev['src'] === '' ? ' fea-mm-noimg' : '');
|
||||
$play = $prev['type'] === 'video'
|
||||
? '<span class="fea-mm-play" aria-hidden="true"></span>' : '';
|
||||
|
||||
$html .= '<a class="fea-mm-card" href="' . esc_url($url) . '">'
|
||||
. '<span class="' . $cls . '">' . $thumb . $play . '</span>'
|
||||
. '<span class="fea-mm-body">'
|
||||
. '<span class="fea-mm-title">' . esc_html($title) . '</span>'
|
||||
. '<span class="fea-mm-date">' . esc_html(get_the_date('j M Y')) . '</span>'
|
||||
. '<span class="fea-mm-excerpt">' . esc_html($excerpt) . '</span>'
|
||||
. '</span></a>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
$total_pages = (int) $q->max_num_pages;
|
||||
wp_reset_postdata();
|
||||
if ($total_pages > 1) {
|
||||
$html .= '<nav class="fea-recop-pages" aria-label="Paginación de multimedia">';
|
||||
if ($paged > 1) $html .= '<a href="' . esc_url(add_query_arg('mmpag', $paged - 1)) . '">Anterior</a>';
|
||||
$html .= '<span>Página ' . $paged . ' de ' . $total_pages . '</span>';
|
||||
if ($paged < $total_pages) $html .= '<a href="' . esc_url(add_query_arg('mmpag', $paged + 1)) . '">Siguiente</a>';
|
||||
$html .= '</nav>';
|
||||
}
|
||||
$html .= '</div>'; // .fea-mm-wrap
|
||||
return $html;
|
||||
}
|
||||
add_shortcode('fea_multimedia_indice', 'fea_mm_indice_render');
|
||||
|
||||
add_action('wp_head', function () {
|
||||
if (is_admin()) return;
|
||||
?>
|
||||
<style id="fea-recop-css">
|
||||
.fea-recop { margin: 1.5rem 0; }
|
||||
.fea-recop-year {
|
||||
font-family: 'Fraunces', Georgia, serif; font-weight: 600;
|
||||
color: #8b1a2e; margin: 1.6rem 0 0.6rem; font-size: 1.3rem;
|
||||
border-bottom: 1px solid #efe7d8; padding-bottom: 0.25rem;
|
||||
}
|
||||
.fea-recop-list { list-style: none; margin: 0; padding: 0; }
|
||||
.fea-recop-list li {
|
||||
padding: 0.35rem 0; border-bottom: 1px solid #f4eee2;
|
||||
display: flex; justify-content: space-between; gap: 1rem; align-items: baseline;
|
||||
}
|
||||
.fea-recop-list a { text-decoration: none; color: #2a2320; }
|
||||
.fea-recop-list a:hover { color: #8b1a2e; text-decoration: underline; }
|
||||
.fea-recop-date { color: #998; font-size: 0.82rem; white-space: nowrap; }
|
||||
.fea-recop-pages {
|
||||
display: flex; gap: 1rem; align-items: center; justify-content: center;
|
||||
margin-top: 1.4rem; font-size: 0.95rem;
|
||||
}
|
||||
.fea-recop-pages a {
|
||||
padding: 0.4rem 0.8rem; border: 1px solid #8b1a2e; border-radius: 6px;
|
||||
text-decoration: none; color: #8b1a2e;
|
||||
}
|
||||
/* Galería multimedia (#110) */
|
||||
/* wrapper: el padre es entry-content alignfull (ancho completo), así que basta
|
||||
centrar con margin:auto; max-width:none vence el cap de is-layout-constrained */
|
||||
.fea-mm-wrap {
|
||||
width: min(1180px, 100%);
|
||||
max-width: none;
|
||||
margin: 1.5rem auto 2rem;
|
||||
}
|
||||
.fea-mm-grid {
|
||||
display: grid; gap: 1.3rem;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@media (min-width: 520px) { .fea-mm-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (min-width: 760px) { .fea-mm-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||
@media (max-width: 700px) { .fea-mm-wrap { width: min(100%, calc(100vw - 1rem)); } }
|
||||
.fea-mm-card {
|
||||
display: flex; flex-direction: column; text-decoration: none;
|
||||
background: #fff; border: 1px solid #efe7d8; border-radius: 10px;
|
||||
overflow: hidden; transition: box-shadow .15s, transform .15s;
|
||||
}
|
||||
.fea-mm-card:hover { box-shadow: 0 6px 18px rgba(139,26,46,.13); transform: translateY(-2px); }
|
||||
.fea-mm-thumb {
|
||||
position: relative; display: block; aspect-ratio: 16/9; background: #f4eee2;
|
||||
overflow: hidden;
|
||||
}
|
||||
.fea-mm-img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.fea-mm-noimg {
|
||||
background: linear-gradient(135deg, #8b1a2e 0%, #b34255 100%);
|
||||
}
|
||||
.fea-mm-play {
|
||||
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||
width: 54px; height: 54px; border-radius: 50%;
|
||||
background: rgba(0,0,0,.55); pointer-events: none;
|
||||
}
|
||||
.fea-mm-play::after {
|
||||
content: ''; position: absolute; top: 50%; left: 54%; transform: translate(-50%, -50%);
|
||||
border-style: solid; border-width: 11px 0 11px 18px;
|
||||
border-color: transparent transparent transparent #fff;
|
||||
}
|
||||
.fea-mm-card:hover .fea-mm-play { background: rgba(139,26,46,.85); }
|
||||
.fea-mm-body { padding: 0.8rem 0.9rem 1rem; display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
.fea-mm-title {
|
||||
font-family: 'Fraunces', Georgia, serif; font-weight: 600; color: #2a2320;
|
||||
font-size: 1.02rem; line-height: 1.25;
|
||||
}
|
||||
.fea-mm-card:hover .fea-mm-title { color: #8b1a2e; }
|
||||
.fea-mm-date { color: #998; font-size: 0.78rem; }
|
||||
.fea-mm-excerpt { color: #5a534e; font-size: 0.85rem; line-height: 1.4; margin-top: 0.15rem; }
|
||||
</style>
|
||||
<?php
|
||||
}, 26);
|
||||
@@ -0,0 +1,632 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fe Adulta — Buscador avanzado (#8)
|
||||
* Description: Replica el «Buscador avanzado» K2 de Joomla con WordPress nativo.
|
||||
* Filtros: palabra (FULLTEXT vía fea-search-fulltext.php), autor, tema
|
||||
* (categoría), cita bíblica (_cita_evangelio), fecha.
|
||||
* Formulario visible en la página de resultados (search template) y en
|
||||
* la página dedicada /buscar. Multiidioma (Polylang).
|
||||
* Version: 1.0
|
||||
*/
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Constantes de configuración
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* IDs de usuarios a excluir del selector de autores.
|
||||
* 1,890,1049,1540 = cuentas técnicas/admin.
|
||||
* 408,409,1563..1570 = pseudo-autores "Nuevo/Antiguo Testamento" (y sus traducciones)
|
||||
* que NO son personas. Además, abajo se excluye cualquier display_name que contenga
|
||||
* "Testament" de forma robusta (por si aparecen nuevos IDs).
|
||||
*/
|
||||
defined('FEA_AUTORES_EXCLUIR') or define('FEA_AUTORES_EXCLUIR', [
|
||||
1, 890, 1049, 1540,
|
||||
408, 409, 1563, 1564, 1565, 1566, 1567, 1568, 1569, 1570,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Categorías de ESTADO DE CARTA a excluir del selector de categorías:
|
||||
* 6 (cartasemana), 21 (cartas-de-otras-semanas), 22 (carta-semana-pasada).
|
||||
*/
|
||||
defined('FEA_CATS_CARTA_EXCLUIR') or define('FEA_CATS_CARTA_EXCLUIR', [6, 21, 22]);
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// i18n mínimo (es / en / fr / it / pt)
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function fea_adv_t(string $key): string {
|
||||
$lang = function_exists('pll_current_language') ? pll_current_language() : 'es';
|
||||
$strings = [
|
||||
'search_advanced' => ['es' => 'Búsqueda avanzada', 'en' => 'Advanced search',
|
||||
'fr' => 'Recherche avancée', 'it' => 'Ricerca avanzata', 'pt' => 'Pesquisa avançada'],
|
||||
'word' => ['es' => 'Palabra o frase', 'en' => 'Word or phrase',
|
||||
'fr' => 'Mot ou phrase', 'it' => 'Parola o frase', 'pt' => 'Palavra ou frase'],
|
||||
'author' => ['es' => 'Autor', 'en' => 'Author',
|
||||
'fr' => 'Auteur', 'it' => 'Autore', 'pt' => 'Autor'],
|
||||
'all_authors' => ['es' => '— Cualquier autor —', 'en' => '— Any author —',
|
||||
'fr' => '— Tout auteur —', 'it' => '— Qualsiasi autore —', 'pt' => '— Qualquer autor —'],
|
||||
'topic' => ['es' => 'Categoría', 'en' => 'Category',
|
||||
'fr' => 'Catégorie', 'it' => 'Categoria', 'pt' => 'Categoria'],
|
||||
'all_topics' => ['es' => '— Cualquier categoría —', 'en' => '— Any category —',
|
||||
'fr' => '— Toute catégorie —', 'it' => '— Qualsiasi categoria —', 'pt' => '— Qualquer categoria —'],
|
||||
'biblical_ref' => ['es' => 'Cita bíblica', 'en' => 'Biblical reference',
|
||||
'fr' => 'Référence biblique', 'it' => 'Citazione biblica', 'pt' => 'Referência bíblica'],
|
||||
'biblical_ph' => ['es' => 'Ej: Jn 3', 'en' => 'E.g. Jn 3',
|
||||
'fr' => 'Ex: Jn 3', 'it' => 'Es: Gv 3', 'pt' => 'Ex: Jo 3'],
|
||||
'date_from' => ['es' => 'Desde', 'en' => 'From',
|
||||
'fr' => 'Du', 'it' => 'Dal', 'pt' => 'De'],
|
||||
'date_to' => ['es' => 'Hasta', 'en' => 'To',
|
||||
'fr' => "Jusqu'au", 'it' => 'Al', 'pt' => 'Até'],
|
||||
'search_btn' => ['es' => 'Buscar', 'en' => 'Search',
|
||||
'fr' => 'Rechercher', 'it' => 'Cerca', 'pt' => 'Pesquisar'],
|
||||
'reset_btn' => ['es' => 'Limpiar', 'en' => 'Clear',
|
||||
'fr' => 'Effacer', 'it' => 'Cancella', 'pt' => 'Limpar'],
|
||||
'results' => ['es' => 'resultado(s)', 'en' => 'result(s)',
|
||||
'fr' => 'résultat(s)', 'it' => 'risultato/i', 'pt' => 'resultado(s)'],
|
||||
'no_results' => ['es' => 'Sin resultados. Prueba con otros términos.',
|
||||
'en' => 'No results. Try other terms.',
|
||||
'fr' => 'Aucun résultat. Essayez d\'autres termes.',
|
||||
'it' => 'Nessun risultato. Prova con altri termini.',
|
||||
'pt' => 'Sem resultados. Tente outros termos.'],
|
||||
'active_filters' => ['es' => 'Filtros activos:', 'en' => 'Active filters:',
|
||||
'fr' => 'Filtres actifs:', 'it' => 'Filtri attivi:', 'pt' => 'Filtros ativos:'],
|
||||
'filter_author' => ['es' => 'Autor', 'en' => 'Author',
|
||||
'fr' => 'Auteur', 'it' => 'Autore', 'pt' => 'Autor'],
|
||||
'filter_topic' => ['es' => 'Categoría', 'en' => 'Category',
|
||||
'fr' => 'Catégorie', 'it' => 'Categoria', 'pt' => 'Categoria'],
|
||||
'filter_cita' => ['es' => 'Cita', 'en' => 'Ref.',
|
||||
'fr' => 'Réf.', 'it' => 'Cit.', 'pt' => 'Ref.'],
|
||||
'filter_date' => ['es' => 'Fecha', 'en' => 'Date',
|
||||
'fr' => 'Date', 'it' => 'Data', 'pt' => 'Data'],
|
||||
'by' => ['es' => 'por', 'en' => 'by',
|
||||
'fr' => 'par', 'it' => 'di', 'pt' => 'por'],
|
||||
];
|
||||
$row = $strings[$key] ?? [];
|
||||
return $row[$lang] ?? $row['es'] ?? $key;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Query vars
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
add_filter('query_vars', function (array $vars): array {
|
||||
$vars[] = 'fea_author';
|
||||
$vars[] = 'fea_cat';
|
||||
$vars[] = 'fea_cita';
|
||||
$vars[] = 'fea_date_from';
|
||||
$vars[] = 'fea_date_to';
|
||||
return $vars;
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// pre_get_posts — aplica los filtros avanzados
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
add_action('pre_get_posts', function (WP_Query $q): void {
|
||||
if (is_admin() || !$q->is_main_query()) return;
|
||||
|
||||
// Leer los parámetros avanzados desde $_GET directamente (más fiable en pre_get_posts)
|
||||
$fea_author = isset($_GET['fea_author']) ? (int)$_GET['fea_author'] : 0;
|
||||
$fea_cat = isset($_GET['fea_cat']) ? (int)$_GET['fea_cat'] : 0;
|
||||
$fea_cita = isset($_GET['fea_cita']) ? sanitize_text_field($_GET['fea_cita']) : '';
|
||||
$fea_dfr = isset($_GET['fea_date_from']) ? sanitize_text_field($_GET['fea_date_from']) : '';
|
||||
$fea_dto = isset($_GET['fea_date_to']) ? sanitize_text_field($_GET['fea_date_to']) : '';
|
||||
|
||||
$has_adv = ($fea_author > 0 || $fea_cat > 0 || $fea_cita !== '' || $fea_dfr !== '' || $fea_dto !== '');
|
||||
$is_search = $q->is_search();
|
||||
|
||||
// Activar si: es búsqueda, o si hay vars avanzadas (con o sin ?s=)
|
||||
if (!$is_search && !$has_adv) return;
|
||||
|
||||
// Si hay filtros avanzados pero no ?s=, convertimos el query en listado de posts
|
||||
// (evitamos que WP muestre la home o una 404)
|
||||
if ($has_adv && !$is_search) {
|
||||
$q->set('post_type', 'post');
|
||||
$q->set('post_status', 'publish');
|
||||
// Forzamos is_search para que el template search se active
|
||||
$q->is_home = false;
|
||||
$q->is_front_page = false;
|
||||
$q->is_archive = false;
|
||||
$q->is_search = true;
|
||||
}
|
||||
|
||||
// Autor
|
||||
if ($fea_author > 0) $q->set('author', $fea_author);
|
||||
|
||||
// Categoría (tema)
|
||||
if ($fea_cat > 0) $q->set('cat', $fea_cat);
|
||||
|
||||
// Cita bíblica: coincidencia por PREFIJO (el valor empieza por el término, ej. "Jn").
|
||||
// Usamos REGEXP '^<term>' con el término escapado para evitar metacaracteres.
|
||||
if ($fea_cita !== '') {
|
||||
$regex = '^' . preg_quote($fea_cita, '/');
|
||||
$q->set('meta_query', [
|
||||
[
|
||||
'key' => '_cita_evangelio',
|
||||
'value' => $regex,
|
||||
'compare' => 'REGEXP',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// Fechas: la UI usa <input type="date"> → formato YYYY-MM-DD (fecha completa).
|
||||
// Tratamos los límites como fechas exactas, inclusive.
|
||||
if ($fea_dfr !== '' || $fea_dto !== '') {
|
||||
$date_query = ['inclusive' => true];
|
||||
if ($fea_dfr !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $fea_dfr)) {
|
||||
[$y, $m, $d] = array_map('intval', explode('-', $fea_dfr));
|
||||
$date_query['after'] = ['year' => $y, 'month' => $m, 'day' => $d];
|
||||
}
|
||||
if ($fea_dto !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $fea_dto)) {
|
||||
[$y, $m, $d] = array_map('intval', explode('-', $fea_dto));
|
||||
$date_query['before'] = ['year' => $y, 'month' => $m, 'day' => $d];
|
||||
}
|
||||
// Sólo aplicamos si quedó al menos un límite válido
|
||||
if (isset($date_query['after']) || isset($date_query['before'])) {
|
||||
$q->set('date_query', [$date_query]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Helpers: obtener datos del formulario
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Devuelve los autores elegibles (≥30 posts, sin excluidos), cacheado por request. */
|
||||
function fea_adv_get_authors(): array {
|
||||
static $cache = null;
|
||||
if ($cache !== null) return $cache;
|
||||
global $wpdb;
|
||||
$excl = implode(',', array_map('intval', FEA_AUTORES_EXCLUIR));
|
||||
// Excluimos por ID y, de forma robusta, cualquier display_name que contenga "Testament"
|
||||
// (Nuevo/Antiguo Testamento, New/Old Testament, Nouveau/Ancien Testament, etc.).
|
||||
$cache = $wpdb->get_results("
|
||||
SELECT u.ID, u.display_name, COUNT(p.ID) as cnt
|
||||
FROM {$wpdb->users} u
|
||||
JOIN {$wpdb->posts} p ON p.post_author = u.ID
|
||||
WHERE p.post_status = 'publish'
|
||||
AND p.post_type = 'post'
|
||||
AND u.ID NOT IN ({$excl})
|
||||
AND u.display_name NOT LIKE '%Testament%'
|
||||
GROUP BY u.ID
|
||||
HAVING cnt >= 30
|
||||
ORDER BY u.display_name
|
||||
");
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista CURADA de categorías-sección reales (term_ids ES, idioma canónico), en orden
|
||||
* editorial. Las 153 categorías reales de la BD incluyen categorías-autor y residuales
|
||||
* de la migración K2 → desplegable inmanejable; por eso se cura a las secciones del sitio.
|
||||
* En idiomas ≠ ES se traduce cada term con Polylang.
|
||||
*/
|
||||
defined('FEA_CATS_CURADA') or define('FEA_CATS_CURADA', [
|
||||
1650, // Artículos
|
||||
1647, // Comentarios al evangelio
|
||||
1645, // Lecturas bíblicas
|
||||
1648, // Eucaristía
|
||||
1646, // Comentario editorial
|
||||
1649, // Multimedia
|
||||
63, // EFFA
|
||||
14, // A la fuente cada día
|
||||
23, // Cartas que nos llegan
|
||||
41, // Noticias de alcance
|
||||
24, // Tablón de anuncios
|
||||
54, // Canciones religiosas
|
||||
45, // Canciones-plegarias
|
||||
]);
|
||||
|
||||
/**
|
||||
* Etiqueta bonita en el desplegable (solo ES) para categorías cuyo nombre en BD quedó
|
||||
* sin formatear en la migración K2 (no toca el dato del término).
|
||||
*/
|
||||
defined('FEA_CATS_LABEL') or define('FEA_CATS_LABEL', [
|
||||
14 => 'A la fuente cada día', // en BD es "Alafuentecadadia"
|
||||
]);
|
||||
|
||||
/**
|
||||
* Devuelve las categorías del selector (lista curada FEA_CATS_CURADA), traducidas al
|
||||
* idioma activo (Polylang) y conservando el orden editorial.
|
||||
* Devuelve array de objetos {term_id, name} con el term_id DEL IDIOMA ACTUAL.
|
||||
*/
|
||||
function fea_adv_get_categories(): array {
|
||||
static $cache = null;
|
||||
if ($cache !== null) return $cache;
|
||||
|
||||
$lang = function_exists('pll_current_language') ? pll_current_language() : '';
|
||||
$default = function_exists('pll_default_language') ? pll_default_language() : 'es';
|
||||
|
||||
$out = [];
|
||||
foreach (FEA_CATS_CURADA as $es_id) {
|
||||
$tid = (int) $es_id;
|
||||
if ($lang && $lang !== $default && function_exists('pll_get_term')) {
|
||||
$tr = pll_get_term($es_id, $lang);
|
||||
if ($tr) $tid = (int) $tr;
|
||||
}
|
||||
$term = get_term($tid, 'category');
|
||||
if (!$term || is_wp_error($term)) continue;
|
||||
$name = $term->name;
|
||||
if ((!$lang || $lang === $default) && isset(FEA_CATS_LABEL[$es_id])) {
|
||||
$name = FEA_CATS_LABEL[$es_id];
|
||||
}
|
||||
$out[] = (object) ['term_id' => (int) $term->term_id, 'name' => $name];
|
||||
}
|
||||
$cache = $out;
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/** URL base del idioma actual (para el action del form). */
|
||||
function fea_adv_lang_base(): string {
|
||||
$base = home_url('/');
|
||||
if (function_exists('pll_current_language')) {
|
||||
$lang = pll_current_language();
|
||||
$default = function_exists('pll_default_language') ? pll_default_language() : 'es';
|
||||
if ($lang && $lang !== $default) $base = home_url('/' . $lang . '/');
|
||||
}
|
||||
return $base;
|
||||
}
|
||||
|
||||
/** Nombre traducido de una categoría (vía Polylang si está disponible). */
|
||||
function fea_adv_cat_name(int $cat_id, string $fallback): string {
|
||||
if (function_exists('pll_current_language')) {
|
||||
$lang = pll_current_language();
|
||||
$default = function_exists('pll_default_language') ? pll_default_language() : 'es';
|
||||
if ($lang !== $default) {
|
||||
$translated_id = function_exists('pll_get_term') ? pll_get_term($cat_id, $lang) : 0;
|
||||
if ($translated_id) {
|
||||
$term = get_term($translated_id);
|
||||
if ($term && !is_wp_error($term)) return $term->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
$term = get_term($cat_id, 'category');
|
||||
return ($term && !is_wp_error($term)) ? $term->name : $fallback;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Renderiza el formulario avanzado
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function fea_adv_form_html(): string {
|
||||
$action = esc_url(fea_adv_lang_base());
|
||||
$s = esc_attr(get_search_query());
|
||||
// Leer de $_GET para fiabilidad (get_query_var puede llegar vacío si la var no estaba registrada aún)
|
||||
$sel_aut = isset($_GET['fea_author']) ? (int)$_GET['fea_author'] : 0;
|
||||
$sel_cat = isset($_GET['fea_cat']) ? (int)$_GET['fea_cat'] : 0;
|
||||
$sel_cit = isset($_GET['fea_cita']) ? esc_attr(sanitize_text_field($_GET['fea_cita'])) : '';
|
||||
$sel_dfr = isset($_GET['fea_date_from'])? esc_attr(sanitize_text_field($_GET['fea_date_from'])) : '';
|
||||
$sel_dto = isset($_GET['fea_date_to']) ? esc_attr(sanitize_text_field($_GET['fea_date_to'])) : '';
|
||||
|
||||
$authors = fea_adv_get_authors();
|
||||
$cats = fea_adv_get_categories();
|
||||
|
||||
$t_title = esc_html(fea_adv_t('search_advanced'));
|
||||
$t_word = esc_html(fea_adv_t('word'));
|
||||
$t_author = esc_html(fea_adv_t('author'));
|
||||
$t_allaut = esc_html(fea_adv_t('all_authors'));
|
||||
$t_topic = esc_html(fea_adv_t('topic'));
|
||||
$t_alltop = esc_html(fea_adv_t('all_topics'));
|
||||
$t_cita = esc_html(fea_adv_t('biblical_ref'));
|
||||
$t_citaph = esc_attr(fea_adv_t('biblical_ph'));
|
||||
$t_dfr = esc_html(fea_adv_t('date_from'));
|
||||
$t_dto = esc_html(fea_adv_t('date_to'));
|
||||
$t_btn = esc_html(fea_adv_t('search_btn'));
|
||||
$t_reset = esc_html(fea_adv_t('reset_btn'));
|
||||
|
||||
$html = '<div class="fea-adv-wrap" id="fea-adv-search">';
|
||||
$html .= '<details class="fea-adv-details" open>';
|
||||
$html .= '<summary class="fea-adv-summary">' . $t_title . '</summary>';
|
||||
$html .= '<form class="fea-adv-form" method="get" action="' . $action . '">';
|
||||
|
||||
// Fila 1: Palabra
|
||||
$html .= '<div class="fea-adv-row">';
|
||||
$html .= '<label class="fea-adv-label" for="fea-s">' . $t_word . '</label>';
|
||||
$html .= '<input class="fea-adv-input" id="fea-s" type="search" name="s" value="' . $s . '" autocomplete="off">';
|
||||
$html .= '</div>';
|
||||
|
||||
// Fila 2: Autor
|
||||
$html .= '<div class="fea-adv-row">';
|
||||
$html .= '<label class="fea-adv-label" for="fea-author">' . $t_author . '</label>';
|
||||
$html .= '<select class="fea-adv-select" id="fea-author" name="fea_author">';
|
||||
$html .= '<option value="">' . $t_allaut . '</option>';
|
||||
foreach ($authors as $a) {
|
||||
$sel = selected($sel_aut, (int)$a->ID, false);
|
||||
$name = esc_html($a->display_name);
|
||||
$html .= "<option value=\"{$a->ID}\"{$sel}>{$name}</option>";
|
||||
}
|
||||
$html .= '</select></div>';
|
||||
|
||||
// Fila 3: Categoría (real, dinámica)
|
||||
$html .= '<div class="fea-adv-row">';
|
||||
$html .= '<label class="fea-adv-label" for="fea-cat">' . $t_topic . '</label>';
|
||||
$html .= '<select class="fea-adv-select" id="fea-cat" name="fea_cat">';
|
||||
$html .= '<option value="">' . $t_alltop . '</option>';
|
||||
foreach ($cats as $c) {
|
||||
$cat_name = esc_html($c->name);
|
||||
$sel = selected($sel_cat, $c->term_id, false);
|
||||
$html .= "<option value=\"{$c->term_id}\"{$sel}>{$cat_name}</option>";
|
||||
}
|
||||
$html .= '</select></div>';
|
||||
|
||||
// Fila 4: Cita bíblica
|
||||
$html .= '<div class="fea-adv-row">';
|
||||
$html .= '<label class="fea-adv-label" for="fea-cita">' . $t_cita . '</label>';
|
||||
$html .= '<input class="fea-adv-input" id="fea-cita" type="text" name="fea_cita" value="' . $sel_cit . '" placeholder="' . $t_citaph . '">';
|
||||
$html .= '</div>';
|
||||
|
||||
// Fila 5: Fechas
|
||||
$html .= '<div class="fea-adv-row fea-adv-dates">';
|
||||
$html .= '<span class="fea-adv-label">' . $t_dfr . '</span>';
|
||||
$html .= '<input class="fea-adv-input fea-adv-date" type="date" name="fea_date_from" value="' . $sel_dfr . '">';
|
||||
$html .= '<span class="fea-adv-label fea-adv-to">' . $t_dto . '</span>';
|
||||
$html .= '<input class="fea-adv-input fea-adv-date" type="date" name="fea_date_to" value="' . $sel_dto . '">';
|
||||
$html .= '</div>';
|
||||
|
||||
// Botones
|
||||
$html .= '<div class="fea-adv-actions">';
|
||||
$html .= '<button class="fea-adv-btn fea-adv-btn-primary" type="submit">' . $t_btn . '</button>';
|
||||
$html .= '<a class="fea-adv-btn fea-adv-btn-secondary" href="' . $action . '">' . $t_reset . '</a>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</form></details></div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Chips de filtros activos + contador de resultados
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function fea_adv_chips_html(): string {
|
||||
$chips = [];
|
||||
|
||||
$aut_id = isset($_GET['fea_author']) ? (int)$_GET['fea_author'] : 0;
|
||||
if ($aut_id > 0) {
|
||||
$udata = get_userdata($aut_id);
|
||||
$name = $udata ? esc_html($udata->display_name) : $aut_id;
|
||||
$chips[] = fea_adv_chip(fea_adv_t('filter_author') . ': ' . $name, 'fea_author');
|
||||
}
|
||||
|
||||
$cat_id = isset($_GET['fea_cat']) ? (int)$_GET['fea_cat'] : 0;
|
||||
if ($cat_id > 0) {
|
||||
$term = get_term($cat_id, 'category');
|
||||
$name = ($term && !is_wp_error($term)) ? esc_html($term->name) : $cat_id;
|
||||
$chips[] = fea_adv_chip(fea_adv_t('filter_topic') . ': ' . $name, 'fea_cat');
|
||||
}
|
||||
|
||||
$cita = isset($_GET['fea_cita']) ? sanitize_text_field($_GET['fea_cita']) : '';
|
||||
if ($cita !== '') {
|
||||
$chips[] = fea_adv_chip(fea_adv_t('filter_cita') . ': ' . esc_html($cita), 'fea_cita');
|
||||
}
|
||||
|
||||
$dfr = isset($_GET['fea_date_from']) ? sanitize_text_field($_GET['fea_date_from']) : '';
|
||||
$dto = isset($_GET['fea_date_to']) ? sanitize_text_field($_GET['fea_date_to']) : '';
|
||||
if ($dfr !== '' || $dto !== '') {
|
||||
$label = fea_adv_t('filter_date') . ': ' . ($dfr ?: '?') . ' – ' . ($dto ?: '?');
|
||||
$chips[] = fea_adv_chip(esc_html($label), 'fea_date_from', 'fea_date_to');
|
||||
}
|
||||
|
||||
if (empty($chips)) return '';
|
||||
return '<div class="fea-adv-chips"><span class="fea-adv-chips-label">' .
|
||||
esc_html(fea_adv_t('active_filters')) . '</span>' . implode('', $chips) . '</div>';
|
||||
}
|
||||
|
||||
/** Genera un chip con botón ✕ que elimina el filtro de la URL. */
|
||||
function fea_adv_chip(string $label, string ...$remove_params): string {
|
||||
$url = remove_query_arg($remove_params);
|
||||
return '<span class="fea-adv-chip">' . $label .
|
||||
' <a href="' . esc_url($url) . '" class="fea-adv-chip-x" aria-label="Eliminar filtro">×</a></span>';
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Inyección en la página de resultados (template search)
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Inyecta el formulario avanzado + chips antes del primer bloque wp:query-title
|
||||
* del template de búsqueda, modificando el HTML renderizado del bloque principal.
|
||||
*/
|
||||
add_filter('render_block', function (string $html, array $block): string {
|
||||
if (is_admin()) return $html;
|
||||
|
||||
// Inyectar en búsquedas y cuando hay filtros avanzados activos
|
||||
$has_adv_get = !empty($_GET['fea_author']) || !empty($_GET['fea_cat']) ||
|
||||
!empty($_GET['fea_cita']) || !empty($_GET['fea_date_from']) || !empty($_GET['fea_date_to']);
|
||||
|
||||
if (!is_search() && !is_page('buscar') && !$has_adv_get) return $html;
|
||||
|
||||
if (($block['blockName'] ?? '') !== 'core/query-title') return $html;
|
||||
|
||||
$form = fea_adv_form_html();
|
||||
$chips = fea_adv_chips_html();
|
||||
|
||||
// Contador de resultados (sólo cuando hay consulta activa)
|
||||
$counter = '';
|
||||
if (is_search() || $has_adv_get) {
|
||||
global $wp_query;
|
||||
if ($wp_query && $wp_query->found_posts !== null) {
|
||||
$n = (int) $wp_query->found_posts;
|
||||
$counter = '<div class="fea-adv-count">' . $n . ' ' . esc_html(fea_adv_t('results')) . '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
return $form . $chips . $counter . $html;
|
||||
}, 10, 2);
|
||||
|
||||
/**
|
||||
* Añade byline (autor) en cada tarjeta fea-archive-card dentro del template search.
|
||||
* Lo hacemos inyectando tras wp:post-date en el contexto correcto.
|
||||
*/
|
||||
add_filter('render_block', function (string $html, array $block): string {
|
||||
if (is_admin()) return $html;
|
||||
$has_adv_get2 = !empty($_GET['fea_author']) || !empty($_GET['fea_cat']) ||
|
||||
!empty($_GET['fea_cita']) || !empty($_GET['fea_date_from']) || !empty($_GET['fea_date_to']);
|
||||
if (!is_search() && !is_page('buscar') && !$has_adv_get2) return $html;
|
||||
if (($block['blockName'] ?? '') !== 'core/post-date') return $html;
|
||||
|
||||
$post_id = $block['attrs']['postId'] ?? (in_the_loop() ? get_the_ID() : 0);
|
||||
if (!$post_id) $post_id = get_the_ID();
|
||||
if (!$post_id) return $html;
|
||||
|
||||
$author_id = (int) get_post_field('post_author', $post_id);
|
||||
$author_name = get_the_author_meta('display_name', $author_id);
|
||||
if (!$author_name) return $html;
|
||||
|
||||
$author_url = get_author_posts_url($author_id);
|
||||
$byline = '<div class="fea-adv-byline">' . esc_html(fea_adv_t('by')) . ' ' .
|
||||
'<a href="' . esc_url($author_url) . '">' . esc_html($author_name) . '</a></div>';
|
||||
|
||||
return $html . $byline;
|
||||
}, 10, 2);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Enlace «Búsqueda avanzada» desde la barra fea-search
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
add_filter('render_block', function (string $html, array $block): string {
|
||||
if (is_admin()) return $html;
|
||||
if (($block['blockName'] ?? '') !== 'core/template-part') return $html;
|
||||
$slug = $block['attrs']['slug'] ?? '';
|
||||
if (!in_array($slug, ['header', 'cabecera-portada'], true)) return $html;
|
||||
|
||||
// Sólo inyectamos el enlace si ya hay una barra de búsqueda (.fea-search-bar)
|
||||
// Buscamos la barra y le añadimos el enlace de búsqueda avanzada.
|
||||
if (strpos($html, 'fea-search-bar') === false) return $html;
|
||||
|
||||
$adv_url = home_url('/buscar/');
|
||||
if (function_exists('pll_current_language')) {
|
||||
$lang = pll_current_language();
|
||||
$default = function_exists('pll_default_language') ? pll_default_language() : 'es';
|
||||
if ($lang && $lang !== $default) {
|
||||
// Intenta obtener la página /buscar traducida
|
||||
$page = get_page_by_path('buscar');
|
||||
if ($page) {
|
||||
$tl = function_exists('pll_get_post') ? pll_get_post($page->ID, $lang) : 0;
|
||||
if ($tl) $adv_url = get_permalink($tl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$label = esc_html(fea_adv_t('search_advanced'));
|
||||
$link = '<div class="fea-adv-link-wrap"><a class="fea-adv-link" href="' . esc_url($adv_url) . '">' . $label . '</a></div>';
|
||||
|
||||
// Insertamos el enlace justo al final del bloque .fea-search-bar (cerrando el div externo)
|
||||
// fea-search.php genera: <div class="fea-search-bar"><form ...>...</form></div>
|
||||
// Reemplazamos la ÚLTIMA ocurrencia del cierre del div de .fea-search-bar
|
||||
$marker = '</div>';
|
||||
$pos = strpos($html, 'fea-search-bar');
|
||||
if ($pos !== false) {
|
||||
// Buscamos el </div> que cierra .fea-search-bar (el wrapper externo)
|
||||
// La estructura es: <div class="fea-search-bar"><form>...</form></div>
|
||||
// Hay dos </div>: uno cierra el form y otro cierra .fea-search-bar
|
||||
// Usamos una sustitución segura: buscamos el patrón exacto del cierre
|
||||
$html = preg_replace('#(</form></div>)#', '$1' . $link, $html, 1);
|
||||
}
|
||||
return $html;
|
||||
}, 25, 2);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Página /buscar — inyectar formulario vía the_content
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
add_filter('the_content', function (string $content): string {
|
||||
if (!is_page('buscar')) return $content;
|
||||
// Envoltura: título + formulario + contenido original de la página
|
||||
$form = fea_adv_form_html();
|
||||
return $form . '<div class="fea-buscar-intro">' . $content . '</div>';
|
||||
});
|
||||
|
||||
// Nota: la página /buscar se crea con scripts/create_buscar_page.php (ya ejecutado).
|
||||
// El formulario se inyecta en the_content (hook arriba) y vía render_block en search.
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// CSS
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
add_action('wp_head', function (): void {
|
||||
if (!is_search() && !is_page('buscar') &&
|
||||
empty($_GET['fea_author']) && empty($_GET['fea_cat']) &&
|
||||
empty($_GET['fea_cita']) && empty($_GET['fea_date_from']) && empty($_GET['fea_date_to'])) return;
|
||||
?>
|
||||
<style>
|
||||
/* ── Formulario buscador avanzado ─────────────────────────────── */
|
||||
.fea-adv-wrap{max-width:860px;margin:0 auto 1.5rem;padding:0 1rem}
|
||||
|
||||
.fea-adv-details{background:#faf6f7;border:1px solid #e8d5da;border-radius:8px;overflow:hidden}
|
||||
.fea-adv-summary{
|
||||
padding:.75rem 1.2rem;font-weight:600;font-size:1rem;color:#8b1a2e;
|
||||
cursor:pointer;list-style:none;display:flex;align-items:center;gap:.5rem;
|
||||
background:#fff0f2;border-bottom:1px solid #e8d5da;
|
||||
}
|
||||
.fea-adv-summary::-webkit-details-marker{display:none}
|
||||
.fea-adv-summary::before{content:"▸";transition:transform .2s}
|
||||
details[open] .fea-adv-summary::before{transform:rotate(90deg)}
|
||||
|
||||
.fea-adv-form{display:grid;grid-template-columns:1fr 1fr;gap:.75rem 1.2rem;padding:1rem 1.2rem}
|
||||
@media(max-width:600px){.fea-adv-form{grid-template-columns:1fr}}
|
||||
|
||||
/* min-width:0 evita que el contenido fuerce a la celda del grid a desbordar */
|
||||
.fea-adv-row{display:flex;flex-direction:column;gap:.25rem;min-width:0}
|
||||
.fea-adv-dates{grid-column:1/-1;flex-direction:row;align-items:center;flex-wrap:wrap;gap:.5rem}
|
||||
.fea-adv-dates .fea-adv-label{white-space:nowrap}
|
||||
|
||||
.fea-adv-label{font-size:.8rem;font-weight:600;color:#5a3a40;text-transform:uppercase;letter-spacing:.04em}
|
||||
.fea-adv-to{margin-left:.5rem}
|
||||
|
||||
.fea-adv-input,.fea-adv-select{
|
||||
box-sizing:border-box;border:1px solid #d9c4c9;border-radius:6px;padding:.5rem .75rem;
|
||||
font-size:.95rem;background:#fff;color:#222;width:100%;max-width:100%;
|
||||
transition:border-color .15s;
|
||||
}
|
||||
.fea-adv-input:focus,.fea-adv-select:focus{border-color:#8b1a2e;outline:0;box-shadow:0 0 0 2px #8b1a2e33}
|
||||
.fea-adv-date{box-sizing:border-box;width:auto;min-width:150px;flex:0 0 auto}
|
||||
|
||||
.fea-adv-actions{grid-column:1/-1;display:flex;gap:.75rem;align-items:center;margin-top:.25rem}
|
||||
.fea-adv-btn{padding:.55rem 1.4rem;border-radius:999px;font-size:.95rem;font-weight:600;cursor:pointer;text-decoration:none;border:2px solid transparent}
|
||||
.fea-adv-btn-primary{background:#8b1a2e;color:#fff;border-color:#8b1a2e}
|
||||
.fea-adv-btn-primary:hover{background:#6f1525;border-color:#6f1525}
|
||||
.fea-adv-btn-secondary{background:transparent;color:#8b1a2e;border-color:#8b1a2e}
|
||||
.fea-adv-btn-secondary:hover{background:#8b1a2e;color:#fff}
|
||||
|
||||
/* ── Contador y chips ─────────────────────────────────────────── */
|
||||
.fea-adv-count{max-width:860px;margin:.5rem auto .25rem;padding:0 1rem;
|
||||
font-size:.9rem;color:#666}
|
||||
|
||||
.fea-adv-chips{max-width:860px;margin:.5rem auto;padding:0 1rem;
|
||||
display:flex;flex-wrap:wrap;gap:.4rem;align-items:center}
|
||||
.fea-adv-chips-label{font-size:.8rem;color:#5a3a40;font-weight:600;margin-right:.25rem}
|
||||
.fea-adv-chip{display:inline-flex;align-items:center;gap:.35rem;background:#f0e2e5;
|
||||
border:1px solid #d9c4c9;border-radius:999px;padding:.2rem .75rem;font-size:.82rem;color:#5a3a40}
|
||||
.fea-adv-chip-x{color:#8b1a2e;text-decoration:none;font-weight:700;font-size:1rem;line-height:1}
|
||||
.fea-adv-chip-x:hover{color:#6f1525}
|
||||
|
||||
/* ── Byline en tarjetas de resultado ──────────────────────────── */
|
||||
.fea-archive-card .fea-adv-byline{font-size:.78rem;color:#7a5a62;margin-top:.1rem}
|
||||
.fea-archive-card .fea-adv-byline a{color:#8b1a2e;text-decoration:none}
|
||||
.fea-archive-card .fea-adv-byline a:hover{text-decoration:underline}
|
||||
|
||||
/* ── Enlace «Búsqueda avanzada» en barra header ───────────────── */
|
||||
.fea-adv-link-wrap{display:none;justify-content:center;padding:.3rem 1rem .4rem;
|
||||
background:#faf6f7;border-bottom:1px solid #efe2e5}
|
||||
@media(max-width:600px){.fea-adv-link-wrap{display:flex}}
|
||||
.fea-adv-link{font-size:.8rem;color:#8b1a2e;text-decoration:none;font-weight:500}
|
||||
.fea-adv-link:hover{text-decoration:underline}
|
||||
</style>
|
||||
<?php
|
||||
}, 10);
|
||||
|
||||
// CSS del enlace «Búsqueda avanzada» en header (se muestra en todas las páginas)
|
||||
add_action('wp_head', function (): void {
|
||||
?>
|
||||
<style>
|
||||
.fea-adv-link-wrap{display:none;justify-content:center;padding:.3rem 1rem .4rem;
|
||||
background:#faf6f7;border-bottom:1px solid #efe2e5}
|
||||
@media(max-width:600px){.fea-adv-link-wrap{display:flex}}
|
||||
.fea-adv-link{font-size:.8rem;color:#8b1a2e;text-decoration:none;font-weight:500}
|
||||
.fea-adv-link:hover{text-decoration:underline}
|
||||
</style>
|
||||
<?php
|
||||
}, 15);
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fe Adulta — Motor FULLTEXT (#8)
|
||||
* Description: Sustituye el LIKE nativo de WP por MATCH AGAINST (MySQL FULLTEXT, InnoDB,
|
||||
* Boolean Mode) cuando se hace una búsqueda por texto (/?s=…). Ordena por
|
||||
* relevancia FULLTEXT si no se pide otro criterio de orden. Degradación elegante:
|
||||
* si no hay término o el índice FULLTEXT no existe, usa el comportamiento nativo.
|
||||
* Convive con fea-search-advanced.php (filtros pre_get_posts de autor/cat/cita/fecha).
|
||||
* Version: 1.1
|
||||
*/
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
/**
|
||||
* Comprueba (cacheado) que existe el índice FULLTEXT 'fea_ft' en wp_posts.
|
||||
* Si no existe, el motor se degrada al comportamiento nativo para no romper la búsqueda
|
||||
* con un error SQL (MATCH AGAINST requiere el índice).
|
||||
*/
|
||||
function fea_ft_index_exists(): bool {
|
||||
static $cached = null;
|
||||
if ($cached !== null) return $cached;
|
||||
|
||||
// Cache persistente 12h vía transient para evitar el SHOW INDEX en cada request.
|
||||
$t = get_transient('fea_ft_index_exists');
|
||||
if ($t !== false) {
|
||||
$cached = ($t === '1');
|
||||
return $cached;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$found = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND INDEX_NAME = 'fea_ft'",
|
||||
$wpdb->posts
|
||||
));
|
||||
$cached = $found > 0;
|
||||
set_transient('fea_ft_index_exists', $cached ? '1' : '0', 12 * HOUR_IN_SECONDS);
|
||||
return $cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula el término FULLTEXT en Boolean Mode (cada palabra con prefijo *).
|
||||
* Devuelve '' si el término sanitizado queda vacío.
|
||||
*/
|
||||
function fea_ft_boolean_term(string $raw): string {
|
||||
$term = trim(substr(preg_replace('/[^\p{L}\p{N}\s\'\-]/u', '', $raw), 0, 200));
|
||||
if ($term === '') return '';
|
||||
|
||||
global $wpdb;
|
||||
$words = preg_split('/\s+/', $term);
|
||||
return implode('* ', array_map(fn($w) => $wpdb->esc_like($w), $words)) . '*';
|
||||
}
|
||||
|
||||
/**
|
||||
* Reemplaza la cláusula WHERE de búsqueda por MATCH AGAINST.
|
||||
* Hook: posts_search (filtra el WHERE que WP construye para /?s=).
|
||||
*/
|
||||
add_filter('posts_search', function (string $search, WP_Query $q): string {
|
||||
if (is_admin() || !$q->is_main_query() || !$q->is_search()) return $search;
|
||||
if (!fea_ft_index_exists()) return $search; // degradación elegante
|
||||
|
||||
$raw = trim((string) $q->get('s'));
|
||||
if ($raw === '') return $search;
|
||||
|
||||
$ft_term = fea_ft_boolean_term($raw);
|
||||
if ($ft_term === '') return $search;
|
||||
|
||||
global $wpdb;
|
||||
$ft_esc = esc_sql($ft_term);
|
||||
|
||||
// WP construye " AND (...) " para la búsqueda; devolvemos un bloque AND compatible.
|
||||
return " AND (MATCH({$wpdb->posts}.post_title, {$wpdb->posts}.post_content) AGAINST ('{$ft_esc}' IN BOOLEAN MODE)) ";
|
||||
}, 10, 2);
|
||||
|
||||
/**
|
||||
* Ordena por relevancia FULLTEXT cuando no se especifica otro orden.
|
||||
* Hook: posts_clauses (permite modificar SELECT y ORDER BY juntos).
|
||||
*/
|
||||
add_filter('posts_clauses', function (array $clauses, WP_Query $q): array {
|
||||
if (is_admin() || !$q->is_main_query() || !$q->is_search()) return $clauses;
|
||||
if (!fea_ft_index_exists()) return $clauses; // degradación elegante
|
||||
|
||||
$raw = trim((string) $q->get('s'));
|
||||
if ($raw === '') return $clauses;
|
||||
|
||||
// Sólo reordenamos por relevancia si el orderby es el nativo de búsqueda.
|
||||
$ob = $q->get('orderby');
|
||||
if (!in_array($ob, ['relevance', 'date', ''], true)) return $clauses;
|
||||
|
||||
$ft_term = fea_ft_boolean_term($raw);
|
||||
if ($ft_term === '') return $clauses;
|
||||
|
||||
global $wpdb;
|
||||
$ft_esc = esc_sql($ft_term);
|
||||
|
||||
// Añadimos la columna de relevancia al SELECT y la usamos en ORDER BY.
|
||||
$score_col = "MATCH({$wpdb->posts}.post_title, {$wpdb->posts}.post_content) AGAINST ('{$ft_esc}' IN BOOLEAN MODE)";
|
||||
$clauses['fields'] .= ", ({$score_col}) AS fea_ft_score";
|
||||
$clauses['orderby'] = "fea_ft_score DESC, {$wpdb->posts}.post_date DESC";
|
||||
|
||||
return $clauses;
|
||||
}, 10, 2);
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fe Adulta — Buscador visible (#8, MVP nativo)
|
||||
* Description: Inyecta una barra de búsqueda visible bajo la cabecera (template part
|
||||
* FSE 'header'), usando el buscador nativo de WordPress (/?s=). Multiidioma:
|
||||
* el form apunta a la home del idioma actual (Polylang filtra por idioma).
|
||||
* Fase 2: sustituir el motor por Typesense (self-host) manteniendo esta UI.
|
||||
* Version: 1.0
|
||||
*/
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
/** HTML del formulario de búsqueda (home del idioma actual como action). */
|
||||
function fea_search_form_html(): string {
|
||||
// Raíz del idioma actual (Polylang) para que /?s= busque en ese idioma:
|
||||
// ES (idioma por defecto) → /; EN/FR/IT/PT → /<lang>/.
|
||||
$base = home_url('/');
|
||||
if (function_exists('pll_current_language')) {
|
||||
$lang = pll_current_language();
|
||||
$default = function_exists('pll_default_language') ? pll_default_language() : 'es';
|
||||
if ($lang && $lang !== $default) $base = home_url('/' . $lang . '/');
|
||||
}
|
||||
$action = esc_url($base);
|
||||
$q = esc_attr(get_search_query());
|
||||
$ph = esc_attr__('Buscar reflexiones, artículos, autores…', 'default');
|
||||
$svg = '<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false">'
|
||||
. '<path fill="currentColor" d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 1 0-.7.7l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0A4.5 4.5 0 1 1 14 9.5 4.5 4.5 0 0 1 9.5 14z"/></svg>';
|
||||
return '<div class="fea-search-bar"><form role="search" method="get" class="fea-search" action="' . $action . '">'
|
||||
. '<input type="search" name="s" value="' . $q . '" placeholder="' . $ph . '" aria-label="Buscar">'
|
||||
. '<button type="submit" aria-label="Buscar">' . $svg . '</button>'
|
||||
. '</form></div>';
|
||||
}
|
||||
|
||||
/** Inyecta la barra al final del template part 'header'. */
|
||||
add_filter('render_block', function ($html, $block) {
|
||||
if (is_admin()) return $html;
|
||||
if (($block['blockName'] ?? '') !== 'core/template-part') return $html;
|
||||
// El home usa el part 'cabecera-portada'; el resto del sitio usa 'header'.
|
||||
if (!in_array($block['attrs']['slug'] ?? '', ['header', 'cabecera-portada'], true)) return $html;
|
||||
return $html . fea_search_form_html();
|
||||
}, 20, 2);
|
||||
|
||||
add_action('wp_head', function () {
|
||||
?>
|
||||
<style>
|
||||
/* En desktop se usa el buscador del menú; esta barra es para móvil, donde el
|
||||
menú colapsa en hamburguesa y el buscador del menú queda oculto (#8). */
|
||||
.fea-search-bar{display:none;justify-content:center;padding:.5rem 1rem;
|
||||
background:#faf6f7;border-top:1px solid #efe2e5;border-bottom:1px solid #efe2e5}
|
||||
@media(max-width:600px){.fea-search-bar{display:flex}}
|
||||
.fea-search{display:flex;align-items:center;width:100%;max-width:560px;
|
||||
background:#fff;border:1px solid #d9c4c9;border-radius:999px;overflow:hidden}
|
||||
.fea-search input[type=search]{flex:1 1 auto;border:0;outline:0;background:transparent;
|
||||
font-size:.95rem;padding:.55rem .9rem;color:#222}
|
||||
.fea-search input[type=search]::placeholder{color:#9a8a8e}
|
||||
.fea-search button{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
|
||||
border:0;cursor:pointer;background:#8b1a2e;color:#fff;width:42px;align-self:stretch}
|
||||
.fea-search button:hover{background:#6f1525}
|
||||
@media(max-width:600px){.fea-search-bar{padding:.45rem .6rem}}
|
||||
</style>
|
||||
<?php
|
||||
});
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
/**
|
||||
* fea-share — Sección "Comparte Fe Adulta" en single posts + Open Graph tags.
|
||||
* Botones: Facebook, Instagram (Web Share API + fallback copiar), Imprimir.
|
||||
* Sin plugins externos, sin JS de terceros, sin tracking.
|
||||
*/
|
||||
|
||||
/** Solo artículos reales: single post_type=post, excluyendo institucionales. */
|
||||
function fea_share_eligible(): bool {
|
||||
if (!is_singular('post')) return false;
|
||||
if (function_exists('fea_hide_static_meta') && fea_hide_static_meta()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Textos por idioma. */
|
||||
function fea_share_labels(): array {
|
||||
$lang = function_exists('fea_current_lang') ? fea_current_lang() : 'es';
|
||||
$all = [
|
||||
'es' => ['section' => 'Comparte FeAdulta', 'fb' => 'Facebook',
|
||||
'ig' => 'Instagram', 'print' => 'Imprimir',
|
||||
'copied' => '¡Enlace copiado! Compártelo en Instagram'],
|
||||
'en' => ['section' => 'Share FeAdulta', 'fb' => 'Facebook',
|
||||
'ig' => 'Instagram', 'print' => 'Print',
|
||||
'copied' => 'Link copied! Share it on Instagram'],
|
||||
'fr' => ['section' => 'Partager FeAdulta', 'fb' => 'Facebook',
|
||||
'ig' => 'Instagram', 'print' => 'Imprimer',
|
||||
'copied' => 'Lien copié ! Partagez-le sur Instagram'],
|
||||
'it' => ['section' => 'Condividi FeAdulta', 'fb' => 'Facebook',
|
||||
'ig' => 'Instagram', 'print' => 'Stampa',
|
||||
'copied' => 'Link copiato! Condividilo su Instagram'],
|
||||
'pt' => ['section' => 'Partilha FeAdulta', 'fb' => 'Facebook',
|
||||
'ig' => 'Instagram', 'print' => 'Imprimir',
|
||||
'copied' => 'Link copiado! Partilha-o no Instagram'],
|
||||
];
|
||||
return $all[$lang] ?? $all['es'];
|
||||
}
|
||||
|
||||
function fea_share_block_html(): string {
|
||||
$t = fea_share_labels();
|
||||
$url = rawurlencode(get_permalink());
|
||||
|
||||
$svg_fb = '<svg viewBox="0 0 24 24" width="17" height="17" aria-hidden="true" focusable="false" fill="currentColor"><path d="M22 12.06C22 6.5 17.52 2 12 2 6.48 2 2 6.5 2 12.06c0 5.02 3.66 9.18 8.44 9.94v-7.03H7.9v-2.9h2.54V9.85c0-2.51 1.49-3.9 3.78-3.9 1.09 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.78-1.63 1.57v1.88h2.78l-.44 2.9h-2.34V22c4.78-.76 8.44-4.92 8.44-9.94z"/></svg>';
|
||||
$svg_ig = '<svg viewBox="0 0 24 24" width="17" height="17" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 1.366.062 2.633.334 3.608 1.308.974.975 1.245 2.242 1.307 3.608.058 1.265.07 1.645.07 4.85s-.012 3.584-.07 4.85c-.062 1.366-.334 2.633-1.308 3.608-.975.974-2.242 1.245-3.608 1.307-1.265.058-1.645.07-4.849.07s-3.584-.012-4.85-.07c-1.366-.062-2.633-.334-3.608-1.308-.974-.975-1.245-2.242-1.307-3.608C2.175 15.584 2.163 15.204 2.163 12s.012-3.584.07-4.85c.062-1.366.334-2.633 1.308-3.608C4.516 2.497 5.783 2.226 7.149 2.164 8.414 2.106 8.794 2.094 12 2.094zm0-2.163c-3.259 0-3.667.014-4.947.072-1.613.074-3.067.39-4.213 1.535C1.695 2.803 1.38 4.257 1.305 5.87.014 8.194 0 8.602 0 12c0 3.259.014 3.667.072 4.947.074 1.613.39 3.067 1.535 4.213 1.146 1.145 2.6 1.46 4.213 1.535C8.333 23.986 8.741 24 12 24c3.259 0 3.667-.014 4.947-.072 1.613-.074 3.067-.39 4.213-1.535 1.145-1.146 1.46-2.6 1.535-4.213C23.986 15.667 24 15.259 24 12c0-3.259-.014-3.667-.072-4.947-.074-1.613-.39-3.067-1.535-4.213C21.247 1.695 19.793 1.38 18.18 1.305 15.807.014 15.399 0 12 0zm0 5.838a6.162 6.162 0 1 0 0 12.324 6.162 6.162 0 0 0 0-12.324zm0 10.162a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm6.406-11.845a1.44 1.44 0 1 0 0 2.881 1.44 1.44 0 0 0 0-2.881z"/></svg>';
|
||||
$svg_pr = '<svg viewBox="0 0 24 24" width="17" height="17" aria-hidden="true" focusable="false" fill="currentColor"><path d="M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7a1 1 0 0 1 0-2 1 1 0 0 1 0 2zm-1-9H6v4h12V3z"/></svg>';
|
||||
|
||||
$copied_msg = esc_js($t['copied']);
|
||||
|
||||
return '
|
||||
<div class="fea-share">
|
||||
<div class="fea-share-rule" aria-hidden="true"></div>
|
||||
<div class="fea-share-row">
|
||||
<span class="fea-share-label">' . esc_html($t['section']) . '</span>
|
||||
<a href="https://www.facebook.com/sharer/sharer.php?u=' . $url . '"
|
||||
target="_blank" rel="noopener noreferrer nofollow"
|
||||
class="fea-share-item fea-share-fb">
|
||||
' . $svg_fb . '<span>' . esc_html($t['fb']) . '</span>
|
||||
</a>
|
||||
<span class="fea-share-sep" aria-hidden="true">·</span>
|
||||
<button type="button" class="fea-share-item fea-share-ig"
|
||||
data-copied="' . esc_attr($t['copied']) . '">
|
||||
' . $svg_ig . '<span>' . esc_html($t['ig']) . '</span>
|
||||
</button>
|
||||
<span class="fea-share-sep" aria-hidden="true">·</span>
|
||||
<button type="button" class="fea-share-item fea-share-print"
|
||||
onclick="window.print()">
|
||||
' . $svg_pr . '<span>' . esc_html($t['print']) . '</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="fea-share-toast" aria-live="polite"></div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
function fea_share_already_rendered(string $content): bool {
|
||||
return strpos($content, 'class="fea-share"') !== false
|
||||
|| strpos($content, "class='fea-share'") !== false;
|
||||
}
|
||||
|
||||
// ── Bloque al final del contenido ────────────────────────────────────────────
|
||||
add_filter('the_content', function ($content) {
|
||||
if (is_admin() || !fea_share_eligible() || !is_main_query() || !in_the_loop()) {
|
||||
return $content;
|
||||
}
|
||||
if (fea_share_already_rendered($content)) return $content;
|
||||
return $content . fea_share_block_html();
|
||||
}, 20);
|
||||
|
||||
add_filter('render_block', function ($block_content, $block) {
|
||||
if (is_admin() || !fea_share_eligible()) return $block_content;
|
||||
if (($block['blockName'] ?? '') !== 'core/post-content') return $block_content;
|
||||
if (fea_share_already_rendered($block_content)) return $block_content;
|
||||
return $block_content . fea_share_block_html();
|
||||
}, 20, 2);
|
||||
|
||||
// ── CSS (screen + print) ─────────────────────────────────────────────────────
|
||||
add_action('wp_head', function () {
|
||||
if (!fea_share_eligible()) return;
|
||||
?>
|
||||
<style>
|
||||
/* ── Sección compartir ── */
|
||||
.fea-share { margin: 2.5rem 0 1rem; }
|
||||
|
||||
.fea-share-rule {
|
||||
border: none; border-top: 1px solid #e2d9d0; margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.fea-share-row {
|
||||
display: flex; align-items: center; flex-wrap: wrap;
|
||||
gap: .15rem .4rem; color: #5a4a42;
|
||||
}
|
||||
|
||||
.fea-share-label {
|
||||
font-size: .78rem; font-weight: 700; letter-spacing: .06em;
|
||||
text-transform: uppercase; color: #8a7a72; margin-right: .4rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Reset total de <button> + alineación compartida con <a> */
|
||||
.fea-share-item {
|
||||
display: inline-flex; align-items: center; gap: .32rem;
|
||||
background: none; border: none; padding: 0; margin: 0; cursor: pointer;
|
||||
font-family: inherit; font-size: .84rem; font-weight: 600; line-height: 1;
|
||||
color: #8b1a2e; text-decoration: none;
|
||||
transition: color .15s;
|
||||
}
|
||||
.fea-share-item:hover { color: #5a1220; }
|
||||
.fea-share-item svg { display: block; flex-shrink: 0; }
|
||||
|
||||
.fea-share-sep { color: #c9b8ae; font-size: .84rem; line-height: 1; user-select: none; }
|
||||
|
||||
/* Toast */
|
||||
.fea-share-toast {
|
||||
font-size: .8rem; color: #1b7a34; margin-top: .5rem;
|
||||
min-height: 1.1em; transition: opacity .3s;
|
||||
}
|
||||
.fea-share-toast.hidden { opacity: 0; }
|
||||
|
||||
/* ── CSS de impresión ── */
|
||||
@media print {
|
||||
/* Ocultar todo lo que no es contenido editorial */
|
||||
#fea-beta-bar, #fea-fb,
|
||||
.fea-share, .fea-share-btns,
|
||||
header, nav, footer,
|
||||
.wp-block-navigation, .wp-site-logo,
|
||||
.fea-pensamientos-wrap, .fea-cookie-consent,
|
||||
#wpadminbar { display: none !important; }
|
||||
|
||||
body, .entry-content, .wp-block-post-content {
|
||||
font-size: 12pt; line-height: 1.6; color: #000;
|
||||
background: #fff; margin: 0; padding: 0;
|
||||
}
|
||||
a { color: #000; text-decoration: underline; }
|
||||
a[href]::after { content: none; } /* no imprimir URLs */
|
||||
img { max-width: 100%; }
|
||||
h1, h2, h3 { page-break-after: avoid; }
|
||||
p { orphans: 3; widows: 3; }
|
||||
|
||||
/* Cabecera de impresión con nombre del sitio */
|
||||
body::before {
|
||||
content: "FeAdulta — feadulta.com";
|
||||
display: block; font-size: 9pt; color: #666;
|
||||
border-bottom: 1px solid #ccc; padding-bottom: 4pt;
|
||||
margin-bottom: 12pt;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
}, 20);
|
||||
|
||||
// ── JS: Instagram (Web Share API + fallback copiar) ──────────────────────────
|
||||
add_action('wp_footer', function () {
|
||||
if (!fea_share_eligible()) return;
|
||||
?>
|
||||
<script>
|
||||
(function(){
|
||||
var btn = document.querySelector('.fea-share-ig');
|
||||
if (!btn) return;
|
||||
var toast = document.querySelector('.fea-share-toast');
|
||||
var copied = btn.getAttribute('data-copied');
|
||||
var url = <?php echo json_encode(get_permalink()); ?>;
|
||||
var title = <?php
|
||||
$post = get_queried_object();
|
||||
$raw = $post instanceof WP_Post ? get_the_title($post) : '';
|
||||
$t = function_exists('fea_title') ? fea_title($raw) : $raw;
|
||||
echo json_encode($t);
|
||||
?>;
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
if (navigator.share) {
|
||||
navigator.share({ title: title, url: url }).catch(function(){});
|
||||
} else {
|
||||
navigator.clipboard.writeText(url).then(function(){
|
||||
if (toast) {
|
||||
toast.textContent = copied;
|
||||
toast.classList.remove('hidden');
|
||||
setTimeout(function(){ toast.classList.add('hidden'); }, 3000);
|
||||
}
|
||||
}).catch(function(){});
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}, 20);
|
||||
|
||||
// ── Open Graph tags ───────────────────────────────────────────────────────────
|
||||
add_action('wp_head', function () {
|
||||
if (!fea_share_eligible()) return;
|
||||
$post = get_queried_object();
|
||||
if (!$post instanceof WP_Post) return;
|
||||
|
||||
$raw = get_the_title($post);
|
||||
$title = function_exists('fea_title') ? fea_title($raw) : $raw;
|
||||
$desc = has_excerpt($post)
|
||||
? get_the_excerpt($post)
|
||||
: wp_trim_words(wp_strip_all_tags($post->post_content), 30, '…');
|
||||
$url = get_permalink($post);
|
||||
|
||||
$img = get_the_post_thumbnail_url($post, 'large');
|
||||
if (!$img && preg_match('~<img[^>]+src="([^"]+)"~', $post->post_content, $m)) {
|
||||
$img = $m[1];
|
||||
}
|
||||
if ($img && strpos($img, 'http') !== 0) {
|
||||
if (strpos($img, '//') === 0) {
|
||||
$img = (is_ssl() ? 'https:' : 'http:') . $img;
|
||||
} elseif ($img[0] === '/') {
|
||||
$origin = preg_replace('~^(https?://[^/]+).*~', '$1', home_url('/'));
|
||||
$img = $origin . $img;
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
echo '<meta property="og:type" content="article" />' . "\n";
|
||||
echo '<meta property="og:site_name" content="Fe Adulta" />' . "\n";
|
||||
echo '<meta property="og:title" content="' . esc_attr($title) . '" />' . "\n";
|
||||
echo '<meta property="og:description" content="' . esc_attr($desc) . '" />' . "\n";
|
||||
echo '<meta property="og:url" content="' . esc_url($url) . '" />' . "\n";
|
||||
if ($img) {
|
||||
echo '<meta property="og:image" content="' . esc_url($img) . '" />' . "\n";
|
||||
}
|
||||
}, 5);
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fe Adulta — Slider home sync (filesystem → Smart Slider 3)
|
||||
* Description: Sincroniza los slides del Smart Slider 3 id 2 ("Slider_home")
|
||||
* con el contenido del directorio `wp-content/uploads/home/`.
|
||||
* Imita el modelo Joomla mod_ariimageslider que leía
|
||||
* `images/home/` automáticamente.
|
||||
* Version: 1.0
|
||||
*
|
||||
* Operativa para el editor: subir/borrar ficheros en `uploads/home/`.
|
||||
* El slider de la portada se actualiza solo al primer pageview.
|
||||
*
|
||||
* Ver issue rafa/feadulta#43.
|
||||
*/
|
||||
|
||||
const FEA_SLIDER_ID = 2;
|
||||
const FEA_SLIDER_DIRNAME = 'home';
|
||||
const FEA_SLIDER_EXTS = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
const FEA_SLIDER_OPT_KEY = 'fea_slider_home_mtime';
|
||||
|
||||
/**
|
||||
* Lista las imágenes del directorio uploads/home/, ordenadas por nombre.
|
||||
*/
|
||||
function fea_slider_home_files() {
|
||||
$uploads = wp_upload_dir();
|
||||
$dir = trailingslashit($uploads['basedir']) . FEA_SLIDER_DIRNAME;
|
||||
if (!is_dir($dir)) return [];
|
||||
$files = [];
|
||||
foreach (scandir($dir) as $name) {
|
||||
if ($name === '.' || $name === '..') continue;
|
||||
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, FEA_SLIDER_EXTS, true)) continue;
|
||||
$files[] = $name;
|
||||
}
|
||||
sort($files, SORT_NATURAL);
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* mtime del directorio (cambia al añadir/borrar ficheros).
|
||||
*/
|
||||
function fea_slider_home_dir_mtime() {
|
||||
$uploads = wp_upload_dir();
|
||||
$dir = trailingslashit($uploads['basedir']) . FEA_SLIDER_DIRNAME;
|
||||
if (!is_dir($dir)) return 0;
|
||||
$m = (int) @filemtime($dir);
|
||||
// Sumar mtime de cada fichero para detectar reemplazos del mismo nombre
|
||||
foreach (fea_slider_home_files() as $f) {
|
||||
$m = max($m, (int) @filemtime($dir . '/' . $f));
|
||||
}
|
||||
return $m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sincroniza la tabla wp_nextend2_smartslider3_slides para el slider 2
|
||||
* con los ficheros del directorio. Idempotente.
|
||||
*
|
||||
* - Reusa slides existentes (manteniendo IDs) cuando coincide la imagen
|
||||
* - Crea slides nuevos para imágenes nuevas
|
||||
* - Borra slides cuya imagen ya no está
|
||||
*/
|
||||
function fea_slider_home_sync_now($force = false) {
|
||||
global $wpdb;
|
||||
$mtime = fea_slider_home_dir_mtime();
|
||||
if (!$force) {
|
||||
$last = (int) get_option(FEA_SLIDER_OPT_KEY, 0);
|
||||
if ($last === $mtime && $mtime > 0) return false;
|
||||
}
|
||||
|
||||
$files = fea_slider_home_files();
|
||||
|
||||
$uploads = wp_upload_dir();
|
||||
$reldir = '$upload$/' . FEA_SLIDER_DIRNAME; // SS3 variable placeholder
|
||||
|
||||
// Leer slides actuales del slider 2 → mapeo imagen → row
|
||||
$existing = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT id, title, params, ordering FROM {$wpdb->prefix}nextend2_smartslider3_slides WHERE slider=%d ORDER BY ordering",
|
||||
FEA_SLIDER_ID
|
||||
), ARRAY_A);
|
||||
|
||||
$by_image = [];
|
||||
foreach ($existing as $row) {
|
||||
$p = json_decode($row['params'], true) ?: [];
|
||||
$img = isset($p['backgroundImage']) ? basename($p['backgroundImage']) : null;
|
||||
if ($img) $by_image[$img] = $row;
|
||||
}
|
||||
|
||||
$keep_ids = [];
|
||||
$ordering = 0;
|
||||
foreach ($files as $file) {
|
||||
$ordering++;
|
||||
if (isset($by_image[$file])) {
|
||||
// Reutilizar slide existente — actualizar params si fuera necesario
|
||||
$row = $by_image[$file];
|
||||
$p = json_decode($row['params'], true) ?: [];
|
||||
$expected = $reldir . '/' . $file;
|
||||
$needs_update = false;
|
||||
if (($p['backgroundImage'] ?? '') !== $expected) { $p['backgroundImage'] = $expected; $needs_update = true; }
|
||||
if (($p['background-type'] ?? '') !== 'image') { $p['background-type'] = 'image'; $needs_update = true; }
|
||||
if ($needs_update) {
|
||||
$wpdb->update(
|
||||
$wpdb->prefix . 'nextend2_smartslider3_slides',
|
||||
['params' => wp_json_encode($p), 'ordering' => $ordering],
|
||||
['id' => $row['id']],
|
||||
['%s','%d'], ['%d']
|
||||
);
|
||||
} else {
|
||||
$wpdb->update(
|
||||
$wpdb->prefix . 'nextend2_smartslider3_slides',
|
||||
['ordering' => $ordering],
|
||||
['id' => $row['id']],
|
||||
['%d'], ['%d']
|
||||
);
|
||||
}
|
||||
$keep_ids[] = (int) $row['id'];
|
||||
} else {
|
||||
// Crear slide nuevo
|
||||
$title = pathinfo($file, PATHINFO_FILENAME);
|
||||
$params = wp_json_encode([
|
||||
'background-type' => 'image',
|
||||
'backgroundImage' => $reldir . '/' . $file,
|
||||
'version' => '3.5.1.32',
|
||||
]);
|
||||
$img_url = $reldir . '/' . $file;
|
||||
$wpdb->insert(
|
||||
$wpdb->prefix . 'nextend2_smartslider3_slides',
|
||||
[
|
||||
'slider' => FEA_SLIDER_ID,
|
||||
'title' => $title,
|
||||
'description' => '',
|
||||
'params' => $params,
|
||||
'slide' => '[]',
|
||||
'thumbnail' => $img_url,
|
||||
'publish_up' => '1970-01-01 00:00:00',
|
||||
'publish_down' => '1970-01-01 00:00:00',
|
||||
'published' => 1,
|
||||
'first' => 0,
|
||||
'generator_id' => 0,
|
||||
'ordering' => $ordering,
|
||||
],
|
||||
['%d','%s','%s','%s','%s','%s','%s','%s','%d','%d','%d','%d']
|
||||
);
|
||||
$keep_ids[] = (int) $wpdb->insert_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Borrar slides cuya imagen ya no está en el directorio
|
||||
if ($keep_ids) {
|
||||
$in = implode(',', array_map('intval', $keep_ids));
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->prefix}nextend2_smartslider3_slides WHERE slider=%d AND id NOT IN ($in)",
|
||||
FEA_SLIDER_ID
|
||||
));
|
||||
} else {
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->prefix}nextend2_smartslider3_slides WHERE slider=%d",
|
||||
FEA_SLIDER_ID
|
||||
));
|
||||
}
|
||||
|
||||
update_option(FEA_SLIDER_OPT_KEY, $mtime);
|
||||
|
||||
// Marcar slider como cambiado para que SS3 regenere su manifest
|
||||
$wpdb->update(
|
||||
$wpdb->prefix . 'nextend2_section_storage',
|
||||
['value' => (string) $mtime],
|
||||
['application' => 'smartslider', 'section' => 'sliderChanged', 'referenceKey' => (string) FEA_SLIDER_ID],
|
||||
['%s'], ['%s','%s','%s']
|
||||
);
|
||||
|
||||
return count($files);
|
||||
}
|
||||
|
||||
// Ejecutar sync en cada carga de portada (la comprobación de mtime evita trabajo si nada cambió).
|
||||
add_action('template_redirect', function() {
|
||||
if (!is_front_page()) return;
|
||||
fea_slider_home_sync_now();
|
||||
}, 5);
|
||||
|
||||
// Sync también al entrar al admin (por si el editor sube ficheros desde wp-admin).
|
||||
add_action('admin_init', function() {
|
||||
fea_slider_home_sync_now();
|
||||
});
|
||||
|
||||
// WP-CLI helper: `wp eval "fea_slider_home_sync_now(true);"` fuerza resync ignorando mtime.
|
||||
Executable
+336
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fe Adulta — Support Campaign
|
||||
* Description: Landing y banner discreto de apoyo económico para la migración de Fe Adulta.
|
||||
* Version: 1.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
const FEA_SUPPORT_TEMPLATE = 'fea-support-campaign.php';
|
||||
const FEA_SUPPORT_TEMPLATE_LABEL = 'Fe Adulta — Campaña de apoyo';
|
||||
|
||||
function fea_support_template_path(): string {
|
||||
return __DIR__ . '/fea-support-campaign/template.php';
|
||||
}
|
||||
|
||||
function fea_support_is_spanish_context(): bool {
|
||||
if (!function_exists('pll_current_language')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return pll_current_language() === 'es';
|
||||
}
|
||||
|
||||
function fea_support_campaign_page(): ?WP_Post {
|
||||
static $page = 'unset';
|
||||
|
||||
if ($page !== 'unset') {
|
||||
return $page instanceof WP_Post ? $page : null;
|
||||
}
|
||||
|
||||
$pages = get_posts([
|
||||
'post_type' => 'page',
|
||||
'post_status' => ['publish', 'draft', 'private'],
|
||||
'posts_per_page' => 1,
|
||||
'meta_key' => '_wp_page_template',
|
||||
'meta_value' => FEA_SUPPORT_TEMPLATE,
|
||||
]);
|
||||
|
||||
if (!$pages) {
|
||||
$slug_page = get_page_by_path('apoya-feadulta', OBJECT, 'page');
|
||||
$page = $slug_page instanceof WP_Post ? $slug_page : null;
|
||||
return $page;
|
||||
}
|
||||
|
||||
$page = $pages[0];
|
||||
return $page;
|
||||
}
|
||||
|
||||
function fea_support_campaign_url(): string {
|
||||
$page = fea_support_campaign_page();
|
||||
if (!$page || $page->post_status !== 'publish') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (string) get_permalink($page);
|
||||
}
|
||||
|
||||
function fea_support_meta(int $page_id, string $key, $default = '') {
|
||||
$value = get_post_meta($page_id, $key, true);
|
||||
return ($value === '' || $value === null) ? $default : $value;
|
||||
}
|
||||
|
||||
function fea_support_campaign_data(?int $page_id = null): array {
|
||||
$page_id = $page_id ?: (fea_support_campaign_page()?->ID ?? 0);
|
||||
if (!$page_id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$goal = (float) fea_support_meta($page_id, 'fea_support_goal', 2000);
|
||||
$raised = (float) fea_support_meta($page_id, 'fea_support_raised', 0);
|
||||
$goal = $goal > 0 ? $goal : 2000;
|
||||
$raised = max(0, $raised);
|
||||
|
||||
return [
|
||||
'page_id' => $page_id,
|
||||
'eyebrow' => (string) fea_support_meta($page_id, 'fea_support_eyebrow', 'Apoya Fe Adulta'),
|
||||
'hero_title' => (string) fea_support_meta($page_id, 'fea_support_hero_title', 'Ayúdanos a sostener la nueva web de Fe Adulta'),
|
||||
'hero_intro' => (string) fea_support_meta($page_id, 'fea_support_hero_intro', 'La migración de Fe Adulta ha requerido meses de trabajo y un coste aproximado de 2000€. Si puedes colaborar, por pequeña que sea la aportación, nos ayudas a sostener este esfuerzo compartido.'),
|
||||
'progress_note' => (string) fea_support_meta($page_id, 'fea_support_progress_note', 'Objetivo aproximado para cubrir el trabajo técnico y la migración.'),
|
||||
'banner_title' => (string) fea_support_meta($page_id, 'fea_support_banner_title', 'Estamos sosteniendo la nueva web entre todos'),
|
||||
'banner_text' => (string) fea_support_meta($page_id, 'fea_support_banner_text', 'La migración ha supuesto meses de trabajo y unos 2000€ de coste. Si puedes colaborar, nos ayudas a cuidar Fe Adulta.'),
|
||||
'goal' => $goal,
|
||||
'raised' => $raised,
|
||||
'stripe_url' => (string) fea_support_meta($page_id, 'fea_support_stripe_url', ''),
|
||||
'cajamar_url' => (string) fea_support_meta($page_id, 'fea_support_cajamar_url', ''),
|
||||
'paypal_url' => (string) fea_support_meta($page_id, 'fea_support_paypal_url', ''),
|
||||
];
|
||||
}
|
||||
|
||||
function fea_support_amount(float $amount): string {
|
||||
if (floor($amount) === $amount) {
|
||||
return number_format_i18n($amount, 0) . '€';
|
||||
}
|
||||
|
||||
return number_format_i18n($amount, 2) . '€';
|
||||
}
|
||||
|
||||
function fea_support_progress_percent(array $data): float {
|
||||
$goal = (float) ($data['goal'] ?? 0);
|
||||
$raised = (float) ($data['raised'] ?? 0);
|
||||
|
||||
if ($goal <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return max(0, min(100, ($raised / $goal) * 100));
|
||||
}
|
||||
|
||||
function fea_support_buttons_html(array $data, string $context = 'page'): string {
|
||||
$buttons = [
|
||||
'stripe_url' => ['label' => 'Donar con Stripe', 'class' => 'is-primary'],
|
||||
'cajamar_url' => ['label' => 'Donar con Cajamar', 'class' => 'is-secondary'],
|
||||
'paypal_url' => ['label' => 'Donar con PayPal', 'class' => 'is-secondary'],
|
||||
];
|
||||
|
||||
$html = '<div class="fea-support-actions fea-support-actions--' . esc_attr($context) . '">';
|
||||
foreach ($buttons as $key => $config) {
|
||||
if (empty($data[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$html .= '<a class="fea-support-button ' . esc_attr($config['class']) . '" href="'
|
||||
. esc_url($data[$key]) . '" target="_blank" rel="noopener">'
|
||||
. esc_html($config['label']) . '</a>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
function fea_support_progress_html(array $data, string $context = 'page'): string {
|
||||
$percent = fea_support_progress_percent($data);
|
||||
$summary = fea_support_amount((float) $data['raised']) . ' de ' . fea_support_amount((float) $data['goal']);
|
||||
|
||||
$html = '<div class="fea-support-progress fea-support-progress--' . esc_attr($context) . '">';
|
||||
$html .= '<div class="fea-support-progress__numbers">';
|
||||
$html .= '<strong>' . esc_html($summary) . '</strong>';
|
||||
$html .= '<span>' . esc_html(number_format_i18n($percent, 0)) . '%</span>';
|
||||
$html .= '</div>';
|
||||
$html .= '<div class="fea-support-progress__track" aria-hidden="true">';
|
||||
$html .= '<span class="fea-support-progress__fill" style="width:' . esc_attr(number_format($percent, 2, '.', '')) . '%"></span>';
|
||||
$html .= '</div>';
|
||||
if (!empty($data['progress_note'])) {
|
||||
$html .= '<p class="fea-support-progress__note">' . esc_html($data['progress_note']) . '</p>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
function fea_support_banner_html(): string {
|
||||
$page = fea_support_campaign_page();
|
||||
if (!$page || $page->post_status !== 'publish') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$data = fea_support_campaign_data($page->ID);
|
||||
$url = get_permalink($page);
|
||||
|
||||
$html = '<section class="fea-support-banner" aria-label="Campaña de apoyo económico">';
|
||||
$html .= '<div class="fea-support-banner__copy">';
|
||||
$html .= '<span class="fea-support-banner__eyebrow">' . esc_html($data['eyebrow']) . '</span>';
|
||||
$html .= '<h2 class="fea-support-banner__title">' . esc_html($data['banner_title']) . '</h2>';
|
||||
$html .= '<p class="fea-support-banner__text">' . esc_html($data['banner_text']) . '</p>';
|
||||
$html .= '</div>';
|
||||
$html .= '<div class="fea-support-banner__side">';
|
||||
$html .= fea_support_progress_html($data, 'banner');
|
||||
$html .= '<div class="fea-support-banner__links">';
|
||||
$html .= '<a class="fea-support-button is-primary" href="' . esc_url($url) . '">Ver la campaña</a>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</section>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
add_filter('theme_page_templates', function(array $templates, WP_Theme $theme, ?WP_Post $post, string $post_type): array {
|
||||
if ($post_type === 'page') {
|
||||
$templates[FEA_SUPPORT_TEMPLATE] = FEA_SUPPORT_TEMPLATE_LABEL;
|
||||
}
|
||||
|
||||
return $templates;
|
||||
}, 10, 4);
|
||||
|
||||
add_filter('template_include', function(string $template): string {
|
||||
if (!is_page()) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
$page = get_queried_object();
|
||||
if (!$page instanceof WP_Post) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
if (get_page_template_slug($page) !== FEA_SUPPORT_TEMPLATE) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
return fea_support_template_path();
|
||||
});
|
||||
|
||||
add_action('acf/init', function() {
|
||||
if (!function_exists('acf_add_local_field_group')) {
|
||||
return;
|
||||
}
|
||||
|
||||
acf_add_local_field_group([
|
||||
'key' => 'group_fea_support_campaign',
|
||||
'title' => 'Campaña de apoyo económico',
|
||||
'fields' => [
|
||||
[
|
||||
'key' => 'field_fea_support_goal',
|
||||
'label' => 'Objetivo económico',
|
||||
'name' => 'fea_support_goal',
|
||||
'type' => 'number',
|
||||
'instructions' => 'Importe objetivo de la campaña.',
|
||||
'default_value' => 2000,
|
||||
'min' => 1,
|
||||
'step' => 1,
|
||||
],
|
||||
[
|
||||
'key' => 'field_fea_support_raised',
|
||||
'label' => 'Importe recaudado',
|
||||
'name' => 'fea_support_raised',
|
||||
'type' => 'number',
|
||||
'instructions' => 'Cantidad actual recaudada.',
|
||||
'default_value' => 0,
|
||||
'min' => 0,
|
||||
'step' => 0.01,
|
||||
],
|
||||
[
|
||||
'key' => 'field_fea_support_eyebrow',
|
||||
'label' => 'Antetítulo',
|
||||
'name' => 'fea_support_eyebrow',
|
||||
'type' => 'text',
|
||||
'default_value' => 'Apoya Fe Adulta',
|
||||
],
|
||||
[
|
||||
'key' => 'field_fea_support_hero_title',
|
||||
'label' => 'Título principal',
|
||||
'name' => 'fea_support_hero_title',
|
||||
'type' => 'text',
|
||||
'default_value' => 'Ayúdanos a sostener la nueva web de Fe Adulta',
|
||||
],
|
||||
[
|
||||
'key' => 'field_fea_support_hero_intro',
|
||||
'label' => 'Texto principal',
|
||||
'name' => 'fea_support_hero_intro',
|
||||
'type' => 'textarea',
|
||||
'rows' => 4,
|
||||
'new_lines' => 'br',
|
||||
'default_value' => 'La migración de Fe Adulta ha requerido meses de trabajo y un coste aproximado de 2000€. Si puedes colaborar, por pequeña que sea la aportación, nos ayudas a sostener este esfuerzo compartido.',
|
||||
],
|
||||
[
|
||||
'key' => 'field_fea_support_progress_note',
|
||||
'label' => 'Nota bajo la barra',
|
||||
'name' => 'fea_support_progress_note',
|
||||
'type' => 'text',
|
||||
'default_value' => 'Objetivo aproximado para cubrir el trabajo técnico y la migración.',
|
||||
],
|
||||
[
|
||||
'key' => 'field_fea_support_banner_title',
|
||||
'label' => 'Título del banner de portada',
|
||||
'name' => 'fea_support_banner_title',
|
||||
'type' => 'text',
|
||||
'default_value' => 'Estamos sosteniendo la nueva web entre todos',
|
||||
],
|
||||
[
|
||||
'key' => 'field_fea_support_banner_text',
|
||||
'label' => 'Texto del banner de portada',
|
||||
'name' => 'fea_support_banner_text',
|
||||
'type' => 'textarea',
|
||||
'rows' => 3,
|
||||
'new_lines' => 'br',
|
||||
'default_value' => 'La migración ha supuesto meses de trabajo y unos 2000€ de coste. Si puedes colaborar, nos ayudas a cuidar Fe Adulta.',
|
||||
],
|
||||
[
|
||||
'key' => 'field_fea_support_stripe_url',
|
||||
'label' => 'URL Stripe',
|
||||
'name' => 'fea_support_stripe_url',
|
||||
'type' => 'url',
|
||||
],
|
||||
[
|
||||
'key' => 'field_fea_support_cajamar_url',
|
||||
'label' => 'URL TPV Cajamar',
|
||||
'name' => 'fea_support_cajamar_url',
|
||||
'type' => 'url',
|
||||
],
|
||||
[
|
||||
'key' => 'field_fea_support_paypal_url',
|
||||
'label' => 'URL PayPal',
|
||||
'name' => 'fea_support_paypal_url',
|
||||
'type' => 'url',
|
||||
],
|
||||
],
|
||||
'location' => [[
|
||||
['param' => 'page_template', 'operator' => '==', 'value' => FEA_SUPPORT_TEMPLATE],
|
||||
]],
|
||||
'position' => 'normal',
|
||||
'style' => 'default',
|
||||
'label_placement' => 'top',
|
||||
]);
|
||||
});
|
||||
|
||||
add_filter('the_content', function(string $content): string {
|
||||
// DESACTIVADO temporalmente: la campaña de apoyo (Codex) aún no está lista
|
||||
// (barra 0€/2.000€, sin enlaces de donación). No mostrar el banner en portada.
|
||||
// Reactivar quitando este return cuando la campaña esté terminada.
|
||||
return $content;
|
||||
|
||||
if (is_admin() || !is_main_query() || !in_the_loop()) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if (!function_exists('fea_is_front_page') || !fea_is_front_page()) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if (!fea_support_is_spanish_context()) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$banner = fea_support_banner_html();
|
||||
if (!$banner) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
return $content . $banner;
|
||||
}, 40);
|
||||
|
||||
add_shortcode('fea_support_campaign_banner', function() {
|
||||
return fea_support_banner_html();
|
||||
});
|
||||
Executable
+244
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
get_header();
|
||||
the_post();
|
||||
|
||||
$data = fea_support_campaign_data(get_the_ID());
|
||||
?>
|
||||
<style>
|
||||
.fea-support-page {
|
||||
--fea-support-burgundy: #8b1a2e;
|
||||
--fea-support-ink: #2a2320;
|
||||
--fea-support-warm: #f5efe7;
|
||||
--fea-support-line: #e6d8c5;
|
||||
--fea-support-card: #fffdf9;
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 2.5rem 1.25rem 4.5rem;
|
||||
color: var(--fea-support-ink);
|
||||
}
|
||||
.fea-support-hero {
|
||||
background: linear-gradient(180deg, #f3ece3 0%, #fbf7f1 100%);
|
||||
border: 1px solid var(--fea-support-line);
|
||||
border-radius: 24px;
|
||||
padding: 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.8fr);
|
||||
gap: 2rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
.fea-support-hero__eyebrow,
|
||||
.fea-support-banner__eyebrow {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.8rem;
|
||||
color: var(--fea-support-burgundy);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.fea-support-hero__title {
|
||||
font-family: "Fraunces", Georgia, serif;
|
||||
font-size: clamp(2rem, 4vw, 3.35rem);
|
||||
line-height: 1.05;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
.fea-support-hero__intro {
|
||||
margin: 0;
|
||||
font-size: 1.03rem;
|
||||
line-height: 1.7;
|
||||
max-width: 58ch;
|
||||
}
|
||||
.fea-support-card {
|
||||
background: var(--fea-support-card);
|
||||
border: 1px solid var(--fea-support-line);
|
||||
border-radius: 18px;
|
||||
padding: 1.25rem;
|
||||
box-shadow: 0 18px 40px -34px rgba(42, 35, 32, 0.55);
|
||||
}
|
||||
.fea-support-card__title {
|
||||
font-size: 0.95rem;
|
||||
margin: 0 0 1rem;
|
||||
color: #6f655c;
|
||||
}
|
||||
.fea-support-progress {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.fea-support-progress__numbers {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.55rem;
|
||||
}
|
||||
.fea-support-progress__numbers strong {
|
||||
font-size: 1.35rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.fea-support-progress__numbers span {
|
||||
font-size: 0.92rem;
|
||||
color: #6f655c;
|
||||
font-weight: 600;
|
||||
}
|
||||
.fea-support-progress__track {
|
||||
width: 100%;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
background: #eadfce;
|
||||
overflow: hidden;
|
||||
}
|
||||
.fea-support-progress__fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #8b1a2e, #c4884b);
|
||||
}
|
||||
.fea-support-progress__note {
|
||||
margin: 0.65rem 0 0;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.5;
|
||||
color: #6f655c;
|
||||
}
|
||||
.fea-support-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
.fea-support-button {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 44px;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 0.94rem;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
.fea-support-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 14px 24px -20px rgba(42, 35, 32, 0.55);
|
||||
}
|
||||
.fea-support-button.is-primary {
|
||||
background: var(--fea-support-burgundy);
|
||||
color: #fff;
|
||||
}
|
||||
.fea-support-button.is-secondary {
|
||||
background: #fff;
|
||||
color: var(--fea-support-ink);
|
||||
border: 1px solid var(--fea-support-line);
|
||||
}
|
||||
.fea-support-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
}
|
||||
.fea-support-content {
|
||||
min-width: 0;
|
||||
}
|
||||
.fea-support-content > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.fea-support-content h2,
|
||||
.fea-support-content h3 {
|
||||
font-family: "Fraunces", Georgia, serif;
|
||||
color: var(--fea-support-ink);
|
||||
}
|
||||
.fea-support-content h2 {
|
||||
margin-top: 2rem;
|
||||
font-size: clamp(1.5rem, 2.4vw, 2rem);
|
||||
}
|
||||
.fea-support-content h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.fea-support-content p,
|
||||
.fea-support-content li {
|
||||
line-height: 1.75;
|
||||
}
|
||||
.fea-support-sidebar {
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
}
|
||||
.fea-support-sidebar__small {
|
||||
margin: 1rem 0 0;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.55;
|
||||
color: #6f655c;
|
||||
}
|
||||
.fea-support-banner {
|
||||
max-width: 1180px;
|
||||
margin: 2rem auto 0;
|
||||
padding: 1.4rem 1.5rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--fea-support-line);
|
||||
background: linear-gradient(180deg, #f8f3eb 0%, #fffdf9 100%);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(260px, 0.85fr);
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
.fea-support-banner__title {
|
||||
font-family: "Fraunces", Georgia, serif;
|
||||
font-size: clamp(1.4rem, 2.6vw, 2rem);
|
||||
margin: 0 0 0.45rem;
|
||||
}
|
||||
.fea-support-banner__text {
|
||||
margin: 0;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.fea-support-banner__links {
|
||||
margin-top: 0.85rem;
|
||||
}
|
||||
@media (max-width: 920px) {
|
||||
.fea-support-hero,
|
||||
.fea-support-layout,
|
||||
.fea-support-banner {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.fea-support-sidebar {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<main id="wp--skip-link--target" class="fea-support-page">
|
||||
<section class="fea-support-hero">
|
||||
<div class="fea-support-hero__copy">
|
||||
<span class="fea-support-hero__eyebrow"><?php echo esc_html($data['eyebrow'] ?? 'Apoya Fe Adulta'); ?></span>
|
||||
<h1 class="fea-support-hero__title"><?php echo esc_html($data['hero_title'] ?? get_the_title()); ?></h1>
|
||||
<p class="fea-support-hero__intro"><?php echo esc_html($data['hero_intro'] ?? ''); ?></p>
|
||||
</div>
|
||||
|
||||
<aside class="fea-support-card" aria-label="Estado de la campaña">
|
||||
<p class="fea-support-card__title">Objetivo de la campaña</p>
|
||||
<?php echo fea_support_progress_html($data, 'page'); ?>
|
||||
<?php echo fea_support_buttons_html($data, 'page'); ?>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<div class="fea-support-layout">
|
||||
<div class="fea-support-content">
|
||||
<?php the_content(); ?>
|
||||
</div>
|
||||
|
||||
<aside class="fea-support-sidebar">
|
||||
<div class="fea-support-card">
|
||||
<p class="fea-support-card__title">Colaborar ahora</p>
|
||||
<?php echo fea_support_progress_html($data, 'sidebar'); ?>
|
||||
<?php echo fea_support_buttons_html($data, 'sidebar'); ?>
|
||||
<p class="fea-support-sidebar__small">
|
||||
Si compartes esta página con otras personas de la comunidad, también nos ayudas a acercarnos al objetivo.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php
|
||||
get_footer();
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
/**
|
||||
* fea-ui — Ajustes estéticos globales del front (issue #95).
|
||||
*
|
||||
* 1. Foco/click: elimina el recuadro gris/negro por defecto al hacer click,
|
||||
* mantiene foco visible y de marca SOLO con teclado (:focus-visible).
|
||||
* 2. Menú: tipografía editorial Fraunces (coherente con los títulos),
|
||||
* línea carmesí en hover/ítem activo, sin subrayado.
|
||||
*
|
||||
* CSS global (el menú y los enlaces están en todo el sitio), no solo en single.
|
||||
*/
|
||||
|
||||
if (!defined('FEA_UI_CRIMSON')) {
|
||||
define('FEA_UI_CRIMSON', '#8b1a2e'); // carmesí de marca
|
||||
}
|
||||
if (!defined('FEA_UI_INK')) {
|
||||
define('FEA_UI_INK', '#2a2320'); // texto de marca
|
||||
}
|
||||
|
||||
add_action('wp_head', function () {
|
||||
if (is_admin()) return;
|
||||
$crimson = FEA_UI_CRIMSON;
|
||||
$ink = FEA_UI_INK;
|
||||
?>
|
||||
<style id="fea-ui">
|
||||
/* ── 1. Foco / click ───────────────────────────────────────────── */
|
||||
a, button, summary, [role="button"] { -webkit-tap-highlight-color: transparent; }
|
||||
|
||||
/* Sin outline al hacer click con ratón/dedo… */
|
||||
a:focus:not(:focus-visible),
|
||||
button:focus:not(:focus-visible) { outline: none; }
|
||||
|
||||
/* …pero foco visible y de marca al navegar con teclado (accesibilidad) */
|
||||
a:focus-visible,
|
||||
button:focus-visible {
|
||||
outline: 2px solid <?php echo $crimson; ?>;
|
||||
outline-offset: 2px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* El core agranda el outline del menú (outline-offset:4px) → lo reducimos */
|
||||
.wp-block-navigation .wp-block-navigation-item .wp-block-navigation-item__content {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ── 2. Menú — Opción C · Sans limpia minimalista (issue #95) ──── */
|
||||
.wp-block-navigation .wp-block-navigation-item__content {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
font-weight: 500;
|
||||
color: <?php echo $ink; ?>;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Nunca subrayado (el core lo pone en hover) */
|
||||
.wp-block-navigation a.wp-block-navigation-item__content:hover,
|
||||
.wp-block-navigation a.wp-block-navigation-item__content:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Línea carmesí inferior, animada, en hover y en el ítem activo */
|
||||
.wp-block-navigation > .wp-block-navigation__container > .wp-block-navigation-item
|
||||
> .wp-block-navigation-item__content::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: -3px;
|
||||
height: 2px;
|
||||
background: <?php echo $crimson; ?>;
|
||||
transform: scaleX(0);
|
||||
transform-origin: left center;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
.wp-block-navigation > .wp-block-navigation__container > .wp-block-navigation-item
|
||||
> .wp-block-navigation-item__content:hover::after,
|
||||
.wp-block-navigation > .wp-block-navigation__container > .wp-block-navigation-item
|
||||
> .wp-block-navigation-item__content[aria-current]::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
.wp-block-navigation > .wp-block-navigation__container > .wp-block-navigation-item
|
||||
> .wp-block-navigation-item__content[aria-current] {
|
||||
color: <?php echo $crimson; ?>;
|
||||
}
|
||||
|
||||
/* Submenús: sin línea inferior; hover marca el texto en carmesí */
|
||||
.wp-block-navigation__submenu-container .wp-block-navigation-item__content::after {
|
||||
display: none;
|
||||
}
|
||||
.wp-block-navigation__submenu-container .wp-block-navigation-item__content:hover,
|
||||
.wp-block-navigation__submenu-container .wp-block-navigation-item__content[aria-current] {
|
||||
color: <?php echo $crimson; ?>;
|
||||
}
|
||||
|
||||
/* ── 3. Fondo cálido en artículos (issue #78 feedback "demasiado blanca") ── */
|
||||
body.single-post {
|
||||
background-color: #f5f0eb !important;
|
||||
--wp--preset--color--base: #f5f0eb;
|
||||
}
|
||||
|
||||
</style>
|
||||
<?php
|
||||
}, 25);
|
||||
@@ -0,0 +1 @@
|
||||
<?php remove_filter("template_redirect", "redirect_canonical"); add_filter("redirect_canonical", "__return_false"); remove_action("template_redirect", "wp_redirect_admin_locations", 1000);
|
||||
Reference in New Issue
Block a user