Skip to content

Commit 701ae83

Browse files
Merge pull request #86 from codersforcauses/issue-67-Add_upcoming_events_to_landing_page_carousel
Issue 67 add upcoming events to landing page carousel
2 parents c12acaa + 2d19071 commit 701ae83

2 files changed

Lines changed: 97 additions & 36 deletions

File tree

client/src/components/ui/eventCarousel.tsx

Lines changed: 76 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,50 @@ import { useEffect, useRef, useState } from "react";
55

66
import { UiEvent as EventType } from "@/hooks/useEvents";
77

8+
import { Button } from "./button";
9+
810
type EventCarouselProps = {
911
items: EventType[];
1012
};
1113

1214
const 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+
1442
export 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>

client/src/pages/index.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ import {
77
eventHighlightCardType,
88
} from "@/components/ui/eventHighlightCard";
99
import LandingGames from "@/components/ui/landingGames";
10-
import { placeholderEvents } from "@/placeholderData";
10+
import { UiEvent, useEvents } from "@/hooks/useEvents";
1111

1212
import { Button } from "../components/ui/button";
1313

1414
export default function Landing() {
15+
const { data, isPending, isError, isFetching } = useEvents({
16+
type: "upcoming",
17+
pageSize: 100,
18+
});
19+
20+
const events: UiEvent[] | undefined = data?.items;
21+
1522
const gameLogoImages = [
1623
{ url: "/godot.png", alt: "Godot Logo", position: "start" },
1724
{ url: "/unity-logo.png", alt: "Unity Logo", position: "end" },
@@ -138,8 +145,20 @@ export default function Landing() {
138145
</section>
139146

140147
<section className="bg-background px-10 py-20">
141-
<EventCarousel items={placeholderEvents} />
148+
{isFetching && !isPending && (
149+
<span className="text-sm text-gray-400">Loading...</span>
150+
)}
151+
152+
{isPending && <p>Loading events...</p>}
153+
154+
{isError && (
155+
<p className="text-red-500" role="alert">
156+
Failed to load events.
157+
</p>
158+
)}
159+
{!isPending && !isError && <EventCarousel items={events ?? []} />}
142160
</section>
161+
143162
{/* Leaving commented out until styling/design is confirmed. */}
144163
{/* <section className="bg-background px-4 py-10 md:px-10">
145164
<div className="flex w-full px-4">

0 commit comments

Comments
 (0)