diff --git a/src/app/[locale]/(main)/elo/page.tsx b/src/app/[locale]/(main)/elo/page.tsx index 79cf1ce..4df5e43 100644 --- a/src/app/[locale]/(main)/elo/page.tsx +++ b/src/app/[locale]/(main)/elo/page.tsx @@ -10,7 +10,9 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { FormProvider, useForm } from "react-hook-form"; import { z } from "zod"; +import { ErrorBox } from "@/components/ErrorBox"; import { Spinner } from "@/components/Spinner"; +import { TranslatedError } from "@/components/TranslatedError"; import { SelectField } from "@/components/form/SelectField"; import { TournamentSelectField } from "@/components/form/TournamentSelectField"; import { fetchTournamentResultsSchema } from "@/schemas"; @@ -170,10 +172,6 @@ export default function Elo() { } }, [searchParams, form, player, kFactor, tournamentId]); - if (error) { - console.error(error); - } - const playerResults = allResults?.data?.find((p) => p.id === player); return ( @@ -278,6 +276,12 @@ export default function Elo() { /> )} + {(!!error || allResults?.serverError) && ( + } + /> + )} + {((!hasTournamentId && !isFetching) || !!error) && ( <>
diff --git a/src/components/ErrorBox.tsx b/src/components/ErrorBox.tsx new file mode 100644 index 0000000..bc8d7c4 --- /dev/null +++ b/src/components/ErrorBox.tsx @@ -0,0 +1,16 @@ +type ErrorBoxProps = { title?: React.ReactNode; error?: React.ReactNode }; + +export const ErrorBox = ({ title, error }: ErrorBoxProps) => { + if (!error) return null; + + return ( +
+
+ {title &&
{title}
} +
+

{error}

+
+
+
+ ); +}; diff --git a/src/components/TranslatedError.tsx b/src/components/TranslatedError.tsx new file mode 100644 index 0000000..d8cd17e --- /dev/null +++ b/src/components/TranslatedError.tsx @@ -0,0 +1,36 @@ +import isNil from "lodash/isNil"; +import { useTranslations } from "next-intl"; + +import { Path } from "@/types"; + +type TranslatedErrorProps = { + err: unknown; +}; + +export const TranslatedError = ({ err }: TranslatedErrorProps) => { + const t = useTranslations("Errors"); + + if (isNil(err)) { + return null; + } + + let error: string | undefined; + + if (typeof err === "string") { + error = err; + } + + if (isNil(error) && err instanceof Error) { + error = err.message; + } + + if (isNil(error)) { + error = "ERR_UNEXPECTED"; + } + + if (error?.startsWith("ERR_")) { + return t(error as Path); + } + + return {error}; +}; diff --git a/src/messages/en.json b/src/messages/en.json index 156c9ea..86dca41 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -16,6 +16,13 @@ "timeControlEnum": "{tc, select, Classic {Classic} Rapid {Rapid} Blitz {Blitz} Other {Other} other {{tc}}}" }, + "Errors": { + "ERR_UNEXPECTED": "An unexpected error occurred", + "ERR_TOURNAMENT_NOT_FOUND": "Tournament not found", + "ERR_TOURNAMENT_RESULTS_NOT_AVAILABLE": "Results are not yet available for this tournament", + "ERR_NO_TOURNAMENT_ID": "The link you provided is incorrect" + }, + "Nav": { "title": "Échecs France", "tournaments": "Tournaments", @@ -191,8 +198,6 @@ "kFactorInfo5": "10 once a player's published rating has reached 2400 and remains at that level subsequently, even if the rating drops below 2400.", "kFactorInfo6": "40 for all players until their 18th birthday, as long as their rating remains under 2300.", - "unknownError": "We were unable to fetch results from this URL. Please contact us if the problem persists.", - "ERR_TOURNAMENT_NOT_FOUND": "Tournament not found", - "ERR_NO_TOURNAMENT_ID": "The link you provided is incorrect" + "unknownError": "We were unable to fetch results from this URL. Please contact us if the problem persists." } } diff --git a/src/messages/fr.json b/src/messages/fr.json index 5db3364..390ed08 100644 --- a/src/messages/fr.json +++ b/src/messages/fr.json @@ -16,6 +16,13 @@ "timeControlEnum": "{tc, select, Classic {Cadence Lente} Rapid {Rapide} Blitz {Blitz} Other {Autres} other {{tc}}}" }, + "Errors": { + "ERR_UNEXPECTED": "Une erreur inattendue est survenue", + "ERR_TOURNAMENT_NOT_FOUND": "Tournoi introuvable", + "ERR_TOURNAMENT_RESULTS_NOT_AVAILABLE": "Les résultats ne sont pas encore disponibles pour ce tournoi.", + "ERR_NO_TOURNAMENT_ID": "L'identifiant du tournoi fourni est incorrect" + }, + "Nav": { "title": "Échecs France", "tournaments": "Tournois", @@ -192,8 +199,6 @@ "kFactorInfo5": "10 une fois que le classement publié d'un joueur a atteint 2400 et reste à ce niveau par la suite, même s'il redescend en dessous de 2400.", "kFactorInfo6": "40 pour tous les joueurs jusqu'à leur 18e anniversaire, tant que leur classement reste inférieur à 2300.", - "unknownError": "Nous n'avons pas pu récupérer les résultats à partir de cette URL. Veuillez nous contacter si le problème persiste.", - "ERR_TOURNAMENT_NOT_FOUND": "Tournoi introuvable", - "ERR_NO_TOURNAMENT_ID": "L'identifiant du tournoi fourni est incorrect" + "unknownError": "Nous n'avons pas pu récupérer les résultats à partir de cette URL. Veuillez nous contacter si le problème persiste." } } diff --git a/src/server/fetchTournamentResults.ts b/src/server/fetchTournamentResults.ts index 91f3654..ad18bc6 100644 --- a/src/server/fetchTournamentResults.ts +++ b/src/server/fetchTournamentResults.ts @@ -124,6 +124,10 @@ export const fetchTournamentResults = action( }, ); + if (rawResults.status >= 500) { + throw new Error("ERR_TOURNAMENT_RESULTS_NOT_AVAILABLE"); + } + const results = await rawResults.json(); if ("detail" in results && results.detail === "Not found") { @@ -131,6 +135,7 @@ export const fetchTournamentResults = action( } const parsedResults = dbSchema.parse(results); + return outputSchema.parse( parsedResults.map[number]>((player) => ({ id: player.id, diff --git a/src/server/safeAction.ts b/src/server/safeAction.ts index 45214ea..98678c7 100644 --- a/src/server/safeAction.ts +++ b/src/server/safeAction.ts @@ -1,3 +1,7 @@ import { createSafeActionClient } from "next-safe-action"; -export const action = createSafeActionClient(); +export const action = createSafeActionClient({ + handleReturnedServerError(error) { + return error.message; + }, +}); diff --git a/src/types.ts b/src/types.ts index 8373de5..acb2168 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,5 +93,11 @@ export type Prettify = { [K in keyof T]: T[K]; } & {}; +export type Path = K extends string + ? T[K] extends Record + ? `${K}.${Path}` + : K + : never; + export type ExtractSafeActionResult = T extends SafeAction ? Result : never;