Files
blog-eleventy-indiekit/changelog.njk
Ricardo c6165bd7af feat: add view mode toggle for changelog page
Add a "Group by: Repository | Change Type" segmented toggle to the
changelog page. Users can now switch between repo-based tabs (Core,
Endpoints, Syndicators, etc.) and commit-type tabs (Features, Fixes,
Refactor, etc.) with reactive Alpine.js computed getters.

Both views share the same commit data — only the grouping dimension
and color scheme change. Tab counts and badge labels update
reactively when switching view modes.

Confab-Link: http://localhost:8080/sessions/5767023f-100b-4b9c-85fc-12d7e1ab248a
2026-03-16 18:43:13 +01:00

292 lines
12 KiB
Plaintext

---
layout: layouts/base.njk
title: Changelog
permalink: /changelog/
eleventyExcludeFromCollections: true
pagefindIgnore: true
withSidebar: false
---
<div class="page-header mb-6 sm:mb-8">
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100 mb-2">Changelog</h1>
<p class="text-surface-600 dark:text-surface-400">Development activity across all Indiekit repositories.</p>
</div>
<div x-data="changelogApp()" x-init="init()">
{# View mode toggle #}
<div class="flex items-center gap-2 mb-4">
<span class="text-sm text-surface-600 dark:text-surface-400">Group by:</span>
<div class="inline-flex rounded-lg border border-surface-200 dark:border-surface-700 overflow-hidden">
<button
@click="setViewMode('repo')"
:class="viewMode === 'repo' ? 'bg-accent-100 dark:bg-accent-900 text-accent-700 dark:text-accent-300' : 'text-surface-600 dark:text-surface-400 hover:bg-surface-50 dark:hover:bg-surface-800'"
class="px-3 py-1.5 text-xs font-medium transition-colors"
>Repository</button>
<button
@click="setViewMode('type')"
:class="viewMode === 'type' ? 'bg-accent-100 dark:bg-accent-900 text-accent-700 dark:text-accent-300' : 'text-surface-600 dark:text-surface-400 hover:bg-surface-50 dark:hover:bg-surface-800'"
class="px-3 py-1.5 text-xs font-medium transition-colors border-l border-surface-200 dark:border-surface-700"
>Change Type</button>
</div>
</div>
{# Tab navigation #}
<div class="flex gap-1 mb-6 border-b border-surface-200 dark:border-surface-700 overflow-x-auto" role="tablist" aria-label="Changelog categories">
<template x-for="tab in activeTabs" :key="tab.key">
<button
@click="activeTab = tab.key"
:class="activeTab === tab.key ? 'border-b-2 border-accent-500 text-accent-600 dark:text-accent-400' : 'text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === tab.key).toString()"
role="tab"
class="flex items-center gap-1.5 px-3 py-2 text-sm font-medium transition-colors -mb-px whitespace-nowrap flex-shrink-0"
>
<span x-text="tab.label"></span>
<span
x-show="getCount(tab.key) > 0"
x-text="getCount(tab.key)"
class="text-xs px-1.5 py-0.5 rounded-full bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-400"
></span>
</button>
</template>
</div>
{# Loading state #}
<div x-show="loading" class="flex items-center justify-center py-12">
<svg class="animate-spin h-6 w-6 text-accent-500" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<span class="ml-3 text-surface-600 dark:text-surface-400">Loading changelog...</span>
</div>
{# Commit list #}
<div x-show="!loading" x-cloak>
<template x-if="filteredCommits().length === 0">
<p class="text-surface-600 dark:text-surface-400 py-8 text-center">No recent activity in this category.</p>
</template>
<ul class="space-y-4">
<template x-for="commit in filteredCommits()" :key="commit.fullSha">
<li class="bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 rounded-lg p-4 shadow-sm">
<div class="flex items-start gap-3">
<a :href="commit.url" target="_blank" rel="noopener"
class="font-mono text-xs bg-surface-100 dark:bg-surface-800 px-1.5 py-0.5 rounded text-accent-600 dark:text-accent-400 hover:underline flex-shrink-0 mt-0.5"
x-text="commit.sha"></a>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-surface-900 dark:text-surface-100 break-words" x-text="commit.title"></p>
<div class="flex flex-wrap items-center gap-2 mt-2">
<span
class="text-xs px-2 py-0.5 rounded-full font-medium"
:class="activeCategoryColors[activeCommitCategory(commit)]"
x-text="activeCategoryLabels[activeCommitCategory(commit)] || activeCommitCategory(commit)"
></span>
<a :href="commit.repoUrl" target="_blank" rel="noopener"
class="text-xs px-2 py-0.5 rounded-full bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-400 hover:text-accent-600 dark:hover:text-accent-400"
x-text="commit.repoName"></a>
<span class="text-xs text-surface-600 dark:text-surface-400 font-mono" x-text="formatDate(commit.date)"></span>
<span class="text-xs text-surface-600 dark:text-surface-400" x-text="'by ' + commit.author"></span>
</div>
<template x-if="commit.body">
<details class="mt-2">
<summary class="text-xs text-surface-600 dark:text-surface-400 cursor-pointer hover:text-surface-700 dark:hover:text-surface-300">Show details</summary>
<pre class="mt-1 text-xs text-surface-600 dark:text-surface-400 whitespace-pre-wrap break-words bg-surface-50 dark:bg-surface-800 rounded p-2" x-text="commit.body"></pre>
</details>
</template>
</div>
</div>
</li>
</template>
</ul>
{# Load more button #}
<div x-show="canLoadMore" class="mt-8 text-center">
<button
@click="loadMore()"
:disabled="loadingMore"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg border border-surface-300 dark:border-surface-600 text-surface-700 dark:text-surface-300 hover:bg-surface-50 dark:hover:bg-surface-800 transition-colors disabled:opacity-50"
>
<svg x-show="loadingMore" class="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<span x-text="loadingMore ? 'Loading...' : 'Load older commits'"></span>
</button>
</div>
{# Summary #}
<div x-show="commits.length > 0" class="mt-6 text-center text-xs text-surface-600 dark:text-surface-400">
<span x-text="commits.length + ' commits'"></span>
<span x-show="currentDays !== 'all'"> from the last <span x-text="currentDays"></span> days</span>
<span x-show="currentDays === 'all'"> (all time)</span>
</div>
</div>
</div>
<script>
function changelogApp() {
return {
activeTab: 'all',
viewMode: 'repo',
loading: true,
loadingMore: false,
commits: [],
categories: {},
commitCategories: {},
currentDays: 30,
daysProgression: [30, 90, 180, 'all'],
repoTabs: [
{ key: 'all', label: 'All' },
{ key: 'core', label: 'Core' },
{ key: 'deployment', label: 'Deployment' },
{ key: 'theme', label: 'Theme' },
{ key: 'endpoints', label: 'Endpoints' },
{ key: 'syndicators', label: 'Syndicators' },
{ key: 'post-types', label: 'Post Types' },
{ key: 'presets', label: 'Presets' },
],
typeTabs: [
{ key: 'all', label: 'All' },
{ key: 'features', label: 'Features' },
{ key: 'fixes', label: 'Fixes' },
{ key: 'refactor', label: 'Refactor' },
{ key: 'performance', label: 'Performance' },
{ key: 'accessibility', label: 'Accessibility' },
{ key: 'documentation', label: 'Docs' },
{ key: 'chores', label: 'Chores' },
{ key: 'tests', label: 'Tests' },
],
repoCategoryLabels: {
core: 'Core',
deployment: 'Deployment',
theme: 'Theme',
endpoints: 'Endpoint',
syndicators: 'Syndicator',
'post-types': 'Post Type',
presets: 'Preset',
other: 'Other',
},
typeCategoryLabels: {
features: 'Feature',
fixes: 'Fix',
performance: 'Perf',
accessibility: 'A11y',
documentation: 'Docs',
refactor: 'Refactor',
chores: 'Chore',
style: 'Style',
tests: 'Test',
other: 'Other',
},
repoCategoryColors: {
core: 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300',
deployment: 'bg-orange-100 dark:bg-orange-900 text-orange-700 dark:text-orange-300',
theme: 'bg-purple-100 dark:bg-purple-900 text-purple-700 dark:text-purple-300',
endpoints: 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300',
syndicators: 'bg-teal-100 dark:bg-teal-900 text-teal-700 dark:text-teal-300',
'post-types': 'bg-pink-100 dark:bg-pink-900 text-pink-700 dark:text-pink-300',
presets: 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300',
other: 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300',
},
typeCategoryColors: {
features: 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300',
fixes: 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300',
performance: 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300',
accessibility: 'bg-purple-100 dark:bg-purple-900 text-purple-700 dark:text-purple-300',
documentation: 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300',
refactor: 'bg-orange-100 dark:bg-orange-900 text-orange-700 dark:text-orange-300',
chores: 'bg-surface-200 dark:bg-surface-700 text-surface-700 dark:text-surface-300',
style: 'bg-pink-100 dark:bg-pink-900 text-pink-700 dark:text-pink-300',
tests: 'bg-teal-100 dark:bg-teal-900 text-teal-700 dark:text-teal-300',
other: 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300',
},
get activeTabs() {
return this.viewMode === 'repo' ? this.repoTabs : this.typeTabs;
},
get activeCategoryLabels() {
return this.viewMode === 'repo' ? this.repoCategoryLabels : this.typeCategoryLabels;
},
get activeCategoryColors() {
return this.viewMode === 'repo' ? this.repoCategoryColors : this.typeCategoryColors;
},
get canLoadMore() {
const idx = this.daysProgression.indexOf(this.currentDays);
return idx >= 0 && idx < this.daysProgression.length - 1;
},
setViewMode(mode) {
this.viewMode = mode;
this.activeTab = 'all';
},
activeCommitCategory(commit) {
return this.viewMode === 'repo' ? commit.category : commit.commitCategory;
},
async init() {
await this.fetchChangelog(30);
},
async fetchChangelog(days) {
try {
const response = await fetch('/githubapi/api/changelog?days=' + days);
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
this.commits = data.commits || [];
this.categories = data.categories || {};
this.commitCategories = data.commitCategories || {};
this.currentDays = data.days;
} catch (err) {
console.error('Changelog error:', err);
} finally {
this.loading = false;
this.loadingMore = false;
}
},
async loadMore() {
const idx = this.daysProgression.indexOf(this.currentDays);
if (idx < 0 || idx >= this.daysProgression.length - 1) return;
const nextDays = this.daysProgression[idx + 1];
this.loadingMore = true;
await this.fetchChangelog(nextDays);
},
filteredCommits() {
if (this.activeTab === 'all') return this.commits;
const field = this.viewMode === 'repo' ? 'category' : 'commitCategory';
return this.commits.filter(c => c[field] === this.activeTab);
},
getCount(tabKey) {
if (tabKey === 'all') return this.commits.length;
const field = this.viewMode === 'repo' ? 'category' : 'commitCategory';
return this.commits.filter(c => c[field] === tabKey).length;
},
formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
const now = new Date();
const diffMs = now - d;
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffHours < 1) return 'just now';
if (diffHours < 24) return diffHours + 'h ago';
if (diffDays < 7) return diffDays + 'd ago';
if (diffDays < 30) return Math.floor(diffDays / 7) + 'w ago';
return d.toLocaleDateString('en', { month: 'short', day: 'numeric', year: d.getFullYear() !== now.getFullYear() ? 'numeric' : undefined });
}
};
}
</script>