---
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.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-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',
    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 fetchJson(paths) {
      for (const path of paths) {
        try {
          const response = await fetch(path);
          if (response.ok) {
            return {
              ok: true,
              data: await response.json(),
            };
          }
        } catch {
          // Try next candidate path.
        }
      }

      return {
        ok: false,
        data: null,
      };
    },

    async fetchChangelog(days) {
      try {
        const result = await this.fetchJson([
          '/github/api/changelog?days=' + days,
          '/githubapi/api/changelog?days=' + days,
        ]);

        if (!result.ok) throw new Error('Failed to fetch');

        const data = result.data || {};
        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>
