Skip to content

Commit e9602d4

Browse files
committed
Recently played improvements
1 parent 388307c commit e9602d4

File tree

2 files changed

+132
-43
lines changed

2 files changed

+132
-43
lines changed

src/app/(content)/recently-played/RecentlyPlayed.tsx

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client';
22

33
import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query';
4-
import { AnimatePresence } from 'framer-motion';
4+
import { AnimatePresence, motion } from 'framer-motion';
5+
import { Activity, Clock } from 'lucide-react';
56
import LiveTrack from '@/components/LiveTrack';
67
import RelistenAPI from '@/lib/RelistenAPI';
78

@@ -30,12 +31,43 @@ const fetchRecentlyPlayed = async (queryClient: QueryClient) => {
3031
const parsed = await RelistenAPI.fetchLiveHistory(String(lastSeenId) || undefined);
3132

3233
if (Array.isArray(parsed)) {
33-
return parsed.concat(cache ?? []).slice(0, 100);
34+
return parsed.concat(cache ?? []).slice(0, 500);
3435
}
3536

3637
return cache ?? [];
3738
};
3839

40+
const LoadingSkeleton = () => (
41+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3">
42+
{Array.from({ length: 8 }).map((_, i) => (
43+
<div key={i} className="animate-pulse space-y-3 rounded-xl border border-gray-100 p-4">
44+
<div className="h-4 rounded bg-gray-200"></div>
45+
<div className="h-3 w-3/4 rounded bg-gray-200"></div>
46+
<div className="space-y-2">
47+
<div className="h-2 w-1/2 rounded bg-gray-200"></div>
48+
<div className="h-2 w-2/3 rounded bg-gray-200"></div>
49+
</div>
50+
</div>
51+
))}
52+
</div>
53+
);
54+
55+
const EmptyState = () => (
56+
<motion.div
57+
initial={{ opacity: 0, y: 20 }}
58+
animate={{ opacity: 1, y: 0 }}
59+
className="flex flex-col items-center justify-center py-16 text-center"
60+
>
61+
<div className="mb-4 rounded-full bg-gray-50 p-4">
62+
<Clock className="h-8 w-8 text-gray-400" />
63+
</div>
64+
<h3 className="mb-2 text-lg font-medium text-gray-900">No recent activity</h3>
65+
<p className="max-w-sm text-gray-500">
66+
Tracks will appear here as people listen to shows across the Relisten community.
67+
</p>
68+
</motion.div>
69+
);
70+
3971
export default function RecentlyPlayed() {
4072
const queryClient = useQueryClient();
4173
const query = useQuery({
@@ -44,18 +76,60 @@ export default function RecentlyPlayed() {
4476
refetchInterval: 7000, // refetch every 7 seconds
4577
});
4678

79+
const tracks = query.data ? uniqBy(query.data, keyFn).slice(0, 40) : [];
80+
4781
return (
48-
<div>
49-
<h1>Recently Played</h1>
50-
51-
<div className="grid grid-flow-row-dense grid-cols-2 gap-4 md:grid-cols-3 xl:grid-cols-3">
52-
<AnimatePresence initial={false}>
53-
{!query.data
54-
? null
55-
: uniqBy(query.data as any[], keyFn)
56-
.slice(0, 40)
57-
.map((data) => <LiveTrack {...data} key={data.track.track.id} />)}
58-
</AnimatePresence>
82+
<div className="min-h-screen bg-gray-50/30">
83+
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
84+
{/* Header */}
85+
<motion.div
86+
initial={{ opacity: 0, y: -20 }}
87+
animate={{ opacity: 1, y: 0 }}
88+
className="text-center"
89+
>
90+
<div className="mb-2 flex items-center justify-center gap-3">
91+
<div className="rounded-full bg-green-100 p-2">
92+
<Activity className="h-6 w-6 text-green-600" />
93+
</div>
94+
<h1 className="mb-0 text-3xl font-bold text-gray-900 sm:text-4xl">Recently Played</h1>
95+
</div>
96+
<p className="mx-auto mb-4 max-w-2xl text-gray-600">
97+
This is what people are listening to right now - join 'em.
98+
</p>
99+
</motion.div>
100+
101+
{/* Content */}
102+
{query.isLoading && <LoadingSkeleton />}
103+
104+
{query.data && tracks.length === 0 && <EmptyState />}
105+
106+
{query.data && tracks.length > 0 && (
107+
<motion.div
108+
initial={{ opacity: 0 }}
109+
animate={{ opacity: 1 }}
110+
transition={{ delay: 0.1 }}
111+
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3"
112+
>
113+
<AnimatePresence initial={false} mode="popLayout">
114+
{tracks.map((data, index) => (
115+
<motion.div
116+
key={data.track.track.id}
117+
initial={{ opacity: 0, y: 20 }}
118+
animate={{ opacity: 1, y: 0 }}
119+
exit={{ opacity: 0, scale: 0.9, transition: { duration: 0.2 } }}
120+
transition={{
121+
delay: index * 0.05,
122+
duration: 0.3,
123+
ease: 'easeOut',
124+
}}
125+
layout
126+
>
127+
<LiveTrack {...data} />
128+
</motion.div>
129+
))}
130+
</AnimatePresence>
131+
</motion.div>
132+
)}
59133
</div>
60134
</div>
61135
);

src/components/LiveTrack.tsx

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ type VenueInfoProps = {
4343
const VenueInfo = ({ track, app_type_description, created_at }: VenueInfoProps) => {
4444
const info = getVenueInfo(track.source);
4545
return info ? (
46-
<div>
47-
<div>
48-
{info.name} &middot; {info.location}
46+
<div className="space-y-1">
47+
<div className="text-foreground-muted">{track.source.display_date}</div>
48+
<div className="line-clamp-1">
49+
{info.name} · {info.location}
4950
</div>
50-
<div>{track.source.display_date}</div>
5151
</div>
52-
) : null;
52+
) : (
53+
<div className="text-foreground-muted">{track.source.display_date}</div>
54+
);
5355
};
5456

5557
// shorten date
@@ -76,44 +78,57 @@ export default function LiveTrack({
7678
if (!track?.track) return null;
7779

7880
return (
79-
<Link href={createURL(track)} prefetch={false}>
81+
<Link href={createURL(track)} prefetch={false} className="group h-full">
8082
<motion.div
83+
whileHover={{ y: -4, scale: 1.02 }}
84+
whileTap={{ scale: 0.98 }}
8185
transition={{
82-
duration: 0.6,
8386
type: 'spring',
84-
borderColor: { duration: 2, type: 'ease-out' },
85-
layout: {
86-
duration: 1.2,
87-
type: 'ease-in-out',
88-
},
87+
damping: 20,
88+
stiffness: 300,
8989
}}
90-
initial={{ opacity: 1, height: 0, borderColor: 'rgba(0,255,50,0.1)' }}
91-
animate={{ opacity: 1, height: '100%', borderColor: 'rgba(0,0,0, 0.3)' }}
92-
className={`relative flex flex-1 cursor-pointer overflow-hidden rounded-sm border-[1px] px-4 py-2 transition-opacity duration-1000 ease-in-out hover:bg-slate-200/20 ${isLastSeen && 'border-b-green-600'}`}
90+
className={`relative h-full rounded-xl border border-gray-100 bg-white p-4 shadow-sm transition-all duration-200 hover:border-gray-200 hover:shadow-lg ${
91+
isLastSeen ? 'border-green-200 ring-2 ring-green-100' : ''
92+
}`}
9393
data-is-last-seen={isLastSeen}
94-
layout
9594
>
96-
<Flex className="flex-1" column>
97-
<Flex gap={1} className="items-center justify-between">
98-
<div className="content font-semibold">{track.track.title}</div>
99-
<span className="align-right text-xxs text-nowrap opacity-70">
100-
{app_type_description} &middot; <TimeAgo date={created_at} formatter={formatterFn} />
101-
</span>
102-
</Flex>
103-
<div className="text-sm">{track.source.artist?.name}</div>
95+
{/* New track indicator */}
96+
{isLastSeen && (
97+
<div className="absolute -top-1 -right-1 h-3 w-3 animate-pulse rounded-full bg-green-500"></div>
98+
)}
99+
100+
<div className="space-y-1">
101+
{/* Track title */}
102+
<div className="truncate leading-tight font-semibold text-gray-900 transition-colors group-hover:text-gray-700">
103+
{track.track.title}
104+
</div>
105+
106+
{/* Artist name */}
107+
<div className="text-sm font-medium text-gray-700">{track.source.artist?.name}</div>
104108

105-
<div className="text-xxs text-foreground-muted">
109+
{/* Venue and date info */}
110+
<div className="text-foreground-muted space-y-1 text-xs">
106111
<VenueInfo
107112
track={track}
108113
app_type_description={app_type_description}
109114
created_at={created_at}
110115
/>
111116
</div>
112-
<ArrowRight
113-
className="absolute top-1/2 right-3 -translate-y-1/2 cursor-pointer text-gray-500 hover:text-gray-900"
114-
size={16}
115-
/>
116-
</Flex>
117+
118+
{/* Footer with app type and time */}
119+
<div className="flex items-center justify-between">
120+
<span className="text-foreground-muted text-xs capitalize">{app_type_description}</span>
121+
<span className="text-foreground-muted text-xs">
122+
<TimeAgo date={created_at} formatter={formatterFn} />
123+
</span>
124+
</div>
125+
</div>
126+
127+
{/* Hover arrow */}
128+
<ArrowRight
129+
className="text-foreground-muted absolute top-4 right-4 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
130+
size={16}
131+
/>
117132
</motion.div>
118133
</Link>
119134
);

0 commit comments

Comments
 (0)