diff --git a/apps/web/app/error.tsx b/apps/web/app/error.tsx index b804d677acb169..0dd34475ecb409 100644 --- a/apps/web/app/error.tsx +++ b/apps/web/app/error.tsx @@ -1,64 +1,59 @@ "use client"; -/** - * Typescript class based component for custom-error - * @link https://nextjs.org/docs/advanced-features/custom-error-page - */ -import type { NextPage } from "next"; -import type { ErrorProps } from "next/error"; +import { captureException } from "@sentry/nextjs"; import React from "react"; +import { getErrorFromUnknown } from "@calcom/lib/errors"; import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; import { redactError } from "@calcom/lib/redactError"; import { ErrorPage } from "@components/error/error-page"; -type NextError = Error & { digest?: string }; - -// Ref: https://nextjs.org/docs/app/api-reference/file-conventions/error#props -export type DefaultErrorProps = { - error: NextError; - reset: () => void; // A function to reset the error boundary -}; - -type AugmentedError = NextError | HttpError | null; - -type CustomErrorProps = { - err?: AugmentedError; - statusCode?: number; - message?: string; -} & Omit; - const log = logger.getSubLogger({ prefix: ["[error]"] }); -const CustomError: NextPage = (props) => { - const { error } = props; - let errorObject: CustomErrorProps = { - message: error.message, - err: error, - }; +export type ErrorProps = { + error: Error; + reset?: () => void; +}; - if (error instanceof HttpError) { - const redactedError = redactError(error); - errorObject = { - statusCode: error.statusCode, - title: redactedError.name, - message: redactedError.message, - err: { - ...redactedError, - ...error, - }, +export default function Error({ error }: ErrorProps) { + React.useEffect(() => { + log.error(error); + + // Log the error to Sentry + captureException(error); + }, [error]); + + const processedError = React.useMemo(() => { + const err = getErrorFromUnknown(error); + + if (err instanceof HttpError) { + const redactedError = redactError(err); + return { + statusCode: err.statusCode, + title: redactedError.name, + name: redactedError.name, + message: redactedError.message, + url: err.url, + method: err.method, + cause: err.cause, + }; + } + + return { + statusCode: 500, + title: "Internal Server Error", + name: "Internal Server Error", + message: "An unexpected error occurred.", }; - } - - // `error.digest` property contains an automatically generated hash of the error that can be used to match the corresponding error in server-side logs - log.debug(`${error?.toString() ?? JSON.stringify(error)}`); - log.info("errorObject: ", errorObject); + }, [error]); return ( - + ); -}; - -export default CustomError; +} diff --git a/apps/web/app/global-error.tsx b/apps/web/app/global-error.tsx index ecf7247dbb5571..df022ffbb18ef0 100644 --- a/apps/web/app/global-error.tsx +++ b/apps/web/app/global-error.tsx @@ -2,9 +2,10 @@ import { type NextPage } from "next"; -import CustomError, { type DefaultErrorProps } from "./error"; +import CustomError from "./error"; +import type { ErrorProps } from "./error"; -export const GlobalError: NextPage = (props) => { +export const GlobalError: NextPage = (props) => { return ( diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx index a9a4bb5c9251f3..95a9a8c97ec5e5 100644 --- a/apps/web/app/not-found.tsx +++ b/apps/web/app/not-found.tsx @@ -1,12 +1,246 @@ -import React from "react"; +import { _generateMetadata, getTranslate } from "app/_utils"; +import { WithLayout } from "app/layoutHOC"; +import { headers } from "next/headers"; +import Link from "next/link"; + +import { + getOrgDomainConfigFromHostname, + subdomainSuffix, +} from "@calcom/features/ee/organizations/lib/orgDomains"; +import { DOCS_URL, IS_CALCOM, WEBSITE_URL } from "@calcom/lib/constants"; +import { Icon } from "@calcom/ui"; + +enum PageType { + ORG = "ORG", + TEAM = "TEAM", + USER = "USER", + OTHER = "OTHER", +} + +function getPageInfo(pathname: string, host: string) { + const { isValidOrgDomain, currentOrgDomain } = getOrgDomainConfigFromHostname({ hostname: host }); + const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? []; + if (!routerUsername || (isValidOrgDomain && currentOrgDomain)) { + return { + username: currentOrgDomain ?? "", + pageType: PageType.ORG, + url: `${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${ + currentOrgDomain?.replace("/", "") ?? "" + }`, + }; + } + + const splitPath = routerUsername.split("/"); + if (splitPath[1] === "team" && splitPath.length === 3) { + return { + username: splitPath[2], + pageType: PageType.TEAM, + url: `${WEBSITE_URL}/signup?callbackUrl=settings/teams/new%3Fslug%3D${splitPath[2].replace("/", "")}`, + }; + } + + return { + username: routerUsername, + pageType: PageType.USER, + url: `${WEBSITE_URL}/signup?username=${routerUsername.replace("/", "")}`, + }; +} + +async function NotFound() { + const t = await getTranslate(); + const headersList = headers(); + const host = headersList.get("x-forwarded-host") ?? ""; + const pathname = headersList.get("x-pathname") ?? ""; + + // This makes more sense after full migration to App Router + // if (!pathname) { + // redirect("/500"); + // } + + const { username, pageType, url } = getPageInfo(pathname, host); + const isBookingSuccessPage = pathname?.startsWith("/booking"); + const isSubpage = pathname?.includes("/", 2) || isBookingSuccessPage; + const isInsights = pathname?.startsWith("/insights"); + + const links = [ + { + title: t("enterprise"), + description: "Learn more about organizations and subdomains in our enterprise plan.", + icon: "shield" as const, + href: `${WEBSITE_URL}/enterprise`, + }, + { + title: t("documentation"), + description: t("documentation_description"), + icon: "file-text" as const, + href: DOCS_URL, + }, + { + title: t("blog"), + description: t("blog_description"), + icon: "book-open" as const, + href: `${WEBSITE_URL}/blog`, + }, + ]; + + /** + * If we're on 404 and the route is insights it means it is disabled + * TODO: Abstract this for all disabled features + **/ + if (isInsights) { + return ( +
+
+
+

