Skip to content

Commit 5f5efee

Browse files
authored
Merge branch 'main' into issue-77-Incorporate_playable_games_into_individual_game_pages
2 parents da87061 + 1df3289 commit 5f5efee

16 files changed

Lines changed: 330 additions & 77 deletions

client/src/components/main/MemberProfile.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import Image from "next/image";
4+
import { SocialIcon } from "react-social-icons";
45

56
// unused atm, as the member isnt linked a project on the backend
67
/* export type MemberProfileProject = {
@@ -15,6 +16,10 @@ export type MemberProfileData = {
1516
about: string;
1617
pronouns?: string;
1718
profile_picture?: string;
19+
social_media?: {
20+
link: string;
21+
socialMediaUserName: string;
22+
}[];
1823
};
1924

2025
type MemberProfileProps = {
@@ -66,7 +71,37 @@ export function MemberProfile({ member }: MemberProfileProps) {
6671
<p className="min-w-fit font-jersey10 text-4xl">{member.name}</p>
6772
<hr className="ml-5 hidden w-full self-center border-light_2 lg:flex" />
6873
</div>
69-
<p className="text-lg">{member.pronouns}</p>
74+
<div className="flex items-center gap-2">
75+
{member.social_media && member.social_media.length > 0 && (
76+
<div className="w-full">
77+
<div className="mt-2 flex flex-wrap items-center gap-2">
78+
{member.social_media.map((sm) => (
79+
<span
80+
key={sm.link}
81+
className="ml-2 flex items-center gap-1"
82+
>
83+
<SocialIcon
84+
url={sm.link}
85+
style={{ height: 24, width: 24 }}
86+
/>
87+
<a
88+
href={sm.link}
89+
target="_blank"
90+
rel="noopener noreferrer"
91+
className="font-firaCode text-base underline hover:text-primary"
92+
>
93+
{sm.socialMediaUserName}
94+
</a>
95+
</span>
96+
))}
97+
</div>
98+
</div>
99+
)}
100+
</div>
101+
<div className="flex items-center gap-2">
102+
<p className="text-lg">{member.pronouns}</p>
103+
</div>
104+
70105
<p>{member.about}</p>
71106
</div>
72107
</div>

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/hooks/useGames.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ type Contributor = {
77
member_id: number;
88
name: string;
99
role: string;
10+
social_media?: Array<{
11+
socialMediaName: string;
12+
link: string;
13+
socialMediaUserName: string;
14+
}>;
1015
};
1116

1217
type ApiGame = {

client/src/hooks/useGameshowcase.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import api from "@/lib/api";
66
type Contributor = {
77
name: string;
88
role: string;
9+
social_media?: {
10+
socialMediaName: string;
11+
link: string;
12+
socialMediaUserName: string;
13+
}[];
914
};
1015

1116
type ApiShowcaseGame = {

client/src/hooks/useMember.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ type ApiMember = {
77
about: string;
88
pronouns?: string;
99
profile_picture?: string;
10+
social_media?: {
11+
link: string;
12+
socialMediaUserName: string;
13+
}[];
1014
};
1115

1216
// return api member, import id number from router, is not enabled if not a number type

client/src/pages/games/[id].tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Image from "next/image";
22
import { useRouter } from "next/router";
33
import React from "react";
4+
import { SocialIcon } from "react-social-icons";
45

56
import { GameEmbed } from "@/components/ui/GameEmbed";
67
import { ItchEmbed } from "@/components/ui/ItchEmbed";
@@ -122,17 +123,29 @@ export default function IndividualGamePage() {
122123
Contributors
123124
</td>
124125
<td className="py-1 text-right sm:py-2">
125-
<div className="grid grid-cols-[auto_auto] gap-x-1 gap-y-1">
126+
<div className="flex flex-col gap-y-1">
126127
{game.contributors.map((c) => (
127-
<React.Fragment key={c.member_id}>
128+
<div
129+
key={c.member_id}
130+
className="flex items-center gap-x-2"
131+
>
128132
<a
129-
href={`/member/${c.member_id}`}
133+
href={`/members/${c.member_id}`}
130134
className="text-primary hover:underline"
131135
>
132136
{c.name}
133137
</a>
134-
<span>{c.role}</span>
135-
</React.Fragment>
138+
{Array.isArray(c.social_media) &&
139+
c.social_media.map((sm) => (
140+
<SocialIcon
141+
key={sm.link}
142+
url={sm.link}
143+
style={{ height: 24, width: 24 }}
144+
title={sm.socialMediaUserName}
145+
/>
146+
))}
147+
<span className="ml-auto">{c.role}</span>
148+
</div>
136149
))}
137150
</div>
138151
</td>

client/src/pages/games/index.tsx

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -125,28 +125,20 @@ export default function HomePage() {
125125
<span className="font-semibold text-foreground">
126126
{contributor.name}
127127
</span>
128-
<span
129-
className="text-foreground"
130-
style={{ marginLeft: 20 }}
131-
>
132-
- {contributor.role}
133-
</span>
134-
{/* Social icons placeholder */}
135-
{/* TODO: Add actual links */}
128+
{/* Social icons from API */}
136129
<span className="flex gap-2 text-primary">
137-
{/* Social icons using react-social-icons */}
138-
<SocialIcon
139-
url="https://facebook.com/"
140-
style={{ height: 24, width: 24 }}
141-
/>
142-
<SocialIcon
143-
url="https://instagram.com/"
144-
style={{ height: 24, width: 24 }}
145-
/>
146-
<SocialIcon
147-
url="https://github.com/"
148-
style={{ height: 24, width: 24 }}
149-
/>
130+
{Array.isArray(contributor.social_media) &&
131+
contributor.social_media.map((sm) => (
132+
<SocialIcon
133+
key={sm.link}
134+
url={sm.link}
135+
style={{ height: 24, width: 24 }}
136+
title={sm.socialMediaUserName}
137+
/>
138+
))}
139+
</span>
140+
<span className="ml-auto text-foreground">
141+
- {contributor.role}
150142
</span>
151143
</li>
152144
))}

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)