Skip to content

Commit 51a2e0b

Browse files
committed
Rework back-office shell with avatar menu and rename Tenants to Accounts
1 parent b60875c commit 51a2e0b

7 files changed

Lines changed: 305 additions & 72 deletions

File tree

application/account/BackOfficeWebApp/routes/index.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@repo/ui/components/Ca
55
import { SidebarInset, SidebarProvider } from "@repo/ui/components/Sidebar";
66
import { createFileRoute } from "@tanstack/react-router";
77

8-
import { BackOfficeHeader } from "@/shared/components/BackOfficeHeader";
98
import { BackOfficeSideMenu } from "@/shared/components/BackOfficeSideMenu";
109
import { useMe } from "@/shared/hooks/useMe";
1110

@@ -21,11 +20,10 @@ function DashboardPage() {
2120
<SidebarProvider>
2221
<BackOfficeSideMenu />
2322
<SidebarInset>
24-
<BackOfficeHeader displayName={me?.displayName} />
2523
<AppLayout
2624
browserTitle={t`Dashboard`}
2725
title={t`Welcome to the Back Office`}
28-
subtitle={t`Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams.`}
26+
subtitle={t`Manage accounts, view system data, see exceptions, and perform various tasks for operational and support teams.`}
2927
>
3028
<Card className="w-full max-w-[40rem]">
3129
<CardHeader>
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { t } from "@lingui/core/macro";
2+
import { useLingui } from "@lingui/react";
3+
import { Trans } from "@lingui/react/macro";
4+
import { preferredLocaleKey } from "@repo/infrastructure/translations/constants";
5+
import localeMap from "@repo/infrastructure/translations/i18n.config.json";
6+
import { type Locale, translationContext } from "@repo/infrastructure/translations/TranslationContext";
7+
import { Avatar, AvatarFallback } from "@repo/ui/components/Avatar";
8+
import {
9+
DropdownMenu,
10+
DropdownMenuContent,
11+
DropdownMenuItem,
12+
DropdownMenuSeparator,
13+
DropdownMenuSub,
14+
DropdownMenuSubContent,
15+
DropdownMenuSubTrigger,
16+
DropdownMenuTrigger
17+
} from "@repo/ui/components/DropdownMenu";
18+
import { collapsedContext } from "@repo/ui/components/Sidebar";
19+
import { SIDE_MENU_DEFAULT_WIDTH_REM } from "@repo/ui/utils/responsive";
20+
import {
21+
CheckIcon,
22+
ChevronsUpDownIcon,
23+
GlobeIcon,
24+
LogOutIcon,
25+
MoonIcon,
26+
MoonStarIcon,
27+
SunIcon,
28+
SunMoonIcon,
29+
ZoomInIcon
30+
} from "lucide-react";
31+
import { useTheme } from "next-themes";
32+
import { use, useContext, useEffect, useState } from "react";
33+
34+
import { useMe } from "@/shared/hooks/useMe";
35+
36+
const zoomLevelStorageKey = "zoom-level";
37+
38+
const zoomLevelOptions = [
39+
{ value: "0.875", label: () => t`Small` },
40+
{ value: "1", label: () => t`Default` },
41+
{ value: "1.125", label: () => t`Large` },
42+
{ value: "1.25", label: () => t`Larger` }
43+
];
44+
45+
export function BackOfficeAvatarMenu() {
46+
const isCollapsed = useContext(collapsedContext);
47+
const { theme, setTheme, resolvedTheme } = useTheme();
48+
const { setLocale } = use(translationContext);
49+
const locales = Object.keys(localeMap) as Locale[];
50+
const getLocaleInfo = (locale: Locale) => localeMap[locale];
51+
const { i18n } = useLingui();
52+
const currentLocale = i18n.locale as Locale;
53+
const [currentZoomLevel, setCurrentZoomLevel] = useState("1");
54+
const [isMenuOpen, setIsMenuOpen] = useState(false);
55+
const { data: me } = useMe();
56+
const displayName = me?.displayName ?? "";
57+
const initials = displayName
58+
? displayName
59+
.split(/\s+/)
60+
.filter(Boolean)
61+
.slice(0, 2)
62+
.map((segment) => segment.charAt(0).toUpperCase())
63+
.join("")
64+
: "PP";
65+
66+
useEffect(() => {
67+
const saved = localStorage.getItem(zoomLevelStorageKey);
68+
if (saved) setCurrentZoomLevel(saved);
69+
}, []);
70+
71+
const handleLocaleChange = (locale: Locale) => {
72+
if (locale !== currentLocale) {
73+
setLocale(locale).then(() => {
74+
localStorage.setItem(preferredLocaleKey, locale);
75+
});
76+
}
77+
};
78+
79+
const handleZoomChange = (value: string) => {
80+
if (value === currentZoomLevel) return;
81+
if (value === "1") {
82+
localStorage.removeItem(zoomLevelStorageKey);
83+
} else {
84+
localStorage.setItem(zoomLevelStorageKey, value);
85+
}
86+
document.documentElement.style.setProperty("--zoom-level", value);
87+
setCurrentZoomLevel(value);
88+
window.location.reload();
89+
};
90+
91+
const handleLogout = () => {
92+
globalThis.location.href = "/.auth/logout";
93+
};
94+
95+
const themeIcon =
96+
theme === "dark" ? (
97+
<MoonIcon className="size-5" />
98+
) : theme === "light" ? (
99+
<SunIcon className="size-5" />
100+
) : resolvedTheme === "dark" ? (
101+
<MoonStarIcon className="size-5" />
102+
) : (
103+
<SunMoonIcon className="size-5" />
104+
);
105+
106+
const triggerClassName = `relative flex h-[var(--control-height)] cursor-pointer items-center gap-0 overflow-visible rounded-md border-0 py-2 font-normal text-sm outline-ring hover:bg-hover-background focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 ${
107+
isCollapsed ? "ml-[0.5625rem] w-[var(--control-height)] justify-center" : "w-full pr-2 pl-3"
108+
} ${isMenuOpen ? "bg-hover-background" : ""}`;
109+
110+
return (
111+
<div className="relative w-full px-3">
112+
<DropdownMenu open={isMenuOpen} onOpenChange={setIsMenuOpen}>
113+
<DropdownMenuTrigger className={triggerClassName} aria-label={t`User menu`}>
114+
<Avatar>
115+
<AvatarFallback>{initials}</AvatarFallback>
116+
</Avatar>
117+
{!isCollapsed && (
118+
<>
119+
<div className="ml-3 flex-1 overflow-hidden text-left font-medium text-ellipsis whitespace-nowrap text-foreground">
120+
{displayName || <Trans>Back Office</Trans>}
121+
</div>
122+
<ChevronsUpDownIcon className="ml-2 size-3.5 shrink-0 text-foreground opacity-70" />
123+
</>
124+
)}
125+
</DropdownMenuTrigger>
126+
<DropdownMenuContent
127+
align="start"
128+
side={isCollapsed ? "right" : "bottom"}
129+
className="w-auto bg-popover"
130+
style={{ minWidth: `${SIDE_MENU_DEFAULT_WIDTH_REM - 1.5}rem` }}
131+
>
132+
<DropdownMenuSub>
133+
<DropdownMenuSubTrigger aria-label={t`Change theme`}>
134+
{themeIcon}
135+
<Trans>Theme</Trans>
136+
</DropdownMenuSubTrigger>
137+
<DropdownMenuSubContent>
138+
<DropdownMenuItem onClick={() => setTheme("system")}>
139+
{resolvedTheme === "dark" ? <MoonStarIcon className="size-5" /> : <SunMoonIcon className="size-5" />}
140+
<Trans>System</Trans>
141+
{theme === "system" && <CheckIcon className="ml-auto size-4" />}
142+
</DropdownMenuItem>
143+
<DropdownMenuItem onClick={() => setTheme("light")}>
144+
<SunIcon className="size-5" />
145+
<Trans>Light</Trans>
146+
{theme === "light" && <CheckIcon className="ml-auto size-4" />}
147+
</DropdownMenuItem>
148+
<DropdownMenuItem onClick={() => setTheme("dark")}>
149+
<MoonIcon className="size-5" />
150+
<Trans>Dark</Trans>
151+
{theme === "dark" && <CheckIcon className="ml-auto size-4" />}
152+
</DropdownMenuItem>
153+
</DropdownMenuSubContent>
154+
</DropdownMenuSub>
155+
156+
<DropdownMenuSub>
157+
<DropdownMenuSubTrigger aria-label={t`Change language`}>
158+
<GlobeIcon className="size-5" />
159+
<Trans>Language</Trans>
160+
</DropdownMenuSubTrigger>
161+
<DropdownMenuSubContent>
162+
{locales.map((locale) => (
163+
<DropdownMenuItem key={locale} onClick={() => handleLocaleChange(locale)}>
164+
<span>{getLocaleInfo(locale).label}</span>
165+
{locale === currentLocale && <CheckIcon className="ml-auto size-4" />}
166+
</DropdownMenuItem>
167+
))}
168+
</DropdownMenuSubContent>
169+
</DropdownMenuSub>
170+
171+
<DropdownMenuSub>
172+
<DropdownMenuSubTrigger aria-label={t`Change zoom level`}>
173+
<ZoomInIcon className="size-5" />
174+
<Trans>Zoom</Trans>
175+
</DropdownMenuSubTrigger>
176+
<DropdownMenuSubContent>
177+
{zoomLevelOptions.map((zoom) => (
178+
<DropdownMenuItem key={zoom.value} onClick={() => handleZoomChange(zoom.value)}>
179+
<span>{zoom.label()}</span>
180+
{zoom.value === currentZoomLevel && <CheckIcon className="ml-auto size-4" />}
181+
</DropdownMenuItem>
182+
))}
183+
</DropdownMenuSubContent>
184+
</DropdownMenuSub>
185+
186+
<DropdownMenuSeparator />
187+
188+
<DropdownMenuItem onClick={handleLogout} aria-label={t`Log out`}>
189+
<LogOutIcon className="size-5" />
190+
<Trans>Log out</Trans>
191+
</DropdownMenuItem>
192+
</DropdownMenuContent>
193+
</DropdownMenu>
194+
</div>
195+
);
196+
}

application/account/BackOfficeWebApp/shared/components/BackOfficeHeader.tsx

Lines changed: 0 additions & 34 deletions
This file was deleted.

application/account/BackOfficeWebApp/shared/components/BackOfficeSideMenu.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { Link as RouterLink, useRouter } from "@tanstack/react-router";
1616
import { Building2Icon, FlagIcon, HomeIcon, LifeBuoyIcon, ListIcon, UsersIcon } from "lucide-react";
1717

18-
import logoMark from "@/shared/images/logo-mark.svg";
18+
import { BackOfficeAvatarMenu } from "./BackOfficeAvatarMenu";
1919

2020
const normalizePath = (path: string): string => path.replace(/\/$/, "") || "/";
2121

@@ -27,12 +27,7 @@ export function BackOfficeSideMenu() {
2727
<Sidebar collapsible="icon">
2828
<nav className="contents" aria-label={t`Main navigation`}>
2929
<SidebarHeader>
30-
<div className="flex items-center gap-3 pl-[0.875rem] text-sm font-semibold">
31-
<img className="size-9 shrink-0" src={logoMark} alt={t`PlatformPlatform logo`} />
32-
<span className="truncate group-data-[collapsible=icon]:hidden">
33-
<Trans>Back Office</Trans>
34-
</span>
35-
</div>
30+
<BackOfficeAvatarMenu />
3631
</SidebarHeader>
3732
<SidebarContent>
3833
<SidebarGroup>
@@ -61,10 +56,10 @@ export function BackOfficeSideMenu() {
6156
<SidebarGroupContent>
6257
<SidebarMenu>
6358
<SidebarMenuItem>
64-
<SidebarMenuButton disabled={true} tooltip={t`Tenants (coming soon)`}>
59+
<SidebarMenuButton disabled={true} tooltip={t`Accounts (coming soon)`}>
6560
<Building2Icon />
6661
<span>
67-
<Trans>Tenants</Trans>
62+
<Trans>Accounts</Trans>
6863
</span>
6964
</SidebarMenuButton>
7065
</SidebarMenuItem>

application/account/BackOfficeWebApp/shared/components/errorPages/AccessDeniedPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ function AccessDeniedNavigation() {
1818
<div className="flex items-center gap-6">
1919
<Button
2020
variant="outline"
21-
aria-label={t`Sign out`}
21+
aria-label={t`Log out`}
2222
onClick={() => {
2323
globalThis.location.href = "/.auth/logout";
2424
}}
2525
>
2626
<LogOutIcon size={16} />
2727
<span className="hidden sm:inline">
28-
<Trans>Sign out</Trans>
28+
<Trans>Log out</Trans>
2929
</span>
3030
</Button>
3131
</div>

0 commit comments

Comments
 (0)