@@ -10,23 +10,61 @@ import posthog from "posthog-js";
1010import { useEffect , useState } from "react" ;
1111import { toast } from "sonner" ;
1212
13- function formatShortDate ( date : string | Date ) {
13+ const EVENTS_TIMEZONE = "Europe/Lisbon" ;
14+
15+ function getDayKey ( date : string | Date ) {
16+ return new Intl . DateTimeFormat ( "en-CA" , {
17+ timeZone : EVENTS_TIMEZONE ,
18+ year : "numeric" ,
19+ month : "2-digit" ,
20+ day : "2-digit" ,
21+ } ) . format ( new Date ( date ) ) ;
22+ }
23+
24+ function getRelativeDayLabel ( date : string | Date ) {
25+ const targetKey = getDayKey ( date ) ;
26+ const now = new Date ( ) ;
27+ const todayKey = getDayKey ( now ) ;
28+ const tomorrowKey = getDayKey ( new Date ( now . getTime ( ) + 24 * 60 * 60 * 1000 ) ) ;
29+
30+ if ( targetKey === todayKey ) {
31+ return "Today" ;
32+ }
33+
34+ if ( targetKey === tomorrowKey ) {
35+ return "Tomorrow" ;
36+ }
37+
38+ return null ;
39+ }
40+
41+ function formatAbsoluteEventDate ( date : string | Date ) {
1442 const d = new Date ( date ) ;
1543 return d . toLocaleDateString ( "en-US" , {
16- timeZone : "Europe/Lisbon" ,
44+ timeZone : EVENTS_TIMEZONE ,
1745 day : "numeric" ,
1846 month : "long" ,
1947 weekday : "long" ,
2048 } ) ;
2149}
2250
51+ function formatEventDate ( date : string | Date , withRelativeLabels = true ) {
52+ if ( ! withRelativeLabels ) {
53+ return formatAbsoluteEventDate ( date ) ;
54+ }
55+
56+ const relativeDayLabel = getRelativeDayLabel ( date ) ;
57+
58+ if ( relativeDayLabel ) {
59+ return relativeDayLabel ;
60+ }
61+
62+ return formatAbsoluteEventDate ( date ) ;
63+ }
64+
2365// Helper function to check if two dates are on the same day
2466function isSameDay ( date1 : Date , date2 : Date ) {
25- return (
26- date1 . getFullYear ( ) === date2 . getFullYear ( ) &&
27- date1 . getMonth ( ) === date2 . getMonth ( ) &&
28- date1 . getDate ( ) === date2 . getDate ( )
29- ) ;
67+ return getDayKey ( date1 ) === getDayKey ( date2 ) ;
3068}
3169
3270interface Event {
@@ -41,16 +79,19 @@ interface Event {
4179
4280interface EventsPageProps {
4381 initialEvents : Event [ ] ;
44- city ?: string ; // Optional city prop
45- user ?: { id : string ; role : string } | null ; // Add user prop
4682}
4783
48- export default function EventsPageClient ( { initialEvents, city, user } : EventsPageProps ) {
84+ function formatCityLabel ( city : string ) {
85+ return city . charAt ( 0 ) . toUpperCase ( ) + city . slice ( 1 ) ;
86+ }
87+
88+ export default function EventsPageClient ( { initialEvents } : EventsPageProps ) {
4989 const [ events , setEvents ] = useState < Event [ ] > ( initialEvents ) ;
5090 const [ selectedDate , setSelectedDate ] = useState < Date | null > ( null ) ;
5191 const [ filteredEvents , setFilteredEvents ] = useState < Event [ ] > ( initialEvents ) ;
5292 const [ pageLoadTime ] = useState ( Date . now ( ) ) ; // ADD THIS
5393 const [ hasClickedEvent , setHasClickedEvent ] = useState ( false ) ; // ADD THIS
94+ const [ hasHydrated , setHasHydrated ] = useState ( false ) ;
5495 const router = useRouter ( ) ;
5596
5697 // Filter for cities, different styles applied depending on the path.
@@ -65,15 +106,22 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
65106 setEvents ( initialEvents ) ;
66107 } , [ initialEvents ] ) ;
67108
109+ useEffect ( ( ) => {
110+ setHasHydrated ( true ) ;
111+ } , [ ] ) ;
112+
68113 // Filter events when selectedDate changes
69114 useEffect ( ( ) => {
115+ const cityFilteredEvents =
116+ cityParam === "all" ? events : events . filter ( ( event ) => event . city . trim ( ) . toLowerCase ( ) === cityParam ) ;
117+
70118 if ( selectedDate ) {
71- const eventsForDate = events . filter ( ( event ) => isSameDay ( new Date ( event . start_time ) , selectedDate ) ) ;
72- setFilteredEvents ( eventsForDate ) ;
73- } else {
74- setFilteredEvents ( events ) ;
119+ setFilteredEvents ( cityFilteredEvents . filter ( ( event ) => isSameDay ( new Date ( event . start_time ) , selectedDate ) ) ) ;
120+ return ;
75121 }
76- } , [ selectedDate , events ] ) ;
122+
123+ setFilteredEvents ( cityFilteredEvents ) ;
124+ } , [ cityParam , selectedDate , events ] ) ;
77125
78126 // Track when events listing is loaded/changed
79127 useEffect ( ( ) => {
@@ -116,6 +164,14 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
116164 router . push ( city === "all" ? "/events" : `/events?city=${ city } ` ) ;
117165 } ;
118166
167+ const cityFilterButtonClass = ( city : string ) =>
168+ cn (
169+ "rounded-full border border-transparent bg-neutral-100 text-muted-foreground transition-[background-color,border-color,color,box-shadow,transform] duration-150 ease hover:shadow-sm motion-reduce:transition-none motion-safe:active:scale-[0.98] dark:bg-[rgba(10,46,61,0.78)] dark:text-[rgba(158,210,225,0.82)] dark:hover:bg-[rgba(4,201,216,0.12)] dark:hover:text-[#4ce4f0]" ,
170+ cityParam === city
171+ ? "bg-[#dff6f7] text-[#28aeb8] hover:bg-[#dff6f7] hover:text-[#28aeb8] dark:border-[rgba(76,228,240,0.4)] dark:bg-[rgba(4,201,216,0.18)] dark:text-[#4ce4f0] dark:hover:bg-[rgba(4,201,216,0.18)] dark:hover:text-[#4ce4f0]"
172+ : "text-neutral-600 hover:text-neutral-900" ,
173+ ) ;
174+
119175 // Handle calendar date click
120176 const handleDateClick = ( date : Date ) => {
121177 // Check if the clicked date has events
@@ -160,13 +216,13 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
160216
161217 return (
162218 < div className = "space-y-8 md:p-4 animate-in" >
163- < div className = "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between" >
164- < h1 className = "text-2xl font-bold leading-tight [ text-wrap:pretty]" >
219+ < div className = "flex flex-col gap-3 pb-4 pt-2 sm:flex-row sm:items-start sm:justify-between md:pb-0 " >
220+ < h1 className = "text-2xl font-extrabold tracking-tight leading-tight text-[#104357] [ text-wrap:pretty] dark:text-[#E3F2F7 ]" >
165221 { selectedDate
166- ? `Events for ${ formatShortDate ( selectedDate ) } `
167- : city
168- ? `Upcoming Events in ${ city . charAt ( 0 ) . toUpperCase ( ) + city . slice ( 1 ) } `
169- : "Upcoming Events" }
222+ ? `Events for ${ formatEventDate ( selectedDate , hasHydrated ) } `
223+ : cityParam !== "all"
224+ ? `Events in ${ formatCityLabel ( cityParam ) } `
225+ : "Events" }
170226 </ h1 >
171227
172228 { selectedDate && (
@@ -182,10 +238,12 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
182238
183239 < div className = "grid grid-cols-1 lg:grid-cols-3 gap-8" >
184240 { /* Events List - Takes up 2/3 of the space on large screens */ }
185- < div className = "space-y-4 lg:col-span-2" >
241+ < div className = "order-2 space-y-4 lg:order-1 lg:col-span-2" >
186242 { filteredEvents . length === 0 ? (
187243 < div className = "rounded-md border border-dashed px-6 py-10 text-center text-base leading-relaxed text-muted-foreground" >
188- { selectedDate ? `No events found for ${ formatShortDate ( selectedDate ) } ` : "No upcoming events found" }
244+ { selectedDate
245+ ? `No events found for ${ formatEventDate ( selectedDate , hasHydrated ) } `
246+ : "No upcoming events found" }
189247 </ div >
190248 ) : (
191249 filteredEvents ?. map ( ( event ) => {
@@ -197,8 +255,8 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
197255 < div key = { event . id } className = "space-y-4" >
198256 { showDateHeading && (
199257 < >
200- < h2 className = "mt-6 text-lg font-semibold text-muted-foreground " >
201- { formatShortDate ( event . start_time ) }
258+ < h2 className = "mt-6 text-lg font-semibold text-[#104357] dark:text-[#E3F2F7] " >
259+ { formatEventDate ( event . start_time , hasHydrated ) }
202260 </ h2 >
203261 </ >
204262 ) }
@@ -218,7 +276,6 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
218276 } ) ;
219277 } }
220278 onDelete = { handleDeleteEvent }
221- isAdmin = { user ?. role === "admin" || process . env . NEXT_ALLOW_BAD_UI === "true" }
222279 />
223280 </ div >
224281 ) ;
@@ -227,8 +284,8 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
227284 </ div >
228285
229286 { /* Calendar Sidebar - Takes up 1/3 of the space on large screens */ }
230- < div className = "lg:col-span-1" >
231- < div className = "sticky top-4 flex flex-col gap-10" >
287+ < div className = "order-1 lg:order-2 lg:col-span-1" >
288+ < div className = "flex flex-col gap-6 lg:sticky lg:top-4 lg: gap-10" >
232289 < section id = "city_filters" className = "flex flex-col gap-3" >
233290 < h2 className = "text-sm font-semibold leading-5 text-[#104357] dark:text-[#E3F2F7]" > Events by City</ h2 >
234291
@@ -240,12 +297,7 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
240297 handleFilterClick ( "all" ) ;
241298 } }
242299 variant = "outline"
243- className = { cn (
244- "rounded-full border-none bg-neutral-100 text-muted-foreground transition-[background-color,color,box-shadow,transform] duration-150 ease hover:shadow-sm motion-reduce:transition-none motion-safe:active:scale-[0.98]" ,
245- cityParam === "all"
246- ? "bg-[#dff6f7] text-[#28aeb8] hover:bg-[#dff6f7] hover:text-[#28aeb8]"
247- : "text-neutral-600 hover:text-neutral-900" ,
248- ) }
300+ className = { cityFilterButtonClass ( "all" ) }
249301 >
250302 < Link href = "/events" > Everything</ Link >
251303 </ Button >
@@ -256,10 +308,7 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
256308 handleFilterClick ( "lisboa" ) ;
257309 } }
258310 variant = "outline"
259- className = { cn (
260- "rounded-full border-none bg-neutral-100 text-muted-foreground transition-[background-color,color,box-shadow,transform] duration-150 ease hover:shadow-sm motion-reduce:transition-none motion-safe:active:scale-[0.98]" ,
261- cityParam === "lisboa" && "bg-[#dff6f7] text-[#28aeb8] hover:bg-[#dff6f7] hover:text-[#28aeb8]" ,
262- ) }
311+ className = { cityFilterButtonClass ( "lisboa" ) }
263312 >
264313 < Link href = "/events?city=lisboa" > Lisboa</ Link >
265314 </ Button >
@@ -270,10 +319,7 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
270319 handleFilterClick ( "porto" ) ;
271320 } }
272321 variant = "outline"
273- className = { cn (
274- "rounded-full border-none bg-neutral-100 text-muted-foreground transition-[background-color,color,box-shadow,transform] duration-150 ease hover:shadow-sm motion-reduce:transition-none motion-safe:active:scale-[0.98]" ,
275- cityParam === "porto" && "bg-[#dff6f7] text-[#28aeb8] hover:bg-[#dff6f7] hover:text-[#28aeb8]" ,
276- ) }
322+ className = { cityFilterButtonClass ( "porto" ) }
277323 >
278324 < Link href = "/events?city=porto" > Porto</ Link >
279325 </ Button >
@@ -284,10 +330,7 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
284330 handleFilterClick ( "online" ) ;
285331 } }
286332 variant = "outline"
287- className = { cn (
288- "rounded-full border-none bg-neutral-100 text-muted-foreground transition-[background-color,color,box-shadow,transform] duration-150 ease hover:shadow-sm motion-reduce:transition-none motion-safe:active:scale-[0.98]" ,
289- cityParam === "online" && "bg-[#dff6f7] text-[#28aeb8] hover:bg-[#dff6f7] hover:text-[#28aeb8]" ,
290- ) }
333+ className = { cityFilterButtonClass ( "online" ) }
291334 >
292335 < Link href = "/events?city=online" > Online</ Link >
293336 </ Button >
0 commit comments