256 lines
10 KiB
Plaintext
256 lines
10 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()">
|
|
|
|
{# 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 tabs" :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="categoryColors[commit.commitCategory]"
|
|
x-text="categoryLabels[commit.commitCategory] || commit.commitCategory"
|
|
></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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const GITEA_URL = '{{ site.gitea.url }}';
|
|
const GITEA_ORG = '{{ site.gitea.org }}';
|
|
const GITEA_REPOS = {{ site.gitea.repos | dump | safe }};
|
|
|
|
const POST_MANAGEMENT_RE = /^(create|update|delete)\s+\w+\s+post$/i;
|
|
|
|
function isPostManagementCommit(msg) {
|
|
return POST_MANAGEMENT_RE.test((msg || '').trim());
|
|
}
|
|
|
|
function categorizeCommit(message) {
|
|
const lower = (message || '').toLowerCase();
|
|
if (/^feat(\(.+\))?!?:/.test(lower)) return 'features';
|
|
if (/^fix(\(.+\))?!?:/.test(lower)) return 'fixes';
|
|
if (/^docs?(\(.+\))?!?:/.test(lower)) return 'documentation';
|
|
if (/^(chore|build|ci|style)(\(.+\))?!?:/.test(lower)) return 'chores';
|
|
if (/^(refactor|perf|test|a11y)(\(.+\))?!?:/.test(lower)) return 'refactor';
|
|
return 'chores';
|
|
}
|
|
|
|
function changelogApp() {
|
|
return {
|
|
activeTab: 'all',
|
|
loading: true,
|
|
loadingMore: false,
|
|
commits: [],
|
|
categories: {},
|
|
currentPage: 1,
|
|
hasMore: false,
|
|
|
|
tabs: [
|
|
{ key: 'all', label: 'All' },
|
|
{ key: 'features', label: 'Features' },
|
|
{ key: 'fixes', label: 'Fixes' },
|
|
{ key: 'documentation', label: 'Docs' },
|
|
{ key: 'chores', label: 'Chores' },
|
|
{ key: 'refactor', label: 'Refactor' },
|
|
],
|
|
|
|
categoryLabels: {
|
|
features: 'Features',
|
|
fixes: 'Fixes',
|
|
documentation: 'Docs',
|
|
chores: 'Chores',
|
|
refactor: 'Refactor',
|
|
},
|
|
|
|
categoryColors: {
|
|
features: 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300',
|
|
fixes: 'bg-orange-100 dark:bg-orange-900 text-orange-700 dark:text-orange-300',
|
|
documentation: 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300',
|
|
chores: 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300',
|
|
refactor: 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300',
|
|
},
|
|
|
|
get canLoadMore() {
|
|
return this.hasMore;
|
|
},
|
|
|
|
async init() {
|
|
await this.fetchChangelog(1);
|
|
},
|
|
|
|
async fetchChangelog(page) {
|
|
try {
|
|
const limit = 50;
|
|
const newCommits = [];
|
|
let anyHasMore = false;
|
|
|
|
await Promise.all(GITEA_REPOS.map(async (repo) => {
|
|
const url = `${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${repo}/commits?limit=${limit}&page=${page}`;
|
|
const r = await fetch(url);
|
|
if (!r.ok) return;
|
|
const commits = await r.json();
|
|
if (!Array.isArray(commits)) return;
|
|
if (commits.length >= limit) anyHasMore = true;
|
|
for (const c of commits) {
|
|
const lines = (c.commit?.message || '').split('\n');
|
|
const title = lines[0];
|
|
if (isPostManagementCommit(title)) continue;
|
|
const body = lines.slice(1).join('\n').trim();
|
|
newCommits.push({
|
|
sha: c.sha.slice(0, 7),
|
|
fullSha: c.sha,
|
|
title,
|
|
body: body || null,
|
|
url: c.html_url,
|
|
repoUrl: `${GITEA_URL}/${GITEA_ORG}/${repo}`,
|
|
repoName: repo,
|
|
date: c.commit?.author?.date || c.created,
|
|
author: c.commit?.author?.name || '',
|
|
commitCategory: categorizeCommit(title),
|
|
});
|
|
}
|
|
}));
|
|
|
|
const merged = page === 1
|
|
? newCommits
|
|
: [...this.commits, ...newCommits];
|
|
merged.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
|
|
const categories = {};
|
|
for (const c of merged) {
|
|
categories[c.commitCategory] = (categories[c.commitCategory] || 0) + 1;
|
|
}
|
|
|
|
this.commits = merged;
|
|
this.categories = categories;
|
|
this.currentPage = page;
|
|
this.hasMore = anyHasMore;
|
|
} catch (err) {
|
|
console.error('Changelog error:', err);
|
|
} finally {
|
|
this.loading = false;
|
|
this.loadingMore = false;
|
|
}
|
|
},
|
|
|
|
async loadMore() {
|
|
this.loadingMore = true;
|
|
await this.fetchChangelog(this.currentPage + 1);
|
|
},
|
|
|
|
filteredCommits() {
|
|
if (this.activeTab === 'all') return this.commits;
|
|
return this.commits.filter(c => c.commitCategory === this.activeTab);
|
|
},
|
|
|
|
getCount(tabKey) {
|
|
if (tabKey === 'all') return this.commits.length;
|
|
return this.commits.filter(c => c.commitCategory === 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>
|