Skip to content

Commit 562c4fd

Browse files
committed
Merge branch 'main' into issue-131-Upcoming_events_list_would_leak_unpublished_events
2 parents 1c47b44 + cfbbb5e commit 562c4fd

44 files changed

Lines changed: 460 additions & 215 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci-backend.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ jobs:
5858
env:
5959
EMAIL_PORT: 1025
6060
FRONTEND_URL: http://localhost:3000
61+
API_ALLOWED_HOSTS: localhost
6162
run: poetry run python manage.py migrate
6263

6364
- name: Run tests 🧪
@@ -66,6 +67,7 @@ jobs:
6667
JWT_SIGNING_KEY: NjMgNmYgNmQgNmQgNzUgNmUgNjkgNzQgNzkgNzMgNzAgNjkgNzIgNjkgNzQgNjYgNmYgNzUgNmUgNjQgNjEgNzQgNjkgNmYgNmU=
6768
EMAIL_PORT: 1025
6869
FRONTEND_URL: http://localhost:3000
70+
API_ALLOWED_HOSTS: localhost
6971
run: |
7072
poetry run python3 -m pip install coverage
7173
poetry run coverage run manage.py test
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Committee Members
2+
3+
Profiles of the Committee Members of the club that are displayed on the about page.
4+
5+
## Fields
6+
7+
**Id:** Required and unique field for the club member that is on the committee. It is an integer field that corresponds to the raw integer id of a row in the Member table, as a Foreign Key if you know databases.
8+
9+
**Role:** Required and unique field for the specific role in the committee that this member has. It is something known as an Enum (Enumeration), which has a discrete number of custom choices. You can choose from 'President', 'Vice President', 'Secretary', 'Treasurer', 'Marketing', 'Events OCM', 'Projects OCM', and 'Fresher Rep'. Since they must be unique, you can only have 8 objects in the Committee table at a time for now. This can definitely be changed in the future when the committee grows.
10+
11+
## Other Notes
12+
13+
Before making a Committee object in the Committee table, you must make a Member object for the person that's on the committee and then link it through the id (just to clarify)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## Games
2+
3+
Pages for games can be added and edited at the row 'Game' of the GAME_DEV section on the main admin page.
4+
5+
### Fields
6+
7+
**Name:** Required field for the game's name. A character field (includes letters, numbers and symbols) of maximum length 200 characters.
8+
9+
**Descripiton:** Required field for the game's description. A text field.
10+
11+
**Completion:** Required field for the game's completion. A multichoice option field with four options including:
12+
- "Work in progress (Unplayable)"
13+
- "Playable - In Development"
14+
- "Beta - Stable but not Final"
15+
- "Completed"
16+
17+
**Active:** Required field for if the game is continued to be being worked on. A boolean field.
18+
19+
**Host URL:** Optional field for the game's host URL. A URL field with a maximum length 2083 characters
20+
21+
**Itch Embed ID:** Optional field for game's embed. If the field is empty the game will display a custom embed ID at the bottom of the page. This value can be gotten from the itch.io page of the game, at the bottom of the page there is an embed button, clicking this will bring up a full embed, the only part needed is the 7 digit number after "https://itch.io/embed/".
22+
23+
**Thumbnail:** Required field for the game's thumbnail. This image is displayed on the game page in place of a game embed or displayed within the game embed before the play button is pressed. Must be an image file.
24+
25+
**Event:** Optional field for the event at which the game was created. Links the game to an event. Foreign key field for an event.
26+
27+
**Itch Game Embed ID:** Optional field for the game's game embed. This ID allows the web version of a game to be played inside the site. This value can be acquired in two ways, either by the developer or through looking in the page source. A developer can get the value by going to the distribution tab of their game and going to the embed game section this will bring up a full embed for the game and the only part needed is the 8 digit number after "https://html-classic.itch.zone/html/". By looking through the page source that link can also be found either in a div or an iframe on the page depending on if the game has been played. **This value is not attainable if there is no web version of the game. hosted on itch.**
28+
29+
**Itch Game Width:** Required field for the game's game embed. This value is gotten in a similar way to the Itch Game Embed, however for the developer it's the number after "width=" and in the page source is found after "data-width=".
30+
31+
**Itch Game Height:** Required field for the game's game embed. This value is gotten in a similar way to the Itch Game Embed, however for the developer it's the number after "height=" and in the page source is found after "data-height=".
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## About/Committee Page
2+
3+
Has a description of the club, it's aim's etc, along with a big feature photo, intended to be a group photo of the committee or a big event? Then below is a display of all the current committee members of the club, showing name, role and pronouns.
4+
5+
## Hardcoded content to be modified by committee members
6+
7+
The club description and feature photo are the only things hardcoded into the website's front-end. Starting from the root directory of this website's source, this code to modify can be found in /client/src/pages/about.tsx
8+
9+
## Other Notes
10+
11+
The Committee Members are always displayed in the same order on the about page, which is the order mentioned in admin-dashboard/committee.md. If a certain Committee object can't be retreived, a placeholder Committee Member portrait will be displayed to maintain the same order.

