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 (
+
+ );
+};
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;