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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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 ? (