feat: add changelog page and refactor footer to 4-zone grid
- New /changelog/ page with Alpine.js tabbed interface showing commits across all indiekit repos, categorized into 7 groups with load-more - Footer refactored from minimal feed links to responsive 4-zone grid: Navigate, Content, Connect (social links), Meta (feeds + changelog) - Footer uses grid-cols-2 on mobile, grid-cols-4 on desktop Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -295,19 +295,51 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="border-t border-surface-200 dark:border-surface-700 mt-12 pt-8 pb-6">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="flex flex-wrap justify-center gap-4 mb-4">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-8 mb-8">
|
||||||
<a href="/feed.xml" class="inline-flex items-center gap-1 text-primary-600 dark:text-primary-400 hover:underline">
|
{# Navigate #}
|
||||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19 7.38 20 6.18 20C5 20 4 19 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20h-2.83A12.73 12.73 0 0 0 4 7.27V4.44m0 5.66a9.9 9.9 0 0 1 9.9 9.9h-2.83A7.07 7.07 0 0 0 4 12.93V10.1Z"/></svg>
|
<div>
|
||||||
RSS Feed
|
<h4 class="text-sm font-semibold uppercase tracking-wider text-surface-500 dark:text-surface-400 mb-3">Navigate</h4>
|
||||||
</a>
|
<ul class="space-y-2">
|
||||||
<a href="/feed.json" class="inline-flex items-center gap-1 text-primary-600 dark:text-primary-400 hover:underline">
|
<li><a href="/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-primary-600 dark:hover:text-primary-400">Home</a></li>
|
||||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2m3 12h2v2H8v-2m4-8h2v10h-2V7m4 4h2v6h-2v-6Z"/></svg>
|
<li><a href="/about/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-primary-600 dark:hover:text-primary-400">About</a></li>
|
||||||
JSON Feed
|
<li><a href="/cv/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-primary-600 dark:hover:text-primary-400">CV</a></li>
|
||||||
</a>
|
<li><a href="/changelog/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-primary-600 dark:hover:text-primary-400">Changelog</a></li>
|
||||||
|
<li><a href="/search/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-primary-600 dark:hover:text-primary-400">Search</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{# Content #}
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold uppercase tracking-wider text-surface-500 dark:text-surface-400 mb-3">Content</h4>
|
||||||
|
<ul class="space-y-2">
|
||||||
|
<li><a href="/blog/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-primary-600 dark:hover:text-primary-400">Blog</a></li>
|
||||||
|
{% for pt in enabledPostTypes %}
|
||||||
|
<li><a href="{{ pt.path }}" class="text-sm text-surface-600 dark:text-surface-400 hover:text-primary-600 dark:hover:text-primary-400">{{ pt.label }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
<li><a href="/interactions/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-primary-600 dark:hover:text-primary-400">Interactions</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{# Connect #}
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold uppercase tracking-wider text-surface-500 dark:text-surface-400 mb-3">Connect</h4>
|
||||||
|
<ul class="space-y-2">
|
||||||
|
{% for social in site.social %}
|
||||||
|
<li><a href="{{ social.url }}" rel="{{ social.rel }}" target="_blank" class="text-sm text-surface-600 dark:text-surface-400 hover:text-primary-600 dark:hover:text-primary-400">{{ social.name }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{# Meta #}
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold uppercase tracking-wider text-surface-500 dark:text-surface-400 mb-3">Meta</h4>
|
||||||
|
<ul class="space-y-2">
|
||||||
|
<li><a href="/feed.xml" class="text-sm text-surface-600 dark:text-surface-400 hover:text-primary-600 dark:hover:text-primary-400">RSS Feed</a></li>
|
||||||
|
<li><a href="/feed.json" class="text-sm text-surface-600 dark:text-surface-400 hover:text-primary-600 dark:hover:text-primary-400">JSON Feed</a></li>
|
||||||
|
<li><a href="/changelog/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-primary-600 dark:hover:text-primary-400">Changelog</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>Powered by <a href="https://getindiekit.com">Indiekit</a> + <a href="https://11ty.dev">Eleventy</a></p>
|
<p class="text-center text-sm text-surface-500 dark:text-surface-400">Powered by <a href="https://getindiekit.com" class="hover:text-primary-600 dark:hover:text-primary-400">Indiekit</a> + <a href="https://11ty.dev" class="hover:text-primary-600 dark:hover:text-primary-400">Eleventy</a></p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
208
changelog.njk
Normal file
208
changelog.njk
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
---
|
||||||
|
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">
|
||||||
|
<template x-for="tab in tabs" :key="tab.key">
|
||||||
|
<button
|
||||||
|
@click="activeTab = tab.key"
|
||||||
|
:class="activeTab === tab.key ? 'border-b-2 border-primary-500 text-primary-600 dark:text-primary-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
|
||||||
|
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-primary-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-500">Loading changelog...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Commit list #}
|
||||||
|
<div x-show="!loading" x-cloak>
|
||||||
|
<template x-if="filteredCommits().length === 0">
|
||||||
|
<p class="text-surface-500 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="border border-surface-200 dark:border-surface-700 rounded-lg p-4">
|
||||||
|
<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-primary-600 dark:text-primary-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.category]"
|
||||||
|
x-text="categoryLabels[commit.category] || commit.category"
|
||||||
|
></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-primary-600 dark:hover:text-primary-400"
|
||||||
|
x-text="commit.repoName"></a>
|
||||||
|
<span class="text-xs text-surface-500" x-text="formatDate(commit.date)"></span>
|
||||||
|
<span class="text-xs 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-500 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-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',
|
||||||
|
loading: true,
|
||||||
|
loadingMore: false,
|
||||||
|
commits: [],
|
||||||
|
categories: {},
|
||||||
|
currentDays: 30,
|
||||||
|
daysProgression: [30, 90, 180, 'all'],
|
||||||
|
|
||||||
|
tabs: [
|
||||||
|
{ 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' },
|
||||||
|
],
|
||||||
|
|
||||||
|
categoryLabels: {
|
||||||
|
core: 'Core',
|
||||||
|
deployment: 'Deployment',
|
||||||
|
theme: 'Theme',
|
||||||
|
endpoints: 'Endpoint',
|
||||||
|
syndicators: 'Syndicator',
|
||||||
|
'post-types': 'Post Type',
|
||||||
|
presets: 'Preset',
|
||||||
|
other: 'Other',
|
||||||
|
},
|
||||||
|
|
||||||
|
categoryColors: {
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
|
||||||
|
get canLoadMore() {
|
||||||
|
const idx = this.daysProgression.indexOf(this.currentDays);
|
||||||
|
return idx >= 0 && idx < this.daysProgression.length - 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
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.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;
|
||||||
|
return this.commits.filter(c => c.category === this.activeTab);
|
||||||
|
},
|
||||||
|
|
||||||
|
getCount(tabKey) {
|
||||||
|
if (tabKey === 'all') return this.commits.length;
|
||||||
|
return this.commits.filter(c => c.category === 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>
|
||||||
Reference in New Issue
Block a user