Skip to content

Commit 139a7c8

Browse files
DmytroHryshynzomarsgrzpab
authored
chore: [app dir bootstrapping 5] add RootLayout (calcom#11982)
Co-authored-by: zomars <[email protected]> Co-authored-by: Greg Pabian <[email protected]>
1 parent 158da51 commit 139a7c8

File tree

6 files changed

+516
-5
lines changed

6 files changed

+516
-5
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
diff --git a/index.cjs b/index.cjs
2+
index b645707a3549fc298508726e404243499bbed499..f34b0891e99b275a9218e253f303f43d31ef3f73 100644
3+
--- a/index.cjs
4+
+++ b/index.cjs
5+
@@ -13,8 +13,8 @@ function withMetadataArgument(func, _arguments) {
6+
// https://github.com/babel/babel/issues/2212#issuecomment-131827986
7+
// An alternative approach:
8+
// https://www.npmjs.com/package/babel-plugin-add-module-exports
9+
-exports = module.exports = min.parsePhoneNumberFromString
10+
-exports['default'] = min.parsePhoneNumberFromString
11+
+// exports = module.exports = min.parsePhoneNumberFromString
12+
+// exports['default'] = min.parsePhoneNumberFromString
13+
14+
// `parsePhoneNumberFromString()` named export is now considered legacy:
15+
// it has been promoted to a default export due to being too verbose.

apps/web/app/layout.tsx

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { Metadata } from "next";
2+
import { headers as nextHeaders, cookies as nextCookies } from "next/headers";
3+
import Script from "next/script";
4+
import React from "react";
5+
6+
import { getLocale } from "@calcom/features/auth/lib/getLocale";
7+
import { IS_PRODUCTION } from "@calcom/lib/constants";
8+
9+
import "../styles/globals.css";
10+
11+
export const metadata: Metadata = {
12+
icons: {
13+
icon: [
14+
{
15+
sizes: "32x32",
16+
url: "/api/logo?type=favicon-32",
17+
},
18+
{
19+
sizes: "16x16",
20+
url: "/api/logo?type=favicon-16",
21+
},
22+
],
23+
apple: {
24+
sizes: "180x180",
25+
url: "/api/logo?type=apple-touch-icon",
26+
},
27+
other: [
28+
{
29+
url: "/safari-pinned-tab.svg",
30+
rel: "mask-icon",
31+
},
32+
],
33+
},
34+
manifest: "/site.webmanifest",
35+
themeColor: [
36+
{ media: "(prefers-color-scheme: light)", color: "#f9fafb" },
37+
{ media: "(prefers-color-scheme: dark)", color: "#1C1C1C" },
38+
],
39+
other: {
40+
"msapplication-TileColor": "#000000",
41+
},
42+
};
43+
44+
const getInitialProps = async (
45+
url: string,
46+
headers: ReturnType<typeof nextHeaders>,
47+
cookies: ReturnType<typeof nextCookies>
48+
) => {
49+
const { pathname, searchParams } = new URL(url);
50+
51+
const isEmbed = pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null;
52+
const embedColorScheme = searchParams?.get("ui.color-scheme");
53+
54+
// @ts-expect-error we cannot access ctx.req in app dir, however headers and cookies are only properties needed to extract the locale
55+
const newLocale = await getLocale({ headers, cookies });
56+
let direction = "ltr";
57+
58+
try {
59+
const intlLocale = new Intl.Locale(newLocale);
60+
// @ts-expect-error INFO: Typescript does not know about the Intl.Locale textInfo attribute
61+
direction = intlLocale.textInfo?.direction;
62+
} catch (e) {
63+
console.error(e);
64+
}
65+
66+
return { isEmbed, embedColorScheme, locale: newLocale, direction };
67+
};
68+
69+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
70+
const headers = nextHeaders();
71+
const cookies = nextCookies();
72+
73+
const fullUrl = headers.get("x-url") ?? "";
74+
const nonce = headers.get("x-csp") ?? "";
75+
76+
const { locale, direction, isEmbed, embedColorScheme } = await getInitialProps(fullUrl, headers, cookies);
77+
return (
78+
<html
79+
lang={locale}
80+
dir={direction}
81+
style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}>
82+
<head nonce={nonce}>
83+
{!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
84+
// eslint-disable-next-line @next/next/no-sync-scripts
85+
<Script
86+
data-project-id="KjpMrKTnXquJVKfeqmjdTffVPf1a6Unw2LZ58iE4"
87+
src="https://snippet.meticulous.ai/v1/stagingMeticulousSnippet.js"
88+
/>
89+
)}
90+
</head>
91+
<body
92+
className="dark:bg-darkgray-50 desktop-transparent bg-subtle antialiased"
93+
style={
94+
isEmbed
95+
? {
96+
background: "transparent",
97+
// Keep the embed hidden till parent initializes and
98+
// - gives it the appropriate styles if UI instruction is there.
99+
// - gives iframe the appropriate height(equal to document height) which can only be known after loading the page once in browser.
100+
// - Tells iframe which mode it should be in (dark/light) - if there is a a UI instruction for that
101+
visibility: "hidden",
102+
}
103+
: {}
104+
}>
105+
{children}
106+
</body>
107+
</html>
108+
);
109+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"use client";
2+
3+
import type { SSRConfig } from "next-i18next";
4+
import { Inter } from "next/font/google";
5+
import localFont from "next/font/local";
6+
// import I18nLanguageHandler from "@components/I18nLanguageHandler";
7+
import { usePathname } from "next/navigation";
8+
import Script from "next/script";
9+
import type { ReactNode } from "react";
10+
11+
import "@calcom/embed-core/src/embed-iframe";
12+
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
13+
import { trpc } from "@calcom/trpc/react";
14+
15+
import type { AppProps } from "@lib/app-providers-app-dir";
16+
import AppProviders from "@lib/app-providers-app-dir";
17+
18+
export interface CalPageWrapper {
19+
(props?: AppProps): JSX.Element;
20+
PageWrapper?: AppProps["Component"]["PageWrapper"];
21+
}
22+
23+
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
24+
const calFont = localFont({
25+
src: "../fonts/CalSans-SemiBold.woff2",
26+
variable: "--font-cal",
27+
preload: true,
28+
display: "swap",
29+
});
30+
31+
export type PageWrapperProps = Readonly<{
32+
getLayout: (page: React.ReactElement) => ReactNode;
33+
children: React.ReactElement;
34+
requiresLicense: boolean;
35+
isThemeSupported: boolean;
36+
isBookingPage: boolean;
37+
nonce: string | undefined;
38+
themeBasis: string | null;
39+
i18n?: SSRConfig;
40+
}>;
41+
42+
function PageWrapper(props: PageWrapperProps) {
43+
const pathname = usePathname();
44+
let pageStatus = "200";
45+
46+
if (pathname === "/404") {
47+
pageStatus = "404";
48+
} else if (pathname === "/500") {
49+
pageStatus = "500";
50+
}
51+
52+
// On client side don't let nonce creep into DOM
53+
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
54+
// See https://github.com/kentcdodds/nonce-hydration-issues
55+
// Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
56+
const nonce = typeof window !== "undefined" ? (props.nonce ? "" : undefined) : props.nonce;
57+
const providerProps: PageWrapperProps = {
58+
...props,
59+
nonce,
60+
};
61+
62+
const getLayout: (page: React.ReactElement) => ReactNode = props.getLayout ?? ((page) => page);
63+
64+
return (
65+
<AppProviders {...providerProps}>
66+
{/* <I18nLanguageHandler locales={props.router.locales || []} /> */}
67+
<>
68+
<Script
69+
nonce={nonce}
70+
id="page-status"
71+
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
72+
/>
73+
<style jsx global>{`
74+
:root {
75+
--font-inter: ${interFont.style.fontFamily};
76+
--font-cal: ${calFont.style.fontFamily};
77+
}
78+
`}</style>
79+
80+
{getLayout(
81+
props.requiresLicense ? <LicenseRequired>{props.children}</LicenseRequired> : props.children
82+
)}
83+
</>
84+
</AppProviders>
85+
);
86+
}
87+
88+
export default trpc.withTRPC(PageWrapper);

0 commit comments

Comments
 (0)