Skip to content

Commit 83ffa4d

Browse files
refactor & styles: landing-hero-contact
1 parent 12d85a0 commit 83ffa4d

File tree

9 files changed

+403
-1071
lines changed

9 files changed

+403
-1071
lines changed

src/components/home/CTASection.tsx

Lines changed: 118 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
17105
export 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

Comments
 (0)