From 1de174c7cf99aa20e0a7e65f880a13cc53a0900e Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Thu, 29 May 2025 17:28:30 +0200 Subject: [PATCH 01/26] Fix flaky AvatarButton loading issues with module federation --- .../back-office/WebApp/shared/components/topMenu/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/application/back-office/WebApp/shared/components/topMenu/index.tsx b/application/back-office/WebApp/shared/components/topMenu/index.tsx index 716c387a4..49b806866 100644 --- a/application/back-office/WebApp/shared/components/topMenu/index.tsx +++ b/application/back-office/WebApp/shared/components/topMenu/index.tsx @@ -6,7 +6,7 @@ import { Button } from "@repo/ui/components/Button"; import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; import { LifeBuoyIcon } from "lucide-react"; import type { ReactNode } from "react"; -import { lazy } from "react"; +import { Suspense, lazy } from "react"; const AvatarButton = lazy(() => import("account-management/AvatarButton")); @@ -31,7 +31,9 @@ export function TopMenu({ children }: Readonly) { - + }> + + ); From e2405c7b61897e55f9ca00400160dd1e9512d41e Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Tue, 22 Jul 2025 00:24:08 +0200 Subject: [PATCH 02/26] Hide hero image on mobile for login and signup pages --- .../WebApp/shared/components/HeroImage.tsx | 30 +++++-------------- .../shared/layouts/HorizontalHeroLayout.tsx | 6 ++-- .../shared/translations/locale/da-DK.po | 3 -- .../shared/translations/locale/en-US.po | 3 -- .../shared/translations/locale/nl-NL.po | 3 -- 5 files changed, 12 insertions(+), 33 deletions(-) diff --git a/application/account-management/WebApp/shared/components/HeroImage.tsx b/application/account-management/WebApp/shared/components/HeroImage.tsx index cc528242e..ff3728728 100644 --- a/application/account-management/WebApp/shared/components/HeroImage.tsx +++ b/application/account-management/WebApp/shared/components/HeroImage.tsx @@ -1,31 +1,17 @@ import heroDesktopBlurImage from "@/shared//images/hero-desktop-blur.webp"; import heroDesktopImage from "@/shared//images/hero-desktop-xl.webp"; -import heroMobileBlurImage from "@/shared/images/hero-mobile-blur.webp"; -import heroMobileImage from "@/shared/images/hero-mobile-xl.webp"; import { t } from "@lingui/core/macro"; import { Image } from "@repo/ui/components/Image"; export function HeroImage() { return ( - <> - {t`Screenshots - {t`Screenshots - + {t`Screenshots ); } diff --git a/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx b/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx index d3797f4aa..ddcfa902d 100644 --- a/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx +++ b/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx @@ -21,8 +21,10 @@ export function HorizontalHeroLayout({ children }: Readonly
-
{children}
-
+
+ {children} +
+
diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po index 7ece3029d..761770166 100644 --- a/application/account-management/WebApp/shared/translations/locale/da-DK.po +++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po @@ -326,9 +326,6 @@ msgstr "Gemmer..." msgid "Screenshots of the dashboard project with desktop and mobile versions" msgstr "Skærmbilleder af dashboard-projektet med desktop- og mobilversioner" -msgid "Screenshots of the dashboard project with mobile versions" -msgstr "Skærmbilleder af dashboard-projektet med mobilversioner" - msgid "Search" msgstr "Søg" diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po index c129df86d..aa6fafebb 100644 --- a/application/account-management/WebApp/shared/translations/locale/en-US.po +++ b/application/account-management/WebApp/shared/translations/locale/en-US.po @@ -326,9 +326,6 @@ msgstr "Saving..." msgid "Screenshots of the dashboard project with desktop and mobile versions" msgstr "Screenshots of the dashboard project with desktop and mobile versions" -msgid "Screenshots of the dashboard project with mobile versions" -msgstr "Screenshots of the dashboard project with mobile versions" - msgid "Search" msgstr "Search" diff --git a/application/account-management/WebApp/shared/translations/locale/nl-NL.po b/application/account-management/WebApp/shared/translations/locale/nl-NL.po index c88a20732..935bcb84f 100644 --- a/application/account-management/WebApp/shared/translations/locale/nl-NL.po +++ b/application/account-management/WebApp/shared/translations/locale/nl-NL.po @@ -326,9 +326,6 @@ msgstr "Opslaan..." msgid "Screenshots of the dashboard project with desktop and mobile versions" msgstr "Schermafbeeldingen van het dashboardproject met desktop en mobiele versies" -msgid "Screenshots of the dashboard project with mobile versions" -msgstr "Schermafbeeldingen van het dashboardproject met mobiele versies" - msgid "Search" msgstr "Zoeken" From 66dce26208ce633d769f5c6ea74227d9733731a7 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Tue, 22 Jul 2025 00:37:51 +0200 Subject: [PATCH 03/26] Show theme, support, and language switcher on mobile screens during login and signup --- .../WebApp/shared/layouts/HorizontalHeroLayout.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx b/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx index ddcfa902d..2719dcd32 100644 --- a/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx +++ b/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx @@ -23,6 +23,14 @@ export function HorizontalHeroLayout({ children }: Readonly
{children} + {/* Mobile-only icon controls at bottom of form */} +
+ + + +
From 947103aff4f51d0b86ccbb739b6067cb9981dfac Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Tue, 22 Jul 2025 11:52:16 +0200 Subject: [PATCH 04/26] Centralize tooltip for LocaleSwitcher --- .../WebApp/shared/components/topMenu/index.tsx | 5 +---- .../shared/layouts/HorizontalHeroLayout.tsx | 4 ++-- .../WebApp/shared/translations/locale/da-DK.po | 3 --- .../WebApp/shared/translations/locale/en-US.po | 3 --- .../WebApp/shared/translations/locale/nl-NL.po | 3 --- .../WebApp/shared/components/topMenu/index.tsx | 2 +- .../WebApp/shared/translations/locale/da-DK.po | 6 +++--- .../WebApp/shared/translations/locale/en-US.po | 6 +++--- .../WebApp/shared/translations/locale/nl-NL.po | 6 +++--- .../translations/LocaleSwitcher.tsx | 16 ++++++++++++++-- 10 files changed, 27 insertions(+), 27 deletions(-) diff --git a/application/account-management/WebApp/shared/components/topMenu/index.tsx b/application/account-management/WebApp/shared/components/topMenu/index.tsx index c3e90feaa..4f476aa41 100644 --- a/application/account-management/WebApp/shared/components/topMenu/index.tsx +++ b/application/account-management/WebApp/shared/components/topMenu/index.tsx @@ -36,10 +36,7 @@ export function TopMenu({ children, sidePaneOpen = false }: Readonly {t`Support`} - - - {t`Change language`} - +
diff --git a/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx b/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx index 2719dcd32..4d41c5427 100644 --- a/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx +++ b/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx @@ -18,7 +18,7 @@ export function HorizontalHeroLayout({ children }: Readonly - +
@@ -29,7 +29,7 @@ export function HorizontalHeroLayout({ children }: Readonly - +
diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po index 761770166..428e84b1c 100644 --- a/application/account-management/WebApp/shared/translations/locale/da-DK.po +++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po @@ -336,9 +336,6 @@ msgstr "Vælg en ny rolle for <0>{0}" msgid "Select dates" msgstr "Vælg datoer" -msgid "Select language" -msgstr "Vælg sprog" - msgid "Send invite" msgstr "Send invitation" diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po index aa6fafebb..808c9af56 100644 --- a/application/account-management/WebApp/shared/translations/locale/en-US.po +++ b/application/account-management/WebApp/shared/translations/locale/en-US.po @@ -336,9 +336,6 @@ msgstr "Select a new role for <0>{0}" msgid "Select dates" msgstr "Select dates" -msgid "Select language" -msgstr "Select language" - msgid "Send invite" msgstr "Send invite" diff --git a/application/account-management/WebApp/shared/translations/locale/nl-NL.po b/application/account-management/WebApp/shared/translations/locale/nl-NL.po index 935bcb84f..b4ad6ea85 100644 --- a/application/account-management/WebApp/shared/translations/locale/nl-NL.po +++ b/application/account-management/WebApp/shared/translations/locale/nl-NL.po @@ -336,9 +336,6 @@ msgstr "Selecteer een nieuwe rol voor <0>{0}" msgid "Select dates" msgstr "Selecteer datums" -msgid "Select language" -msgstr "Selecteer taal" - msgid "Send invite" msgstr "Uitnodiging verzenden" diff --git a/application/back-office/WebApp/shared/components/topMenu/index.tsx b/application/back-office/WebApp/shared/components/topMenu/index.tsx index 49b806866..9b00c97ae 100644 --- a/application/back-office/WebApp/shared/components/topMenu/index.tsx +++ b/application/back-office/WebApp/shared/components/topMenu/index.tsx @@ -29,7 +29,7 @@ export function TopMenu({ children }: Readonly) { - + }> diff --git a/application/back-office/WebApp/shared/translations/locale/da-DK.po b/application/back-office/WebApp/shared/translations/locale/da-DK.po index 3665bf5c4..8936db2f2 100644 --- a/application/back-office/WebApp/shared/translations/locale/da-DK.po +++ b/application/back-office/WebApp/shared/translations/locale/da-DK.po @@ -16,6 +16,9 @@ msgstr "" msgid "Account management" msgstr "Kontoadministration" +msgid "Change language" +msgstr "Skift sprog" + msgid "Help" msgstr "Hjælp" @@ -25,9 +28,6 @@ msgstr "Hjem" msgid "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams." msgstr "Administrer lejere, se systemdata, se undtagelser og udfør forskellige opgaver for drifts- og supportteams." -msgid "Select language" -msgstr "Vælg sprog" - msgid "Toggle collapsed menu" msgstr "Skift kollapset menu" diff --git a/application/back-office/WebApp/shared/translations/locale/en-US.po b/application/back-office/WebApp/shared/translations/locale/en-US.po index 46e456001..85491ac5d 100644 --- a/application/back-office/WebApp/shared/translations/locale/en-US.po +++ b/application/back-office/WebApp/shared/translations/locale/en-US.po @@ -16,6 +16,9 @@ msgstr "" msgid "Account management" msgstr "Account management" +msgid "Change language" +msgstr "Change language" + msgid "Help" msgstr "Help" @@ -25,9 +28,6 @@ msgstr "Home" msgid "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams." msgstr "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams." -msgid "Select language" -msgstr "Select language" - msgid "Toggle collapsed menu" msgstr "Toggle collapsed menu" diff --git a/application/back-office/WebApp/shared/translations/locale/nl-NL.po b/application/back-office/WebApp/shared/translations/locale/nl-NL.po index 733610398..4aa301e50 100644 --- a/application/back-office/WebApp/shared/translations/locale/nl-NL.po +++ b/application/back-office/WebApp/shared/translations/locale/nl-NL.po @@ -16,6 +16,9 @@ msgstr "" msgid "Account management" msgstr "Accountbeheer" +msgid "Change language" +msgstr "Taal wijzigen" + msgid "Help" msgstr "Help" @@ -25,9 +28,6 @@ msgstr "Home" msgid "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams." msgstr "Beheer huurders, bekijk systeemgegevens, zie uitzonderingen en voer diverse taken uit voor operationele en ondersteuningsteams." -msgid "Select language" -msgstr "Selecteer taal" - msgid "Toggle collapsed menu" msgstr "Ingeklapt menu wisselen" diff --git a/application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx b/application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx index e13e354a7..6924bb274 100644 --- a/application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx +++ b/application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx @@ -4,12 +4,13 @@ import { AuthenticationContext } from "@repo/infrastructure/auth/AuthenticationP import { enhancedFetch } from "@repo/infrastructure/http/httpClient"; import { Button } from "@repo/ui/components/Button"; import { Menu, MenuItem, MenuTrigger } from "@repo/ui/components/Menu"; +import { Tooltip, TooltipTrigger } from "@repo/ui/components/Tooltip"; import { CheckIcon, GlobeIcon } from "lucide-react"; import { use, useContext, useMemo } from "react"; import { type Locale, translationContext } from "./TranslationContext"; import { preferredLocaleKey } from "./constants"; -export function LocaleSwitcher({ "aria-label": ariaLabel }: { "aria-label": string }) { +export function LocaleSwitcher({ "aria-label": ariaLabel, tooltip }: { "aria-label": string; tooltip?: string }) { const { setLocale, getLocaleInfo, locales } = use(translationContext); const { i18n } = useLingui(); const { userInfo } = useContext(AuthenticationContext); @@ -45,7 +46,7 @@ export function LocaleSwitcher({ "aria-label": ariaLabel }: { "aria-label": stri const currentLocale = i18n.locale as Locale; - return ( + const menuContent = (
- {/* Theme Section - button that cycles themes */} + {/* Theme Section - using ThemeModeSelector with mobile menu variant */}
- + />
{/* Language Section - styled like menu item */} diff --git a/application/account-management/WebApp/shared/components/topMenu/index.tsx b/application/account-management/WebApp/shared/components/topMenu/index.tsx index 4f476aa41..d622f3d08 100644 --- a/application/account-management/WebApp/shared/components/topMenu/index.tsx +++ b/application/account-management/WebApp/shared/components/topMenu/index.tsx @@ -29,7 +29,7 @@ export function TopMenu({ children, sidePaneOpen = false }: Readonly - + @@ -25,7 +25,7 @@ export function HorizontalHeroLayout({ children }: Readonly - + diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po index 428e84b1c..66c2da0e7 100644 --- a/application/account-management/WebApp/shared/translations/locale/da-DK.po +++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po @@ -99,6 +99,9 @@ msgstr "Skift profilbillede" msgid "Change role" msgstr "Skift rolle" +msgid "Change theme" +msgstr "Skift tema" + msgid "Change user role" msgstr "Skift brugerrolle" @@ -130,9 +133,6 @@ msgstr "Oprettet" msgid "Danger zone" msgstr "Farezone" -msgid "Dark" -msgstr "Mørk" - msgid "Delete" msgstr "Slet" @@ -224,9 +224,6 @@ msgstr "Sprog" msgid "Last name" msgstr "Efternavn" -msgid "Light" -msgstr "Lys" - msgid "Log in" msgstr "Log ind" @@ -360,15 +357,9 @@ msgstr "Succes" msgid "Support" msgstr "Support" -msgid "System" -msgstr "System" - msgid "Terms of use" msgstr "Brugsvilkår" -msgid "Theme" -msgstr "Tema" - msgid "This is the region where your data is stored" msgstr "Dette er den region, hvor dine data er lagret" diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po index 808c9af56..a48087f35 100644 --- a/application/account-management/WebApp/shared/translations/locale/en-US.po +++ b/application/account-management/WebApp/shared/translations/locale/en-US.po @@ -99,6 +99,9 @@ msgstr "Change profile picture" msgid "Change role" msgstr "Change role" +msgid "Change theme" +msgstr "Change theme" + msgid "Change user role" msgstr "Change user role" @@ -130,9 +133,6 @@ msgstr "Created" msgid "Danger zone" msgstr "Danger zone" -msgid "Dark" -msgstr "Dark" - msgid "Delete" msgstr "Delete" @@ -224,9 +224,6 @@ msgstr "Language" msgid "Last name" msgstr "Last name" -msgid "Light" -msgstr "Light" - msgid "Log in" msgstr "Log in" @@ -360,15 +357,9 @@ msgstr "Success" msgid "Support" msgstr "Support" -msgid "System" -msgstr "System" - msgid "Terms of use" msgstr "Terms of use" -msgid "Theme" -msgstr "Theme" - msgid "This is the region where your data is stored" msgstr "This is the region where your data is stored" diff --git a/application/account-management/WebApp/shared/translations/locale/nl-NL.po b/application/account-management/WebApp/shared/translations/locale/nl-NL.po index b4ad6ea85..707f16f5a 100644 --- a/application/account-management/WebApp/shared/translations/locale/nl-NL.po +++ b/application/account-management/WebApp/shared/translations/locale/nl-NL.po @@ -99,6 +99,9 @@ msgstr "Profielfoto wijzigen" msgid "Change role" msgstr "Rol wijzigen" +msgid "Change theme" +msgstr "Thema wijzigen" + msgid "Change user role" msgstr "Gebruikersrol wijzigen" @@ -130,9 +133,6 @@ msgstr "Aangemaakt" msgid "Danger zone" msgstr "Gevaarzone" -msgid "Dark" -msgstr "Donker" - msgid "Delete" msgstr "Verwijderen" @@ -224,9 +224,6 @@ msgstr "Taal" msgid "Last name" msgstr "Achternaam" -msgid "Light" -msgstr "Licht" - msgid "Log in" msgstr "Inloggen" @@ -360,15 +357,9 @@ msgstr "Succes" msgid "Support" msgstr "Ondersteuning" -msgid "System" -msgstr "Systeem" - msgid "Terms of use" msgstr "Gebruiksvoorwaarden" -msgid "Theme" -msgstr "Thema" - msgid "This is the region where your data is stored" msgstr "Dit is de regio waar je gegevens zijn opgeslagen" diff --git a/application/back-office/WebApp/shared/components/topMenu/index.tsx b/application/back-office/WebApp/shared/components/topMenu/index.tsx index 9b00c97ae..5cbf6c364 100644 --- a/application/back-office/WebApp/shared/components/topMenu/index.tsx +++ b/application/back-office/WebApp/shared/components/topMenu/index.tsx @@ -25,7 +25,7 @@ export function TopMenu({ children }: Readonly) {
- + diff --git a/application/back-office/WebApp/shared/translations/locale/da-DK.po b/application/back-office/WebApp/shared/translations/locale/da-DK.po index 8936db2f2..fb1cdcf1a 100644 --- a/application/back-office/WebApp/shared/translations/locale/da-DK.po +++ b/application/back-office/WebApp/shared/translations/locale/da-DK.po @@ -19,6 +19,9 @@ msgstr "Kontoadministration" msgid "Change language" msgstr "Skift sprog" +msgid "Change theme" +msgstr "Skift tema" + msgid "Help" msgstr "Hjælp" @@ -31,9 +34,6 @@ msgstr "Administrer lejere, se systemdata, se undtagelser og udfør forskellige msgid "Toggle collapsed menu" msgstr "Skift kollapset menu" -msgid "Toggle theme" -msgstr "Skift tema" - msgid "User profile menu" msgstr "Brugerprofilmenu" diff --git a/application/back-office/WebApp/shared/translations/locale/en-US.po b/application/back-office/WebApp/shared/translations/locale/en-US.po index 85491ac5d..c75c0597b 100644 --- a/application/back-office/WebApp/shared/translations/locale/en-US.po +++ b/application/back-office/WebApp/shared/translations/locale/en-US.po @@ -19,6 +19,9 @@ msgstr "Account management" msgid "Change language" msgstr "Change language" +msgid "Change theme" +msgstr "Change theme" + msgid "Help" msgstr "Help" @@ -31,9 +34,6 @@ msgstr "Manage tenants, view system data, see exceptions, and perform various ta msgid "Toggle collapsed menu" msgstr "Toggle collapsed menu" -msgid "Toggle theme" -msgstr "Toggle theme" - msgid "User profile menu" msgstr "User profile menu" diff --git a/application/back-office/WebApp/shared/translations/locale/nl-NL.po b/application/back-office/WebApp/shared/translations/locale/nl-NL.po index 4aa301e50..b73e905e5 100644 --- a/application/back-office/WebApp/shared/translations/locale/nl-NL.po +++ b/application/back-office/WebApp/shared/translations/locale/nl-NL.po @@ -19,6 +19,9 @@ msgstr "Accountbeheer" msgid "Change language" msgstr "Taal wijzigen" +msgid "Change theme" +msgstr "Thema wijzigen" + msgid "Help" msgstr "Help" @@ -31,9 +34,6 @@ msgstr "Beheer huurders, bekijk systeemgegevens, zie uitzonderingen en voer dive msgid "Toggle collapsed menu" msgstr "Ingeklapt menu wisselen" -msgid "Toggle theme" -msgstr "Schakel thema om" - msgid "User profile menu" msgstr "Gebruikersprofielmenu" diff --git a/application/shared-webapp/ui/theme/ThemeModeSelector.tsx b/application/shared-webapp/ui/theme/ThemeModeSelector.tsx index 2d9fd609d..335976394 100644 --- a/application/shared-webapp/ui/theme/ThemeModeSelector.tsx +++ b/application/shared-webapp/ui/theme/ThemeModeSelector.tsx @@ -1,37 +1,120 @@ +import type { Key } from "@react-types/shared"; import { Button } from "@repo/ui/components/Button"; +import { Menu, MenuItem, MenuTrigger } from "@repo/ui/components/Menu"; import { Tooltip, TooltipTrigger } from "@repo/ui/components/Tooltip"; -import { MoonIcon, MoonStarIcon, SunIcon, SunMoonIcon } from "lucide-react"; -import { toggleThemeMode, useThemeMode } from "./mode/ThemeMode"; +import { CheckIcon, MoonIcon, MoonStarIcon, SunIcon, SunMoonIcon } from "lucide-react"; +import { useThemeMode } from "./mode/ThemeMode"; +import { useSystemThemeMode } from "./mode/useSystemThemeMode"; import { SystemThemeMode, ThemeMode } from "./mode/utils"; /** - * A button that toggles the theme mode between system, light and dark. + * A button that opens a menu to select theme mode between system, light and dark. */ -export function ThemeModeSelector({ "aria-label": ariaLabel }: { "aria-label": string }) { - const { themeMode, resolvedThemeMode, setThemeMode } = useThemeMode(); - - const tooltipText = getTooltipText(themeMode, resolvedThemeMode); - - return ( - - - {tooltipText} - +export function ThemeModeSelector({ + "aria-label": ariaLabel, + tooltip, + variant = "icon", + onAction +}: { + readonly "aria-label": string; + readonly tooltip?: string; + readonly variant?: "icon" | "mobile-menu"; + readonly onAction?: () => void; +}) { + const { themeMode, setThemeMode } = useThemeMode(); + + const handleThemeChange = (key: Key) => { + setThemeMode(key as ThemeMode); + onAction?.(); + }; + + const getThemeName = (mode: ThemeMode) => { + switch (mode) { + case ThemeMode.System: + return "System"; + case ThemeMode.Light: + return "Light"; + case ThemeMode.Dark: + return "Dark"; + default: + return "System"; + } + }; + + const menuContent = ( + + {variant === "icon" ? ( + + ) : ( + + )} + + +
+ + System + {themeMode === ThemeMode.System && } +
+
+ +
+ + Light + {themeMode === ThemeMode.Light && } +
+
+ +
+ + Dark + {themeMode === ThemeMode.Dark && } +
+
+
+
); -} -function getTooltipText(themeMode: ThemeMode, resolvedThemeMode: SystemThemeMode): string { - if (resolvedThemeMode === SystemThemeMode.Dark) { - return themeMode === ThemeMode.System ? "System mode (dark)" : "Dark mode"; + if (tooltip) { + return ( + + {menuContent} + {tooltip} + + ); } - return themeMode === ThemeMode.System ? "System mode (light)" : "Light mode"; + + return menuContent; } -function ThemeModeIcon({ themeMode, resolvedThemeMode }: { themeMode: ThemeMode; resolvedThemeMode: SystemThemeMode }) { - if (resolvedThemeMode === SystemThemeMode.Dark) { - return themeMode === ThemeMode.System ? : ; +// Component that always shows the system theme icon +function SystemThemeIcon({ className }: { className: string }) { + const systemTheme = useSystemThemeMode(); + + return systemTheme === SystemThemeMode.Dark ? ( + + ) : ( + + ); +} + +function ThemeModeIcon({ themeMode }: Readonly<{ themeMode: ThemeMode }>) { + // For System mode, show special icons based on resolved theme + if (themeMode === ThemeMode.System) { + return ; } - return themeMode === ThemeMode.System ? : ; + + // For explicit Light/Dark modes, show icons based on the selected mode + return themeMode === ThemeMode.Dark ? : ; } From d3f10d7b5f782636a9658912f0b490f46deadb01 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Tue, 22 Jul 2025 13:26:10 +0200 Subject: [PATCH 06/26] Create sharable support dialog using module federation and change support icon to contact support mail icon --- .../WebApp/rsbuild.config.ts | 3 +- .../components/support/SupportButton.tsx | 19 ++++++++ .../components/support/SupportDialog.tsx | 44 +++++++++++++++++++ .../shared/components/topMenu/index.tsx | 11 +---- .../shared/layouts/HorizontalHeroLayout.tsx | 11 ++--- .../shared/translations/locale/da-DK.po | 18 +++++--- .../shared/translations/locale/en-US.po | 18 +++++--- .../shared/translations/locale/nl-NL.po | 18 +++++--- .../shared/components/topMenu/index.tsx | 8 ++-- .../shared/translations/locale/da-DK.po | 4 +- .../shared/translations/locale/en-US.po | 4 +- .../shared/translations/locale/nl-NL.po | 4 +- .../account-management.d.ts | 3 ++ 13 files changed, 119 insertions(+), 46 deletions(-) create mode 100644 application/account-management/WebApp/shared/components/support/SupportButton.tsx create mode 100644 application/account-management/WebApp/shared/components/support/SupportDialog.tsx diff --git a/application/account-management/WebApp/rsbuild.config.ts b/application/account-management/WebApp/rsbuild.config.ts index d0184b235..bf227f43e 100644 --- a/application/account-management/WebApp/rsbuild.config.ts +++ b/application/account-management/WebApp/rsbuild.config.ts @@ -21,7 +21,8 @@ export default defineConfig({ DevelopmentServerPlugin({ port: 9101 }), ModuleFederationPlugin({ exposes: { - "./AvatarButton": "./shared/components/AvatarButton.tsx" + "./AvatarButton": "./shared/components/AvatarButton.tsx", + "./SupportButton": "./shared/components/support/SupportButton.tsx" } }) ] diff --git a/application/account-management/WebApp/shared/components/support/SupportButton.tsx b/application/account-management/WebApp/shared/components/support/SupportButton.tsx new file mode 100644 index 000000000..aa7cb1739 --- /dev/null +++ b/application/account-management/WebApp/shared/components/support/SupportButton.tsx @@ -0,0 +1,19 @@ +import { t } from "@lingui/core/macro"; +import { Button } from "@repo/ui/components/Button"; +import { Tooltip, TooltipTrigger } from "@repo/ui/components/Tooltip"; +import { MailQuestion } from "lucide-react"; +import { SupportDialog } from "./SupportDialog"; +import "@repo/ui/tailwind.css"; + +export default function SupportButton({ "aria-label": ariaLabel }: Readonly<{ "aria-label": string }>) { + return ( + + + + {t`Contact support`} + + + ); +} diff --git a/application/account-management/WebApp/shared/components/support/SupportDialog.tsx b/application/account-management/WebApp/shared/components/support/SupportDialog.tsx new file mode 100644 index 000000000..887900815 --- /dev/null +++ b/application/account-management/WebApp/shared/components/support/SupportDialog.tsx @@ -0,0 +1,44 @@ +import { t } from "@lingui/core/macro"; +import { Button } from "@repo/ui/components/Button"; +import { Dialog, DialogTrigger } from "@repo/ui/components/Dialog"; +import { Heading } from "@repo/ui/components/Heading"; +import { Modal } from "@repo/ui/components/Modal"; +import { MailIcon, XIcon } from "lucide-react"; +import type { ReactNode } from "react"; + +interface SupportDialogProps { + children: ReactNode; +} + +export function SupportDialog({ children }: Readonly) { + return ( + + {children} + + + {({ close }) => ( + <> + + + {t`Contact support`} + +

{t`Need help? Our support team is here to assist you.`}

+
+ +

{t`Feel free to reach out with any questions or issues you may have.`}

+
+ +
+
+ + )} +
+
+
+ ); +} diff --git a/application/account-management/WebApp/shared/components/topMenu/index.tsx b/application/account-management/WebApp/shared/components/topMenu/index.tsx index d622f3d08..dff219d2a 100644 --- a/application/account-management/WebApp/shared/components/topMenu/index.tsx +++ b/application/account-management/WebApp/shared/components/topMenu/index.tsx @@ -2,12 +2,10 @@ import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { LocaleSwitcher } from "@repo/infrastructure/translations/LocaleSwitcher"; import { Breadcrumb, Breadcrumbs } from "@repo/ui/components/Breadcrumbs"; -import { Button } from "@repo/ui/components/Button"; -import { Tooltip, TooltipTrigger } from "@repo/ui/components/Tooltip"; import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; -import { LifeBuoyIcon } from "lucide-react"; import type { ReactNode } from "react"; import AvatarButton from "../AvatarButton"; +import SupportButton from "../support/SupportButton"; interface TopMenuProps { children?: ReactNode; @@ -30,12 +28,7 @@ export function TopMenu({ children, sidePaneOpen = false }: Readonly - - - {t`Support`} - + diff --git a/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx b/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx index 13f523a76..3ecce387c 100644 --- a/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx +++ b/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx @@ -1,9 +1,8 @@ import { HeroImage } from "@/shared/components/HeroImage"; +import SupportButton from "@/shared/components/support/SupportButton"; import { t } from "@lingui/core/macro"; import { LocaleSwitcher } from "@repo/infrastructure/translations/LocaleSwitcher"; -import { Button } from "@repo/ui/components/Button"; import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; -import { LifeBuoyIcon } from "lucide-react"; import type { ReactNode } from "react"; interface HorizontalHeroLayoutProps { @@ -15,9 +14,7 @@ export function HorizontalHeroLayout({ children }: Readonly
- +
@@ -26,9 +23,7 @@ export function HorizontalHeroLayout({ children }: Readonly - +
diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po index 66c2da0e7..ccbf43a52 100644 --- a/application/account-management/WebApp/shared/translations/locale/da-DK.po +++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po @@ -118,9 +118,15 @@ msgstr "Ryd" msgid "Clear filters" msgstr "Ryd filtre" +msgid "Close" +msgstr "Luk" + msgid "Close user profile" msgstr "Luk brugerprofil" +msgid "Contact support" +msgstr "Kontakt support" + msgid "Continue" msgstr "Fortsæt" @@ -188,6 +194,9 @@ msgstr "Fejl: Noget gik galt!" msgid "Europe" msgstr "Europa" +msgid "Feel free to reach out with any questions or issues you may have." +msgstr "Du er velkommen til at kontakte os med eventuelle spørgsmål eller problemer, du måtte have." + msgid "Fetching data..." msgstr "Henter data..." @@ -197,9 +206,6 @@ msgstr "Filtre" msgid "First name" msgstr "Fornavn" -msgid "Help" -msgstr "Hjælp" - msgid "Here's your overview of what's happening." msgstr "Her er din oversigt over, hvad der sker." @@ -257,6 +263,9 @@ msgstr "Navn" msgid "Navigation" msgstr "Navigation" +msgid "Need help? Our support team is here to assist you." +msgstr "Har du brug for hjælp? Vores supportteam er klart til at hjælpe." + msgid "Next" msgstr "Næste" @@ -354,9 +363,6 @@ msgstr "Tilmeldingsbekræftelseskode" msgid "Success" msgstr "Succes" -msgid "Support" -msgstr "Support" - msgid "Terms of use" msgstr "Brugsvilkår" diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po index a48087f35..f03511cc8 100644 --- a/application/account-management/WebApp/shared/translations/locale/en-US.po +++ b/application/account-management/WebApp/shared/translations/locale/en-US.po @@ -118,9 +118,15 @@ msgstr "Clear" msgid "Clear filters" msgstr "Clear filters" +msgid "Close" +msgstr "Close" + msgid "Close user profile" msgstr "Close user profile" +msgid "Contact support" +msgstr "Contact support" + msgid "Continue" msgstr "Continue" @@ -188,6 +194,9 @@ msgstr "Error: Something went wrong!" msgid "Europe" msgstr "Europe" +msgid "Feel free to reach out with any questions or issues you may have." +msgstr "Feel free to reach out with any questions or issues you may have." + msgid "Fetching data..." msgstr "Fetching data..." @@ -197,9 +206,6 @@ msgstr "Filters" msgid "First name" msgstr "First name" -msgid "Help" -msgstr "Help" - msgid "Here's your overview of what's happening." msgstr "Here's your overview of what's happening." @@ -257,6 +263,9 @@ msgstr "Name" msgid "Navigation" msgstr "Navigation" +msgid "Need help? Our support team is here to assist you." +msgstr "Need help? Our support team is here to assist you." + msgid "Next" msgstr "Next" @@ -354,9 +363,6 @@ msgstr "Signup verification code" msgid "Success" msgstr "Success" -msgid "Support" -msgstr "Support" - msgid "Terms of use" msgstr "Terms of use" diff --git a/application/account-management/WebApp/shared/translations/locale/nl-NL.po b/application/account-management/WebApp/shared/translations/locale/nl-NL.po index 707f16f5a..9a733c6ae 100644 --- a/application/account-management/WebApp/shared/translations/locale/nl-NL.po +++ b/application/account-management/WebApp/shared/translations/locale/nl-NL.po @@ -118,9 +118,15 @@ msgstr "Wissen" msgid "Clear filters" msgstr "Filters wissen" +msgid "Close" +msgstr "Sluiten" + msgid "Close user profile" msgstr "Gebruikersprofiel sluiten" +msgid "Contact support" +msgstr "Ondersteuning contacteren" + msgid "Continue" msgstr "Verder" @@ -188,6 +194,9 @@ msgstr "Fout: Er is iets misgegaan!" msgid "Europe" msgstr "Europa" +msgid "Feel free to reach out with any questions or issues you may have." +msgstr "Voel je vrij om contact op te nemen met vragen of problemen die je hebt." + msgid "Fetching data..." msgstr "Gegevens ophalen..." @@ -197,9 +206,6 @@ msgstr "Filters" msgid "First name" msgstr "Voornaam" -msgid "Help" -msgstr "Help" - msgid "Here's your overview of what's happening." msgstr "Hier is je overzicht van wat er gebeurt." @@ -257,6 +263,9 @@ msgstr "Naam" msgid "Navigation" msgstr "Navigatie" +msgid "Need help? Our support team is here to assist you." +msgstr "Hulp nodig? Ons ondersteuningsteam staat voor je klaar." + msgid "Next" msgstr "Volgende" @@ -354,9 +363,6 @@ msgstr "Verificatiecode voor aanmelding" msgid "Success" msgstr "Succes" -msgid "Support" -msgstr "Ondersteuning" - msgid "Terms of use" msgstr "Gebruiksvoorwaarden" diff --git a/application/back-office/WebApp/shared/components/topMenu/index.tsx b/application/back-office/WebApp/shared/components/topMenu/index.tsx index 5cbf6c364..97692589b 100644 --- a/application/back-office/WebApp/shared/components/topMenu/index.tsx +++ b/application/back-office/WebApp/shared/components/topMenu/index.tsx @@ -4,11 +4,11 @@ import { LocaleSwitcher } from "@repo/infrastructure/translations/LocaleSwitcher import { Breadcrumb, Breadcrumbs } from "@repo/ui/components/Breadcrumbs"; import { Button } from "@repo/ui/components/Button"; import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; -import { LifeBuoyIcon } from "lucide-react"; import type { ReactNode } from "react"; import { Suspense, lazy } from "react"; const AvatarButton = lazy(() => import("account-management/AvatarButton")); +const SupportButton = lazy(() => import("account-management/SupportButton")); interface TopMenuProps { children?: ReactNode; @@ -26,9 +26,9 @@ export function TopMenu({ children }: Readonly) {
- + }> + + }> diff --git a/application/back-office/WebApp/shared/translations/locale/da-DK.po b/application/back-office/WebApp/shared/translations/locale/da-DK.po index fb1cdcf1a..1b76a413b 100644 --- a/application/back-office/WebApp/shared/translations/locale/da-DK.po +++ b/application/back-office/WebApp/shared/translations/locale/da-DK.po @@ -22,8 +22,8 @@ msgstr "Skift sprog" msgid "Change theme" msgstr "Skift tema" -msgid "Help" -msgstr "Hjælp" +msgid "Contact support" +msgstr "Kontakt support" msgid "Home" msgstr "Hjem" diff --git a/application/back-office/WebApp/shared/translations/locale/en-US.po b/application/back-office/WebApp/shared/translations/locale/en-US.po index c75c0597b..1068dcede 100644 --- a/application/back-office/WebApp/shared/translations/locale/en-US.po +++ b/application/back-office/WebApp/shared/translations/locale/en-US.po @@ -22,8 +22,8 @@ msgstr "Change language" msgid "Change theme" msgstr "Change theme" -msgid "Help" -msgstr "Help" +msgid "Contact support" +msgstr "Contact support" msgid "Home" msgstr "Home" diff --git a/application/back-office/WebApp/shared/translations/locale/nl-NL.po b/application/back-office/WebApp/shared/translations/locale/nl-NL.po index b73e905e5..b7c463a2c 100644 --- a/application/back-office/WebApp/shared/translations/locale/nl-NL.po +++ b/application/back-office/WebApp/shared/translations/locale/nl-NL.po @@ -22,8 +22,8 @@ msgstr "Taal wijzigen" msgid "Change theme" msgstr "Thema wijzigen" -msgid "Help" -msgstr "Help" +msgid "Contact support" +msgstr "Ondersteuning contacteren" msgid "Home" msgstr "Home" diff --git a/application/shared-webapp/build/module-federation-types/account-management.d.ts b/application/shared-webapp/build/module-federation-types/account-management.d.ts index 137a5fe17..d33f38b62 100644 --- a/application/shared-webapp/build/module-federation-types/account-management.d.ts +++ b/application/shared-webapp/build/module-federation-types/account-management.d.ts @@ -3,3 +3,6 @@ declare module "account-management/AvatarButton" { export default ReactNode; } +declare module "account-management/SupportButton" { + export default ReactNode; +} From a41e535517ea9ad6e7bfa3cb304ba20d1aeb665f Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Tue, 22 Jul 2025 13:52:19 +0200 Subject: [PATCH 07/26] Add contact support option to the mobile menu --- .../shared/components/SharedSideMenu.tsx | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/application/account-management/WebApp/shared/components/SharedSideMenu.tsx b/application/account-management/WebApp/shared/components/SharedSideMenu.tsx index 01e584341..cbbc7062e 100644 --- a/application/account-management/WebApp/shared/components/SharedSideMenu.tsx +++ b/application/account-management/WebApp/shared/components/SharedSideMenu.tsx @@ -12,9 +12,19 @@ import { Menu, MenuItem, MenuTrigger } from "@repo/ui/components/Menu"; import { MenuButton, SideMenu, SideMenuSeparator, overlayContext } from "@repo/ui/components/SideMenu"; import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; import { useQueryClient } from "@tanstack/react-query"; -import { CheckIcon, CircleUserIcon, GlobeIcon, HomeIcon, LogOutIcon, UserIcon, UsersIcon } from "lucide-react"; +import { + CheckIcon, + CircleUserIcon, + GlobeIcon, + HomeIcon, + LogOutIcon, + MailQuestion, + UserIcon, + UsersIcon +} from "lucide-react"; import type React from "react"; import { use, useContext, useState } from "react"; +import { SupportDialog } from "./support/SupportDialog"; import UserProfileModal from "./userModals/UserProfileModal"; type SharedSideMenuProps = { @@ -160,6 +170,24 @@ export function SharedSideMenu({ children, ariaLabel }: Readonly
+ + {/* Support Section - styled like menu item */} +
+ + + +
{/* Divider */} From b0a105fb23687bd924bfb0c94be78cb363d5ac1c Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Tue, 22 Jul 2025 16:51:47 +0200 Subject: [PATCH 08/26] Make translations in module federation available to all self-contained systems --- .../WebApp/rsbuild.config.ts | 5 +- application/back-office/WebApp/bootstrap.tsx | 4 +- application/biome.json | 2 +- .../account-management.d.ts | 12 +++ .../build/plugin/ModuleFederationPlugin.ts | 11 ++- .../createFederatedTranslation.ts | 97 +++++++++++++++++++ 6 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 application/shared-webapp/infrastructure/translations/createFederatedTranslation.ts diff --git a/application/account-management/WebApp/rsbuild.config.ts b/application/account-management/WebApp/rsbuild.config.ts index bf227f43e..a57209dfe 100644 --- a/application/account-management/WebApp/rsbuild.config.ts +++ b/application/account-management/WebApp/rsbuild.config.ts @@ -22,7 +22,10 @@ export default defineConfig({ ModuleFederationPlugin({ exposes: { "./AvatarButton": "./shared/components/AvatarButton.tsx", - "./SupportButton": "./shared/components/support/SupportButton.tsx" + "./SupportButton": "./shared/components/support/SupportButton.tsx", + "./translations/en-US": "./shared/translations/locale/en-US.ts", + "./translations/da-DK": "./shared/translations/locale/da-DK.ts", + "./translations/nl-NL": "./shared/translations/locale/nl-NL.ts" } }) ] diff --git a/application/back-office/WebApp/bootstrap.tsx b/application/back-office/WebApp/bootstrap.tsx index ada41486d..9a26128c9 100644 --- a/application/back-office/WebApp/bootstrap.tsx +++ b/application/back-office/WebApp/bootstrap.tsx @@ -2,13 +2,13 @@ import "@repo/ui/tailwind.css"; import { router } from "@/shared/lib/router/router"; import { ApplicationInsightsProvider } from "@repo/infrastructure/applicationInsights/ApplicationInsightsProvider"; import { setupGlobalErrorHandlers } from "@repo/infrastructure/http/errorHandler"; -import { Translation } from "@repo/infrastructure/translations/Translation"; +import { createFederatedTranslation } from "@repo/infrastructure/translations/createFederatedTranslation"; import { GlobalToastRegion } from "@repo/ui/components/Toast"; import { RouterProvider } from "@tanstack/react-router"; import React from "react"; import reactDom from "react-dom/client"; -const { TranslationProvider } = await Translation.create( +const { TranslationProvider } = await createFederatedTranslation( (locale) => import(`@/shared/translations/locale/${locale}.ts`) ); diff --git a/application/biome.json b/application/biome.json index fc4b5ebbb..7985363d0 100644 --- a/application/biome.json +++ b/application/biome.json @@ -82,7 +82,7 @@ } } }, - "ignore": ["*/environment.d.ts", "*/tailwind-preset.ts"] + "ignore": ["*/environment.d.ts", "*/tailwind-preset.ts", "*/module-federation-types/*.d.ts"] }, "files": { "ignore": ["*.Api.json"] diff --git a/application/shared-webapp/build/module-federation-types/account-management.d.ts b/application/shared-webapp/build/module-federation-types/account-management.d.ts index d33f38b62..7d9601087 100644 --- a/application/shared-webapp/build/module-federation-types/account-management.d.ts +++ b/application/shared-webapp/build/module-federation-types/account-management.d.ts @@ -6,3 +6,15 @@ declare module "account-management/AvatarButton" { declare module "account-management/SupportButton" { export default ReactNode; } +declare module "account-management/translations/en-US" { + import type { Messages } from "@lingui/core"; + export const messages: Messages; +} +declare module "account-management/translations/da-DK" { + import type { Messages } from "@lingui/core"; + export const messages: Messages; +} +declare module "account-management/translations/nl-NL" { + import type { Messages } from "@lingui/core"; + export const messages: Messages; +} diff --git a/application/shared-webapp/build/plugin/ModuleFederationPlugin.ts b/application/shared-webapp/build/plugin/ModuleFederationPlugin.ts index 8928a0431..b8529b83d 100644 --- a/application/shared-webapp/build/plugin/ModuleFederationPlugin.ts +++ b/application/shared-webapp/build/plugin/ModuleFederationPlugin.ts @@ -18,7 +18,7 @@ const { dependencies } = require(applicationPackageJson); const SYSTEM_ID = getSystemId(); type ModuleFederationPluginOptions = { - exposes?: Record<`./${string}`, `./${string}.tsx` | `./${string}.ts`>; + exposes?: Record<`./${string}`, `./${string}.tsx` | `./${string}.ts` | `./${string}`>; remotes?: Record; }; @@ -109,6 +109,15 @@ function generateModuleFederationTypesFolder(system: string, exposes: Record { logger.info(`[Module Federation] Expose: ${exportPath} => ${importPath}`); + + // Pattern matching for different module types + // Translation files follow the pattern: ./translations/xx-XX (e.g., ./translations/en-US) + const translationPattern = /^\.\/translations\/[a-z]{2}-[A-Z]{2}$/; + if (translationPattern.test(exportPath)) { + return `declare module "${exportPath.replace(/^\./, system)}" {\n import type { Messages } from "@lingui/core";\n export const messages: Messages;\n}`; + } + + // Default to ReactNode export for components return `declare module "${exportPath.replace(/^\./, system)}" {\n export default ReactNode;\n}`; }) .join("\n"); diff --git a/application/shared-webapp/infrastructure/translations/createFederatedTranslation.ts b/application/shared-webapp/infrastructure/translations/createFederatedTranslation.ts new file mode 100644 index 000000000..20fa98d29 --- /dev/null +++ b/application/shared-webapp/infrastructure/translations/createFederatedTranslation.ts @@ -0,0 +1,97 @@ +import type { Messages } from "@lingui/core"; +import { type Locale, type LocaleFile, Translation } from "./Translation"; + +// Module federation container type +type FederatedContainer = { + get(module: string): Promise<() => { messages: Messages }>; +}; + +// Cache for loaded translation modules +const translationModuleCache = new Map(); + +/** + * Configuration for federated translations + * Each application that consumes federated modules should configure + * which remotes might provide translations + */ +const FEDERATED_TRANSLATION_REMOTES = [ + "account-management" + // Add more remotes here as they are created +] as const; + +/** + * Creates a Translation instance that automatically loads and merges translations + * from all federated modules configured in the current application. + * + * This function: + * 1. Uses the base translation loader for the host application + * 2. Automatically discovers and loads translations from configured federated remotes + * 3. Merges all translations together with remote translations taking precedence + * + * @param baseLoader - Function to load base translations for the host application + * @returns Translation instance with federated translation support + */ +export function createFederatedTranslation(baseLoader: (locale: Locale) => Promise): Promise { + const federatedLoader = createFederatedLoader(baseLoader); + return Translation.create(federatedLoader); +} + +/** + * Try to load translations from a federated module + */ +async function loadRemoteTranslations(remoteName: string, locale: Locale): Promise { + // Check cache first + const cacheKey = `${remoteName}:${locale}`; + const cached = translationModuleCache.get(cacheKey); + if (cached) { + return cached; + } + + // Get container using RSBuild's naming convention (hyphens to underscores) + const containerName = remoteName.replace(/-/g, "_"); + const container = (window as unknown as Record)[containerName] as FederatedContainer | null; + + if (!container?.get) { + return null; + } + + try { + const factory = await container.get(`./translations/${locale}`); + const module = factory(); + + if (module?.messages) { + translationModuleCache.set(cacheKey, module.messages); + return module.messages; + } + } catch (_error) { + // Silently fail - the remote might not have translations for this locale + } + + return null; +} + +/** + * Creates a translation loader that merges translations from federated modules + */ +function createFederatedLoader( + baseLoader: (locale: Locale) => Promise +): (locale: Locale) => Promise { + return async (locale: Locale): Promise => { + // Load base translations first + const baseMessages = await baseLoader(locale); + + // Load and merge translations from all configured remotes + const allMessages = { ...baseMessages.messages }; + + await Promise.all( + FEDERATED_TRANSLATION_REMOTES.map(async (remoteName) => { + const remoteMessages = await loadRemoteTranslations(remoteName, locale); + if (remoteMessages) { + Object.assign(allMessages, remoteMessages); + } + }) + ); + + return { messages: allMessages }; + }; +} From 91100ca12625cf80e2285005792258295eb2f987 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Tue, 22 Jul 2025 22:25:07 +0200 Subject: [PATCH 09/26] Replace account deletion with support contact dialog --- .../-components/DeleteAccountConfirmation.tsx | 44 ++++++++++++------- .../shared/translations/locale/da-DK.po | 9 ++-- .../shared/translations/locale/en-US.po | 9 ++-- .../shared/translations/locale/nl-NL.po | 9 ++-- 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/application/account-management/WebApp/routes/admin/account/-components/DeleteAccountConfirmation.tsx b/application/account-management/WebApp/routes/admin/account/-components/DeleteAccountConfirmation.tsx index 4e20054c1..0b98fde39 100644 --- a/application/account-management/WebApp/routes/admin/account/-components/DeleteAccountConfirmation.tsx +++ b/application/account-management/WebApp/routes/admin/account/-components/DeleteAccountConfirmation.tsx @@ -1,7 +1,9 @@ import { t } from "@lingui/core/macro"; -import { Trans } from "@lingui/react/macro"; -import { AlertDialog } from "@repo/ui/components/AlertDialog"; +import { Button } from "@repo/ui/components/Button"; +import { Dialog } from "@repo/ui/components/Dialog"; +import { Heading } from "@repo/ui/components/Heading"; import { Modal } from "@repo/ui/components/Modal"; +import { MailIcon, XIcon } from "lucide-react"; type DeleteAccountConfirmationProps = { isOpen: boolean; @@ -10,20 +12,30 @@ type DeleteAccountConfirmationProps = { export default function DeleteAccountConfirmation({ isOpen, onOpenChange }: Readonly) { return ( - - onOpenChange(false)} - > - - You are about to permanently delete the account and the entire data environment via PlatformPlatform. -
-
- This action is permanent and irreversible. -
-
+ + + {({ close }) => ( + <> + + + {t`Delete account`} + +

{t`To delete your account, please contact our support team.`}

+
+ +

{t`Our support team will assist you with the account deletion process and ensure all your data is properly removed.`}

+
+ +
+
+ + )} +
); } diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po index ccbf43a52..26c005a6e 100644 --- a/application/account-management/WebApp/shared/translations/locale/da-DK.po +++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po @@ -278,6 +278,9 @@ msgstr "Kun kontoejere kan ændre kontonavnet" msgid "Organization" msgstr "Organisation" +msgid "Our support team will assist you with the account deletion process and ensure all your data is properly removed." +msgstr "Vores supportteam hjælper dig med at slette din konto og sikrer, at alle dine data bliver fjernet." + msgid "Owner" msgstr "Ejer" @@ -372,6 +375,9 @@ msgstr "Dette er den region, hvor dine data er lagret" msgid "Title" msgstr "Titel" +msgid "To delete your account, please contact our support team." +msgstr "For at slette din konto bedes du kontakte vores supportteam." + msgid "Toggle collapsed menu" msgstr "Skift kollapset menu" @@ -460,9 +466,6 @@ msgstr "Velkommen hjem" msgid "Welcome home, {0}" msgstr "Velkommen hjem, {0}" -msgid "You are about to permanently delete the account and the entire data environment via PlatformPlatform.<0/><1/>This action is permanent and irreversible." -msgstr "Du er ved at slette kontoen og hele dataomgivelserne permanent via PlatformPlatform.<0/><1/>Denne handling er permanent og kan ikke fortrydes." - msgid "Your verification code has expired" msgstr "Din bekræftelseskode er udløbet" diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po index f03511cc8..9d899ff53 100644 --- a/application/account-management/WebApp/shared/translations/locale/en-US.po +++ b/application/account-management/WebApp/shared/translations/locale/en-US.po @@ -278,6 +278,9 @@ msgstr "Only account owners can modify the account name" msgid "Organization" msgstr "Organization" +msgid "Our support team will assist you with the account deletion process and ensure all your data is properly removed." +msgstr "Our support team will assist you with the account deletion process and ensure all your data is properly removed." + msgid "Owner" msgstr "Owner" @@ -372,6 +375,9 @@ msgstr "This is the region where your data is stored" msgid "Title" msgstr "Title" +msgid "To delete your account, please contact our support team." +msgstr "To delete your account, please contact our support team." + msgid "Toggle collapsed menu" msgstr "Toggle collapsed menu" @@ -460,9 +466,6 @@ msgstr "Welcome home" msgid "Welcome home, {0}" msgstr "Welcome home, {0}" -msgid "You are about to permanently delete the account and the entire data environment via PlatformPlatform.<0/><1/>This action is permanent and irreversible." -msgstr "You are about to permanently delete the account and the entire data environment via PlatformPlatform.<0/><1/>This action is permanent and irreversible." - msgid "Your verification code has expired" msgstr "Your verification code has expired" diff --git a/application/account-management/WebApp/shared/translations/locale/nl-NL.po b/application/account-management/WebApp/shared/translations/locale/nl-NL.po index 9a733c6ae..8a18068ea 100644 --- a/application/account-management/WebApp/shared/translations/locale/nl-NL.po +++ b/application/account-management/WebApp/shared/translations/locale/nl-NL.po @@ -278,6 +278,9 @@ msgstr "Alleen accounteigenaren kunnen de accountnaam wijzigen" msgid "Organization" msgstr "Organisatie" +msgid "Our support team will assist you with the account deletion process and ensure all your data is properly removed." +msgstr "Ons ondersteuningsteam helpt je met het verwijderen van je account en al je gegevens." + msgid "Owner" msgstr "Eigenaar" @@ -372,6 +375,9 @@ msgstr "Dit is de regio waar je gegevens zijn opgeslagen" msgid "Title" msgstr "Titel" +msgid "To delete your account, please contact our support team." +msgstr "Neem contact op met ons ondersteuningsteam om je account te verwijderen." + msgid "Toggle collapsed menu" msgstr "Ingeklapt menu wisselen" @@ -460,9 +466,6 @@ msgstr "Welkom home" msgid "Welcome home, {0}" msgstr "Welkom home, {0}" -msgid "You are about to permanently delete the account and the entire data environment via PlatformPlatform.<0/><1/>This action is permanent and irreversible." -msgstr "Je staat op het punt om het account en de volledige gegevensomgeving permanent te verwijderen via PlatformPlatform.<0/><1/>Deze actie is definitief en onomkeerbaar." - msgid "Your verification code has expired" msgstr "Je verificatiecode is verlopen" From dfd73c958d1a55242f9ae9474f6107ea789de84c Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 23 Jul 2025 01:43:15 +0200 Subject: [PATCH 10/26] Create shared menu using module federation --- .../WebApp/routes/admin/account/index.tsx | 22 +++- .../WebApp/routes/admin/index.tsx | 21 +++- .../WebApp/routes/admin/users/index.tsx | 22 +++- .../WebApp/rsbuild.config.ts | 1 + .../shared/components/SharedSideMenu.tsx | 100 ++++++++++-------- .../shared/translations/locale/da-DK.po | 3 + .../shared/translations/locale/en-US.po | 3 + .../shared/translations/locale/nl-NL.po | 3 + .../WebApp/routes/back-office/index.tsx | 22 +++- .../shared/components/SharedSideMenu.tsx | 25 ----- .../shared/translations/locale/da-DK.po | 3 - .../shared/translations/locale/en-US.po | 3 - .../shared/translations/locale/nl-NL.po | 3 - .../account-management.d.ts | 3 + 14 files changed, 145 insertions(+), 89 deletions(-) delete mode 100644 application/back-office/WebApp/shared/components/SharedSideMenu.tsx diff --git a/application/account-management/WebApp/routes/admin/account/index.tsx b/application/account-management/WebApp/routes/admin/account/index.tsx index 23aa21891..43abe206f 100644 --- a/application/account-management/WebApp/routes/admin/account/index.tsx +++ b/application/account-management/WebApp/routes/admin/account/index.tsx @@ -1,9 +1,11 @@ -import { SharedSideMenu } from "@/shared/components/SharedSideMenu"; +import SharedSideMenu from "@/shared/components/SharedSideMenu"; import { TopMenu } from "@/shared/components/topMenu"; import logoWrap from "@/shared/images/logo-wrap.svg"; import { UserRole, api } from "@/shared/lib/api/client"; import { t } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react"; import { Trans } from "@lingui/react/macro"; +import { type Locale, translationContext } from "@repo/infrastructure/translations/TranslationContext"; import { AppLayout } from "@repo/ui/components/AppLayout"; import { Breadcrumb } from "@repo/ui/components/Breadcrumbs"; import { Button } from "@repo/ui/components/Button"; @@ -13,7 +15,7 @@ import { toastQueue } from "@repo/ui/components/Toast"; import { mutationSubmitter } from "@repo/ui/forms/mutationSubmitter"; import { createFileRoute } from "@tanstack/react-router"; import { Trash2 } from "lucide-react"; -import { useEffect, useState } from "react"; +import { use, useEffect, useState } from "react"; import { Separator } from "react-aria-components"; import DeleteAccountConfirmation from "./-components/DeleteAccountConfirmation"; @@ -26,6 +28,11 @@ export function AccountSettings() { const { data: tenant, isLoading: tenantLoading } = api.useQuery("get", "/api/account-management/tenants/current"); const { data: currentUser, isLoading: userLoading } = api.useQuery("get", "/api/account-management/users/me"); const updateCurrentTenantMutation = api.useMutation("put", "/api/account-management/tenants/current"); + const { i18n } = useLingui(); + const { getLocaleInfo, locales, setLocale } = use(translationContext); + + const currentLocale = i18n.locale as Locale; + const currentLocaleLabel = getLocaleInfo(currentLocale).label; const isOwner = currentUser?.role === UserRole.Owner; @@ -45,7 +52,16 @@ export function AccountSettings() { return ( <> - + ({ + value: locale, + label: getLocaleInfo(locale).label + }))} + onLocaleChange={(locale) => setLocale(locale as Locale)} + /> - + ({ + value: locale, + label: getLocaleInfo(locale).label + }))} + onLocaleChange={(locale) => setLocale(locale as Locale)} + /> }>

{userInfo?.firstName ? Welcome home, {userInfo.firstName} : Welcome home}

diff --git a/application/account-management/WebApp/routes/admin/users/index.tsx b/application/account-management/WebApp/routes/admin/users/index.tsx index ea393fd02..bf23665b6 100644 --- a/application/account-management/WebApp/routes/admin/users/index.tsx +++ b/application/account-management/WebApp/routes/admin/users/index.tsx @@ -1,12 +1,14 @@ -import { SharedSideMenu } from "@/shared/components/SharedSideMenu"; +import SharedSideMenu from "@/shared/components/SharedSideMenu"; import { TopMenu } from "@/shared/components/topMenu"; import { SortOrder, SortableUserProperties, UserRole, UserStatus, api, type components } from "@/shared/lib/api/client"; import { t } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react"; import { Trans } from "@lingui/react/macro"; +import { type Locale, translationContext } from "@repo/infrastructure/translations/TranslationContext"; import { AppLayout } from "@repo/ui/components/AppLayout"; import { Breadcrumb } from "@repo/ui/components/Breadcrumbs"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import { useEffect, useState } from "react"; +import { use, useEffect, useState } from "react"; import { z } from "zod"; import { ChangeUserRoleDialog } from "./-components/ChangeUserRoleDialog"; import { DeleteUserDialog } from "./-components/DeleteUserDialog"; @@ -42,6 +44,11 @@ export default function UsersPage() { const [tableUsers, setTableUsers] = useState([]); const navigate = useNavigate({ from: Route.fullPath }); const { userId } = Route.useSearch(); + const { i18n } = useLingui(); + const { getLocaleInfo, locales, setLocale } = use(translationContext); + + const currentLocale = i18n.locale as Locale; + const currentLocaleLabel = getLocaleInfo(currentLocale).label; const handleCloseProfile = () => { setProfileUser(null); @@ -105,7 +112,16 @@ export default function UsersPage() { return ( <> - + ({ + value: locale, + label: getLocaleInfo(locale).label + }))} + onLocaleChange={(locale) => setLocale(locale as Locale)} + /> ; + onLocaleChange?: (locale: string) => void; }; -export function SharedSideMenu({ children, ariaLabel }: Readonly) { +export default function SharedSideMenu({ + children, + ariaLabel, + currentLocale, + currentLocaleLabel, + locales, + onLocaleChange +}: Readonly) { const userInfo = useUserInfo(); - const { i18n } = useLingui(); - const { getLocaleInfo, locales, setLocale } = use(translationContext); const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); const queryClient = useQueryClient(); // Access mobile menu overlay context to close menu when needed const overlayCtx = useContext(overlayContext); - const currentLocale = i18n.locale as Locale; - const currentLocaleLabel = getLocaleInfo(currentLocale).label; - const logoutMutation = api.useMutation("post", "/api/account-management/authentication/logout", { onMutate: async () => { await queryClient.cancelQueries(); @@ -135,41 +141,43 @@ export function SharedSideMenu({ children, ariaLabel }: Readonly {/* Language Section - styled like menu item */} -

- - - { - const locale = key.toString() as Locale; - if (locale !== currentLocale) { - setLocale(locale); - } - }} - placement="bottom end" - > - {locales.map((locale) => ( - -
- {getLocaleInfo(locale).label} - {locale === currentLocale && } -
-
- ))} -
-
-
+ {currentLocale && currentLocaleLabel && locales && onLocaleChange && ( +
+ + + { + const locale = key.toString(); + if (locale !== currentLocale) { + onLocaleChange(locale); + } + }} + placement="bottom end" + > + {locales.map((locale) => ( + +
+ {locale.label} + {locale.value === currentLocale && } +
+
+ ))} +
+
+
+ )} {/* Support Section - styled like menu item */}
@@ -204,6 +212,7 @@ export function SharedSideMenu({ children, ariaLabel }: Readonly + {children}
@@ -221,6 +230,7 @@ export function SharedSideMenu({ children, ariaLabel }: Readonly + {children} diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po index 26c005a6e..74ac8fef0 100644 --- a/application/account-management/WebApp/shared/translations/locale/da-DK.po +++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po @@ -75,6 +75,9 @@ msgstr "Er du sikker på, at du vil slette <0>{0} brugere?" msgid "Are you sure you want to delete <0>{userDisplayName}?" msgstr "Er du sikker på, at du vil slette <0>{userDisplayName}?" +msgid "Back Office" +msgstr "Back Office" + msgid "Back to login" msgstr "Tilbage til login" diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po index 9d899ff53..827ba43f0 100644 --- a/application/account-management/WebApp/shared/translations/locale/en-US.po +++ b/application/account-management/WebApp/shared/translations/locale/en-US.po @@ -75,6 +75,9 @@ msgstr "Are you sure you want to delete <0>{0} users?" msgid "Are you sure you want to delete <0>{userDisplayName}?" msgstr "Are you sure you want to delete <0>{userDisplayName}?" +msgid "Back Office" +msgstr "Back Office" + msgid "Back to login" msgstr "Back to login" diff --git a/application/account-management/WebApp/shared/translations/locale/nl-NL.po b/application/account-management/WebApp/shared/translations/locale/nl-NL.po index 8a18068ea..09ada74e6 100644 --- a/application/account-management/WebApp/shared/translations/locale/nl-NL.po +++ b/application/account-management/WebApp/shared/translations/locale/nl-NL.po @@ -75,6 +75,9 @@ msgstr "Weet je zeker dat je <0>{0} gebruikers wilt verwijderen?" msgid "Are you sure you want to delete <0>{userDisplayName}?" msgstr "Weet je zeker dat je <0>{userDisplayName} wilt verwijderen?" +msgid "Back Office" +msgstr "Backoffice" + msgid "Back to login" msgstr "Terug naar inloggen" diff --git a/application/back-office/WebApp/routes/back-office/index.tsx b/application/back-office/WebApp/routes/back-office/index.tsx index 897b58977..0cb3d32e0 100644 --- a/application/back-office/WebApp/routes/back-office/index.tsx +++ b/application/back-office/WebApp/routes/back-office/index.tsx @@ -1,18 +1,36 @@ -import { SharedSideMenu } from "@/shared/components/SharedSideMenu"; import { TopMenu } from "@/shared/components/topMenu"; import { t } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react"; import { Trans } from "@lingui/react/macro"; +import { type Locale, translationContext } from "@repo/infrastructure/translations/TranslationContext"; import { AppLayout } from "@repo/ui/components/AppLayout"; import { createFileRoute } from "@tanstack/react-router"; +import SharedSideMenu from "account-management/SharedSideMenu"; +import { use } from "react"; export const Route = createFileRoute("/back-office/")({ component: Home }); export default function Home() { + const { i18n } = useLingui(); + const { getLocaleInfo, locales, setLocale } = use(translationContext); + + const currentLocale = i18n.locale as Locale; + const currentLocaleLabel = getLocaleInfo(currentLocale).label; + return ( <> - + ({ + value: locale, + label: getLocaleInfo(locale).label + }))} + onLocaleChange={(locale: string) => setLocale(locale as Locale)} + /> }>

Welcome to the Back Office diff --git a/application/back-office/WebApp/shared/components/SharedSideMenu.tsx b/application/back-office/WebApp/shared/components/SharedSideMenu.tsx deleted file mode 100644 index 8c5df1006..000000000 --- a/application/back-office/WebApp/shared/components/SharedSideMenu.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { t } from "@lingui/core/macro"; -import { useUserInfo } from "@repo/infrastructure/auth/hooks"; -import { MenuButton, SideMenu, SideMenuSpacer } from "@repo/ui/components/SideMenu"; -import { BoxIcon, HomeIcon } from "lucide-react"; -import type React from "react"; - -type SharedSideMenuProps = { - children?: React.ReactNode; - ariaLabel: string; -}; - -export function SharedSideMenu({ children, ariaLabel }: Readonly) { - const userInfo = useUserInfo(); - - return ( - - - {children} - - - - - - ); -} diff --git a/application/back-office/WebApp/shared/translations/locale/da-DK.po b/application/back-office/WebApp/shared/translations/locale/da-DK.po index 1b76a413b..69c4dcefb 100644 --- a/application/back-office/WebApp/shared/translations/locale/da-DK.po +++ b/application/back-office/WebApp/shared/translations/locale/da-DK.po @@ -13,9 +13,6 @@ msgstr "" "Plural-Forms: \n" "X-Generator: @lingui/cli\n" -msgid "Account management" -msgstr "Kontoadministration" - msgid "Change language" msgstr "Skift sprog" diff --git a/application/back-office/WebApp/shared/translations/locale/en-US.po b/application/back-office/WebApp/shared/translations/locale/en-US.po index 1068dcede..83e976eb2 100644 --- a/application/back-office/WebApp/shared/translations/locale/en-US.po +++ b/application/back-office/WebApp/shared/translations/locale/en-US.po @@ -13,9 +13,6 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" -msgid "Account management" -msgstr "Account management" - msgid "Change language" msgstr "Change language" diff --git a/application/back-office/WebApp/shared/translations/locale/nl-NL.po b/application/back-office/WebApp/shared/translations/locale/nl-NL.po index b7c463a2c..4fc70ded6 100644 --- a/application/back-office/WebApp/shared/translations/locale/nl-NL.po +++ b/application/back-office/WebApp/shared/translations/locale/nl-NL.po @@ -13,9 +13,6 @@ msgstr "" "Plural-Forms: \n" "X-Generator: @lingui/cli\n" -msgid "Account management" -msgstr "Accountbeheer" - msgid "Change language" msgstr "Taal wijzigen" diff --git a/application/shared-webapp/build/module-federation-types/account-management.d.ts b/application/shared-webapp/build/module-federation-types/account-management.d.ts index 7d9601087..bd6cb068a 100644 --- a/application/shared-webapp/build/module-federation-types/account-management.d.ts +++ b/application/shared-webapp/build/module-federation-types/account-management.d.ts @@ -6,6 +6,9 @@ declare module "account-management/AvatarButton" { declare module "account-management/SupportButton" { export default ReactNode; } +declare module "account-management/SharedSideMenu" { + export default ReactNode; +} declare module "account-management/translations/en-US" { import type { Messages } from "@lingui/core"; export const messages: Messages; From 079b26942ac996818562dc93f7f771f19a5a82b9 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 23 Jul 2025 11:36:15 +0200 Subject: [PATCH 11/26] Create federated navigation with SPA reload only when navigating between systems --- .../WebApp/routes/admin/account/index.tsx | 1 + .../WebApp/routes/admin/index.tsx | 1 + .../WebApp/routes/admin/users/index.tsx | 1 + .../shared/components/SharedSideMenu.tsx | 77 ++++++-- .../shared/translations/locale/da-DK.po | 3 + .../shared/translations/locale/en-US.po | 3 + .../shared/translations/locale/nl-NL.po | 3 + .../WebApp/routes/back-office/index.tsx | 1 + .../shared-webapp/ui/components/SideMenu.tsx | 168 ++++++++++++++++-- 9 files changed, 232 insertions(+), 26 deletions(-) diff --git a/application/account-management/WebApp/routes/admin/account/index.tsx b/application/account-management/WebApp/routes/admin/account/index.tsx index 43abe206f..ddec81e77 100644 --- a/application/account-management/WebApp/routes/admin/account/index.tsx +++ b/application/account-management/WebApp/routes/admin/account/index.tsx @@ -54,6 +54,7 @@ export function AccountSettings() { <> ({ diff --git a/application/account-management/WebApp/routes/admin/index.tsx b/application/account-management/WebApp/routes/admin/index.tsx index 82222f4cf..7eb5330d7 100644 --- a/application/account-management/WebApp/routes/admin/index.tsx +++ b/application/account-management/WebApp/routes/admin/index.tsx @@ -28,6 +28,7 @@ export default function Home() { <> ({ diff --git a/application/account-management/WebApp/routes/admin/users/index.tsx b/application/account-management/WebApp/routes/admin/users/index.tsx index bf23665b6..973c0e245 100644 --- a/application/account-management/WebApp/routes/admin/users/index.tsx +++ b/application/account-management/WebApp/routes/admin/users/index.tsx @@ -114,6 +114,7 @@ export default function UsersPage() { <> ({ diff --git a/application/account-management/WebApp/shared/components/SharedSideMenu.tsx b/application/account-management/WebApp/shared/components/SharedSideMenu.tsx index e4775680e..785209abd 100644 --- a/application/account-management/WebApp/shared/components/SharedSideMenu.tsx +++ b/application/account-management/WebApp/shared/components/SharedSideMenu.tsx @@ -7,7 +7,7 @@ import { createLoginUrlWithReturnPath } from "@repo/infrastructure/auth/util"; import { Avatar } from "@repo/ui/components/Avatar"; import { Button } from "@repo/ui/components/Button"; import { Menu, MenuItem, MenuTrigger } from "@repo/ui/components/Menu"; -import { MenuButton, SideMenu, SideMenuSeparator, SideMenuSpacer, overlayContext } from "@repo/ui/components/SideMenu"; +import { FederatedMenuButton, SideMenu, SideMenuSeparator, overlayContext } from "@repo/ui/components/SideMenu"; import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; import { useQueryClient } from "@tanstack/react-query"; import { @@ -21,15 +21,14 @@ import { UserIcon, UsersIcon } from "lucide-react"; -import type React from "react"; import { useContext, useState } from "react"; import { SupportDialog } from "./support/SupportDialog"; import UserProfileModal from "./userModals/UserProfileModal"; import "@repo/ui/tailwind.css"; type SharedSideMenuProps = { - children?: React.ReactNode; ariaLabel: string; + currentSystem: "account-management" | "back-office"; currentLocale?: string; currentLocaleLabel?: string; locales?: Array<{ value: string; label: string }>; @@ -37,8 +36,8 @@ type SharedSideMenuProps = { }; export default function SharedSideMenu({ - children, ariaLabel, + currentSystem, currentLocale, currentLocaleLabel, locales, @@ -74,7 +73,7 @@ export default function SharedSideMenu({
{userInfo.fullName}
-
{userInfo.title || userInfo.email}
+
{userInfo.title ?? userInfo.email}
{/* Spacer to push content up */} @@ -224,14 +247,38 @@ export default function SharedSideMenu({ return ( <> - + + Organization - - - - {children} + + + + + Back Office + + diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po index 74ac8fef0..7b745efca 100644 --- a/application/account-management/WebApp/shared/translations/locale/da-DK.po +++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po @@ -142,6 +142,9 @@ msgstr "Oprettet" msgid "Danger zone" msgstr "Farezone" +msgid "Dashboard" +msgstr "Dashboard" + msgid "Delete" msgstr "Slet" diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po index 827ba43f0..95be918f2 100644 --- a/application/account-management/WebApp/shared/translations/locale/en-US.po +++ b/application/account-management/WebApp/shared/translations/locale/en-US.po @@ -142,6 +142,9 @@ msgstr "Created" msgid "Danger zone" msgstr "Danger zone" +msgid "Dashboard" +msgstr "Dashboard" + msgid "Delete" msgstr "Delete" diff --git a/application/account-management/WebApp/shared/translations/locale/nl-NL.po b/application/account-management/WebApp/shared/translations/locale/nl-NL.po index 09ada74e6..238a1f89a 100644 --- a/application/account-management/WebApp/shared/translations/locale/nl-NL.po +++ b/application/account-management/WebApp/shared/translations/locale/nl-NL.po @@ -142,6 +142,9 @@ msgstr "Aangemaakt" msgid "Danger zone" msgstr "Gevaarzone" +msgid "Dashboard" +msgstr "Dashboard" + msgid "Delete" msgstr "Verwijderen" diff --git a/application/back-office/WebApp/routes/back-office/index.tsx b/application/back-office/WebApp/routes/back-office/index.tsx index 0cb3d32e0..a0dffd405 100644 --- a/application/back-office/WebApp/routes/back-office/index.tsx +++ b/application/back-office/WebApp/routes/back-office/index.tsx @@ -23,6 +23,7 @@ export default function Home() { <> ({ diff --git a/application/shared-webapp/ui/components/SideMenu.tsx b/application/shared-webapp/ui/components/SideMenu.tsx index 27e75c4f9..8bce9c9a0 100644 --- a/application/shared-webapp/ui/components/SideMenu.tsx +++ b/application/shared-webapp/ui/components/SideMenu.tsx @@ -88,6 +88,10 @@ type MenuButtonProps = { forceReload: true; href: string; } + | { + federatedNavigation: true; + href: string; + } ); // Helper function to get target path from href @@ -152,13 +156,9 @@ function ActiveIndicator({ ); } -export function MenuButton({ - icon: Icon, - label, - href: to, - isDisabled = false, - forceReload = false -}: Readonly) { +export function MenuButton({ icon: Icon, label, href: to, isDisabled = false, ...props }: Readonly) { + const forceReload = "forceReload" in props ? props.forceReload : false; + const federatedNavigation = "federatedNavigation" in props ? props.federatedNavigation : false; const isCollapsed = useContext(collapsedContext); const overlayCtx = useContext(overlayContext); const router = useRouter(); @@ -201,8 +201,25 @@ export function MenuButton({ overlayCtx.close(); } - // Handle navigation for React Aria Link - if (forceReload) { + // Smart navigation for federated modules + if (federatedNavigation) { + // Check if the target route exists in the current router + try { + const matchResult = router.matchRoute({ to }); + if (matchResult !== false) { + // Route exists in current system - use SPA navigation + // Don't do anything, let React Aria handle the navigation + return; + } + } catch { + // Route doesn't exist in current system + } + + // Route doesn't exist in current system - force reload + window.location.href = to; + } + // Legacy forceReload behavior + else if (forceReload) { window.location.href = to; } }; @@ -214,7 +231,7 @@ export function MenuButton({ + + + + + + ); + } + + // For regular navigation, use TanStack Router Link return (
@@ -249,6 +287,114 @@ export function MenuButton({ ); } +// Federated menu button for module federation +type FederatedMenuButtonProps = { + icon: LucideIcon; + label: string; + href: string; + isCurrentSystem: boolean; + isDisabled?: boolean; +}; + +export function FederatedMenuButton({ + icon: Icon, + label, + href: to, + isCurrentSystem, + isDisabled = false +}: Readonly) { + const isCollapsed = useContext(collapsedContext); + const overlayCtx = useContext(overlayContext); + const router = useRouter(); + + // Check if this menu item is active + const currentPath = router.state.location.pathname; + const targetPath = to; + const isActive = normalizePath(currentPath) === normalizePath(targetPath); + + // Check if we're in the mobile menu context + const isMobileMenu = !window.matchMedia(MEDIA_QUERIES.sm).matches && !!overlayCtx?.isOpen; + + const linkClassName = menuButtonStyles({ isCollapsed, isActive, isDisabled }); + + const handleClick = (e: React.MouseEvent) => { + if (isDisabled) { + e.preventDefault(); + return; + } + + // Auto-close overlay after navigation + if (overlayCtx?.isOpen) { + overlayCtx.close(); + } + + // For string hrefs in federated modules, let browser handle the navigation + // The browser will either do SPA navigation (if same origin) or full reload + }; + + const handlePress = () => { + if (isDisabled) { + return; + } + + // Auto-close overlay after navigation + if (overlayCtx?.isOpen) { + overlayCtx.close(); + } + + if (isCurrentSystem) { + // Same system - use pushState for SPA navigation + window.history.pushState({}, "", to); + // Trigger a popstate event to let the router handle the navigation + window.dispatchEvent(new PopStateEvent("popstate")); + } else { + // Different system - force reload + window.location.href = to; + } + }; + + // For collapsed menu, wrap in TooltipTrigger + if (isCollapsed) { + return ( +
+ + + + + + + {label} + + +
+ ); + } + + // For expanded menu, use a regular anchor tag with onClick handler + return ( +
+ + + + +
+ ); +} + const sideMenuStyles = tv({ base: "group fixed top-0 left-0 z-50 flex h-screen flex-col bg-background transition-[width] duration-100 md:z-[70]", variants: { From 8b56a65a6f95f64757659e4a35f89a2486fe3744 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 23 Jul 2025 13:45:29 +0200 Subject: [PATCH 12/26] Simplify contract for SharedSideMenu --- .../WebApp/routes/admin/account/index.tsx | 21 +--- .../WebApp/routes/admin/index.tsx | 20 +--- .../WebApp/routes/admin/users/index.tsx | 22 +--- .../shared/components/SharedSideMenu.tsx | 103 +++++++++--------- .../WebApp/routes/back-office/index.tsx | 22 +--- .../shared/translations/locale/da-DK.po | 3 - .../shared/translations/locale/en-US.po | 3 - .../shared/translations/locale/nl-NL.po | 3 - 8 files changed, 57 insertions(+), 140 deletions(-) diff --git a/application/account-management/WebApp/routes/admin/account/index.tsx b/application/account-management/WebApp/routes/admin/account/index.tsx index ddec81e77..16f761133 100644 --- a/application/account-management/WebApp/routes/admin/account/index.tsx +++ b/application/account-management/WebApp/routes/admin/account/index.tsx @@ -3,9 +3,7 @@ import { TopMenu } from "@/shared/components/topMenu"; import logoWrap from "@/shared/images/logo-wrap.svg"; import { UserRole, api } from "@/shared/lib/api/client"; import { t } from "@lingui/core/macro"; -import { useLingui } from "@lingui/react"; import { Trans } from "@lingui/react/macro"; -import { type Locale, translationContext } from "@repo/infrastructure/translations/TranslationContext"; import { AppLayout } from "@repo/ui/components/AppLayout"; import { Breadcrumb } from "@repo/ui/components/Breadcrumbs"; import { Button } from "@repo/ui/components/Button"; @@ -15,7 +13,7 @@ import { toastQueue } from "@repo/ui/components/Toast"; import { mutationSubmitter } from "@repo/ui/forms/mutationSubmitter"; import { createFileRoute } from "@tanstack/react-router"; import { Trash2 } from "lucide-react"; -import { use, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { Separator } from "react-aria-components"; import DeleteAccountConfirmation from "./-components/DeleteAccountConfirmation"; @@ -28,11 +26,6 @@ export function AccountSettings() { const { data: tenant, isLoading: tenantLoading } = api.useQuery("get", "/api/account-management/tenants/current"); const { data: currentUser, isLoading: userLoading } = api.useQuery("get", "/api/account-management/users/me"); const updateCurrentTenantMutation = api.useMutation("put", "/api/account-management/tenants/current"); - const { i18n } = useLingui(); - const { getLocaleInfo, locales, setLocale } = use(translationContext); - - const currentLocale = i18n.locale as Locale; - const currentLocaleLabel = getLocaleInfo(currentLocale).label; const isOwner = currentUser?.role === UserRole.Owner; @@ -52,17 +45,7 @@ export function AccountSettings() { return ( <> - ({ - value: locale, - label: getLocaleInfo(locale).label - }))} - onLocaleChange={(locale) => setLocale(locale as Locale)} - /> + - ({ - value: locale, - label: getLocaleInfo(locale).label - }))} - onLocaleChange={(locale) => setLocale(locale as Locale)} - /> + }>

{userInfo?.firstName ? Welcome home, {userInfo.firstName} : Welcome home}

diff --git a/application/account-management/WebApp/routes/admin/users/index.tsx b/application/account-management/WebApp/routes/admin/users/index.tsx index 973c0e245..4b1a1cc59 100644 --- a/application/account-management/WebApp/routes/admin/users/index.tsx +++ b/application/account-management/WebApp/routes/admin/users/index.tsx @@ -1,14 +1,11 @@ import SharedSideMenu from "@/shared/components/SharedSideMenu"; import { TopMenu } from "@/shared/components/topMenu"; import { SortOrder, SortableUserProperties, UserRole, UserStatus, api, type components } from "@/shared/lib/api/client"; -import { t } from "@lingui/core/macro"; -import { useLingui } from "@lingui/react"; import { Trans } from "@lingui/react/macro"; -import { type Locale, translationContext } from "@repo/infrastructure/translations/TranslationContext"; import { AppLayout } from "@repo/ui/components/AppLayout"; import { Breadcrumb } from "@repo/ui/components/Breadcrumbs"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import { use, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { z } from "zod"; import { ChangeUserRoleDialog } from "./-components/ChangeUserRoleDialog"; import { DeleteUserDialog } from "./-components/DeleteUserDialog"; @@ -44,11 +41,6 @@ export default function UsersPage() { const [tableUsers, setTableUsers] = useState([]); const navigate = useNavigate({ from: Route.fullPath }); const { userId } = Route.useSearch(); - const { i18n } = useLingui(); - const { getLocaleInfo, locales, setLocale } = use(translationContext); - - const currentLocale = i18n.locale as Locale; - const currentLocaleLabel = getLocaleInfo(currentLocale).label; const handleCloseProfile = () => { setProfileUser(null); @@ -112,17 +104,7 @@ export default function UsersPage() { return ( <> - ({ - value: locale, - label: getLocaleInfo(locale).label - }))} - onLocaleChange={(locale) => setLocale(locale as Locale)} - /> + ; - onLocaleChange?: (locale: string) => void; + currentSystem: "account-management" | "back-office"; // Add your self-contained system here }; -export default function SharedSideMenu({ - ariaLabel, - currentSystem, - currentLocale, - currentLocaleLabel, - locales, - onLocaleChange -}: Readonly) { +export default function SharedSideMenu({ currentSystem }: Readonly) { const userInfo = useUserInfo(); const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); const queryClient = useQueryClient(); + const { i18n } = useLingui(); + + const currentLocale = i18n.locale as Locale; + const locales = Object.keys(localeMap) as Locale[]; + const currentLocaleLabel = localeMap[currentLocale].label; // Access mobile menu overlay context to close menu when needed const overlayCtx = useContext(overlayContext); @@ -140,43 +136,44 @@ export default function SharedSideMenu({

{/* Language Section - styled like menu item */} - {currentLocale && currentLocaleLabel && locales && onLocaleChange && ( -
- - - { - const locale = key.toString(); - if (locale !== currentLocale) { - onLocaleChange(locale); - } - }} - placement="bottom end" - > - {locales.map((locale) => ( - -
- {locale.label} - {locale.value === currentLocale && } -
-
- ))} -
-
-
- )} +
+ + + { + const locale = key.toString() as Locale; + if (locale !== currentLocale) { + // Dynamically load and activate the locale + const localeModule = await import(`@/shared/translations/locale/${locale}.ts`); + i18n.loadAndActivate({ locale, messages: localeModule.messages }); + document.documentElement.lang = locale; + } + }} + placement="bottom end" + > + {locales.map((locale) => ( + +
+ {localeMap[locale].label} + {locale === currentLocale && } +
+
+ ))} +
+
+
{/* Support Section - styled like menu item */}
@@ -246,7 +243,7 @@ export default function SharedSideMenu({ return ( <> - + Organization + Back Office + - ({ - value: locale, - label: getLocaleInfo(locale).label - }))} - onLocaleChange={(locale: string) => setLocale(locale as Locale)} - /> + }>

Welcome to the Back Office diff --git a/application/back-office/WebApp/shared/translations/locale/da-DK.po b/application/back-office/WebApp/shared/translations/locale/da-DK.po index 69c4dcefb..1d2719613 100644 --- a/application/back-office/WebApp/shared/translations/locale/da-DK.po +++ b/application/back-office/WebApp/shared/translations/locale/da-DK.po @@ -28,9 +28,6 @@ msgstr "Hjem" msgid "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams." msgstr "Administrer lejere, se systemdata, se undtagelser og udfør forskellige opgaver for drifts- og supportteams." -msgid "Toggle collapsed menu" -msgstr "Skift kollapset menu" - msgid "User profile menu" msgstr "Brugerprofilmenu" diff --git a/application/back-office/WebApp/shared/translations/locale/en-US.po b/application/back-office/WebApp/shared/translations/locale/en-US.po index 83e976eb2..668e855c8 100644 --- a/application/back-office/WebApp/shared/translations/locale/en-US.po +++ b/application/back-office/WebApp/shared/translations/locale/en-US.po @@ -28,9 +28,6 @@ msgstr "Home" msgid "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams." msgstr "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams." -msgid "Toggle collapsed menu" -msgstr "Toggle collapsed menu" - msgid "User profile menu" msgstr "User profile menu" diff --git a/application/back-office/WebApp/shared/translations/locale/nl-NL.po b/application/back-office/WebApp/shared/translations/locale/nl-NL.po index 4fc70ded6..a54dba2b7 100644 --- a/application/back-office/WebApp/shared/translations/locale/nl-NL.po +++ b/application/back-office/WebApp/shared/translations/locale/nl-NL.po @@ -28,9 +28,6 @@ msgstr "Home" msgid "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams." msgstr "Beheer huurders, bekijk systeemgegevens, zie uitzonderingen en voer diverse taken uit voor operationele en ondersteuningsteams." -msgid "Toggle collapsed menu" -msgstr "Ingeklapt menu wisselen" - msgid "User profile menu" msgstr "Gebruikersprofielmenu" From b5771f8915913de3ca99b39168f812ac43d3dcfd Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 23 Jul 2025 15:31:52 +0200 Subject: [PATCH 13/26] Restructure side menu into mobile menu and side menu, move to subfolder --- .../WebApp/routes/admin/account/index.tsx | 2 +- .../WebApp/routes/admin/index.tsx | 2 +- .../WebApp/routes/admin/users/index.tsx | 2 +- .../WebApp/rsbuild.config.ts | 2 +- .../MobileMenu.tsx} | 123 ++++-------------- .../sideMenu/NavigationMenuItems.tsx | 49 +++++++ .../components/sideMenu/SharedSideMenu.tsx | 24 ++++ 7 files changed, 100 insertions(+), 104 deletions(-) rename application/account-management/WebApp/shared/components/{SharedSideMenu.tsx => sideMenu/MobileMenu.tsx} (74%) create mode 100644 application/account-management/WebApp/shared/components/sideMenu/NavigationMenuItems.tsx create mode 100644 application/account-management/WebApp/shared/components/sideMenu/SharedSideMenu.tsx diff --git a/application/account-management/WebApp/routes/admin/account/index.tsx b/application/account-management/WebApp/routes/admin/account/index.tsx index 16f761133..95235bcce 100644 --- a/application/account-management/WebApp/routes/admin/account/index.tsx +++ b/application/account-management/WebApp/routes/admin/account/index.tsx @@ -1,4 +1,4 @@ -import SharedSideMenu from "@/shared/components/SharedSideMenu"; +import SharedSideMenu from "@/shared/components/sideMenu/SharedSideMenu"; import { TopMenu } from "@/shared/components/topMenu"; import logoWrap from "@/shared/images/logo-wrap.svg"; import { UserRole, api } from "@/shared/lib/api/client"; diff --git a/application/account-management/WebApp/routes/admin/index.tsx b/application/account-management/WebApp/routes/admin/index.tsx index 0e859d8f6..0c9ccc970 100644 --- a/application/account-management/WebApp/routes/admin/index.tsx +++ b/application/account-management/WebApp/routes/admin/index.tsx @@ -1,4 +1,4 @@ -import SharedSideMenu from "@/shared/components/SharedSideMenu"; +import SharedSideMenu from "@/shared/components/sideMenu/SharedSideMenu"; import { TopMenu } from "@/shared/components/topMenu"; import { UserStatus, api } from "@/shared/lib/api/client"; import { t } from "@lingui/core/macro"; diff --git a/application/account-management/WebApp/routes/admin/users/index.tsx b/application/account-management/WebApp/routes/admin/users/index.tsx index 4b1a1cc59..d9e13ffc6 100644 --- a/application/account-management/WebApp/routes/admin/users/index.tsx +++ b/application/account-management/WebApp/routes/admin/users/index.tsx @@ -1,4 +1,4 @@ -import SharedSideMenu from "@/shared/components/SharedSideMenu"; +import SharedSideMenu from "@/shared/components/sideMenu/SharedSideMenu"; import { TopMenu } from "@/shared/components/topMenu"; import { SortOrder, SortableUserProperties, UserRole, UserStatus, api, type components } from "@/shared/lib/api/client"; import { Trans } from "@lingui/react/macro"; diff --git a/application/account-management/WebApp/rsbuild.config.ts b/application/account-management/WebApp/rsbuild.config.ts index 16607231e..576f8e697 100644 --- a/application/account-management/WebApp/rsbuild.config.ts +++ b/application/account-management/WebApp/rsbuild.config.ts @@ -23,7 +23,7 @@ export default defineConfig({ exposes: { "./AvatarButton": "./shared/components/AvatarButton.tsx", "./SupportButton": "./shared/components/support/SupportButton.tsx", - "./SharedSideMenu": "./shared/components/SharedSideMenu.tsx", + "./SharedSideMenu": "./shared/components/sideMenu/SharedSideMenu.tsx", "./translations/en-US": "./shared/translations/locale/en-US.ts", "./translations/da-DK": "./shared/translations/locale/da-DK.ts", "./translations/nl-NL": "./shared/translations/locale/nl-NL.ts" diff --git a/application/account-management/WebApp/shared/components/SharedSideMenu.tsx b/application/account-management/WebApp/shared/components/sideMenu/MobileMenu.tsx similarity index 74% rename from application/account-management/WebApp/shared/components/SharedSideMenu.tsx rename to application/account-management/WebApp/shared/components/sideMenu/MobileMenu.tsx index fede48273..273f11ca2 100644 --- a/application/account-management/WebApp/shared/components/SharedSideMenu.tsx +++ b/application/account-management/WebApp/shared/components/sideMenu/MobileMenu.tsx @@ -10,42 +10,28 @@ import localeMap from "@repo/infrastructure/translations/i18n.config.json"; import { Avatar } from "@repo/ui/components/Avatar"; import { Button } from "@repo/ui/components/Button"; import { Menu, MenuItem, MenuTrigger } from "@repo/ui/components/Menu"; -import { FederatedMenuButton, SideMenu, SideMenuSeparator, overlayContext } from "@repo/ui/components/SideMenu"; +import { SideMenuSeparator, overlayContext } from "@repo/ui/components/SideMenu"; import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; import { useQueryClient } from "@tanstack/react-query"; -import { - BoxIcon, - CheckIcon, - CircleUserIcon, - GlobeIcon, - HomeIcon, - LogOutIcon, - MailQuestion, - UserIcon, - UsersIcon -} from "lucide-react"; +import { CheckIcon, GlobeIcon, LogOutIcon, MailQuestion, UserIcon } from "lucide-react"; import { useContext, useState } from "react"; -import { SupportDialog } from "./support/SupportDialog"; -import UserProfileModal from "./userModals/UserProfileModal"; -import "@repo/ui/tailwind.css"; +import { SupportDialog } from "../support/SupportDialog"; +import UserProfileModal from "../userModals/UserProfileModal"; +import { NavigationMenuItems } from "./NavigationMenuItems"; +import type { SharedSideMenuProps } from "./SharedSideMenu"; -type SharedSideMenuProps = { - currentSystem: "account-management" | "back-office"; // Add your self-contained system here -}; - -export default function SharedSideMenu({ currentSystem }: Readonly) { +// Mobile menu header section with user profile and settings +function MobileMenuHeader() { const userInfo = useUserInfo(); const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); const queryClient = useQueryClient(); const { i18n } = useLingui(); + const overlayCtx = useContext(overlayContext); const currentLocale = i18n.locale as Locale; const locales = Object.keys(localeMap) as Locale[]; const currentLocaleLabel = localeMap[currentLocale].label; - // Access mobile menu overlay context to close menu when needed - const overlayCtx = useContext(overlayContext); - const logoutMutation = api.useMutation("post", "/api/account-management/authentication/logout", { onMutate: async () => { await queryClient.cancelQueries(); @@ -59,8 +45,8 @@ export default function SharedSideMenu({ currentSystem }: Readonly + return ( +
{/* User Profile Section */}
{/* User Profile */} @@ -194,6 +180,17 @@ export default function SharedSideMenu({ currentSystem }: Readonly
+ +
+ ); +} + +// Complete mobile menu including header and navigation +export function MobileMenu({ currentSystem }: Readonly<{ currentSystem: SharedSideMenuProps["currentSystem"] }>) { + return ( +
+ + {/* Divider */}
@@ -202,85 +199,11 @@ export default function SharedSideMenu({ currentSystem }: Readonly Navigation - - - - Organization - - - - - - Back Office - - +
{/* Spacer to push content up */}
); - - return ( - <> - - - - - Organization - - - - - - - Back Office - - - - - - - - ); } diff --git a/application/account-management/WebApp/shared/components/sideMenu/NavigationMenuItems.tsx b/application/account-management/WebApp/shared/components/sideMenu/NavigationMenuItems.tsx new file mode 100644 index 000000000..de04a2461 --- /dev/null +++ b/application/account-management/WebApp/shared/components/sideMenu/NavigationMenuItems.tsx @@ -0,0 +1,49 @@ +import { t } from "@lingui/core/macro"; +import { Trans } from "@lingui/react/macro"; +import { FederatedMenuButton, SideMenuSeparator } from "@repo/ui/components/SideMenu"; +import { BoxIcon, CircleUserIcon, HomeIcon, UsersIcon } from "lucide-react"; +import type { SharedSideMenuProps } from "./SharedSideMenu"; + +// Navigation items shared between mobile and desktop menus +export function NavigationMenuItems({ + currentSystem +}: Readonly<{ currentSystem: SharedSideMenuProps["currentSystem"] }>) { + return ( + <> + + + + Organization + + + + + + + Back Office + + + + + ); +} diff --git a/application/account-management/WebApp/shared/components/sideMenu/SharedSideMenu.tsx b/application/account-management/WebApp/shared/components/sideMenu/SharedSideMenu.tsx new file mode 100644 index 000000000..5d539db81 --- /dev/null +++ b/application/account-management/WebApp/shared/components/sideMenu/SharedSideMenu.tsx @@ -0,0 +1,24 @@ +import { t } from "@lingui/core/macro"; +import { useUserInfo } from "@repo/infrastructure/auth/hooks"; +import { SideMenu } from "@repo/ui/components/SideMenu"; +import { MobileMenu } from "./MobileMenu"; +import { NavigationMenuItems } from "./NavigationMenuItems"; +import "@repo/ui/tailwind.css"; + +export type SharedSideMenuProps = { + currentSystem: "account-management" | "back-office"; // Add your self-contained system here +}; + +export default function SharedSideMenu({ currentSystem }: Readonly) { + const userInfo = useUserInfo(); + + return ( + } + tenantName={userInfo?.tenantName} + > + + + ); +} From 23df6797811f162684b835bba1d0bdeeef8a8d1a Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 23 Jul 2025 15:58:55 +0200 Subject: [PATCH 14/26] Restructure federated modules into dedicated top-level directory for better visibility --- .../components => federated-modules}/AvatarButton.tsx | 2 +- .../sideMenu/FederatedSideMenu.tsx} | 4 ++-- .../sideMenu/MobileMenu.tsx | 4 ++-- .../sideMenu/NavigationMenuItems.tsx | 4 ++-- .../support/SupportButton.tsx | 0 .../support/SupportDialog.tsx | 0 .../userModals/UserProfileModal.tsx | 0 .../WebApp/routes/admin/account/index.tsx | 4 ++-- .../account-management/WebApp/routes/admin/index.tsx | 4 ++-- .../account-management/WebApp/routes/admin/users/index.tsx | 4 ++-- application/account-management/WebApp/rsbuild.config.ts | 6 +++--- .../WebApp/shared/components/topMenu/index.tsx | 4 ++-- .../WebApp/shared/layouts/HorizontalHeroLayout.tsx | 2 +- application/back-office/WebApp/routes/back-office/index.tsx | 4 ++-- .../build/module-federation-types/account-management.d.ts | 2 +- 15 files changed, 22 insertions(+), 22 deletions(-) rename application/account-management/WebApp/{shared/components => federated-modules}/AvatarButton.tsx (98%) rename application/account-management/WebApp/{shared/components/sideMenu/SharedSideMenu.tsx => federated-modules/sideMenu/FederatedSideMenu.tsx} (83%) rename application/account-management/WebApp/{shared/components => federated-modules}/sideMenu/MobileMenu.tsx (98%) rename application/account-management/WebApp/{shared/components => federated-modules}/sideMenu/NavigationMenuItems.tsx (90%) rename application/account-management/WebApp/{shared/components => federated-modules}/support/SupportButton.tsx (100%) rename application/account-management/WebApp/{shared/components => federated-modules}/support/SupportDialog.tsx (100%) rename application/account-management/WebApp/{shared/components => federated-modules}/userModals/UserProfileModal.tsx (100%) diff --git a/application/account-management/WebApp/shared/components/AvatarButton.tsx b/application/account-management/WebApp/federated-modules/AvatarButton.tsx similarity index 98% rename from application/account-management/WebApp/shared/components/AvatarButton.tsx rename to application/account-management/WebApp/federated-modules/AvatarButton.tsx index c519670f6..9f41c1c29 100644 --- a/application/account-management/WebApp/shared/components/AvatarButton.tsx +++ b/application/account-management/WebApp/federated-modules/AvatarButton.tsx @@ -1,4 +1,4 @@ -import UserProfileModal from "@/shared/components/userModals/UserProfileModal"; +import UserProfileModal from "@/federated-modules/userModals/UserProfileModal"; import { api } from "@/shared/lib/api/client"; import { Trans } from "@lingui/react/macro"; import { loginPath } from "@repo/infrastructure/auth/constants"; diff --git a/application/account-management/WebApp/shared/components/sideMenu/SharedSideMenu.tsx b/application/account-management/WebApp/federated-modules/sideMenu/FederatedSideMenu.tsx similarity index 83% rename from application/account-management/WebApp/shared/components/sideMenu/SharedSideMenu.tsx rename to application/account-management/WebApp/federated-modules/sideMenu/FederatedSideMenu.tsx index 5d539db81..cfa5327b4 100644 --- a/application/account-management/WebApp/shared/components/sideMenu/SharedSideMenu.tsx +++ b/application/account-management/WebApp/federated-modules/sideMenu/FederatedSideMenu.tsx @@ -5,11 +5,11 @@ import { MobileMenu } from "./MobileMenu"; import { NavigationMenuItems } from "./NavigationMenuItems"; import "@repo/ui/tailwind.css"; -export type SharedSideMenuProps = { +export type FederatedSideMenuProps = { currentSystem: "account-management" | "back-office"; // Add your self-contained system here }; -export default function SharedSideMenu({ currentSystem }: Readonly) { +export default function FederatedSideMenu({ currentSystem }: Readonly) { const userInfo = useUserInfo(); return ( diff --git a/application/account-management/WebApp/shared/components/sideMenu/MobileMenu.tsx b/application/account-management/WebApp/federated-modules/sideMenu/MobileMenu.tsx similarity index 98% rename from application/account-management/WebApp/shared/components/sideMenu/MobileMenu.tsx rename to application/account-management/WebApp/federated-modules/sideMenu/MobileMenu.tsx index 273f11ca2..0de637a6a 100644 --- a/application/account-management/WebApp/shared/components/sideMenu/MobileMenu.tsx +++ b/application/account-management/WebApp/federated-modules/sideMenu/MobileMenu.tsx @@ -17,8 +17,8 @@ import { CheckIcon, GlobeIcon, LogOutIcon, MailQuestion, UserIcon } from "lucide import { useContext, useState } from "react"; import { SupportDialog } from "../support/SupportDialog"; import UserProfileModal from "../userModals/UserProfileModal"; +import type { FederatedSideMenuProps } from "./FederatedSideMenu"; import { NavigationMenuItems } from "./NavigationMenuItems"; -import type { SharedSideMenuProps } from "./SharedSideMenu"; // Mobile menu header section with user profile and settings function MobileMenuHeader() { @@ -186,7 +186,7 @@ function MobileMenuHeader() { } // Complete mobile menu including header and navigation -export function MobileMenu({ currentSystem }: Readonly<{ currentSystem: SharedSideMenuProps["currentSystem"] }>) { +export function MobileMenu({ currentSystem }: Readonly<{ currentSystem: FederatedSideMenuProps["currentSystem"] }>) { return (
diff --git a/application/account-management/WebApp/shared/components/sideMenu/NavigationMenuItems.tsx b/application/account-management/WebApp/federated-modules/sideMenu/NavigationMenuItems.tsx similarity index 90% rename from application/account-management/WebApp/shared/components/sideMenu/NavigationMenuItems.tsx rename to application/account-management/WebApp/federated-modules/sideMenu/NavigationMenuItems.tsx index de04a2461..de67971ca 100644 --- a/application/account-management/WebApp/shared/components/sideMenu/NavigationMenuItems.tsx +++ b/application/account-management/WebApp/federated-modules/sideMenu/NavigationMenuItems.tsx @@ -2,12 +2,12 @@ import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { FederatedMenuButton, SideMenuSeparator } from "@repo/ui/components/SideMenu"; import { BoxIcon, CircleUserIcon, HomeIcon, UsersIcon } from "lucide-react"; -import type { SharedSideMenuProps } from "./SharedSideMenu"; +import type { FederatedSideMenuProps } from "./FederatedSideMenu"; // Navigation items shared between mobile and desktop menus export function NavigationMenuItems({ currentSystem -}: Readonly<{ currentSystem: SharedSideMenuProps["currentSystem"] }>) { +}: Readonly<{ currentSystem: FederatedSideMenuProps["currentSystem"] }>) { return ( <> - + - + }>

{userInfo?.firstName ? Welcome home, {userInfo.firstName} : Welcome home}

diff --git a/application/account-management/WebApp/routes/admin/users/index.tsx b/application/account-management/WebApp/routes/admin/users/index.tsx index d9e13ffc6..eb0ff777e 100644 --- a/application/account-management/WebApp/routes/admin/users/index.tsx +++ b/application/account-management/WebApp/routes/admin/users/index.tsx @@ -1,4 +1,4 @@ -import SharedSideMenu from "@/shared/components/sideMenu/SharedSideMenu"; +import FederatedSideMenu from "@/federated-modules/sideMenu/FederatedSideMenu"; import { TopMenu } from "@/shared/components/topMenu"; import { SortOrder, SortableUserProperties, UserRole, UserStatus, api, type components } from "@/shared/lib/api/client"; import { Trans } from "@lingui/react/macro"; @@ -104,7 +104,7 @@ export default function UsersPage() { return ( <> - + - + }>

Welcome to the Back Office diff --git a/application/shared-webapp/build/module-federation-types/account-management.d.ts b/application/shared-webapp/build/module-federation-types/account-management.d.ts index bd6cb068a..8d0cc0dff 100644 --- a/application/shared-webapp/build/module-federation-types/account-management.d.ts +++ b/application/shared-webapp/build/module-federation-types/account-management.d.ts @@ -6,7 +6,7 @@ declare module "account-management/AvatarButton" { declare module "account-management/SupportButton" { export default ReactNode; } -declare module "account-management/SharedSideMenu" { +declare module "account-management/FederatedSideMenu" { export default ReactNode; } declare module "account-management/translations/en-US" { From f3d4cea0a73c7d3b906c463ac924c0bda375c356 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 23 Jul 2025 18:10:35 +0200 Subject: [PATCH 15/26] Create FederatedTopMenu placeholder --- .../topMenu/FederatedTopMenu.tsx | 10 +++++ .../WebApp/rsbuild.config.ts | 1 + .../shared/components/topMenu/index.tsx | 41 ++++++++++--------- .../account-management.d.ts | 3 ++ 4 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx diff --git a/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx b/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx new file mode 100644 index 000000000..2e31f1729 --- /dev/null +++ b/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx @@ -0,0 +1,10 @@ +import type { ReactNode } from "react"; +import "@repo/ui/tailwind.css"; + +interface FederatedTopMenuProps { + children?: ReactNode; +} + +export default function FederatedTopMenu({ children }: Readonly) { + return ; +} diff --git a/application/account-management/WebApp/rsbuild.config.ts b/application/account-management/WebApp/rsbuild.config.ts index f9313b158..ad4188d80 100644 --- a/application/account-management/WebApp/rsbuild.config.ts +++ b/application/account-management/WebApp/rsbuild.config.ts @@ -24,6 +24,7 @@ export default defineConfig({ "./AvatarButton": "./federated-modules/AvatarButton.tsx", "./SupportButton": "./federated-modules/support/SupportButton.tsx", "./FederatedSideMenu": "./federated-modules/sideMenu/FederatedSideMenu.tsx", + "./FederatedTopMenu": "./federated-modules/topMenu/FederatedTopMenu.tsx", "./translations/en-US": "./shared/translations/locale/en-US.ts", "./translations/da-DK": "./shared/translations/locale/da-DK.ts", "./translations/nl-NL": "./shared/translations/locale/nl-NL.ts" diff --git a/application/back-office/WebApp/shared/components/topMenu/index.tsx b/application/back-office/WebApp/shared/components/topMenu/index.tsx index 97692589b..a294432f7 100644 --- a/application/back-office/WebApp/shared/components/topMenu/index.tsx +++ b/application/back-office/WebApp/shared/components/topMenu/index.tsx @@ -7,6 +7,7 @@ import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; import type { ReactNode } from "react"; import { Suspense, lazy } from "react"; +const FederatedTopMenu = lazy(() => import("account-management/FederatedTopMenu")); const AvatarButton = lazy(() => import("account-management/AvatarButton")); const SupportButton = lazy(() => import("account-management/SupportButton")); @@ -16,25 +17,27 @@ interface TopMenuProps { export function TopMenu({ children }: Readonly) { return ( - +

+ + ); } diff --git a/application/shared-webapp/build/module-federation-types/account-management.d.ts b/application/shared-webapp/build/module-federation-types/account-management.d.ts index 8d0cc0dff..68e2055d6 100644 --- a/application/shared-webapp/build/module-federation-types/account-management.d.ts +++ b/application/shared-webapp/build/module-federation-types/account-management.d.ts @@ -9,6 +9,9 @@ declare module "account-management/SupportButton" { declare module "account-management/FederatedSideMenu" { export default ReactNode; } +declare module "account-management/FederatedTopMenu" { + export default ReactNode; +} declare module "account-management/translations/en-US" { import type { Messages } from "@lingui/core"; export const messages: Messages; From d0f13ff3827c39f16348a2e2e8cf2ef87f4f9f88 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 23 Jul 2025 18:22:01 +0200 Subject: [PATCH 16/26] Add AvatarButton to new federation module top menu --- .../{ => topMenu}/AvatarButton.tsx | 0 .../topMenu/FederatedTopMenu.tsx | 18 +++++++++++++-- .../WebApp/rsbuild.config.ts | 1 - .../shared/components/topMenu/index.tsx | 2 +- .../shared/components/topMenu/index.tsx | 23 ++++++++----------- .../shared/translations/locale/da-DK.po | 3 --- .../shared/translations/locale/en-US.po | 3 --- .../shared/translations/locale/nl-NL.po | 3 --- .../account-management.d.ts | 3 --- 9 files changed, 27 insertions(+), 29 deletions(-) rename application/account-management/WebApp/federated-modules/{ => topMenu}/AvatarButton.tsx (100%) diff --git a/application/account-management/WebApp/federated-modules/AvatarButton.tsx b/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx similarity index 100% rename from application/account-management/WebApp/federated-modules/AvatarButton.tsx rename to application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx diff --git a/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx b/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx index 2e31f1729..35b272460 100644 --- a/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx +++ b/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx @@ -1,10 +1,24 @@ +import { t } from "@lingui/core/macro"; import type { ReactNode } from "react"; +import AvatarButton from "./AvatarButton"; import "@repo/ui/tailwind.css"; interface FederatedTopMenuProps { children?: ReactNode; + rightContent?: ReactNode; } -export default function FederatedTopMenu({ children }: Readonly) { - return ; +export default function FederatedTopMenu({ children, rightContent }: Readonly) { + return ( + + ); } + +// Re-export AvatarButton for backward compatibility +export { default as AvatarButton } from "./AvatarButton"; diff --git a/application/account-management/WebApp/rsbuild.config.ts b/application/account-management/WebApp/rsbuild.config.ts index ad4188d80..9ed865960 100644 --- a/application/account-management/WebApp/rsbuild.config.ts +++ b/application/account-management/WebApp/rsbuild.config.ts @@ -21,7 +21,6 @@ export default defineConfig({ DevelopmentServerPlugin({ port: 9101 }), ModuleFederationPlugin({ exposes: { - "./AvatarButton": "./federated-modules/AvatarButton.tsx", "./SupportButton": "./federated-modules/support/SupportButton.tsx", "./FederatedSideMenu": "./federated-modules/sideMenu/FederatedSideMenu.tsx", "./FederatedTopMenu": "./federated-modules/topMenu/FederatedTopMenu.tsx", diff --git a/application/account-management/WebApp/shared/components/topMenu/index.tsx b/application/account-management/WebApp/shared/components/topMenu/index.tsx index 4f09fde26..79370fe99 100644 --- a/application/account-management/WebApp/shared/components/topMenu/index.tsx +++ b/application/account-management/WebApp/shared/components/topMenu/index.tsx @@ -1,4 +1,4 @@ -import AvatarButton from "@/federated-modules/AvatarButton"; +import { AvatarButton } from "@/federated-modules/topMenu/FederatedTopMenu"; import SupportButton from "@/federated-modules/support/SupportButton"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; diff --git a/application/back-office/WebApp/shared/components/topMenu/index.tsx b/application/back-office/WebApp/shared/components/topMenu/index.tsx index a294432f7..b7697d159 100644 --- a/application/back-office/WebApp/shared/components/topMenu/index.tsx +++ b/application/back-office/WebApp/shared/components/topMenu/index.tsx @@ -8,7 +8,6 @@ import type { ReactNode } from "react"; import { Suspense, lazy } from "react"; const FederatedTopMenu = lazy(() => import("account-management/FederatedTopMenu")); -const AvatarButton = lazy(() => import("account-management/AvatarButton")); const SupportButton = lazy(() => import("account-management/SupportButton")); interface TopMenuProps { @@ -18,14 +17,8 @@ interface TopMenuProps { export function TopMenu({ children }: Readonly) { return ( }> - - - - Home - - {children} - -
+ }> @@ -33,10 +26,14 @@ export function TopMenu({ children }: Readonly) { - }> - - -
+ } + > + + + Home + + {children} +
); diff --git a/application/back-office/WebApp/shared/translations/locale/da-DK.po b/application/back-office/WebApp/shared/translations/locale/da-DK.po index 1d2719613..316efc738 100644 --- a/application/back-office/WebApp/shared/translations/locale/da-DK.po +++ b/application/back-office/WebApp/shared/translations/locale/da-DK.po @@ -28,8 +28,5 @@ msgstr "Hjem" msgid "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams." msgstr "Administrer lejere, se systemdata, se undtagelser og udfør forskellige opgaver for drifts- og supportteams." -msgid "User profile menu" -msgstr "Brugerprofilmenu" - msgid "Welcome to the Back Office" msgstr "Velkommen til Back Office" diff --git a/application/back-office/WebApp/shared/translations/locale/en-US.po b/application/back-office/WebApp/shared/translations/locale/en-US.po index 668e855c8..4c60af5e8 100644 --- a/application/back-office/WebApp/shared/translations/locale/en-US.po +++ b/application/back-office/WebApp/shared/translations/locale/en-US.po @@ -28,8 +28,5 @@ msgstr "Home" msgid "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams." msgstr "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams." -msgid "User profile menu" -msgstr "User profile menu" - msgid "Welcome to the Back Office" msgstr "Welcome to the Back Office" diff --git a/application/back-office/WebApp/shared/translations/locale/nl-NL.po b/application/back-office/WebApp/shared/translations/locale/nl-NL.po index a54dba2b7..adf9f0076 100644 --- a/application/back-office/WebApp/shared/translations/locale/nl-NL.po +++ b/application/back-office/WebApp/shared/translations/locale/nl-NL.po @@ -28,8 +28,5 @@ msgstr "Home" msgid "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams." msgstr "Beheer huurders, bekijk systeemgegevens, zie uitzonderingen en voer diverse taken uit voor operationele en ondersteuningsteams." -msgid "User profile menu" -msgstr "Gebruikersprofielmenu" - msgid "Welcome to the Back Office" msgstr "Welkom bij de Backoffice" diff --git a/application/shared-webapp/build/module-federation-types/account-management.d.ts b/application/shared-webapp/build/module-federation-types/account-management.d.ts index 68e2055d6..05b9d185a 100644 --- a/application/shared-webapp/build/module-federation-types/account-management.d.ts +++ b/application/shared-webapp/build/module-federation-types/account-management.d.ts @@ -1,8 +1,5 @@ // This file was auto-generated by the ModuleFederationPlugin -declare module "account-management/AvatarButton" { - export default ReactNode; -} declare module "account-management/SupportButton" { export default ReactNode; } From c5b63ac3d7fbb5b85335943dfb33d054dd0cd430 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 23 Jul 2025 20:13:31 +0200 Subject: [PATCH 17/26] Create FederatedTopMenu with theme, support, language, and user profile options --- .../topMenu/FederatedTopMenu.tsx | 17 ++- .../topMenu/LocaleSwitcher.tsx | 86 +++++++++++ .../topMenu/ThemeModeSelector.tsx | 137 ++++++++++++++++++ .../shared/components/topMenu/index.tsx | 25 +--- .../shared/translations/locale/da-DK.po | 9 ++ .../shared/translations/locale/en-US.po | 9 ++ .../shared/translations/locale/nl-NL.po | 9 ++ .../shared/components/topMenu/index.tsx | 17 +-- .../shared/translations/locale/da-DK.po | 9 -- .../shared/translations/locale/en-US.po | 9 -- .../shared/translations/locale/nl-NL.po | 9 -- 11 files changed, 269 insertions(+), 67 deletions(-) create mode 100644 application/account-management/WebApp/federated-modules/topMenu/LocaleSwitcher.tsx create mode 100644 application/account-management/WebApp/federated-modules/topMenu/ThemeModeSelector.tsx diff --git a/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx b/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx index 35b272460..7368f8a3c 100644 --- a/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx +++ b/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx @@ -1,19 +1,30 @@ import { t } from "@lingui/core/macro"; +import { Button } from "@repo/ui/components/Button"; import type { ReactNode } from "react"; +import { Suspense, lazy } from "react"; import AvatarButton from "./AvatarButton"; +import LocaleSwitcher from "./LocaleSwitcher"; +import ThemeModeSelector from "./ThemeModeSelector"; import "@repo/ui/tailwind.css"; +const SupportButton = lazy(() => import("../support/SupportButton")); + interface FederatedTopMenuProps { children?: ReactNode; - rightContent?: ReactNode; } -export default function FederatedTopMenu({ children, rightContent }: Readonly) { +export default function FederatedTopMenu({ children }: Readonly) { return ( diff --git a/application/account-management/WebApp/federated-modules/topMenu/LocaleSwitcher.tsx b/application/account-management/WebApp/federated-modules/topMenu/LocaleSwitcher.tsx new file mode 100644 index 000000000..32fdfb4e1 --- /dev/null +++ b/application/account-management/WebApp/federated-modules/topMenu/LocaleSwitcher.tsx @@ -0,0 +1,86 @@ +import { t } from "@lingui/core/macro"; +import type { Key } from "@react-types/shared"; +import { enhancedFetch } from "@repo/infrastructure/http/httpClient"; +import type { Locale } from "@repo/infrastructure/translations/TranslationContext"; +import localeMap from "@repo/infrastructure/translations/i18n.config.json"; +import { Button } from "@repo/ui/components/Button"; +import { Menu, MenuItem, MenuTrigger } from "@repo/ui/components/Menu"; +import { Tooltip, TooltipTrigger } from "@repo/ui/components/Tooltip"; +import { CheckIcon, GlobeIcon } from "lucide-react"; +import { useEffect, useState } from "react"; + +const PREFERRED_LOCALE_KEY = "preferred-locale"; + +const locales = Object.entries(localeMap).map(([id, info]) => ({ + id: id as Locale, + label: info.label +})); + +async function updateLocaleOnBackend(locale: Locale) { + try { + const response = await enhancedFetch("/api/account-management/users/me/change-locale", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ Locale: locale }) + }); + + return response.ok || response.status === 401; + } catch { + return true; // Continue even if API call fails + } +} + +export default function LocaleSwitcher() { + const [currentLocale, setCurrentLocale] = useState("en-US"); + + useEffect(() => { + // Get current locale from document or localStorage + const htmlLang = document.documentElement.lang as Locale; + const savedLocale = localStorage.getItem(PREFERRED_LOCALE_KEY) as Locale; + + if (savedLocale && locales.some(l => l.id === savedLocale)) { + setCurrentLocale(savedLocale); + } else if (htmlLang && locales.some(l => l.id === htmlLang)) { + setCurrentLocale(htmlLang); + } + }, []); + + const handleLocaleChange = async (key: Key) => { + const locale = key.toString() as Locale; + if (locale !== currentLocale) { + // Save to localStorage + localStorage.setItem(PREFERRED_LOCALE_KEY, locale); + + // Try to update backend + await updateLocaleOnBackend(locale); + + // Reload page to apply new locale + window.location.reload(); + } + }; + + const menuContent = ( + + + + {locales.map((locale) => ( + +
+ {locale.label} + {locale.id === currentLocale && } +
+
+ ))} +
+
+ ); + + return ( + + {menuContent} + {t`Change language`} + + ); +} \ No newline at end of file diff --git a/application/account-management/WebApp/federated-modules/topMenu/ThemeModeSelector.tsx b/application/account-management/WebApp/federated-modules/topMenu/ThemeModeSelector.tsx new file mode 100644 index 000000000..acd8a1110 --- /dev/null +++ b/application/account-management/WebApp/federated-modules/topMenu/ThemeModeSelector.tsx @@ -0,0 +1,137 @@ +import { t } from "@lingui/core/macro"; +import { Trans } from "@lingui/react/macro"; +import type { Key } from "@react-types/shared"; +import { Button } from "@repo/ui/components/Button"; +import { Menu, MenuItem, MenuTrigger } from "@repo/ui/components/Menu"; +import { Tooltip, TooltipTrigger } from "@repo/ui/components/Tooltip"; +import { CheckIcon, MoonIcon, MoonStarIcon, SunIcon, SunMoonIcon } from "lucide-react"; +import { useEffect, useState } from "react"; + +const THEME_MODE_KEY = "theme-mode"; + +enum ThemeMode { + System = "system", + Light = "light", + Dark = "dark" +} + +export default function ThemeModeSelector() { + const [themeMode, setThemeModeState] = useState(ThemeMode.System); + + useEffect(() => { + // Read initial theme mode from localStorage + const savedMode = localStorage.getItem(THEME_MODE_KEY) as ThemeMode; + const initialMode = savedMode && Object.values(ThemeMode).includes(savedMode) ? savedMode : ThemeMode.System; + setThemeModeState(initialMode); + + // Apply initial theme + const root = document.documentElement; + root.classList.remove("light", "dark"); + + if (initialMode === ThemeMode.Dark) { + root.classList.add("dark"); + root.style.colorScheme = "dark"; + } else if (initialMode === ThemeMode.Light) { + root.classList.add("light"); + root.style.colorScheme = "light"; + } else { + // System mode - check system preference + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + if (prefersDark) { + root.classList.add("dark"); + root.style.colorScheme = "dark"; + } else { + root.classList.add("light"); + root.style.colorScheme = "light"; + } + } + }, []); + + const handleThemeChange = (key: Key) => { + const newMode = key as ThemeMode; + setThemeModeState(newMode); + localStorage.setItem(THEME_MODE_KEY, newMode); + + // Apply theme to DOM - matching the original implementation + const root = document.documentElement; + + // Remove both light and dark classes first + root.classList.remove("light", "dark"); + + if (newMode === ThemeMode.Dark) { + root.classList.add("dark"); + root.style.colorScheme = "dark"; + } else if (newMode === ThemeMode.Light) { + root.classList.add("light"); + root.style.colorScheme = "light"; + } else { + // System mode - check system preference + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + if (prefersDark) { + root.classList.add("dark"); + root.style.colorScheme = "dark"; + } else { + root.classList.add("light"); + root.style.colorScheme = "light"; + } + } + + // Dispatch event to notify other components + window.dispatchEvent(new CustomEvent("theme-mode-changed", { detail: newMode })); + }; + + const getThemeIcon = () => { + switch (themeMode) { + case ThemeMode.Dark: + return ; + case ThemeMode.Light: + return ; + default: + // For system mode, show icon based on actual system preference + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + return prefersDark ? : ; + } + }; + + const menuContent = ( + + + + +
+ {window.matchMedia("(prefers-color-scheme: dark)").matches ? ( + + ) : ( + + )} + System + {themeMode === ThemeMode.System && } +
+
+ +
+ + Light + {themeMode === ThemeMode.Light && } +
+
+ +
+ + Dark + {themeMode === ThemeMode.Dark && } +
+
+
+
+ ); + + return ( + + {menuContent} + {t`Change theme`} + + ); +} \ No newline at end of file diff --git a/application/account-management/WebApp/shared/components/topMenu/index.tsx b/application/account-management/WebApp/shared/components/topMenu/index.tsx index 79370fe99..de1f6cb56 100644 --- a/application/account-management/WebApp/shared/components/topMenu/index.tsx +++ b/application/account-management/WebApp/shared/components/topMenu/index.tsx @@ -1,38 +1,21 @@ -import { AvatarButton } from "@/federated-modules/topMenu/FederatedTopMenu"; -import SupportButton from "@/federated-modules/support/SupportButton"; -import { t } from "@lingui/core/macro"; +import FederatedTopMenu from "@/federated-modules/topMenu/FederatedTopMenu"; import { Trans } from "@lingui/react/macro"; -import { LocaleSwitcher } from "@repo/infrastructure/translations/LocaleSwitcher"; import { Breadcrumb, Breadcrumbs } from "@repo/ui/components/Breadcrumbs"; -import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; import type { ReactNode } from "react"; interface TopMenuProps { children?: ReactNode; - sidePaneOpen?: boolean; } -export function TopMenu({ children, sidePaneOpen = false }: Readonly) { +export function TopMenu({ children }: Readonly) { return ( - + ); } diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po index 7b745efca..6fa0c2781 100644 --- a/application/account-management/WebApp/shared/translations/locale/da-DK.po +++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po @@ -142,6 +142,9 @@ msgstr "Oprettet" msgid "Danger zone" msgstr "Farezone" +msgid "Dark" +msgstr "Mørk" + msgid "Dashboard" msgstr "Dashboard" @@ -236,6 +239,9 @@ msgstr "Sprog" msgid "Last name" msgstr "Efternavn" +msgid "Light" +msgstr "Lys" + msgid "Log in" msgstr "Log ind" @@ -372,6 +378,9 @@ msgstr "Tilmeldingsbekræftelseskode" msgid "Success" msgstr "Succes" +msgid "System" +msgstr "System" + msgid "Terms of use" msgstr "Brugsvilkår" diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po index 95be918f2..848cfcc39 100644 --- a/application/account-management/WebApp/shared/translations/locale/en-US.po +++ b/application/account-management/WebApp/shared/translations/locale/en-US.po @@ -142,6 +142,9 @@ msgstr "Created" msgid "Danger zone" msgstr "Danger zone" +msgid "Dark" +msgstr "Dark" + msgid "Dashboard" msgstr "Dashboard" @@ -236,6 +239,9 @@ msgstr "Language" msgid "Last name" msgstr "Last name" +msgid "Light" +msgstr "Light" + msgid "Log in" msgstr "Log in" @@ -372,6 +378,9 @@ msgstr "Signup verification code" msgid "Success" msgstr "Success" +msgid "System" +msgstr "System" + msgid "Terms of use" msgstr "Terms of use" diff --git a/application/account-management/WebApp/shared/translations/locale/nl-NL.po b/application/account-management/WebApp/shared/translations/locale/nl-NL.po index 238a1f89a..4b754f812 100644 --- a/application/account-management/WebApp/shared/translations/locale/nl-NL.po +++ b/application/account-management/WebApp/shared/translations/locale/nl-NL.po @@ -142,6 +142,9 @@ msgstr "Aangemaakt" msgid "Danger zone" msgstr "Gevaarzone" +msgid "Dark" +msgstr "Donker" + msgid "Dashboard" msgstr "Dashboard" @@ -236,6 +239,9 @@ msgstr "Taal" msgid "Last name" msgstr "Achternaam" +msgid "Light" +msgstr "Licht" + msgid "Log in" msgstr "Inloggen" @@ -372,6 +378,9 @@ msgstr "Verificatiecode voor aanmelding" msgid "Success" msgstr "Succes" +msgid "System" +msgstr "Systeem" + msgid "Terms of use" msgstr "Gebruiksvoorwaarden" diff --git a/application/back-office/WebApp/shared/components/topMenu/index.tsx b/application/back-office/WebApp/shared/components/topMenu/index.tsx index b7697d159..f3c0eff8e 100644 --- a/application/back-office/WebApp/shared/components/topMenu/index.tsx +++ b/application/back-office/WebApp/shared/components/topMenu/index.tsx @@ -1,14 +1,9 @@ -import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; -import { LocaleSwitcher } from "@repo/infrastructure/translations/LocaleSwitcher"; import { Breadcrumb, Breadcrumbs } from "@repo/ui/components/Breadcrumbs"; -import { Button } from "@repo/ui/components/Button"; -import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; import type { ReactNode } from "react"; import { Suspense, lazy } from "react"; const FederatedTopMenu = lazy(() => import("account-management/FederatedTopMenu")); -const SupportButton = lazy(() => import("account-management/SupportButton")); interface TopMenuProps { children?: ReactNode; @@ -17,17 +12,7 @@ interface TopMenuProps { export function TopMenu({ children }: Readonly) { return ( }> - - - }> - - - - - } - > + Home diff --git a/application/back-office/WebApp/shared/translations/locale/da-DK.po b/application/back-office/WebApp/shared/translations/locale/da-DK.po index 316efc738..2f551a5f4 100644 --- a/application/back-office/WebApp/shared/translations/locale/da-DK.po +++ b/application/back-office/WebApp/shared/translations/locale/da-DK.po @@ -13,15 +13,6 @@ msgstr "" "Plural-Forms: \n" "X-Generator: @lingui/cli\n" -msgid "Change language" -msgstr "Skift sprog" - -msgid "Change theme" -msgstr "Skift tema" - -msgid "Contact support" -msgstr "Kontakt support" - msgid "Home" msgstr "Hjem" diff --git a/application/back-office/WebApp/shared/translations/locale/en-US.po b/application/back-office/WebApp/shared/translations/locale/en-US.po index 4c60af5e8..b64e6f282 100644 --- a/application/back-office/WebApp/shared/translations/locale/en-US.po +++ b/application/back-office/WebApp/shared/translations/locale/en-US.po @@ -13,15 +13,6 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" -msgid "Change language" -msgstr "Change language" - -msgid "Change theme" -msgstr "Change theme" - -msgid "Contact support" -msgstr "Contact support" - msgid "Home" msgstr "Home" diff --git a/application/back-office/WebApp/shared/translations/locale/nl-NL.po b/application/back-office/WebApp/shared/translations/locale/nl-NL.po index adf9f0076..c43b1e885 100644 --- a/application/back-office/WebApp/shared/translations/locale/nl-NL.po +++ b/application/back-office/WebApp/shared/translations/locale/nl-NL.po @@ -13,15 +13,6 @@ msgstr "" "Plural-Forms: \n" "X-Generator: @lingui/cli\n" -msgid "Change language" -msgstr "Taal wijzigen" - -msgid "Change theme" -msgstr "Thema wijzigen" - -msgid "Contact support" -msgstr "Ondersteuning contacteren" - msgid "Home" msgstr "Home" From d8a9e6f727cac8c280b6e0c61e452d231d337ef7 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 23 Jul 2025 22:52:49 +0200 Subject: [PATCH 18/26] Centralize federated components and fix language switching in mobile menu --- .../{topMenu => common}/LocaleSwitcher.tsx | 67 ++++++++-- .../{support => common}/SupportButton.tsx | 0 .../{support => common}/SupportDialog.tsx | 0 .../{topMenu => common}/ThemeModeSelector.tsx | 101 ++++++++++++--- .../UserProfileModal.tsx | 0 .../federated-modules/sideMenu/MobileMenu.tsx | 67 +++------- .../topMenu/AvatarButton.tsx | 2 +- .../topMenu/FederatedTopMenu.tsx | 6 +- .../(index)/-components/FeatureSection3.tsx | 5 +- .../(index)/-components/HeroSection.tsx | 5 +- .../WebApp/rsbuild.config.ts | 2 +- .../shared/layouts/HorizontalHeroLayout.tsx | 14 +- .../shared/translations/locale/da-DK.po | 6 +- .../shared/translations/locale/en-US.po | 6 +- .../shared/translations/locale/nl-NL.po | 6 +- .../translations/LocaleSwitcher.tsx | 77 ----------- .../ui/theme/ThemeModeSelector.tsx | 120 ------------------ 17 files changed, 179 insertions(+), 305 deletions(-) rename application/account-management/WebApp/federated-modules/{topMenu => common}/LocaleSwitcher.tsx (56%) rename application/account-management/WebApp/federated-modules/{support => common}/SupportButton.tsx (100%) rename application/account-management/WebApp/federated-modules/{support => common}/SupportDialog.tsx (100%) rename application/account-management/WebApp/federated-modules/{topMenu => common}/ThemeModeSelector.tsx (64%) rename application/account-management/WebApp/federated-modules/{userModals => common}/UserProfileModal.tsx (100%) delete mode 100644 application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx delete mode 100644 application/shared-webapp/ui/theme/ThemeModeSelector.tsx diff --git a/application/account-management/WebApp/federated-modules/topMenu/LocaleSwitcher.tsx b/application/account-management/WebApp/federated-modules/common/LocaleSwitcher.tsx similarity index 56% rename from application/account-management/WebApp/federated-modules/topMenu/LocaleSwitcher.tsx rename to application/account-management/WebApp/federated-modules/common/LocaleSwitcher.tsx index 32fdfb4e1..c4591d624 100644 --- a/application/account-management/WebApp/federated-modules/topMenu/LocaleSwitcher.tsx +++ b/application/account-management/WebApp/federated-modules/common/LocaleSwitcher.tsx @@ -1,5 +1,7 @@ import { t } from "@lingui/core/macro"; +import { Trans } from "@lingui/react/macro"; import type { Key } from "@react-types/shared"; +import { useIsAuthenticated } from "@repo/infrastructure/auth/hooks"; import { enhancedFetch } from "@repo/infrastructure/http/httpClient"; import type { Locale } from "@repo/infrastructure/translations/TranslationContext"; import localeMap from "@repo/infrastructure/translations/i18n.config.json"; @@ -23,24 +25,31 @@ async function updateLocaleOnBackend(locale: Locale) { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ Locale: locale }) }); - + return response.ok || response.status === 401; } catch { return true; // Continue even if API call fails } } -export default function LocaleSwitcher() { +export default function LocaleSwitcher({ + variant = "icon", + onAction +}: { + variant?: "icon" | "mobile-menu"; + onAction?: () => void; +} = {}) { const [currentLocale, setCurrentLocale] = useState("en-US"); + const isAuthenticated = useIsAuthenticated(); useEffect(() => { // Get current locale from document or localStorage const htmlLang = document.documentElement.lang as Locale; const savedLocale = localStorage.getItem(PREFERRED_LOCALE_KEY) as Locale; - - if (savedLocale && locales.some(l => l.id === savedLocale)) { + + if (savedLocale && locales.some((l) => l.id === savedLocale)) { setCurrentLocale(savedLocale); - } else if (htmlLang && locales.some(l => l.id === htmlLang)) { + } else if (htmlLang && locales.some((l) => l.id === htmlLang)) { setCurrentLocale(htmlLang); } }, []); @@ -48,17 +57,55 @@ export default function LocaleSwitcher() { const handleLocaleChange = async (key: Key) => { const locale = key.toString() as Locale; if (locale !== currentLocale) { + // Call onAction if provided (for closing mobile menu) + onAction?.(); + // Save to localStorage localStorage.setItem(PREFERRED_LOCALE_KEY, locale); - - // Try to update backend - await updateLocaleOnBackend(locale); - + + // Only update backend if user is authenticated + if (isAuthenticated) { + await updateLocaleOnBackend(locale); + } + // Reload page to apply new locale window.location.reload(); } }; + const currentLocaleLabel = locales.find((l) => l.id === currentLocale)?.label || currentLocale; + + if (variant === "mobile-menu") { + return ( + + + + {locales.map((locale) => ( + +
+ {locale.label} + {locale.id === currentLocale && } +
+
+ ))} +
+
+ ); + } + + // Icon variant const menuContent = ( + {variant === "icon" ? ( + + ) : ( + + )}
@@ -128,10 +187,14 @@ export default function ThemeModeSelector() { ); - return ( - - {menuContent} - {t`Change theme`} - - ); -} \ No newline at end of file + if (variant === "icon") { + return ( + + {menuContent} + {t`Change theme`} + + ); + } + + return menuContent; +} diff --git a/application/account-management/WebApp/federated-modules/userModals/UserProfileModal.tsx b/application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx similarity index 100% rename from application/account-management/WebApp/federated-modules/userModals/UserProfileModal.tsx rename to application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx diff --git a/application/account-management/WebApp/federated-modules/sideMenu/MobileMenu.tsx b/application/account-management/WebApp/federated-modules/sideMenu/MobileMenu.tsx index 0de637a6a..0117d4c10 100644 --- a/application/account-management/WebApp/federated-modules/sideMenu/MobileMenu.tsx +++ b/application/account-management/WebApp/federated-modules/sideMenu/MobileMenu.tsx @@ -1,22 +1,18 @@ import { api } from "@/shared/lib/api/client"; -import { t } from "@lingui/core/macro"; -import { useLingui } from "@lingui/react"; import { Trans } from "@lingui/react/macro"; import { loginPath } from "@repo/infrastructure/auth/constants"; import { useUserInfo } from "@repo/infrastructure/auth/hooks"; import { createLoginUrlWithReturnPath } from "@repo/infrastructure/auth/util"; -import type { Locale } from "@repo/infrastructure/translations/TranslationContext"; -import localeMap from "@repo/infrastructure/translations/i18n.config.json"; import { Avatar } from "@repo/ui/components/Avatar"; import { Button } from "@repo/ui/components/Button"; -import { Menu, MenuItem, MenuTrigger } from "@repo/ui/components/Menu"; import { SideMenuSeparator, overlayContext } from "@repo/ui/components/SideMenu"; -import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; import { useQueryClient } from "@tanstack/react-query"; -import { CheckIcon, GlobeIcon, LogOutIcon, MailQuestion, UserIcon } from "lucide-react"; +import { LogOutIcon, MailQuestion, UserIcon } from "lucide-react"; import { useContext, useState } from "react"; -import { SupportDialog } from "../support/SupportDialog"; -import UserProfileModal from "../userModals/UserProfileModal"; +import LocaleSwitcher from "../common/LocaleSwitcher"; +import { SupportDialog } from "../common/SupportDialog"; +import ThemeModeSelector from "../common/ThemeModeSelector"; +import UserProfileModal from "../common/UserProfileModal"; import type { FederatedSideMenuProps } from "./FederatedSideMenu"; import { NavigationMenuItems } from "./NavigationMenuItems"; @@ -25,13 +21,8 @@ function MobileMenuHeader() { const userInfo = useUserInfo(); const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); const queryClient = useQueryClient(); - const { i18n } = useLingui(); const overlayCtx = useContext(overlayContext); - const currentLocale = i18n.locale as Locale; - const locales = Object.keys(localeMap) as Locale[]; - const currentLocaleLabel = localeMap[currentLocale].label; - const logoutMutation = api.useMutation("post", "/api/account-management/authentication/logout", { onMutate: async () => { await queryClient.cancelQueries(); @@ -110,7 +101,6 @@ function MobileMenuHeader() { {/* Theme Section - using ThemeModeSelector with mobile menu variant */}
{ // Close mobile menu if it's open @@ -121,44 +111,17 @@ function MobileMenuHeader() { />
- {/* Language Section - styled like menu item */} + {/* Language Section - using LocaleSwitcher with mobile menu variant */}
- - - { - const locale = key.toString() as Locale; - if (locale !== currentLocale) { - // Dynamically load and activate the locale - const localeModule = await import(`@/shared/translations/locale/${locale}.ts`); - i18n.loadAndActivate({ locale, messages: localeModule.messages }); - document.documentElement.lang = locale; - } - }} - placement="bottom end" - > - {locales.map((locale) => ( - -
- {localeMap[locale].label} - {locale === currentLocale && } -
-
- ))} -
-
+ { + // Close mobile menu if it's open + if (overlayCtx?.isOpen) { + overlayCtx.close(); + } + }} + />
{/* Support Section - styled like menu item */} diff --git a/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx b/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx index 9f41c1c29..b543efc8e 100644 --- a/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx +++ b/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx @@ -1,4 +1,4 @@ -import UserProfileModal from "@/federated-modules/userModals/UserProfileModal"; +import UserProfileModal from "@/federated-modules/common/UserProfileModal"; import { api } from "@/shared/lib/api/client"; import { Trans } from "@lingui/react/macro"; import { loginPath } from "@repo/infrastructure/auth/constants"; diff --git a/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx b/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx index 7368f8a3c..27a507412 100644 --- a/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx +++ b/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx @@ -2,12 +2,12 @@ import { t } from "@lingui/core/macro"; import { Button } from "@repo/ui/components/Button"; import type { ReactNode } from "react"; import { Suspense, lazy } from "react"; +import LocaleSwitcher from "../common/LocaleSwitcher"; +import ThemeModeSelector from "../common/ThemeModeSelector"; import AvatarButton from "./AvatarButton"; -import LocaleSwitcher from "./LocaleSwitcher"; -import ThemeModeSelector from "./ThemeModeSelector"; import "@repo/ui/tailwind.css"; -const SupportButton = lazy(() => import("../support/SupportButton")); +const SupportButton = lazy(() => import("../common/SupportButton")); interface FederatedTopMenuProps { children?: ReactNode; diff --git a/application/account-management/WebApp/routes/(index)/-components/FeatureSection3.tsx b/application/account-management/WebApp/routes/(index)/-components/FeatureSection3.tsx index 5e8b08678..96633f37f 100644 --- a/application/account-management/WebApp/routes/(index)/-components/FeatureSection3.tsx +++ b/application/account-management/WebApp/routes/(index)/-components/FeatureSection3.tsx @@ -1,5 +1,4 @@ -import { t } from "@lingui/core/macro"; -import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; +import ThemeModeSelector from "@/federated-modules/common/ThemeModeSelector"; import { createAccountUrl } from "./cdnImages"; // FeatureSection3: A functional component that displays the third feature section @@ -68,7 +67,7 @@ function FeatureList() {

Just click the - + button to switch to the Dark Side.
diff --git a/application/account-management/WebApp/routes/(index)/-components/HeroSection.tsx b/application/account-management/WebApp/routes/(index)/-components/HeroSection.tsx index 9f0a77a74..91715b2eb 100644 --- a/application/account-management/WebApp/routes/(index)/-components/HeroSection.tsx +++ b/application/account-management/WebApp/routes/(index)/-components/HeroSection.tsx @@ -1,5 +1,5 @@ +import ThemeModeSelector from "@/federated-modules/common/ThemeModeSelector"; import logoMark from "@/shared/images/logo-mark.svg"; -import { t } from "@lingui/core/macro"; import { LoginButton } from "@repo/infrastructure/auth/LoginButton"; import { SignUpButton } from "@repo/infrastructure/auth/SignUpButton"; import { Badge } from "@repo/ui/components/Badge"; @@ -7,7 +7,6 @@ import { Button } from "@repo/ui/components/Button"; import { Dialog } from "@repo/ui/components/Dialog"; import { Link } from "@repo/ui/components/Link"; import { Popover } from "@repo/ui/components/Popover"; -import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; import { ArrowRightIcon, ChevronDownIcon, GithubIcon } from "lucide-react"; import type React from "react"; import { DialogTrigger } from "react-aria-components"; @@ -32,7 +31,7 @@ export function HeroSection() { Github - + Log in Get started today
diff --git a/application/account-management/WebApp/rsbuild.config.ts b/application/account-management/WebApp/rsbuild.config.ts index 9ed865960..b1cc0ba05 100644 --- a/application/account-management/WebApp/rsbuild.config.ts +++ b/application/account-management/WebApp/rsbuild.config.ts @@ -21,7 +21,7 @@ export default defineConfig({ DevelopmentServerPlugin({ port: 9101 }), ModuleFederationPlugin({ exposes: { - "./SupportButton": "./federated-modules/support/SupportButton.tsx", + "./SupportButton": "./federated-modules/common/SupportButton.tsx", "./FederatedSideMenu": "./federated-modules/sideMenu/FederatedSideMenu.tsx", "./FederatedTopMenu": "./federated-modules/topMenu/FederatedTopMenu.tsx", "./translations/en-US": "./shared/translations/locale/en-US.ts", diff --git a/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx b/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx index 8a63ac492..7ea27d471 100644 --- a/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx +++ b/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx @@ -1,8 +1,8 @@ -import SupportButton from "@/federated-modules/support/SupportButton"; +import LocaleSwitcher from "@/federated-modules/common/LocaleSwitcher"; +import SupportButton from "@/federated-modules/common/SupportButton"; +import ThemeModeSelector from "@/federated-modules/common/ThemeModeSelector"; import { HeroImage } from "@/shared/components/HeroImage"; import { t } from "@lingui/core/macro"; -import { LocaleSwitcher } from "@repo/infrastructure/translations/LocaleSwitcher"; -import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector"; import type { ReactNode } from "react"; interface HorizontalHeroLayoutProps { @@ -13,18 +13,18 @@ export function HorizontalHeroLayout({ children }: Readonly
- + - +
{children} {/* Mobile-only icon controls at bottom of form */}
- + - +
diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po index 6fa0c2781..717439d03 100644 --- a/application/account-management/WebApp/shared/translations/locale/da-DK.po +++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po @@ -384,6 +384,9 @@ msgstr "System" msgid "Terms of use" msgstr "Brugsvilkår" +msgid "Theme" +msgstr "" + msgid "This is the region where your data is stored" msgstr "Dette er den region, hvor dine data er lagret" @@ -396,9 +399,6 @@ msgstr "For at slette din konto bedes du kontakte vores supportteam." msgid "Toggle collapsed menu" msgstr "Skift kollapset menu" -msgid "Toggle theme" -msgstr "Skift tema" - msgid "Total users" msgstr "Totalt antal brugere" diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po index 848cfcc39..cd04bfb1e 100644 --- a/application/account-management/WebApp/shared/translations/locale/en-US.po +++ b/application/account-management/WebApp/shared/translations/locale/en-US.po @@ -384,6 +384,9 @@ msgstr "System" msgid "Terms of use" msgstr "Terms of use" +msgid "Theme" +msgstr "Theme" + msgid "This is the region where your data is stored" msgstr "This is the region where your data is stored" @@ -396,9 +399,6 @@ msgstr "To delete your account, please contact our support team." msgid "Toggle collapsed menu" msgstr "Toggle collapsed menu" -msgid "Toggle theme" -msgstr "Toggle theme" - msgid "Total users" msgstr "Total users" diff --git a/application/account-management/WebApp/shared/translations/locale/nl-NL.po b/application/account-management/WebApp/shared/translations/locale/nl-NL.po index 4b754f812..cd2da2d03 100644 --- a/application/account-management/WebApp/shared/translations/locale/nl-NL.po +++ b/application/account-management/WebApp/shared/translations/locale/nl-NL.po @@ -384,6 +384,9 @@ msgstr "Systeem" msgid "Terms of use" msgstr "Gebruiksvoorwaarden" +msgid "Theme" +msgstr "" + msgid "This is the region where your data is stored" msgstr "Dit is de regio waar je gegevens zijn opgeslagen" @@ -396,9 +399,6 @@ msgstr "Neem contact op met ons ondersteuningsteam om je account te verwijderen. msgid "Toggle collapsed menu" msgstr "Ingeklapt menu wisselen" -msgid "Toggle theme" -msgstr "Thema wisselen" - msgid "Total users" msgstr "Totale gebruikers" diff --git a/application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx b/application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx deleted file mode 100644 index 6924bb274..000000000 --- a/application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useLingui } from "@lingui/react"; -import type { Key } from "@react-types/shared"; -import { AuthenticationContext } from "@repo/infrastructure/auth/AuthenticationProvider"; -import { enhancedFetch } from "@repo/infrastructure/http/httpClient"; -import { Button } from "@repo/ui/components/Button"; -import { Menu, MenuItem, MenuTrigger } from "@repo/ui/components/Menu"; -import { Tooltip, TooltipTrigger } from "@repo/ui/components/Tooltip"; -import { CheckIcon, GlobeIcon } from "lucide-react"; -import { use, useContext, useMemo } from "react"; -import { type Locale, translationContext } from "./TranslationContext"; -import { preferredLocaleKey } from "./constants"; - -export function LocaleSwitcher({ "aria-label": ariaLabel, tooltip }: { "aria-label": string; tooltip?: string }) { - const { setLocale, getLocaleInfo, locales } = use(translationContext); - const { i18n } = useLingui(); - const { userInfo } = useContext(AuthenticationContext); - - const items = useMemo( - () => - locales.map((locale) => ({ - id: locale, - label: getLocaleInfo(locale).label - })), - [locales, getLocaleInfo] - ); - - const handleLocaleChange = (key: Key) => { - const locale = key.toString() as Locale; - if (locale !== currentLocale) { - if (userInfo?.isAuthenticated) { - enhancedFetch("/api/account-management/users/me/change-locale", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ locale }) - }).then(async (_: Response) => { - await setLocale(locale); - localStorage.setItem(preferredLocaleKey, locale); - }); - } else { - setLocale(locale).then(() => { - localStorage.setItem(preferredLocaleKey, locale); - }); - } - } - }; - - const currentLocale = i18n.locale as Locale; - - const menuContent = ( - - - - {items.map((item) => ( - -
- {item.label} - {item.id === currentLocale && } -
-
- ))} -
-
- ); - - if (tooltip) { - return ( - - {menuContent} - {tooltip} - - ); - } - - return menuContent; -} diff --git a/application/shared-webapp/ui/theme/ThemeModeSelector.tsx b/application/shared-webapp/ui/theme/ThemeModeSelector.tsx deleted file mode 100644 index 335976394..000000000 --- a/application/shared-webapp/ui/theme/ThemeModeSelector.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import type { Key } from "@react-types/shared"; -import { Button } from "@repo/ui/components/Button"; -import { Menu, MenuItem, MenuTrigger } from "@repo/ui/components/Menu"; -import { Tooltip, TooltipTrigger } from "@repo/ui/components/Tooltip"; -import { CheckIcon, MoonIcon, MoonStarIcon, SunIcon, SunMoonIcon } from "lucide-react"; -import { useThemeMode } from "./mode/ThemeMode"; -import { useSystemThemeMode } from "./mode/useSystemThemeMode"; -import { SystemThemeMode, ThemeMode } from "./mode/utils"; - -/** - * A button that opens a menu to select theme mode between system, light and dark. - */ -export function ThemeModeSelector({ - "aria-label": ariaLabel, - tooltip, - variant = "icon", - onAction -}: { - readonly "aria-label": string; - readonly tooltip?: string; - readonly variant?: "icon" | "mobile-menu"; - readonly onAction?: () => void; -}) { - const { themeMode, setThemeMode } = useThemeMode(); - - const handleThemeChange = (key: Key) => { - setThemeMode(key as ThemeMode); - onAction?.(); - }; - - const getThemeName = (mode: ThemeMode) => { - switch (mode) { - case ThemeMode.System: - return "System"; - case ThemeMode.Light: - return "Light"; - case ThemeMode.Dark: - return "Dark"; - default: - return "System"; - } - }; - - const menuContent = ( - - {variant === "icon" ? ( - - ) : ( - - )} - - -
- - System - {themeMode === ThemeMode.System && } -
-
- -
- - Light - {themeMode === ThemeMode.Light && } -
-
- -
- - Dark - {themeMode === ThemeMode.Dark && } -
-
-
-
- ); - - if (tooltip) { - return ( - - {menuContent} - {tooltip} - - ); - } - - return menuContent; -} - -// Component that always shows the system theme icon -function SystemThemeIcon({ className }: { className: string }) { - const systemTheme = useSystemThemeMode(); - - return systemTheme === SystemThemeMode.Dark ? ( - - ) : ( - - ); -} - -function ThemeModeIcon({ themeMode }: Readonly<{ themeMode: ThemeMode }>) { - // For System mode, show special icons based on resolved theme - if (themeMode === ThemeMode.System) { - return ; - } - - // For explicit Light/Dark modes, show icons based on the selected mode - return themeMode === ThemeMode.Dark ? : ; -} From adc1f3d67f9ba5c8fb8674801dab981e9a2e43f0 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Thu, 24 Jul 2025 00:09:49 +0200 Subject: [PATCH 19/26] Make toast notifications work across module federation boundaries --- .../shared-webapp/ui/components/Toast.tsx | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/application/shared-webapp/ui/components/Toast.tsx b/application/shared-webapp/ui/components/Toast.tsx index 2b52f2827..f89e3d223 100644 --- a/application/shared-webapp/ui/components/Toast.tsx +++ b/application/shared-webapp/ui/components/Toast.tsx @@ -40,12 +40,62 @@ type ToastContext = { const toastContext = createContext({}); -export const toastQueue = new ToastQueue({ +// Create a unique identifier for this instance +const instanceId = Math.random().toString(36).substring(7); + +// Create a custom ToastQueue that dispatches events for federation +class FederatedToastQueue extends ToastQueue { + private isFederatedAdd = false; + + add(content: T, options?: { timeout?: number; priority?: number }) { + // First add to the local queue + const result = super.add(content, options); + + // Only dispatch event if this isn't already a federated add + if (!this.isFederatedAdd) { + const event = new CustomEvent("federated-toast", { + detail: { content, options, sourceInstanceId: instanceId }, + bubbles: true, + composed: true + }); + window.dispatchEvent(event); + } + + return result; + } + + federatedAdd(content: T, options?: { timeout?: number; priority?: number }) { + this.isFederatedAdd = true; + const result = this.add(content, options); + this.isFederatedAdd = false; + return result; + } +} + +export const toastQueue = new FederatedToastQueue({ maxVisibleToasts: 5 }); export function GlobalToastRegion(props: AriaToastRegionProps) { const state = useToastQueue(toastQueue); + + // Listen for federated toast events from other modules + useEffect(() => { + const handleFederatedToast = (event: CustomEvent) => { + const { content, options, sourceInstanceId } = event.detail; + // Only process events from other instances + if (sourceInstanceId !== instanceId) { + // Use the federatedAdd method to add without re-dispatching the event + (toastQueue as FederatedToastQueue).federatedAdd(content, options); + } + }; + + window.addEventListener("federated-toast", handleFederatedToast as EventListener); + return () => { + window.removeEventListener("federated-toast", handleFederatedToast as EventListener); + }; + }, []); + return state.visibleToasts.length > 0 ? createPortal(, document.body) : null; } From 8596c5db4dee502cb08e710d5e21fffabd4775b4 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Tue, 15 Jul 2025 20:20:59 +0200 Subject: [PATCH 20/26] Ensure styling of federated modules is available in self-contained system hosting the module --- .../WebApp/federated-modules/topMenu/AvatarButton.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx b/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx index b543efc8e..2770ec50c 100644 --- a/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx +++ b/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx @@ -10,6 +10,7 @@ import { Menu, MenuHeader, MenuItem, MenuSeparator, MenuTrigger } from "@repo/ui import { useQueryClient } from "@tanstack/react-query"; import { LogOutIcon, UserIcon } from "lucide-react"; import { useEffect, useState } from "react"; +import "@repo/ui/tailwind.css"; export default function AvatarButton({ "aria-label": ariaLabel }: Readonly<{ "aria-label": string }>) { const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); From fbac0e8de230025f58b4aa170140df935d8e93d3 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Thu, 24 Jul 2025 01:08:17 +0200 Subject: [PATCH 21/26] Fix user profile modal in mobile menu and move aria-labels into components --- .../WebApp/federated-modules/common/SupportButton.tsx | 4 ++-- .../WebApp/federated-modules/topMenu/AvatarButton.tsx | 5 +++-- .../WebApp/federated-modules/topMenu/FederatedTopMenu.tsx | 5 ++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/application/account-management/WebApp/federated-modules/common/SupportButton.tsx b/application/account-management/WebApp/federated-modules/common/SupportButton.tsx index aa7cb1739..ac80064b8 100644 --- a/application/account-management/WebApp/federated-modules/common/SupportButton.tsx +++ b/application/account-management/WebApp/federated-modules/common/SupportButton.tsx @@ -5,11 +5,11 @@ import { MailQuestion } from "lucide-react"; import { SupportDialog } from "./SupportDialog"; import "@repo/ui/tailwind.css"; -export default function SupportButton({ "aria-label": ariaLabel }: Readonly<{ "aria-label": string }>) { +export default function SupportButton() { return ( - {t`Contact support`} diff --git a/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx b/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx index 2770ec50c..be60f056e 100644 --- a/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx +++ b/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx @@ -1,5 +1,6 @@ import UserProfileModal from "@/federated-modules/common/UserProfileModal"; import { api } from "@/shared/lib/api/client"; +import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { loginPath } from "@repo/infrastructure/auth/constants"; import { useUserInfo } from "@repo/infrastructure/auth/hooks"; @@ -12,7 +13,7 @@ import { LogOutIcon, UserIcon } from "lucide-react"; import { useEffect, useState } from "react"; import "@repo/ui/tailwind.css"; -export default function AvatarButton({ "aria-label": ariaLabel }: Readonly<{ "aria-label": string }>) { +export default function AvatarButton() { const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); const [hasAutoOpenedModal, setHasAutoOpenedModal] = useState(false); const userInfo = useUserInfo(); @@ -58,7 +59,7 @@ export default function AvatarButton({ "aria-label": ariaLabel }: Readonly<{ "ar return ( <> - diff --git a/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx b/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx index 27a507412..7991e138a 100644 --- a/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx +++ b/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx @@ -1,4 +1,3 @@ -import { t } from "@lingui/core/macro"; import { Button } from "@repo/ui/components/Button"; import type { ReactNode } from "react"; import { Suspense, lazy } from "react"; @@ -21,11 +20,11 @@ export default function FederatedTopMenu({ children }: Readonly }> - + - +
); From a30ac27c2b0668682753064ae3d10a0207c633da Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Thu, 24 Jul 2025 01:16:20 +0200 Subject: [PATCH 22/26] Remove SupportButton from module federation exports --- .../federated-modules/topMenu/FederatedTopMenu.tsx | 9 ++------- application/account-management/WebApp/rsbuild.config.ts | 1 - .../module-federation-types/account-management.d.ts | 3 --- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx b/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx index 7991e138a..504abbf4f 100644 --- a/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx +++ b/application/account-management/WebApp/federated-modules/topMenu/FederatedTopMenu.tsx @@ -1,13 +1,10 @@ -import { Button } from "@repo/ui/components/Button"; import type { ReactNode } from "react"; -import { Suspense, lazy } from "react"; import LocaleSwitcher from "../common/LocaleSwitcher"; +import SupportButton from "../common/SupportButton"; import ThemeModeSelector from "../common/ThemeModeSelector"; import AvatarButton from "./AvatarButton"; import "@repo/ui/tailwind.css"; -const SupportButton = lazy(() => import("../common/SupportButton")); - interface FederatedTopMenuProps { children?: ReactNode; } @@ -19,9 +16,7 @@ export default function FederatedTopMenu({ children }: Readonly - }> - - + diff --git a/application/account-management/WebApp/rsbuild.config.ts b/application/account-management/WebApp/rsbuild.config.ts index b1cc0ba05..df0771fb6 100644 --- a/application/account-management/WebApp/rsbuild.config.ts +++ b/application/account-management/WebApp/rsbuild.config.ts @@ -21,7 +21,6 @@ export default defineConfig({ DevelopmentServerPlugin({ port: 9101 }), ModuleFederationPlugin({ exposes: { - "./SupportButton": "./federated-modules/common/SupportButton.tsx", "./FederatedSideMenu": "./federated-modules/sideMenu/FederatedSideMenu.tsx", "./FederatedTopMenu": "./federated-modules/topMenu/FederatedTopMenu.tsx", "./translations/en-US": "./shared/translations/locale/en-US.ts", diff --git a/application/shared-webapp/build/module-federation-types/account-management.d.ts b/application/shared-webapp/build/module-federation-types/account-management.d.ts index 05b9d185a..a84b904fc 100644 --- a/application/shared-webapp/build/module-federation-types/account-management.d.ts +++ b/application/shared-webapp/build/module-federation-types/account-management.d.ts @@ -1,8 +1,5 @@ // This file was auto-generated by the ModuleFederationPlugin -declare module "account-management/SupportButton" { - export default ReactNode; -} declare module "account-management/FederatedSideMenu" { export default ReactNode; } From 1d73a8fd69039915661c2897cb6ee5aeaf71cf04 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Thu, 24 Jul 2025 22:11:20 +0200 Subject: [PATCH 23/26] Center theme selector menu to match language selector alignment --- .../WebApp/federated-modules/common/ThemeModeSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/account-management/WebApp/federated-modules/common/ThemeModeSelector.tsx b/application/account-management/WebApp/federated-modules/common/ThemeModeSelector.tsx index e30666417..e4879032e 100644 --- a/application/account-management/WebApp/federated-modules/common/ThemeModeSelector.tsx +++ b/application/account-management/WebApp/federated-modules/common/ThemeModeSelector.tsx @@ -157,7 +157,7 @@ export default function ThemeModeSelector({
)} - +
{window.matchMedia("(prefers-color-scheme: dark)").matches ? ( From 921247c797b96c414a53dcb551b134e9ea6d720c Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Thu, 24 Jul 2025 23:50:25 +0200 Subject: [PATCH 24/26] Fix full page reload when navigating SideMenu in Safari --- .../shared-webapp/ui/components/SideMenu.tsx | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/application/shared-webapp/ui/components/SideMenu.tsx b/application/shared-webapp/ui/components/SideMenu.tsx index 8bce9c9a0..c229c4957 100644 --- a/application/shared-webapp/ui/components/SideMenu.tsx +++ b/application/shared-webapp/ui/components/SideMenu.tsx @@ -328,25 +328,14 @@ export function FederatedMenuButton({ overlayCtx.close(); } - // For string hrefs in federated modules, let browser handle the navigation - // The browser will either do SPA navigation (if same origin) or full reload - }; - - const handlePress = () => { - if (isDisabled) { - return; - } - - // Auto-close overlay after navigation - if (overlayCtx?.isOpen) { - overlayCtx.close(); - } + // Always prevent default to handle navigation ourselves + e.preventDefault(); if (isCurrentSystem) { - // Same system - use pushState for SPA navigation + // Same system - use programmatic navigation window.history.pushState({}, "", to); - // Trigger a popstate event to let the router handle the navigation - window.dispatchEvent(new PopStateEvent("popstate")); + // Dispatch a popstate event using the standard Event constructor + window.dispatchEvent(new Event("popstate")); } else { // Different system - force reload window.location.href = to; @@ -366,7 +355,26 @@ export function FederatedMenuButton({ underline={false} isDisabled={isDisabled} aria-current={isActive ? "page" : undefined} - onPress={handlePress} + onPress={() => { + if (isDisabled) { + return; + } + + // Auto-close overlay after navigation + if (overlayCtx?.isOpen) { + overlayCtx.close(); + } + + if (isCurrentSystem) { + // Same system - use programmatic navigation + window.history.pushState({}, "", to); + // Dispatch a popstate event using the standard Event constructor + window.dispatchEvent(new Event("popstate")); + } else { + // Different system - force reload + window.location.href = to; + } + }} > From ce2faf9c7be0a3db2b3e3747756e2298e61902bc Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Thu, 24 Jul 2025 00:57:50 +0200 Subject: [PATCH 25/26] Fix bug showing user profile when opened from mobile menu --- .../common/UserProfileModal.tsx | 9 ++++--- .../sideMenu/FederatedSideMenu.tsx | 21 ++++++++++------ .../federated-modules/sideMenu/MobileMenu.tsx | 25 ++++++------------- .../topMenu/AvatarButton.tsx | 9 ++----- .../shared/translations/locale/da-DK.po | 9 ++++--- .../shared/translations/locale/en-US.po | 7 ++++-- .../shared/translations/locale/nl-NL.po | 9 ++++--- .../shared-webapp/ui/components/SideMenu.tsx | 17 +++++++++---- 8 files changed, 59 insertions(+), 47 deletions(-) diff --git a/application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx b/application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx index 775612573..c5bc34da9 100644 --- a/application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx +++ b/application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx @@ -13,6 +13,7 @@ import { useMutation } from "@tanstack/react-query"; import { CameraIcon, MailIcon, Trash2Icon, XIcon } from "lucide-react"; import { useCallback, useContext, useEffect, useRef, useState } from "react"; import { FileTrigger, Form, Heading, Label } from "react-aria-components"; +import { createPortal } from "react-dom"; const MAX_FILE_SIZE = 1024 * 1024; // 1MB in bytes const ALLOWED_FILE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"]; // Align with backend @@ -33,10 +34,12 @@ export default function UserProfileModal({ isOpen, onOpenChange }: Readonly + // Use a portal to render the modal at the document body level to avoid overlay conflicts + return createPortal( + - + , + document.body ); } diff --git a/application/account-management/WebApp/federated-modules/sideMenu/FederatedSideMenu.tsx b/application/account-management/WebApp/federated-modules/sideMenu/FederatedSideMenu.tsx index cfa5327b4..199ec0a8b 100644 --- a/application/account-management/WebApp/federated-modules/sideMenu/FederatedSideMenu.tsx +++ b/application/account-management/WebApp/federated-modules/sideMenu/FederatedSideMenu.tsx @@ -1,6 +1,8 @@ import { t } from "@lingui/core/macro"; import { useUserInfo } from "@repo/infrastructure/auth/hooks"; import { SideMenu } from "@repo/ui/components/SideMenu"; +import { useState } from "react"; +import UserProfileModal from "../common/UserProfileModal"; import { MobileMenu } from "./MobileMenu"; import { NavigationMenuItems } from "./NavigationMenuItems"; import "@repo/ui/tailwind.css"; @@ -11,14 +13,19 @@ export type FederatedSideMenuProps = { export default function FederatedSideMenu({ currentSystem }: Readonly) { const userInfo = useUserInfo(); + const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); return ( - } - tenantName={userInfo?.tenantName} - > - - + <> + setIsProfileModalOpen(true)} />} + tenantName={userInfo?.tenantName} + > + + + + ); } diff --git a/application/account-management/WebApp/federated-modules/sideMenu/MobileMenu.tsx b/application/account-management/WebApp/federated-modules/sideMenu/MobileMenu.tsx index 0117d4c10..c0508abfd 100644 --- a/application/account-management/WebApp/federated-modules/sideMenu/MobileMenu.tsx +++ b/application/account-management/WebApp/federated-modules/sideMenu/MobileMenu.tsx @@ -8,18 +8,16 @@ import { Button } from "@repo/ui/components/Button"; import { SideMenuSeparator, overlayContext } from "@repo/ui/components/SideMenu"; import { useQueryClient } from "@tanstack/react-query"; import { LogOutIcon, MailQuestion, UserIcon } from "lucide-react"; -import { useContext, useState } from "react"; +import { useContext } from "react"; import LocaleSwitcher from "../common/LocaleSwitcher"; import { SupportDialog } from "../common/SupportDialog"; import ThemeModeSelector from "../common/ThemeModeSelector"; -import UserProfileModal from "../common/UserProfileModal"; import type { FederatedSideMenuProps } from "./FederatedSideMenu"; import { NavigationMenuItems } from "./NavigationMenuItems"; // Mobile menu header section with user profile and settings -function MobileMenuHeader() { +function MobileMenuHeader({ onEditProfile }: { onEditProfile: () => void }) { const userInfo = useUserInfo(); - const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); const queryClient = useQueryClient(); const overlayCtx = useContext(overlayContext); @@ -52,18 +50,10 @@ function MobileMenuHeader() {

- - ); } // Complete mobile menu including header and navigation -export function MobileMenu({ currentSystem }: Readonly<{ currentSystem: FederatedSideMenuProps["currentSystem"] }>) { +export function MobileMenu({ + currentSystem, + onEditProfile +}: Readonly<{ currentSystem: FederatedSideMenuProps["currentSystem"]; onEditProfile: () => void }>) { return (
- + {/* Divider */}
diff --git a/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx b/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx index be60f056e..ffb98e369 100644 --- a/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx +++ b/application/account-management/WebApp/federated-modules/topMenu/AvatarButton.tsx @@ -1,4 +1,3 @@ -import UserProfileModal from "@/federated-modules/common/UserProfileModal"; import { api } from "@/shared/lib/api/client"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; @@ -11,6 +10,7 @@ import { Menu, MenuHeader, MenuItem, MenuSeparator, MenuTrigger } from "@repo/ui import { useQueryClient } from "@tanstack/react-query"; import { LogOutIcon, UserIcon } from "lucide-react"; import { useEffect, useState } from "react"; +import UserProfileModal from "../common/UserProfileModal"; import "@repo/ui/tailwind.css"; export default function AvatarButton() { @@ -32,11 +32,6 @@ export default function AvatarButton() { } }, [userInfo, hasAutoOpenedModal, isProfileModalOpen]); - const handleProfileModalClose = (isOpen: boolean) => { - setIsProfileModalOpen(isOpen); - // No need to check userInfo state here - once modal is closed by user, don't auto-open again - }; - const logoutMutation = api.useMutation("post", "/api/account-management/authentication/logout", { onMutate: async () => { // Cancel all ongoing queries and remove them from cache to prevent 401 errors @@ -84,7 +79,7 @@ export default function AvatarButton() { - + ); } diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po index 717439d03..343d4c44b 100644 --- a/application/account-management/WebApp/shared/translations/locale/da-DK.po +++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po @@ -287,6 +287,9 @@ msgstr "OK" msgid "Only account owners can modify the account name" msgstr "Kun kontoejere kan ændre kontonavnet" +msgid "Open navigation menu" +msgstr "Åbn navigationsmenu" + msgid "Organization" msgstr "Organisation" @@ -385,7 +388,7 @@ msgid "Terms of use" msgstr "Brugsvilkår" msgid "Theme" -msgstr "" +msgstr "Tema" msgid "This is the region where your data is stored" msgstr "Dette er den region, hvor dine data er lagret" @@ -396,8 +399,8 @@ msgstr "Titel" msgid "To delete your account, please contact our support team." msgstr "For at slette din konto bedes du kontakte vores supportteam." -msgid "Toggle collapsed menu" -msgstr "Skift kollapset menu" +msgid "Toggle sidebar" +msgstr "Skift sidepanel" msgid "Total users" msgstr "Totalt antal brugere" diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po index cd04bfb1e..5cd3ed62b 100644 --- a/application/account-management/WebApp/shared/translations/locale/en-US.po +++ b/application/account-management/WebApp/shared/translations/locale/en-US.po @@ -287,6 +287,9 @@ msgstr "OK" msgid "Only account owners can modify the account name" msgstr "Only account owners can modify the account name" +msgid "Open navigation menu" +msgstr "Open navigation menu" + msgid "Organization" msgstr "Organization" @@ -396,8 +399,8 @@ msgstr "Title" msgid "To delete your account, please contact our support team." msgstr "To delete your account, please contact our support team." -msgid "Toggle collapsed menu" -msgstr "Toggle collapsed menu" +msgid "Toggle sidebar" +msgstr "Toggle sidebar" msgid "Total users" msgstr "Total users" diff --git a/application/account-management/WebApp/shared/translations/locale/nl-NL.po b/application/account-management/WebApp/shared/translations/locale/nl-NL.po index cd2da2d03..7bc27d81d 100644 --- a/application/account-management/WebApp/shared/translations/locale/nl-NL.po +++ b/application/account-management/WebApp/shared/translations/locale/nl-NL.po @@ -287,6 +287,9 @@ msgstr "OK" msgid "Only account owners can modify the account name" msgstr "Alleen accounteigenaren kunnen de accountnaam wijzigen" +msgid "Open navigation menu" +msgstr "Navigatiemenu openen" + msgid "Organization" msgstr "Organisatie" @@ -385,7 +388,7 @@ msgid "Terms of use" msgstr "Gebruiksvoorwaarden" msgid "Theme" -msgstr "" +msgstr "Thema" msgid "This is the region where your data is stored" msgstr "Dit is de regio waar je gegevens zijn opgeslagen" @@ -396,8 +399,8 @@ msgstr "Titel" msgid "To delete your account, please contact our support team." msgstr "Neem contact op met ons ondersteuningsteam om je account te verwijderen." -msgid "Toggle collapsed menu" -msgstr "Ingeklapt menu wisselen" +msgid "Toggle sidebar" +msgstr "Zijbalk wisselen" msgid "Total users" msgstr "Totale gebruikers" diff --git a/application/shared-webapp/ui/components/SideMenu.tsx b/application/shared-webapp/ui/components/SideMenu.tsx index c229c4957..d1f3267d0 100644 --- a/application/shared-webapp/ui/components/SideMenu.tsx +++ b/application/shared-webapp/ui/components/SideMenu.tsx @@ -444,7 +444,8 @@ const chevronStyles = tv({ type SideMenuProps = { children: React.ReactNode; - ariaLabel: string; + sidebarToggleAriaLabel: string; + mobileMenuAriaLabel: string; topMenuContent?: React.ReactNode; tenantName?: string; }; @@ -633,7 +634,13 @@ const ResizableToggleButton = ({ ); -export function SideMenu({ children, ariaLabel, topMenuContent, tenantName }: Readonly) { +export function SideMenu({ + children, + sidebarToggleAriaLabel, + mobileMenuAriaLabel, + topMenuContent, + tenantName +}: Readonly) { const { className, forceCollapsed, overlayMode, isHidden } = useResponsiveMenu(); const sideMenuRef = useRef(null); const toggleButtonRef = useRef(null); @@ -855,13 +862,13 @@ export function SideMenu({ children, ariaLabel, topMenuContent, tenantName }: Re toggleMenu={toggleMenu} menuWidth={menuWidth} setMenuWidth={setMenuWidth} - ariaLabel={ariaLabel} + ariaLabel={sidebarToggleAriaLabel} actualIsCollapsed={actualIsCollapsed} /> ) : (
}> - + ); From c554bb1dcd2a20fa84314fe389430fca3f4c9457 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 30 Jul 2025 00:12:21 +0200 Subject: [PATCH 26/26] Add platform configuration to restrict BackOffice menu option to internal users --- .../Core/Features/Users/Domain/User.cs | 3 + .../Features/Users/Shared/UserInfoFactory.cs | 3 +- .../Tests/Users/Domain/UserTests.cs | 63 +++++++++++++++++++ .../sideMenu/NavigationMenuItems.tsx | 25 +++++--- .../SharedKernel/Authentication/UserInfo.cs | 20 ++++-- .../SharedDependencyConfiguration.cs | 2 + .../SharedKernel/Platform/Settings.cs | 44 +++++++++++++ .../Platform/platform-settings.jsonc | 27 ++++++++ .../SharedKernel/SharedKernel.csproj | 4 ++ .../shared-webapp/build/environment.d.ts | 4 ++ 10 files changed, 181 insertions(+), 14 deletions(-) create mode 100644 application/account-management/Tests/Users/Domain/UserTests.cs create mode 100644 application/shared-kernel/SharedKernel/Platform/Settings.cs create mode 100644 application/shared-kernel/SharedKernel/Platform/platform-settings.jsonc diff --git a/application/account-management/Core/Features/Users/Domain/User.cs b/application/account-management/Core/Features/Users/Domain/User.cs index 7f3db3035..327b5f900 100644 --- a/application/account-management/Core/Features/Users/Domain/User.cs +++ b/application/account-management/Core/Features/Users/Domain/User.cs @@ -1,4 +1,5 @@ using PlatformPlatform.SharedKernel.Domain; +using PlatformPlatform.SharedKernel.Platform; namespace PlatformPlatform.AccountManagement.Features.Users.Domain; @@ -37,6 +38,8 @@ public string Email public string Locale { get; private set; } + public bool IsInternalUser => Email.EndsWith(Settings.Current.Identity.InternalEmailDomain, StringComparison.OrdinalIgnoreCase); + public TenantId TenantId { get; } public static User Create(TenantId tenantId, string email, UserRole role, bool emailConfirmed, string? locale) diff --git a/application/account-management/Core/Features/Users/Shared/UserInfoFactory.cs b/application/account-management/Core/Features/Users/Shared/UserInfoFactory.cs index 46a78941f..1945a393e 100644 --- a/application/account-management/Core/Features/Users/Shared/UserInfoFactory.cs +++ b/application/account-management/Core/Features/Users/Shared/UserInfoFactory.cs @@ -32,7 +32,8 @@ public async Task CreateUserInfoAsync(User user, CancellationToken can Title = user.Title, AvatarUrl = user.Avatar.Url, TenantName = tenant?.Name, - Locale = user.Locale + Locale = user.Locale, + IsInternalUser = user.IsInternalUser }; } } diff --git a/application/account-management/Tests/Users/Domain/UserTests.cs b/application/account-management/Tests/Users/Domain/UserTests.cs new file mode 100644 index 000000000..3b1016e35 --- /dev/null +++ b/application/account-management/Tests/Users/Domain/UserTests.cs @@ -0,0 +1,63 @@ +using FluentAssertions; +using PlatformPlatform.AccountManagement.Features.Users.Domain; +using PlatformPlatform.SharedKernel.Domain; +using PlatformPlatform.SharedKernel.Platform; +using Xunit; + +namespace PlatformPlatform.AccountManagement.Tests.Users.Domain; + +public sealed class UserTests +{ + private readonly TenantId _tenantId = TenantId.NewId(); + + [Fact] + public void IsInternalUser_ShouldReturnTrueForInternalEmails() + { + // Arrange + var internalEmails = new[] + { + $"user{Settings.Current.Identity.InternalEmailDomain}", + $"admin{Settings.Current.Identity.InternalEmailDomain}", + $"test.user{Settings.Current.Identity.InternalEmailDomain}", + $"user+tag{Settings.Current.Identity.InternalEmailDomain}", + $"USER{Settings.Current.Identity.InternalEmailDomain.ToUpperInvariant()}" + }; + + foreach (var email in internalEmails) + { + // Arrange + var user = User.Create(_tenantId, email, UserRole.Member, true, "en-US"); + + // Act + var isInternal = user.IsInternalUser; + + // Assert + isInternal.Should().BeTrue($"Email {email} should be identified as internal"); + } + } + + [Fact] + public void IsInternalUser_ShouldReturnFalseForExternalEmails() + { + // Arrange + var externalEmails = new[] + { + "user@example.com", + "user@company.net", + $"{Settings.Current.Identity.InternalEmailDomain.Substring(1)}@example.com", + $"user@subdomain.{Settings.Current.Identity.InternalEmailDomain.Substring(1)}" + }; + + foreach (var email in externalEmails) + { + // Arrange + var user = User.Create(_tenantId, email, UserRole.Member, true, "en-US"); + + // Act + var isInternal = user.IsInternalUser; + + // Assert + isInternal.Should().BeFalse($"Email {email} should be identified as external"); + } + } +} diff --git a/application/account-management/WebApp/federated-modules/sideMenu/NavigationMenuItems.tsx b/application/account-management/WebApp/federated-modules/sideMenu/NavigationMenuItems.tsx index de67971ca..7515f2448 100644 --- a/application/account-management/WebApp/federated-modules/sideMenu/NavigationMenuItems.tsx +++ b/application/account-management/WebApp/federated-modules/sideMenu/NavigationMenuItems.tsx @@ -1,5 +1,6 @@ import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; +import { useUserInfo } from "@repo/infrastructure/auth/hooks"; import { FederatedMenuButton, SideMenuSeparator } from "@repo/ui/components/SideMenu"; import { BoxIcon, CircleUserIcon, HomeIcon, UsersIcon } from "lucide-react"; import type { FederatedSideMenuProps } from "./FederatedSideMenu"; @@ -8,6 +9,8 @@ import type { FederatedSideMenuProps } from "./FederatedSideMenu"; export function NavigationMenuItems({ currentSystem }: Readonly<{ currentSystem: FederatedSideMenuProps["currentSystem"] }>) { + const userInfo = useUserInfo(); + return ( <> - - Back Office - + {userInfo?.isInternalUser && ( + <> + + Back Office + - + + + )} ); } diff --git a/application/shared-kernel/SharedKernel/Authentication/UserInfo.cs b/application/shared-kernel/SharedKernel/Authentication/UserInfo.cs index c0068ab05..44f017dc0 100644 --- a/application/shared-kernel/SharedKernel/Authentication/UserInfo.cs +++ b/application/shared-kernel/SharedKernel/Authentication/UserInfo.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using PlatformPlatform.SharedKernel.Domain; +using PlatformPlatform.SharedKernel.Platform; using PlatformPlatform.SharedKernel.SinglePageApp; namespace PlatformPlatform.SharedKernel.Authentication; @@ -18,7 +19,8 @@ public class UserInfo public static readonly UserInfo System = new() { IsAuthenticated = false, - Locale = DefaultLocale + Locale = DefaultLocale, + IsInternalUser = false }; public bool IsAuthenticated { get; init; } @@ -43,6 +45,8 @@ public class UserInfo public string? TenantName { get; init; } + public bool IsInternalUser { get; init; } + public static UserInfo Create(ClaimsPrincipal? user, string? browserLocale) { if (user?.Identity?.IsAuthenticated != true) @@ -50,25 +54,28 @@ public static UserInfo Create(ClaimsPrincipal? user, string? browserLocale) return new UserInfo { IsAuthenticated = user?.Identity?.IsAuthenticated ?? false, - Locale = GetValidLocale(browserLocale) + Locale = GetValidLocale(browserLocale), + IsInternalUser = false }; } var userId = user.FindFirstValue(ClaimTypes.NameIdentifier); var tenantId = user.FindFirstValue("tenant_id"); + var email = user.FindFirstValue(ClaimTypes.Email); return new UserInfo { IsAuthenticated = true, Id = userId == null ? null : new UserId(userId), TenantId = tenantId == null ? null : new TenantId(long.Parse(tenantId)), Role = user.FindFirstValue(ClaimTypes.Role), - Email = user.FindFirstValue(ClaimTypes.Email), + Email = email, FirstName = user.FindFirstValue(ClaimTypes.GivenName), LastName = user.FindFirstValue(ClaimTypes.Surname), Title = user.FindFirstValue("title"), AvatarUrl = user.FindFirstValue("avatar_url"), TenantName = user.FindFirstValue("tenant_name"), - Locale = GetValidLocale(user.FindFirstValue("locale")) + Locale = GetValidLocale(user.FindFirstValue("locale")), + IsInternalUser = IsInternalUserEmail(email) }; } @@ -91,4 +98,9 @@ private static string GetValidLocale(string? locale) return foundLocale ?? DefaultLocale; } + + private static bool IsInternalUserEmail(string? email) + { + return email is not null && email.EndsWith(Settings.Current.Identity.InternalEmailDomain, StringComparison.OrdinalIgnoreCase); + } } diff --git a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs index 8f07faf25..2b8bfa50e 100644 --- a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs +++ b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs @@ -15,6 +15,7 @@ using PlatformPlatform.SharedKernel.Integrations.Email; using PlatformPlatform.SharedKernel.Persistence; using PlatformPlatform.SharedKernel.PipelineBehaviors; +using PlatformPlatform.SharedKernel.Platform; using PlatformPlatform.SharedKernel.Telemetry; namespace PlatformPlatform.SharedKernel.Configuration; @@ -39,6 +40,7 @@ public static IServiceCollection AddSharedServices(this IServiceCollection se return services .AddServiceDiscovery() .AddSingleton(GetTokenSigningService()) + .AddSingleton(Settings.Current) .AddAuthentication() .AddDefaultJsonSerializerOptions() .AddPersistenceHelpers() diff --git a/application/shared-kernel/SharedKernel/Platform/Settings.cs b/application/shared-kernel/SharedKernel/Platform/Settings.cs new file mode 100644 index 000000000..eb5135e49 --- /dev/null +++ b/application/shared-kernel/SharedKernel/Platform/Settings.cs @@ -0,0 +1,44 @@ +using System.Text.Json; + +namespace PlatformPlatform.SharedKernel.Platform; + +public sealed class Settings +{ + private static readonly Lazy Instance = new(LoadFromEmbeddedResource); + + public static Settings Current => Instance.Value; + + public required IdentityConfig Identity { get; init; } + + public required BrandingConfig Branding { get; init; } + + private static Settings LoadFromEmbeddedResource() + { + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = "PlatformPlatform.SharedKernel.Platform.platform-settings.jsonc"; + + using var stream = assembly.GetManifestResourceStream(resourceName) + ?? throw new InvalidOperationException($"Could not find embedded resource: {resourceName}"); + + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip + }; + + return JsonSerializer.Deserialize(stream, options) + ?? throw new InvalidOperationException("Failed to deserialize platform settings"); + } + + public sealed class IdentityConfig + { + public required string InternalEmailDomain { get; init; } + } + + public sealed class BrandingConfig + { + public required string ProductName { get; init; } + + public required string SupportEmail { get; init; } + } +} diff --git a/application/shared-kernel/SharedKernel/Platform/platform-settings.jsonc b/application/shared-kernel/SharedKernel/Platform/platform-settings.jsonc new file mode 100644 index 000000000..ed280d9b4 --- /dev/null +++ b/application/shared-kernel/SharedKernel/Platform/platform-settings.jsonc @@ -0,0 +1,27 @@ +{ + // Platform-wide configuration settings + // This file is prepared for sharing between backend (.NET), frontend (TypeScript), and tests + // + // IMPORTANT: This configuration is embedded in the backend and injected at build time in the frontend + // Not all values are currently used - some are placeholders for future functionality + // + // Security Note: Only include non-sensitive configuration here + // Sensitive values (like API keys) should be stored in environment variables or key vaults + + "identity": { + // Email domain suffix that identifies internal users + // Users with this domain get access to BackOffice and other internal features + // Currently used by backend only - frontend relies on isInternalUser flag from backend + "internalEmailDomain": "@platformplatform.net" + }, + + "branding": { + // Product/platform name used throughout the application + // Placeholder for future use - currently hardcoded in various places + "productName": "PlatformPlatform", + + // Support email address for user inquiries + // Placeholder for future use - not currently referenced in the codebase + "supportEmail": "support@platformplatform.net" + } +} \ No newline at end of file diff --git a/application/shared-kernel/SharedKernel/SharedKernel.csproj b/application/shared-kernel/SharedKernel/SharedKernel.csproj index 7ce0d6a93..c1d87707c 100644 --- a/application/shared-kernel/SharedKernel/SharedKernel.csproj +++ b/application/shared-kernel/SharedKernel/SharedKernel.csproj @@ -57,4 +57,8 @@ + + + + diff --git a/application/shared-webapp/build/environment.d.ts b/application/shared-webapp/build/environment.d.ts index ab4212b28..8bd9d5ce5 100644 --- a/application/shared-webapp/build/environment.d.ts +++ b/application/shared-webapp/build/environment.d.ts @@ -84,6 +84,10 @@ export declare global { * Tenant name **/ tenantName?: string; + /** + * Is internal user (has access to BackOffice) + **/ + isInternalUser?: boolean; } /**