This commit is contained in:
Felix Klement committed 2026-10-04 19:12:42 +02:00
1 parent 00edb9dfc2
commit 3865c45642
18 files changed
+439 -87

No files matched your search

+94 -15
View File
@@ -1,5 +1,5 @@
---
import { getCollection } from 'astro:content';
import { feedType, feedTypes, getFeed } from '../data/insights';
import { Picture } from 'astro:assets';
import Base from '../layouts/Base.astro';
import { formatDate, localePath, t, type Lang } from '../i18n/ui';
@@ -7,7 +7,16 @@ import { formatDate, localePath, t, type Lang } from '../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const ui = t(lang);
const posts = (await getCollection('insights')).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
const feed = await getFeed();
const m = ui.insights.mention;
const f = ui.insights.filter;
/* Only filters that have entries; "All" first. */
const filters = [
{ key: 'all', label: f.all, count: feed.length },
...feedTypes
.map((key) => ({ key, label: f[key], count: feed.filter((item) => feedType(item) === key).length }))
.filter((x) => x.count > 0),
];
---
<Base lang={lang} title={ui.meta.insights.title} description={ui.meta.insights.description}>
<section class="section section--intro">
@@ -19,16 +28,29 @@ const posts = (await getCollection('insights')).sort((a, b) => b.data.date.value
<section class="section--list" aria-label={ui.insights.title}>
<div class="container">
<ul class="posts" role="list">
{posts.map((post) => (
<li class="post">
<a class="post__link" href={localePath(lang, `/insights/${post.id}`)} lang={post.data.lang !== lang ? post.data.lang : undefined}>
{filters.length > 2 && (
<fieldset class="choice-group filters" data-filters>
<legend class="choice-group__legend">{f.label}</legend>
<div class="choices">
{filters.map((x) => (
<label class="choice">
<input type="radio" name="insights-type" value={x.key} checked={x.key === 'all'} />
<span>{x.label} <span class="choice__n">{x.count}</span></span>
</label>
))}
</div>
</fieldset>
)}
<ul class="posts" role="list" data-feed>
{feed.map((item) => item.kind === 'post' ? (
<li class="post" data-type={feedType(item)}>
<a class="post__link" href={localePath(lang, `/insights/${item.entry.id}`)} lang={item.entry.data.lang !== lang ? item.entry.data.lang : undefined}>
<div class="post__media">
<Picture
src={post.data.cover}
alt={post.data.coverAlt}
src={item.entry.data.cover}
alt={item.entry.data.coverAlt}
formats={['avif']}
fallbackFormat="webp"
fallbackFormat="webp"
widths={[400, 640, 960]}
sizes="(min-width: 48rem) 40vw, 100vw"
loading="lazy"
@@ -38,13 +60,32 @@ const posts = (await getCollection('insights')).sort((a, b) => b.data.date.value
</div>
<div class="post__text">
<p class="small muted post__meta">
<span>{post.data.category}</span>
<time datetime={post.data.date.toISOString().slice(0, 10)}>{formatDate(post.data.date, lang)}</time>
<span>{ui.insights.readingTime(post.data.readingTime)}</span>
<span>{item.entry.data.category}</span>
<time datetime={item.date.toISOString().slice(0, 10)}>{formatDate(item.date, lang)}</time>
<span>{ui.insights.readingTime(item.entry.data.readingTime)}</span>
</p>
<h2 class="h2">{post.data.title}</h2>
{post.data.subtitle && <p class="muted">{post.data.subtitle}</p>}
{post.data.lang !== lang && <p class="small muted">{ui.insights.inEnglish}</p>}
<h2 class="h2">{item.entry.data.title}</h2>
{item.entry.data.subtitle && <p class="muted">{item.entry.data.subtitle}</p>}
{item.entry.data.lang !== lang && <p class="small muted">{ui.insights.inEnglish}</p>}
</div>
</a>
</li>
) : (
<li class="post" data-type={feedType(item)}>
<a class="post__link" href={localePath(lang, `/insights/${item.entry.id}`)} lang={item.entry.data.lang !== lang ? item.entry.data.lang : undefined}>
<div class="post__media mention__panel" aria-hidden="true">
<span class="mention__source">{item.entry.data.source}</span>
<span class="mention__platform">{item.entry.data.platform}</span>
</div>
<div class="post__text">
<p class="small muted post__meta">
<span>{m.category(item.entry.data.platform)}</span>
<time datetime={item.date.toISOString().slice(0, 10)}>{formatDate(item.date, lang)}</time>
<span>{m.source(item.entry.data.source)}</span>
</p>
<h2 class="h2">{item.entry.data.title}</h2>
<p class="muted mention__summary">{item.entry.data.summary}</p>
{item.entry.data.lang !== lang && <p class="small muted">{item.entry.data.lang === 'en' ? m.inEnglish : m.inGerman}</p>}
</div>
</a>
</li>
@@ -54,6 +95,29 @@ const posts = (await getCollection('insights')).sort((a, b) => b.data.date.value
</section>
</Base>
<script>
/* Filter the list by type. The choice lives in ?type= so a filtered view can be linked;
without script every entry shows. */
const bar = document.querySelector<HTMLElement>('[data-filters]');
const items = document.querySelectorAll<HTMLElement>('[data-feed] > li');
if (bar) {
const inputs = bar.querySelectorAll<HTMLInputElement>('input[name="insights-type"]');
const keys = [...inputs].map((i) => i.value);
const apply = (key: string, push: boolean) => {
if (!keys.includes(key)) key = 'all';
inputs.forEach((i) => { i.checked = i.value === key; });
items.forEach((li) => { li.hidden = key !== 'all' && li.dataset.type !== key; });
if (push) {
const url = new URL(location.href);
if (key === 'all') url.searchParams.delete('type'); else url.searchParams.set('type', key);
history.replaceState(null, '', url);
}
};
bar.addEventListener('change', (e) => apply((e.target as HTMLInputElement).value, true));
apply(new URL(location.href).searchParams.get('type') ?? 'all', false);
}
</script>
<style>
.index__title { grid-column: 1 / -1; }
.index__lead { grid-column: 1 / -1; max-width: var(--measure); color: var(--ink-muted); }
@@ -62,6 +126,9 @@ const posts = (await getCollection('insights')).sort((a, b) => b.data.date.value
.index__lead { grid-column: 1 / span 5; }
}
.section--list { padding-bottom: var(--section); }
/* Filter row: the shared segmented choice (base.css), as on the research list. */
.filters { padding-bottom: var(--space-6); }
.post[hidden] { display: none; }
.posts { border-top: var(--hairline) solid var(--line); }
.post { border-bottom: var(--hairline) solid var(--line); }
.post__link {
@@ -74,6 +141,18 @@ const posts = (await getCollection('insights')).sort((a, b) => b.data.date.value
.post__img, :global(.post__img) { width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; }
.post__text { display: grid; gap: var(--space-4); align-content: start; }
.post__meta { display: flex; flex-wrap: wrap; gap: var(--space-4); }
/* A mention has no cover: a typographic panel names who posted it and where. */
.mention__panel {
display: flex;
flex-direction: column;
justify-content: space-between;
aspect-ratio: 3 / 2;
padding: var(--space-5);
background: var(--paper-deep);
}
.mention__source { font-size: var(--step-title); line-height: var(--leading-title); letter-spacing: var(--track-h2); color: var(--ink-muted); }
.mention__platform { font-size: var(--step-small); line-height: var(--leading-small); color: var(--ink-muted); }
.mention__summary { max-width: var(--measure); }
@media (min-width: 48rem) {
.post__link { grid-template-columns: 5fr 7fr; }
}