feat: drive blog hub from json
This commit is contained in:
+151
-66
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user