@@ -14,11 +14,99 @@ const emptyEvent = (orderNumber: number): Event => ({
1414 orderNumber,
1515} ) ;
1616
17+ const MOBILE_H = "h-[12rem]" ;
18+ const DESKTOP_LARGE_H = "min-h-[10rem]" ;
19+ const DESKTOP_SMALL_H = "min-h-[6rem]" ;
20+
21+ function SkeletonCard ( { className = "" } : { className ?: string } ) {
22+ return (
23+ < div className = { `rounded-xl overflow-hidden bg-gray-200 dark:bg-gray-700 animate-pulse ${ className } ` } />
24+ ) ;
25+ }
26+
27+ function EventCardMobile ( { event, onClick } : { event : Event ; onClick : ( ) => void } ) {
28+ return (
29+ < div
30+ className = { `group relative rounded-xl overflow-hidden cursor-pointer min-w-0 w-full ${ MOBILE_H } ` }
31+ onClick = { ( ) => event ?. imageUrl && onClick ( ) }
32+ >
33+ < div className = "relative w-full h-full bg-gray-100 dark:bg-gray-800" >
34+ { event . imageUrl ? (
35+ < >
36+ < img
37+ src = { event . imageUrl }
38+ alt = { event . title }
39+ className = "object-cover w-full h-full min-w-0 min-h-0"
40+ onError = { ( e ) => ( ( e . target as HTMLImageElement ) . src = "/images/common/loading.png" ) }
41+ />
42+ < div className = "absolute bottom-0 left-0 right-0 h-20 bg-black/70 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
43+ < div className = "absolute bottom-4 left-4 right-4 text-white z-10 opacity-0 group-hover:opacity-100 transition-opacity" >
44+ < h4 className = "font-semibold truncate text-white text-lg" > { event . title } </ h4 >
45+ { event . location && < p className = "text-white/90 text-sm" > { event . location } </ p > }
46+ </ div >
47+ </ >
48+ ) : (
49+ < div className = "w-full h-full flex items-center justify-center" >
50+ < img src = "/images/common/loading.png" alt = "" className = "w-full h-full object-cover" />
51+ </ div >
52+ ) }
53+ </ div >
54+ </ div >
55+ ) ;
56+ }
57+
58+ function EventCardDesktop ( {
59+ event,
60+ onClick,
61+ size,
62+ className = "" ,
63+ } : {
64+ event : Event ;
65+ onClick : ( ) => void ;
66+ size : "large" | "small" ;
67+ className ?: string ;
68+ } ) {
69+ const isSmall = size === "small" ;
70+ const heightClass = isSmall ? DESKTOP_SMALL_H : DESKTOP_LARGE_H ;
71+ return (
72+ < div
73+ className = { `group relative rounded-xl overflow-hidden cursor-pointer min-w-0 w-full ${ heightClass } ${ className } ` }
74+ onClick = { ( ) => event ?. imageUrl && onClick ( ) }
75+ >
76+ < div className = { `relative w-full h-full ${ heightClass } bg-gray-100 dark:bg-gray-800` } >
77+ { event . imageUrl ? (
78+ < >
79+ < img
80+ src = { event . imageUrl }
81+ alt = { event . title }
82+ className = "object-cover w-full h-full min-w-0 min-h-0"
83+ onError = { ( e ) => ( ( e . target as HTMLImageElement ) . src = "/images/common/loading.png" ) }
84+ />
85+ < div
86+ className = { `absolute bottom-0 left-0 right-0 ${ isSmall ? "h-16" : "h-20" } bg-black/70 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity` }
87+ />
88+ < div className = "absolute bottom-4 left-4 right-4 text-white z-10 opacity-0 group-hover:opacity-100 transition-opacity" >
89+ < h4 className = { `font-semibold truncate text-white ${ isSmall ? "text-base" : "text-lg" } ` } > { event . title } </ h4 >
90+ { event . location && (
91+ < p className = { `text-white/90 ${ isSmall ? "text-xs" : "text-sm" } ` } > { event . location } </ p >
92+ ) }
93+ </ div >
94+ </ >
95+ ) : (
96+ < div className = "w-full h-full flex items-center justify-center" >
97+ < img src = "/images/common/loading.png" alt = "" className = "w-full h-full object-cover" />
98+ </ div >
99+ ) }
100+ </ div >
101+ </ div >
102+ ) ;
103+ }
104+
17105export default function CTASection ( ) {
18106 const [ modalOpen , setModalOpen ] = useState ( false ) ;
19107 const [ selectedEvent , setSelectedEvent ] = useState < Event | null > ( null ) ;
20108
21- const { data : events = [ ] } = useQuery ( {
109+ const { data : events = [ ] , isLoading } = useQuery ( {
22110 queryKey : [ "event-images" ] ,
23111 queryFn : async ( ) => {
24112 const res = await fetch ( "/api/event-images" ) ;
@@ -33,9 +121,11 @@ export default function CTASection() {
33121 const getEvent = ( orderNumber : number ) =>
34122 events . find ( ( e : Event ) => e . orderNumber === orderNumber ) ?? emptyEvent ( orderNumber ) ;
35123
36- const handleCloseModal = ( ) => {
37- setModalOpen ( false ) ;
38- setSelectedEvent ( null ) ;
124+ const openModal = ( event : Event ) => {
125+ if ( event ?. imageUrl ) {
126+ setSelectedEvent ( event ) ;
127+ setModalOpen ( true ) ;
128+ }
39129 } ;
40130
41131 return (
@@ -44,153 +134,39 @@ export default function CTASection() {
44134 < div className = "mb-8 lg:mb-16 flex flex-wrap justify-between items-center gap-3 min-w-0" >
45135 < div className = "mb-4 lg:mb-6 flex items-center gap-3" >
46136 < StarIcon size = "lg" className = "w-16 h-16" />
47- < h2 className = "text-2xl lg:text-4xl xl:text-5xl font-bold text-gray-900 dark:text-white" >
48- Events
49- </ h2 >
137+ < h2 className = "text-2xl lg:text-4xl xl:text-5xl font-bold text-gray-900 dark:text-white" > Events</ h2 >
50138 </ div >
51139 </ div >
52140
53- < div className = "space-y-3 min-w-0" >
54- < div className = "flex flex-col lg:flex-row gap-3 min-w-0" >
55- { [ 0 , 1 ] . map ( ( orderNumber ) => {
56- const event = getEvent ( orderNumber ) ;
57- const cn = orderNumber === 0 ? "lg:w-[70%] min-w-0 h-70" : "lg:w-[30%] min-w-0 h-70" ;
58- return (
59- < div
60- key = { orderNumber }
61- className = { `group relative rounded-xl overflow-hidden cursor-pointer min-w-0 w-full ${ cn } ` }
62- onClick = { ( ) => {
63- if ( event ?. imageUrl ) {
64- setSelectedEvent ( event ) ;
65- setModalOpen ( true ) ;
66- }
67- } }
68- >
69- < div className = "relative w-full h-full bg-gray-100 dark:bg-gray-800 min-h-[12rem]" >
70- { event . imageUrl ? (
71- < >
72- < img
73- src = { event . imageUrl }
74- alt = { event . title }
75- className = "object-cover w-full h-full"
76- onError = { ( e ) => ( ( e . target as HTMLImageElement ) . src = "/images/common/loading.png" ) }
77- />
78- < div className = "absolute bottom-0 left-0 right-0 h-20 bg-black/70 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
79- < div className = "absolute bottom-4 left-4 right-4 text-white z-10 opacity-0 group-hover:opacity-100 transition-opacity" >
80- < h4 className = "text-lg font-semibold truncate text-white " >
81- { event . title }
82- </ h4 >
83- { event . location && (
84- < p className = "text-sm text-white/90 " > { event . location } </ p >
85- ) }
86- </ div >
87- </ >
88- ) : (
89- < div className = "w-full h-full flex items-center justify-center" >
90- < img src = "/images/common/loading.png" alt = "" className = "w-full h-full object-cover" />
91- </ div >
92- ) }
93- </ div >
94- </ div >
95- ) ;
96- } ) }
97- </ div >
141+ { /* ——— Mobile only ——— */ }
142+ < div className = "lg:hidden space-y-3 min-w-0" >
143+ { [ 0 , 1 , 2 , 3 , 4 , 5 ] . map ( ( orderNumber ) => (
144+ < EventCardMobile
145+ key = { orderNumber }
146+ event = { getEvent ( orderNumber ) }
147+ onClick = { ( ) => openModal ( getEvent ( orderNumber ) ) }
148+ />
149+ ) ) }
150+ </ div >
98151
99- < div className = "flex flex-col lg:flex-row gap-3 min-w-0" >
100- < div className = "flex flex-col sm:flex-row gap-3 lg:w-[70%] min-w-0" >
101- { [ 2 , 3 ] . map ( ( orderNumber ) => {
102- const event = getEvent ( orderNumber ) ;
103- return (
104- < div
105- key = { orderNumber }
106- className = "group relative rounded-xl overflow-hidden cursor-pointer min-w-0 w-full sm:w-1/2 min-h-[12rem]"
107- onClick = { ( ) => {
108- if ( event ?. imageUrl ) {
109- setSelectedEvent ( event ) ;
110- setModalOpen ( true ) ;
111- }
112- } }
113- >
114- < div className = "relative w-full h-full bg-gray-100 dark:bg-gray-800 min-h-[12rem]" >
115- { event . imageUrl ? (
116- < >
117- < img
118- src = { event . imageUrl }
119- alt = { event . title }
120- className = "object-cover w-full h-full"
121- onError = { ( e ) => ( ( e . target as HTMLImageElement ) . src = "/images/common/loading.png" ) }
122- />
123- < div className = "absolute bottom-0 left-0 right-0 h-20 bg-black/70 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
124- < div className = "absolute bottom-4 left-4 right-4 text-white z-10 opacity-0 group-hover:opacity-100 transition-opacity" >
125- < h4 className = "text-lg font-semibold truncate text-white " >
126- { event . title }
127- </ h4 >
128- { event . location && (
129- < p className = "text-sm text-white/90 " > { event . location } </ p >
130- ) }
131- </ div >
132- </ >
133- ) : (
134- < div className = "w-full h-full flex items-center justify-center" >
135- < img src = "/images/common/loading.png" alt = "" className = "w-full h-full object-cover" />
136- </ div >
137- ) }
138- </ div >
139- </ div >
140- ) ;
141- } ) }
152+ < div className = "hidden lg:block space-y-3 min-w-0" >
153+ < div className = "flex flex-row gap-3 min-w-0" >
154+ < EventCardDesktop event = { getEvent ( 0 ) } onClick = { ( ) => openModal ( getEvent ( 0 ) ) } size = "large" className = "lg:w-[70%]" />
155+ < EventCardDesktop event = { getEvent ( 1 ) } onClick = { ( ) => openModal ( getEvent ( 1 ) ) } size = "large" className = "lg:w-[30%]" />
156+ </ div >
157+ < div className = "flex flex-row gap-3 min-w-0" >
158+ < div className = "flex flex-row gap-3 w-[70%] min-w-0" >
159+ < EventCardDesktop event = { getEvent ( 2 ) } onClick = { ( ) => openModal ( getEvent ( 2 ) ) } size = "large" className = "w-1/2" />
160+ < EventCardDesktop event = { getEvent ( 3 ) } onClick = { ( ) => openModal ( getEvent ( 3 ) ) } size = "large" className = "w-1/2" />
142161 </ div >
143- < div className = "flex flex-col gap-3 lg:w-[30%] min-w-0" >
144- { [ 4 , 5 ] . map ( ( orderNumber ) => {
145- const event = getEvent ( orderNumber ) ;
146- return (
147- < div
148- key = { orderNumber }
149- className = "group relative rounded-xl overflow-hidden cursor-pointer min-w-0 w-full min-h-[8rem]"
150- onClick = { ( ) => {
151- if ( event ?. imageUrl ) {
152- setSelectedEvent ( event ) ;
153- setModalOpen ( true ) ;
154- }
155- } }
156- >
157- < div className = "relative w-full h-full bg-gray-100 dark:bg-gray-800 min-h-[8rem]" >
158- { event . imageUrl ? (
159- < >
160- < img
161- src = { event . imageUrl }
162- alt = { event . title }
163- className = "object-cover w-full h-full"
164- onError = { ( e ) => ( ( e . target as HTMLImageElement ) . src = "/images/common/loading.png" ) }
165- />
166- < div className = "absolute bottom-0 left-0 right-0 h-16 bg-black/70 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
167- < div className = "absolute bottom-3 left-3 right-3 text-white z-10 opacity-0 group-hover:opacity-100 transition-opacity" >
168- < h4 className = "text-base font-semibold truncate text-white " >
169- { event . title }
170- </ h4 >
171- { event . location && (
172- < p className = "text-xs text-white/90 " > { event . location } </ p >
173- ) }
174- </ div >
175- </ >
176- ) : (
177- < div className = "w-full h-full flex items-center justify-center" >
178- < img src = "/images/common/loading.png" alt = "" className = "w-full h-full object-cover" />
179- </ div >
180- ) }
181- </ div >
182- </ div >
183- ) ;
184- } ) }
162+ < div className = "flex flex-col gap-3 w-[30%] min-w-0" >
163+ < EventCardDesktop event = { getEvent ( 4 ) } onClick = { ( ) => openModal ( getEvent ( 4 ) ) } size = "small" />
164+ < EventCardDesktop event = { getEvent ( 5 ) } onClick = { ( ) => openModal ( getEvent ( 5 ) ) } size = "small" />
185165 </ div >
186166 </ div >
187167 </ div >
188168
189- < EventModal
190- event = { selectedEvent }
191- isOpen = { modalOpen }
192- onClose = { handleCloseModal }
193- />
169+ < EventModal event = { selectedEvent } isOpen = { modalOpen } onClose = { ( ) => { setModalOpen ( false ) ; setSelectedEvent ( null ) ; } } />
194170 </ div >
195171 </ section >
196172 ) ;
0 commit comments