Add Next.js web app

Feed with live updates and pinning, profiles with follow/pin, featured song
picker, settings for all music services, and account deletion.

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-03 09:59:02 -04:00
co-authored by Claude Opus 5.5
parent 6791b62d87
commit 5004beb10d
22 changed files with 1127 additions and 0 deletions
@@ -0,0 +1,51 @@
'use client';
import Link from 'next/link';
import { SERVICE_LABELS, type NowPlayingDTO, type StreamingService } from '@musicshare/shared';
import { timeAgo } from '@musicshare/shared';
import { Avatar } from './Avatar';
import { TrackCard } from './TrackCard';
export function NowPlayingCard({
np,
preferred,
showUser = true,
pinned,
onTogglePin,
}: {
np: NowPlayingDTO;
preferred: StreamingService | null | undefined;
showUser?: boolean;
pinned?: boolean;
onTogglePin?: () => void;
}) {
return (
<article className={`card ${pinned ? 'card-pinned' : ''}`}>
<header className="card-header">
{showUser && (
<Link href={`/u/${np.user.username}`} className="user-chip">
<Avatar user={np.user} size={28} />
<span>{np.user.displayName ?? np.user.username}</span>
</Link>
)}
<span className="row">
<span className={`status ${np.isPlaying ? 'status-live' : ''}`}>
{np.isPlaying ? 'Listening now' : `Played ${timeAgo(np.updatedAt)}`}
<span className="muted"> via {SERVICE_LABELS[np.source]}</span>
</span>
{onTogglePin && (
<button
className={`pin ${pinned ? 'pinned' : ''}`}
onClick={onTogglePin}
aria-pressed={pinned}
title={pinned ? 'Unpin from top of feed' : 'Pin to top of feed'}
>
{pinned ? '★' : '☆'}
</button>
)}
</span>
</header>
<TrackCard track={np.track} preferred={preferred} />
</article>
);
}