{t("error_404")}

+

+ {t("feature_currently_disabled") ?? "Feature is currently disabled"} +

+
+
+
+ + {t("or_go_back_home")} + + +
+
+
+
+ ); + } -const NotFound = () => { return ( -
-

404 - Page Not Found

-

Sorry, the page you are looking for does not exist.

+
+
+
+

{t("error_404")}

+

+ {isBookingSuccessPage ? "Booking not found" : t("page_doesnt_exist")} +

+ {isSubpage && pageType !== PageType.TEAM ? ( + {t("check_spelling_mistakes_or_go_back")} + ) : IS_CALCOM ? ( + + {t(`404_the_${pageType.toLowerCase()}`)} + + {username ? ( + <> + {username} {t("is_still_available")}{" "} + {t("register_now")}. + + ) : null} + + ) : ( + + {t(`404_the_${pageType.toLowerCase()}`)}{" "} + {username ? ( + <> + {username}{" "} + {t("is_still_available")} + + ) : null} + + )} +
+
+ {((!isSubpage && IS_CALCOM) || pageType === PageType.ORG || pageType === PageType.TEAM) && ( + + )} +

{t("popular_pages")}

+ +
+ + {t("or_go_back_home")} + + +
+
+
); +} + +export const generateMetadata = async () => { + const headersList = headers(); + const pathname = headersList.get("x-pathname") ?? ""; + const isInsights = pathname?.startsWith("/insights"); + + const metadata = await _generateMetadata( + (t) => + isInsights + ? t("feature_currently_disabled") ?? "Feature is currently disabled" + : t("404_page_not_found"), + (t) => t("404_page_not_found") + ); + return { + ...metadata, + robots: { + index: false, + follow: false, + }, + }; }; -export default NotFound; +export default WithLayout({ + ServerPage: NotFound, +});