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
105 lines
3.5 KiB
TypeScript
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>
|
|
);
|
|
}
|