-
Notifications
You must be signed in to change notification settings - Fork 3
Issue 30 events page #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
93d830f
530d5da
cd67f0a
7b36317
b1871ad
8766697
8bc5d94
9000431
44b8482
7b13add
8edadea
38d4927
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { useQuery } from "@tanstack/react-query"; | ||
| import { AxiosError } from "axios"; | ||
|
|
||
| import api from "@/lib/api"; | ||
|
|
||
| type ApiEvent = { | ||
| id: number; | ||
| name: string; | ||
| description: string; | ||
| publicationDate: string; | ||
| date: string; | ||
| startTime: string | null; | ||
| location: string; | ||
| cover_image: string | null; | ||
| }; | ||
|
|
||
| type UiEvent = Omit<ApiEvent, "cover_image"> & { | ||
| coverImage: string; | ||
| }; | ||
|
|
||
| function transformApiEventToUiEvent(data: ApiEvent): UiEvent { | ||
| return { | ||
| ...data, | ||
| coverImage: data.cover_image ?? "/game_dev_club_logo.svg", | ||
| }; | ||
| } | ||
|
|
||
| export type EventTypeFilter = "past" | "upcoming"; | ||
|
|
||
| export function useEvents(type?: EventTypeFilter) { | ||
| return useQuery<ApiEvent[], AxiosError, UiEvent[]>({ | ||
| queryKey: ["events", type ?? "all"], | ||
| queryFn: async () => { | ||
| const response = await api.get<ApiEvent[]>("/events/", { | ||
| params: type ? { type } : {}, | ||
| }); | ||
| return response.data; | ||
| }, | ||
| select: (data) => data.map(transformApiEventToUiEvent), | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| import Image from "next/image"; | ||
| import Link from "next/link"; | ||
| import { useRouter } from "next/router"; | ||
|
|
||
| import { EventTypeFilter, useEvents } from "@/hooks/useEvents"; | ||
|
|
||
| function formatDateTimeLine(dateString: string): string { | ||
| try { | ||
| const date = new Date(dateString); | ||
|
|
||
| const d = new Intl.DateTimeFormat("en-US", { | ||
| month: "long", | ||
| day: "numeric", | ||
| year: "numeric", | ||
| }).format(date); | ||
|
|
||
| const t = new Intl.DateTimeFormat("en-US", { | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| hour12: true, | ||
| }) | ||
| .format(date) | ||
| .replace("AM", "am") | ||
| .replace("PM", "pm"); | ||
|
|
||
| return `${d} ・ ${t}`; | ||
| } catch { | ||
| return ""; | ||
| } | ||
| } | ||
|
|
||
| type EventsByYear<T> = Record<number, T[]>; | ||
|
|
||
| function groupEventsByYear<T extends { date: string }>( | ||
| events: T[], | ||
| ): EventsByYear<T> { | ||
| return events.reduce((acc, event) => { | ||
| const year = new Date(event.date).getFullYear(); | ||
| if (!acc[year]) acc[year] = []; | ||
| acc[year].push(event); | ||
| return acc; | ||
| }, {} as EventsByYear<T>); | ||
| } | ||
|
|
||
| export default function EventsPage() { | ||
| const router = useRouter(); | ||
|
|
||
| const typeParam = router.query.type; | ||
| const type = | ||
| typeof typeParam === "string" && | ||
| (typeParam === "past" || typeParam === "upcoming") | ||
| ? (typeParam as EventTypeFilter) | ||
| : undefined; | ||
|
|
||
| const { | ||
| data: events, | ||
| isPending, | ||
| isError, | ||
| } = useEvents(router.isReady ? type : undefined); | ||
|
|
||
| if (isPending) { | ||
| return ( | ||
| <main className="mx-auto min-h-dvh max-w-6xl px-6 py-16 md:px-20"> | ||
| <p>Loading events...</p> | ||
| </main> | ||
| ); | ||
| } | ||
|
|
||
| if (isError) { | ||
| return ( | ||
| <main className="md:20 mx-auto min-h-dvh max-w-6xl px-6 py-16"> | ||
| <p className="text-red-500" role="alert"> | ||
| Failed to load events. | ||
| </p> | ||
| </main> | ||
| ); | ||
| } | ||
|
|
||
| if (!events || events.length === 0) { | ||
| return ( | ||
| <main className="mx-auto min-h-dvh max-w-6xl px-6 py-16 md:px-20"> | ||
| <h1 className="mb-8 font-jersey10 text-4xl text-primary">Events</h1> | ||
| <p>No events available.</p> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be a good idea to still have the toggle displayed here even if no events are available. This way users can easily flip the toggle to see events instead of having to navigate back to access the toggle |
||
| </main> | ||
| ); | ||
| } | ||
|
|
||
| const eventsByYear = groupEventsByYear(events); | ||
| const sortedYears = Object.keys(eventsByYear) | ||
| .map(Number) | ||
| .sort((a, b) => b - a); | ||
|
|
||
| return ( | ||
| <main className="mx-auto min-h-dvh max-w-6xl px-6 py-16 md:px-20"> | ||
| <h1 className="mb-8 font-jersey10 text-4xl text-primary">Events</h1> | ||
|
|
||
| <div className="mb-10 flex w-fit overflow-hidden rounded-md border border-gray-600"> | ||
| <button | ||
| type="button" | ||
| onClick={() => router.push("/events?type=past")} | ||
| className={`px-6 py-2 text-sm font-medium transition-colors ${ | ||
| type === "past" | ||
| ? "bg-white text-black" | ||
| : "bg-transparent text-gray-300 hover:bg-gray-700" | ||
| }`} | ||
| > | ||
| Past | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={() => router.push("/events?type=upcoming")} | ||
| className={`px-6 py-2 text-sm font-medium transition-colors ${ | ||
| type === "upcoming" | ||
| ? "bg-white text-black" | ||
| : "bg-transparent text-gray-300 hover:bg-gray-700" | ||
| }`} | ||
| > | ||
| Upcoming | ||
| </button> | ||
| </div> | ||
|
|
||
| <div className="flex flex-col gap-14"> | ||
| {sortedYears.map((year) => ( | ||
| <section key={year}> | ||
| <div className="flex gap-6 md:gap-10"> | ||
| <div className="relative w-14 flex-shrink-0 md:w-20"> | ||
| <div className="text-2xl font-semibold text-gray-200 md:text-3xl"> | ||
| {year} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this was supposed to be in monospace according to the figma |
||
| </div> | ||
| <div | ||
| aria-hidden="true" | ||
| className="absolute bottom-0 left-2 top-12 w-px bg-gray-600/60 md:left-4" | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="flex min-w-0 flex-1 flex-col gap-6"> | ||
| {eventsByYear[year].map((event) => ( | ||
| <Link | ||
| key={event.id} | ||
| href={`/events/${event.id}`} | ||
| className="group block overflow-hidden rounded-xl border border-indigo-300/30 bg-gray-950/30 shadow-[0_0_0_1px_rgba(99,102,241,0.10)] transition-colors hover:bg-gray-950/45" | ||
| > | ||
| <div className="flex flex-col md:flex-row"> | ||
| <div className="flex min-w-0 flex-1 flex-col gap-4 px-8 py-7"> | ||
| <h3 className="min-w-0 font-jersey10 text-4xl text-white md:text-5xl"> | ||
| <span className="block truncate">{event.name}</span> | ||
| </h3> | ||
|
|
||
| <div className="space-y-1 text-sm md:text-base"> | ||
| <div className="text-primary"> | ||
| {formatDateTimeLine(event.date)} | ||
| </div> | ||
| <div className="text-primary">{event.location}</div> | ||
| </div> | ||
|
|
||
| <p className="max-w-3xl text-sm leading-relaxed text-gray-200/90 md:text-base"> | ||
| {event.description} | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className="relative h-56 w-full flex-shrink-0 border-t border-indigo-300/20 md:h-auto md:w-80 md:border-l md:border-t-0"> | ||
| <Image | ||
| src={event.coverImage} | ||
| alt={`Cover image for ${event.name}`} | ||
| fill | ||
| className="object-cover transition-transform duration-300 group-hover:scale-105" | ||
| onError={(e) => { | ||
| e.currentTarget.src = "/game_dev_club_logo.svg"; | ||
| }} | ||
| /> | ||
| </div> | ||
| </div> | ||
| </Link> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </section> | ||
| ))} | ||
| </div> | ||
| </main> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| from django.urls import path | ||
| from .views import EventDetailAPIView | ||
| from .views import EventListAPIView, EventDetailAPIView | ||
|
|
||
| urlpatterns = [ | ||
| path("events/", EventListAPIView.as_view(), name="events-list"), | ||
| path("events/<int:id>/", EventDetailAPIView.as_view()), | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you please remove the z-50 here? James will fix it in a separate PR :)