11'use client' ;
22
33import { 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' ;
56import LiveTrack from '@/components/LiveTrack' ;
67import 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+
3971export 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 ) ;
0 commit comments