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:
@@ -0,0 +1,90 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import useSWR from 'swr';
|
||||
import { sortFeed, type FeedItemDTO } from '@musicshare/shared';
|
||||
import { NowPlayingCard } from '@/components/NowPlayingCard';
|
||||
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);
|
||||
|
||||
useRealtime(true, (msg) => {
|
||||
if (msg.type !== 'now_playing' || msg.data.user.id === me?.id) 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 />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user