client/src/components/main/MemberProfile.tsx

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
"use client";
22

3+
import { Palette, Sparkles } from "lucide-react";
34
import Image from "next/image";
45
import { SocialIcon } from "react-social-icons";
56

6-
// unused atm, as the member isnt linked a project on the backend
7-
/* export type MemberProfileProject = {
8-
id: string;
9-
name: string;
10-
description?: string;
11-
href?: string;
12-
}; */
7+
import MemberProjectSection from "../ui/MemberProjectSection";
138

149
export type MemberProfileData = {
1510
name: string;
@@ -25,7 +20,6 @@ export type MemberProfileData = {
2520

2621
type MemberProfileProps = {
2722
member: MemberProfileData;
28-
//projects?: MemberProfileProject[];
2923
};
3024

3125
function initialsFromName(name: string) {
@@ -55,7 +49,7 @@ export function MemberProfile({ member }: MemberProfileProps) {
5549
/>
5650
) : (
5751
<div className="flex h-full w-full items-center justify-center font-jersey10 text-5xl text-muted-foreground">
58-
{initials}
52+
<p className="mb-2"> {initials} </p>
5953
</div>
6054
)}
6155
</div>
@@ -107,23 +101,16 @@ export function MemberProfile({ member }: MemberProfileProps) {
107101
</div>
108102
</div>
109103
</div>
110-
{/* Template for Projects section */}
111-
<div className="m-auto min-h-80 w-11/12">
112-
<h2 className="mt-7 text-center font-jersey10 text-5xl">Projects</h2>
113-
<div className="m-auto my-5 flex flex-wrap justify-center gap-8">
114-
{/* Div below is a single project card */}
115-
<div className="w-fit rounded-md p-5">
116-
<div className="mb-2 h-44 w-96 overflow-clip rounded-md p-5 text-neutral_1">
117-
{/* Image and/or Link to Project */}
118-
</div>
119-
<p className="max-w-96 font-firaCode text-xl font-semibold">
120-
{/* Project Title */}
121-
</p>
122-
<p className="line-clamp-1 max-w-96 font-firaCode text-[--light-3]">
123-
{/* Project description */}
124-
</p>
125-
</div>
126-
</div>
104+
<div className="m-auto mb-10 min-h-80 w-11/12">
105+
<h2 className="mt-7 flex justify-center text-center font-jersey10 text-5xl">
106+
Games
107+
<Sparkles size={32} className="ml-2 self-center text-yellow-300" />
108+
</h2>
109+
<MemberProjectSection id={window.location.pathname.slice(9)} />
110+
<h2 className="mt-7 flex justify-center text-center font-jersey10 text-5xl">
111+
Artwork
112+
<Palette size={32} className="ml-2 self-center text-yellow-300" />
113+
</h2>
127114
</div>
128115
</>
129116
);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export type EventDateParts = {
2+
weekday: string;
3+
day: string;
4+
month: string;
5+
time: string;
6+
};
7+
8+
/** Parses a date string and returns parts for separate styling. Returns null on invalid input. */
9+
export function getEventDateParts(dateString: string): EventDateParts | null {
10+
try {
11+
const date = new Date(dateString);
12+
const weekday = new Intl.DateTimeFormat("en-US", {
13+
weekday: "long",
14+
}).format(date);
15+
const day = new Intl.DateTimeFormat("en-US", { day: "numeric" }).format(
16+
date,
17+
);
18+
const month = new Intl.DateTimeFormat("en-US", { month: "short" }).format(
19+
date,
20+
);
21+
const time = new Intl.DateTimeFormat("en-US", {
22+
hour: "2-digit",
23+
minute: "2-digit",
24+
hour12: true,
25+
})
26+
.format(date)
27+
.replace("AM", "am")
28+
.replace("PM", "pm");
29+
return { weekday, day, month, time };
30+
} catch {
31+
return null;
32+
}
33+
}
34+
35+
type EventDateDisplayProps = { date: string };
36+
37+
/** Renders event date as: weekday・ day month・ time. */
38+
export function EventDateDisplay({ date }: EventDateDisplayProps) {
39+
const parts = getEventDateParts(date);
40+
if (!parts) return null;
41+
return (
42+
<div className="flex flex-wrap items-baseline gap-x-1">
43+
<span className="whitespace-nowrap text-primary">
44+
{parts.weekday}
45+
{"・"}
46+
</span>
47+
<span className="whitespace-nowrap text-primary">
48+
{parts.day} {parts.month}
49+
{"・"}
50+
</span>
51+
<span className="text-secondary">{parts.time}</span>
52+
</div>
53+
);
54+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ArrowUpRight } from "lucide-react";
2+
import Image from "next/image";
3+
import Link from "next/link";
4+
import React from "react";
5+
6+
import { useContributor } from "@/hooks/useContributor";
7+
8+
type MemberProjectSectionProps = {
9+
id: string;
10+
};
11+
12+
export default function MemberProjectSection(props: MemberProjectSectionProps) {
13+
const { data: games, isError, error } = useContributor(props.id);
14+
15+
{
16+
/* Error handling from Games Showcase page */
17+
}
18+
if (isError) {
19+
const errorMessage =
20+
error?.response?.status === 404
21+
? "Games not found."
22+
: "Failed to Load Games";
23+
return (
24+
<div className="mx-auto min-h-screen max-w-7xl px-6 py-16">
25+
<p
26+
className="my-10 text-center font-firaCode text-lg text-red-500"
27+
role="alert"
28+
>
29+
{errorMessage}
30+
</p>
31+
</div>
32+
);
33+
}
34+
35+
return (
36+
<div className="mb-12">
37+
{!games || games.length === 0 ? (
38+
<p className="my-10 text-center font-firaCode text-lg text-[--light-3]">
39+
No games available.
40+
</p>
41+
) : (
42+
<div className="m-auto my-5 flex flex-wrap justify-center gap-8">
43+
{games.map((game) => (
44+
<React.Fragment key={game.game_id}>
45+
<div className="w-fit rounded-md p-5">
46+
<div className="group mb-2 grid h-44 w-96 grid-cols-1 grid-rows-1 overflow-clip rounded-md">
47+
<Image
48+
src={game.game_data.thumbnail}
49+
alt={`${game.game_data.name} cover image`}
50+
width={384}
51+
height={176}
52+
className="group-hover:brightness-75 group-hover:duration-200"
53+
/>
54+
<Link
55+
className="mb-16 hidden justify-self-center rounded-md bg-accent p-3 font-firaCode text-light_1 drop-shadow-md hover:underline group-hover:flex group-hover:blur-0 group-hover:duration-200"
56+
href="#"
57+
onClick={() => window.open(`/games/${game.game_id}`)}
58+
>
59+
Visit Game <ArrowUpRight className="ml-1" />
60+
</Link>
61+
</div>
62+
<p className="max-w-96 font-firaCode text-xl font-semibold">
63+
{game.game_data.name}
64+
</p>
65+
<p className="line-clamp-1 max-w-96 font-firaCode text-[--light-3]">
66+
{game.game_data.description}
67+
</p>
68+
</div>
69+
</React.Fragment>
70+
))}
71+
</div>
72+
)}
73+
</div>
74+
);
75+
}

client/src/components/ui/eventCarousel.tsx

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,14 @@ import { useEffect, useRef, useState } from "react";
66
import { UiEvent as EventType } from "@/hooks/useEvents";
77

88
import { Button } from "./button";
9+
import { EventDateDisplay } from "./EventDateDisplay";
910

1011
type EventCarouselProps = {
1112
items: EventType[];
1213
};
1314

1415
const GAP = 40;
1516

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-
4217
export default function EventCarousel({ items }: EventCarouselProps) {
4318
const viewportRef = useRef<HTMLDivElement>(null);
4419
const firstItemRef = useRef<HTMLAnchorElement>(null);
@@ -76,16 +51,15 @@ export default function EventCarousel({ items }: EventCarouselProps) {
7651

7752
useEffect(() => {
7853
const updateVisibleCount = () => {
79-
if (window.innerWidth < 768) {
80-
setVisibleCount(1);
81-
} else {
82-
setVisibleCount(3);
83-
}
54+
const newVisibleCount = window.innerWidth < 768 ? 1 : 3;
55+
const newMaxIndex = Math.max(0, items.length - newVisibleCount);
56+
setVisibleCount(newVisibleCount);
57+
setCurrentIndex((prev) => Math.min(prev, newMaxIndex));
8458
};
8559
updateVisibleCount();
8660
window.addEventListener("resize", updateVisibleCount);
8761
return () => window.removeEventListener("resize", updateVisibleCount);
88-
}, []);
62+
}, [items.length]);
8963

9064
return (
9165
<div className="container mx-auto rounded-lg bg-primary-foreground px-4 py-8 lg:px-12">
@@ -124,7 +98,7 @@ export default function EventCarousel({ items }: EventCarouselProps) {
12498
)}
12599

126100
<div className="mt-10 px-10">
127-
<div ref={viewportRef} className="overflow-hidden">
101+
<div ref={viewportRef} className="overflow-hidden px-2 md:px-4">
128102
<div
129103
className="flex transition-transform duration-300 ease-out"
130104
style={{
@@ -137,7 +111,7 @@ export default function EventCarousel({ items }: EventCarouselProps) {
137111
href={`/events/${event.id}`}
138112
key={event.id}
139113
ref={index === 0 ? firstItemRef : undefined}
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" : ""}`}
114+
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 === currentIndex ? "origin-left" : ""}`}
141115
>
142116
<div className="relative aspect-[16/9] w-full overflow-hidden rounded-lg">
143117
<Image
@@ -152,8 +126,8 @@ export default function EventCarousel({ items }: EventCarouselProps) {
152126
{event.name}
153127
</h3>
154128

155-
<p className="mb-4 text-sm text-primary">
156-
{formatEventDateDisplay(event.date)}
129+
<p className="mb-4 text-base text-primary">
130+
<EventDateDisplay date={event.date} />
157131
</p>
158132

159133
<div className="mt-3 w-full border-b border-white/20" />

client/src/hooks/useContributor.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { AxiosError } from "axios";
3+
4+
import api from "@/lib/api";
5+
6+
type ApiContributorGameData = {
7+
name: string;
8+
thumbnail: string;
9+
description: string;
10+
};
11+
12+
type ApiContributorGamesList = {
13+
game_id: number;
14+
role: string;
15+
game_data: ApiContributorGameData;
16+
};
17+
18+
export const useContributor = (member: string | string[] | undefined) => {
19+
return useQuery<ApiContributorGamesList[], AxiosError>({
20+
queryKey: ["contributor", member],
21+
queryFn: async () => {
22+
const response = await api.get(`/games/contributor/${member}/`);
23+
return response.data;
24+
},
25+
enabled: !!member,
26+
});
27+
};

0 commit comments

Comments
 (0)