Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1de174c
Fix flaky AvatarButton loading issues with module federation
tjementum May 29, 2025
e2405c7
Hide hero image on mobile for login and signup pages
tjementum Jul 21, 2025
66dce26
Show theme, support, and language switcher on mobile screens during l…
tjementum Jul 21, 2025
947103a
Centralize tooltip for LocaleSwitcher
tjementum Jul 22, 2025
3eef6cc
Change theme selector to use dropdown instead of tristate toggle button
tjementum Jul 22, 2025
d3f10d7
Create sharable support dialog using module federation and change sup…
tjementum Jul 22, 2025
a41e535
Add contact support option to the mobile menu
tjementum Jul 22, 2025
b0a105f
Make translations in module federation available to all self-containe…
tjementum Jul 22, 2025
91100ca
Replace account deletion with support contact dialog
tjementum Jul 22, 2025
dfd73c9
Create shared menu using module federation
tjementum Jul 22, 2025
079b269
Create federated navigation with SPA reload only when navigating betw…
tjementum Jul 23, 2025
8b56a65
Simplify contract for SharedSideMenu
tjementum Jul 23, 2025
b5771f8
Restructure side menu into mobile menu and side menu, move to subfolder
tjementum Jul 23, 2025
23df679
Restructure federated modules into dedicated top-level directory for …
tjementum Jul 23, 2025
f3d4cea
Create FederatedTopMenu placeholder
tjementum Jul 23, 2025
d0f13ff
Add AvatarButton to new federation module top menu
tjementum Jul 23, 2025
c5b63ac
Create FederatedTopMenu with theme, support, language, and user profi…
tjementum Jul 23, 2025
d8a9e6f
Centralize federated components and fix language switching in mobile …
tjementum Jul 23, 2025
adc1f3d
Make toast notifications work across module federation boundaries
tjementum Jul 23, 2025
8596c5d
Ensure styling of federated modules is available in self-contained sy…
tjementum Jul 15, 2025
fbac0e8
Fix user profile modal in mobile menu and move aria-labels into compo…
tjementum Jul 23, 2025
a30ac27
Remove SupportButton from module federation exports
tjementum Jul 23, 2025
1d73a8f
Center theme selector menu to match language selector alignment
tjementum Jul 24, 2025
921247c
Fix full page reload when navigating SideMenu in Safari
tjementum Jul 24, 2025
ce2faf9
Fix bug showing user profile when opened from mobile menu
tjementum Jul 23, 2025
c554bb1
Add platform configuration to restrict BackOffice menu option to inte…
tjementum Jul 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using PlatformPlatform.SharedKernel.Domain;
using PlatformPlatform.SharedKernel.Platform;

namespace PlatformPlatform.AccountManagement.Features.Users.Domain;

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public async Task<UserInfo> CreateUserInfoAsync(User user, CancellationToken can
Title = user.Title,
AvatarUrl = user.Avatar.Url,
TenantName = tenant?.Name,
Locale = user.Locale
Locale = user.Locale,
IsInternalUser = user.IsInternalUser
};
}
}
63 changes: 63 additions & 0 deletions application/account-management/Tests/Users/Domain/UserTests.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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";
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({
variant = "icon",
onAction
}: {
variant?: "icon" | "mobile-menu";
onAction?: () => void;
} = {}) {
const [currentLocale, setCurrentLocale] = useState<Locale>("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)) {
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) {
// Call onAction if provided (for closing mobile menu)
onAction?.();

// Save to localStorage
localStorage.setItem(PREFERRED_LOCALE_KEY, 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 (
<MenuTrigger>
<Button
variant="ghost"
className="flex h-11 w-full items-center justify-start gap-4 px-3 py-2 font-normal text-base text-muted-foreground hover:bg-hover-background hover:text-foreground"
style={{ pointerEvents: "auto" }}
>
<div className="flex h-6 w-6 shrink-0 items-center justify-center">
<GlobeIcon className="h-5 w-5 stroke-current" />
</div>
<div className="min-w-0 flex-1 overflow-hidden whitespace-nowrap text-start">
<Trans>Language</Trans>
</div>
<div className="shrink-0 text-base text-muted-foreground">{currentLocaleLabel}</div>
</Button>
<Menu onAction={handleLocaleChange} placement="bottom end">
{locales.map((locale) => (
<MenuItem key={locale.id} id={locale.id} textValue={locale.label}>
<div className="flex items-center gap-2">
<span>{locale.label}</span>
{locale.id === currentLocale && <CheckIcon className="ml-auto h-4 w-4" />}
</div>
</MenuItem>
))}
</Menu>
</MenuTrigger>
);
}

// Icon variant
const menuContent = (
<MenuTrigger>
<Button variant="icon" aria-label={t`Change language`}>
<GlobeIcon className="h-5 w-5" />
</Button>
<Menu onAction={handleLocaleChange} aria-label={t`Change language`}>
{locales.map((locale) => (
<MenuItem key={locale.id} id={locale.id} textValue={locale.label}>
<div className="flex items-center gap-2">
<span>{locale.label}</span>
{locale.id === currentLocale && <CheckIcon className="ml-auto h-4 w-4" />}
</div>
</MenuItem>
))}
</Menu>
</MenuTrigger>
);

return (
<TooltipTrigger>
{menuContent}
<Tooltip>{t`Change language`}</Tooltip>
</TooltipTrigger>
);
}
Original file line number Diff line number Diff line change
@@ -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() {
return (
<SupportDialog>
<TooltipTrigger>
<Button variant="icon" aria-label={t`Contact support`}>
<MailQuestion size={20} />
</Button>
<Tooltip>{t`Contact support`}</Tooltip>
</TooltipTrigger>
</SupportDialog>
);
}
Original file line number Diff line number Diff line change
@@ -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<SupportDialogProps>) {
return (
<DialogTrigger>
{children}
<Modal isDismissable={true} zIndex="high">
<Dialog className="max-w-lg">
{({ close }) => (
<>
<XIcon onClick={close} className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" />
<Heading slot="title" className="text-2xl">
{t`Contact support`}
</Heading>
<p className="text-muted-foreground text-sm">{t`Need help? Our support team is here to assist you.`}</p>
<div className="mt-4 flex flex-col gap-4">
<div className="flex items-center gap-3 rounded-lg border border-input bg-input-background p-4 opacity-50">
<MailIcon className="h-5 w-5 text-muted-foreground" />
<a href="mailto:support@platformplatform.net" className="text-primary hover:underline">
support@platformplatform.net
</a>
</div>
<p className="text-muted-foreground text-sm">{t`Feel free to reach out with any questions or issues you may have.`}</p>
<div className="mt-6 flex justify-end gap-4">
<Button onPress={close}>{t`Close`}</Button>
</div>
</div>
</>
)}
</Dialog>
</Modal>
</DialogTrigger>
);
}
Loading