diff --git a/package.json b/package.json
index 310bd8a..880f46b 100644
--- a/package.json
+++ b/package.json
@@ -29,11 +29,11 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
- "@tanstack/react-query": "^5.90.5",
"classnames": "^2.5.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "react-markdown": "^10.1.0"
+ "react-markdown": "^10.1.0",
+ "zustand": "^5.0.11"
},
"devDependencies": {
"@eslint/js": "^9.38.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ffd5047..9e11be1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,9 +17,6 @@ importers:
'@dnd-kit/utilities':
specifier: ^3.2.2
version: 3.2.2(react@19.2.0)
- '@tanstack/react-query':
- specifier: ^5.90.5
- version: 5.90.5(react@19.2.0)
classnames:
specifier: ^2.5.1
version: 2.5.1
@@ -32,6 +29,9 @@ importers:
react-markdown:
specifier: ^10.1.0
version: 10.1.0(@types/react@19.2.2)(react@19.2.0)
+ zustand:
+ specifier: ^5.0.11
+ version: 5.0.11(@types/react@19.2.2)(react@19.2.0)
devDependencies:
'@eslint/js':
specifier: ^9.38.0
@@ -1097,14 +1097,6 @@ packages:
'@swc/types@0.1.25':
resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==}
- '@tanstack/query-core@5.90.5':
- resolution: {integrity: sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==}
-
- '@tanstack/react-query@5.90.5':
- resolution: {integrity: sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==}
- peerDependencies:
- react: ^18 || ^19
-
'@ts-morph/common@0.27.0':
resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==}
@@ -3127,6 +3119,24 @@ packages:
zod@4.1.12:
resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==}
+ zustand@5.0.11:
+ resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=18.0.0'
+ immer: '>=9.0.6'
+ react: '>=18.0.0'
+ use-sync-external-store: '>=1.2.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ use-sync-external-store:
+ optional: true
+
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -4027,13 +4037,6 @@ snapshots:
dependencies:
'@swc/counter': 0.1.3
- '@tanstack/query-core@5.90.5': {}
-
- '@tanstack/react-query@5.90.5(react@19.2.0)':
- dependencies:
- '@tanstack/query-core': 5.90.5
- react: 19.2.0
-
'@ts-morph/common@0.27.0':
dependencies:
fast-glob: 3.3.3
@@ -6523,4 +6526,9 @@ snapshots:
zod@4.1.12: {}
+ zustand@5.0.11(@types/react@19.2.2)(react@19.2.0):
+ optionalDependencies:
+ '@types/react': 19.2.2
+ react: 19.2.0
+
zwitch@2.0.4: {}
diff --git a/src/App.tsx b/src/App.tsx
index c704309..fb16d43 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,13 +1,14 @@
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { useEffect } from "react";
import Main from "./Main";
+import { useStore } from "./store";
export default function App() {
- const queryClient = new QueryClient();
+ const fetchAll = useStore((s) => s.fetchAll);
- return (
-
-
-
- );
+ useEffect(() => {
+ fetchAll();
+ }, [fetchAll]);
+
+ return ;
}
diff --git a/src/api/api.ts b/src/api/api.ts
index 9970ba3..0e77b27 100644
--- a/src/api/api.ts
+++ b/src/api/api.ts
@@ -2,7 +2,7 @@ const baseUrl = "/api";
const currentYear = new Date().getFullYear();
// const currentYear = 2024;
-export const apiPaths = {
+const apiPaths = {
driverStandings: `${currentYear}/driverstandings/`,
constructorStandings: `${currentYear}/constructorstandings/`,
raceSchedule: `${currentYear}/races/`,
@@ -11,31 +11,9 @@ export const apiPaths = {
constructors: `${currentYear}/constructors/`,
} as const;
-async function get(path: string) {
- const response = await fetch(`${baseUrl}/${path}`);
- return response.json();
-}
-
-export async function getDriverStandings() {
- return get(apiPaths.driverStandings);
-}
+export type ApiEndpoint = keyof typeof apiPaths;
-export async function getConstructorStandings() {
- return get(apiPaths.constructorStandings);
-}
-
-export async function getDrivers() {
- return get(apiPaths.drivers);
-}
-
-export async function getConstructors() {
- return get(apiPaths.constructors);
-}
-
-export async function getRaceSchedule() {
- return get(apiPaths.raceSchedule);
-}
-
-export async function getRaceResults() {
- return get(apiPaths.raceResults);
+export async function fetchApi(endpoint: ApiEndpoint): Promise {
+ const response = await fetch(`${baseUrl}/${apiPaths[endpoint]}`);
+ return response.json();
}
diff --git a/src/api/transforms.ts b/src/api/transforms.ts
new file mode 100644
index 0000000..809182f
--- /dev/null
+++ b/src/api/transforms.ts
@@ -0,0 +1,126 @@
+import {
+ IConstructorStanding,
+ IConstructorStandings,
+ IDriverStanding,
+ IDriverStandings,
+ IRace,
+ IRaceSchedule,
+ ITime,
+} from "../types/api";
+import {
+ RaceEvent,
+ RaceTable,
+ RaceType,
+ StandingsList,
+} from "../types/entities";
+
+export function transformDriverStandings(data: IDriverStandings) {
+ if (!data?.MRData?.StandingsTable?.StandingsLists?.length) {
+ return;
+ }
+
+ const [standingsList] = data.MRData.StandingsTable.StandingsLists;
+
+ const driverStandings: StandingsList = {
+ ...standingsList,
+ round: Number(standingsList?.round),
+ DriverStandings: standingsList.DriverStandings.map(
+ (standing: IDriverStanding) => ({
+ ...standing,
+ points: Number(standing.points),
+ position: Number(standing.position),
+ wins: Number(standing.wins),
+ Driver: {
+ ...standing.Driver,
+ permanentNumber: Number(standing.Driver.permanentNumber),
+ },
+ }),
+ ),
+ };
+
+ return driverStandings;
+}
+
+export function transformConstructorStandings(data?: IConstructorStandings) {
+ if (!data?.MRData?.StandingsTable?.StandingsLists?.length) {
+ return;
+ }
+
+ const [standingsList] = data.MRData.StandingsTable.StandingsLists;
+
+ const constructorStandings: StandingsList = {
+ ...standingsList,
+ round: Number(standingsList.round),
+ ConstructorStandings: standingsList.ConstructorStandings.map(
+ (standing: IConstructorStanding) => ({
+ ...standing,
+ position: Number(standing.position),
+ points: Number(standing.points),
+ wins: Number(standing.wins),
+ }),
+ ),
+ };
+
+ return constructorStandings;
+}
+
+function dateTimeToDate(dateTime: ITime): Date {
+ return new Date(`${dateTime.date}T${dateTime.time}`);
+}
+
+export function transformRaceSchedule(
+ responseData: IRaceSchedule,
+): RaceTable | undefined {
+ if (!Array.isArray(responseData?.MRData?.RaceTable?.Races)) {
+ return;
+ }
+
+ const raceTable = responseData.MRData.RaceTable;
+
+ return {
+ ...raceTable,
+ season: Number(raceTable.season),
+ Races: raceTable.Races.map((race: IRace) => ({
+ ...race,
+ round: Number(race.round),
+ FirstPractice: dateTimeToDate(race.FirstPractice),
+ Qualifying: dateTimeToDate(race.Qualifying),
+ SecondPractice: race.SecondPractice
+ ? dateTimeToDate(race.SecondPractice)
+ : undefined,
+ ThirdPractice: race.ThirdPractice
+ ? dateTimeToDate(race.ThirdPractice)
+ : undefined,
+ Sprint: race.Sprint ? dateTimeToDate(race.Sprint) : undefined,
+ Circuit: {
+ ...race.Circuit,
+ Location: {
+ ...race.Circuit.Location,
+ lat: Number(race.Circuit.Location.lat),
+ long: Number(race.Circuit.Location.long),
+ },
+ },
+ })),
+ };
+}
+
+export function getEventsSchedule(raceSchedule: RaceTable): RaceEvent[] {
+ return raceSchedule.Races.reduce((eventList: RaceEvent[], current) => {
+ const id = current.round.toString();
+
+ if (current.Sprint) {
+ eventList.push({
+ Race: current,
+ eventType: RaceType.SPRINT_RACE,
+ id: id + "-s",
+ });
+ }
+
+ eventList.push({
+ Race: current,
+ eventType: RaceType.GRAND_PRIX,
+ id,
+ });
+ return eventList;
+ }, []);
+}
diff --git a/src/api/useConstructorStandings.ts b/src/api/useConstructorStandings.ts
deleted file mode 100644
index 485f8b0..0000000
--- a/src/api/useConstructorStandings.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { useQuery } from "@tanstack/react-query";
-
-import { IConstructorStanding, IConstructorStandings } from "../types/api";
-import { StandingsList } from "../types/entities";
-import { getConstructorStandings } from "./api";
-
-function transformConstructorStandings(data?: IConstructorStandings) {
- if (!data?.MRData?.StandingsTable?.StandingsLists?.length) {
- return;
- }
-
- const [standingsList] = data.MRData.StandingsTable.StandingsLists;
-
- const constructorStandings: StandingsList = {
- ...standingsList,
- round: Number(standingsList.round),
- ConstructorStandings: standingsList.ConstructorStandings.map(
- (standing: IConstructorStanding) => ({
- ...standing,
- position: Number(standing.position),
- points: Number(standing.points),
- wins: Number(standing.wins),
- }),
- ),
- };
-
- return constructorStandings;
-}
-
-export function useConstructorStandings() {
- const { data, isLoading, isError } = useQuery({
- queryKey: ["constructorStandings"],
- queryFn: getConstructorStandings,
- });
-
- const constructorStandings = transformConstructorStandings(data);
-
- return { constructorStandings, isLoading, isError };
-}
diff --git a/src/api/useConstructors.ts b/src/api/useConstructors.ts
deleted file mode 100644
index f995b3c..0000000
--- a/src/api/useConstructors.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { useQuery } from "@tanstack/react-query";
-
-import { IConstructor } from "../types/api";
-import { getConstructors } from "./api";
-
-export function useConstructors() {
- const {
- data: constructors,
- isLoading,
- isError,
- } = useQuery({
- queryKey: ["constructors"],
- queryFn: getConstructors,
- });
-
- return { constructors, isLoading, isError };
-}
diff --git a/src/api/useDriverStandings.ts b/src/api/useDriverStandings.ts
deleted file mode 100644
index 78ea329..0000000
--- a/src/api/useDriverStandings.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { useQuery } from "@tanstack/react-query";
-import { useMemo } from "react";
-
-import { IDriverStanding, IDriverStandings } from "../types/api";
-import { StandingsList } from "../types/entities";
-import { getDriverStandings } from "./api";
-
-function transformDriverStandings(data: IDriverStandings) {
- if (!data?.MRData?.StandingsTable?.StandingsLists?.length) {
- return;
- }
-
- const [standingsList] = data.MRData.StandingsTable.StandingsLists;
-
- const driverStandings: StandingsList = {
- ...standingsList,
- round: Number(standingsList?.round),
- DriverStandings: standingsList.DriverStandings.map(
- (standing: IDriverStanding) => ({
- ...standing,
- points: Number(standing.points),
- position: Number(standing.position),
- wins: Number(standing.wins),
- Driver: {
- ...standing.Driver,
- permanentNumber: Number(standing.Driver.permanentNumber),
- },
- }),
- ),
- };
-
- return driverStandings;
-}
-
-export function useDriverStandings() {
- const { data, isLoading, isError } = useQuery({
- queryKey: ["driverStandings"],
- queryFn: getDriverStandings,
- });
-
- const driverStandings = data ? transformDriverStandings(data) : undefined;
-
- return { driverStandings, isLoading, isError };
-}
-
-export function useDefaultRaceResult() {
- const { driverStandings } = useDriverStandings();
-
- return useMemo(() => {
- if (!driverStandings?.DriverStandings) {
- return [];
- }
-
- const standings = driverStandings.DriverStandings;
- return standings.map((driverStanding, index) => ({
- Driver: driverStanding.Driver,
- Constructors: driverStanding.Constructors,
- fastestLap: index === 0,
- }));
- }, [driverStandings?.DriverStandings]);
-}
diff --git a/src/api/useDrivers.ts b/src/api/useDrivers.ts
deleted file mode 100644
index da71fb5..0000000
--- a/src/api/useDrivers.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { useQuery } from "@tanstack/react-query";
-
-import { IDriver } from "../types/api";
-import { getDrivers } from "./api";
-
-export function useDrivers() {
- const {
- data: drivers,
- isLoading,
- isError,
- } = useQuery({
- queryKey: ["drivers"],
- queryFn: getDrivers,
- });
-
- return { drivers, isLoading, isError };
-}
diff --git a/src/api/useRaceSchedule.ts b/src/api/useRaceSchedule.ts
deleted file mode 100644
index 9343279..0000000
--- a/src/api/useRaceSchedule.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { useQuery } from "@tanstack/react-query";
-
-import { IRace, IRaceSchedule, ITime } from "../types/api";
-import { RaceEvent, RaceTable, RaceType } from "../types/entities";
-import { getRaceSchedule } from "./api";
-
-function dateTimeToDate(dateTime: ITime): Date {
- return new Date(`${dateTime.date}T${dateTime.time}`);
-}
-
-function transformRaceSchedule(
- responseData: IRaceSchedule,
-): RaceTable | undefined {
- if (!Array.isArray(responseData?.MRData?.RaceTable?.Races)) {
- return;
- }
-
- const raceTable = responseData.MRData.RaceTable;
-
- return {
- ...raceTable,
- season: Number(raceTable.season),
- Races: raceTable.Races.map((race: IRace) => ({
- ...race,
- round: Number(race.round),
- FirstPractice: dateTimeToDate(race.FirstPractice),
- Qualifying: dateTimeToDate(race.Qualifying),
- SecondPractice: race.SecondPractice
- ? dateTimeToDate(race.SecondPractice)
- : undefined,
- ThirdPractice: race.ThirdPractice
- ? dateTimeToDate(race.ThirdPractice)
- : undefined,
- Sprint: race.Sprint ? dateTimeToDate(race.Sprint) : undefined,
- Circuit: {
- ...race.Circuit,
- Location: {
- ...race.Circuit.Location,
- lat: Number(race.Circuit.Location.lat),
- long: Number(race.Circuit.Location.long),
- },
- },
- })),
- };
-}
-
-function getEventsSchedule(raceSchedule: RaceTable): RaceEvent[] {
- return raceSchedule.Races.reduce((eventList: RaceEvent[], current) => {
- const id = current.round.toString();
-
- if (current.Sprint) {
- eventList.push({
- Race: current,
- eventType: RaceType.SPRINT_RACE,
- id: id + "-s",
- });
- }
-
- eventList.push({
- Race: current,
- eventType: RaceType.GRAND_PRIX,
- id,
- });
- return eventList;
- }, []);
-}
-
-export function useRaceSchedule() {
- const { data, isLoading } = useQuery({
- queryKey: ["raceSchedule"],
- queryFn: getRaceSchedule,
- });
-
- const raceScheduleObject = data ? transformRaceSchedule(data) : undefined;
-
- const eventSchedule = raceScheduleObject
- ? getEventsSchedule(raceScheduleObject)
- : [];
-
- return {
- raceSchedule: raceScheduleObject,
- eventSchedule,
- isLoading,
- };
-}
diff --git a/src/changelog/Changelog.tsx b/src/changelog/Changelog.tsx
index 3c2e581..4eb8095 100644
--- a/src/changelog/Changelog.tsx
+++ b/src/changelog/Changelog.tsx
@@ -1,13 +1,16 @@
-import { useQuery } from "@tanstack/react-query";
+import { useEffect } from "react";
import Markdown from "react-markdown";
import { css } from "../../styled-system/css";
+import { useStore } from "../store";
export function Changelog() {
- const { data: changelog } = useQuery({
- queryKey: ["changelog"],
- queryFn: () => fetch("/changelog.md").then((res) => res.text()),
- });
+ const changelog = useStore((s) => s.changelog);
+ const fetchChangelog = useStore((s) => s.fetchChangelog);
+
+ useEffect(() => {
+ fetchChangelog();
+ }, [fetchChangelog]);
if (!changelog) return null;
diff --git a/src/components/StandingsController.tsx b/src/components/StandingsController.tsx
index b3ac8db..c7f247c 100644
--- a/src/components/StandingsController.tsx
+++ b/src/components/StandingsController.tsx
@@ -1,9 +1,7 @@
import { useState } from "react";
-import { useConstructorStandings } from "../api/useConstructorStandings";
-import { useDriverStandings } from "../api/useDriverStandings";
-import { useRaceSchedule } from "../api/useRaceSchedule";
import { getStandingsAfterRounds } from "../services/standings";
+import { useStore } from "../store";
import { RaceType, UpcomingRaceResult } from "../types/entities";
import { LoadingLayout } from "./LoadingLayout";
import { NoSeasonData } from "./NoSeasonData";
@@ -11,24 +9,15 @@ import { Standings } from "./Standings/Standings";
import { UpcomingRaceResultList } from "./UpcomingRaceResults/UpcomingRaceResultList";
export function StandingsController() {
- const {
- raceSchedule,
- eventSchedule,
- isLoading: isRaceScheduleLoading,
- } = useRaceSchedule();
- const { driverStandings, isLoading: isDriverStandingsLoading } =
- useDriverStandings();
- const { constructorStandings, isLoading: isConstructorStandingsLoading } =
- useConstructorStandings();
+ const raceSchedule = useStore((s) => s.raceSchedule);
+ const eventSchedule = useStore((s) => s.eventSchedule);
+ const driverStandings = useStore((s) => s.driverStandings);
+ const constructorStandings = useStore((s) => s.constructorStandings);
+ const isLoading = useStore((s) => s.isLoading);
const [upcomingRaceResultList, setUpcomingRaceResultList] = useState<
UpcomingRaceResult[]
>([]);
-
- const isLoading =
- isRaceScheduleLoading ||
- isDriverStandingsLoading ||
- isConstructorStandingsLoading;
const hasRequiredData =
raceSchedule &&
driverStandings?.DriverStandings &&
diff --git a/src/store.ts b/src/store.ts
new file mode 100644
index 0000000..0378e15
--- /dev/null
+++ b/src/store.ts
@@ -0,0 +1,79 @@
+import { create } from "zustand";
+
+import { fetchApi } from "./api/api";
+import {
+ getEventsSchedule,
+ transformConstructorStandings,
+ transformDriverStandings,
+ transformRaceSchedule,
+} from "./api/transforms";
+import {
+ IConstructorStandings,
+ IDriverStandings,
+ IRaceSchedule,
+} from "./types/api";
+import { RaceEvent, RaceTable, StandingsList } from "./types/entities";
+
+interface F1Store {
+ driverStandings: StandingsList | undefined;
+ constructorStandings: StandingsList | undefined;
+ raceSchedule: RaceTable | undefined;
+ eventSchedule: RaceEvent[];
+ changelog: string | undefined;
+ isLoading: boolean;
+ isError: boolean;
+ fetchAll: () => Promise;
+ fetchChangelog: () => Promise;
+}
+
+export const useStore = create((set, get) => ({
+ driverStandings: undefined,
+ constructorStandings: undefined,
+ raceSchedule: undefined,
+ eventSchedule: [],
+ changelog: undefined,
+ isLoading: false,
+ isError: false,
+
+ fetchAll: async () => {
+ if (get().isLoading || get().driverStandings) return;
+
+ set({ isLoading: true, isError: false });
+
+ try {
+ const [driverData, constructorData, raceData] = await Promise.all([
+ fetchApi("driverStandings"),
+ fetchApi("constructorStandings"),
+ fetchApi("raceSchedule"),
+ ]);
+
+ const driverStandings = transformDriverStandings(driverData);
+ const constructorStandings =
+ transformConstructorStandings(constructorData);
+ const raceSchedule = transformRaceSchedule(raceData);
+ const eventSchedule = raceSchedule ? getEventsSchedule(raceSchedule) : [];
+
+ set({
+ driverStandings,
+ constructorStandings,
+ raceSchedule,
+ eventSchedule,
+ isLoading: false,
+ });
+ } catch {
+ set({ isLoading: false, isError: true });
+ }
+ },
+
+ fetchChangelog: async () => {
+ if (get().changelog) return;
+
+ try {
+ const response = await fetch("/changelog.md");
+ const text = await response.text();
+ set({ changelog: text });
+ } catch {
+ // Changelog is non-critical, silently fail
+ }
+ },
+}));