From 6f0b005d8cb16acfba5308232d6023638889bece Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Tue, 22 Oct 2024 20:38:26 +0200 Subject: [PATCH 01/29] feat(api): add archive endpoint --- Cargo.lock | 1 + Cargo.toml | 2 +- crates/api/Cargo.toml | 1 + crates/api/src/endpoints/archive.rs | 91 +++++++++++++++++++++++++++++ crates/api/src/main.rs | 7 ++- 5 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 crates/api/src/endpoints/archive.rs diff --git a/Cargo.lock b/Cargo.lock index 27f5235f..86c17a08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,6 +78,7 @@ dependencies = [ "regex", "reqwest", "serde", + "serde_json", "tokio", "tracing", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 60791229..e920ee31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ axum = { version = "0.7.5", features = ["http2"] } tower-http = { version = "0.5.2", features = ["cors"] } tower_governor = "0.4.2" tokio-tungstenite = { version = "0.23.1", features = ["native-tls", "url"] } -reqwest = { version = "0.12.4", features = ["native-tls"] } +reqwest = { version = "0.12.4", features = ["native-tls", "json"] } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["raw_value"] } diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 8e4d0533..5d96b082 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -18,6 +18,7 @@ tracing.workspace = true tracing-subscriber.workspace = true serde.workspace = true +serde_json.workspace = true regex.workspace = true reqwest.workspace = true diff --git a/crates/api/src/endpoints/archive.rs b/crates/api/src/endpoints/archive.rs new file mode 100644 index 00000000..257ba775 --- /dev/null +++ b/crates/api/src/endpoints/archive.rs @@ -0,0 +1,91 @@ +use std::any; + +use anyhow::Error; +use axum::{extract::Path, http::status}; +use chrono::Datelike; +use reqwest::{get, StatusCode}; +use serde::{Deserialize, Serialize}; +use serde_json; +use tracing::{debug, error, trace}; + +use super::schedule::{Round, Session}; + +// A meeting represents a full race or testing weekend. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all(serialize = "camelCase", deserialize = "PascalCase"))] +pub struct Meeting { + key: u32, + code: String, + number: u8, + location: String, + official_name: String, + name: String, + // i think the name as string should suffice, right?... + country: Country, + // might not need circuit? + // circuit: String, + sessions: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all(serialize = "camelCase", deserialize = "PascalCase"))] +struct Country { + key: i32, + code: String, + name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all(serialize = "camelCase", deserialize = "PascalCase"))] +pub struct MeetingResponse { + year: u16, + meetings: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all(serialize = "camelCase", deserialize = "PascalCase"))] +pub struct RaceSession { + key: i32, + r#type: String, + #[serde(default)] + name: String, + #[serde(default)] + path: String, +} + +pub async fn get_sessions_for_year( + Path(year): Path, +) -> Result>, axum::http::StatusCode> { + let url = format!("https://livetiming.formula1.com/static/{year}/Index.json"); + + let result = reqwest::get(url).await; + let res = match result { + Ok(res) => res, + Err(err) => { + error!("Error fetching {} sessions: {}", year, err); + return Err(axum::http::StatusCode::BAD_GATEWAY); + } + }; + + let text = match res.status() { + axum::http::StatusCode::OK => res.text().await.unwrap_or(String::new()), + status => { + return Err(status); + } + }; + + let json = serde_json::from_str::(&text); + + match json { + Ok(mut json) => { + for (_, el) in json.meetings.iter_mut().enumerate() { + el.sessions.retain(|s| s.key != -1); + } + Ok(axum::Json(json.meetings)) + } + Err(err) => { + error!("{}", err); + Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR) + } + } +} diff --git a/crates/api/src/main.rs b/crates/api/src/main.rs index e723ec39..a4a962dc 100644 --- a/crates/api/src/main.rs +++ b/crates/api/src/main.rs @@ -6,6 +6,7 @@ use tracing::level_filters::LevelFilter; use env; mod endpoints { + pub(crate) mod archive; pub(crate) mod health; pub(crate) mod schedule; } @@ -18,7 +19,11 @@ async fn main() { let app = Router::new() .route("/api/schedule", get(endpoints::schedule::get)) .route("/api/schedule/next", get(endpoints::schedule::get_next)) - .route("/api/health", get(endpoints::health::check)); + .route("/api/health", get(endpoints::health::check)) + .route( + "/api/archive/:year", + get(endpoints::archive::get_sessions_for_year), + ); let addr = addr(); From 0cc05509e1172cef05e1a28aab075c49decde396 Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Wed, 23 Oct 2024 20:26:47 +0200 Subject: [PATCH 02/29] chore(api): remove unused --- crates/api/src/endpoints/archive.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/crates/api/src/endpoints/archive.rs b/crates/api/src/endpoints/archive.rs index 257ba775..a7c3bb11 100644 --- a/crates/api/src/endpoints/archive.rs +++ b/crates/api/src/endpoints/archive.rs @@ -1,14 +1,7 @@ -use std::any; - -use anyhow::Error; -use axum::{extract::Path, http::status}; -use chrono::Datelike; -use reqwest::{get, StatusCode}; +use axum::extract::Path; use serde::{Deserialize, Serialize}; use serde_json; -use tracing::{debug, error, trace}; - -use super::schedule::{Round, Session}; +use tracing::error; // A meeting represents a full race or testing weekend. #[derive(Serialize, Deserialize, Debug, Clone)] From f2907578cce5a4fea426f5f3010e3ed3368cd6de Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Wed, 23 Oct 2024 20:27:04 +0200 Subject: [PATCH 03/29] feat(dash): add archive page --- dash/src/app/(nav)/archive/page.tsx | 37 +++++++++++++++++++++++++++++ dash/src/components/Menubar.tsx | 5 ++++ 2 files changed, 42 insertions(+) create mode 100644 dash/src/app/(nav)/archive/page.tsx diff --git a/dash/src/app/(nav)/archive/page.tsx b/dash/src/app/(nav)/archive/page.tsx new file mode 100644 index 00000000..1c54c975 --- /dev/null +++ b/dash/src/app/(nav)/archive/page.tsx @@ -0,0 +1,37 @@ +import { env } from "@/env.mjs"; + +type Meeting = { + officialName: string; +}; +const getArchiveForYear = async (year: string): Promise => { + try { + const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { + next: { revalidate: 60 * 60 * 4 }, + }); + const schedule: Meeting[] = await nextReq.json(); + return schedule; + } catch (e) { + return null; + } +}; + +export default async function ArchivePage({ + searchParams, +}: { + searchParams: { [key: string]: string | string[] | undefined }; +}) { + let year = searchParams["year"]; + //const searchParams = useSearchParams(); + //let year = searchParams.get("year"); + const currentYear = new Date(Date.now()).getFullYear().toString(); + if (year == null || year < "2017" || year > currentYear || typeof year !== "string") { + year = currentYear; + } + const archive = await getArchiveForYear(year); + return ( +
+

{archive?.length}

+
    {archive?.map((meet) =>
  • {meet.officialName}
  • )}
