Web: redesign, logo, PWA, QR codes, Spotify guidance

- Album-art-tinted cards that crossfade on song change, equalizer bars,
  live avatar rings, drifting ambient colour, Space Grotesk display font,
  brand purple-to-blue gradient from the logo
- Feed cards rise in and glide to new positions (motion); respects
  reduced-motion
- New logo as nav mark, favicon and Apple touch icon
- Installable PWA: manifest, maskable icons, small service worker
  (cached build assets + offline page), security headers
- Show-QR-code on your own profile; centred profile header with stats
- Settings explains connecting Spotify via Last.fm or ListenBrainz

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-05 19:16:40 -04:00
co-authored by Claude Opus 5.5
parent 586acdbeab
commit 17ae36064c
23 changed files with 551 additions and 152 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+38
View File
@@ -0,0 +1,38 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#07070b" />
<title>musicshare · offline</title>
<style>
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: radial-gradient(circle at 20% 10%, #3b1170 0, transparent 45%),
radial-gradient(circle at 90% 90%, #0f3f86 0, transparent 50%), #07070b;
color: #f4f3fa;
font: 16px/1.5 ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
text-align: center;
padding: 1.5rem;
}
img { width: 88px; height: 88px; border-radius: 22px; }
h1 { font-size: 1.6rem; margin: 1rem 0 0.4rem; }
p { color: #9b99ae; margin: 0 0 1.5rem; }
button {
font: inherit; font-weight: 600; color: #fff; border: 0; border-radius: 999px; padding: 0.7rem 1.4rem; cursor: pointer;
background: linear-gradient(135deg, #7b2ff7, #5b4cf0, #2f8cff);
}
</style>
</head>
<body>
<main>
<img src="/icon-192.png" alt="" />
<h1>You're offline</h1>
<p>musicshare needs a connection to see what your friends are playing.</p>
<button onclick="location.reload()">Try again</button>
</main>
</body>
</html>
+49
View File
@@ -0,0 +1,49 @@
// musicshare service worker.
// Deliberately small: live data must always come from the network, so we only
// cache Next.js's immutable build assets, and show an offline page when a
// navigation fails. (Web push notifications will hook in here later.)
const VERSION = 'v1';
const STATIC_CACHE = `static-${VERSION}`;
const OFFLINE_URL = '/offline.html';
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(STATIC_CACHE).then((cache) => cache.addAll([OFFLINE_URL, '/icon-192.png'])));
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches
.keys()
.then((keys) => Promise.all(keys.filter((k) => k !== STATIC_CACHE).map((k) => caches.delete(k))))
.then(() => self.clients.claim()),
);
});
self.addEventListener('fetch', (event) => {
const { request } = event;
if (request.method !== 'GET') return;
const url = new URL(request.url);
if (url.origin !== self.location.origin) return; // API calls, artwork, etc.: straight to the network
// Hashed build assets never change: cache first.
if (url.pathname.startsWith('/_next/static/')) {
event.respondWith(
caches.match(request).then(
(hit) =>
hit ||
fetch(request).then((res) => {
const copy = res.clone();
caches.open(STATIC_CACHE).then((cache) => cache.put(request, copy));
return res;
}),
),
);
return;
}
// Pages: always fresh; offline page if the network is gone.
if (request.mode === 'navigate') {
event.respondWith(fetch(request).catch(() => caches.match(OFFLINE_URL)));
}
});