Files
svemagie 02f13db22c feat: convert Funkwhale/Last.fm to client-side Alpine.js
Replaces build-time Eleventy data fetches with live client-side fetches
so listening data is always current without requiring a blog rebuild.

- Add js/listening.js with three Alpine.data components:
  listeningWidget, funkwhalePage, listeningPage
- Replace _data/funkwhaleActivity.js and lastfmActivity.js with sync
  stubs (source: 'indiekit') — keeps slashes.njk links and widget
  guard working at build time
- Rewrite widgets/funkwhale.njk, funkwhale.njk, listening.njk as
  Alpine-driven templates with loading skeletons and error states
- Load listening.js globally in base.njk before Alpine core

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 08:23:05 +02:00

96 lines
5.2 KiB
Plaintext

{# Listening Widget — combined Funkwhale + Last.fm recent tracks (client-side) #}
{% set hasListening = (funkwhaleActivity and funkwhaleActivity.source == 'indiekit') or (lastfmActivity and lastfmActivity.source == 'indiekit') %}
{% if hasListening %}
<is-land on:visible>
<div class="widget" x-data="listeningWidget()" x-init="init()">
<h3 class="widget-title flex items-center gap-2">
<svg class="w-5 h-5 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
</svg>
Listening
</h3>
{# Loading skeleton #}
<div x-show="loading" class="space-y-2 animate-pulse">
<div class="h-4 bg-surface-200 dark:bg-surface-700 rounded w-3/4"></div>
<div class="h-4 bg-surface-200 dark:bg-surface-700 rounded w-1/2"></div>
<div class="h-4 bg-surface-200 dark:bg-surface-700 rounded w-2/3"></div>
</div>
{# Error state #}
<p x-show="!loading && error" class="text-xs text-surface-500 dark:text-surface-400">Unavailable</p>
{# Now Playing #}
<template x-if="!loading && nowPlaying">
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3 mb-3">
<div class="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400 mb-2">
<span class="flex gap-0.5 items-end h-2.5" aria-hidden="true">
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 30%;"></span>
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 70%; animation-delay: 0.2s;"></span>
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 50%; animation-delay: 0.4s;"></span>
</span>
Now Playing
<span class="ml-1" :class="nowPlaying?._source === 'funkwhale' ? 'text-purple-600 dark:text-purple-400' : 'text-red-600 dark:text-red-400'" x-text="nowPlaying?._source === 'funkwhale' ? '(Funkwhale)' : '(Last.fm)'"></span>
</div>
<div class="flex items-center gap-3">
<template x-if="nowPlaying?.coverUrl">
<img :src="nowPlaying.coverUrl" alt="" class="w-10 h-10 rounded object-cover flex-shrink-0 shadow-lg" loading="lazy">
</template>
<div class="min-w-0 flex-1">
<p class="font-medium text-sm text-surface-900 dark:text-surface-100 truncate">
<template x-if="nowPlaying?.trackUrl">
<a :href="nowPlaying.trackUrl" class="hover:text-purple-600 dark:hover:text-purple-400" target="_blank" rel="noopener" x-text="nowPlaying.track"></a>
</template>
<template x-if="!nowPlaying?.trackUrl">
<span x-text="nowPlaying?.track"></span>
</template>
</p>
<p class="text-xs text-surface-600 dark:text-surface-400 truncate" x-text="nowPlaying?.artist"></p>
</div>
</div>
</div>
</template>
{# Recent tracks #}
<ul x-show="!loading && recentTracks.length" class="space-y-2">
<template x-for="item in recentTracks" :key="item._ts + item.track">
<li class="flex items-center gap-2">
<template x-if="item.coverUrl">
<img :src="item.coverUrl" alt="" class="w-8 h-8 rounded object-cover flex-shrink-0 shadow-lg" loading="lazy">
</template>
<template x-if="!item.coverUrl">
<div :class="item._source === 'funkwhale' ? 'bg-purple-100 dark:bg-purple-900/30' : 'bg-red-100 dark:bg-red-900/30'" class="w-8 h-8 rounded flex items-center justify-center flex-shrink-0">
<svg class="w-4 h-4" :class="item._source === 'funkwhale' ? 'text-purple-400' : 'text-red-400'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13"/>
</svg>
</div>
</template>
<div class="min-w-0 flex-1">
<p class="text-sm text-surface-900 dark:text-surface-100 truncate">
<template x-if="item.trackUrl">
<a :href="item.trackUrl" class="hover:text-purple-600 dark:hover:text-purple-400" target="_blank" rel="noopener" x-text="item.track"></a>
</template>
<template x-if="!item.trackUrl">
<span x-text="item.track"></span>
</template>
<template x-if="item.loved">
<span class="text-red-500 ml-0.5">&#9829;</span>
</template>
</p>
<p class="text-xs text-surface-600 dark:text-surface-400 truncate">
<span x-text="item.artist"></span>
<span class="ml-1" :class="item._source === 'funkwhale' ? 'text-purple-500' : 'text-red-500'" x-text="item._source === 'funkwhale' ? 'Funkwhale' : 'Last.fm'"></span>
</p>
</div>
</li>
</template>
</ul>
<a href="/listening/" class="text-sm text-purple-600 dark:text-purple-400 hover:underline flex items-center gap-1 mt-3">
View full listening history
<svg class="w-3 h-3" 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"/></svg>
</a>
</div>
</is-land>
{% endif %}