diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 46358486..b8c0c80a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -56,10 +56,10 @@ Chayn team members usually respond within 3 business days.
Chayn is open to all kinds of contributions, such as:
- additional software tests / test coverage
-- dependency updates *check Dependabot pull requests
+- dependency updates \*check Dependabot pull requests
- code (requested features, bug fixes, quality enhancements, maintenance help)
- accessibility and language support.
-- no-code (documentation, translations) *see spam policy below for accepted documentation changes.
+- no-code (documentation, translations) \*see spam policy below for accepted documentation changes.
# Chayn's Spam Contribution Policy 🚫
diff --git a/app/ThemeRegistry.tsx b/app/ThemeRegistry.tsx
new file mode 100644
index 00000000..873d699e
--- /dev/null
+++ b/app/ThemeRegistry.tsx
@@ -0,0 +1,17 @@
+import { AppRouterCacheProvider } from '@mui/material-nextjs/v13-appRouter';
+import CssBaseline from '@mui/material/CssBaseline';
+import { ThemeProvider } from '@mui/material/styles';
+import theme from '../styles/theme';
+
+// This implementation is from mui integrations with nextjs app router
+// see https://mui.com/material-ui/integrations/nextjs/#app-router
+export default function ThemeRegistry({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/pages/chat.tsx b/app/[locale]/chat/chat.tsx
similarity index 58%
rename from pages/chat.tsx
rename to app/[locale]/chat/chat.tsx
index 895d7771..02423617 100644
--- a/pages/chat.tsx
+++ b/app/[locale]/chat/chat.tsx
@@ -1,22 +1,18 @@
+'use client';
+
import { Box } from '@mui/material';
import { ISbStoryData, useStoryblokState } from '@storyblok/react';
-import { GetStaticPropsContext, NextPage } from 'next';
import { useTranslations } from 'next-intl';
import Head from 'next/head';
-import { SignUpBanner } from '../components/banner/SignUpBanner';
-import NoDataAvailable from '../components/common/NoDataAvailable';
-import CrispButton from '../components/crisp/CrispButton';
-import Header, { HeaderProps } from '../components/layout/Header';
-import StoryblokPageSection from '../components/storyblok/StoryblokPageSection';
-import { useTypedSelector } from '../hooks/store';
-import { getStoryblokPageProps } from '../utils/getStoryblokPageProps';
-import { getEventUserData } from '../utils/logEvent';
-
-interface Props {
- story: ISbStoryData | null;
-}
+import { SignUpBanner } from '../../../components/banner/SignUpBanner';
+import NoDataAvailable from '../../../components/common/NoDataAvailable';
+import CrispButton from '../../../components/crisp/CrispButton';
+import Header, { HeaderProps } from '../../../components/layout/Header';
+import StoryblokPageSection from '../../../components/storyblok/StoryblokPageSection';
+import { useTypedSelector } from '../../../hooks/store';
+import { getEventUserData } from '../../../utils/logEvent';
-const Chat: NextPage = ({ story }) => {
+const Chat = ({ story }: { story: ISbStoryData | null }) => {
story = useStoryblokState(story);
const t = useTranslations('Courses');
@@ -68,21 +64,4 @@ const Chat: NextPage = ({ story }) => {
);
};
-export async function getStaticProps({ locale, preview = false }: GetStaticPropsContext) {
- const storyblokProps = await getStoryblokPageProps('chat', locale, preview);
-
- return {
- props: {
- ...storyblokProps,
- messages: {
- ...require(`../messages/shared/${locale}.json`),
- ...require(`../messages/navigation/${locale}.json`),
- ...require(`../messages/courses/${locale}.json`),
- ...require(`../messages/chat/${locale}.json`),
- },
- },
- revalidate: 3600, // revalidate every hour
- };
-}
-
export default Chat;
diff --git a/app/[locale]/chat/page.tsx b/app/[locale]/chat/page.tsx
new file mode 100644
index 00000000..a3e130a2
--- /dev/null
+++ b/app/[locale]/chat/page.tsx
@@ -0,0 +1,19 @@
+import { getLocale } from 'next-intl/server';
+import { locales } from '../../../i18n/config';
+import { getStoryblokPageProps } from '../../../utils/getStoryblokPageProps';
+import Chat from './chat';
+
+export const revalidate = 3600;
+
+export default async function Page() {
+ const preview = false;
+ const locale = await getLocale();
+ const storyblokProps = await getStoryblokPageProps('chat', locale, preview);
+ return ;
+}
+
+export async function generateStaticParams() {
+ return locales.map((locale) => {
+ return { params: { locale } };
+ });
+}
diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx
new file mode 100644
index 00000000..450d822f
--- /dev/null
+++ b/app/[locale]/layout.tsx
@@ -0,0 +1,13 @@
+import { locales } from '../../i18n/config';
+
+const Layout = ({ children }: { children: React.ReactNode }) => {
+ return <>{children}>;
+};
+
+export async function generateStaticParams() {
+ return locales.map((locale) => {
+ return { params: { locale } };
+ });
+}
+
+export default Layout;
diff --git a/app/[locale]/meet-the-team/meet-the-team.tsx b/app/[locale]/meet-the-team/meet-the-team.tsx
new file mode 100644
index 00000000..e6304ea1
--- /dev/null
+++ b/app/[locale]/meet-the-team/meet-the-team.tsx
@@ -0,0 +1,23 @@
+'use client';
+
+import { ISbStoryData, useStoryblokState } from '@storyblok/react';
+import NoDataAvailable from '../../../components/common/NoDataAvailable';
+import StoryblokMeetTheTeamPage, {
+ StoryblokMeetTheTeamPageProps,
+} from '../../../components/storyblok/StoryblokMeetTheTeamPage';
+
+interface MeetTheTeamProps {
+ story: ISbStoryData | null;
+}
+
+const MeetTheTeam = ({ story }: MeetTheTeamProps) => {
+ const storyData = useStoryblokState(story);
+
+ if (!storyData) {
+ return ;
+ }
+
+ return ;
+};
+
+export default MeetTheTeam;
diff --git a/app/[locale]/meet-the-team/page.tsx b/app/[locale]/meet-the-team/page.tsx
new file mode 100644
index 00000000..a24daf43
--- /dev/null
+++ b/app/[locale]/meet-the-team/page.tsx
@@ -0,0 +1,11 @@
+import { getStoryblokPageProps } from '../../../utils/getStoryblokPageProps';
+import MeetTheTeam from './meet-the-team';
+
+export const revalidate = 3600;
+
+export default async function Page({ params }: { params: { locale: string } }) {
+ const preview = false;
+ const locale = params.locale;
+ const storyblokProps = await getStoryblokPageProps('meet-the-team', locale, preview);
+ return ;
+}
diff --git a/app/[locale]/welcome/[partnerName]/page.tsx b/app/[locale]/welcome/[partnerName]/page.tsx
new file mode 100644
index 00000000..903489a8
--- /dev/null
+++ b/app/[locale]/welcome/[partnerName]/page.tsx
@@ -0,0 +1,46 @@
+import { getStoryblokApi, ISbStoriesParams, ISbStoryData } from '@storyblok/react';
+import { locales } from '../../../../i18n/config';
+import { getStoryblokPageProps } from '../../../../utils/getStoryblokPageProps';
+import Welcome from './welcome';
+
+export const revalidate = 3600;
+
+export default async function Page({
+ params,
+}: {
+ params: { partnerName: string; locale: string };
+}) {
+ const preview = false;
+ const locale = params.locale;
+ const partnerName = params?.partnerName;
+ const storyblokProps = await getStoryblokPageProps(`welcome/${partnerName}`, locale, preview);
+ return ;
+}
+
+export async function generateStaticParams() {
+ let sbParams: ISbStoriesParams = {
+ published: true,
+ starts_with: 'partnership/',
+ };
+
+ const storyblokApi = getStoryblokApi();
+ let data = await storyblokApi.getAll('cdn/links', sbParams);
+
+ let paths: any = [];
+
+ data.forEach((story: Partial) => {
+ if (!story.slug) return;
+
+ // get array for slug because of catch all
+ let splittedSlug = story.slug.split('/');
+
+ if (locales) {
+ // create additional languages
+ for (const locale of locales) {
+ paths.push({ params: { partnerName: splittedSlug[1] } });
+ }
+ }
+ });
+
+ return paths;
+}
diff --git a/app/[locale]/welcome/[partnerName]/welcome.tsx b/app/[locale]/welcome/[partnerName]/welcome.tsx
new file mode 100644
index 00000000..9b5edd09
--- /dev/null
+++ b/app/[locale]/welcome/[partnerName]/welcome.tsx
@@ -0,0 +1,28 @@
+'use client';
+
+import { ISbStoryData, useStoryblokState } from '@storyblok/react';
+import NoDataAvailable from '../../../../components/common/NoDataAvailable';
+import StoryblokWelcomePage, {
+ StoryblokWelcomePageProps,
+} from '../../../../components/storyblok/StoryblokWelcomePage';
+
+interface WelcomeProps {
+ story: ISbStoryData | null;
+}
+
+const Welcome = ({ story }: WelcomeProps) => {
+ story = useStoryblokState(story);
+
+ if (!story) {
+ return ;
+ }
+
+ return (
+
+ );
+};
+
+export default Welcome;
diff --git a/app/appLayout.tsx b/app/appLayout.tsx
new file mode 100644
index 00000000..ad804588
--- /dev/null
+++ b/app/appLayout.tsx
@@ -0,0 +1,61 @@
+'use client';
+
+import { Analytics } from '@mui/icons-material';
+import { usePathname } from 'next/navigation';
+import { Hotjar } from 'nextjs-hotjar';
+import { useEffect } from 'react';
+import { AppBarSpacer } from '../components/layout/AppBarSpacer';
+import Consent from '../components/layout/Consent';
+import Footer from '../components/layout/Footer';
+import LanguageMenuAppRoute from '../components/layout/LanguageMenuAppRoute';
+import LeaveSiteButton from '../components/layout/LeaveSiteButton';
+import TopBar from '../components/layout/TopBar';
+import firebase from '../config/firebase';
+import { AuthGuard } from '../guards/AuthGuard';
+
+interface AppLayoutProps {
+ children?: React.ReactNode;
+}
+
+// Init firebase
+firebase;
+
+export default function AppLayout({ children }: AppLayoutProps) {
+ const pathname = usePathname();
+
+ // Get top level directory of path e.g pathname /courses/course_name has pathHead courses
+ const pathHead = pathname?.split('/')[1]; // E.g. courses | therapy | partner-admin
+
+ useEffect(() => {
+ // Check if entry path is from a partner referral and if so, store referring partner in local storage
+ // This enables us to redirect a user to the correct sign up page later (e.g. in SignUpBanner)
+ const path = pathname;
+
+ if (path?.includes('/welcome/')) {
+ const referralPartner = path.split('/')[2]; // Gets "bumble" from /welcome/bumble
+
+ if (referralPartner) {
+ window.localStorage.setItem('referralPartner', referralPartner);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+ <>
+
+
+
+
+ {pathHead !== 'partner-admin' && }
+ {children as JSX.Element}
+
+
+ {!!process.env.NEXT_PUBLIC_HOTJAR_ID && process.env.NEXT_PUBLIC_ENV !== 'local' && (
+
+ )}
+ {/* Vercel analytics */}
+
+ >
+ );
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 00000000..fe5744a1
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,91 @@
+import newrelic from 'newrelic';
+import Script from 'next/script';
+
+import { Metadata } from 'next';
+import { NextIntlClientProvider } from 'next-intl';
+import { getLocale, getMessages } from 'next-intl/server';
+import CrispScript from '../components/crisp/CrispScript';
+import GoogleTagManagerScript from '../components/head/GoogleTagManagerScript';
+import OpenGraphMetadata from '../components/head/OpenGraphMetadata';
+import RollbarScript from '../components/head/RollbarScript';
+import ErrorBoundary from '../components/layout/ErrorBoundary';
+import { storyblok } from '../config/storyblok';
+import StoreProvider from '../store/storeProvider';
+import '../styles/globals.css';
+import AppLayout from './appLayout';
+import ThemeRegistry from './ThemeRegistry';
+
+// Init storyblok
+storyblok;
+
+export const metadata: Metadata = {
+ title: 'Bloom',
+};
+
+export const dynamicParams = false;
+
+export default async function RootLayout({
+ // Layouts must accept a children prop.
+ // This will be populated with nested layouts or pages
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ // Configuration according to Newrelic app router example
+ // See https://github.com/newrelic/newrelic-node-nextjs?tab=readme-ov-file#example-projects
+ // @ts-ignore
+ if (newrelic.agent.collector.isConnected() === false) {
+ await new Promise((resolve) => {
+ // @ts-ignore
+ newrelic.agent.on('connected', resolve);
+ });
+ }
+
+ const browserTimingHeader = newrelic.getBrowserTimingHeader({
+ hasToRemoveScriptWrapper: true,
+ // @ts-ignore
+ allowTransactionlessInjection: true,
+ });
+
+ const locale = await getLocale();
+
+ // Providing all messages to the client
+ // side is the easiest way to get started
+ const messages = await getMessages();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/not-found.tsx b/app/not-found.tsx
new file mode 100644
index 00000000..180594f5
--- /dev/null
+++ b/app/not-found.tsx
@@ -0,0 +1,5 @@
+import { redirect } from 'next/navigation';
+
+export default function Page() {
+ redirect('/404');
+}
diff --git a/components/banner/UserResearchBanner.tsx b/components/banner/UserResearchBanner.tsx
index ac715ebe..0bd4da29 100644
--- a/components/banner/UserResearchBanner.tsx
+++ b/components/banner/UserResearchBanner.tsx
@@ -1,6 +1,6 @@
import { Alert, AlertTitle, Button, Collapse, Stack } from '@mui/material';
import Cookies from 'js-cookie';
-import { useRouter } from 'next/router';
+import { usePathname } from 'next/navigation';
import React from 'react';
import { FeatureFlag } from '../../config/featureFlag';
import { USER_BANNER_DISMISSED, USER_BANNER_INTERESTED } from '../../constants/events';
@@ -28,7 +28,7 @@ export default function UserResearchBanner() {
const partnerAdmin = useTypedSelector((state) => state.partnerAdmin);
const eventUserData = getEventUserData(userCreatedAt, partnerAccesses, partnerAdmin);
- const router = useRouter();
+ const pathname = usePathname();
const isBannerNotInteracted = !Boolean(Cookies.get(USER_RESEARCH_BANNER_INTERACTED));
const isBannerFeatureEnabled = FeatureFlag.isUserResearchBannerEnabled();
// const isPublicUser = partnerAccesses.length === 0 && !partnerAdmin.id;
@@ -36,9 +36,7 @@ export default function UserResearchBanner() {
return pa.partner.name.toLowerCase() === 'badoo';
});
- const isTargetPage = !(
- router.pathname.includes('auth') || router.pathname.includes('partnerName')
- );
+ const isTargetPage = !(pathname?.includes('auth') || pathname?.includes('partnerName'));
const showBanner = isBannerFeatureEnabled && isBadooUser && isTargetPage && isBannerNotInteracted;
return showBanner ? (
diff --git a/components/common/Link.tsx b/components/common/Link.tsx
index 13d1fdc5..c1a99ded 100644
--- a/components/common/Link.tsx
+++ b/components/common/Link.tsx
@@ -1,9 +1,11 @@
+'use client';
+
import OpenInNew from '@mui/icons-material/OpenInNew';
import MuiLink, { LinkProps as MuiLinkProps } from '@mui/material/Link';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import NextLink, { LinkProps as NextLinkProps } from 'next/link';
-import { useRouter } from 'next/router';
+import { usePathname } from 'next/navigation';
import * as React from 'react';
// Add support for the sx prop for consistency with the other branches.
@@ -65,10 +67,10 @@ const Link = React.forwardRef(function Link(props,
...other
} = props;
- const router = useRouter();
- const pathname = typeof href === 'string' ? href : href.pathname;
+ const pathname = usePathname();
+ const hrefPathname = typeof href === 'string' ? href : href.pathname;
const className = clsx(classNameProps, {
- [activeClassName]: router.pathname === pathname && activeClassName,
+ [activeClassName]: pathname === hrefPathname && activeClassName,
});
const isExternal =
diff --git a/components/crisp/CrispScript.tsx b/components/crisp/CrispScript.tsx
index 82ae6875..4b8c77eb 100644
--- a/components/crisp/CrispScript.tsx
+++ b/components/crisp/CrispScript.tsx
@@ -1,4 +1,6 @@
-import { useRouter } from 'next/router';
+'use client';
+
+import { useLocale } from 'next-intl';
import Script from 'next/script';
import { useEffect } from 'react';
import { CHAT_MESSAGE_SENT, CHAT_STARTED, FIRST_CHAT_STARTED } from '../../constants/events';
@@ -7,6 +9,7 @@ import logEvent, { getEventUserData } from '../../utils/logEvent';
import { createCrispProfileData } from './utils/createCrispProfileData';
const CrispScript = () => {
+ const locale = useLocale();
const userCreatedAt = useTypedSelector((state) => state.user.createdAt);
const userEmail = useTypedSelector((state) => state.user.email);
const userCrispTokenId = useTypedSelector((state) => state.user.crispTokenId);
@@ -14,8 +17,6 @@ const CrispScript = () => {
const partnerAdmin = useTypedSelector((state) => state.partnerAdmin);
const courses = useTypedSelector((state) => state.courses);
- const router = useRouter();
-
const eventUserData = getEventUserData(userCreatedAt, partnerAccesses, partnerAdmin);
useEffect(() => {
@@ -81,7 +82,7 @@ const CrispScript = () => {
__html: `
window.$crisp=[];
CRISP_RUNTIME_CONFIG = {
- locale : ${router.locale ? `"${router.locale}"` : 'en'}
+ locale : ${locale ? `"${locale}"` : 'en'}
};
window.CRISP_WEBSITE_ID="${process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID}";
(function(){
diff --git a/components/forms/WelcomeCodeForm.tsx b/components/forms/WelcomeCodeForm.tsx
index 2e3ca642..2b171d1b 100644
--- a/components/forms/WelcomeCodeForm.tsx
+++ b/components/forms/WelcomeCodeForm.tsx
@@ -1,6 +1,6 @@
import { Box, Button, TextField } from '@mui/material';
import { useTranslations } from 'next-intl';
-import { useRouter } from 'next/router';
+import { useRouter } from 'next/navigation';
import * as React from 'react';
import { useEffect, useState } from 'react';
import { generatePartnerPromoGetStartedEvent } from '../../constants/events';
diff --git a/components/head/OpenGraphMetadata.tsx b/components/head/OpenGraphMetadata.tsx
index 771872a2..79c4f851 100644
--- a/components/head/OpenGraphMetadata.tsx
+++ b/components/head/OpenGraphMetadata.tsx
@@ -1,4 +1,4 @@
-import theme from '../../styles/theme';
+import { COLOUR_PRIMARY_MAIN } from '../../styles/common';
const descriptionContent =
'Join us on your healing journey. Bloom is here for you to learn, heal and grow towards a confident future. It is bought to you by Chayn, a global non-profit, run by survivors and allies from around the world.';
@@ -19,7 +19,7 @@ const OpenGraphMetadata = () => {
{/** PWA specific tags **/}
-
+
>
);
};
diff --git a/components/head/RollbarScript.tsx b/components/head/RollbarScript.tsx
index 57b74f2e..6e0a71d8 100644
--- a/components/head/RollbarScript.tsx
+++ b/components/head/RollbarScript.tsx
@@ -1,19 +1,22 @@
+import Script from 'next/script';
+
const RollbarScript = () => {
return (
-