From 1b39498ba1cd460405c355ccc3695eb6bc772598 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 30 Aug 2024 19:04:56 +0300 Subject: [PATCH 1/5] feat(error-handling): implement a way to handle errors that occur inside server components --- .../components/research_and_updates.tsx | 143 ++++----- .../(home)/components/tournaments_block.tsx | 89 +++--- .../components/project_contributions.tsx | 295 +++++++++--------- .../components/project_leaderboard.tsx | 65 ++-- .../medals/components/medals_page.tsx | 158 +++++----- .../components/sidebar/news_match/index.tsx | 13 +- .../sidebar/similar_questions/index.tsx | 19 +- front_end/src/app/(main)/questions/page.tsx | 2 +- front_end/src/components/posts_feed/index.tsx | 39 ++- front_end/src/components/refresh_button.tsx | 17 + .../server_component_error_boundary.tsx | 25 ++ 11 files changed, 468 insertions(+), 397 deletions(-) create mode 100644 front_end/src/components/refresh_button.tsx create mode 100644 front_end/src/components/server_component_error_boundary.tsx diff --git a/front_end/src/app/(main)/(home)/components/research_and_updates.tsx b/front_end/src/app/(main)/(home)/components/research_and_updates.tsx index 7e909285a5..16a0eeab24 100644 --- a/front_end/src/app/(main)/(home)/components/research_and_updates.tsx +++ b/front_end/src/app/(main)/(home)/components/research_and_updates.tsx @@ -10,85 +10,88 @@ import imagePlaceholder from "@/app/assets/images/tournament.webp"; import PostsApi from "@/services/posts"; import { PostWithNotebook } from "@/types/post"; import { getNotebookSummary } from "@/utils/questions"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { postIds: number[]; }; const ResearchAndUpdatesBlock: FC = async ({ postIds }) => { - const t = await getTranslations(); - const locale = await getLocale(); - const { results } = await PostsApi.getPostsWithCP({ - ids: postIds, - }); - const posts = results as PostWithNotebook[]; + return ServerComponentErrorBoundary(async () => { + const t = await getTranslations(); + const locale = await getLocale(); + const { results } = await PostsApi.getPostsWithCP({ + ids: postIds, + }); + const posts = results as PostWithNotebook[]; - return ( -
-

- {t("research")} &{" "} - - {t("updates")} - -

-

- {t("partnersUseForecasts")} -

-
- {posts.map(({ title, created_at, id, notebook, url_title }) => ( - - {notebook.image_url && notebook.image_url.startsWith("https:") ? ( - - ) : ( - - )} + return ( +
+

+ {t("research")} &{" "} + + {t("updates")} + +

+

+ {t("partnersUseForecasts")} +

+
+ {posts.map(({ title, created_at, id, notebook, url_title }) => ( + + {notebook.image_url && notebook.image_url.startsWith("https:") ? ( + + ) : ( + + )} -
- - {intlFormat( - new Date(created_at), - { - year: "numeric", - month: "short", - }, - { locale } - )} - -

{title}

-

- {getNotebookSummary(notebook.markdown, 200, 80)} -

-
- - ))} +
+ + {intlFormat( + new Date(created_at), + { + year: "numeric", + month: "short", + }, + { locale } + )} + +

{title}

+

+ {getNotebookSummary(notebook.markdown, 200, 80)} +

+
+ + ))} +
+ + {t("seeMorePosts")} + +
- - {t("seeMorePosts")} - - -
- ); + ); + }); }; export default ResearchAndUpdatesBlock; diff --git a/front_end/src/app/(main)/(home)/components/tournaments_block.tsx b/front_end/src/app/(main)/(home)/components/tournaments_block.tsx index de7f11c305..15534710bf 100644 --- a/front_end/src/app/(main)/(home)/components/tournaments_block.tsx +++ b/front_end/src/app/(main)/(home)/components/tournaments_block.tsx @@ -5,6 +5,7 @@ import { getTranslations } from "next-intl/server"; import { FC } from "react"; import TournamentCard from "@/components/tournament_card"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; import ProjectsApi from "@/services/projects"; import { Tournament, TournamentType } from "@/types/projects"; @@ -13,51 +14,53 @@ type Props = { }; const TournamentsBlock: FC = async ({ postSlugs }) => { - const t = await getTranslations(); - const tournamentPromises = postSlugs.map( - (slug) => ProjectsApi.getSlugTournament(slug) as Promise - ); - const tournaments = await Promise.all(tournamentPromises); + return ServerComponentErrorBoundary(async () => { + const t = await getTranslations(); + const tournamentPromises = postSlugs.map( + (slug) => ProjectsApi.getSlugTournament(slug) as Promise + ); + const tournaments = await Promise.all(tournamentPromises); - return ( -
-

- {t("forecasting")}{" "} - - {t("tournaments")} - -

-

- {t("joinTournaments")} -

-
- {tournaments.map((tournament) => ( - - ))} + return ( +
+

+ {t("forecasting")}{" "} + + {t("tournaments")} + +

+

+ {t("joinTournaments")} +

+
+ {tournaments.map((tournament) => ( + + ))} +
+ + {t("seeAllTournaments")} + +
- - {t("seeAllTournaments")} - - -
- ); + ); + }); }; export default TournamentsBlock; diff --git a/front_end/src/app/(main)/(leaderboards)/contributions/components/project_contributions.tsx b/front_end/src/app/(main)/(leaderboards)/contributions/components/project_contributions.tsx index 25ce312dc7..cd5bd6f35f 100644 --- a/front_end/src/app/(main)/(leaderboards)/contributions/components/project_contributions.tsx +++ b/front_end/src/app/(main)/(leaderboards)/contributions/components/project_contributions.tsx @@ -6,6 +6,7 @@ import InfoToggle from "@/components/ui/info_toggle"; import SectionToggle from "@/components/ui/section_toggle"; import LeaderboardApi from "@/services/leaderboard"; import { Tournament } from "@/types/projects"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { project: Tournament; @@ -13,169 +14,175 @@ type Props = { }; const ProjectContributions: FC = async ({ project, userId }) => { - const t = await getTranslations(); - const contributionsDetails = await LeaderboardApi.getContributions({ - type: "project", - userId, - projectId: project.id, - }); + return ServerComponentErrorBoundary(async () => { + const t = await getTranslations(); + const contributionsDetails = await LeaderboardApi.getContributions({ + type: "project", + userId, + projectId: project.id, + }); - return ( - - {!!contributionsDetails.contributions.length && ( - - - - - - {project.score_type === "relative_legacy_tournament" && ( + return ( + + {!!contributionsDetails.contributions.length && ( +
- {t("Question")} - {t("score")}
+ + + - )} - - - - {contributionsDetails.contributions.map((contribution, i) => ( - - + )} + + + + {contributionsDetails.contributions.map((contribution, i) => ( + + + + {project.score_type === "relative_legacy_tournament" && ( + + )} + + ))} + + + + + + + + + + + {project.score_type === "relative_legacy_tournament" && ( )} - ))} - + +
+ {t("Question")} + - {t("coverage")} + {t("score")}
- - {contribution.question_title} - + {project.score_type === "relative_legacy_tournament" && ( + + {t("coverage")} +
+ + {contribution.question_title} + + + {contribution.score ? contribution.score.toFixed(2) : "-"} + + {contribution.coverage + ? `${(contribution.coverage * 100).toFixed(0)}%` + : "0%"} +
+ {t("totalTake")} + + {contributionsDetails.leaderboard_entry.take + ? `${contributionsDetails.leaderboard_entry.take.toFixed(3)}` + : "-"}
+ {t("totalScore")} + - {contribution.score ? contribution.score.toFixed(2) : "-"} + {contributionsDetails.leaderboard_entry.score + ? `${contributionsDetails.leaderboard_entry.score.toFixed(2)}` + : "-"} - {contribution.coverage - ? `${(contribution.coverage * 100).toFixed(0)}%` + {contributionsDetails.leaderboard_entry.coverage + ? `${(contributionsDetails.leaderboard_entry.coverage * 100).toFixed(2)}%` : "0%"}
+ )} - - - {t("totalTake")} - - {contributionsDetails.leaderboard_entry.take - ? `${contributionsDetails.leaderboard_entry.take.toFixed(3)}` - : "-"} - - - - - - - {t("totalScore")} - - - {contributionsDetails.leaderboard_entry.score - ? `${contributionsDetails.leaderboard_entry.score.toFixed(2)}` - : "-"} - + +
+
+
+
{t("score")}
+ {project.score_type === "peer_tournament" ? ( +
+ {t.rich("peerScoreInfo", { + link: (chunks) => ( + + {chunks} + + ), + })} +
+ ) : ( +
+ {t.rich("relativeScoreInfo", { + link: (chunks) => ( + + {chunks} + + ), + })} +
+ )} +
+
+
+ {t("totalScore")} +
+ {project.score_type === "peer_tournament" ? ( +
+ {t.rich("totalPeerScoreInfo", { + link: (chunks) => ( + + {chunks} + + ), + })} +
+ ) : ( +
+ {t.rich("totalRelativeScoreInfo", { + link: (chunks) => ( + + {chunks} + + ), + })} +
+ )} +
{project.score_type === "relative_legacy_tournament" && ( - - {contributionsDetails.leaderboard_entry.coverage - ? `${(contributionsDetails.leaderboard_entry.coverage * 100).toFixed(2)}%` - : "0%"} - - )} - - - - )} - - -
-
-
-
{t("score")}
- {project.score_type === "peer_tournament" ? ( -
- {t.rich("peerScoreInfo", { - link: (chunks) => ( - - {chunks} - - ), - })} -
- ) : ( -
- {t.rich("relativeScoreInfo", { - link: (chunks) => ( - - {chunks} - - ), - })} -
- )} -
-
-
- {t("totalScore")} -
- {project.score_type === "peer_tournament" ? ( -
- {t.rich("totalPeerScoreInfo", { - link: (chunks) => ( - - {chunks} - - ), - })} -
- ) : ( -
- {t.rich("totalRelativeScoreInfo", { - link: (chunks) => ( - - {chunks} - - ), - })} -
+
+
+ {t("coverage")} +
+
{t("relativeCoverageInfo")}
+
)} -
- {project.score_type === "relative_legacy_tournament" && (
- {t("coverage")} + {t("totalTake")}
-
{t("relativeCoverageInfo")}
+ {project.score_type === "peer_tournament" ? ( +
{t("peerTakeInfo")}
+ ) : ( +
{t("relativeTakeInfo")}
+ )}
- )} -
-
- {t("totalTake")} -
- {project.score_type === "peer_tournament" ? ( -
{t("peerTakeInfo")}
- ) : ( -
{t("relativeTakeInfo")}
- )} -
-
-
-
- - ); +
+
+
+
+ ); + }); }; export default ProjectContributions; diff --git a/front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard.tsx b/front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard.tsx index 676bbb9d29..b1975462d1 100644 --- a/front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard.tsx +++ b/front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard.tsx @@ -7,6 +7,7 @@ import LeaderboardApi from "@/services/leaderboard"; import { LeaderboardType } from "@/types/scoring"; import ProjectLeaderboardTable from "./project_leaderboard_table"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { projectId: number; @@ -23,37 +24,39 @@ const ProjectLeaderboard: FC = async ({ isQuestionSeries, userId, }) => { - const leaderboardDetails = await LeaderboardApi.getProjectLeaderboard( - projectId, - leaderboardType - ); - - if (!leaderboardDetails || !leaderboardDetails.entries.length) { - return null; - } - - const prizePoolValue = !isNaN(Number(prizePool)) ? Number(prizePool) : 0; - - const t = await getTranslations(); - - const leaderboardTitle = isQuestionSeries - ? t("openLeaderboard") - : t("leaderboard"); - - return ( - - - - ); + return ServerComponentErrorBoundary(async () => { + const leaderboardDetails = await LeaderboardApi.getProjectLeaderboard( + projectId, + leaderboardType + ); + + if (!leaderboardDetails || !leaderboardDetails.entries.length) { + return null; + } + + const prizePoolValue = !isNaN(Number(prizePool)) ? Number(prizePool) : 0; + + const t = await getTranslations(); + + const leaderboardTitle = isQuestionSeries + ? t("openLeaderboard") + : t("leaderboard"); + + return ( + + + + ); + }); }; export default ProjectLeaderboard; diff --git a/front_end/src/app/(main)/(leaderboards)/medals/components/medals_page.tsx b/front_end/src/app/(main)/(leaderboards)/medals/components/medals_page.tsx index 81d72a4a42..70a8554e1f 100644 --- a/front_end/src/app/(main)/(leaderboards)/medals/components/medals_page.tsx +++ b/front_end/src/app/(main)/(leaderboards)/medals/components/medals_page.tsx @@ -1,108 +1,100 @@ import classNames from "classnames"; -import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { FC } from "react"; -import Tooltip from "@/components/ui/tooltip"; import LeaderboardApi from "@/services/leaderboard"; import MedalIcon from "../../components/medal_icon"; import { RANKING_CATEGORIES } from "../../ranking_categories"; -import { CONTRIBUTIONS_USER_FILTER } from "../../contributions/search_params"; - -import { - SCORING_CATEGORY_FILTER, - SCORING_YEAR_FILTER, -} from "../../search_params"; import { getMedalCategories } from "../helpers/medal_categories"; import { getMedalDisplayTitle } from "../helpers/medal_title"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { profileId: number; }; const MedalsPage: FC = async ({ profileId }) => { - const t = await getTranslations(); + return ServerComponentErrorBoundary(async () => { + const t = await getTranslations(); - const userMedals = await LeaderboardApi.getUserMedals(profileId); - const categories = getMedalCategories(userMedals, true); - type MedalType = "gold" | "silver" | "bronze"; + const userMedals = await LeaderboardApi.getUserMedals(profileId); + const categories = getMedalCategories(userMedals, true); + type MedalType = "gold" | "silver" | "bronze"; - function getMedalClassName(medalType: MedalType): string { - switch (medalType) { - case "gold": - return "bg-blue-700/5 dark:bg-blue-950 md:bg-white md:bg-gradient-to-b from-[#F6D84D]/0 dark:from-blue-950 dark:to-blue-950 md:from-[#F6D84D]/30 dark:md:from-[#F6D84D]/20 from-0% to-30% to-white md:dark:to-blue-950/75"; - case "silver": - return "bg-blue-700/5 dark:bg-blue-950 md:bg-white md:bg-gradient-to-b from-[#A7B1C0]/0 dark:from-blue-950 dark:to-blue-950 md:from-[#A7B1C0]/15 dark:md:from-[#A7B1C0]/15 dark:from-[#A7B1C0]/25 from-0% to-30% to-white md:dark:to-blue-950/75"; - case "bronze": - return "bg-blue-700/5 dark:bg-blue-950 md:bg-white md:bg-gradient-to-b from-[#F09B59]/0 dark:from-blue-950 dark:to-blue-950 md:from-[#F09B59]/20 dark:md:from-[#F09B59]/20 from-0% to-30% to-white md:dark:to-blue-950/75"; - default: - return ""; + function getMedalClassName(medalType: MedalType): string { + switch (medalType) { + case "gold": + return "bg-blue-700/5 dark:bg-blue-950 md:bg-white md:bg-gradient-to-b from-[#F6D84D]/0 dark:from-blue-950 dark:to-blue-950 md:from-[#F6D84D]/30 dark:md:from-[#F6D84D]/20 from-0% to-30% to-white md:dark:to-blue-950/75"; + case "silver": + return "bg-blue-700/5 dark:bg-blue-950 md:bg-white md:bg-gradient-to-b from-[#A7B1C0]/0 dark:from-blue-950 dark:to-blue-950 md:from-[#A7B1C0]/15 dark:md:from-[#A7B1C0]/15 dark:from-[#A7B1C0]/25 from-0% to-30% to-white md:dark:to-blue-950/75"; + case "bronze": + return "bg-blue-700/5 dark:bg-blue-950 md:bg-white md:bg-gradient-to-b from-[#F09B59]/0 dark:from-blue-950 dark:to-blue-950 md:from-[#F09B59]/20 dark:md:from-[#F09B59]/20 from-0% to-30% to-white md:dark:to-blue-950/75"; + default: + return ""; + } } - } - return ( -
-
- {categories?.map((category, index) => ( -
-
- - {t(RANKING_CATEGORIES[category.name].translationKey)} - -
-
- {!!category.medals.length ? ( - category.medals.map((medal, index) => { - return ( - -
-
-
- -
-
- - {getMedalDisplayTitle(medal)} - - - {t("rank")}: - #{medal.rank}{" "} - - {t("outOfRank", { total: medal.totalEntries })} + return ( +
+
+ {categories?.map((category, index) => ( +
+
+ + {t(RANKING_CATEGORIES[category.name].translationKey)} + +
+
+ {!!category.medals.length ? ( + category.medals.map((medal, index) => { + return ( +
+
+
+
+ +
+
+ + {getMedalDisplayTitle(medal)} + + + {t("rank")}: + + #{medal.rank} + {" "} + + {t("outOfRank", { total: medal.totalEntries })} + - +
- - ); - }) - ) : ( - - {t("noMedals")} - - )} + ); + }) + ) : ( + + {t("noMedals")} + + )} +
-
- ))} -
-
- ); + ))} +
+ + ); + }); }; export default MedalsPage; diff --git a/front_end/src/app/(main)/questions/[id]/components/sidebar/news_match/index.tsx b/front_end/src/app/(main)/questions/[id]/components/sidebar/news_match/index.tsx index 9d115ca944..330722e952 100644 --- a/front_end/src/app/(main)/questions/[id]/components/sidebar/news_match/index.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/sidebar/news_match/index.tsx @@ -3,6 +3,7 @@ import { FC } from "react"; import PostsApi from "@/services/posts"; import NewsMatchDrawer from "./news_match_drawer"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; interface Props { questionId: number; @@ -13,11 +14,15 @@ const fetchArticles = async (postId: number) => { }; const NewsMatch: FC = async ({ questionId }) => { - const articles = await fetchArticles(questionId); + return ServerComponentErrorBoundary(async () => { + const articles = await fetchArticles(questionId); - if (articles.length > 0) { - return ; - } + if (articles.length > 0) { + return ; + } else { + return null; + } + }); }; export default NewsMatch; diff --git a/front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx b/front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx index 5cf1b24ee6..dbe22f73ca 100644 --- a/front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx @@ -5,22 +5,25 @@ import { PostStatus } from "@/types/post"; import { QuestionOrder } from "@/types/question"; import SimilarQuestionsDrawer from "./similar_questions_drawer"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { post_id: number; }; const SimilarQuestions: FC = async ({ post_id }) => { - const response = await PostsApi.getPostsWithCP({ - similar_to_post_id: post_id, - order_by: QuestionOrder.PredictionCountDesc, - statuses: PostStatus.OPEN, - }); - const { results: questions } = response; + return ServerComponentErrorBoundary(async () => { + const response = await PostsApi.getPostsWithCP({ + similar_to_post_id: post_id, + order_by: QuestionOrder.PredictionCountDesc, + statuses: PostStatus.OPEN, + }); + const { results: questions } = response; - if (!questions.length) return null; + if (!questions.length) return null; - return ; + return ; + }); }; export default SimilarQuestions; diff --git a/front_end/src/app/(main)/questions/page.tsx b/front_end/src/app/(main)/questions/page.tsx index 12b4840bd5..43ab17a2b3 100644 --- a/front_end/src/app/(main)/questions/page.tsx +++ b/front_end/src/app/(main)/questions/page.tsx @@ -34,7 +34,7 @@ export default async function Questions({ } > - +
diff --git a/front_end/src/components/posts_feed/index.tsx b/front_end/src/components/posts_feed/index.tsx index 0ee317206c..4ab75176cc 100644 --- a/front_end/src/components/posts_feed/index.tsx +++ b/front_end/src/components/posts_feed/index.tsx @@ -5,26 +5,39 @@ import PaginatedPostsFeed, { } from "@/components/posts_feed/paginated_feed"; import { POSTS_PER_PAGE } from "@/constants/posts_feed"; import PostsApi, { PostsParams } from "@/services/posts"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import { Topic } from "@/types/projects"; type Props = { filters: PostsParams; type?: PostsFeedType; + topics?: Topic[]; }; -const AwaitedPostsFeed: FC = async ({ filters, type }) => { - const { results: questions, count } = await PostsApi.getPostsWithCP({ - ...filters, - limit: POSTS_PER_PAGE, - }); +const AwaitedPostsFeed: FC = async ({ filters, type, topics }) => { + return ServerComponentErrorBoundary(async () => { + if (topics && !topics?.some((topic) => topic.slug === filters.topic)) { + return ( +
+ Such topic does not exist +
+ ); + } + + const { results: questions, count } = await PostsApi.getPostsWithCP({ + ...filters, + limit: POSTS_PER_PAGE, + }); - return ( - - ); + return ( + + ); + }); }; export default AwaitedPostsFeed; diff --git a/front_end/src/components/refresh_button.tsx b/front_end/src/components/refresh_button.tsx new file mode 100644 index 0000000000..ea7b7ca96a --- /dev/null +++ b/front_end/src/components/refresh_button.tsx @@ -0,0 +1,17 @@ +"use client"; + +import React from "react"; +import { useRouter } from "next/navigation"; +import Button from "./ui/button"; + +const RefreshButton: React.FC = () => { + const router = useRouter(); + + return ( + + ); +}; + +export default RefreshButton; diff --git a/front_end/src/components/server_component_error_boundary.tsx b/front_end/src/components/server_component_error_boundary.tsx new file mode 100644 index 0000000000..ad252a342a --- /dev/null +++ b/front_end/src/components/server_component_error_boundary.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { notFound } from "next/navigation"; +import RefreshButton from "./refresh_button"; + +const ServerComponentErrorBoundary = async ( + fn: () => Promise +) => { + try { + return await fn(); + } catch (error) { + if (error instanceof Error) { + if (error.message.includes("404")) return notFound(); + return ( +
+

{error.message ?? "Unknown error"}

+ +
+ ); + } + + throw error; + } +}; + +export default ServerComponentErrorBoundary; From b60116eaa27c564c23da263b4d1f3a467c0179dc Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 30 Aug 2024 19:04:59 +0300 Subject: [PATCH 2/5] feat(erro-handling): wrap server-side components in custom error boundary --- .../components/contributions_hero.tsx | 9 +- .../components/global_contributions.tsx | 31 +-- .../components/global_leaderboard.tsx | 31 +-- .../medals/components/medals_widget.tsx | 139 +++++++------- .../elections/components/card_forecast.tsx | 19 +- .../index.tsx | 123 ++++++------ .../components/state_by_forecast/index.tsx | 177 +++++++++--------- .../discovery/components/tags_discovery.tsx | 33 ++-- 8 files changed, 294 insertions(+), 268 deletions(-) diff --git a/front_end/src/app/(main)/(leaderboards)/contributions/components/contributions_hero.tsx b/front_end/src/app/(main)/(leaderboards)/contributions/components/contributions_hero.tsx index f4d2fa114f..9e0ab98f15 100644 --- a/front_end/src/app/(main)/(leaderboards)/contributions/components/contributions_hero.tsx +++ b/front_end/src/app/(main)/(leaderboards)/contributions/components/contributions_hero.tsx @@ -16,6 +16,7 @@ import { SCORING_DURATION_FILTER, SCORING_YEAR_FILTER, } from "../../search_params"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { category: CategoryKey; @@ -73,10 +74,12 @@ const ContributionsHero: FC = ({ year, duration, category, userId }) => { }; const AwaitedUserHeader: FC<{ userId: number }> = async ({ userId }) => { - const t = await getTranslations(); - const profile = await ProfileApi.getProfileById(userId); + return ServerComponentErrorBoundary(async () => { + const t = await getTranslations(); + const profile = await ProfileApi.getProfileById(userId); - return {profile.username ?? t("user")}; + return {profile.username ?? t("user")}; + }); }; const UserHeader: FC = ({ children }) => ( diff --git a/front_end/src/app/(main)/(leaderboards)/contributions/components/global_contributions.tsx b/front_end/src/app/(main)/(leaderboards)/contributions/components/global_contributions.tsx index 254b769073..288e0094d8 100644 --- a/front_end/src/app/(main)/(leaderboards)/contributions/components/global_contributions.tsx +++ b/front_end/src/app/(main)/(leaderboards)/contributions/components/global_contributions.tsx @@ -3,6 +3,7 @@ import { FC } from "react"; import ContributionsTable from "@/app/(main)/(leaderboards)/contributions/components/contributions_table"; import LeaderboardApi from "@/services/leaderboard"; import { CategoryKey, LeaderboardType } from "@/types/scoring"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { startTime: string; @@ -19,21 +20,23 @@ const GlobalContributions: FC = async ({ startTime, category, }) => { - const contributionsDetails = await LeaderboardApi.getContributions({ - type: "global", - leaderboardType, - userId, - startTime, - endTime, - }); + return ServerComponentErrorBoundary(async () => { + const contributionsDetails = await LeaderboardApi.getContributions({ + type: "global", + leaderboardType, + userId, + startTime, + endTime, + }); - return ( - - ); + return ( + + ); + }); }; export default GlobalContributions; diff --git a/front_end/src/app/(main)/(leaderboards)/leaderboard/components/global_leaderboard.tsx b/front_end/src/app/(main)/(leaderboards)/leaderboard/components/global_leaderboard.tsx index 3012508807..bca292270f 100644 --- a/front_end/src/app/(main)/(leaderboards)/leaderboard/components/global_leaderboard.tsx +++ b/front_end/src/app/(main)/(leaderboards)/leaderboard/components/global_leaderboard.tsx @@ -4,6 +4,7 @@ import LeaderboardApi from "@/services/leaderboard"; import { CategoryKey, LeaderboardType } from "@/types/scoring"; import LeaderboardTable from "./leaderboard_table"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { startTime: string; @@ -24,21 +25,23 @@ const GlobalLeaderboard: FC = async ({ category, cardSized, }) => { - const leaderboardDetails = await LeaderboardApi.getGlobalLeaderboard( - startTime, - endTime, - leaderboardType - ); + return ServerComponentErrorBoundary(async () => { + const leaderboardDetails = await LeaderboardApi.getGlobalLeaderboard( + startTime, + endTime, + leaderboardType + ); - return ( - - ); + return ( + + ); + }); }; export default GlobalLeaderboard; diff --git a/front_end/src/app/(main)/(leaderboards)/medals/components/medals_widget.tsx b/front_end/src/app/(main)/(leaderboards)/medals/components/medals_widget.tsx index 90f82eb14a..41e84a32c9 100644 --- a/front_end/src/app/(main)/(leaderboards)/medals/components/medals_widget.tsx +++ b/front_end/src/app/(main)/(leaderboards)/medals/components/medals_widget.tsx @@ -11,85 +11,90 @@ import { RANKING_CATEGORIES } from "../../ranking_categories"; import { SCORING_CATEGORY_FILTER } from "../../search_params"; import { getMedalCategories } from "../helpers/medal_categories"; import { getMedalDisplayTitle } from "../helpers/medal_title"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { profileId: number; }; const MedalsWidget: FC = async ({ profileId }) => { - const t = await getTranslations(); + return ServerComponentErrorBoundary(async () => { + const t = await getTranslations(); - const userMedals = await LeaderboardApi.getUserMedals(profileId); - const categories = getMedalCategories(userMedals, true); + const userMedals = await LeaderboardApi.getUserMedals(profileId); + const categories = getMedalCategories(userMedals, true); - return ( -
-
-

- {t("medals")} -

- - View All - -
-
- {categories?.map((category, index) => ( -
+
+

+ {t("medals")} +

+ - +
+
+ {categories?.map((category, index) => ( +
- {t(RANKING_CATEGORIES[category.name].translationKey)} - -
- {!!category.medals.length ? ( - category.medals.map((medal, index) => { - const tooltipContent = ( -
- - {getMedalDisplayTitle(medal)} - - - {t("rank")}:{" "} - #{medal.rank}{" "} - {t("outOfRank", { total: medal.totalEntries })} - -
- ); - - return ( - -
- - - - ); - }) - ) : ( - - {t("noMedals")} + + + {t(RANKING_CATEGORIES[category.name].translationKey)} - )} + +
+ {!!category.medals.length ? ( + category.medals.map((medal, index) => { + const tooltipContent = ( +
+ + {getMedalDisplayTitle(medal)} + + + {t("rank")}:{" "} + #{medal.rank}{" "} + {t("outOfRank", { total: medal.totalEntries })} + +
+ ); + + return ( + + + + + + ); + }) + ) : ( + + {t("noMedals")} + + )} +
-
- ))} -
-
- ); + ))} + + + ); + }); }; export default MedalsWidget; diff --git a/front_end/src/app/(main)/experiments/elections/components/card_forecast.tsx b/front_end/src/app/(main)/experiments/elections/components/card_forecast.tsx index 28d3d29627..bddb081312 100644 --- a/front_end/src/app/(main)/experiments/elections/components/card_forecast.tsx +++ b/front_end/src/app/(main)/experiments/elections/components/card_forecast.tsx @@ -3,21 +3,24 @@ import { FC } from "react"; import ForecastCard from "@/components/forecast_card"; import PostsApi from "@/services/posts"; import { TimelineChartZoomOption } from "@/types/charts"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { postId: number; }; const CardForecast: FC = async ({ postId }) => { - const post = await PostsApi.getPost(postId); - if (!post) return null; + return ServerComponentErrorBoundary(async () => { + const post = await PostsApi.getPost(postId); + if (!post) return null; - return ( - - ); + return ( + + ); + }); }; export default CardForecast; diff --git a/front_end/src/app/(main)/experiments/elections/components/expected_electoral_votes_forecast/index.tsx b/front_end/src/app/(main)/experiments/elections/components/expected_electoral_votes_forecast/index.tsx index d23ad39a33..b80417c0b1 100644 --- a/front_end/src/app/(main)/experiments/elections/components/expected_electoral_votes_forecast/index.tsx +++ b/front_end/src/app/(main)/experiments/elections/components/expected_electoral_votes_forecast/index.tsx @@ -11,6 +11,7 @@ import { Candle } from "@/types/experiments"; import { QuestionType, QuestionWithForecasts } from "@/types/question"; import { getDisplayValue } from "@/utils/charts"; import { computeQuartilesFromCDF } from "@/utils/math"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { democratPostId: number; @@ -21,82 +22,84 @@ const ExpectedElectoralVotesForecast: FC = async ({ democratPostId, republicanPostId, }) => { - const t = await getTranslations(); - const [democratPost, republicanPost] = await Promise.all([ - PostsApi.getPost(democratPostId), - PostsApi.getPost(republicanPostId), - ]); - if (!democratPost?.question || !republicanPost?.question) { - return null; - } + return ServerComponentErrorBoundary(async () => { + const t = await getTranslations(); + const [democratPost, republicanPost] = await Promise.all([ + PostsApi.getPost(democratPostId), + PostsApi.getPost(republicanPostId), + ]); + if (!democratPost?.question || !republicanPost?.question) { + return null; + } - const { candles, democratPrediction, republicanPrediction } = - getForecastData(democratPost.question, republicanPost.question) ?? {}; + const { candles, democratPrediction, republicanPrediction } = + getForecastData(democratPost.question, republicanPost.question) ?? {}; - return ( -
-
- ({t("270VotesToWin")}) -
-
- ({t("270VotesToWin")}) -
- - {t("expectedElectoralVotes")} - -
- - {t("0Votes")} - - - {t("538Votes")} + return ( +
+
+ ({t("270VotesToWin")}) +
+
+ ({t("270VotesToWin")}) +
+ + {t("expectedElectoralVotes")} - -
+
+ + {t("0Votes")} + + + {t("538Votes")} + + +
+
+ {democratPrediction} + {t("democrat")} +
+ +
+
+ {!!candles && } + +
- {democratPrediction} - {t("democrat")} + {republicanPrediction} + {t("republican")}
- {!!candles && } - -
-
- {republicanPrediction} - {t("republican")} -
- -
-
- ); + ); + }); }; type ForecastData = { diff --git a/front_end/src/app/(main)/experiments/elections/components/state_by_forecast/index.tsx b/front_end/src/app/(main)/experiments/elections/components/state_by_forecast/index.tsx index ba014881e6..6d97f3ce07 100644 --- a/front_end/src/app/(main)/experiments/elections/components/state_by_forecast/index.tsx +++ b/front_end/src/app/(main)/experiments/elections/components/state_by_forecast/index.tsx @@ -17,6 +17,7 @@ import { extractQuestionGroupName } from "@/utils/questions"; import MiddleVotesArrow from "./middle_votes_arrow"; import StateByForecastCharts from "./state_by_forecast_charts"; import { US_MAP_AREAS } from "./us_areas"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { questionGroupId: number; @@ -31,105 +32,107 @@ const StateByForecast: FC = async ({ republicanPostId, isEmbed, }) => { - const t = await getTranslations(); - const post = await PostsApi.getPost(questionGroupId); - if (!post?.group_of_questions) { - return null; - } + return ServerComponentErrorBoundary(async () => { + const t = await getTranslations(); + const post = await PostsApi.getPost(questionGroupId); + if (!post?.group_of_questions) { + return null; + } - let democratPrediction = null; - let republicanPrediction = null; - if (democratPostId && republicanPostId) { - const [demPost, repPost] = await Promise.all([ - PostsApi.getPost(democratPostId), - PostsApi.getPost(republicanPostId), - ]); - const predictions = getDemocratRepublicanPrediction({ demPost, repPost }); - if (predictions) { - democratPrediction = predictions.democratPrediction; - republicanPrediction = predictions.republicanPrediction; + let democratPrediction = null; + let republicanPrediction = null; + if (democratPostId && republicanPostId) { + const [demPost, repPost] = await Promise.all([ + PostsApi.getPost(democratPostId), + PostsApi.getPost(republicanPostId), + ]); + const predictions = getDemocratRepublicanPrediction({ demPost, repPost }); + if (predictions) { + democratPrediction = predictions.democratPrediction; + republicanPrediction = predictions.republicanPrediction; + } } - } - const stateByItems = getStateByItems( - post.id, - post.group_of_questions.questions - ); + const stateByItems = getStateByItems( + post.id, + post.group_of_questions.questions + ); - if (isEmbed) { - return ( -
-
- -
- {democratPrediction && ( -
- {t("democrat")} -
- {t("numVotes", { num: democratPrediction })} -
- )} - {republicanPrediction && ( -
- {t("republican")} -
- {t("numVotes", { num: republicanPrediction })} -
- )} + if (isEmbed) { + return ( +
+
+ +
+ {democratPrediction && ( +
+ {t("democrat")} +
+ {t("numVotes", { num: democratPrediction })} +
+ )} + {republicanPrediction && ( +
+ {t("republican")} +
+ {t("numVotes", { num: republicanPrediction })} +
+ )} +
-
- -
- ); - } + +
+ ); + } - return ( -
-
- - {t("stateByStateForecasts")} - - - - -
- -
- -
+ {t("stateByStateForecasts")} + + - + +
-
+
+ +
-
- {t.rich("electionHubDisclaimer", { - bold: (chunks) => {chunks}, - link: (chunks) => ( - - {chunks} - - ), - })} + + +
+ +
+ {t.rich("electionHubDisclaimer", { + bold: (chunks) => {chunks}, + link: (chunks) => ( + + {chunks} + + ), + })} +
-
- ); + ); + }); }; const getStateByItems = ( diff --git a/front_end/src/app/(main)/questions/discovery/components/tags_discovery.tsx b/front_end/src/app/(main)/questions/discovery/components/tags_discovery.tsx index 02f837b2c9..50164dda83 100644 --- a/front_end/src/app/(main)/questions/discovery/components/tags_discovery.tsx +++ b/front_end/src/app/(main)/questions/discovery/components/tags_discovery.tsx @@ -9,6 +9,7 @@ import { SearchParams } from "@/types/navigation"; import DiscoverySection from "./section"; import AwaitedTags from "./tags"; import { TAGS_TEXT_SEARCH_FILTER } from "../constants/tags_feed"; +import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; const getFilters = (searchParams: SearchParams) => { const filters: TagsParams = {}; @@ -23,21 +24,23 @@ const getFilters = (searchParams: SearchParams) => { const TagsDiscovery: FC<{ searchParams: SearchParams }> = async ({ searchParams, }) => { - const filters = getFilters(searchParams); - let tags = await ProjectsApi.getTags(filters); - const t = await getTranslations(); - - return ( - - - } - > - - - - ); + return ServerComponentErrorBoundary(async () => { + const filters = getFilters(searchParams); + let tags = await ProjectsApi.getTags(filters); + const t = await getTranslations(); + + return ( + + + } + > + + + + ); + }); }; export default TagsDiscovery; From ff794bc256dd12be09561bacc454c7ee4c974744 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 30 Aug 2024 19:05:03 +0300 Subject: [PATCH 3/5] fix(question-feed): fix topic filter bug --- front_end/src/components/posts_feed/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/front_end/src/components/posts_feed/index.tsx b/front_end/src/components/posts_feed/index.tsx index 4ab75176cc..52d2be5508 100644 --- a/front_end/src/components/posts_feed/index.tsx +++ b/front_end/src/components/posts_feed/index.tsx @@ -16,7 +16,11 @@ type Props = { const AwaitedPostsFeed: FC = async ({ filters, type, topics }) => { return ServerComponentErrorBoundary(async () => { - if (topics && !topics?.some((topic) => topic.slug === filters.topic)) { + if ( + topics && + filters.topic && + !topics?.some((topic) => topic.slug === filters.topic) + ) { return (
Such topic does not exist From 0d607a7bd363d349d7353ba44e622fc23ab1c04d Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 30 Aug 2024 19:05:08 +0300 Subject: [PATCH 4/5] fix(error-handling): fix error boundary component --- .../components/server_component_error_boundary.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/front_end/src/components/server_component_error_boundary.tsx b/front_end/src/components/server_component_error_boundary.tsx index ad252a342a..e09b1442ea 100644 --- a/front_end/src/components/server_component_error_boundary.tsx +++ b/front_end/src/components/server_component_error_boundary.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { notFound } from "next/navigation"; import RefreshButton from "./refresh_button"; const ServerComponentErrorBoundary = async ( @@ -9,16 +8,21 @@ const ServerComponentErrorBoundary = async ( return await fn(); } catch (error) { if (error instanceof Error) { - if (error.message.includes("404")) return notFound(); + const { digest } = error as Error & { digest?: string }; return (
-

{error.message ?? "Unknown error"}

+

{digest ?? "Unknown error"}

); } - throw error; + return ( +
+

Unknown error

+ +
+ ); } }; From a56747f4adbf84bcd4798b335b6168071b2d9549 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 2 Sep 2024 12:57:11 +0300 Subject: [PATCH 5/5] fix(error-handle): refactor async server component error boundary to HOC --- .../components/research_and_updates.tsx | 146 +++++++------- .../(home)/components/tournaments_block.tsx | 92 +++++---- .../components/contributions_hero.tsx | 7 +- .../components/global_contributions.tsx | 6 +- .../components/project_contributions.tsx | 6 +- .../components/global_leaderboard.tsx | 6 +- .../components/project_leaderboard.tsx | 6 +- .../medals/components/medals_page.tsx | 148 +++++++------- .../medals/components/medals_widget.tsx | 6 +- .../elections/components/card_forecast.tsx | 22 +-- .../index.tsx | 126 ++++++------ .../components/state_by_forecast/index.tsx | 180 +++++++++--------- .../components/sidebar/news_match/index.tsx | 22 +-- .../sidebar/similar_questions/index.tsx | 22 +-- .../discovery/components/tags_discovery.tsx | 36 ++-- front_end/src/components/posts_feed/index.tsx | 30 ++- .../server_component_error_boundary.tsx | 41 ++-- 17 files changed, 434 insertions(+), 468 deletions(-) diff --git a/front_end/src/app/(main)/(home)/components/research_and_updates.tsx b/front_end/src/app/(main)/(home)/components/research_and_updates.tsx index 16a0eeab24..a712745b8d 100644 --- a/front_end/src/app/(main)/(home)/components/research_and_updates.tsx +++ b/front_end/src/app/(main)/(home)/components/research_and_updates.tsx @@ -10,88 +10,86 @@ import imagePlaceholder from "@/app/assets/images/tournament.webp"; import PostsApi from "@/services/posts"; import { PostWithNotebook } from "@/types/post"; import { getNotebookSummary } from "@/utils/questions"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { postIds: number[]; }; const ResearchAndUpdatesBlock: FC = async ({ postIds }) => { - return ServerComponentErrorBoundary(async () => { - const t = await getTranslations(); - const locale = await getLocale(); - const { results } = await PostsApi.getPostsWithCP({ - ids: postIds, - }); - const posts = results as PostWithNotebook[]; + const t = await getTranslations(); + const locale = await getLocale(); + const { results } = await PostsApi.getPostsWithCP({ + ids: postIds, + }); + const posts = results as PostWithNotebook[]; - return ( -
-

- {t("research")} &{" "} - - {t("updates")} - -

-

- {t("partnersUseForecasts")} -

-
- {posts.map(({ title, created_at, id, notebook, url_title }) => ( - - {notebook.image_url && notebook.image_url.startsWith("https:") ? ( - - ) : ( - - )} + return ( +
+

+ {t("research")} &{" "} + + {t("updates")} + +

+

+ {t("partnersUseForecasts")} +

+
+ {posts.map(({ title, created_at, id, notebook, url_title }) => ( + + {notebook.image_url && notebook.image_url.startsWith("https:") ? ( + + ) : ( + + )} -
- - {intlFormat( - new Date(created_at), - { - year: "numeric", - month: "short", - }, - { locale } - )} - -

{title}

-

- {getNotebookSummary(notebook.markdown, 200, 80)} -

-
- - ))} -
- - {t("seeMorePosts")} - - +
+ + {intlFormat( + new Date(created_at), + { + year: "numeric", + month: "short", + }, + { locale } + )} + +

{title}

+

+ {getNotebookSummary(notebook.markdown, 200, 80)} +

+
+ + ))}
- ); - }); + + {t("seeMorePosts")} + + +
+ ); }; -export default ResearchAndUpdatesBlock; +export default WithServerComponentErrorBoundary(ResearchAndUpdatesBlock); diff --git a/front_end/src/app/(main)/(home)/components/tournaments_block.tsx b/front_end/src/app/(main)/(home)/components/tournaments_block.tsx index 15534710bf..55742ea6df 100644 --- a/front_end/src/app/(main)/(home)/components/tournaments_block.tsx +++ b/front_end/src/app/(main)/(home)/components/tournaments_block.tsx @@ -5,7 +5,7 @@ import { getTranslations } from "next-intl/server"; import { FC } from "react"; import TournamentCard from "@/components/tournament_card"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; import ProjectsApi from "@/services/projects"; import { Tournament, TournamentType } from "@/types/projects"; @@ -14,53 +14,51 @@ type Props = { }; const TournamentsBlock: FC = async ({ postSlugs }) => { - return ServerComponentErrorBoundary(async () => { - const t = await getTranslations(); - const tournamentPromises = postSlugs.map( - (slug) => ProjectsApi.getSlugTournament(slug) as Promise - ); - const tournaments = await Promise.all(tournamentPromises); + const t = await getTranslations(); + const tournamentPromises = postSlugs.map( + (slug) => ProjectsApi.getSlugTournament(slug) as Promise + ); + const tournaments = await Promise.all(tournamentPromises); - return ( -
-

- {t("forecasting")}{" "} - - {t("tournaments")} - -

-

- {t("joinTournaments")} -

-
- {tournaments.map((tournament) => ( - - ))} -
- - {t("seeAllTournaments")} - - + return ( +
+

+ {t("forecasting")}{" "} + + {t("tournaments")} + +

+

+ {t("joinTournaments")} +

+
+ {tournaments.map((tournament) => ( + + ))}
- ); - }); + + {t("seeAllTournaments")} + + +
+ ); }; -export default TournamentsBlock; +export default WithServerComponentErrorBoundary(TournamentsBlock); diff --git a/front_end/src/app/(main)/(leaderboards)/contributions/components/contributions_hero.tsx b/front_end/src/app/(main)/(leaderboards)/contributions/components/contributions_hero.tsx index 9e0ab98f15..d755cb41ae 100644 --- a/front_end/src/app/(main)/(leaderboards)/contributions/components/contributions_hero.tsx +++ b/front_end/src/app/(main)/(leaderboards)/contributions/components/contributions_hero.tsx @@ -16,7 +16,7 @@ import { SCORING_DURATION_FILTER, SCORING_YEAR_FILTER, } from "../../search_params"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { category: CategoryKey; @@ -73,14 +73,13 @@ const ContributionsHero: FC = ({ year, duration, category, userId }) => { ); }; -const AwaitedUserHeader: FC<{ userId: number }> = async ({ userId }) => { - return ServerComponentErrorBoundary(async () => { +const AwaitedUserHeader: FC<{ userId: number }> = + WithServerComponentErrorBoundary(async ({ userId }) => { const t = await getTranslations(); const profile = await ProfileApi.getProfileById(userId); return {profile.username ?? t("user")}; }); -}; const UserHeader: FC = ({ children }) => (

diff --git a/front_end/src/app/(main)/(leaderboards)/contributions/components/global_contributions.tsx b/front_end/src/app/(main)/(leaderboards)/contributions/components/global_contributions.tsx index 288e0094d8..4454bed8d3 100644 --- a/front_end/src/app/(main)/(leaderboards)/contributions/components/global_contributions.tsx +++ b/front_end/src/app/(main)/(leaderboards)/contributions/components/global_contributions.tsx @@ -3,7 +3,7 @@ import { FC } from "react"; import ContributionsTable from "@/app/(main)/(leaderboards)/contributions/components/contributions_table"; import LeaderboardApi from "@/services/leaderboard"; import { CategoryKey, LeaderboardType } from "@/types/scoring"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { startTime: string; @@ -20,7 +20,6 @@ const GlobalContributions: FC = async ({ startTime, category, }) => { - return ServerComponentErrorBoundary(async () => { const contributionsDetails = await LeaderboardApi.getContributions({ type: "global", leaderboardType, @@ -36,7 +35,6 @@ const GlobalContributions: FC = async ({ contributions={contributionsDetails.contributions} /> ); - }); }; -export default GlobalContributions; +export default WithServerComponentErrorBoundary(GlobalContributions); diff --git a/front_end/src/app/(main)/(leaderboards)/contributions/components/project_contributions.tsx b/front_end/src/app/(main)/(leaderboards)/contributions/components/project_contributions.tsx index cd5bd6f35f..5e8657b924 100644 --- a/front_end/src/app/(main)/(leaderboards)/contributions/components/project_contributions.tsx +++ b/front_end/src/app/(main)/(leaderboards)/contributions/components/project_contributions.tsx @@ -6,7 +6,7 @@ import InfoToggle from "@/components/ui/info_toggle"; import SectionToggle from "@/components/ui/section_toggle"; import LeaderboardApi from "@/services/leaderboard"; import { Tournament } from "@/types/projects"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { project: Tournament; @@ -14,7 +14,6 @@ type Props = { }; const ProjectContributions: FC = async ({ project, userId }) => { - return ServerComponentErrorBoundary(async () => { const t = await getTranslations(); const contributionsDetails = await LeaderboardApi.getContributions({ type: "project", @@ -182,7 +181,6 @@ const ProjectContributions: FC = async ({ project, userId }) => { ); - }); }; -export default ProjectContributions; +export default WithServerComponentErrorBoundary(ProjectContributions); diff --git a/front_end/src/app/(main)/(leaderboards)/leaderboard/components/global_leaderboard.tsx b/front_end/src/app/(main)/(leaderboards)/leaderboard/components/global_leaderboard.tsx index bca292270f..cb8692eb14 100644 --- a/front_end/src/app/(main)/(leaderboards)/leaderboard/components/global_leaderboard.tsx +++ b/front_end/src/app/(main)/(leaderboards)/leaderboard/components/global_leaderboard.tsx @@ -4,7 +4,7 @@ import LeaderboardApi from "@/services/leaderboard"; import { CategoryKey, LeaderboardType } from "@/types/scoring"; import LeaderboardTable from "./leaderboard_table"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { startTime: string; @@ -25,7 +25,6 @@ const GlobalLeaderboard: FC = async ({ category, cardSized, }) => { - return ServerComponentErrorBoundary(async () => { const leaderboardDetails = await LeaderboardApi.getGlobalLeaderboard( startTime, endTime, @@ -41,7 +40,6 @@ const GlobalLeaderboard: FC = async ({ cardSized={cardSized} /> ); - }); }; -export default GlobalLeaderboard; +export default WithServerComponentErrorBoundary(GlobalLeaderboard); diff --git a/front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard.tsx b/front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard.tsx index b1975462d1..4adc0fd826 100644 --- a/front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard.tsx +++ b/front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard.tsx @@ -7,7 +7,7 @@ import LeaderboardApi from "@/services/leaderboard"; import { LeaderboardType } from "@/types/scoring"; import ProjectLeaderboardTable from "./project_leaderboard_table"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { projectId: number; @@ -24,7 +24,6 @@ const ProjectLeaderboard: FC = async ({ isQuestionSeries, userId, }) => { - return ServerComponentErrorBoundary(async () => { const leaderboardDetails = await LeaderboardApi.getProjectLeaderboard( projectId, leaderboardType @@ -56,7 +55,6 @@ const ProjectLeaderboard: FC = async ({ /> ); - }); }; -export default ProjectLeaderboard; +export default WithServerComponentErrorBoundary(ProjectLeaderboard); diff --git a/front_end/src/app/(main)/(leaderboards)/medals/components/medals_page.tsx b/front_end/src/app/(main)/(leaderboards)/medals/components/medals_page.tsx index 70a8554e1f..ace01acd77 100644 --- a/front_end/src/app/(main)/(leaderboards)/medals/components/medals_page.tsx +++ b/front_end/src/app/(main)/(leaderboards)/medals/components/medals_page.tsx @@ -8,93 +8,89 @@ import MedalIcon from "../../components/medal_icon"; import { RANKING_CATEGORIES } from "../../ranking_categories"; import { getMedalCategories } from "../helpers/medal_categories"; import { getMedalDisplayTitle } from "../helpers/medal_title"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { profileId: number; }; const MedalsPage: FC = async ({ profileId }) => { - return ServerComponentErrorBoundary(async () => { - const t = await getTranslations(); + const t = await getTranslations(); - const userMedals = await LeaderboardApi.getUserMedals(profileId); - const categories = getMedalCategories(userMedals, true); - type MedalType = "gold" | "silver" | "bronze"; + const userMedals = await LeaderboardApi.getUserMedals(profileId); + const categories = getMedalCategories(userMedals, true); + type MedalType = "gold" | "silver" | "bronze"; - function getMedalClassName(medalType: MedalType): string { - switch (medalType) { - case "gold": - return "bg-blue-700/5 dark:bg-blue-950 md:bg-white md:bg-gradient-to-b from-[#F6D84D]/0 dark:from-blue-950 dark:to-blue-950 md:from-[#F6D84D]/30 dark:md:from-[#F6D84D]/20 from-0% to-30% to-white md:dark:to-blue-950/75"; - case "silver": - return "bg-blue-700/5 dark:bg-blue-950 md:bg-white md:bg-gradient-to-b from-[#A7B1C0]/0 dark:from-blue-950 dark:to-blue-950 md:from-[#A7B1C0]/15 dark:md:from-[#A7B1C0]/15 dark:from-[#A7B1C0]/25 from-0% to-30% to-white md:dark:to-blue-950/75"; - case "bronze": - return "bg-blue-700/5 dark:bg-blue-950 md:bg-white md:bg-gradient-to-b from-[#F09B59]/0 dark:from-blue-950 dark:to-blue-950 md:from-[#F09B59]/20 dark:md:from-[#F09B59]/20 from-0% to-30% to-white md:dark:to-blue-950/75"; - default: - return ""; - } + function getMedalClassName(medalType: MedalType): string { + switch (medalType) { + case "gold": + return "bg-blue-700/5 dark:bg-blue-950 md:bg-white md:bg-gradient-to-b from-[#F6D84D]/0 dark:from-blue-950 dark:to-blue-950 md:from-[#F6D84D]/30 dark:md:from-[#F6D84D]/20 from-0% to-30% to-white md:dark:to-blue-950/75"; + case "silver": + return "bg-blue-700/5 dark:bg-blue-950 md:bg-white md:bg-gradient-to-b from-[#A7B1C0]/0 dark:from-blue-950 dark:to-blue-950 md:from-[#A7B1C0]/15 dark:md:from-[#A7B1C0]/15 dark:from-[#A7B1C0]/25 from-0% to-30% to-white md:dark:to-blue-950/75"; + case "bronze": + return "bg-blue-700/5 dark:bg-blue-950 md:bg-white md:bg-gradient-to-b from-[#F09B59]/0 dark:from-blue-950 dark:to-blue-950 md:from-[#F09B59]/20 dark:md:from-[#F09B59]/20 from-0% to-30% to-white md:dark:to-blue-950/75"; + default: + return ""; } - return ( -
-
- {categories?.map((category, index) => ( -
-
- - {t(RANKING_CATEGORIES[category.name].translationKey)} - -
-
- {!!category.medals.length ? ( - category.medals.map((medal, index) => { - return ( -
-
-
-
- -
-
- - {getMedalDisplayTitle(medal)} - - - {t("rank")}: - - #{medal.rank} - {" "} - - {t("outOfRank", { total: medal.totalEntries })} - + } + return ( +
+
+ {categories?.map((category, index) => ( +
+
+ + {t(RANKING_CATEGORIES[category.name].translationKey)} + +
+
+ {!!category.medals.length ? ( + category.medals.map((medal, index) => { + return ( +
+
+
+
+ +
+
+ + {getMedalDisplayTitle(medal)} + + + {t("rank")}: + #{medal.rank}{" "} + + {t("outOfRank", { total: medal.totalEntries })} -
+
- ); - }) - ) : ( - - {t("noMedals")} - - )} -
+
+ ); + }) + ) : ( + + {t("noMedals")} + + )}
- ))} -
-
- ); - }); +

+ ))} +
+ + ); }; -export default MedalsPage; +export default WithServerComponentErrorBoundary(MedalsPage); diff --git a/front_end/src/app/(main)/(leaderboards)/medals/components/medals_widget.tsx b/front_end/src/app/(main)/(leaderboards)/medals/components/medals_widget.tsx index 41e84a32c9..67f3963dc1 100644 --- a/front_end/src/app/(main)/(leaderboards)/medals/components/medals_widget.tsx +++ b/front_end/src/app/(main)/(leaderboards)/medals/components/medals_widget.tsx @@ -11,14 +11,13 @@ import { RANKING_CATEGORIES } from "../../ranking_categories"; import { SCORING_CATEGORY_FILTER } from "../../search_params"; import { getMedalCategories } from "../helpers/medal_categories"; import { getMedalDisplayTitle } from "../helpers/medal_title"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { profileId: number; }; const MedalsWidget: FC = async ({ profileId }) => { - return ServerComponentErrorBoundary(async () => { const t = await getTranslations(); const userMedals = await LeaderboardApi.getUserMedals(profileId); @@ -94,7 +93,6 @@ const MedalsWidget: FC = async ({ profileId }) => {
); - }); }; -export default MedalsWidget; +export default WithServerComponentErrorBoundary(MedalsWidget); diff --git a/front_end/src/app/(main)/experiments/elections/components/card_forecast.tsx b/front_end/src/app/(main)/experiments/elections/components/card_forecast.tsx index bddb081312..0bf7e7f946 100644 --- a/front_end/src/app/(main)/experiments/elections/components/card_forecast.tsx +++ b/front_end/src/app/(main)/experiments/elections/components/card_forecast.tsx @@ -3,24 +3,22 @@ import { FC } from "react"; import ForecastCard from "@/components/forecast_card"; import PostsApi from "@/services/posts"; import { TimelineChartZoomOption } from "@/types/charts"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { postId: number; }; const CardForecast: FC = async ({ postId }) => { - return ServerComponentErrorBoundary(async () => { - const post = await PostsApi.getPost(postId); - if (!post) return null; + const post = await PostsApi.getPost(postId); + if (!post) return null; - return ( - - ); - }); + return ( + + ); }; -export default CardForecast; +export default WithServerComponentErrorBoundary(CardForecast); diff --git a/front_end/src/app/(main)/experiments/elections/components/expected_electoral_votes_forecast/index.tsx b/front_end/src/app/(main)/experiments/elections/components/expected_electoral_votes_forecast/index.tsx index b80417c0b1..4e4b54d4bf 100644 --- a/front_end/src/app/(main)/experiments/elections/components/expected_electoral_votes_forecast/index.tsx +++ b/front_end/src/app/(main)/experiments/elections/components/expected_electoral_votes_forecast/index.tsx @@ -11,7 +11,7 @@ import { Candle } from "@/types/experiments"; import { QuestionType, QuestionWithForecasts } from "@/types/question"; import { getDisplayValue } from "@/utils/charts"; import { computeQuartilesFromCDF } from "@/utils/math"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { democratPostId: number; @@ -22,84 +22,82 @@ const ExpectedElectoralVotesForecast: FC = async ({ democratPostId, republicanPostId, }) => { - return ServerComponentErrorBoundary(async () => { - const t = await getTranslations(); - const [democratPost, republicanPost] = await Promise.all([ - PostsApi.getPost(democratPostId), - PostsApi.getPost(republicanPostId), - ]); - if (!democratPost?.question || !republicanPost?.question) { - return null; - } + const t = await getTranslations(); + const [democratPost, republicanPost] = await Promise.all([ + PostsApi.getPost(democratPostId), + PostsApi.getPost(republicanPostId), + ]); + if (!democratPost?.question || !republicanPost?.question) { + return null; + } - const { candles, democratPrediction, republicanPrediction } = - getForecastData(democratPost.question, republicanPost.question) ?? {}; + const { candles, democratPrediction, republicanPrediction } = + getForecastData(democratPost.question, republicanPost.question) ?? {}; - return ( -
-
- ({t("270VotesToWin")}) -
-
- ({t("270VotesToWin")}) -
- - {t("expectedElectoralVotes")} + return ( +
+
+ ({t("270VotesToWin")}) +
+
+ ({t("270VotesToWin")}) +
+ + {t("expectedElectoralVotes")} + +
+ + {t("0Votes")} -
- - {t("0Votes")} - - - {t("538Votes")} - - -
-
- {democratPrediction} - {t("democrat")} -
- -
-
- {!!candles && } - -
+ + {t("538Votes")} + + +
- {republicanPrediction} - {t("republican")} + {democratPrediction} + {t("democrat")}
- ); - }); + {!!candles && } + +
+
+ {republicanPrediction} + {t("republican")} +
+ +
+
+ ); }; type ForecastData = { @@ -153,4 +151,4 @@ function getForecastData( }; } -export default ExpectedElectoralVotesForecast; +export default WithServerComponentErrorBoundary(ExpectedElectoralVotesForecast); diff --git a/front_end/src/app/(main)/experiments/elections/components/state_by_forecast/index.tsx b/front_end/src/app/(main)/experiments/elections/components/state_by_forecast/index.tsx index 6d97f3ce07..7b3997731d 100644 --- a/front_end/src/app/(main)/experiments/elections/components/state_by_forecast/index.tsx +++ b/front_end/src/app/(main)/experiments/elections/components/state_by_forecast/index.tsx @@ -17,7 +17,7 @@ import { extractQuestionGroupName } from "@/utils/questions"; import MiddleVotesArrow from "./middle_votes_arrow"; import StateByForecastCharts from "./state_by_forecast_charts"; import { US_MAP_AREAS } from "./us_areas"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { questionGroupId: number; @@ -32,107 +32,105 @@ const StateByForecast: FC = async ({ republicanPostId, isEmbed, }) => { - return ServerComponentErrorBoundary(async () => { - const t = await getTranslations(); - const post = await PostsApi.getPost(questionGroupId); - if (!post?.group_of_questions) { - return null; - } + const t = await getTranslations(); + const post = await PostsApi.getPost(questionGroupId); + if (!post?.group_of_questions) { + return null; + } - let democratPrediction = null; - let republicanPrediction = null; - if (democratPostId && republicanPostId) { - const [demPost, repPost] = await Promise.all([ - PostsApi.getPost(democratPostId), - PostsApi.getPost(republicanPostId), - ]); - const predictions = getDemocratRepublicanPrediction({ demPost, repPost }); - if (predictions) { - democratPrediction = predictions.democratPrediction; - republicanPrediction = predictions.republicanPrediction; - } + let democratPrediction = null; + let republicanPrediction = null; + if (democratPostId && republicanPostId) { + const [demPost, repPost] = await Promise.all([ + PostsApi.getPost(democratPostId), + PostsApi.getPost(republicanPostId), + ]); + const predictions = getDemocratRepublicanPrediction({ demPost, repPost }); + if (predictions) { + democratPrediction = predictions.democratPrediction; + republicanPrediction = predictions.republicanPrediction; } + } - const stateByItems = getStateByItems( - post.id, - post.group_of_questions.questions - ); + const stateByItems = getStateByItems( + post.id, + post.group_of_questions.questions + ); - if (isEmbed) { - return ( -
-
- -
- {democratPrediction && ( -
- {t("democrat")} -
- {t("numVotes", { num: democratPrediction })} -
- )} - {republicanPrediction && ( -
- {t("republican")} -
- {t("numVotes", { num: republicanPrediction })} -
- )} -
+ if (isEmbed) { + return ( +
+
+ +
+ {democratPrediction && ( +
+ {t("democrat")} +
+ {t("numVotes", { num: democratPrediction })} +
+ )} + {republicanPrediction && ( +
+ {t("republican")} +
+ {t("numVotes", { num: republicanPrediction })} +
+ )}
- -
- ); - } - return ( -
-
- +
+ ); + } + + return ( +
+
+ + {t("stateByStateForecasts")} + - + + + - -
+ +
-
- -
+
+ +
- - -
- -
- {t.rich("electionHubDisclaimer", { - bold: (chunks) => {chunks}, - link: (chunks) => ( - - {chunks} - - ), - })} -
+ + +
+ +
+ {t.rich("electionHubDisclaimer", { + bold: (chunks) => {chunks}, + link: (chunks) => ( + + {chunks} + + ), + })}
- ); - }); +
+ ); }; const getStateByItems = ( @@ -229,4 +227,4 @@ function getDemocratRepublicanPrediction({ }; } -export default StateByForecast; +export default WithServerComponentErrorBoundary(StateByForecast); diff --git a/front_end/src/app/(main)/questions/[id]/components/sidebar/news_match/index.tsx b/front_end/src/app/(main)/questions/[id]/components/sidebar/news_match/index.tsx index 330722e952..62c58089ed 100644 --- a/front_end/src/app/(main)/questions/[id]/components/sidebar/news_match/index.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/sidebar/news_match/index.tsx @@ -3,26 +3,20 @@ import { FC } from "react"; import PostsApi from "@/services/posts"; import NewsMatchDrawer from "./news_match_drawer"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; interface Props { questionId: number; } -const fetchArticles = async (postId: number) => { - return await PostsApi.getRelatedNews(postId); -}; - const NewsMatch: FC = async ({ questionId }) => { - return ServerComponentErrorBoundary(async () => { - const articles = await fetchArticles(questionId); + const articles = await PostsApi.getRelatedNews(questionId); - if (articles.length > 0) { - return ; - } else { - return null; - } - }); + if (articles.length > 0) { + return ; + } else { + return null; + } }; -export default NewsMatch; +export default WithServerComponentErrorBoundary(NewsMatch); diff --git a/front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx b/front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx index dbe22f73ca..06524b2081 100644 --- a/front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx @@ -5,25 +5,23 @@ import { PostStatus } from "@/types/post"; import { QuestionOrder } from "@/types/question"; import SimilarQuestionsDrawer from "./similar_questions_drawer"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; type Props = { post_id: number; }; const SimilarQuestions: FC = async ({ post_id }) => { - return ServerComponentErrorBoundary(async () => { - const response = await PostsApi.getPostsWithCP({ - similar_to_post_id: post_id, - order_by: QuestionOrder.PredictionCountDesc, - statuses: PostStatus.OPEN, - }); - const { results: questions } = response; + const response = await PostsApi.getPostsWithCP({ + similar_to_post_id: post_id, + order_by: QuestionOrder.PredictionCountDesc, + statuses: PostStatus.OPEN, + }); + const { results: questions } = response; - if (!questions.length) return null; + if (!questions.length) return null; - return ; - }); + return ; }; -export default SimilarQuestions; +export default WithServerComponentErrorBoundary(SimilarQuestions) as FC; diff --git a/front_end/src/app/(main)/questions/discovery/components/tags_discovery.tsx b/front_end/src/app/(main)/questions/discovery/components/tags_discovery.tsx index 50164dda83..2dcdadfa9c 100644 --- a/front_end/src/app/(main)/questions/discovery/components/tags_discovery.tsx +++ b/front_end/src/app/(main)/questions/discovery/components/tags_discovery.tsx @@ -9,7 +9,7 @@ import { SearchParams } from "@/types/navigation"; import DiscoverySection from "./section"; import AwaitedTags from "./tags"; import { TAGS_TEXT_SEARCH_FILTER } from "../constants/tags_feed"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; const getFilters = (searchParams: SearchParams) => { const filters: TagsParams = {}; @@ -24,23 +24,21 @@ const getFilters = (searchParams: SearchParams) => { const TagsDiscovery: FC<{ searchParams: SearchParams }> = async ({ searchParams, }) => { - return ServerComponentErrorBoundary(async () => { - const filters = getFilters(searchParams); - let tags = await ProjectsApi.getTags(filters); - const t = await getTranslations(); - - return ( - - - } - > - - - - ); - }); + const filters = getFilters(searchParams); + let tags = await ProjectsApi.getTags(filters); + const t = await getTranslations(); + + return ( + + + } + > + + + + ); }; -export default TagsDiscovery; +export default WithServerComponentErrorBoundary(TagsDiscovery); diff --git a/front_end/src/components/posts_feed/index.tsx b/front_end/src/components/posts_feed/index.tsx index 52d2be5508..7b3f109bfa 100644 --- a/front_end/src/components/posts_feed/index.tsx +++ b/front_end/src/components/posts_feed/index.tsx @@ -5,7 +5,7 @@ import PaginatedPostsFeed, { } from "@/components/posts_feed/paginated_feed"; import { POSTS_PER_PAGE } from "@/constants/posts_feed"; import PostsApi, { PostsParams } from "@/services/posts"; -import ServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; import { Topic } from "@/types/projects"; type Props = { @@ -15,7 +15,6 @@ type Props = { }; const AwaitedPostsFeed: FC = async ({ filters, type, topics }) => { - return ServerComponentErrorBoundary(async () => { if ( topics && filters.topic && @@ -28,20 +27,19 @@ const AwaitedPostsFeed: FC = async ({ filters, type, topics }) => { ); } - const { results: questions, count } = await PostsApi.getPostsWithCP({ - ...filters, - limit: POSTS_PER_PAGE, - }); - - return ( - - ); + const { results: questions, count } = await PostsApi.getPostsWithCP({ + ...filters, + limit: POSTS_PER_PAGE, }); + + return ( + + ); }; -export default AwaitedPostsFeed; +export default WithServerComponentErrorBoundary(AwaitedPostsFeed); diff --git a/front_end/src/components/server_component_error_boundary.tsx b/front_end/src/components/server_component_error_boundary.tsx index e09b1442ea..ffca597bf0 100644 --- a/front_end/src/components/server_component_error_boundary.tsx +++ b/front_end/src/components/server_component_error_boundary.tsx @@ -1,29 +1,32 @@ -import React from "react"; +import { FC } from "react"; import RefreshButton from "./refresh_button"; -const ServerComponentErrorBoundary = async ( - fn: () => Promise -) => { - try { - return await fn(); - } catch (error) { - if (error instanceof Error) { - const { digest } = error as Error & { digest?: string }; +const WithServerComponentErrorBoundary =

( + Component: FC

+): FC

=> { + const WrappedComponent = async (props: P) => { + try { + return await Component(props); + } catch (error) { + if (error instanceof Error) { + const { digest } = error as Error & { digest?: string }; + return ( +

+

{digest ?? "Unknown error"}

+ +
+ ); + } + return (
-

{digest ?? "Unknown error"}

+

Unknown error

); } - - return ( -
-

Unknown error

- -
- ); - } + }; + return WrappedComponent as FC

; }; -export default ServerComponentErrorBoundary; +export default WithServerComponentErrorBoundary;