Files
oc-guy/js/feed.js
T
The OC Guy 77d3f00072 Warm styleguide homepage, JSON post feed from n8n, daily sync
- css/site.css: oat/espresso palette, Fraunces display, rounded surfaces, warm photo grade
- index.html: Module of the Month replaced by the blog; tips, blog and community sections render from data/posts.json
- js/feed.js: client-side feed + search over one manifest (blog/index.json technique)
- scripts/sync_posts.py: daily webhook fetch, image download, dedupe by id and content hash, non-image attachments skipped
- data/posts.json: manifest, one published post (Odoo 20 Community review)
2026-10-23 21:41:25 +00:00

152 lines
5.3 KiB
JavaScript

/* The OC Guy — post feed.
One manifest (data/posts.json) written by scripts/sync_posts.py, three views:
Tips & Tricks, Community news and the Blog. Search is client-side over
headline + teaser + category, the same technique as blog/index.json on
odoo4projects.com. No JS, no feed: the sections say so instead of lying. */
(function () {
'use strict';
var SOURCE = 'data/posts.json';
var BADGE = { tips: 'Tips & Tricks', news: 'Community news', blog: 'Blog' };
function el(tag, cls, text) {
var n = document.createElement(tag);
if (cls) n.className = cls;
if (text != null) n.textContent = text;
return n;
}
function fmtDate(iso) {
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
var d = new Date(iso + 'T00:00:00Z');
if (isNaN(d.getTime())) return iso || '';
return d.getUTCDate() + ' ' + months[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
}
function matches(p, term) {
if (!term) return true;
var hay = (p.headline + ' ' + p.teaser + ' ' + p.category + ' ' + (p.tags || []).join(' ')).toLowerCase();
return hay.indexOf(term) !== -1;
}
function emptyState(node, kind) {
node.textContent = '';
var box = el('p', 'feed-empty');
box.textContent = kind === 'news'
? 'No community news published yet. Posts from the daily feed land here.'
: 'Nothing published in this section yet.';
node.appendChild(box);
}
function card(p) {
var a = el('a', 'card');
a.href = p.link;
if (p.image) {
var img = el('img', 'card-img');
img.src = p.image;
img.alt = '';
img.loading = 'lazy';
img.width = 1000;
img.height = 666;
a.appendChild(img);
}
a.appendChild(el('span', 'card-cat', BADGE[p.category] || p.category));
a.appendChild(el('h3', null, p.headline));
a.appendChild(el('p', 'card-desc', p.teaser));
a.appendChild(el('span', 'card-meta', fmtDate(p.date)));
return a;
}
function newsRow(p) {
var li = el('li');
var when = el('span', 'news-date');
var t = el('time');
t.dateTime = p.date;
t.textContent = fmtDate(p.date);
when.appendChild(t);
var body = el('div', 'news-body');
var h3 = el('h3');
var a = el('a', null, p.headline);
a.href = p.link;
h3.appendChild(a);
body.appendChild(h3);
body.appendChild(el('p', null, p.teaser));
li.appendChild(when);
li.appendChild(body);
li.appendChild(el('span', 'news-source', p.category === 'tips' ? 'Tips & Tricks' : 'Community'));
return li;
}
function render(view, posts, term) {
var list = posts.filter(function (p) {
return (view.all || p.category === view.category) && matches(p, term);
});
var node = view.node;
if (view.countBox) {
view.countBox.textContent = list.length + (list.length === 1 ? ' post' : ' posts');
}
if (!list.length) {
emptyState(node, view.kind);
return;
}
node.textContent = '';
if (view.kind === 'news') {
list.slice(0, view.limit || 5).forEach(function (p) { node.appendChild(newsRow(p)); });
} else {
list.slice(0, view.limit || 50).forEach(function (p) { node.appendChild(card(p)); });
}
if (view.count) view.count.textContent = String(list.length);
}
function pickCategory(p) {
var c = (p.category || 'blog').toLowerCase();
return BADGE[c] ? c : 'blog';
}
fetch(SOURCE + '?_=' + Date.now())
.then(function (r) { if (!r.ok) throw new Error('http ' + r.status); return r.json(); })
.then(function (data) {
var posts = (data && data.posts ? data.posts : []).map(function (p) {
return {
id: p.id, date: p.date, headline: p.headline, teaser: p.teaser || '',
image: p.image || null, link: p.link || '#',
category: pickCategory(p), tags: p.tags || []
};
});
var views = [
{ node: document.getElementById('tipsGrid'), category: 'tips', kind: 'cards',
search: document.getElementById('tipsSearch'), count: document.getElementById('tipsCount'), limit: 3 },
{ node: document.getElementById('newsList'), category: 'news', kind: 'news',
search: document.getElementById('newsSearch'), count: document.getElementById('newsCount'), limit: 6 },
{ node: document.getElementById('blogGrid'), category: 'blog', kind: 'cards', all: true,
search: document.getElementById('blogSearch'), count: document.getElementById('blogCount'), limit: 60 }
].filter(function (v) { return v.node; });
views.forEach(function (v) {
v.term = '';
if (v.count) v.countBox = v.count.parentElement;
render(v, posts, '');
if (v.search) {
v.search.addEventListener('input', function () {
v.term = this.value.trim().toLowerCase();
render(v, posts, v.term);
});
}
});
/* posts tagged both tips and blog are listed twice on purpose:
the sections are channels, not storage. */
var total = document.getElementById('feedTotal');
if (total) total.textContent = String(posts.length);
})
.catch(function () {
['tipsGrid', 'newsList', 'blogGrid'].forEach(function (id) {
var n = document.getElementById(id);
if (n) emptyState(n, id === 'newsList' ? 'news' : 'cards');
});
});
})();