diff --git a/apps/web/app/global-error.tsx b/apps/web/app/global-error.tsx deleted file mode 100644 index ecf7247dbb5571..00000000000000 --- a/apps/web/app/global-error.tsx +++ /dev/null @@ -1,17 +0,0 @@ -"use client"; - -import { type NextPage } from "next"; - -import CustomError, { type DefaultErrorProps } from "./error"; - -export const GlobalError: NextPage = (props) => { - return ( - - - - - - ); -}; - -export default GlobalError; diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx index a9a4bb5c9251f3..87f7bf726d62d5 100644 --- a/apps/web/app/not-found.tsx +++ b/apps/web/app/not-found.tsx @@ -1,12 +1,243 @@ -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") ?? ""; + + 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 const dynamic = "force-static"; + +export default WithLayout({ + ServerPage: NotFound, +}); diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 3e27481444ea00..cd9843a5d19034 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -160,6 +160,7 @@ export const config = { matcher: [ "/403", "/500", + "/icons", "/d/:path*", "/more/:path*", "/maintenance/:path*", @@ -171,6 +172,11 @@ export const config = { "/api/auth/signup", "/api/trpc/:path*", "/login", + "/auth/:path*", + "/apps/:path*", + "/event-types/:path*", + "/workflows/:path*", + "/getting-started/:path*", "/auth/login", "/future/auth/login", "/workflows/:path*", @@ -181,11 +187,18 @@ export const config = { "/bookings/:status/", "/video/:path*", "/teams", - "/future/teams/", + "/signup", "/settings/:path*", "/reschedule/:path*", "/availability/:path*", "/booking/:path*", + "/payment/:path*", + "/router/:path*", + "/routing-forms/:path*", + "/org/:path*", + "/team/:path*", + "/:user/:type/", + "/:user/", ], }; diff --git a/apps/web/pages/404.tsx b/apps/web/pages/404.tsx deleted file mode 100644 index a90e17681f85a6..00000000000000 --- a/apps/web/pages/404.tsx +++ /dev/null @@ -1,259 +0,0 @@ -"use client"; - -import type { GetStaticPropsContext } from "next"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import { useEffect, useState } from "react"; - -import { - getOrgDomainConfigFromHostname, - subdomainSuffix, -} from "@calcom/features/ee/organizations/lib/orgDomains"; -import { DOCS_URL, IS_CALCOM, WEBSITE_URL } from "@calcom/lib/constants"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { HeadSeo } from "@calcom/ui"; -import { Icon } from "@calcom/ui"; - -import PageWrapper from "@components/PageWrapper"; - -import { getTranslations } from "@server/lib/getTranslations"; - -enum pageType { - ORG = "org", - TEAM = "team", - USER = "user", - OTHER = "other", -} - -export default function Custom404() { - const pathname = usePathname(); - const { t } = useLocale(); - const [username, setUsername] = useState(""); - const [currentPageType, setCurrentPageType] = useState(pageType.USER); - - 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`, - }, - ]; - - const [url, setUrl] = useState(`${WEBSITE_URL}/signup`); - useEffect(() => { - const { isValidOrgDomain, currentOrgDomain } = getOrgDomainConfigFromHostname({ - hostname: window.location.host, - }); - - const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? []; - if (routerUsername && (!isValidOrgDomain || !currentOrgDomain)) { - const splitPath = routerUsername.split("/"); - if (splitPath[1] === "team" && splitPath.length === 3) { - // Accessing a non-existent team - setUsername(splitPath[2]); - setCurrentPageType(pageType.TEAM); - setUrl( - `${WEBSITE_URL}/signup?callbackUrl=settings/teams/new%3Fslug%3D${splitPath[2].replace("/", "")}` - ); - } else { - setUsername(routerUsername); - setUrl(`${WEBSITE_URL}/signup?username=${routerUsername.replace("/", "")}`); - } - } else { - setUsername(currentOrgDomain ?? ""); - setCurrentPageType(pageType.ORG); - setUrl( - `${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${ - currentOrgDomain?.replace("/", "") ?? "" - }` - ); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const isSuccessPage = pathname?.startsWith("/booking"); - const isSubpage = pathname?.includes("/", 2) || isSuccessPage; - - /** - * If we're on 404 and the route is insights it means it is disabled - * TODO: Abstract this for all disabled features - **/ - const isInsights = pathname?.startsWith("/insights"); - if (isInsights) { - return ( - <> - -
-
-
-

{t("error_404")}

-

- Feature is currently disabled -

-
-
-
- - {t("or_go_back_home")} - - -
-
-
-
- - ); - } - - if (!username) return null; - - return ( - <> - -
-
-
-

{t("error_404")}

-

- {isSuccessPage ? "Booking not found" : t("page_doesnt_exist")} -

- {isSubpage && currentPageType !== pageType.TEAM ? ( - {t("check_spelling_mistakes_or_go_back")} - ) : IS_CALCOM ? ( - - {t(`404_the_${currentPageType.toLowerCase()}`)}{" "} - {username} {t("is_still_available")}{" "} - {t("register_now")}. - - ) : ( - - {t(`404_the_${currentPageType.toLowerCase()}`)}{" "} - {username}{" "} - {t("is_still_available")} - - )} -
-
- {((!isSubpage && IS_CALCOM) || - currentPageType === pageType.ORG || - currentPageType === pageType.TEAM) && ( - - )} -

- {t("popular_pages")} -

- -
- - {t("or_go_back_home")} - - -
-
-
-
- - ); -} - -Custom404.PageWrapper = PageWrapper; - -export const getStaticProps = async (context: GetStaticPropsContext) => { - const i18n = await getTranslations(context); - - return { - props: { - i18n, - }, - }; -}; diff --git a/apps/web/playwright/organization/booking.e2e.ts b/apps/web/playwright/organization/booking.e2e.ts index c63055eff273b0..cce657cb1cb00c 100644 --- a/apps/web/playwright/organization/booking.e2e.ts +++ b/apps/web/playwright/organization/booking.e2e.ts @@ -31,6 +31,8 @@ function getOrgOrigin(orgSlug: string | null) { return orgOrigin; } +test.describe.configure({ mode: "parallel" }); + test.describe("Bookings", () => { test.afterEach(async ({ orgs, users, page }) => { await users.deleteAll(); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 197e317a6031d6..8133ae2c6e8178 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2919,6 +2919,7 @@ "dry_run_mode_active": "You are doing a test booking.", "icon_showcase": "Icon showcase", "icons_showcase": "Icons showcase", + "feature_currently_disabled": "Feature is currently disabled", "choose_an_option": "Choose an option", "enter_option_value": "Enter option value", "remove_option": "Remove option", diff --git a/apps/web/test/lib/pagesAndRewritePaths.test.ts b/apps/web/test/lib/pagesAndRewritePaths.test.ts index 55eabe63c8d41d..734dabb16b3fdd 100644 --- a/apps/web/test/lib/pagesAndRewritePaths.test.ts +++ b/apps/web/test/lib/pagesAndRewritePaths.test.ts @@ -23,7 +23,6 @@ describe("pagesAndRewritePaths", () => { "video", "workflows", "403", - "404", "500", "bookings", "event-types",