302 lines
13 KiB
HTML
302 lines
13 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="es">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet, noimageindex" />
|
||
<meta name="googlebot" content="noindex, nofollow" />
|
||
<title>FAQ — OCA Paraguay</title>
|
||
<link rel="icon" type="image/svg+xml" href="../img/favicon.svg" />
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Sora:wght@600;700;800&display=swap" rel="stylesheet" />
|
||
<style>
|
||
:root{
|
||
--plum:#714B67; --plum-dark:#4b2f43; --violet:#6C4CE0; --violet-soft:#8f7bf0;
|
||
--mint:#16C79A; --mint-dark:#0e8f6e; --py-red:#D52B1E; --py-blue:#0038A8;
|
||
--ink:#231b2b; --muted:#6b6473; --line:#e7e3ec; --bg:#faf9fc;
|
||
}
|
||
*{box-sizing:border-box}
|
||
[hidden]{display:none !important}
|
||
body{margin:0;font-family:Inter,system-ui,sans-serif;background:var(--bg);color:var(--ink);line-height:1.55}
|
||
header{background:#fff;border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20}
|
||
.wrap{max-width:920px;margin:0 auto;padding:0 20px}
|
||
.head{display:flex;align-items:center;gap:14px;padding:14px 0;flex-wrap:wrap}
|
||
.head img{width:38px;height:38px;border-radius:9px}
|
||
.head h1{font-family:Sora,sans-serif;font-size:1.05rem;margin:0}
|
||
.head h1 small{display:block;font-family:Inter,sans-serif;font-weight:500;font-size:.72rem;color:var(--muted);letter-spacing:.04em;text-transform:uppercase}
|
||
.tools{margin-left:auto;display:flex;align-items:center;gap:10px}
|
||
.noidx{font-size:.64rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--py-red);border:1px solid currentColor;border-radius:999px;padding:3px 9px;white-space:nowrap}
|
||
.lang{display:inline-flex;background:#f2eff8;border-radius:999px;padding:3px}
|
||
.lang button{border:none;background:transparent;font:inherit;font-size:.76rem;font-weight:700;padding:5px 11px;border-radius:999px;cursor:pointer;color:var(--muted)}
|
||
.lang button.on{background:#fff;color:var(--violet);box-shadow:0 1px 3px rgba(0,0,0,.12)}
|
||
main{padding:26px 0 60px}
|
||
.bar{margin-bottom:22px}
|
||
.bar input{width:100%;padding:12px 15px;border:1px solid var(--line);border-radius:12px;font:inherit;background:#fff}
|
||
.bar input:focus{outline:2px solid rgba(108,76,224,.22);border-color:var(--violet-soft)}
|
||
.msg{padding:12px 16px;border-radius:12px;margin-bottom:18px;font-size:.9rem;display:none}
|
||
.msg.show{display:block}
|
||
.msg.err{background:#fdf1ef;color:#8f2117;border:1px solid #f0c9c4}
|
||
.auth{background:#fff;border:1px solid var(--line);border-radius:14px;padding:18px 20px;display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end}
|
||
.auth h2{font-family:Sora,sans-serif;font-size:.95rem;margin:0 0 12px;width:100%;color:var(--muted)}
|
||
.auth input{flex:1 1 160px;padding:11px 13px;border:1px solid var(--line);border-radius:10px;font:inherit;background:#fff}
|
||
.auth input:focus{outline:2px solid rgba(108,76,224,.22);border-color:var(--violet-soft)}
|
||
.auth button{font:inherit;font-weight:600;border-radius:10px;border:1px solid transparent;padding:11px 18px;cursor:pointer;background:var(--violet);color:#fff}
|
||
.auth .err{width:100%;margin:4px 0 0;font-size:.85rem;color:var(--py-red);font-weight:600;min-height:1em}
|
||
.area{margin:0 0 26px}
|
||
.area h2{font-family:Sora,sans-serif;font-size:.8rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin:0 0 10px}
|
||
.card{background:#fff;border:1px solid var(--line);border-radius:14px;margin-bottom:10px;overflow:hidden;transition:border-color .18s,box-shadow .18s}
|
||
.card[open]{border-color:var(--violet-soft);box-shadow:0 8px 26px rgba(108,76,224,.09)}
|
||
.card summary{cursor:pointer;padding:15px 18px;font-weight:600;list-style:none;display:flex;align-items:center;gap:12px}
|
||
.card summary::-webkit-details-marker{display:none}
|
||
.card summary::after{content:"+";margin-left:auto;color:var(--violet);font-weight:800;font-size:1.15rem;line-height:1}
|
||
.card[open] summary::after{content:"–"}
|
||
.card .ans{padding:0 18px 17px;margin:0;white-space:pre-line;color:#41394a;font-size:.93rem}
|
||
.empty{color:var(--muted);padding:16px 0}
|
||
footer{border-top:1px solid var(--line);background:#fff;padding:18px 0;font-size:.76rem;color:var(--muted);text-align:center}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<div class="wrap head">
|
||
<img src="../img/logo.svg" alt="OCA Paraguay" width="38" height="38" />
|
||
<h1><span data-i18n="title">Preguntas frecuentes</span><small data-i18n="subtitle">OCA Paraguay · uso interno</small></h1>
|
||
<div class="tools">
|
||
<span class="noidx" data-i18n="badge">no indexada</span>
|
||
<span class="lang" role="group" aria-label="Language">
|
||
<button type="button" data-lang="es">ES</button>
|
||
<button type="button" data-lang="en">EN</button>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="wrap">
|
||
<div class="msg" id="msg"></div>
|
||
|
||
<form class="auth" id="auth" hidden>
|
||
<h2 data-i18n="loginTitle">Acceso</h2>
|
||
<input id="au" data-i18n-placeholder="user" placeholder="Usuario" autocomplete="username" autocapitalize="none" />
|
||
<input id="ap" type="password" data-i18n-placeholder="pass" placeholder="Contraseña" autocomplete="current-password" />
|
||
<button type="submit" data-i18n="enter">Entrar</button>
|
||
<p class="err" id="auth-err"></p>
|
||
</form>
|
||
<div class="bar">
|
||
<input type="search" id="q" data-i18n-placeholder="search" placeholder="Buscar…" aria-label="Buscar" />
|
||
</div>
|
||
<div id="list"><p class="empty" data-i18n="loading">Cargando…</p></div>
|
||
</main>
|
||
|
||
<footer><span data-i18n="footer">Página interna · no indexada</span> · <span id="count">—</span></footer>
|
||
|
||
<script>
|
||
const API = 'https://n8n.odoo4projects.com/webhook/oca-py/faq';
|
||
const el = (id) => document.getElementById(id);
|
||
|
||
const I18N = {
|
||
es: {
|
||
title: 'Preguntas frecuentes', subtitle: 'OCA Paraguay · uso interno', badge: 'no indexada',
|
||
search: 'Buscar pregunta, área o respuesta…', loading: 'Cargando…', empty: 'Sin resultados.',
|
||
footer: 'Página interna · no indexada', count: (n, total) => n + ' de ' + total + ' preguntas',
|
||
group: 'General', errRead: 'No se pudo leer el webhook',
|
||
errData: 'El webhook respondió sin datos',
|
||
hint: 'En n8n, poné el nodo Webhook en "Respond: When Last Node Finishes".',
|
||
loginTitle: 'Acceso restringido', user: 'Usuario', pass: 'Contraseña', enter: 'Entrar',
|
||
badCred: 'Usuario o contraseña incorrectos.',
|
||
},
|
||
en: {
|
||
title: 'Frequently asked questions', subtitle: 'OCA Paraguay · internal use', badge: 'not indexed',
|
||
search: 'Search question, area or answer…', loading: 'Loading…', empty: 'No results.',
|
||
footer: 'Internal page · not indexed', count: (n, total) => n + ' of ' + total + ' questions',
|
||
group: 'General', errRead: 'Could not read the webhook',
|
||
errData: 'The webhook returned no data',
|
||
hint: 'In n8n, set the Webhook node to "Respond: When Last Node Finishes".',
|
||
loginTitle: 'Restricted access', user: 'User', pass: 'Password', enter: 'Sign in',
|
||
badCred: 'Wrong user or password.',
|
||
}
|
||
};
|
||
|
||
function store(key, val) {
|
||
try { localStorage.setItem(key, val); } catch (e) { /* private mode / blocked storage */ }
|
||
}
|
||
function stored(key) {
|
||
try { return localStorage.getItem(key); } catch (e) { return null; }
|
||
}
|
||
|
||
const state = {
|
||
lang: ((stored('ocaFaqLang') || (navigator.language || 'es').slice(0, 2)) === 'en') ? 'en' : 'es',
|
||
items: [],
|
||
};
|
||
|
||
const t = (k) => I18N[state.lang][k] ?? k;
|
||
|
||
function applyLang() {
|
||
document.documentElement.lang = state.lang;
|
||
document.querySelectorAll('[data-i18n]').forEach(n => { n.textContent = t(n.dataset.i18n); });
|
||
document.querySelectorAll('[data-i18n-placeholder]').forEach(n => { n.placeholder = t(n.dataset.i18nPlaceholder); });
|
||
document.querySelectorAll('.lang button').forEach(b => b.classList.toggle('on', b.dataset.lang === state.lang));
|
||
render();
|
||
}
|
||
|
||
function flash(text) {
|
||
const m = el('msg');
|
||
m.textContent = text;
|
||
m.className = 'msg show err';
|
||
}
|
||
|
||
function hideMsg() { el('msg').className = 'msg'; }
|
||
|
||
function normalize(payload) {
|
||
if (Array.isArray(payload)) {
|
||
if (payload.length && Array.isArray(payload[0].data)) return payload[0].data;
|
||
if (payload.length && payload[0].id !== undefined) return payload;
|
||
}
|
||
if (payload && Array.isArray(payload.data)) return payload.data;
|
||
return [];
|
||
}
|
||
|
||
// Optional per-language variants: Question_en / Answer_es / area_en …
|
||
function pick(it, field) {
|
||
const want = (field + '_' + state.lang).toLowerCase();
|
||
const key = Object.keys(it).find(k => k.toLowerCase() === want);
|
||
if (key && it[key]) return it[key];
|
||
return it[field] ?? '';
|
||
}
|
||
|
||
// One field, two languages: "texto ES\n=== EN ===\ntext EN"
|
||
const LANG_SPLIT = /\n\s*===\s*EN\s*===\s*\n/i;
|
||
function content(it, field) {
|
||
const raw = pick(it, field);
|
||
if (typeof raw !== 'string' || !LANG_SPLIT.test(raw)) return raw;
|
||
const parts = raw.split(LANG_SPLIT);
|
||
return (state.lang === 'en' ? parts[1] : parts[0]) || parts[0] || '';
|
||
}
|
||
|
||
// Optional language tag on an item (lang / language / idioma)
|
||
function itemLang(it) {
|
||
const key = Object.keys(it).find(k => ['lang', 'language', 'idioma'].includes(k.toLowerCase()));
|
||
const v = key ? String(it[key] || '').toLowerCase().slice(0, 2) : '';
|
||
return (v === 'es' || v === 'en') ? v : '';
|
||
}
|
||
|
||
function render() {
|
||
const list = el('list');
|
||
if (!state.items.length) {
|
||
list.innerHTML = '<p class="empty">' + t('empty') + '</p>';
|
||
el('count').textContent = t('count')(0, 0);
|
||
return;
|
||
}
|
||
const term = (el('q').value || '').trim().toLowerCase();
|
||
const shown = state.items.filter(it => {
|
||
const lang = itemLang(it);
|
||
if (lang && lang !== state.lang) return false;
|
||
if (!term) return true;
|
||
return [pick(it, 'area'), pick(it, 'Question'), pick(it, 'Answer')]
|
||
.join(' ').toLowerCase().includes(term);
|
||
});
|
||
|
||
el('count').textContent = t('count')(shown.length, state.items.length);
|
||
list.innerHTML = '';
|
||
if (!shown.length) { list.innerHTML = '<p class="empty">' + t('empty') + '</p>'; return; }
|
||
|
||
const groups = {};
|
||
shown.forEach(it => {
|
||
const area = pick(it, 'area') || t('group');
|
||
(groups[area] = groups[area] || []).push(it);
|
||
});
|
||
|
||
Object.keys(groups).sort().forEach(area => {
|
||
const sec = document.createElement('section');
|
||
sec.className = 'area';
|
||
const h = document.createElement('h2');
|
||
h.textContent = area;
|
||
sec.appendChild(h);
|
||
|
||
groups[area].forEach(it => {
|
||
const card = document.createElement('details');
|
||
card.className = 'card';
|
||
const sum = document.createElement('summary');
|
||
sum.textContent = content(it, 'Question') || '—';
|
||
const ans = document.createElement('p');
|
||
ans.className = 'ans';
|
||
ans.textContent = content(it, 'Answer') || '';
|
||
card.append(sum, ans);
|
||
sec.appendChild(card);
|
||
});
|
||
list.appendChild(sec);
|
||
});
|
||
}
|
||
|
||
const AUTH_KEY = 'ocaFaqAuth';
|
||
function sGet(k) { try { return sessionStorage.getItem(k); } catch (e) { return null; } }
|
||
function sSet(k, v) { try { sessionStorage.setItem(k, v); } catch (e) {} }
|
||
function sDel(k) { try { sessionStorage.removeItem(k); } catch (e) {} }
|
||
|
||
function showAuth(on, bad) {
|
||
el('auth').hidden = !on;
|
||
document.querySelector('.bar').hidden = on;
|
||
el('list').hidden = on;
|
||
el('auth-err').textContent = (on && bad) ? t('badCred') : '';
|
||
el('count').textContent = '';
|
||
if (on) setTimeout(() => el('au').focus(), 60);
|
||
}
|
||
|
||
async function load() {
|
||
const cred = sGet(AUTH_KEY);
|
||
if (!cred) { showAuth(true); return; }
|
||
try {
|
||
const res = await fetch(API, {
|
||
headers: { Accept: 'application/json', Authorization: 'Basic ' + cred },
|
||
});
|
||
if (res.status === 401) { sDel(AUTH_KEY); showAuth(true, true); return; }
|
||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||
const payload = await res.json();
|
||
state.items = normalize(payload);
|
||
showAuth(false);
|
||
hideMsg();
|
||
render();
|
||
if (!state.items.length && !Array.isArray(payload)) {
|
||
flash(t('errData') + ' (' + JSON.stringify(payload) + '). ' + t('hint'));
|
||
}
|
||
} catch (err) {
|
||
state.items = [];
|
||
render();
|
||
flash(t('errRead') + ' (' + err.message + ').');
|
||
}
|
||
}
|
||
|
||
const langBox = document.querySelector('.lang');
|
||
if (langBox) {
|
||
langBox.addEventListener('click', (ev) => {
|
||
const b = ev.target.closest('button[data-lang]');
|
||
if (!b) return;
|
||
state.lang = b.dataset.lang;
|
||
store('ocaFaqLang', state.lang);
|
||
applyLang();
|
||
});
|
||
}
|
||
// Accordion: only one answer open at a time
|
||
el('list').addEventListener('toggle', (ev) => {
|
||
const d = ev.target;
|
||
if (!d || d.tagName !== 'DETAILS' || !d.open) return;
|
||
el('list').querySelectorAll('details[open]').forEach(o => { if (o !== d) o.open = false; });
|
||
}, true);
|
||
|
||
el('q').addEventListener('input', render);
|
||
|
||
el('auth').addEventListener('submit', (ev) => {
|
||
ev.preventDefault();
|
||
const u = el('au').value.trim();
|
||
const p = el('ap').value;
|
||
if (!u) { el('au').focus(); return; }
|
||
try { sSet(AUTH_KEY, btoa(u + ':' + p)); } catch (e) { sSet(AUTH_KEY, ''); }
|
||
el('ap').value = '';
|
||
load();
|
||
});
|
||
|
||
applyLang();
|
||
load();
|
||
</script>
|
||
</body>
|
||
</html> |