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
+44 -30
View File
@@ -99,9 +99,7 @@ function ProfileForm({ me }: { me: MeDTO }) {
function Connections() {
const params = useSearchParams();
const { data: connections = [], mutate } = useSWR<ConnectionDTO[]>('/connections', fetcher);
const errorParam = params.get('error');
const spotifyLimited = errorParam === 'spotify_limited';
const [error, setError] = useState<string | null>(spotifyLimited ? null : errorParam);
const [error, setError] = useState<string | null>(params.get('error'));
const byProvider = new Map(connections.map((c) => [c.provider, c]));
const run = async (fn: () => Promise<unknown>) => {
@@ -114,7 +112,7 @@ function Connections() {
}
};
const connectOAuth = (provider: 'spotify' | 'plex' | 'lastfm') =>
const connectOAuth = (provider: 'plex' | 'lastfm') =>
run(async () => {
const { url } = await api<{ url: string }>(`/connections/${provider}/authorize`, { body: { client: 'web' } });
window.location.href = url;
@@ -153,24 +151,50 @@ function Connections() {
<p className="muted">Connect where you listen. Your friends see what you&apos;re playing in real time.</p>
{error && <p className="error">{error}</p>}
<div className="connections">
{(['spotify', 'plex'] as const).map((p) => (
<div className="connection" key={p}>
<div>
<strong>{SERVICE_LABELS[p]}</strong>
<div>{status(p) ?? (p === 'plex' && <span className="muted">Music you play from your Plex servers</span>)}</div>
<WebhookHint connection={byProvider.get(p)} />
{/* Spotify caps new apps at a handful of accounts; Spotify's own Last.fm/ListenBrainz link works for everyone. */}
<div className="connection spotify-how">
<div>
<strong>Spotify</strong>
<div className="muted">
Spotify sends everything you play to <strong>Last.fm</strong> or <strong>ListenBrainz</strong>, and we share it from
there. Connect one of them below, then link Spotify on their site:
</div>
{byProvider.get(p)?.status === 'active' ? (
<button className="btn" onClick={() => disconnect(p)}>
Disconnect
</button>
) : (
<button className="btn btn-primary" onClick={() => connectOAuth(p)}>
{byProvider.has(p) ? 'Reconnect' : 'Connect'}
</button>
)}
<ol className="steps">
<li>
<strong>Last.fm</strong> (most popular): sign in below, then{' '}
<a href={SPOTIFY_LINK_PAGES.lastfm} target="_blank" rel="noreferrer">
connect “Spotify Scrobbling” on Last.fm
</a>
.
</li>
<li>
<strong>ListenBrainz</strong> (open source): add your username below, then{' '}
<a href={SPOTIFY_LINK_PAGES.listenbrainz} target="_blank" rel="noreferrer">
connect Spotify on ListenBrainz
</a>
.
</li>
</ol>
<div className="muted">On Android, the musicshare app shares Spotify automatically.</div>
</div>
))}
</div>
<div className="connection">
<div>
<strong>Plex</strong>
<div>{status('plex') ?? <span className="muted">Music you play from your Plex servers</span>}</div>
<WebhookHint connection={byProvider.get('plex')} />
</div>
{byProvider.get('plex')?.status === 'active' ? (
<button className="btn" onClick={() => disconnect('plex')}>
Disconnect
</button>
) : (
<button className="btn btn-primary" onClick={() => connectOAuth('plex')}>
{byProvider.has('plex') ? 'Reconnect' : 'Connect'}
</button>
)}
</div>
<div className="connection">
<div className="grow">
@@ -203,16 +227,6 @@ function Connections() {
.
</div>
))}
{spotifyLimited && (
<div className="notice">
<strong>Spotify sign-in is limited during beta.</strong> Spotify only lets a few accounts sign in to new apps
directly. You can still share Spotify through Last.fm: sign in to Last.fm below, then{' '}
<a href="https://www.last.fm/settings/applications" target="_blank" rel="noreferrer">
connect “Spotify Scrobbling” on Last.fm
</a>
.
</div>
)}
{(['lastfm', 'listenbrainz'] as const).map((p) => (
<div className="connection" key={p}>
<div>