Files
musicshare/apps/web/src/app/page.tsx
T
chrisandClaude Opus 5.5 23faffc251 Add pause controls and app attribution to the web app
Settings gets a Sharing card (pause 1 hour / until tomorrow / until
turned back on); the feed shows a resume banner while paused. Now-playing
cards say which app a phone reported, e.g. "via Spotify".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
2026-10-03 17:37:30 -04:00

105 lines
3.5 KiB
TypeScript

'use client';
import Link from 'next/link';
import useSWR from 'swr';
import { sortFeed, type FeedItemDTO, type ProfileDTO } from '@musicshare/shared';
import { NowPlayingCard } from '@/components/NowPlayingCard';
import { SharingControls } from '@/components/SharingControls';
import { UserSearch } from '@/components/UserSearch';
import { api, fetcher, useMe } from '@/lib/api';
import { useRealtime } from '@/lib/useRealtime';
export default function Home() {
const { me, isLoading, loggedOut } = useMe();
if (isLoading) return null;
if (loggedOut || !me) return <Landing />;
return <Feed />;
}
function Feed() {
const { me } = useMe();
const { data: feed, mutate } = useSWR<FeedItemDTO[]>('/feed', fetcher);
// Your own profile, for the "You" card. Loading it also marks you as watched, so your updates arrive quickly.
const { data: mine, mutate: mutateMine } = useSWR<ProfileDTO>(me ? `/users/${me.username}` : null, fetcher);
useRealtime(true, (msg) => {
if (msg.type !== 'now_playing') return;
if (msg.data.user.id === me?.id) {
if (mine) mutateMine({ ...mine, nowPlaying: msg.data }, { revalidate: false });
return;
}
mutate(
(current = []) => {
const existing = current.find((n) => n.user.id === msg.data.user.id);
const item = { ...msg.data, pinnedAt: existing?.pinnedAt ?? null };
return sortFeed([item, ...current.filter((n) => n.user.id !== msg.data.user.id)]);
},
{ revalidate: false },
);
});
const togglePin = async (item: FeedItemDTO) => {
const pinnedAt = item.pinnedAt ? null : new Date().toISOString();
// optimistic: reorder immediately, roll back if the server refuses (e.g. pin limit)
const previous = feed;
mutate(sortFeed((feed ?? []).map((n) => (n.user.id === item.user.id ? { ...n, pinnedAt } : n))), { revalidate: false });
try {
await api(`/users/${item.user.username}/pin`, { method: pinnedAt ? 'PUT' : 'DELETE', body: pinnedAt ? {} : undefined });
} catch (err) {
mutate(previous, { revalidate: false });
alert((err as Error).message);
}
};
return (
<div className="stack">
<UserSearch />
<SharingControls compact />
{mine?.nowPlaying && (
<section className="stack">
<h2 className="section-label">You</h2>
<NowPlayingCard np={mine.nowPlaying} preferred={me?.preferredService} showUser={false} />
</section>
)}
<h1>Friends listening</h1>
{!me?.preferredService && (
<p className="notice">
Pick your streaming service in <Link href="/settings">Settings</Link> so songs open in the right app.
</p>
)}
{feed?.length === 0 && (
<p className="muted">Nothing yet. Follow some friends and their music will show up here live.</p>
)}
{feed?.map((np) => (
<NowPlayingCard
key={np.user.id}
np={np}
preferred={me?.preferredService}
pinned={Boolean(np.pinnedAt)}
onTogglePin={() => togglePin(np)}
/>
))}
</div>
);
}
function Landing() {
return (
<section className="hero">
<h1>See what your friends are playing.</h1>
<p>
Connect Spotify, Last.fm, ListenBrainz and more. When a friend on Apple Music is playing something you like,
one tap opens it in your app.
</p>
<div className="row">
<Link href="/register" className="btn btn-primary">
Create account
</Link>
<Link href="/login" className="btn">
Log in
</Link>
</div>
</section>
);
}