// blog-page.jsx — Resources / Insights listing for Green Integrations.
// Variant-grid layout matching Claude's design for the "All Articles" tab,
// uniform 3-col grid for filtered tabs. Data from /wp-json/claude-pages/v1/posts.
const API_BASE = '/wp-json/claude-pages/v1';
// "All" tab loads up to 19 posts in one editorial layout (matches Claude design).
// Filtered tabs paginate 9 at a time.
const ALL_PER_PAGE = 14;
const FILTERED_PER_PAGE = 9;
// Category presentation — MUST stay in sync with blog-endpoints.php and blog-post-page.jsx.
const CAT_DISPLAY = {
'power-solar-storage': { label: 'Power · Solar & Storage', chipBg: '#E8F2EC', chipFg: '#1E4D2B', accent: '#2A4A38' },
'transform-ev-electrification':{ label: 'Transform · EV & Electrification', chipBg: '#E4EBF6', chipFg: '#1A3562', accent: '#1F3556' },
'policy-compliance': { label: 'Policy & Compliance', chipBg: '#F7EDDA', chipFg: '#7A5010', accent: '#C9952E' },
'industry-focus': { label: 'Industry Focus', chipBg: '#E4EEF5', chipFg: '#1A4060', accent: '#21455F' },
'market-technology': { label: 'Market & Technology', chipBg: '#F0EBF7', chipFg: '#4A2D6B', accent: '#3A2D55' },
};
const TABS = [
{ id: 'all', label: 'All Articles' },
{ id: 'power-solar-storage', label: 'Power · Solar & Storage' },
{ id: 'transform-ev-electrification', label: 'Transform · EV & Electrification' },
{ id: 'policy-compliance', label: 'Policy & Compliance' },
{ id: 'industry-focus', label: 'Industry Focus' },
{ id: 'market-technology', label: 'Market & Technology' },
];
// ============================================================
// Primitives
// ============================================================
const MonoCaption = ({ children, color = 'rgba(255,255,255,0.42)', tracking = '0.16em', size = 10 }) =>
{children} ;
const CategoryChip = ({ cat }) => {
if (!cat || !CAT_DISPLAY[cat.slug]) return null;
const c = CAT_DISPLAY[cat.slug];
return (
{cat.label}
);
};
const Divider = ({ marginTop = 24, marginBottom = 24 }) =>
;
const RowLabel = ({ label, link = null, marginTop = 36 }) =>
;
// ============================================================
// ArticleCard — renders one card. `size` controls layout proportions per
// guide §4.3: large (190px image, 16px headline), standard (130px, 15px),
// compact (110px, no excerpt, tags only). Wide & narrow share standard.
// ============================================================
const ArticleCard = ({ post, size = 'standard' }) => {
const href = post.permalink || ('/resources/insights/' + post.slug + '/');
const cfg = (() => {
switch (size) {
case 'large': return { imgHeight: 190, padding: '22px 22px 18px', titleFont: 16, titleLh: 1.32, showExcerpt: true, showTags: true };
case 'wide': return { imgHeight: 190, padding: '22px 22px 18px', titleFont: 16, titleLh: 1.32, showExcerpt: true, showTags: true };
case 'standard': return { imgHeight: 130, padding: '18px 20px 16px', titleFont: 15, titleLh: 1.36, showExcerpt: true, showTags: true };
case 'narrow': return { imgHeight: 130, padding: '18px 20px 16px', titleFont: 15, titleLh: 1.36, showExcerpt: true, showTags: true };
case 'compact': return { imgHeight: 110, padding: '14px 18px 14px', titleFont: 14, titleLh: 1.36, showExcerpt: false, showTags: true };
default: return { imgHeight: 130, padding: '18px 20px 16px', titleFont: 15, titleLh: 1.36, showExcerpt: true, showTags: true };
}
})();
// Tags: max 2 (per guide §4.1). Hide if no tags or compact card with no space.
const tagsToShow = (post.tags || []).slice(0, 2);
const showTagsRow = cfg.showTags && tagsToShow.length > 0;
return (
{post.featured_image
?
:
}
{post.category &&
}
{post.title}
{cfg.showExcerpt && post.excerpt && (
{post.excerpt}
)}
{showTagsRow && (
{tagsToShow.map((tag) => (
{tag.label}
))}
)}
{/* Footer: date + Read → arrow, with thin border-top */}
{post.date_formatted}
Read →
);
};
// ============================================================
// FeaturedPanel — large sticky/editorial card at the top of "all" view
// Per guide §2.3:
// - Badge "Featured · [Category]" in amber, bottom-left of image panel
// - Category label above headline in amber: "Category · Tag1 · Tag2"
// - 2-sentence excerpt (server-side truncates)
// - "Read article →" CTA in accent green
// ============================================================
const FeaturedPanel = ({ post }) => {
if (!post) return null;
const href = post.permalink || ('/resources/insights/' + post.slug + '/');
const cat = post.category && CAT_DISPLAY[post.category.slug] ? post.category : null;
// Build the amber category label: "Category · Tag1 · Tag2" (up to 2 tags)
const labelParts = [];
if (post.category) labelParts.push(post.category.label);
(post.tags || []).slice(0, 2).forEach((t) => labelParts.push(t.label));
const amberLabel = labelParts.join(' · ');
return (
);
};
// ============================================================
// BlogHeader — navy header band with title + tabs (per guide §2.1, §2.2)
// ============================================================
const BlogHeader = ({ activeTab, setActiveTab, counts }) => (
Green Integrations · Insights
{/* H1 per guide §2.1: "Energy insights for C&I operators. No filler." with amber accent on "No filler." */}
Energy insights for C&I operators.{' '}
No filler.
Commercial and industrial energy strategy, policy, and market coverage — written for decision-makers.
{TABS.map((tab) => {
const active = activeTab === tab.id;
const count = counts ? (counts[tab.id] || 0) : 0;
return (
setActiveTab(tab.id)} style={{
padding: '16px 18px', border: 'none', cursor: 'pointer',
background: 'transparent',
color: active ? '#fff' : 'rgba(255,255,255,0.55)',
fontFamily: 'var(--ff-mono)', fontSize: 10.5, fontWeight: 500,
letterSpacing: '0.12em', textTransform: 'uppercase',
borderBottom: active ? '2px solid var(--gi-color-gold, #C9952E)' : '2px solid transparent',
marginBottom: -1, transition: 'color 160ms ease',
}}>
{tab.label}
{counts && count > 0 && {count} }
);
})}
);
// ============================================================
// VariantGrid — Claude's editorial layout for "all" view (per guide §2.4)
// 5 rows: 2 large · 3 standard · (1 wide + 2 narrow) · 3 standard · 3 compact
// = 14 posts per page. Allocate from items in order; skip rows we can't fill.
// ============================================================
const VariantGrid = ({ items }) => {
let i = 0;
const take = (n) => { const slice = items.slice(i, i + n); i += slice.length; return slice; };
const row1 = take(2); // 2 large
const row2 = take(3); // 3 standard
const row3 = take(3); // 1 wide + 2 narrow
const row4 = take(3); // 3 standard
const row5 = take(3); // 3 compact
return (
{row1.length > 0 && (<>
>)}
{row2.length > 0 && (
)}
{row3.length > 0 && (<>
>)}
{row4.length > 0 && (
)}
{row5.length > 0 && (<>
>)}
);
};
// ============================================================
// UniformGrid — 3-column grid for filtered tabs + paginated pages
// ============================================================
const UniformGrid = ({ items, label }) => (
{label &&
}
{items.length > 0 ? (
) : (
No articles in this category yet.
)}
);
// ============================================================
// Pagination — numbered
// ============================================================
const paginationBtn = (disabled, active) => ({
padding: '10px 14px', minWidth: 40, cursor: disabled ? 'default' : 'pointer',
background: active ? 'var(--gi-deep-ink, #0D1C35)' : '#fff',
color: active ? '#fff' : 'var(--gi-deep-ink, #0D1C35)',
border: '0.5px solid var(--gi-platinum, #E5E2DA)', borderRadius: 6,
fontFamily: 'var(--ff-mono)', fontSize: 11, fontWeight: 500,
letterSpacing: '0.10em', textTransform: 'uppercase',
opacity: disabled ? 0.4 : 1,
});
const Pagination = ({ page, totalPages, onChange }) => {
if (totalPages <= 1) return null;
const pages = [];
for (let i = 1; i <= totalPages; i++) pages.push(i);
return (
onChange(Math.max(1, page - 1))} disabled={page === 1}
style={paginationBtn(page === 1, false)}>← Previous
{pages.map((p) => (
onChange(p)} style={paginationBtn(false, p === page)}>{p}
))}
onChange(Math.min(totalPages, page + 1))} disabled={page === totalPages}
style={paginationBtn(page === totalPages, false)}>Next →
);
};
// ============================================================
// EditorialStandards — quiet "who writes this" strip
// ============================================================
const EditorialStandards = () => (
Editorial standards
Written from real industry experience.
{[
['Ontario-Focused', 'Built around Ontario utility rates, IESO programs, and real commercial energy economics.'],
['Executive-Level', 'Written for facility managers, operations teams, CFOs, and commercial property decision-makers.'],
['Data-Backed', 'Insights supported by published rate schedules, utility filings, and real project analysis.'],
].map(([title, body]) => (
))}
);
// ============================================================
// SubscribeBand — visual only (form does not yet POST anywhere)
// ============================================================
const SubscribeBand = () => (
Subscribe
New analysis, once a month.
Insights on commercial energy in Canada.
);
// ============================================================
// Main BlogPage
// ============================================================
const BlogPage = () => {
const [activeTab, setActiveTab] = React.useState('all');
const [page, setPage] = React.useState(1);
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
const isFiltered = activeTab !== 'all';
const perPage = isFiltered ? FILTERED_PER_PAGE : ALL_PER_PAGE;
React.useEffect(() => {
setLoading(true);
const cat = isFiltered ? activeTab : '';
const url = `${API_BASE}/posts?page=${page}&per_page=${perPage}&category=${encodeURIComponent(cat)}`;
fetch(url)
.then((r) => r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status)))
.then((json) => { setData(json); setError(null); })
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
}, [activeTab, page, perPage, isFiltered]);
const handleTab = (id) => { setActiveTab(id); setPage(1); };
return (
{error && (
Could not load articles. Please try refreshing the page.
({error})
)}
{!error && loading && (
Loading…
)}
{!error && !loading && data && (
<>
{/* Featured panel — only on page 1 of "all" tab */}
{!isFiltered && page === 1 && data.featured && (
)}
{/* Grid — variant on page 1 of "all", uniform otherwise */}
{!isFiltered && page === 1
?
:
t.id === activeTab).label}`
: `Page ${data.pagination.page} of ${data.pagination.total_pages}`} />
}
{ setPage(p); window.scrollTo({ top: 0, behavior: 'smooth' }); }} />
>
)}
{/* */}
);
};