diff --git a/apps/backoffice/src/app/features/profile/pages/ProfilePage.tsx b/apps/backoffice/src/app/features/profile/pages/ProfilePage.tsx index 3ee3aae2e..e3f4ef131 100644 --- a/apps/backoffice/src/app/features/profile/pages/ProfilePage.tsx +++ b/apps/backoffice/src/app/features/profile/pages/ProfilePage.tsx @@ -26,6 +26,8 @@ import { IconCircleCheckFilled, IconDeviceDesktop, IconDeviceFloppy, + IconLayoutNavbar, + IconLayoutSidebar, IconLock, IconMail, IconMoon, @@ -46,6 +48,8 @@ import { setUser } from '@ema-platform/auth'; import type { AuthUser } from '@ema-platform/auth'; import { SUPPORTED_LANGUAGES, type AppLanguage } from '../../../i18n/config'; import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import { setLayoutMode } from '../../../store/preferences.slice'; +import type { LayoutMode } from '../../../store/preferences.slice'; import classes from './ProfilePage.module.css'; function getInitials(name: string, fallback: string) { @@ -72,6 +76,7 @@ export function ProfilePage() { const dispatch = useAppDispatch(); const user = useAppSelector((state) => state.auth.user); const { colorScheme, setColorScheme } = useMantineColorScheme(); + const layoutMode = useAppSelector((state) => state.preferences.layoutMode); const [updateTrigger] = useApiMutation(); const [meTrigger] = useApiMutation(); @@ -572,6 +577,48 @@ export function ProfilePage() { +
+ {t('profile.layout.title')} + + {t('profile.layout.subtitle')} + + + {([{ value: 'top', label: t('profile.layout.top'), icon: IconLayoutNavbar }, { value: 'sidebar', label: t('profile.layout.sidebar'), icon: IconLayoutSidebar }] as const).map(({ value, label, icon: Icon }) => { + const active = layoutMode === value; + return ( + dispatch(setLayoutMode(value))} + className={`${classes.choice} ${active ? classes.choiceActive : ''}`} + p="md" + > + + + + {label} + + {active && ( + + )} + + + ); + })} + +
+ + + diff --git a/apps/backoffice/src/app/i18n/locales/am.ts b/apps/backoffice/src/app/i18n/locales/am.ts index 1bca84ffe..a2a5f9d85 100644 --- a/apps/backoffice/src/app/i18n/locales/am.ts +++ b/apps/backoffice/src/app/i18n/locales/am.ts @@ -168,6 +168,12 @@ export const am: Translations = { title: 'ባለሁለት ደረጃ ማረጋገጫ', desc: 'በየጊዜው ሲገቡ ከስልክዎ የአንድ ጊዜ ኮድ ያስፈልጋል።', }, + layout: { + title: 'አቀማመጥ', + subtitle: 'የማውጫ አቀማመጥ እንዴት እንደሚታይ ይምረጡ።', + top: 'የላይኛው ትሮች', + sidebar: 'የጎን አሞሌ', + }, notifications: { title: 'የኢሜይል ማሳወቂያዎች', desc: 'ስለ መለያ እንቅስቃሴዎ በኢሜይል ዝማኔዎችን ይቀበሉ።', diff --git a/apps/backoffice/src/app/i18n/locales/en.ts b/apps/backoffice/src/app/i18n/locales/en.ts index b19c8aab3..bdce8c9fe 100644 --- a/apps/backoffice/src/app/i18n/locales/en.ts +++ b/apps/backoffice/src/app/i18n/locales/en.ts @@ -166,6 +166,12 @@ export const en = { title: 'Two-step verification', desc: 'Require a one-time code from your phone each time you sign in.', }, + layout: { + title: 'Layout', + subtitle: 'Choose how the navigation is displayed.', + top: 'Top tabs', + sidebar: 'Sidebar', + }, notifications: { title: 'Email notifications', desc: 'Receive updates about your account activity by email.', diff --git a/apps/backoffice/src/app/layouts/BackofficeLayout.tsx b/apps/backoffice/src/app/layouts/BackofficeLayout.tsx index 1fc2176ea..9e2c2a000 100644 --- a/apps/backoffice/src/app/layouts/BackofficeLayout.tsx +++ b/apps/backoffice/src/app/layouts/BackofficeLayout.tsx @@ -1,10 +1,11 @@ -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { AppShell, rem } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { logout } from '@ema-platform/auth'; -import { AppHeader } from '@ema-platform/ui'; +import { AppHeader, AppSidebar } from '@ema-platform/ui'; +import type { NavItem } from '@ema-platform/ui'; import { IconLayoutDashboard, IconUsers, @@ -14,14 +15,6 @@ import { import { notify } from '@ema-platform/ui'; import { SUPPORTED_LANGUAGES } from '../i18n/config'; import { useAppDispatch, useAppSelector } from '../store/hooks'; -import type { Icon } from '@tabler/icons-react'; - -interface NavItem { - label: string; - icon: Icon; - to?: string; - soon?: boolean; -} const NAV_ITEMS: NavItem[] = [ { to: '/dashboard', label: 'Dashboard', icon: IconLayoutDashboard }, @@ -38,7 +31,9 @@ export function BackofficeLayout() { const location = useLocation(); const dispatch = useAppDispatch(); const [opened, { toggle: toggleNav }] = useDisclosure(); + const [collapsed, setCollapsed] = useState(false); const user = useAppSelector((state) => state.auth.user); + const layoutMode = useAppSelector((state) => state.preferences.layoutMode); const displayName = user?.name?.en || user?.username || ''; const initials = displayName @@ -70,9 +65,20 @@ export function BackofficeLayout() { } }; + const handleToggleCollapse = useCallback(() => { + setCollapsed((prev) => !prev); + }, []); + + const isSidebar = layoutMode === 'sidebar'; + return ( - {/* Top bar */}
- {/* Legacy-style tab bar — matching UM AppMenuTabs look */} -
+ {NAV_ITEMS.map((item) => { + const active = !!item.to && (location.pathname === item.to || location.pathname.startsWith(`${item.to}/`)); + const ItemIcon = item.icon; + return ( + + ); + })} +
+ )} +
+ + {isSidebar && ( + - {NAV_ITEMS.map((item) => { - const active = !!item.to && (location.pathname === item.to || location.pathname.startsWith(`${item.to}/`)); - const ItemIcon = item.icon; - return ( - - ); - })} - - + + + )} +
diff --git a/apps/backoffice/src/app/store/index.ts b/apps/backoffice/src/app/store/index.ts index 1b00fdc9f..141736dea 100644 --- a/apps/backoffice/src/app/store/index.ts +++ b/apps/backoffice/src/app/store/index.ts @@ -9,6 +9,7 @@ import { logout, } from '@ema-platform/auth'; import type { AuthUser } from '@ema-platform/auth'; +import { preferencesReducer } from './preferences.slice'; configureAuthStorage('ema-backoffice'); @@ -25,6 +26,7 @@ export const store = configureStore({ reducer: { auth: authReducer, signup: signupReducer, + preferences: preferencesReducer, [baseApi.reducerPath]: baseApi.reducer, }, preloadedState: preloadedAuth ? { auth: preloadedAuth } : undefined, diff --git a/apps/backoffice/src/app/store/preferences.slice.ts b/apps/backoffice/src/app/store/preferences.slice.ts new file mode 100644 index 000000000..3b25d4b87 --- /dev/null +++ b/apps/backoffice/src/app/store/preferences.slice.ts @@ -0,0 +1,39 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; + +export type LayoutMode = 'top' | 'sidebar'; + +interface PreferencesState { + layoutMode: LayoutMode; +} + +const PREFERENCES_KEY = 'ema-backoffice-preferences'; + +const loadPreferences = (): PreferencesState => { + try { + const stored = localStorage.getItem(PREFERENCES_KEY); + if (stored) return JSON.parse(stored); + } catch {} + return { layoutMode: 'top' }; +}; + +const savePreferences = (state: PreferencesState) => { + try { + localStorage.setItem(PREFERENCES_KEY, JSON.stringify(state)); + } catch {} +}; + +const initialState: PreferencesState = loadPreferences(); + +const preferencesSlice = createSlice({ + name: 'preferences', + initialState, + reducers: { + setLayoutMode(state, action: PayloadAction) { + state.layoutMode = action.payload; + savePreferences(state); + }, + }, +}); + +export const { setLayoutMode } = preferencesSlice.actions; +export const preferencesReducer = preferencesSlice.reducer;