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:
@@ -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'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>
|
||||
|
||||
Reference in New Issue
Block a user