+
+ ); +} diff --git a/dash/src/components/Menubar.tsx b/dash/src/components/Menubar.tsx index e34ac333..a0a589a5 100644 --- a/dash/src/components/Menubar.tsx +++ b/dash/src/components/Menubar.tsx @@ -46,6 +46,8 @@ export default function Menubar() { router.prefetch("/"); router.prefetch("/dashboard"); router.prefetch("/schedule"); + // should we prefetch archive? + router.prefetch("/archive"); router.prefetch("/settings"); router.prefetch("/help"); }, []); @@ -62,6 +64,9 @@ export default function Menubar() { liveTimingGuard("/schedule")}> Schedule + liveTimingGuard("/archive")}> + Archive + liveTimingGuard("/settings")}> Settings From 48ead2ae5b18126a8f9e4fdfcbf155290e9979bd Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 3 Nov 2024 19:04:46 +0100 Subject: [PATCH 04/29] feat: wip on the display --- dash/src/app/(nav)/archive/page.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/dash/src/app/(nav)/archive/page.tsx b/dash/src/app/(nav)/archive/page.tsx index 1c54c975..d41097ef 100644 --- a/dash/src/app/(nav)/archive/page.tsx +++ b/dash/src/app/(nav)/archive/page.tsx @@ -1,7 +1,14 @@ import { env } from "@/env.mjs"; type Meeting = { + key: number; + location: string; officialName: string; + name: string; + country: { + name: string; + }; + sessions: {}; }; const getArchiveForYear = async (year: string): Promise => { try { @@ -31,7 +38,16 @@ export default async function ArchivePage({ return (

{archive?.length}

-
    {archive?.map((meet) =>
  • {meet.officialName}
  • )}
+
    + {archive?.map((meet) => ( +
  • +
    +
    {meet.officialName}
    +
    {meet.country.name}
    +
    +
  • + ))} +
); } From 22d11c5e367e566d1efe32ff0a9c2e0501276a0f Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 3 Nov 2024 19:48:19 +0100 Subject: [PATCH 05/29] feat: finish design draft + add start and end date of event --- crates/api/src/endpoints/archive.rs | 4 +++ dash/src/app/(nav)/archive/page.tsx | 50 ++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/crates/api/src/endpoints/archive.rs b/crates/api/src/endpoints/archive.rs index a7c3bb11..18bf9b16 100644 --- a/crates/api/src/endpoints/archive.rs +++ b/crates/api/src/endpoints/archive.rs @@ -44,6 +44,10 @@ pub struct RaceSession { name: String, #[serde(default)] path: String, + #[serde(default)] + start_date: String, + #[serde(default)] + end_date: String, } pub async fn get_sessions_for_year( diff --git a/dash/src/app/(nav)/archive/page.tsx b/dash/src/app/(nav)/archive/page.tsx index d41097ef..7cff79b2 100644 --- a/dash/src/app/(nav)/archive/page.tsx +++ b/dash/src/app/(nav)/archive/page.tsx @@ -1,4 +1,6 @@ import { env } from "@/env.mjs"; +import { utc } from "moment"; +import Footer from "@/components/Footer"; // Adjust the import path as necessary type Meeting = { key: number; @@ -8,8 +10,12 @@ type Meeting = { country: { name: string; }; - sessions: {}; + sessions: { + startDate: string; + endDate: string; + }[]; }; + const getArchiveForYear = async (year: string): Promise => { try { const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { @@ -28,26 +34,48 @@ export default async function ArchivePage({ searchParams: { [key: string]: string | string[] | undefined }; }) { let year = searchParams["year"]; - //const searchParams = useSearchParams(); - //let year = searchParams.get("year"); const currentYear = new Date(Date.now()).getFullYear().toString(); if (year == null || year < "2017" || year > currentYear || typeof year !== "string") { year = currentYear; } const archive = await getArchiveForYear(year); + + if (!archive) { + return ( +
+

No archive data found for {year}

+
+ ); + } + return ( -
-

{archive?.length}

-
    - {archive?.map((meet) => ( -
  • -
    -
    {meet.officialName}
    -
    {meet.country.name}
    +
    +
    +

    Archive for {year}

    +

    All times are local time

    +
    +
      + {archive.map((meet) => ( +
    • +
      +
      +

      {meet.officialName}

      +

      {meet.country.name}

      +

      {meet.location}

      +
      +
      +

      + {utc(meet.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} + {utc(meet.sessions[meet.sessions.length - 1].endDate) + .local() + .format("MMMM D, YYYY")} +

      +
    • ))}
    +
    ); } From 21ab74d0f21f698e5efed9efeffab81787fc7f29 Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Mon, 4 Nov 2024 20:11:59 +0100 Subject: [PATCH 06/29] feat: add year picker --- dash/src/app/(nav)/archive/page.tsx | 72 ++++++++++++++------------ dash/src/components/SegmentedLinks.tsx | 48 +++++++++++++++++ 2 files changed, 88 insertions(+), 32 deletions(-) create mode 100644 dash/src/components/SegmentedLinks.tsx diff --git a/dash/src/app/(nav)/archive/page.tsx b/dash/src/app/(nav)/archive/page.tsx index 7cff79b2..1077c978 100644 --- a/dash/src/app/(nav)/archive/page.tsx +++ b/dash/src/app/(nav)/archive/page.tsx @@ -1,6 +1,7 @@ import { env } from "@/env.mjs"; import { utc } from "moment"; import Footer from "@/components/Footer"; // Adjust the import path as necessary +import SegmentedLinks from "@/components/SegmentedLinks"; type Meeting = { key: number; @@ -33,48 +34,55 @@ export default async function ArchivePage({ }: { searchParams: { [key: string]: string | string[] | undefined }; }) { + const currentYear = new Date(Date.now()).getFullYear(); let year = searchParams["year"]; - const currentYear = new Date(Date.now()).getFullYear().toString(); - if (year == null || year < "2017" || year > currentYear || typeof year !== "string") { - year = currentYear; + if (year == null || year < "2017" || year > currentYear.toString() || typeof year !== "string") { + year = currentYear.toString(); } + const archive = await getArchiveForYear(year); - if (!archive) { - return ( -
    -

    No archive data found for {year}

    -
    - ); + const years = []; + for (let i = 2017; i <= currentYear; i++) { + years.push({ label: i.toString(), href: `?year=${i.toString()}` }); } return (
    -
    +

    Archive for {year}

    -

    All times are local time

    +
    -
      - {archive.map((meet) => ( -
    • -
      -
      -

      {meet.officialName}

      -

      {meet.country.name}

      -

      {meet.location}

      -
      -
      -

      - {utc(meet.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} - {utc(meet.sessions[meet.sessions.length - 1].endDate) - .local() - .format("MMMM D, YYYY")} -

      -
      -
      -
    • - ))} -
    + {!archive ? ( +
    +

    No archive data found for {year}

    +
    + ) : ( + <> +

    All times are local time

    +
      + {archive.map((meet) => ( +
    • +
      +
      +

      {meet.officialName}

      +

      {meet.country.name}

      +

      {meet.location}

      +
      +
      +

      + {utc(meet.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} + {utc(meet.sessions[meet.sessions.length - 1].endDate) + .local() + .format("MMMM D, YYYY")} +

      +
      +
      +
    • + ))} +
    + + )}
    ); diff --git a/dash/src/components/SegmentedLinks.tsx b/dash/src/components/SegmentedLinks.tsx new file mode 100644 index 00000000..055e9999 --- /dev/null +++ b/dash/src/components/SegmentedLinks.tsx @@ -0,0 +1,48 @@ +"use client"; + +import clsx from "clsx"; +import { LayoutGroup, motion } from "framer-motion"; +import Link from "next/link"; + +type Props = { + id?: string; + className?: string; + options: { + label: string; + href: string; + }[]; + selected: string; + onSelect?: (val: string) => void; +}; + +export default function SegmentedLinks({ id, className, options, selected, onSelect }: Props) { + return ( + +
    + {options.map((option, i) => { + const isActive = option.href === selected; + return ( + + + {isActive && ( + + )} + {option.label} + + + ); + })} +
    +
    + ); +} From 7839e99a1ac51d13f6c98ae66293b12f20f5318b Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Mon, 4 Nov 2024 20:14:11 +0100 Subject: [PATCH 07/29] fix: remove 2017 --- dash/src/app/(nav)/archive/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/src/app/(nav)/archive/page.tsx b/dash/src/app/(nav)/archive/page.tsx index 1077c978..bf78e7c9 100644 --- a/dash/src/app/(nav)/archive/page.tsx +++ b/dash/src/app/(nav)/archive/page.tsx @@ -36,14 +36,14 @@ export default async function ArchivePage({ }) { const currentYear = new Date(Date.now()).getFullYear(); let year = searchParams["year"]; - if (year == null || year < "2017" || year > currentYear.toString() || typeof year !== "string") { + if (year == null || year < "2018" || year > currentYear.toString() || typeof year !== "string") { year = currentYear.toString(); } const archive = await getArchiveForYear(year); const years = []; - for (let i = 2017; i <= currentYear; i++) { + for (let i = 2018; i <= currentYear; i++) { years.push({ label: i.toString(), href: `?year=${i.toString()}` }); } From 1d4b97937da169ed45c78f3643087373e4d4a7ab Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:12:09 +0100 Subject: [PATCH 08/29] feat(api): add caching --- crates/api/src/endpoints/archive.rs | 31 +++++++++++++---------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/crates/api/src/endpoints/archive.rs b/crates/api/src/endpoints/archive.rs index 18bf9b16..b8df67e7 100644 --- a/crates/api/src/endpoints/archive.rs +++ b/crates/api/src/endpoints/archive.rs @@ -1,4 +1,5 @@ use axum::extract::Path; +use cached::proc_macro::io_cached; use serde::{Deserialize, Serialize}; use serde_json; use tracing::error; @@ -50,26 +51,22 @@ pub struct RaceSession { end_date: String, } +#[io_cached( + map_error = r##"|e| anyhow::anyhow!(format!("disk cache error {:?}", e))"##, + disk = true, + time = 1800 +)] +pub async fn fetch_sessions_for_year(year: u32) -> Result { + let url = format!("https://livetiming.formula1.com/static/{year}/Index.json"); + let res = reqwest::get(url).await?; + let text = res.text().await?; + Ok(text) +} + pub async fn get_sessions_for_year( Path(year): Path, ) -> Result>, axum::http::StatusCode> { - let url = format!("https://livetiming.formula1.com/static/{year}/Index.json"); - - let result = reqwest::get(url).await; - let res = match result { - Ok(res) => res, - Err(err) => { - error!("Error fetching {} sessions: {}", year, err); - return Err(axum::http::StatusCode::BAD_GATEWAY); - } - }; - - let text = match res.status() { - axum::http::StatusCode::OK => res.text().await.unwrap_or(String::new()), - status => { - return Err(status); - } - }; + let text = fetch_sessions_for_year(year).await.unwrap(); let json = serde_json::from_str::(&text); From 57359895c24237817ea34b3916734fe53bd9946e Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:29:05 +0100 Subject: [PATCH 09/29] feat: add details page --- .../app/(nav)/archive/[year]/[key]/page.tsx | 71 +++++++++++++++ dash/src/app/(nav)/archive/[year]/layout.tsx | 33 +++++++ dash/src/app/(nav)/archive/[year]/page.tsx | 75 ++++++++++++++++ dash/src/app/(nav)/archive/page.tsx | 90 +------------------ dash/src/types/archive.type.ts | 15 ++++ 5 files changed, 197 insertions(+), 87 deletions(-) create mode 100644 dash/src/app/(nav)/archive/[year]/[key]/page.tsx create mode 100644 dash/src/app/(nav)/archive/[year]/layout.tsx create mode 100644 dash/src/app/(nav)/archive/[year]/page.tsx create mode 100644 dash/src/types/archive.type.ts diff --git a/dash/src/app/(nav)/archive/[year]/[key]/page.tsx b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx new file mode 100644 index 00000000..ff000e2d --- /dev/null +++ b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx @@ -0,0 +1,71 @@ +import Footer from "@/components/Footer"; // Adjust the import path as necessary +import { utc } from "moment"; +import { Meeting } from "@/types/archive.type"; +import Link from "next/link"; +import { env } from "@/env.mjs"; + +const getArchiveForYear = async (year: string): Promise => { + try { + const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { + next: { revalidate: 60 * 60 * 4 }, + }); + const schedule: Meeting[] = await nextReq.json(); + return schedule; + } catch (e) { + return null; + } +}; + +export default async function MeetingDetailsPage({ params }: { params: { key: string; year: string } }) { + const { key, year } = params; + const archive = await getArchiveForYear(year); + const meeting = archive?.find((meet) => meet.key.toString() === key); + + if (!meeting) { + return ( +
    +
    +

    No meeting details found for key: {key}

    +
    +
    +
    + ); + } + + return ( +
    + +
    ← Back to Year Overview
    + +
    +

    {meeting.officialName}

    +

    {meeting.country.name}

    +

    {meeting.location}

    +

    + {utc(meeting.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} + {utc(meeting.sessions[meeting.sessions.length - 1].endDate) + .local() + .format("MMMM D, YYYY")} +

    +
    +

    Sessions

    +
      + {meeting.sessions.map((session, index) => ( +
    • +

      {session.name}

      +

      + {utc(session.startDate).local().format("MMMM D, YYYY")} -{" "} + {utc(session.endDate).local().format("MMMM D, YYYY")} +

      +

      + {session.startDate} - {session.endDate} +

      +
    • + ))} +
    +
    +
    +
    +
    + ); +} diff --git a/dash/src/app/(nav)/archive/[year]/layout.tsx b/dash/src/app/(nav)/archive/[year]/layout.tsx new file mode 100644 index 00000000..552fc603 --- /dev/null +++ b/dash/src/app/(nav)/archive/[year]/layout.tsx @@ -0,0 +1,33 @@ +import { env } from "@/env.mjs"; +import { Meeting } from "@/types/archive.type"; +import { ReactNode } from "react"; + +const getArchiveForYear = async (year: string): Promise => { + try { + const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { + next: { revalidate: 60 * 60 * 4 }, + }); + const schedule: Meeting[] = await nextReq.json(); + return schedule; + } catch (e) { + return null; + } +}; + +export default async function ArchiveLayout({ + children, + params, +}: { + children: ReactNode; + params: Promise<{ year: string }>; +}) { + const currentYear = new Date(Date.now()).getFullYear(); + let year = (await params).year; + if (year == null || year < "2018" || year > currentYear.toString() || typeof year !== "string") { + year = currentYear.toString(); + } + + await getArchiveForYear(year); + + return
    {children}
    ; +} diff --git a/dash/src/app/(nav)/archive/[year]/page.tsx b/dash/src/app/(nav)/archive/[year]/page.tsx new file mode 100644 index 00000000..340cc671 --- /dev/null +++ b/dash/src/app/(nav)/archive/[year]/page.tsx @@ -0,0 +1,75 @@ +import { env } from "@/env.mjs"; +import Footer from "@/components/Footer"; // Adjust the import path as necessary +import SegmentedLinks from "@/components/SegmentedLinks"; +import { utc } from "moment"; +import Link from "next/link"; +import { Meeting } from "@/types/archive.type"; + +const getArchiveForYear = async (year: string): Promise => { + try { + const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { + next: { revalidate: 60 * 60 * 4 }, + }); + const schedule: Meeting[] = await nextReq.json(); + return schedule; + } catch (e) { + return null; + } +}; + +export default async function ArchivePage({ params }: { params: { year: string } }) { + const currentYear = new Date(Date.now()).getFullYear(); + let year = params.year; + if (year == null || year < "2018" || year > currentYear.toString() || typeof year !== "string") { + year = currentYear.toString(); + } + const archive = await getArchiveForYear(year); + + const years = []; + for (let i = 2018; i <= currentYear; i++) { + years.push({ label: i.toString(), href: `/archive/${i.toString()}` }); + } + + return ( +
    +
    +

    Archive for {year}

    + +
    + {!archive ? ( +
    +

    No archive data found for {year}

    +
    + ) : ( + <> +

    All times are local time

    +
      + {archive.map((meet) => ( +
    • +
      +
      +

      {meet.officialName}

      +

      {meet.country.name}

      +

      {meet.location}

      +
      +
      +

      + {utc(meet.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} + {utc(meet.sessions[meet.sessions.length - 1].endDate) + .local() + .format("MMMM D, YYYY")} +

      +
      + +
      View Details
      + +
      +
    • + ))} +
    + + )} +
    +
    + ); +} diff --git a/dash/src/app/(nav)/archive/page.tsx b/dash/src/app/(nav)/archive/page.tsx index bf78e7c9..c0058098 100644 --- a/dash/src/app/(nav)/archive/page.tsx +++ b/dash/src/app/(nav)/archive/page.tsx @@ -1,89 +1,5 @@ -import { env } from "@/env.mjs"; -import { utc } from "moment"; -import Footer from "@/components/Footer"; // Adjust the import path as necessary -import SegmentedLinks from "@/components/SegmentedLinks"; +import { redirect } from "next/navigation"; -type Meeting = { - key: number; - location: string; - officialName: string; - name: string; - country: { - name: string; - }; - sessions: { - startDate: string; - endDate: string; - }[]; -}; - -const getArchiveForYear = async (year: string): Promise => { - try { - const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { - next: { revalidate: 60 * 60 * 4 }, - }); - const schedule: Meeting[] = await nextReq.json(); - return schedule; - } catch (e) { - return null; - } -}; - -export default async function ArchivePage({ - searchParams, -}: { - searchParams: { [key: string]: string | string[] | undefined }; -}) { - const currentYear = new Date(Date.now()).getFullYear(); - let year = searchParams["year"]; - if (year == null || year < "2018" || year > currentYear.toString() || typeof year !== "string") { - year = currentYear.toString(); - } - - const archive = await getArchiveForYear(year); - - const years = []; - for (let i = 2018; i <= currentYear; i++) { - years.push({ label: i.toString(), href: `?year=${i.toString()}` }); - } - - return ( -
    -
    -

    Archive for {year}

    - -
    - {!archive ? ( -
    -

    No archive data found for {year}

    -
    - ) : ( - <> -

    All times are local time

    -
      - {archive.map((meet) => ( -
    • -
      -
      -

      {meet.officialName}

      -

      {meet.country.name}

      -

      {meet.location}

      -
      -
      -

      - {utc(meet.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} - {utc(meet.sessions[meet.sessions.length - 1].endDate) - .local() - .format("MMMM D, YYYY")} -

      -
      -
      -
    • - ))} -
    - - )} -
    -
    - ); +export default function ArchiveRedirectPage() { + redirect(`/archive/${new Date(Date.now()).getFullYear()}`); } diff --git a/dash/src/types/archive.type.ts b/dash/src/types/archive.type.ts new file mode 100644 index 00000000..e9ee4309 --- /dev/null +++ b/dash/src/types/archive.type.ts @@ -0,0 +1,15 @@ +export type Meeting = { + key: number; + location: string; + officialName: string; + name: string; + country: { + name: string; + }; + sessions: { + key: number; + name: string; + startDate: string; + endDate: string; + }[]; +}; From 8dcd367665a6444108199a2b896035927ea86c41 Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:26:32 +0100 Subject: [PATCH 10/29] fix: await params see: https://nextjs.org/docs/messages/sync-dynamic-apis --- dash/src/app/(nav)/archive/[year]/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/src/app/(nav)/archive/[year]/page.tsx b/dash/src/app/(nav)/archive/[year]/page.tsx index 340cc671..aa2a0eff 100644 --- a/dash/src/app/(nav)/archive/[year]/page.tsx +++ b/dash/src/app/(nav)/archive/[year]/page.tsx @@ -17,9 +17,9 @@ const getArchiveForYear = async (year: string): Promise => { } }; -export default async function ArchivePage({ params }: { params: { year: string } }) { +export default async function ArchivePage({ params }: { params: Promise<{ year: string }> }) { const currentYear = new Date(Date.now()).getFullYear(); - let year = params.year; + let year = (await params).year; if (year == null || year < "2018" || year > currentYear.toString() || typeof year !== "string") { year = currentYear.toString(); } From a2146e96434bf16270eef3683f18948090a2df23 Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:28:47 +0100 Subject: [PATCH 11/29] fix: await params --- dash/src/app/(nav)/archive/[year]/[key]/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/src/app/(nav)/archive/[year]/[key]/page.tsx b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx index ff000e2d..785264f5 100644 --- a/dash/src/app/(nav)/archive/[year]/[key]/page.tsx +++ b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx @@ -16,8 +16,8 @@ const getArchiveForYear = async (year: string): Promise => { } }; -export default async function MeetingDetailsPage({ params }: { params: { key: string; year: string } }) { - const { key, year } = params; +export default async function MeetingDetailsPage({ params }: { params: Promise<{ key: string; year: string }> }) { + const { key, year } = await params; const archive = await getArchiveForYear(year); const meeting = archive?.find((meet) => meet.key.toString() === key); From 36483d971f4e3e09cb00b5b871dd4177f1e9398b Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:37:44 +0100 Subject: [PATCH 12/29] fix(dash): clean up the archive dates and times --- dash/src/app/(nav)/archive/[year]/[key]/page.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/dash/src/app/(nav)/archive/[year]/[key]/page.tsx b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx index 785264f5..0fb44ed0 100644 --- a/dash/src/app/(nav)/archive/[year]/[key]/page.tsx +++ b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx @@ -53,12 +53,9 @@ export default async function MeetingDetailsPage({ params }: { params: Promise<{ {meeting.sessions.map((session, index) => (
  • {session.name}

    +

    {utc(session.startDate).local().format("MMMM D, YYYY")}

    - {utc(session.startDate).local().format("MMMM D, YYYY")} -{" "} - {utc(session.endDate).local().format("MMMM D, YYYY")} -

    -

    - {session.startDate} - {session.endDate} + {utc(session.startDate).local().format("HH:mm")} -{utc(session.endDate).local().format("HH:mm")}

  • ))} From 3bb3123dfdf57ba7aaea95a81f401df8060a33ba Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 1 Dec 2024 13:23:06 +0100 Subject: [PATCH 13/29] feat: add dropdown to archive for better vivibility with more years --- dash/src/components/Dropdown.tsx | 56 ++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 dash/src/components/Dropdown.tsx diff --git a/dash/src/components/Dropdown.tsx b/dash/src/components/Dropdown.tsx new file mode 100644 index 00000000..db3a7eb2 --- /dev/null +++ b/dash/src/components/Dropdown.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useState, useRef } from "react"; +import Link from "next/link"; +import { AnimatePresence, motion } from "framer-motion"; + +type Props = { + options: { label: string; href: string }[]; +}; + +export default function Dropdown({ options }: Props) { + const [showDropdown, setShowDropdown] = useState(false); + const dropdownRef = useRef(null); + + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setShowDropdown(false); + } + }; + + if (typeof window !== "undefined") { + document.addEventListener("mousedown", handleClickOutside); + } + + return ( +
    + + + {showDropdown && ( + +
      + {options.map((option) => ( +
    • + + {option.label} + +
    • + ))} +
    +
    + )} +
    +
    + ); +} From f21171a2078684f69d34478b7f8e1967c6b65024 Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 1 Dec 2024 13:23:26 +0100 Subject: [PATCH 14/29] feat: use dropwdown --- dash/src/app/(nav)/archive/[year]/page.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dash/src/app/(nav)/archive/[year]/page.tsx b/dash/src/app/(nav)/archive/[year]/page.tsx index aa2a0eff..094da0c8 100644 --- a/dash/src/app/(nav)/archive/[year]/page.tsx +++ b/dash/src/app/(nav)/archive/[year]/page.tsx @@ -4,6 +4,7 @@ import SegmentedLinks from "@/components/SegmentedLinks"; import { utc } from "moment"; import Link from "next/link"; import { Meeting } from "@/types/archive.type"; +import Dropdown from "@/components/Dropdown"; const getArchiveForYear = async (year: string): Promise => { try { @@ -30,11 +31,17 @@ export default async function ArchivePage({ params }: { params: Promise<{ year: years.push({ label: i.toString(), href: `/archive/${i.toString()}` }); } + const firstThreeYears = years.slice(years.length - 3); + const previousYears = years.slice(0, years.length - 3).reverse(); + return (

    Archive for {year}

    - +
    + + +
    {!archive ? (
    From 6e24ce43db43e7654eeb92904a14104c4d52eb04 Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 3 Nov 2024 19:04:46 +0100 Subject: [PATCH 15/29] feat: wip on the display --- dash/src/app/(nav)/archive/page.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/dash/src/app/(nav)/archive/page.tsx b/dash/src/app/(nav)/archive/page.tsx index 1c54c975..d41097ef 100644 --- a/dash/src/app/(nav)/archive/page.tsx +++ b/dash/src/app/(nav)/archive/page.tsx @@ -1,7 +1,14 @@ import { env } from "@/env.mjs"; type Meeting = { + key: number; + location: string; officialName: string; + name: string; + country: { + name: string; + }; + sessions: {}; }; const getArchiveForYear = async (year: string): Promise => { try { @@ -31,7 +38,16 @@ export default async function ArchivePage({ return (

    {archive?.length}

    -
      {archive?.map((meet) =>
    • {meet.officialName}
    • )}
    +
      + {archive?.map((meet) => ( +
    • +
      +
      {meet.officialName}
      +
      {meet.country.name}
      +
      +
    • + ))} +
    ); } From 9764882dbf4d654dbfea4d27c0e017cbfd517796 Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 3 Nov 2024 19:48:19 +0100 Subject: [PATCH 16/29] feat: finish design draft + add start and end date of event --- crates/api/src/endpoints/archive.rs | 4 +++ dash/src/app/(nav)/archive/page.tsx | 50 ++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/crates/api/src/endpoints/archive.rs b/crates/api/src/endpoints/archive.rs index a7c3bb11..18bf9b16 100644 --- a/crates/api/src/endpoints/archive.rs +++ b/crates/api/src/endpoints/archive.rs @@ -44,6 +44,10 @@ pub struct RaceSession { name: String, #[serde(default)] path: String, + #[serde(default)] + start_date: String, + #[serde(default)] + end_date: String, } pub async fn get_sessions_for_year( diff --git a/dash/src/app/(nav)/archive/page.tsx b/dash/src/app/(nav)/archive/page.tsx index d41097ef..7cff79b2 100644 --- a/dash/src/app/(nav)/archive/page.tsx +++ b/dash/src/app/(nav)/archive/page.tsx @@ -1,4 +1,6 @@ import { env } from "@/env.mjs"; +import { utc } from "moment"; +import Footer from "@/components/Footer"; // Adjust the import path as necessary type Meeting = { key: number; @@ -8,8 +10,12 @@ type Meeting = { country: { name: string; }; - sessions: {}; + sessions: { + startDate: string; + endDate: string; + }[]; }; + const getArchiveForYear = async (year: string): Promise => { try { const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { @@ -28,26 +34,48 @@ export default async function ArchivePage({ searchParams: { [key: string]: string | string[] | undefined }; }) { let year = searchParams["year"]; - //const searchParams = useSearchParams(); - //let year = searchParams.get("year"); const currentYear = new Date(Date.now()).getFullYear().toString(); if (year == null || year < "2017" || year > currentYear || typeof year !== "string") { year = currentYear; } const archive = await getArchiveForYear(year); + + if (!archive) { + return ( +
    +

    No archive data found for {year}

    +
    + ); + } + return ( -
    -

    {archive?.length}

    -
      - {archive?.map((meet) => ( -
    • -
      -
      {meet.officialName}
      -
      {meet.country.name}
      +
      +
      +

      Archive for {year}

      +

      All times are local time

      +
      +
        + {archive.map((meet) => ( +
      • +
        +
        +

        {meet.officialName}

        +

        {meet.country.name}

        +

        {meet.location}

        +
        +
        +

        + {utc(meet.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} + {utc(meet.sessions[meet.sessions.length - 1].endDate) + .local() + .format("MMMM D, YYYY")} +

        +
      • ))}
      +
      ); } From b79af22bbc7f2f052a3bc581f59178948a0e5e1e Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Mon, 4 Nov 2024 20:11:59 +0100 Subject: [PATCH 17/29] feat: add year picker --- dash/src/app/(nav)/archive/page.tsx | 72 ++++++++++++++------------ dash/src/components/SegmentedLinks.tsx | 48 +++++++++++++++++ 2 files changed, 88 insertions(+), 32 deletions(-) create mode 100644 dash/src/components/SegmentedLinks.tsx diff --git a/dash/src/app/(nav)/archive/page.tsx b/dash/src/app/(nav)/archive/page.tsx index 7cff79b2..1077c978 100644 --- a/dash/src/app/(nav)/archive/page.tsx +++ b/dash/src/app/(nav)/archive/page.tsx @@ -1,6 +1,7 @@ import { env } from "@/env.mjs"; import { utc } from "moment"; import Footer from "@/components/Footer"; // Adjust the import path as necessary +import SegmentedLinks from "@/components/SegmentedLinks"; type Meeting = { key: number; @@ -33,48 +34,55 @@ export default async function ArchivePage({ }: { searchParams: { [key: string]: string | string[] | undefined }; }) { + const currentYear = new Date(Date.now()).getFullYear(); let year = searchParams["year"]; - const currentYear = new Date(Date.now()).getFullYear().toString(); - if (year == null || year < "2017" || year > currentYear || typeof year !== "string") { - year = currentYear; + if (year == null || year < "2017" || year > currentYear.toString() || typeof year !== "string") { + year = currentYear.toString(); } + const archive = await getArchiveForYear(year); - if (!archive) { - return ( -
      -

      No archive data found for {year}

      -
      - ); + const years = []; + for (let i = 2017; i <= currentYear; i++) { + years.push({ label: i.toString(), href: `?year=${i.toString()}` }); } return (
      -
      +

      Archive for {year}

      -

      All times are local time

      +
      -
        - {archive.map((meet) => ( -
      • -
        -
        -

        {meet.officialName}

        -

        {meet.country.name}

        -

        {meet.location}

        -
        -
        -

        - {utc(meet.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} - {utc(meet.sessions[meet.sessions.length - 1].endDate) - .local() - .format("MMMM D, YYYY")} -

        -
        -
        -
      • - ))} -
      + {!archive ? ( +
      +

      No archive data found for {year}

      +
      + ) : ( + <> +

      All times are local time

      +
        + {archive.map((meet) => ( +
      • +
        +
        +

        {meet.officialName}

        +

        {meet.country.name}

        +

        {meet.location}

        +
        +
        +

        + {utc(meet.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} + {utc(meet.sessions[meet.sessions.length - 1].endDate) + .local() + .format("MMMM D, YYYY")} +

        +
        +
        +
      • + ))} +
      + + )}
      ); diff --git a/dash/src/components/SegmentedLinks.tsx b/dash/src/components/SegmentedLinks.tsx new file mode 100644 index 00000000..055e9999 --- /dev/null +++ b/dash/src/components/SegmentedLinks.tsx @@ -0,0 +1,48 @@ +"use client"; + +import clsx from "clsx"; +import { LayoutGroup, motion } from "framer-motion"; +import Link from "next/link"; + +type Props = { + id?: string; + className?: string; + options: { + label: string; + href: string; + }[]; + selected: string; + onSelect?: (val: string) => void; +}; + +export default function SegmentedLinks({ id, className, options, selected, onSelect }: Props) { + return ( + +
      + {options.map((option, i) => { + const isActive = option.href === selected; + return ( + + + {isActive && ( + + )} + {option.label} + + + ); + })} +
      +
      + ); +} From ca07c58009e86cf8a7c47d89d1d0293b9b0e5adc Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Mon, 4 Nov 2024 20:14:11 +0100 Subject: [PATCH 18/29] fix: remove 2017 --- dash/src/app/(nav)/archive/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/src/app/(nav)/archive/page.tsx b/dash/src/app/(nav)/archive/page.tsx index 1077c978..bf78e7c9 100644 --- a/dash/src/app/(nav)/archive/page.tsx +++ b/dash/src/app/(nav)/archive/page.tsx @@ -36,14 +36,14 @@ export default async function ArchivePage({ }) { const currentYear = new Date(Date.now()).getFullYear(); let year = searchParams["year"]; - if (year == null || year < "2017" || year > currentYear.toString() || typeof year !== "string") { + if (year == null || year < "2018" || year > currentYear.toString() || typeof year !== "string") { year = currentYear.toString(); } const archive = await getArchiveForYear(year); const years = []; - for (let i = 2017; i <= currentYear; i++) { + for (let i = 2018; i <= currentYear; i++) { years.push({ label: i.toString(), href: `?year=${i.toString()}` }); } From 8593a7e493df910b62ebf6a9bd547b5b51183e01 Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:12:09 +0100 Subject: [PATCH 19/29] feat(api): add caching --- crates/api/src/endpoints/archive.rs | 31 +++++++++++++---------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/crates/api/src/endpoints/archive.rs b/crates/api/src/endpoints/archive.rs index 18bf9b16..b8df67e7 100644 --- a/crates/api/src/endpoints/archive.rs +++ b/crates/api/src/endpoints/archive.rs @@ -1,4 +1,5 @@ use axum::extract::Path; +use cached::proc_macro::io_cached; use serde::{Deserialize, Serialize}; use serde_json; use tracing::error; @@ -50,26 +51,22 @@ pub struct RaceSession { end_date: String, } +#[io_cached( + map_error = r##"|e| anyhow::anyhow!(format!("disk cache error {:?}", e))"##, + disk = true, + time = 1800 +)] +pub async fn fetch_sessions_for_year(year: u32) -> Result { + let url = format!("https://livetiming.formula1.com/static/{year}/Index.json"); + let res = reqwest::get(url).await?; + let text = res.text().await?; + Ok(text) +} + pub async fn get_sessions_for_year( Path(year): Path, ) -> Result>, axum::http::StatusCode> { - let url = format!("https://livetiming.formula1.com/static/{year}/Index.json"); - - let result = reqwest::get(url).await; - let res = match result { - Ok(res) => res, - Err(err) => { - error!("Error fetching {} sessions: {}", year, err); - return Err(axum::http::StatusCode::BAD_GATEWAY); - } - }; - - let text = match res.status() { - axum::http::StatusCode::OK => res.text().await.unwrap_or(String::new()), - status => { - return Err(status); - } - }; + let text = fetch_sessions_for_year(year).await.unwrap(); let json = serde_json::from_str::(&text); From 13a881eb5813481625075180efe2c02e59f718d6 Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:29:05 +0100 Subject: [PATCH 20/29] feat: add details page --- .../app/(nav)/archive/[year]/[key]/page.tsx | 71 +++++++++++++++ dash/src/app/(nav)/archive/[year]/layout.tsx | 33 +++++++ dash/src/app/(nav)/archive/[year]/page.tsx | 75 ++++++++++++++++ dash/src/app/(nav)/archive/page.tsx | 90 +------------------ dash/src/types/archive.type.ts | 15 ++++ 5 files changed, 197 insertions(+), 87 deletions(-) create mode 100644 dash/src/app/(nav)/archive/[year]/[key]/page.tsx create mode 100644 dash/src/app/(nav)/archive/[year]/layout.tsx create mode 100644 dash/src/app/(nav)/archive/[year]/page.tsx create mode 100644 dash/src/types/archive.type.ts diff --git a/dash/src/app/(nav)/archive/[year]/[key]/page.tsx b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx new file mode 100644 index 00000000..ff000e2d --- /dev/null +++ b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx @@ -0,0 +1,71 @@ +import Footer from "@/components/Footer"; // Adjust the import path as necessary +import { utc } from "moment"; +import { Meeting } from "@/types/archive.type"; +import Link from "next/link"; +import { env } from "@/env.mjs"; + +const getArchiveForYear = async (year: string): Promise => { + try { + const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { + next: { revalidate: 60 * 60 * 4 }, + }); + const schedule: Meeting[] = await nextReq.json(); + return schedule; + } catch (e) { + return null; + } +}; + +export default async function MeetingDetailsPage({ params }: { params: { key: string; year: string } }) { + const { key, year } = params; + const archive = await getArchiveForYear(year); + const meeting = archive?.find((meet) => meet.key.toString() === key); + + if (!meeting) { + return ( +
      +
      +

      No meeting details found for key: {key}

      +
      +
      +
      + ); + } + + return ( +
      + +
      ← Back to Year Overview
      + +
      +

      {meeting.officialName}

      +

      {meeting.country.name}

      +

      {meeting.location}

      +

      + {utc(meeting.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} + {utc(meeting.sessions[meeting.sessions.length - 1].endDate) + .local() + .format("MMMM D, YYYY")} +

      +
      +

      Sessions

      +
        + {meeting.sessions.map((session, index) => ( +
      • +

        {session.name}

        +

        + {utc(session.startDate).local().format("MMMM D, YYYY")} -{" "} + {utc(session.endDate).local().format("MMMM D, YYYY")} +

        +

        + {session.startDate} - {session.endDate} +

        +
      • + ))} +
      +
      +
      +
      +
      + ); +} diff --git a/dash/src/app/(nav)/archive/[year]/layout.tsx b/dash/src/app/(nav)/archive/[year]/layout.tsx new file mode 100644 index 00000000..552fc603 --- /dev/null +++ b/dash/src/app/(nav)/archive/[year]/layout.tsx @@ -0,0 +1,33 @@ +import { env } from "@/env.mjs"; +import { Meeting } from "@/types/archive.type"; +import { ReactNode } from "react"; + +const getArchiveForYear = async (year: string): Promise => { + try { + const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { + next: { revalidate: 60 * 60 * 4 }, + }); + const schedule: Meeting[] = await nextReq.json(); + return schedule; + } catch (e) { + return null; + } +}; + +export default async function ArchiveLayout({ + children, + params, +}: { + children: ReactNode; + params: Promise<{ year: string }>; +}) { + const currentYear = new Date(Date.now()).getFullYear(); + let year = (await params).year; + if (year == null || year < "2018" || year > currentYear.toString() || typeof year !== "string") { + year = currentYear.toString(); + } + + await getArchiveForYear(year); + + return
      {children}
      ; +} diff --git a/dash/src/app/(nav)/archive/[year]/page.tsx b/dash/src/app/(nav)/archive/[year]/page.tsx new file mode 100644 index 00000000..340cc671 --- /dev/null +++ b/dash/src/app/(nav)/archive/[year]/page.tsx @@ -0,0 +1,75 @@ +import { env } from "@/env.mjs"; +import Footer from "@/components/Footer"; // Adjust the import path as necessary +import SegmentedLinks from "@/components/SegmentedLinks"; +import { utc } from "moment"; +import Link from "next/link"; +import { Meeting } from "@/types/archive.type"; + +const getArchiveForYear = async (year: string): Promise => { + try { + const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { + next: { revalidate: 60 * 60 * 4 }, + }); + const schedule: Meeting[] = await nextReq.json(); + return schedule; + } catch (e) { + return null; + } +}; + +export default async function ArchivePage({ params }: { params: { year: string } }) { + const currentYear = new Date(Date.now()).getFullYear(); + let year = params.year; + if (year == null || year < "2018" || year > currentYear.toString() || typeof year !== "string") { + year = currentYear.toString(); + } + const archive = await getArchiveForYear(year); + + const years = []; + for (let i = 2018; i <= currentYear; i++) { + years.push({ label: i.toString(), href: `/archive/${i.toString()}` }); + } + + return ( +
      +
      +

      Archive for {year}

      + +
      + {!archive ? ( +
      +

      No archive data found for {year}

      +
      + ) : ( + <> +

      All times are local time

      +
        + {archive.map((meet) => ( +
      • +
        +
        +

        {meet.officialName}

        +

        {meet.country.name}

        +

        {meet.location}

        +
        +
        +

        + {utc(meet.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} + {utc(meet.sessions[meet.sessions.length - 1].endDate) + .local() + .format("MMMM D, YYYY")} +

        +
        + +
        View Details
        + +
        +
      • + ))} +
      + + )} +
      +
      + ); +} diff --git a/dash/src/app/(nav)/archive/page.tsx b/dash/src/app/(nav)/archive/page.tsx index bf78e7c9..c0058098 100644 --- a/dash/src/app/(nav)/archive/page.tsx +++ b/dash/src/app/(nav)/archive/page.tsx @@ -1,89 +1,5 @@ -import { env } from "@/env.mjs"; -import { utc } from "moment"; -import Footer from "@/components/Footer"; // Adjust the import path as necessary -import SegmentedLinks from "@/components/SegmentedLinks"; +import { redirect } from "next/navigation"; -type Meeting = { - key: number; - location: string; - officialName: string; - name: string; - country: { - name: string; - }; - sessions: { - startDate: string; - endDate: string; - }[]; -}; - -const getArchiveForYear = async (year: string): Promise => { - try { - const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { - next: { revalidate: 60 * 60 * 4 }, - }); - const schedule: Meeting[] = await nextReq.json(); - return schedule; - } catch (e) { - return null; - } -}; - -export default async function ArchivePage({ - searchParams, -}: { - searchParams: { [key: string]: string | string[] | undefined }; -}) { - const currentYear = new Date(Date.now()).getFullYear(); - let year = searchParams["year"]; - if (year == null || year < "2018" || year > currentYear.toString() || typeof year !== "string") { - year = currentYear.toString(); - } - - const archive = await getArchiveForYear(year); - - const years = []; - for (let i = 2018; i <= currentYear; i++) { - years.push({ label: i.toString(), href: `?year=${i.toString()}` }); - } - - return ( -
      -
      -

      Archive for {year}

      - -
      - {!archive ? ( -
      -

      No archive data found for {year}

      -
      - ) : ( - <> -

      All times are local time

      -
        - {archive.map((meet) => ( -
      • -
        -
        -

        {meet.officialName}

        -

        {meet.country.name}

        -

        {meet.location}

        -
        -
        -

        - {utc(meet.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} - {utc(meet.sessions[meet.sessions.length - 1].endDate) - .local() - .format("MMMM D, YYYY")} -

        -
        -
        -
      • - ))} -
      - - )} -
      -
      - ); +export default function ArchiveRedirectPage() { + redirect(`/archive/${new Date(Date.now()).getFullYear()}`); } diff --git a/dash/src/types/archive.type.ts b/dash/src/types/archive.type.ts new file mode 100644 index 00000000..e9ee4309 --- /dev/null +++ b/dash/src/types/archive.type.ts @@ -0,0 +1,15 @@ +export type Meeting = { + key: number; + location: string; + officialName: string; + name: string; + country: { + name: string; + }; + sessions: { + key: number; + name: string; + startDate: string; + endDate: string; + }[]; +}; From 1e3bd6f35b9813272bd0bf96b77ba4503a0f5ecf Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:26:32 +0100 Subject: [PATCH 21/29] fix: await params see: https://nextjs.org/docs/messages/sync-dynamic-apis --- dash/src/app/(nav)/archive/[year]/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/src/app/(nav)/archive/[year]/page.tsx b/dash/src/app/(nav)/archive/[year]/page.tsx index 340cc671..aa2a0eff 100644 --- a/dash/src/app/(nav)/archive/[year]/page.tsx +++ b/dash/src/app/(nav)/archive/[year]/page.tsx @@ -17,9 +17,9 @@ const getArchiveForYear = async (year: string): Promise => { } }; -export default async function ArchivePage({ params }: { params: { year: string } }) { +export default async function ArchivePage({ params }: { params: Promise<{ year: string }> }) { const currentYear = new Date(Date.now()).getFullYear(); - let year = params.year; + let year = (await params).year; if (year == null || year < "2018" || year > currentYear.toString() || typeof year !== "string") { year = currentYear.toString(); } From 1eab4be30703e7488e6630d23cf2b6e1e00e1400 Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:28:47 +0100 Subject: [PATCH 22/29] fix: await params --- dash/src/app/(nav)/archive/[year]/[key]/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/src/app/(nav)/archive/[year]/[key]/page.tsx b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx index ff000e2d..785264f5 100644 --- a/dash/src/app/(nav)/archive/[year]/[key]/page.tsx +++ b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx @@ -16,8 +16,8 @@ const getArchiveForYear = async (year: string): Promise => { } }; -export default async function MeetingDetailsPage({ params }: { params: { key: string; year: string } }) { - const { key, year } = params; +export default async function MeetingDetailsPage({ params }: { params: Promise<{ key: string; year: string }> }) { + const { key, year } = await params; const archive = await getArchiveForYear(year); const meeting = archive?.find((meet) => meet.key.toString() === key); From ebb52861084bba3962b166147588138d5603c55a Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:37:44 +0100 Subject: [PATCH 23/29] fix(dash): clean up the archive dates and times --- dash/src/app/(nav)/archive/[year]/[key]/page.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/dash/src/app/(nav)/archive/[year]/[key]/page.tsx b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx index 785264f5..0fb44ed0 100644 --- a/dash/src/app/(nav)/archive/[year]/[key]/page.tsx +++ b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx @@ -53,12 +53,9 @@ export default async function MeetingDetailsPage({ params }: { params: Promise<{ {meeting.sessions.map((session, index) => (
    • {session.name}

      +

      {utc(session.startDate).local().format("MMMM D, YYYY")}

      - {utc(session.startDate).local().format("MMMM D, YYYY")} -{" "} - {utc(session.endDate).local().format("MMMM D, YYYY")} -

      -

      - {session.startDate} - {session.endDate} + {utc(session.startDate).local().format("HH:mm")} -{utc(session.endDate).local().format("HH:mm")}

    • ))} From 9c5d7ab35a591de3f4a40c27a6e00d6e883fb7ad Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 1 Dec 2024 13:23:06 +0100 Subject: [PATCH 24/29] feat: add dropdown to archive for better vivibility with more years --- dash/src/components/Dropdown.tsx | 56 ++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 dash/src/components/Dropdown.tsx diff --git a/dash/src/components/Dropdown.tsx b/dash/src/components/Dropdown.tsx new file mode 100644 index 00000000..db3a7eb2 --- /dev/null +++ b/dash/src/components/Dropdown.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useState, useRef } from "react"; +import Link from "next/link"; +import { AnimatePresence, motion } from "framer-motion"; + +type Props = { + options: { label: string; href: string }[]; +}; + +export default function Dropdown({ options }: Props) { + const [showDropdown, setShowDropdown] = useState(false); + const dropdownRef = useRef(null); + + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setShowDropdown(false); + } + }; + + if (typeof window !== "undefined") { + document.addEventListener("mousedown", handleClickOutside); + } + + return ( +
      + + + {showDropdown && ( + +
        + {options.map((option) => ( +
      • + + {option.label} + +
      • + ))} +
      +
      + )} +
      +
      + ); +} From 04eef578e6e5eea8962522a1bc79cf24620f79d9 Mon Sep 17 00:00:00 2001 From: dev-viinz <42003446+dev-viinz@users.noreply.github.com> Date: Sun, 1 Dec 2024 13:23:26 +0100 Subject: [PATCH 25/29] feat: use dropwdown --- dash/src/app/(nav)/archive/[year]/page.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dash/src/app/(nav)/archive/[year]/page.tsx b/dash/src/app/(nav)/archive/[year]/page.tsx index aa2a0eff..094da0c8 100644 --- a/dash/src/app/(nav)/archive/[year]/page.tsx +++ b/dash/src/app/(nav)/archive/[year]/page.tsx @@ -4,6 +4,7 @@ import SegmentedLinks from "@/components/SegmentedLinks"; import { utc } from "moment"; import Link from "next/link"; import { Meeting } from "@/types/archive.type"; +import Dropdown from "@/components/Dropdown"; const getArchiveForYear = async (year: string): Promise => { try { @@ -30,11 +31,17 @@ export default async function ArchivePage({ params }: { params: Promise<{ year: years.push({ label: i.toString(), href: `/archive/${i.toString()}` }); } + const firstThreeYears = years.slice(years.length - 3); + const previousYears = years.slice(0, years.length - 3).reverse(); + return (

      Archive for {year}

      - +
      + + +
      {!archive ? (
      From 9d2c32e9b60f26c3d4990cefe3932d7df258abba Mon Sep 17 00:00:00 2001 From: Slowlydev Date: Tue, 27 May 2025 23:49:57 +0200 Subject: [PATCH 26/29] fix: use new axum paths --- services/api/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/api/src/main.rs b/services/api/src/main.rs index 274f582a..e2a4eda1 100644 --- a/services/api/src/main.rs +++ b/services/api/src/main.rs @@ -36,7 +36,7 @@ async fn main() -> Result<(), anyhow::Error> { let app = Router::new() .route("/api/schedule", get(endpoints::schedule::get)) .route("/api/schedule/next", get(endpoints::schedule::get_next)) - .route("/api/archive/:year", get(archive::get_sessions_for_year)) + .route("/api/archive/{year}", get(archive::get_sessions_for_year)) .route("/api/health", get(endpoints::health::check)); let listener = TcpListener::bind(addr).await?; From 4ea43ee10a5f734911d39718158db6b0e0e8dfb2 Mon Sep 17 00:00:00 2001 From: Slowlydev Date: Tue, 27 May 2025 23:50:17 +0200 Subject: [PATCH 27/29] fix: remove footers and update env var names --- .../app/(nav)/archive/[year]/[key]/page.tsx | 28 +++++++++++++------ dash/src/app/(nav)/archive/[year]/page.tsx | 20 +++++++------ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/dash/src/app/(nav)/archive/[year]/[key]/page.tsx b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx index 0fb44ed0..9d41e4eb 100644 --- a/dash/src/app/(nav)/archive/[year]/[key]/page.tsx +++ b/dash/src/app/(nav)/archive/[year]/[key]/page.tsx @@ -1,23 +1,34 @@ -import Footer from "@/components/Footer"; // Adjust the import path as necessary import { utc } from "moment"; -import { Meeting } from "@/types/archive.type"; import Link from "next/link"; -import { env } from "@/env.mjs"; + +import type { Meeting } from "@/types/archive.type"; + +import { env } from "@/env"; const getArchiveForYear = async (year: string): Promise => { try { - const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { + const nextReq = await fetch(`${env.API_URL}/api/archive/${year}`, { next: { revalidate: 60 * 60 * 4 }, }); const schedule: Meeting[] = await nextReq.json(); return schedule; - } catch (e) { + } catch { return null; } }; -export default async function MeetingDetailsPage({ params }: { params: Promise<{ key: string; year: string }> }) { +type Prams = Promise<{ + key: string; + year: string; +}>; + +type Props = { + params: Prams; +}; + +export default async function MeetingDetailsPage({ params }: Props) { const { key, year } = await params; + const archive = await getArchiveForYear(year); const meeting = archive?.find((meet) => meet.key.toString() === key); @@ -27,7 +38,6 @@ export default async function MeetingDetailsPage({ params }: { params: Promise<{

      No meeting details found for key: {key}

      -
      ); } @@ -37,10 +47,11 @@ export default async function MeetingDetailsPage({ params }: { params: Promise<{
      ← Back to Year Overview
      +

      {meeting.officialName}

      {meeting.country.name}

      -

      {meeting.location}

      +

      {meeting.location}

      {utc(meeting.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} {utc(meeting.sessions[meeting.sessions.length - 1].endDate) @@ -62,7 +73,6 @@ export default async function MeetingDetailsPage({ params }: { params: Promise<{

    -
    ); } diff --git a/dash/src/app/(nav)/archive/[year]/page.tsx b/dash/src/app/(nav)/archive/[year]/page.tsx index 094da0c8..317df1bb 100644 --- a/dash/src/app/(nav)/archive/[year]/page.tsx +++ b/dash/src/app/(nav)/archive/[year]/page.tsx @@ -1,19 +1,21 @@ -import { env } from "@/env.mjs"; -import Footer from "@/components/Footer"; // Adjust the import path as necessary -import SegmentedLinks from "@/components/SegmentedLinks"; import { utc } from "moment"; import Link from "next/link"; -import { Meeting } from "@/types/archive.type"; + +import SegmentedLinks from "@/components/SegmentedLinks"; import Dropdown from "@/components/Dropdown"; +import type { Meeting } from "@/types/archive.type"; + +import { env } from "@/env"; + const getArchiveForYear = async (year: string): Promise => { try { - const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { + const nextReq = await fetch(`${env.API_URL}/api/archive/${year}`, { next: { revalidate: 60 * 60 * 4 }, }); const schedule: Meeting[] = await nextReq.json(); return schedule; - } catch (e) { + } catch { return null; } }; @@ -35,7 +37,7 @@ export default async function ArchivePage({ params }: { params: Promise<{ year: const previousYears = years.slice(0, years.length - 3).reverse(); return ( -
    +

    Archive for {year}

    @@ -43,6 +45,7 @@ export default async function ArchivePage({ params }: { params: Promise<{ year:
    + {!archive ? (

    No archive data found for {year}

    @@ -57,7 +60,7 @@ export default async function ArchivePage({ params }: { params: Promise<{ year:

    {meet.officialName}

    {meet.country.name}

    -

    {meet.location}

    +

    {meet.location}

    @@ -76,7 +79,6 @@ export default async function ArchivePage({ params }: { params: Promise<{ year:

)} -
); } From 337e9b87d0d707218f38661f38dcabcf24681fde Mon Sep 17 00:00:00 2001 From: Slowlydev Date: Tue, 27 May 2025 23:50:27 +0200 Subject: [PATCH 28/29] refactor: remove unsessary layout --- dash/src/app/(nav)/archive/[year]/layout.tsx | 33 -------------------- 1 file changed, 33 deletions(-) delete mode 100644 dash/src/app/(nav)/archive/[year]/layout.tsx diff --git a/dash/src/app/(nav)/archive/[year]/layout.tsx b/dash/src/app/(nav)/archive/[year]/layout.tsx deleted file mode 100644 index 552fc603..00000000 --- a/dash/src/app/(nav)/archive/[year]/layout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { env } from "@/env.mjs"; -import { Meeting } from "@/types/archive.type"; -import { ReactNode } from "react"; - -const getArchiveForYear = async (year: string): Promise => { - try { - const nextReq = await fetch(`${env.NEXT_PUBLIC_API_URL}/api/archive/${year}`, { - next: { revalidate: 60 * 60 * 4 }, - }); - const schedule: Meeting[] = await nextReq.json(); - return schedule; - } catch (e) { - return null; - } -}; - -export default async function ArchiveLayout({ - children, - params, -}: { - children: ReactNode; - params: Promise<{ year: string }>; -}) { - const currentYear = new Date(Date.now()).getFullYear(); - let year = (await params).year; - if (year == null || year < "2018" || year > currentYear.toString() || typeof year !== "string") { - year = currentYear.toString(); - } - - await getArchiveForYear(year); - - return
{children}
; -} From 20f643e4c97597408a0075a5886adc0114747c35 Mon Sep 17 00:00:00 2001 From: Slowlydev Date: Sat, 31 May 2025 17:08:21 +0200 Subject: [PATCH 29/29] feat: unify design of archive and schedule --- dash/src/app/(nav)/archive/[year]/page.tsx | 129 +++++++++--------- dash/src/app/(nav)/archive/meeting.tsx | 35 +++++ dash/src/app/(nav)/archive/page.tsx | 3 +- dash/src/app/(nav)/layout.tsx | 4 +- .../(nav)/schedule/countdown.tsx} | 0 dash/src/app/(nav)/schedule/page.tsx | 116 ++++++++++++++-- .../(nav)/schedule/round.tsx} | 44 ++---- .../(nav)/schedule/weekend-schedule.tsx} | 0 dash/src/components/Dropdown.tsx | 56 -------- dash/src/components/SegmentedLinks.tsx | 48 ------- dash/src/components/schedule/NextRound.tsx | 56 -------- dash/src/components/schedule/Schedule.tsx | 45 ------ dash/src/lib/country.ts | 31 +++++ services/api/src/endpoints/archive.rs | 24 ++-- 14 files changed, 263 insertions(+), 328 deletions(-) create mode 100644 dash/src/app/(nav)/archive/meeting.tsx rename dash/src/{components/schedule/Countdown.tsx => app/(nav)/schedule/countdown.tsx} (100%) rename dash/src/{components/schedule/Round.tsx => app/(nav)/schedule/round.tsx} (67%) rename dash/src/{components/schedule/WeekendSchedule.tsx => app/(nav)/schedule/weekend-schedule.tsx} (100%) delete mode 100644 dash/src/components/Dropdown.tsx delete mode 100644 dash/src/components/SegmentedLinks.tsx delete mode 100644 dash/src/components/schedule/NextRound.tsx delete mode 100644 dash/src/components/schedule/Schedule.tsx create mode 100644 dash/src/lib/country.ts diff --git a/dash/src/app/(nav)/archive/[year]/page.tsx b/dash/src/app/(nav)/archive/[year]/page.tsx index 317df1bb..6c510325 100644 --- a/dash/src/app/(nav)/archive/[year]/page.tsx +++ b/dash/src/app/(nav)/archive/[year]/page.tsx @@ -1,84 +1,91 @@ -import { utc } from "moment"; -import Link from "next/link"; +import { notFound } from "next/navigation"; +import { connection } from "next/server"; -import SegmentedLinks from "@/components/SegmentedLinks"; -import Dropdown from "@/components/Dropdown"; +import { env } from "@/env"; -import type { Meeting } from "@/types/archive.type"; +import Meeting from "../meeting"; +import type { Meeting as MeetingType } from "@/types/archive.type"; +import { Suspense } from "react"; -import { env } from "@/env"; +const getArchiveForYear = async (year: string) => { + await connection(); -const getArchiveForYear = async (year: string): Promise => { try { const nextReq = await fetch(`${env.API_URL}/api/archive/${year}`, { - next: { revalidate: 60 * 60 * 4 }, + cache: "no-store", }); - const schedule: Meeting[] = await nextReq.json(); + const schedule: MeetingType[] = await nextReq.json(); return schedule; - } catch { + } catch (e) { + console.error("error fetching next round", e); return null; } }; export default async function ArchivePage({ params }: { params: Promise<{ year: string }> }) { - const currentYear = new Date(Date.now()).getFullYear(); - let year = (await params).year; - if (year == null || year < "2018" || year > currentYear.toString() || typeof year !== "string") { - year = currentYear.toString(); - } - const archive = await getArchiveForYear(year); + const { year } = await params; + + if (year < "2018") notFound(); + + return ( +
+
+

{year} Archive

+

All times are local time

+
- const years = []; - for (let i = 2018; i <= currentYear; i++) { - years.push({ label: i.toString(), href: `/archive/${i.toString()}` }); + }> + + +
+ ); +} + +const Meetings = async ({ year }: { year: string }) => { + const meetings = await getArchiveForYear(year); + + if (!meetings) { + return ( +
+

No archive data found for {year}

+
+ ); } - const firstThreeYears = years.slice(years.length - 3); - const previousYears = years.slice(0, years.length - 3).reverse(); + return ( +
    + {meetings.map((meeting) => ( + + ))} +
+ ); +}; +const Loading = () => { return ( -
-
-

Archive for {year}

-
- - +
    + {Array.from({ length: 10 }).map((_, i) => ( + + ))} +
+ ); +}; + +const MeetingLoading = () => { + return ( +
+
+
+ +
+
+
- {!archive ? ( -
-

No archive data found for {year}

-
- ) : ( - <> -

All times are local time

-
    - {archive.map((meet) => ( -
  • -
    -
    -

    {meet.officialName}

    -

    {meet.country.name}

    -

    {meet.location}

    -
    -
    -

    - {utc(meet.sessions[0].startDate).local().format("MMMM D, YYYY")} -{" "} - {utc(meet.sessions[meet.sessions.length - 1].endDate) - .local() - .format("MMMM D, YYYY")} -

    -
    - -
    View Details
    - -
    -
  • - ))} -
- - )} +
+ +
); -} +}; diff --git a/dash/src/app/(nav)/archive/meeting.tsx b/dash/src/app/(nav)/archive/meeting.tsx new file mode 100644 index 00000000..2180c30e --- /dev/null +++ b/dash/src/app/(nav)/archive/meeting.tsx @@ -0,0 +1,35 @@ +import Link from "next/link"; +import { utc } from "moment"; + +import type { Meeting } from "@/types/archive.type"; + +import { getCountryCode } from "@/lib/country"; + +import Flag from "@/components/Flag"; + +type Props = { + meeting: Meeting; + year: string; +}; + +export default function Meeting({ meeting, year }: Props) { + const start = meeting.sessions[0].startDate; + const end = meeting.sessions[meeting.sessions.length - 1].endDate; + + return ( + +
+ +

{meeting.officialName}

+
+ +

+ {meeting.country.name} - {meeting.location} +

+ +

+ {utc(start).local().format("MMMM D, YYYY")} - {utc(end).local().format("MMMM D, YYYY")} +

+ + ); +} diff --git a/dash/src/app/(nav)/archive/page.tsx b/dash/src/app/(nav)/archive/page.tsx index c0058098..1264b241 100644 --- a/dash/src/app/(nav)/archive/page.tsx +++ b/dash/src/app/(nav)/archive/page.tsx @@ -1,5 +1,6 @@ import { redirect } from "next/navigation"; export default function ArchiveRedirectPage() { - redirect(`/archive/${new Date(Date.now()).getFullYear()}`); + const currentYear = new Date().getFullYear(); + redirect(`/archive/${currentYear}`); } diff --git a/dash/src/app/(nav)/layout.tsx b/dash/src/app/(nav)/layout.tsx index 8b5530d0..7ca71c94 100644 --- a/dash/src/app/(nav)/layout.tsx +++ b/dash/src/app/(nav)/layout.tsx @@ -12,6 +12,8 @@ type Props = { }; export default function Layout({ children }: Props) { + const year = new Date().getFullYear(); + return ( <>