feat: drive blog hub from json

This commit is contained in:
NGOs WebDev
2026-03-22 17:35:40 -03:00
parent 00e042a5ae
commit 5883044e7c
2 changed files with 195 additions and 66 deletions
+151 -66
View File
@@ -144,28 +144,10 @@
<h2 class="text-3xl font-semibold" data-i18n="blog-title"></h2>
<p class="text-brand-ink/80" data-i18n="blog-subtitle"></p>
</div>
<a href="#" class="text-brand-accent font-semibold" data-i18n="blog-link"></a>
</div>
<div class="grid gap-6 md:grid-cols-3">
<article class="border border-brand-smoke rounded-3xl p-6 h-full flex flex-col">
<p class="text-xs uppercase tracking-widest text-brand-primary mb-3" data-i18n="blog1-tag"></p>
<h3 class="text-xl font-semibold mb-3" data-i18n="blog1-title"></h3>
<p class="text-sm text-brand-ink/80 mb-6" data-i18n="blog1-desc"></p>
<a href="#" class="mt-auto text-sm font-semibold text-brand-accent" data-i18n="blog1-link" data-i18n-aria="blog1-aria"></a>
</article>
<article class="border border-brand-smoke rounded-3xl p-6 h-full flex flex-col">
<p class="text-xs uppercase tracking-widest text-brand-primary mb-3" data-i18n="blog2-tag"></p>
<h3 class="text-xl font-semibold mb-3" data-i18n="blog2-title"></h3>
<p class="text-sm text-brand-ink/80 mb-6" data-i18n="blog2-desc"></p>
<a href="#" class="mt-auto text-sm font-semibold text-brand-accent" data-i18n="blog2-link" data-i18n-aria="blog2-aria"></a>
</article>
<article class="border border-brand-smoke rounded-3xl p-6 h-full flex flex-col">
<p class="text-xs uppercase tracking-widest text-brand-primary mb-3" data-i18n="blog3-tag"></p>
<h3 class="text-xl font-semibold mb-3" data-i18n="blog3-title"></h3>
<p class="text-sm text-brand-ink/80 mb-6" data-i18n="blog3-desc"></p>
<a href="#" class="mt-auto text-sm font-semibold text-brand-accent" data-i18n="blog3-link" data-i18n-aria="blog3-aria"></a>
</article>
<button id="blog-toggle" type="button" class="text-brand-accent font-semibold hover:underline underline-offset-4">Browse all articles →</button>
</div>
<div id="blog-grid" class="grid gap-6 md:grid-cols-3"></div>
<p id="blog-error" class="text-sm text-brand-ink/70 mt-6 hidden"></p>
</div>
</section>
@@ -264,21 +246,10 @@
'blog-title': 'Blog & resource hub',
'blog-subtitle': 'Stay ahead with tutorials, case studies, and playbooks for NGO ERP rollouts.',
'blog-link': 'Browse all articles →',
'blog1-tag': 'Grant compliance',
'blog1-title': 'Grant-ready NGO ERP checklist',
'blog1-desc': 'Map every grant condition to automated workflows and never miss a reporting milestone again.',
'blog1-link': 'Read the guide',
'blog1-aria': 'Read the grant-ready NGO ERP checklist',
'blog2-tag': 'Field operations',
'blog2-title': 'Digitize volunteer onboarding',
'blog2-desc': 'Standardize approvals, ID checks, and training with low-code workflows tied to your ERP.',
'blog2-link': 'View playbook',
'blog2-aria': 'Read the volunteer onboarding playbook',
'blog3-tag': 'Impact storytelling',
'blog3-title': 'From KPI to donor story',
'blog3-desc': 'Turn ERP data into visual narratives that inspire donors and board members.',
'blog3-link': 'See examples',
'blog3-aria': 'See donor storytelling examples',
'blog-all-open': 'Showing all articles',
'blog-read-more': 'Read more',
'blog-show-less': 'Show less',
'blog-error': 'Unable to load articles right now.',
'trial-tagline': 'Sign up free trial',
'trial-heading': 'Launch your NGO ERP sandbox in under 5 minutes',
'trial-body': 'Experience localized NGO templates, donor CRM, and automation recipes. No credit card required—just choose your focus area and invite teammates.',
@@ -335,21 +306,10 @@
'blog-title': 'Blog y centro de recursos',
'blog-subtitle': 'Aprende con tutoriales, casos prácticos y playbooks para implementar un ERP en tu ONG.',
'blog-link': 'Ver todos los artículos →',
'blog1-tag': 'Cumplimiento',
'blog1-title': 'Checklist ERP listo para subvenciones',
'blog1-desc': 'Relaciona cada requisito de la subvención con flujos automatizados y evita retrasos en los reportes.',
'blog1-link': 'Leer la guía',
'blog1-aria': 'Leer la guía ERP para subvenciones',
'blog2-tag': 'Operaciones de campo',
'blog2-title': 'Digitaliza la incorporación de voluntarios',
'blog2-desc': 'Estandariza aprobaciones, verificaciones e inducciones con flujos low-code conectados a tu ERP.',
'blog2-link': 'Ver playbook',
'blog2-aria': 'Ver el playbook de voluntariado',
'blog3-tag': 'Narrativas de impacto',
'blog3-title': 'Del KPI a la historia del donante',
'blog3-desc': 'Convierte datos del ERP en historias visuales que inspiran a donantes y juntas directivas.',
'blog3-link': 'Ver ejemplos',
'blog3-aria': 'Ver ejemplos de historias para donantes',
'blog-all-open': 'Mostrando todos los artículos',
'blog-read-more': 'Leer más',
'blog-show-less': 'Mostrar menos',
'blog-error': 'No pudimos cargar los artículos en este momento.',
'trial-tagline': 'Activa tu prueba',
'trial-heading': 'Lanza tu sandbox ERP para ONG en menos de 5 minutos',
'trial-body': 'Explora plantillas localizadas, CRM de donantes y recetas de automatización. Sin tarjeta; solo elige tu prioridad e invita al equipo.',
@@ -406,21 +366,10 @@
'blog-title': 'Blog & Wissenshub',
'blog-subtitle': 'Tutorials, Fallstudien und Playbooks für erfolgreiche NGO-ERP-Rollouts.',
'blog-link': 'Alle Artikel ansehen →',
'blog1-tag': 'Förder-Compliance',
'blog1-title': 'ERP-Checkliste für Fördermittel',
'blog1-desc': 'Verknüpfe jede Auflage mit automatisierten Workflows und vermeide verspätete Berichte.',
'blog1-link': 'Zur Anleitung',
'blog1-aria': 'ERP-Checkliste für Fördermittel lesen',
'blog2-tag': 'Feldeinsätze',
'blog2-title': 'Ehrenamtliche digital onboarden',
'blog2-desc': 'Standardisiere Freigaben, ID-Prüfungen und Schulungen mit Low-Code-Flows im ERP.',
'blog2-link': 'Playbook ansehen',
'blog2-aria': 'Playbook zur Ehrenamtlichen-Onboarding ansehen',
'blog3-tag': 'Impact Storytelling',
'blog3-title': 'Vom KPI zur Spenderstory',
'blog3-desc': 'Mach aus ERP-Daten visuelle Geschichten, die Spender und Boards überzeugen.',
'blog3-link': 'Beispiele ansehen',
'blog3-aria': 'Beispiele für Spenderstorys ansehen',
'blog-all-open': 'Alle Artikel werden angezeigt',
'blog-read-more': 'Mehr lesen',
'blog-show-less': 'Weniger anzeigen',
'blog-error': 'Artikel konnten gerade nicht geladen werden.',
'trial-tagline': 'Kostenlos testen',
'trial-heading': 'Starte dein NGO-ERP-Sandbox in unter 5 Minuten',
'trial-body': 'Teste lokalisierte Templates, Donor-CRM und Automatisierungen. Keine Kreditkarte wähle den Fokus und lade dein Team ein.',
@@ -455,6 +404,140 @@
}
};
const BLOG_LIMIT = 3;
const blogGrid = document.getElementById('blog-grid');
const blogToggle = document.getElementById('blog-toggle');
const blogError = document.getElementById('blog-error');
let blogPosts = [];
let blogExpanded = false;
if (blogGrid) {
fetch('blog.json')
.then(response => response.json())
.then(data => {
blogPosts = Array.isArray(data) ? data : [];
renderBlogCards();
})
.catch(error => {
if (blogError) {
blogError.classList.remove('hidden');
blogError.textContent = getTranslation(document.documentElement.lang, 'blog-error', 'Unable to load articles right now.');
}
console.error('blog.json load failed', error);
});
}
if (blogToggle) {
blogToggle.addEventListener('click', () => {
if (blogExpanded || blogPosts.length <= BLOG_LIMIT) return;
blogExpanded = true;
renderBlogCards();
});
}
function getTranslation(locale, key, fallback) {
return (translations[locale] && translations[locale][key]) ?? translations.en[key] ?? fallback;
}
function renderBlogCards() {
if (!blogGrid) {
return;
}
if (!blogPosts.length) {
blogGrid.innerHTML = '';
updateBlogToggleLabel();
return;
}
const limit = blogExpanded ? blogPosts.length : Math.min(BLOG_LIMIT, blogPosts.length);
blogGrid.innerHTML = '';
blogPosts.slice(0, limit).forEach(post => {
blogGrid.appendChild(createBlogCard(post));
});
updateBlogToggleLabel();
}
function createBlogCard(post) {
const card = document.createElement('article');
card.className = 'border border-brand-smoke rounded-3xl p-6 h-full flex flex-col';
const meta = document.createElement('div');
meta.className = 'flex items-center justify-between mb-3';
const area = document.createElement('p');
area.className = 'text-xs uppercase tracking-widest text-brand-primary';
area.textContent = post.area || '';
const dateEl = document.createElement('time');
dateEl.className = 'text-xs text-brand-ink/60';
if (post.date) {
dateEl.dateTime = post.date;
}
dateEl.textContent = formatBlogDate(post.date);
meta.append(area, dateEl);
card.appendChild(meta);
const title = document.createElement('h3');
title.className = 'text-xl font-semibold mb-3';
title.textContent = post.title || '';
card.appendChild(title);
const teaser = document.createElement('div');
teaser.className = 'text-sm text-brand-ink/80 space-y-3 mb-4';
teaser.innerHTML = post.teaser || '';
card.appendChild(teaser);
let contentBlock = null;
const hasMore = post.content && post.content.trim() !== (post.teaser || '').trim();
if (post.content) {
contentBlock = document.createElement('div');
contentBlock.className = 'text-sm text-brand-ink/80 space-y-3 mb-4 hidden';
contentBlock.innerHTML = post.content;
card.appendChild(contentBlock);
}
if (hasMore && contentBlock) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'text-sm font-semibold text-brand-accent underline-offset-4 hover:underline self-start';
btn.textContent = getTranslation(document.documentElement.lang, 'blog-read-more', 'Read more');
btn.addEventListener('click', () => {
const isHidden = contentBlock.classList.contains('hidden');
if (isHidden) {
contentBlock.classList.remove('hidden');
btn.textContent = getTranslation(document.documentElement.lang, 'blog-show-less', 'Show less');
} else {
contentBlock.classList.add('hidden');
btn.textContent = getTranslation(document.documentElement.lang, 'blog-read-more', 'Read more');
}
});
card.appendChild(btn);
}
return card;
}
function formatBlogDate(dateString) {
if (!dateString) return '';
try {
const locale = document.documentElement.lang || 'en';
return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'short', day: 'numeric' }).format(new Date(dateString));
} catch (error) {
return dateString;
}
}
function updateBlogToggleLabel() {
if (!blogToggle) return;
if (blogPosts.length <= BLOG_LIMIT) {
blogToggle.classList.add('hidden');
return;
}
blogToggle.classList.remove('hidden');
blogToggle.disabled = blogExpanded;
const locale = document.documentElement.lang || 'en';
blogToggle.textContent = blogExpanded
? getTranslation(locale, 'blog-all-open', 'Showing all articles')
: getTranslation(locale, 'blog-link', 'Browse all articles →');
}
function setLanguage(lang) {
const locale = translations[lang] ? lang : 'en';
document.documentElement.lang = locale;
@@ -478,6 +561,8 @@
const value = translations[locale][key] ?? translations.en[key] ?? '';
if (value !== undefined) el.setAttribute('aria-label', value);
});
renderBlogCards();
updateBlogToggleLabel();
}
document.getElementById('lang-selector').addEventListener('change', (event) => {