---
layout: layouts/base.njk
title: Interactions
permalink: /interactions/
---
<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">Interactions</h1>
  <p class="text-surface-600 dark:text-surface-400">Activity across the IndieWeb - both my engagement and responses to my content.</p>
</div>

<noscript>
  <div class="p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg mb-6">
    <p class="text-amber-800 dark:text-amber-200 text-sm">The inbound webmentions tab requires JavaScript to load data from the API. Enable JavaScript for the full interactive experience. Outbound interactions are shown below.</p>
  </div>
</noscript>

{# Tab navigation for Outbound/Inbound #}
<div x-data="interactionsApp()" x-init="init()">
  {# Tab buttons #}
  <div class="flex border-b border-surface-200 dark:border-surface-700 mb-6" role="tablist" aria-label="Interaction direction">
    <button
      @click="activeTab = 'outbound'"
      :class="activeTab === 'outbound' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
      :aria-selected="(activeTab === 'outbound').toString()"
      role="tab" id="interactions-tab-outbound" aria-controls="interactions-panel-outbound"
      class="px-4 py-3 text-sm font-medium border-b-2 -mb-px transition-colors">
      My Activity
      <span class="ml-1 text-xs text-surface-600 dark:text-surface-400">(outbound)</span>
    </button>
    <button
      @click="activeTab = 'inbound'"
      :class="activeTab === 'inbound' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
      :aria-selected="(activeTab === 'inbound').toString()"
      role="tab" id="interactions-tab-inbound" aria-controls="interactions-panel-inbound"
      class="px-4 py-3 text-sm font-medium border-b-2 -mb-px transition-colors">
      Received
      <span class="ml-1 text-xs text-surface-600 dark:text-surface-400">(inbound)</span>
      <span x-show="totalInbound > 0" x-text="totalInbound" class="ml-1 px-1.5 py-0.5 text-xs bg-rose-100 dark:bg-rose-900 text-rose-700 dark:text-rose-300 rounded-full"></span>
    </button>
  </div>

  {# ===== OUTBOUND TAB - My Activity ===== #}
  <div x-show="activeTab === 'outbound'" x-transition role="tabpanel" id="interactions-panel-outbound" aria-labelledby="interactions-tab-outbound">
    <p class="text-surface-600 dark:text-surface-400 text-sm mb-6">Content I've interacted with across the web.</p>

    <div class="grid gap-4 sm:gap-6 md:grid-cols-2 lg:grid-cols-3">
      {# Likes #}
      <a href="/likes/" class="block p-6 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group shadow-sm">
        <div class="flex items-center gap-4 mb-4">
          <div class="p-3 bg-rose-100 dark:bg-rose-900/30 rounded-full">
            <svg class="w-6 h-6 text-rose-500" fill="currentColor" viewBox="0 0 24 24">
              <path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
            </svg>
          </div>
          <div>
            <h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 group-hover:text-rose-600 dark:group-hover:text-rose-400">Likes</h2>
            <p class="text-sm text-surface-600 dark:text-surface-400">{{ collections.likes.length }} item{% if collections.likes.length != 1 %}s{% endif %}</p>
          </div>
        </div>
        <p class="text-surface-600 dark:text-surface-400 text-sm">Content I've appreciated across the web.</p>
      </a>

      {# Replies #}
      <a href="/replies/" class="block p-6 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group shadow-sm">
        <div class="flex items-center gap-4 mb-4">
          <div class="p-3 bg-rose-100 dark:bg-rose-900/30 rounded-full">
            <svg class="w-6 h-6 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/>
            </svg>
          </div>
          <div>
            <h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 group-hover:text-rose-600 dark:group-hover:text-rose-400">Replies</h2>
            <p class="text-sm text-surface-600 dark:text-surface-400">{{ collections.replies.length }} item{% if collections.replies.length != 1 %}s{% endif %}</p>
          </div>
        </div>
        <p class="text-surface-600 dark:text-surface-400 text-sm">My responses to posts across the web.</p>
      </a>

      {# Bookmarks #}
      <a href="/bookmarks/" class="block p-6 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group shadow-sm">
        <div class="flex items-center gap-4 mb-4">
          <div class="p-3 bg-yellow-100 dark:bg-yellow-900/30 rounded-full">
            <svg class="w-6 h-6 text-yellow-500" fill="currentColor" viewBox="0 0 24 24">
              <path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/>
            </svg>
          </div>
          <div>
            <h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 group-hover:text-amber-600 dark:group-hover:text-amber-400">Bookmarks</h2>
            <p class="text-sm text-surface-600 dark:text-surface-400">{{ collections.bookmarks.length }} item{% if collections.bookmarks.length != 1 %}s{% endif %}</p>
          </div>
        </div>
        <p class="text-surface-600 dark:text-surface-400 text-sm">Links I've saved for later.</p>
      </a>

      {# Reposts #}
      <a href="/reposts/" class="block p-6 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group shadow-sm">
        <div class="flex items-center gap-4 mb-4">
          <div class="p-3 bg-rose-100 dark:bg-rose-900/30 rounded-full">
            <svg class="w-6 h-6 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
            </svg>
          </div>
          <div>
            <h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 group-hover:text-rose-600 dark:group-hover:text-rose-400">Reposts</h2>
            <p class="text-sm text-surface-600 dark:text-surface-400">{{ collections.reposts.length }} item{% if collections.reposts.length != 1 %}s{% endif %}</p>
          </div>
        </div>
        <p class="text-surface-600 dark:text-surface-400 text-sm">Content I've shared from others.</p>
      </a>

      {# Photos #}
      <a href="/photos/" class="block p-6 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group shadow-sm">
        <div class="flex items-center gap-4 mb-4">
          <div class="p-3 bg-purple-100 dark:bg-purple-900/30 rounded-full">
            <svg class="w-6 h-6 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
            </svg>
          </div>
          <div>
            <h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">Photos</h2>
            <p class="text-sm text-surface-600 dark:text-surface-400">{{ collections.photos.length }} item{% if collections.photos.length != 1 %}s{% endif %}</p>
          </div>
        </div>
        <p class="text-surface-600 dark:text-surface-400 text-sm">Photo posts and images.</p>
      </a>
    </div>

    <div class="mt-12 p-6 bg-surface-50 dark:bg-surface-800/50 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
      <h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 mb-2">About IndieWeb Interactions</h2>
      <p class="text-surface-600 dark:text-surface-400 text-sm mb-4">
        These pages show different types of IndieWeb interactions I've made. Each type uses specific microformat properties
        to indicate the relationship to the original content.
      </p>
      <ul class="text-sm text-surface-600 dark:text-surface-400 space-y-1">
        <li><strong>Likes</strong> use <code class="bg-surface-200 dark:bg-surface-700 px-1 rounded">u-like-of</code></li>
        <li><strong>Replies</strong> use <code class="bg-surface-200 dark:bg-surface-700 px-1 rounded">u-in-reply-to</code></li>
        <li><strong>Bookmarks</strong> use <code class="bg-surface-200 dark:bg-surface-700 px-1 rounded">u-bookmark-of</code></li>
        <li><strong>Reposts</strong> use <code class="bg-surface-200 dark:bg-surface-700 px-1 rounded">u-repost-of</code></li>
      </ul>
    </div>
  </div>

  {# ===== INBOUND TAB - Received Webmentions ===== #}
  <div x-show="activeTab === 'inbound'" x-transition role="tabpanel" id="interactions-panel-inbound" aria-labelledby="interactions-tab-inbound">
    <div class="flex items-center justify-between mb-6">
      <p class="text-surface-600 dark:text-surface-400 text-sm">Webmentions and interactions others have made with my content.</p>
      <button
        @click="fetchWebmentions()"
        :disabled="loading"
        class="inline-flex items-center gap-2 px-3 py-1.5 text-sm bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700 rounded-lg transition-colors disabled:opacity-50">
        <svg class="w-4 h-4" :class="loading ? 'animate-spin' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
        </svg>
        <span x-text="loading ? 'Loading...' : 'Refresh'"></span>
      </button>
    </div>

    {# Loading state #}
    <div x-show="loading && !webmentions.length" class="text-center py-12">
      <div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-rose-500"></div>
      <p class="mt-4 text-surface-600 dark:text-surface-400">Loading webmentions...</p>
    </div>

    {# Setup required state — shown when webmentions proxy is not configured #}
    <div x-show="notConfigured" class="p-6 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg mb-6">
      <h3 class="text-lg font-semibold text-amber-800 dark:text-amber-200 mb-3">Webmentions Not Configured</h3>
      <p class="text-amber-700 dark:text-amber-300 text-sm mb-4">
        To receive inbound webmentions, you need to set up the webmentions proxy plugin
        (<code class="bg-amber-100 dark:bg-amber-900/40 px-1 rounded">@rmdes/indiekit-endpoint-webmentions-proxy</code>).
      </p>
      <ol class="text-sm text-amber-700 dark:text-amber-300 space-y-2 list-decimal list-inside">
        <li>Register your domain at <a href="https://webmention.io" target="_blank" rel="noopener" class="underline font-medium">webmention.io</a> and get your API token</li>
        <li>Set the <code class="bg-amber-100 dark:bg-amber-900/40 px-1 rounded">WEBMENTION_IO_TOKEN</code> environment variable</li>
        <li>Ensure the webmentions proxy plugin is installed and configured in your Indiekit config</li>
        <li>Restart Indiekit to apply the changes</li>
      </ol>
    </div>

    {# Error state — only shown for real errors, not missing config #}
    <div x-show="error && !notConfigured" class="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-300 mb-6">
      <p x-text="error"></p>
    </div>

    {# Filter by type #}
    <div x-show="!notConfigured && (!loading || webmentions.length)" class="flex flex-wrap gap-2 mb-6">
      <button
        @click="filterType = 'all'"
        :class="filterType === 'all' ? 'bg-rose-500 text-white' : 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300'"
        class="px-3 py-1.5 text-sm rounded-full transition-colors">
        All <span x-text="'(' + totalInbound + ')'" class="text-xs opacity-75"></span>
      </button>
      <button
        @click="filterType = 'like-of'"
        :class="filterType === 'like-of' ? 'bg-red-500 text-white' : 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300'"
        class="px-3 py-1.5 text-sm rounded-full transition-colors">
        ❤️ Likes <span x-text="'(' + likes.length + ')'" class="text-xs opacity-75"></span>
      </button>
      <button
        @click="filterType = 'repost-of'"
        :class="filterType === 'repost-of' ? 'bg-green-500 text-white' : 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300'"
        class="px-3 py-1.5 text-sm rounded-full transition-colors">
        🔄 Reposts <span x-text="'(' + reposts.length + ')'" class="text-xs opacity-75"></span>
      </button>
      <button
        @click="filterType = 'in-reply-to'"
        :class="filterType === 'in-reply-to' ? 'bg-sky-500 text-white' : 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300'"
        class="px-3 py-1.5 text-sm rounded-full transition-colors">
        💬 Replies <span x-text="'(' + replies.length + ')'" class="text-xs opacity-75"></span>
      </button>
      <button
        @click="filterType = 'mention-of'"
        :class="filterType === 'mention-of' ? 'bg-yellow-500 text-white' : 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300'"
        class="px-3 py-1.5 text-sm rounded-full transition-colors">
        📣 Mentions <span x-text="'(' + mentions.length + ')'" class="text-xs opacity-75"></span>
      </button>
    </div>

    {# Webmentions list #}
    <div x-show="!notConfigured && (!loading || webmentions.length)" class="space-y-4">
      <template x-for="wm in paginatedWebmentions" :key="wm['wm-id']">
        <div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
          <div class="flex gap-3">
            {# Author avatar #}
            <a :href="wm.author?.url || '#'" target="_blank" rel="noopener" class="flex-shrink-0">
              <img
                :src="wm.author?.photo || '/images/default-avatar.svg'"
                :alt="wm.author?.name || (wm.author?.url?.includes('bsky.app/profile/') ? wm.author.url.split('/profile/')[1].split('/')[0] : 'Anonymous')"
                class="w-10 h-10 rounded-full"
                loading="lazy"
                onerror="this.src='/images/default-avatar.svg'"
              >
            </a>

            <div class="flex-1 min-w-0">
              {# Header with author, type badge, and date #}
              <div class="flex flex-wrap items-center gap-2 mb-1">
                <a :href="wm.author?.url || '#'" target="_blank" rel="noopener" class="font-semibold text-surface-900 dark:text-surface-100 hover:underline" x-text="wm.author?.name || (wm.author?.url?.includes('bsky.app/profile/') ? wm.author.url.split('/profile/')[1].split('/')[0] : 'Anonymous')"></a>

                {# Type badge #}
                <span x-show="wm['wm-property'] === 'like-of'" class="inline-flex items-center gap-1 px-2 py-0.5 text-xs bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-full">
                  ❤️ liked
                </span>
                <span x-show="wm['wm-property'] === 'repost-of'" class="inline-flex items-center gap-1 px-2 py-0.5 text-xs bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-full">
                  🔄 reposted
                </span>
                <span x-show="wm['wm-property'] === 'in-reply-to'" class="inline-flex items-center gap-1 px-2 py-0.5 text-xs bg-sky-100 dark:bg-sky-900/30 text-sky-700 dark:text-sky-300 rounded-full">
                  💬 replied
                </span>
                <span x-show="wm['wm-property'] === 'mention-of'" class="inline-flex items-center gap-1 px-2 py-0.5 text-xs bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300 rounded-full">
                  📣 mentioned
                </span>
                <span x-show="wm['wm-property'] === 'bookmark-of'" class="inline-flex items-center gap-1 px-2 py-0.5 text-xs bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded-full">
                  🔖 bookmarked
                </span>

                {# Platform badge (from conversations API) #}
                <span x-show="wm.platform === 'mastodon'" class="inline-flex items-center gap-1 px-1.5 py-0.5 text-xs bg-indigo-100 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 rounded-full" title="Mastodon">
                  <svg class="w-3 h-3" viewBox="0 0 24 24" fill="currentColor"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.668 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
                </span>
                <span x-show="wm.platform === 'bluesky'" class="inline-flex items-center gap-1 px-1.5 py-0.5 text-xs bg-sky-100 dark:bg-sky-900/30 text-sky-600 dark:text-sky-400 rounded-full" title="Bluesky">
                  <svg class="w-3 h-3" viewBox="0 0 568 501" fill="currentColor"><path d="M123.121 33.664C188.241 82.553 258.281 181.68 284 234.873c25.719-53.192 95.759-152.32 160.879-201.21C491.866-1.611 568-28.906 568 57.947c0 17.346-9.945 145.713-15.778 166.555-20.275 72.453-94.155 90.933-159.875 79.748C507.222 323.8 536.444 388.56 473.333 453.32c-119.86 122.992-172.272-30.859-185.702-70.281-2.462-7.227-3.614-10.608-3.631-7.733-.017-2.875-1.169.506-3.631 7.733-13.43 39.422-65.842 193.273-185.702 70.281-63.111-64.76-33.89-129.52 80.986-149.071-65.72 11.185-139.6-7.295-159.875-79.748C9.945 203.659 0 75.291 0 57.946 0-28.906 76.135-1.612 123.121 33.664Z"/></svg>
                </span>
                <span x-show="wm.platform === 'activitypub'" class="inline-flex items-center gap-1 px-1.5 py-0.5 text-xs bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 rounded-full" title="Fediverse (ActivityPub)">
                  <svg class="w-3 h-3" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M13.09 4.43L24 10.73v2.51L13.09 19.58v-2.51L21.83 12 13.09 6.98v-2.55zM13.09 9.49L17.44 12l-4.35 2.51V9.49z"/><path d="M10.91 4.43L0 10.73v2.51l8.74-5.03v10.09l2.18 1.28V4.43zM6.56 12L2.18 14.51l4.35 2.51V12z"/></svg>
                </span>
                <span x-show="wm.platform === 'webmention' || !wm.platform" class="inline-flex items-center gap-1 px-1.5 py-0.5 text-xs bg-rose-100 dark:bg-rose-900/30 text-rose-600 dark:text-rose-400 rounded-full" title="IndieWeb (Webmention)">
                  <svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>
                </span>

                <a :href="wm.url || '#'" target="_blank" rel="noopener" class="text-xs text-surface-600 dark:text-surface-400 hover:underline">
                  <time class="font-mono text-sm" :datetime="wm.published || wm['wm-received']" x-text="formatDate(wm.published || wm['wm-received'])"></time>
                </a>
              </div>

              {# Content (for replies) #}
              <div x-show="wm.content?.text" class="text-surface-700 dark:text-surface-300 text-sm mt-2" x-text="truncateText(wm.content?.text, 280)"></div>

              {# Target URL - which of my posts this is about #}
              <div class="mt-2 text-xs text-surface-600 dark:text-surface-400">
                <span>on </span>
                <a :href="wm['wm-target']" class="text-rose-600 dark:text-rose-400 hover:underline" x-text="formatTargetUrl(wm['wm-target'])"></a>
              </div>
            </div>
          </div>
        </div>
      </template>

      {# Empty state #}
      <div x-show="!loading && filteredWebmentions.length === 0" class="text-center py-12 text-surface-600 dark:text-surface-400">
        <p>No webmentions found for this filter.</p>
      </div>

      {# Pagination controls #}
      <div x-show="totalPages > 1" class="pt-6">
        <nav class="pagination" aria-label="Webmentions pagination">
          <div class="pagination-info">
            Page <span x-text="currentPage"></span> of <span x-text="totalPages"></span>
            <span class="text-surface-600 dark:text-surface-400 ml-1">(<span x-text="filteredWebmentions.length"></span> total)</span>
          </div>
          <div class="pagination-links">
            <button
              @click="goToPage(currentPage - 1)"
              :disabled="currentPage <= 1"
              class="pagination-link"
              :class="currentPage <= 1 ? 'disabled' : ''">
              <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
              Previous
            </button>

            <template x-for="p in pageNumbers" :key="p">
              <button
                @click="typeof p === 'number' && goToPage(p)"
                :disabled="p === '…'"
                :class="p === currentPage ? 'bg-accent-600 text-white dark:bg-accent-500' : p === '…' ? 'cursor-default opacity-50' : 'bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700'"
                class="px-3 py-1.5 text-sm rounded-lg transition-colors"
                x-text="p">
              </button>
            </template>

            <button
              @click="goToPage(currentPage + 1)"
              :disabled="currentPage >= totalPages"
              class="pagination-link"
              :class="currentPage >= totalPages ? 'disabled' : ''">
              Next
              <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
            </button>
          </div>
        </nav>
      </div>
    </div>

    {# Info box #}
    <div class="mt-12 p-6 bg-surface-50 dark:bg-surface-800/50 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
      <h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 mb-2">About Webmentions</h2>
      <p class="text-surface-600 dark:text-surface-400 text-sm mb-4">
        Webmentions are a W3C standard for cross-site communication. When someone likes, reposts, or replies to my content
        from their own site (or via Bluesky/Mastodon bridges), I receive a webmention notification.
      </p>
      <ul class="text-sm text-surface-600 dark:text-surface-400 space-y-1">
        <li><strong>Likes</strong> - Someone appreciated this post</li>
        <li><strong>Reposts</strong> - Someone shared this post</li>
        <li><strong>Replies</strong> - Someone responded to this post</li>
        <li><strong>Mentions</strong> - Someone linked to this post</li>
      </ul>
    </div>
  </div>
</div>

<script>
function interactionsApp() {
  return {
    activeTab: 'inbound',
    loading: false,
    error: null,
    notConfigured: false,
    webmentions: [],
    filterType: 'all',
    currentPage: 1,
    displayPerPage: 20,
    fetchPerPage: 200,
    refreshInterval: null,

    get likes() {
      return this.webmentions.filter(wm => wm['wm-property'] === 'like-of');
    },

    get reposts() {
      return this.webmentions.filter(wm => wm['wm-property'] === 'repost-of');
    },

    get replies() {
      return this.webmentions.filter(wm => wm['wm-property'] === 'in-reply-to');
    },

    get mentions() {
      return this.webmentions.filter(wm => wm['wm-property'] === 'mention-of');
    },

    get totalInbound() {
      return this.webmentions.length;
    },

    get filteredWebmentions() {
      if (this.filterType === 'all') return this.webmentions;
      return this.webmentions.filter(wm => wm['wm-property'] === this.filterType);
    },

    get totalPages() {
      return Math.max(1, Math.ceil(this.filteredWebmentions.length / this.displayPerPage));
    },

    get paginatedWebmentions() {
      const start = (this.currentPage - 1) * this.displayPerPage;
      return this.filteredWebmentions.slice(start, start + this.displayPerPage);
    },

    get pageNumbers() {
      const total = this.totalPages;
      const current = this.currentPage;
      if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
      const pages = [];
      pages.push(1);
      if (current > 3) pages.push('…');
      for (let i = Math.max(2, current - 1); i <= Math.min(total - 1, current + 1); i++) {
        pages.push(i);
      }
      if (current < total - 2) pages.push('…');
      pages.push(total);
      return pages;
    },

    goToPage(page) {
      if (page < 1 || page > this.totalPages) return;
      this.currentPage = page;
      // Scroll to top of inbound tab
      this.$el.closest('[x-show]')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
    },

    async init() {
      // Reset page when filter changes
      this.$watch('filterType', () => { this.currentPage = 1; });
      await this.fetchWebmentions();
      if (!this.notConfigured) {
        this.refreshInterval = setInterval(() => this.fetchWebmentions(true), 5 * 60 * 1000);
      }
    },

    async fetchWebmentions(silent = false) {
      if (!silent) {
        this.loading = true;
        this.webmentions = [];
      }
      this.error = null;

      try {
        // Fetch all available webmentions by paging through the APIs
        let allWm = [];
        let allConv = [];
        let wmPage = 0;
        let wmDone = false;
        let wmConfigured = true;

        // Fetch all pages from both APIs
        while (!wmDone) {
          const [wmResult, convResult] = await Promise.allSettled([
            fetch(`/webmentions/api/mentions?per-page=${this.fetchPerPage}&page=${wmPage}`).then(r => {
              if (r.status === 404) return { children: [], notConfigured: true };
              if (!r.ok) throw new Error(`HTTP ${r.status}`);
              return r.json();
            }),
            fetch(`/conversations/api/mentions?per-page=${this.fetchPerPage}&page=${wmPage}`).then(r => {
              if (!r.ok) return { children: [] };
              return r.json();
            }).catch(() => ({ children: [] })),
          ]);

          const wmData = wmResult.status === 'fulfilled' ? wmResult.value : { children: [] };
          const convData = convResult.status === 'fulfilled' ? convResult.value : { children: [] };

          if (wmData.notConfigured) wmConfigured = false;

          allWm = allWm.concat(wmData.children || []);
          allConv = allConv.concat(convData.children || []);

          // Stop if both APIs returned fewer than a full page
          const wmCount = (wmData.children || []).length;
          const convCount = (convData.children || []).length;
          if (wmCount < this.fetchPerPage && convCount < this.fetchPerPage) {
            wmDone = true;
          } else {
            wmPage++;
            // Safety cap to prevent infinite loops
            if (wmPage > 20) wmDone = true;
          }
        }

        if (!wmConfigured && allConv.length === 0) {
          this.notConfigured = true;
          return;
        }
        this.notConfigured = false;

        const merged = this.mergeAndDeduplicate(allWm, allConv);

        merged.sort((a, b) => {
          const dateA = new Date(a.published || a['wm-received'] || 0);
          const dateB = new Date(b.published || b['wm-received'] || 0);
          return dateB - dateA;
        });

        this.webmentions = merged;
        if (!silent) this.currentPage = 1;
      } catch (err) {
        this.error = `Failed to load webmentions: ${err.message}`;
        console.error('[Interactions]', err);
      } finally {
        this.loading = false;
      }
    },

    detectPlatform(item) {
      const source = item['wm-source'] || '';
      const authorUrl = item.author?.url || '';
      if (source.includes('brid.gy/') && source.includes('/mastodon/')) return 'mastodon';
      if (source.includes('brid.gy/') && source.includes('/bluesky/')) return 'bluesky';
      if (source.includes('fed.brid.gy')) return 'activitypub';
      if (authorUrl.includes('bsky.app')) return 'bluesky';
      if (authorUrl.includes('mstdn') || authorUrl.includes('mastodon') || authorUrl.includes('social.') ||
          authorUrl.includes('fosstodon.') || authorUrl.includes('hachyderm.') || authorUrl.includes('infosec.exchange') ||
          authorUrl.includes('pleroma.') || authorUrl.includes('misskey.') || authorUrl.includes('pixelfed.')) return 'mastodon';
      return 'webmention';
    },

    mergeAndDeduplicate(wmItems, convItems) {
      const convUrls = new Set(convItems.map(c => c.url).filter(Boolean));
      const seen = new Set();
      const result = [];

      for (const item of convItems) {
        const key = item['wm-id'] || item.url;
        if (key && !seen.has(key)) {
          seen.add(key);
          result.push(item);
        }
      }

      for (const item of wmItems) {
        const wmKey = item['wm-id'];
        if (seen.has(wmKey)) continue;
        if (item.url && convUrls.has(item.url)) continue;

        // Filter out self-webmentions from own syndicated Bluesky posts
        if (item.url && (
          item.url.includes('did:plc:g4utqyolpyb5zpwwodmm3hht') ||
          item.url.includes('bsky.app/profile/svemagie.bsky.social')
        )) continue;

        if (!item.platform) {
          const detected = this.detectPlatform(item);
          if (detected) item.platform = detected;
        }

        seen.add(wmKey);
        result.push(item);
      }

      return result;
    },

    formatDate(dateStr) {
      if (!dateStr) return '';
      const date = new Date(dateStr);
      return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
    },

    truncateText(text, maxLen = 280) {
      if (!text) return '';
      if (text.length <= maxLen) return text;
      return text.slice(0, maxLen).trim() + '...';
    },

    formatTargetUrl(url) {
      if (!url) return '';
      try {
        const u = new URL(url);
        let path = u.pathname;
        if (path.length > 50) {
          path = path.slice(0, 47) + '...';
        }
        return path || '/';
      } catch {
        return url;
      }
    }
  };
}
</script>
