diff --git a/public/nextjs_migration/client/js/userMenu.js b/public/nextjs_migration/client/js/userMenu.js
new file mode 100644
index 00000000000..4720c3b2536
--- /dev/null
+++ b/public/nextjs_migration/client/js/userMenu.js
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const userMenuButton = document.querySelector('.user-menu-button')
+const userMenuPopover = document.querySelector('.user-menu-popover')
+const userMenuWrapper = document.querySelector('.user-menu-wrapper')
+
+function handleBlur (event, onBlur) {
+ const currentTarget = event.currentTarget
+
+ requestAnimationFrame(() => {
+ const isChildElement = currentTarget.contains(document.activeElement)
+
+ if (!isChildElement) {
+ onBlur()
+ }
+ })
+}
+
+function handleMenuButton () {
+ if (!userMenuPopover || !userMenuWrapper) {
+ return
+ }
+
+ if (userMenuPopover.hasAttribute('hidden')) {
+ // Show popover
+ userMenuPopover.setAttribute('aria-expanded', true)
+ userMenuPopover.removeAttribute('hidden')
+
+ // Handle onblur
+ userMenuWrapper.addEventListener('blur', (event) => handleBlur(event, handleMenuButton))
+ userMenuWrapper.focus()
+
+ // TODO: Re-enable event gtag events
+ // window.gtag('event', 'opened_closed_user_menu', { action: 'open' })
+ } else {
+ // Hide popover
+ userMenuPopover.setAttribute('aria-expanded', false)
+ userMenuPopover.setAttribute('hidden', '')
+
+ userMenuButton.focus()
+
+ // window.gtag('event', 'opened_closed_user_menu', { action: 'close' })
+ }
+}
+
+if (userMenuButton) {
+ userMenuButton.addEventListener('click', handleMenuButton)
+}
diff --git a/src/app/(nextjs_migration)/(authenticated)/user/breaches/page.tsx b/src/app/(nextjs_migration)/(authenticated)/user/breaches/page.tsx
new file mode 100644
index 00000000000..5c63a275f8a
--- /dev/null
+++ b/src/app/(nextjs_migration)/(authenticated)/user/breaches/page.tsx
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+export default async function UserBreaches() {
+ return
Dashboard
;
+}
diff --git a/src/app/(nextjs_migration)/(authenticated)/user/layout.tsx b/src/app/(nextjs_migration)/(authenticated)/user/layout.tsx
new file mode 100644
index 00000000000..ba35d54c9c4
--- /dev/null
+++ b/src/app/(nextjs_migration)/(authenticated)/user/layout.tsx
@@ -0,0 +1,109 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { ReactNode } from "react";
+import { getServerSession } from "next-auth";
+import { redirect } from "next/navigation";
+import Image from "next/image";
+
+import "../../../../client/css/index.css";
+import { UserMenu } from "../../components/client/UserMenu";
+import { SiteNavigation } from "../../components/client/SiteNavigation";
+import AppConstants from "../../../../appConstants.js";
+import MonitorLogo from "../../../../client/images/monitor-logo-transparent@2x.webp";
+import MozillaLogo from "../../../../client/images/moz-logo-1color-white-rgb-01.svg";
+import { getL10n } from "../../../functions/server/l10n";
+import { authOptions } from "../../../api/auth/[...nextauth]/route";
+export type Props = {
+ children: ReactNode;
+};
+
+const MainLayout = async (props: Props) => {
+ const session = await getServerSession(authOptions);
+ if (!session) {
+ redirect("/");
+ }
+
+ const l10n = getL10n();
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ {props.children}
+
+
+ >
+ );
+};
+
+export default MainLayout;
diff --git a/src/app/(nextjs_migration)/(guest)/layout.tsx b/src/app/(nextjs_migration)/(guest)/layout.tsx
index 74731068205..c9649158237 100644
--- a/src/app/(nextjs_migration)/(guest)/layout.tsx
+++ b/src/app/(nextjs_migration)/(guest)/layout.tsx
@@ -2,46 +2,79 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { ReactNode } from 'react'
-import '../../../client/css/index.css'
-import Image from 'next/image'
-import MonitorLogo from '../../../client/images/monitor-logo-transparent@2x.webp'
-import MozillaLogo from '../../../client/images/moz-logo-1color-white-rgb-01.svg'
-import { getL10n } from '../../functions/server/l10n'
+import { ReactNode } from "react";
+import "../../../client/css/index.css";
+import Image from "next/image";
+import MonitorLogo from "../../../client/images/monitor-logo-transparent@2x.webp";
+import MozillaLogo from "../../../client/images/moz-logo-1color-white-rgb-01.svg";
+import { SignInButton } from "../components/client/SignInButton";
+import { getL10n } from "../../functions/server/l10n";
export type Props = {
- children: ReactNode
-}
+ children: ReactNode;
+};
const GuestLayout = (props: Props) => {
- const l10n = getL10n()
+ const l10n = getL10n();
return (
<>
-
-
+
+
-
- {props.children}
-
-