From add6ffdfc447c8393bbb3a58f11600e8b9bd94ea Mon Sep 17 00:00:00 2001 From: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com> Date: Tue, 12 Dec 2023 20:43:15 +0200 Subject: [PATCH] chore: [app-router-migration-4]: apps/categories page (#12619) --- .env.example | 4 + apps/web/abTest/middlewareFactory.ts | 6 +- .../apps/categories/[category]/page.tsx | 79 +++++++++++++++++++ .../apps/categories/page.tsx | 56 +++++++++++++ apps/web/app/layout.tsx | 16 ++++ apps/web/components/PageWrapperAppDir.tsx | 17 ---- apps/web/lib/app-providers-app-dir.tsx | 6 +- apps/web/middleware.ts | 6 +- apps/web/pages/apps/categories/[category].tsx | 2 + apps/web/pages/apps/categories/index.tsx | 4 +- apps/web/playwright/ab-tests-redirect.e2e.ts | 54 +++++++++++++ turbo.json | 2 + 12 files changed, 225 insertions(+), 27 deletions(-) create mode 100644 apps/web/app/future/(individual-page-wrapper)/apps/categories/[category]/page.tsx create mode 100644 apps/web/app/future/(individual-page-wrapper)/apps/categories/page.tsx diff --git a/.env.example b/.env.example index c0ecaa4d65..dd9aa60e05 100644 --- a/.env.example +++ b/.env.example @@ -297,3 +297,7 @@ APP_ROUTER_EVENT_TYPES_ENABLED=1 APP_ROUTER_SETTINGS_ADMIN_ENABLED=1 APP_ROUTER_APPS_SLUG_ENABLED=1 APP_ROUTER_APPS_SLUG_SETUP_ENABLED=1 +# whether we redirect to the future/apps/categories from /apps/categories or not +APP_ROUTER_APPS_CATEGORIES_ENABLED=1 +# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not +APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=1 diff --git a/apps/web/abTest/middlewareFactory.ts b/apps/web/abTest/middlewareFactory.ts index 596a8b3035..f6743672da 100644 --- a/apps/web/abTest/middlewareFactory.ts +++ b/apps/web/abTest/middlewareFactory.ts @@ -6,8 +6,10 @@ import z from "zod"; const ROUTES: [URLPattern, boolean][] = [ ["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const, ["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const, - ["/apps/:slug", Boolean(process.env.APP_ROUTER_APPS_SLUG_ENABLED)] as const, - ["/apps/:slug/setup", Boolean(process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED)] as const, + ["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const, + ["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const, + ["/apps/categories", process.env.APP_ROUTER_APPS_CATEGORIES_ENABLED === "1"] as const, + ["/apps/categories/:category", process.env.APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED === "1"] as const, ].map(([pathname, enabled]) => [ new URLPattern({ pathname, diff --git a/apps/web/app/future/(individual-page-wrapper)/apps/categories/[category]/page.tsx b/apps/web/app/future/(individual-page-wrapper)/apps/categories/[category]/page.tsx new file mode 100644 index 0000000000..b58e845d83 --- /dev/null +++ b/apps/web/app/future/(individual-page-wrapper)/apps/categories/[category]/page.tsx @@ -0,0 +1,79 @@ +import CategoryPage from "@pages/apps/categories/[category]"; +import { Prisma } from "@prisma/client"; +import { _generateMetadata } from "app/_utils"; +import { notFound } from "next/navigation"; +import z from "zod"; + +import { getAppRegistry } from "@calcom/app-store/_appRegistry"; +import { APP_NAME } from "@calcom/lib/constants"; +import prisma from "@calcom/prisma"; +import { AppCategories } from "@calcom/prisma/enums"; + +import PageWrapper from "@components/PageWrapperAppDir"; + +export const generateMetadata = async () => { + return await _generateMetadata( + () => `${APP_NAME} | ${APP_NAME}`, + () => "" + ); +}; + +export const generateStaticParams = async () => { + const paths = Object.keys(AppCategories); + + try { + await prisma.$queryRaw`SELECT 1`; + } catch (e: unknown) { + if (e instanceof Prisma.PrismaClientInitializationError) { + // Database is not available at build time. Make sure we fall back to building these pages on demand + return []; + } else { + throw e; + } + } + + return paths.map((category) => ({ category })); +}; + +const querySchema = z.object({ + category: z.nativeEnum(AppCategories), +}); + +const getPageProps = async ({ params }: { params: Record }) => { + const p = querySchema.safeParse(params); + + if (!p.success) { + return notFound(); + } + + const appQuery = await prisma.app.findMany({ + where: { + categories: { + has: p.data.category, + }, + }, + select: { + slug: true, + }, + }); + + const dbAppsSlugs = appQuery.map((category) => category.slug); + + const appStore = await getAppRegistry(); + + const apps = appStore.filter((app) => dbAppsSlugs.includes(app.slug)); + return { + apps, + }; +}; + +export default async function Page({ params }: { params: Record }) { + const { apps } = await getPageProps({ params }); + return ( + + + + ); +} + +export const dynamic = "force-static"; diff --git a/apps/web/app/future/(individual-page-wrapper)/apps/categories/page.tsx b/apps/web/app/future/(individual-page-wrapper)/apps/categories/page.tsx new file mode 100644 index 0000000000..c0d6c3d15e --- /dev/null +++ b/apps/web/app/future/(individual-page-wrapper)/apps/categories/page.tsx @@ -0,0 +1,56 @@ +import LegacyPage from "@pages/apps/categories/index"; +import { ssrInit } from "app/_trpc/ssrInit"; +import { _generateMetadata } from "app/_utils"; +import { cookies, headers } from "next/headers"; + +import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { APP_NAME } from "@calcom/lib/constants"; + +import PageWrapper from "@components/PageWrapperAppDir"; + +export const generateMetadata = async () => { + return await _generateMetadata( + () => `Categories | ${APP_NAME}`, + () => "" + ); +}; + +async function getPageProps() { + const ssr = await ssrInit(); + const req = { headers: headers(), cookies: cookies() }; + + // @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | IncomingMessage + const session = await getServerSession({ req }); + + let appStore; + if (session?.user?.id) { + appStore = await getAppRegistryWithCredentials(session.user.id); + } else { + appStore = await getAppRegistry(); + } + + const categories = appStore.reduce((c, app) => { + for (const category of app.categories) { + c[category] = c[category] ? c[category] + 1 : 1; + } + return c; + }, {} as Record); + + return { + categories: Object.entries(categories).map(([name, count]) => ({ name, count })), + dehydratedState: await ssr.dehydrate(), + }; +} + +export default async function Page() { + const props = await getPageProps(); + const h = headers(); + const nonce = h.get("x-nonce") ?? undefined; + + return ( + + + + ); +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index f17543074f..579ba8b770 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,4 +1,6 @@ import { dir } from "i18next"; +import { Inter } from "next/font/google"; +import localFont from "next/font/local"; import { headers, cookies } from "next/headers"; import Script from "next/script"; import React from "react"; @@ -10,6 +12,14 @@ import { prepareRootMetadata } from "@lib/metadata"; import "../styles/globals.css"; +const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" }); +const calFont = localFont({ + src: "../fonts/CalSans-SemiBold.woff2", + variable: "--font-cal", + preload: true, + display: "block", +}); + export const generateMetadata = () => prepareRootMetadata({ twitterCreator: "@calcom", @@ -66,6 +76,12 @@ export default async function RootLayout({ children }: { children: React.ReactNo src="https://snippet.meticulous.ai/v1/stagingMeticulousSnippet.js" /> )} + ReactNode) | null; children: React.ReactElement; @@ -71,13 +61,6 @@ function PageWrapper(props: PageWrapperProps) { id="page-status" dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }} /> - - {getLayout( props.requiresLicense ? {props.children} : props.children )} diff --git a/apps/web/lib/app-providers-app-dir.tsx b/apps/web/lib/app-providers-app-dir.tsx index 2b2d57d2ea..0b6c8f009c 100644 --- a/apps/web/lib/app-providers-app-dir.tsx +++ b/apps/web/lib/app-providers-app-dir.tsx @@ -97,13 +97,9 @@ const CustomI18nextProvider = (props: { children: React.ReactElement; i18n?: SSR const clientViewerI18n = useViewerI18n(locale); const i18n = clientViewerI18n.data?.i18n ?? props.i18n; - if (!i18n || !i18n._nextI18Next) { - return null; - } - return ( // @ts-expect-error AppWithTranslationHoc expects AppProps - + {props.children} ); diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 40f1705315..fa4da2d680 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -103,12 +103,14 @@ export const config = { "/future/event-types/", "/settings/admin/:path*", "/future/settings/admin/:path*", - "/apps/:slug/", "/future/apps/:slug/", - "/apps/:slug/setup/", "/future/apps/:slug/setup/", + "/apps/categories/", + "/future/apps/categories/", + "/apps/categories/:category/", + "/future/apps/categories/:category/", ], }; diff --git a/apps/web/pages/apps/categories/[category].tsx b/apps/web/pages/apps/categories/[category].tsx index 81de5e70c8..0cad97bfa1 100644 --- a/apps/web/pages/apps/categories/[category].tsx +++ b/apps/web/pages/apps/categories/[category].tsx @@ -1,3 +1,5 @@ +"use client"; + import { Prisma } from "@prisma/client"; import type { GetStaticPropsContext, InferGetStaticPropsType } from "next"; import Link from "next/link"; diff --git a/apps/web/pages/apps/categories/index.tsx b/apps/web/pages/apps/categories/index.tsx index 7b40ade47b..1012640fc8 100644 --- a/apps/web/pages/apps/categories/index.tsx +++ b/apps/web/pages/apps/categories/index.tsx @@ -1,3 +1,5 @@ +"use client"; + import type { GetServerSidePropsContext } from "next"; import Link from "next/link"; @@ -13,7 +15,7 @@ import PageWrapper from "@components/PageWrapper"; import { ssrInit } from "@server/lib/ssr"; -export default function Apps({ categories }: inferSSRProps) { +export default function Apps({ categories }: Omit, "trpcState">) { const { t, isLocaleReady } = useLocale(); return ( diff --git a/apps/web/playwright/ab-tests-redirect.e2e.ts b/apps/web/playwright/ab-tests-redirect.e2e.ts index feb64ac1eb..601c04f282 100644 --- a/apps/web/playwright/ab-tests-redirect.e2e.ts +++ b/apps/web/playwright/ab-tests-redirect.e2e.ts @@ -58,4 +58,58 @@ test.describe("apps/ A/B tests", () => { await expect(locator).toBeVisible(); }); + + test("should point to the /future/apps/categories", async ({ page, users, context }) => { + await context.addCookies([ + { + name: "x-calcom-future-routes-override", + value: "1", + url: "http://localhost:3000", + }, + ]); + const user = await users.create(); + + await user.apiLogin(); + + await page.goto("/apps/categories"); + + await page.waitForLoadState(); + + const dataNextJsRouter = await page.evaluate(() => + window.document.documentElement.getAttribute("data-nextjs-router") + ); + + expect(dataNextJsRouter).toEqual("app"); + + const locator = page.getByTestId("app-store-category-messaging"); + + await expect(locator).toBeVisible(); + }); + + test("should point to the /future/apps/categories/[category]", async ({ page, users, context }) => { + await context.addCookies([ + { + name: "x-calcom-future-routes-override", + value: "1", + url: "http://localhost:3000", + }, + ]); + const user = await users.create(); + + await user.apiLogin(); + + await page.goto("/apps/categories/messaging"); + + await page.waitForLoadState(); + + const dataNextJsRouter = await page.evaluate(() => + window.document.documentElement.getAttribute("data-nextjs-router") + ); + + expect(dataNextJsRouter).toEqual("app"); + + const locator = page.getByText(/messaging apps/i); + + await expect(locator).toBeVisible(); + }); }); diff --git a/turbo.json b/turbo.json index 100cd41c08..f6a0f7145b 100644 --- a/turbo.json +++ b/turbo.json @@ -198,6 +198,8 @@ "ALLOWED_HOSTNAMES", "ANALYZE", "API_KEY_PREFIX", + "APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED", + "APP_ROUTER_APPS_CATEGORIES_ENABLED", "APP_ROUTER_APPS_SLUG_ENABLED", "APP_ROUTER_APPS_SLUG_SETUP_ENABLED", "APP_ROUTER_EVENT_TYPES_ENABLED",