@@ -5,20 +5,50 @@ import { useEffect, useRef, useState } from "react";
55
66import { UiEvent as EventType } from "@/hooks/useEvents" ;
77
8+ import { Button } from "./button" ;
9+
810type EventCarouselProps = {
911 items : EventType [ ] ;
1012} ;
1113
1214const GAP = 40 ;
1315
16+ function formatEventDateDisplay ( dateString : string ) : string {
17+ try {
18+ const date = new Date ( dateString ) ;
19+ const weekday = new Intl . DateTimeFormat ( "en-US" , {
20+ weekday : "long" ,
21+ } ) . format ( date ) ;
22+ const day = new Intl . DateTimeFormat ( "en-US" , { day : "numeric" } ) . format (
23+ date ,
24+ ) ;
25+ const month = new Intl . DateTimeFormat ( "en-US" , { month : "short" } ) . format (
26+ date ,
27+ ) ;
28+ const time = new Intl . DateTimeFormat ( "en-US" , {
29+ hour : "2-digit" ,
30+ minute : "2-digit" ,
31+ hour12 : true ,
32+ } )
33+ . format ( date )
34+ . replace ( "AM" , "am" )
35+ . replace ( "PM" , "pm" ) ;
36+ return `${ weekday } ${ day } ${ month } ${ time } ` ;
37+ } catch {
38+ return "" ;
39+ }
40+ }
41+
1442export default function EventCarousel ( { items } : EventCarouselProps ) {
1543 const viewportRef = useRef < HTMLDivElement > ( null ) ;
16- const firstItemRef = useRef < HTMLDivElement > ( null ) ;
44+ const firstItemRef = useRef < HTMLAnchorElement > ( null ) ;
1745
1846 const [ currentIndex , setCurrentIndex ] = useState ( 0 ) ;
1947 const [ visibleCount , setVisibleCount ] = useState ( 3 ) ;
2048 const [ itemWidth , setItemWidth ] = useState ( 0 ) ;
2149
50+ const isEmpty = items . length === 0 ;
51+
2252 const maxIndex = Math . max ( items . length - visibleCount , 0 ) ;
2353 const slideLeft = ( ) => {
2454 setCurrentIndex ( ( prev ) => Math . max ( prev - 1 , 0 ) ) ;
@@ -28,16 +58,21 @@ export default function EventCarousel({ items }: EventCarouselProps) {
2858 } ;
2959 const translateX = - ( currentIndex * ( itemWidth + GAP ) ) ;
3060
31- /* Observe item width */
61+ /* Observe item width – re-run when items change so we measure after first item mounts */
3262 useEffect ( ( ) => {
33- if ( ! firstItemRef . current ) return ;
34- const observer = new ResizeObserver ( ( ) => {
35- const width = firstItemRef . current ?. clientWidth ?? 0 ;
36- setItemWidth ( width ) ;
37- } ) ;
38- observer . observe ( firstItemRef . current ) ;
63+ const el = firstItemRef . current ;
64+ if ( ! el || items . length === 0 ) return ;
65+ const readWidth = ( ) => {
66+ requestAnimationFrame ( ( ) => {
67+ const w = firstItemRef . current ?. clientWidth ?? 0 ;
68+ setItemWidth ( w ) ;
69+ } ) ;
70+ } ;
71+ readWidth ( ) ;
72+ const observer = new ResizeObserver ( readWidth ) ;
73+ observer . observe ( el ) ;
3974 return ( ) => observer . disconnect ( ) ;
40- } , [ ] ) ;
75+ } , [ items . length ] ) ;
4176
4277 useEffect ( ( ) => {
4378 const updateVisibleCount = ( ) => {
@@ -59,28 +94,35 @@ export default function EventCarousel({ items }: EventCarouselProps) {
5994 < h2 className = "font-jersey10 text-4xl tracking-wide text-white" >
6095 Upcoming Events
6196 </ h2 >
62-
63- < div className = "ml-5 flex gap-3 text-lg text-white/60" >
64- < ChevronLeft
65- className = { `hover:text-white ${
66- currentIndex === 0 ? "opacity-40" : "cursor-pointer"
67- } `}
68- onClick = { slideLeft }
69- />
70- < ChevronRight
71- className = { `hover:text-white ${
72- currentIndex === maxIndex ? "opacity-40" : "cursor-pointer"
73- } `}
74- onClick = { slideRight }
75- />
76- </ div >
97+ { ! isEmpty && (
98+ < div className = "ml-5 flex gap-3 text-lg text-white/60" >
99+ < ChevronLeft
100+ className = { `hover:text-white ${
101+ currentIndex === 0 ? "opacity-40" : "cursor-pointer"
102+ } `}
103+ onClick = { slideLeft }
104+ />
105+ < ChevronRight
106+ className = { `hover:text-white ${
107+ currentIndex === maxIndex ? "opacity-40" : "cursor-pointer"
108+ } `}
109+ onClick = { slideRight }
110+ />
111+ </ div >
112+ ) }
77113 </ div >
78114
79- < Link href = "/events" className = "font-jersey10" >
80- See More
81- </ Link >
115+ { ! isEmpty && (
116+ < Link href = "/events" className = "font-jersey10" >
117+ < Button > See More</ Button >
118+ </ Link >
119+ ) }
82120 </ div >
83121
122+ { isEmpty && (
123+ < p className = "mt-10 px-10 text-sm text-primary" > No events available.</ p >
124+ ) }
125+
84126 < div className = "mt-10 px-10" >
85127 < div ref = { viewportRef } className = "overflow-hidden" >
86128 < div
@@ -91,10 +133,11 @@ export default function EventCarousel({ items }: EventCarouselProps) {
91133 } }
92134 >
93135 { items . map ( ( event , index ) => (
94- < div
136+ < Link
137+ href = { `/events/${ event . id } ` }
95138 key = { event . id }
96139 ref = { index === 0 ? firstItemRef : undefined }
97- className = " w-full flex-shrink-0 md:w-[calc((100%-80px)/3)]"
140+ className = { `block w-full flex-shrink-0 rounded-xl transition-transform duration-200 ease-in-out hover:scale-110 md:w-[calc((100%-80px)/3)] ${ index === 0 ? "origin-left" : "" } ` }
98141 >
99142 < div className = "relative aspect-[16/9] w-full overflow-hidden rounded-lg" >
100143 < Image
@@ -105,17 +148,16 @@ export default function EventCarousel({ items }: EventCarouselProps) {
105148 />
106149 </ div >
107150
108- < h3 className = "mt-6 font-firaCode text-lg font-semibold tracking-wide text-white" >
151+ < h3 className = "mb-2 mt-4 font-jersey10 text-2xl text-white" >
109152 { event . name }
110153 </ h3 >
111154
112- { /* Needs proper processing and laying out */ }
113- < p className = "text-sm tracking-wide text-white/70" >
114- { event . startTime }
155+ < p className = "mb-4 text-sm text-primary" >
156+ { formatEventDateDisplay ( event . date ) }
115157 </ p >
116158
117159 < div className = "mt-3 w-full border-b border-white/20" />
118- </ div >
160+ </ Link >
119161 ) ) }
120162 </ div >
121163 </ div >
0 commit comments