11import { useMemo } from "react" ;
22import type { HTConference } from "@/types/db" ;
3- import { ConferenceCard } from "./ConferenceCell " ;
3+ import { ConferenceCard } from "./ConferenceCard " ;
44
5+ /** Firestore / date helpers */
56type FirestoreTimestampLike = { toDate : ( ) => Date } ;
67type DateLike =
78 | Date
@@ -14,7 +15,6 @@ type DateLike =
1415function isFirestoreTimestamp ( v : unknown ) : v is FirestoreTimestampLike {
1516 return typeof ( v as { toDate ?: unknown } ) ?. toDate === "function" ;
1617}
17-
1818function toMillis ( value : DateLike ) : number {
1919 if ( ! value ) return 0 ;
2020 if ( value instanceof Date ) return value . getTime ( ) ;
@@ -62,62 +62,169 @@ export function DisplayConferences({
6262 const history = conferences . filter (
6363 ( c ) => startMs ( c as ConferenceWithDates < HTConference > ) < now
6464 ) ;
65+
66+ const THIRTY_DAYS = 1000 * 60 * 60 * 24 * 30 ;
67+
68+ const recent = [ ...conferences ]
69+ . filter ( ( c ) => {
70+ const updated = updatedMs ( c as ConferenceWithDates < HTConference > ) ;
71+ return updated && Date . now ( ) - updated <= THIRTY_DAYS ;
72+ } )
73+ . sort ( byUpdatedDesc )
74+ . reduce < HTConference [ ] > ( ( acc , c ) => {
75+ if ( ! acc . find ( ( x ) => x . id === c . id ) ) acc . push ( c ) ;
76+ return acc ;
77+ } , [ ] ) ;
78+
6579 return {
6680 upcoming : future . sort ( byStartAsc ) ,
67- updated : [ ... conferences ] . sort ( byUpdatedDesc ) . slice ( 0 , 8 ) ,
81+ updated : recent ,
6882 past : history . sort ( byStartDesc ) ,
6983 } ;
7084 } , [ conferences ] ) ;
7185
7286 return (
73- < div className = "space-y-8 my -10" >
87+ < div className = "my-10 space-y-10" >
7488 { /* Upcoming */ }
75- < section id = "upcoming" className = "space-y-4" >
76- < h2 className = "text-lg font-semibold text-neutral-200" >
77- Upcoming Conferences
78- </ h2 >
89+ < Section
90+ id = "upcoming"
91+ title = "Upcoming Conferences"
92+ count = { upcoming . length }
93+ ariaLabel = "Upcoming conferences"
94+ >
7995 { upcoming . length ? (
80- < div className = "grid gap-4 sm:gap-5 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 [grid-auto-rows:1fr]" >
96+ < CardsGrid >
8197 { upcoming . map ( ( c ) => (
82- < ConferenceCard key = { c . id } conference = { c } />
98+ < CardWrap key = { c . id } >
99+ < ConferenceCard conference = { c } />
100+ </ CardWrap >
83101 ) ) }
84- </ div >
102+ </ CardsGrid >
85103 ) : (
86- < p className = "text-neutral-400" > No upcoming conferences found.</ p >
104+ < EmptyState message = " No upcoming conferences found." / >
87105 ) }
88- </ section >
106+ </ Section >
89107
90108 { /* Recently Updated */ }
91- < section id = "updated" className = "space-y-4" >
92- < h2 className = "text-lg font-semibold text-neutral-200" >
93- Recently Updated
94- </ h2 >
109+ < Section
110+ id = "updated"
111+ title = "Recently Updated"
112+ count = { updated . length }
113+ ariaLabel = "Recently updated conferences"
114+ >
95115 { updated . length ? (
96- < div className = "grid gap-4 sm:gap-5 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 [grid-auto-rows:1fr]" >
116+ < CardsGrid >
97117 { updated . map ( ( c ) => (
98- < ConferenceCard key = { c . id } conference = { c } />
118+ < CardWrap key = { c . id } >
119+ < ConferenceCard conference = { c } />
120+ </ CardWrap >
99121 ) ) }
100- </ div >
122+ </ CardsGrid >
101123 ) : (
102- < p className = "text-neutral-400" > No recent updates.</ p >
124+ < EmptyState message = " No recent updates." / >
103125 ) }
104- </ section >
126+ </ Section >
105127
106128 { /* Past */ }
107- < section id = "past" className = "space-y-4" >
108- < h2 className = "text-lg font-semibold text-neutral-200" >
109- Past Conferences
110- </ h2 >
129+ < Section
130+ id = "past"
131+ title = "Past Conferences"
132+ count = { past . length }
133+ ariaLabel = "Past conferences"
134+ >
111135 { past . length ? (
112- < div className = "grid gap-4 sm:gap-5 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 [grid-auto-rows:1fr]" >
136+ < CardsGrid >
113137 { past . map ( ( c ) => (
114- < ConferenceCard key = { c . id } conference = { c } />
138+ < CardWrap key = { c . id } >
139+ < ConferenceCard conference = { c } />
140+ </ CardWrap >
115141 ) ) }
116- </ div >
142+ </ CardsGrid >
117143 ) : (
118- < p className = "text-neutral-400" > No past conferences found.</ p >
144+ < EmptyState message = " No past conferences found." / >
119145 ) }
120- </ section >
146+ </ Section >
147+ </ div >
148+ ) ;
149+ }
150+
151+ function Section ( {
152+ id,
153+ title,
154+ count,
155+ children,
156+ } : {
157+ id : string ;
158+ title : string ;
159+ count ?: number ;
160+ ariaLabel : string ;
161+ children : React . ReactNode ;
162+ } ) {
163+ return (
164+ < section
165+ id = { id }
166+ role = "region"
167+ aria-labelledby = { `${ id } -title` }
168+ className = "space-y-4"
169+ >
170+ < header className = "flex items-center justify-between" >
171+ < h2
172+ id = { `${ id } -title` }
173+ className = "group inline-flex items-center gap-2 text-base sm:text-lg font-semibold text-neutral-100"
174+ >
175+ { title }
176+ { typeof count === "number" && (
177+ < span className = "rounded-full bg-neutral-800 text-neutral-300 text-xs px-2 py-0.5" >
178+ { count }
179+ </ span >
180+ ) }
181+ < a
182+ href = { `#${ id } ` }
183+ className = "opacity-0 group-hover:opacity-100 transition-opacity text-neutral-500 hover:text-neutral-300"
184+ aria-label = { `Link to section ${ title } ` }
185+ >
186+ #
187+ </ a >
188+ </ h2 >
189+ </ header >
190+
191+ < div className = "h-px bg-gradient-to-r from-transparent via-neutral-800 to-transparent" />
192+
193+ { children }
194+ </ section >
195+ ) ;
196+ }
197+
198+ function CardsGrid ( { children } : { children : React . ReactNode } ) {
199+ return (
200+ < div className = "grid items-stretch gap-4 sm:gap-5 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4" >
201+ { children }
202+ </ div >
203+ ) ;
204+ }
205+
206+ function CardWrap ( { children } : { children : React . ReactNode } ) {
207+ return < div className = "h-full" > { children } </ div > ;
208+ }
209+
210+ function EmptyState ( { message } : { message : string } ) {
211+ return (
212+ < div className = "flex items-center gap-3 rounded-lg border border-neutral-800 bg-neutral-900/50 px-4 py-6 text-neutral-400" >
213+ < svg
214+ className = "h-5 w-5 shrink-0 text-neutral-500"
215+ viewBox = "0 0 24 24"
216+ fill = "none"
217+ stroke = "currentColor"
218+ strokeWidth = "1.5"
219+ aria-hidden = "true"
220+ >
221+ < path
222+ strokeLinecap = "round"
223+ strokeLinejoin = "round"
224+ d = "M3 3l18 18M12 21c-4.5-5.5-6.75-9-6.75-11.25a6.75 6.75 0 1113.5 0c0 2.25-2.25 5.75-6.75 11.25z"
225+ />
226+ </ svg >
227+ < p className = "text-sm" > { message } </ p >
121228 </ div >
122229 ) ;
123230}
0 commit comments