From b8331658cef441b92824c1923751e8d686e2eb5b Mon Sep 17 00:00:00 2001 From: Florian Zia Date: Thu, 21 Nov 2024 14:44:19 +0100 Subject: [PATCH] feat: Add navigation for settings page tabs --- .../(dashboard)/settings/SettingsContent.tsx | 35 ++++++++--- .../user/(dashboard)/settings/View.tsx | 5 +- .../settings/{ => [[...slug]]}/page.tsx | 62 +++++++++++++------ .../settings/panels/SettingsPanelEditInfo.tsx | 2 +- .../panels/SettingsPanelManageAccount.tsx | 2 +- .../panels/SettingsPanelNotifications.tsx | 2 +- .../(dashboard)/settings/panels/index.tsx | 2 +- 7 files changed, 77 insertions(+), 33 deletions(-) rename src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/{ => [[...slug]]}/page.tsx (62%) diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/SettingsContent.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/SettingsContent.tsx index 8e4fe1de2dc..2a82fff6fc7 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/SettingsContent.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/SettingsContent.tsx @@ -4,21 +4,32 @@ "use client"; -import { useState } from "react"; +import { ReactNode, useEffect, useState } from "react"; +import { usePathname } from "next/navigation"; import { useL10n } from "../../../../../../hooks/l10n"; -import { TabType } from "../dashboard/View"; import { TabList } from "../../../../../../components/client/TabList"; import styles from "./SettingsContent.module.scss"; import { SettingsPanel } from "./panels"; +import { TabType } from "./View"; import { EmailOutlineIcon, MailboxOutlineIcon, ContactsOutlineIcon, } from "../../../../../../components/server/Icons"; -function SettingsContent() { +type SettingContentProps = { + activeTab: TabType; +}; + +type TabData = { + name: ReactNode; + key: TabType; +}; + +function SettingsContent(props: SettingContentProps) { const l10n = useL10n(); - const tabsData = [ + const pathname = usePathname(); + const tabsData: TabData[] = [ { name: ( <> @@ -35,7 +46,7 @@ function SettingsContent() { {l10n.getString("settings-tab-label-notifications")} ), - key: "label-notification", + key: "notifications", }, { name: ( @@ -47,9 +58,17 @@ function SettingsContent() { key: "manage-account", }, ]; - const [activeTab, setActiveTab] = useState<(typeof tabsData)[number]["key"]>( - tabsData[0].key, - ); + const [activeTab, setActiveTab] = useState(props.activeTab); + + useEffect(() => { + const nextPathname = `/user/settings/${activeTab}`; + if (pathname !== nextPathname) { + // Directly interacting with the history API is recommended by Next.js to + // avoid re-rendering on the server: + // See https://github.com/vercel/next.js/discussions/48110#discussioncomment-7563979. + window.history.replaceState(null, "", nextPathname); + } + }, [pathname, activeTab]); return (
diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/View.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/View.tsx index d6fe2622dfd..6778c1daa71 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/View.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/View.tsx @@ -25,6 +25,8 @@ import { ExperimentData } from "../../../../../../../telemetry/generated/nimbus/ import { SubscriberEmailPreferencesOutput } from "../../../../../../../db/tables/subscriber_email_preferences"; import { SettingsContent } from "./SettingsContent"; +export type TabType = "edit-info" | "notifications" | "manage-account"; + export type Props = { l10n: ExtendedReactLocalization; user: Session["user"]; @@ -44,6 +46,7 @@ export type Props = { experimentData: ExperimentData; lastScanDate?: Date; isMonthlySubscriber: boolean; + activeTab: TabType; }; export const SettingsView = (props: Props) => { @@ -60,7 +63,7 @@ export const SettingsView = (props: Props) => { experimentData={props.experimentData} /> {props.enabledFeatureFlags.includes("SettingsPageRedesign") ? ( - + ) : (
diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/page.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/[[...slug]]/page.tsx similarity index 62% rename from src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/page.tsx rename to src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/[[...slug]]/page.tsx index ee10fea52c4..cbd3bc6cfb8 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/page.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/[[...slug]]/page.tsx @@ -4,41 +4,62 @@ import { redirect } from "next/navigation"; import { headers } from "next/headers"; -import { getServerSession } from "../../../../../../functions/server/getServerSession"; -import { SettingsView } from "./View"; +import { getServerSession } from "../../../../../../../functions/server/getServerSession"; +import { SettingsView, TabType } from "../View"; import { getSubscriptionBillingAmount, getPremiumSubscriptionUrl, -} from "../../../../../../functions/server/getPremiumSubscriptionInfo"; -import { getL10n } from "../../../../../../functions/l10n/serverComponents"; -import { getUserEmails } from "../../../../../../../db/tables/emailAddresses"; -import { getBreaches } from "../../../../../../functions/server/getBreaches"; -import { getBreachesForEmail } from "../../../../../../../utils/hibp"; -import { getSha1 } from "../../../../../../../utils/fxa"; -import { getAttributionsFromCookiesOrDb } from "../../../../../../functions/server/attributions"; -import { getEnabledFeatureFlags } from "../../../../../../../db/tables/featureFlags"; -import { getLatestOnerepScan } from "../../../../../../../db/tables/onerep_scans"; -import { getExperimentationId } from "../../../../../../functions/server/getExperimentationId"; -import { getExperiments } from "../../../../../../functions/server/getExperiments"; -import { getLocale } from "../../../../../../functions/universal/getLocale"; -import { getCountryCode } from "../../../../../../functions/server/getCountryCode"; -import { getSubscriberById } from "../../../../../../../db/tables/subscribers"; -import { checkSession } from "../../../../../../functions/server/checkSession"; -import { checkUserHasMonthlySubscription } from "../../../../../../functions/server/user"; -import { getEmailPreferenceForPrimaryEmail } from "../../../../../../../db/tables/subscriber_email_preferences"; +} from "../../../../../../../functions/server/getPremiumSubscriptionInfo"; +import { getL10n } from "../../../../../../../functions/l10n/serverComponents"; +import { getUserEmails } from "../../../../../../../../db/tables/emailAddresses"; +import { getBreaches } from "../../../../../../../functions/server/getBreaches"; +import { getBreachesForEmail } from "../../../../../../../../utils/hibp"; +import { getSha1 } from "../../../../../../../../utils/fxa"; +import { getAttributionsFromCookiesOrDb } from "../../../../../../../functions/server/attributions"; +import { getEnabledFeatureFlags } from "../../../../../../../../db/tables/featureFlags"; +import { getLatestOnerepScan } from "../../../../../../../../db/tables/onerep_scans"; +import { getExperimentationId } from "../../../../../../../functions/server/getExperimentationId"; +import { getExperiments } from "../../../../../../../functions/server/getExperiments"; +import { getLocale } from "../../../../../../../functions/universal/getLocale"; +import { getCountryCode } from "../../../../../../../functions/server/getCountryCode"; +import { getSubscriberById } from "../../../../../../../../db/tables/subscribers"; +import { checkSession } from "../../../../../../../functions/server/checkSession"; +import { checkUserHasMonthlySubscription } from "../../../../../../../functions/server/user"; +import { getEmailPreferenceForPrimaryEmail } from "../../../../../../../../db/tables/subscriber_email_preferences"; + +export const settingsTabSlugs = [ + "edit-info", + "notifications", + "manage-account", +]; + type Props = { + params: { + slug: string[] | undefined; + }; searchParams: { nimbus_preview?: string; }; }; -export default async function SettingsPage({ searchParams }: Props) { +export default async function SettingsPage({ params, searchParams }: Props) { const session = await getServerSession(); if (!session?.user?.subscriber?.id || !checkSession(session)) { return redirect("/auth/logout"); } + const { slug } = params; + const defaultTab = settingsTabSlugs[0]; + const activeTab = slug?.[0] ?? defaultTab; + // Only allow the tab slugs. Otherwise: Redirect to the default settings route. + if ( + typeof slug !== "undefined" && + (!settingsTabSlugs.includes(activeTab) || slug.length >= 2) + ) { + return redirect(`/user/settings/${defaultTab}`); + } + const emailAddresses = await getUserEmails(session.user.subscriber.id); const isMonthlySubscriber = await checkUserHasMonthlySubscription( session.user, @@ -112,6 +133,7 @@ export default async function SettingsPage({ searchParams }: Props) { experimentData={experimentData} lastScanDate={lastOneRepScan?.created_at} isMonthlySubscriber={isMonthlySubscriber} + activeTab={activeTab as TabType} /> ); } diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/SettingsPanelEditInfo.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/SettingsPanelEditInfo.tsx index 647db27b240..e50dbddf6e7 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/SettingsPanelEditInfo.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/SettingsPanelEditInfo.tsx @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ function SettingsPanelEditInfo() { - return
Edit info
; + return
Edit info content
; } export { SettingsPanelEditInfo }; diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/SettingsPanelManageAccount.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/SettingsPanelManageAccount.tsx index a208a5178aa..5428aeebd75 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/SettingsPanelManageAccount.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/SettingsPanelManageAccount.tsx @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ function SettingsPanelManageAccount() { - return
Manage account
; + return
Manage account content
; } export { SettingsPanelManageAccount }; diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/SettingsPanelNotifications.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/SettingsPanelNotifications.tsx index 0253dc00a58..c0f74773134 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/SettingsPanelNotifications.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/SettingsPanelNotifications.tsx @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ function SettingsPanelNotifications() { - return
Notifications
; + return
Set notifications content
; } export { SettingsPanelNotifications }; diff --git a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/index.tsx b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/index.tsx index ee5d19c18cf..1c865036cec 100644 --- a/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/index.tsx +++ b/src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/settings/panels/index.tsx @@ -11,7 +11,7 @@ function Panel(props: { type: string }) { switch (props.type) { case "edit-info": return ; - case "label-notification": + case "notifications": return ; case "manage-account": return ;