From bd64cac29db0a49617e9518ea3ab633866af93e2 Mon Sep 17 00:00:00 2001
From: Xavier Abad <77491413+xabg2@users.noreply.github.com>
Date: Tue, 24 Mar 2026 16:12:07 +0100
Subject: [PATCH 1/7] feat: add Preferences Dialog basic components and
navigation
---
.../preferences/components/Section.tsx | 42 ++++++++++++++
.../preferences/components/SectionItem.tsx | 55 +++++++++++++++++++
.../preferences/components/SectionList.tsx | 30 ++++++++++
.../components/SectionListWrapper.tsx | 21 +++++++
src/components/preferences/index.tsx | 32 +++++++++++
src/context/dialog-manager/types/index.ts | 1 -
.../preferences/usePreferencesNavigation.tsx | 51 +++++++++++++++++
src/i18n/locales/en.json | 22 ++++++++
src/types/preferences/index.ts | 20 +++++++
9 files changed, 273 insertions(+), 1 deletion(-)
create mode 100644 src/components/preferences/components/Section.tsx
create mode 100644 src/components/preferences/components/SectionItem.tsx
create mode 100644 src/components/preferences/components/SectionList.tsx
create mode 100644 src/components/preferences/components/SectionListWrapper.tsx
create mode 100644 src/components/preferences/index.tsx
create mode 100644 src/hooks/preferences/usePreferencesNavigation.tsx
create mode 100644 src/types/preferences/index.ts
diff --git a/src/components/preferences/components/Section.tsx b/src/components/preferences/components/Section.tsx
new file mode 100644
index 0000000..96fec1c
--- /dev/null
+++ b/src/components/preferences/components/Section.tsx
@@ -0,0 +1,42 @@
+import { type ReactNode } from 'react';
+import { CaretLeftIcon, XIcon } from '@phosphor-icons/react';
+
+const Section = ({
+ className = '',
+ children,
+ title,
+ onBackButtonClicked,
+ onClosePreferences,
+}: {
+ className?: string;
+ children: ReactNode;
+ title: string;
+ onBackButtonClicked?: () => void;
+ onClosePreferences: () => void;
+}): JSX.Element => {
+ return (
+
+
+
+ {onBackButtonClicked && (
+
+ )}
+
{title}
+
+
+
+
{children}
+
+ );
+};
+
+export default Section;
diff --git a/src/components/preferences/components/SectionItem.tsx b/src/components/preferences/components/SectionItem.tsx
new file mode 100644
index 0000000..7836d33
--- /dev/null
+++ b/src/components/preferences/components/SectionItem.tsx
@@ -0,0 +1,55 @@
+export interface SectionItemProps {
+ text: string;
+ isActive?: boolean;
+ isSection?: boolean;
+ isSubsection?: boolean;
+ isDisabled?: boolean;
+ onClick?: () => void;
+ notificationsNumber?: number;
+}
+
+const SectionItem = ({
+ text,
+ isActive,
+ isDisabled,
+ isSection,
+ isSubsection,
+ notificationsNumber,
+ onClick,
+}: SectionItemProps) => {
+ const isClickable = !!onClick;
+ const clickableContainerClass = isClickable ? 'hover:bg-gray-1 hover:bg-gray-5' : '';
+ const activeContainerClass = isActive ? 'bg-primary' : clickableContainerClass;
+ const containerClass = isDisabled ? '' : activeContainerClass;
+ const clickableClass = isClickable ? 'hover:cursor-pointer' : '';
+ const activeTextClass = isActive ? 'text-white' : 'text-gray-80';
+ const disabledTextClass = isDisabled ? 'text-gray-40' : activeTextClass;
+ const sectionTextClass = isSection ? 'font-semibold' : '';
+ const subsectionTextClass = isSubsection ? 'px-3' : '';
+ const notificationClass = isActive ? 'bg-white' : ' bg-primary';
+ const notificationTextClass = isActive ? 'text-primary' : 'text-white';
+
+ const Element = isClickable ? 'button' : 'div';
+
+ return (
+
+
+
+ {text}
+
+
+ {notificationsNumber && notificationsNumber > 0 && (
+
+ {notificationsNumber}
+
+ )}
+
+ );
+};
+
+export default SectionItem;
diff --git a/src/components/preferences/components/SectionList.tsx b/src/components/preferences/components/SectionList.tsx
new file mode 100644
index 0000000..c62cfa2
--- /dev/null
+++ b/src/components/preferences/components/SectionList.tsx
@@ -0,0 +1,30 @@
+import type { PreferencesActivePath, PreferencesSectionItem } from '@/types/preferences';
+import SectionItem from './SectionItem';
+
+interface SectionListProps {
+ sectionItems: (PreferencesSectionItem & { text: string })[];
+ activePath: PreferencesActivePath;
+ onSelectSection: (path: PreferencesActivePath) => void;
+}
+
+const SectionList = ({ sectionItems, activePath, onSelectSection }: SectionListProps) => {
+ return (
+
+ {sectionItems.map((item) => {
+ const isActive = item.section === activePath.section && item.subsection === activePath.subsection;
+
+ return (
+ onSelectSection({ section: item.section, subsection: item.subsection })}
+ />
+ );
+ })}
+
+ );
+};
+
+export default SectionList;
diff --git a/src/components/preferences/components/SectionListWrapper.tsx b/src/components/preferences/components/SectionListWrapper.tsx
new file mode 100644
index 0000000..1517e98
--- /dev/null
+++ b/src/components/preferences/components/SectionListWrapper.tsx
@@ -0,0 +1,21 @@
+import { useTranslationContext } from '@/i18n';
+import { PREFERENCES_DEFAULT_SECTIONS, type PreferencesActivePath } from '@/types/preferences';
+import SectionList from './SectionList';
+
+interface SectionListWrapperProps {
+ activePath: PreferencesActivePath;
+ onSelectSection: (path: PreferencesActivePath) => void;
+}
+
+const SectionListWrapper = ({ activePath, onSelectSection }: SectionListWrapperProps) => {
+ const { translate } = useTranslationContext();
+
+ const sectionItems = PREFERENCES_DEFAULT_SECTIONS.map((item) => ({
+ ...item,
+ text: translate(`modals.preferences.sections.${item.subsection ?? item.section}.title`),
+ }));
+
+ return ;
+};
+
+export default SectionListWrapper;
diff --git a/src/components/preferences/index.tsx b/src/components/preferences/index.tsx
new file mode 100644
index 0000000..5371fa5
--- /dev/null
+++ b/src/components/preferences/index.tsx
@@ -0,0 +1,32 @@
+import { usePreferencesNavigation } from '@/hooks/preferences/usePreferencesNavigation';
+import { useTranslationContext } from '@/i18n';
+import { Modal } from '@internxt/ui';
+import { useEffect } from 'react';
+import SectionListWrapper from './components/SectionListWrapper';
+
+export const PreferencesDialog = () => {
+ const { translate } = useTranslationContext();
+ const { isOpen, activePath, openSection, close } = usePreferencesNavigation();
+
+ const title = translate(`modals.preferences.sections.${activePath.subsection ?? activePath.section}.title`);
+
+ useEffect(() => {
+ if (isOpen) {
+ document.title = `${title} | Internxt Mail`;
+ }
+ }, [isOpen, title]);
+
+ return (
+
+
+ {translate('modals.preferences.title')}
+
+
+
+ );
+};
diff --git a/src/context/dialog-manager/types/index.ts b/src/context/dialog-manager/types/index.ts
index e65ea9d..4e65f8a 100644
--- a/src/context/dialog-manager/types/index.ts
+++ b/src/context/dialog-manager/types/index.ts
@@ -1,6 +1,5 @@
export enum ActionDialog {
ComposeMessage = 'compose-message',
- Settings = 'settings',
}
export interface ActionDialogState {
diff --git a/src/hooks/preferences/usePreferencesNavigation.tsx b/src/hooks/preferences/usePreferencesNavigation.tsx
new file mode 100644
index 0000000..baef92e
--- /dev/null
+++ b/src/hooks/preferences/usePreferencesNavigation.tsx
@@ -0,0 +1,51 @@
+import { useSearchParams } from 'react-router-dom';
+import { useCallback, useMemo } from 'react';
+import {
+ PREFERENCES_DEFAULT_SECTIONS,
+ type PreferencesActivePath,
+ type PreferencesSection,
+ type PreferencesSubsection,
+} from '@/types/preferences';
+
+const DEFAULT_SECTION: PreferencesActivePath = {
+ section: 'general',
+ subsection: 'general',
+};
+
+const isValidSection = (value: string): value is PreferencesSection => {
+ return PREFERENCES_DEFAULT_SECTIONS.some((item) => item.section === value);
+};
+
+const isValidSubsection = (section: string, value: string): value is PreferencesSubsection => {
+ return PREFERENCES_DEFAULT_SECTIONS.some((item) => item.section === section && item.subsection === value);
+};
+
+export const usePreferencesNavigation = () => {
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const isOpen = searchParams.get('preferences') === 'open';
+
+ const activePath: PreferencesActivePath = useMemo(() => {
+ const section = searchParams.get('section');
+ const subsection = searchParams.get('subsection');
+
+ if (section && isValidSection(section) && subsection && isValidSubsection(section, subsection)) {
+ return { section, subsection };
+ }
+
+ return DEFAULT_SECTION;
+ }, [searchParams]);
+
+ const openSection = useCallback(
+ ({ section, subsection }: PreferencesActivePath) => {
+ setSearchParams({ preferences: 'open', section, subsection }, { replace: true });
+ },
+ [setSearchParams],
+ );
+
+ const close = useCallback(() => {
+ setSearchParams({}, { replace: true });
+ }, [setSearchParams]);
+
+ return { isOpen, activePath, openSection, close };
+};
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 66fd3b9..69ed961 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -113,6 +113,28 @@
"title": "Unlock Cleaner",
"description": "Upgrade now to keep your files optimized and free up space."
}
+ },
+ "preferences": {
+ "title": "Preferences",
+ "sections": {
+ "general": {
+ "title": "General",
+ "appearance": {
+ "title": "Appearance",
+ "dark": "Dark",
+ "light": "Light",
+ "system": "System"
+ },
+ "language": {
+ "title": "Language",
+ "en": "English (English)",
+ "es": "Español (Spanish)",
+ "fr": "Français (French)",
+ "it": "Italiano (Italian)"
+ },
+ "support": "Support"
+ }
+ }
}
}
}
diff --git a/src/types/preferences/index.ts b/src/types/preferences/index.ts
new file mode 100644
index 0000000..aba7d2e
--- /dev/null
+++ b/src/types/preferences/index.ts
@@ -0,0 +1,20 @@
+export type PreferencesSection = 'general';
+export type PreferencesSubsection = 'general';
+
+export interface PreferencesSectionItem {
+ section: PreferencesSection;
+ subsection: PreferencesSubsection;
+ isSubsection?: boolean;
+}
+
+export interface PreferencesActivePath {
+ section: PreferencesSection;
+ subsection: PreferencesSubsection;
+}
+
+export const PREFERENCES_DEFAULT_SECTIONS: PreferencesSectionItem[] = [
+ {
+ section: 'general',
+ subsection: 'general',
+ },
+];
From 4c7d721072975e97def56ae8a25c0ba912b9240f Mon Sep 17 00:00:00 2001
From: Xavier Abad <77491413+xabg2@users.noreply.github.com>
Date: Tue, 24 Mar 2026 16:55:59 +0100
Subject: [PATCH 2/7] refactor: use only one param to identify the active tab
---
.../preferences/components/Section.tsx | 6 ++--
.../preferences/components/SectionList.tsx | 29 +++++++---------
.../components/SectionListWrapper.tsx | 14 ++++----
src/components/preferences/index.tsx | 17 ++++++++--
.../preferences/sections/general/index.tsx | 14 ++++++++
.../preferences/usePreferencesNavigation.tsx | 33 ++++++-------------
src/types/preferences/index.ts | 18 ++--------
7 files changed, 63 insertions(+), 68 deletions(-)
create mode 100644 src/components/preferences/sections/general/index.tsx
diff --git a/src/components/preferences/components/Section.tsx b/src/components/preferences/components/Section.tsx
index 96fec1c..c23896b 100644
--- a/src/components/preferences/components/Section.tsx
+++ b/src/components/preferences/components/Section.tsx
@@ -6,13 +6,13 @@ const Section = ({
children,
title,
onBackButtonClicked,
- onClosePreferences,
+ onClose,
}: {
className?: string;
children: ReactNode;
title: string;
onBackButtonClicked?: () => void;
- onClosePreferences: () => void;
+ onClose: () => void;
}): JSX.Element => {
return (
@@ -29,7 +29,7 @@ const Section = ({
diff --git a/src/components/preferences/components/SectionList.tsx b/src/components/preferences/components/SectionList.tsx
index c62cfa2..7eaf9e1 100644
--- a/src/components/preferences/components/SectionList.tsx
+++ b/src/components/preferences/components/SectionList.tsx
@@ -1,28 +1,23 @@
-import type { PreferencesActivePath, PreferencesSectionItem } from '@/types/preferences';
+import type { PreferencesSection, PreferencesSectionItem } from '@/types/preferences';
import SectionItem from './SectionItem';
interface SectionListProps {
sectionItems: (PreferencesSectionItem & { text: string })[];
- activePath: PreferencesActivePath;
- onSelectSection: (path: PreferencesActivePath) => void;
+ activeSection: PreferencesSection;
+ onSelectSection: (section: PreferencesSection) => void;
}
-const SectionList = ({ sectionItems, activePath, onSelectSection }: SectionListProps) => {
+const SectionList = ({ sectionItems, activeSection, onSelectSection }: SectionListProps) => {
return (
- {sectionItems.map((item) => {
- const isActive = item.section === activePath.section && item.subsection === activePath.subsection;
-
- return (
- onSelectSection({ section: item.section, subsection: item.subsection })}
- />
- );
- })}
+ {sectionItems.map((item) => (
+ onSelectSection(item.id)}
+ />
+ ))}
);
};
diff --git a/src/components/preferences/components/SectionListWrapper.tsx b/src/components/preferences/components/SectionListWrapper.tsx
index 1517e98..361ada6 100644
--- a/src/components/preferences/components/SectionListWrapper.tsx
+++ b/src/components/preferences/components/SectionListWrapper.tsx
@@ -1,21 +1,21 @@
import { useTranslationContext } from '@/i18n';
-import { PREFERENCES_DEFAULT_SECTIONS, type PreferencesActivePath } from '@/types/preferences';
+import { PREFERENCES_SECTIONS, type PreferencesSection } from '@/types/preferences';
import SectionList from './SectionList';
interface SectionListWrapperProps {
- activePath: PreferencesActivePath;
- onSelectSection: (path: PreferencesActivePath) => void;
+ activeSection: PreferencesSection;
+ onSelectSection: (section: PreferencesSection) => void;
}
-const SectionListWrapper = ({ activePath, onSelectSection }: SectionListWrapperProps) => {
+const SectionListWrapper = ({ activeSection, onSelectSection }: SectionListWrapperProps) => {
const { translate } = useTranslationContext();
- const sectionItems = PREFERENCES_DEFAULT_SECTIONS.map((item) => ({
+ const sectionItems = PREFERENCES_SECTIONS.map((item) => ({
...item,
- text: translate(`modals.preferences.sections.${item.subsection ?? item.section}.title`),
+ text: translate(`modals.preferences.sections.${item.id}.title`),
}));
- return ;
+ return ;
};
export default SectionListWrapper;
diff --git a/src/components/preferences/index.tsx b/src/components/preferences/index.tsx
index 5371fa5..5af2f6e 100644
--- a/src/components/preferences/index.tsx
+++ b/src/components/preferences/index.tsx
@@ -1,14 +1,21 @@
import { usePreferencesNavigation } from '@/hooks/preferences/usePreferencesNavigation';
import { useTranslationContext } from '@/i18n';
+import type { PreferencesSection } from '@/types/preferences';
import { Modal } from '@internxt/ui';
+import type { FC } from 'react';
import { useEffect } from 'react';
import SectionListWrapper from './components/SectionListWrapper';
+import GeneralSection from './sections/general';
+
+const SECTION_COMPONENTS: Record void }>> = {
+ general: GeneralSection,
+};
export const PreferencesDialog = () => {
const { translate } = useTranslationContext();
- const { isOpen, activePath, openSection, close } = usePreferencesNavigation();
+ const { isOpen, activeSection, openSection, close } = usePreferencesNavigation();
- const title = translate(`modals.preferences.sections.${activePath.subsection ?? activePath.section}.title`);
+ const title = translate(`modals.preferences.sections.${activeSection}.title`);
useEffect(() => {
if (isOpen) {
@@ -16,6 +23,8 @@ export const PreferencesDialog = () => {
}
}, [isOpen, title]);
+ const ActiveSectionComponent = SECTION_COMPONENTS[activeSection];
+
return (
{
>
{translate('modals.preferences.title')}
-
+
+
+
);
};
diff --git a/src/components/preferences/sections/general/index.tsx b/src/components/preferences/sections/general/index.tsx
new file mode 100644
index 0000000..765eb25
--- /dev/null
+++ b/src/components/preferences/sections/general/index.tsx
@@ -0,0 +1,14 @@
+import { useTranslationContext } from '@/i18n';
+import Section from '../../components/Section';
+
+const GeneralSection = ({ onClose }: { onClose: () => void }) => {
+ const { translate } = useTranslationContext();
+ return (
+
+ {/* TODO: Add appearance, language and support components */}
+ {translate('modals.preferences.sections.general.title')}
+
+ );
+};
+
+export default GeneralSection;
diff --git a/src/hooks/preferences/usePreferencesNavigation.tsx b/src/hooks/preferences/usePreferencesNavigation.tsx
index baef92e..189f51d 100644
--- a/src/hooks/preferences/usePreferencesNavigation.tsx
+++ b/src/hooks/preferences/usePreferencesNavigation.tsx
@@ -1,23 +1,11 @@
import { useSearchParams } from 'react-router-dom';
import { useCallback, useMemo } from 'react';
-import {
- PREFERENCES_DEFAULT_SECTIONS,
- type PreferencesActivePath,
- type PreferencesSection,
- type PreferencesSubsection,
-} from '@/types/preferences';
-
-const DEFAULT_SECTION: PreferencesActivePath = {
- section: 'general',
- subsection: 'general',
-};
+import { PREFERENCES_SECTIONS, type PreferencesSection } from '@/types/preferences';
-const isValidSection = (value: string): value is PreferencesSection => {
- return PREFERENCES_DEFAULT_SECTIONS.some((item) => item.section === value);
-};
+const DEFAULT_SECTION: PreferencesSection = 'general';
-const isValidSubsection = (section: string, value: string): value is PreferencesSubsection => {
- return PREFERENCES_DEFAULT_SECTIONS.some((item) => item.section === section && item.subsection === value);
+const isValidSection = (value: string): value is PreferencesSection => {
+ return PREFERENCES_SECTIONS.some((item) => item.id === value);
};
export const usePreferencesNavigation = () => {
@@ -25,20 +13,19 @@ export const usePreferencesNavigation = () => {
const isOpen = searchParams.get('preferences') === 'open';
- const activePath: PreferencesActivePath = useMemo(() => {
+ const activeSection: PreferencesSection = useMemo(() => {
const section = searchParams.get('section');
- const subsection = searchParams.get('subsection');
- if (section && isValidSection(section) && subsection && isValidSubsection(section, subsection)) {
- return { section, subsection };
+ if (section && isValidSection(section)) {
+ return section;
}
return DEFAULT_SECTION;
}, [searchParams]);
const openSection = useCallback(
- ({ section, subsection }: PreferencesActivePath) => {
- setSearchParams({ preferences: 'open', section, subsection }, { replace: true });
+ (section: PreferencesSection) => {
+ setSearchParams({ preferences: 'open', section }, { replace: true });
},
[setSearchParams],
);
@@ -47,5 +34,5 @@ export const usePreferencesNavigation = () => {
setSearchParams({}, { replace: true });
}, [setSearchParams]);
- return { isOpen, activePath, openSection, close };
+ return { isOpen, activeSection, openSection, close };
};
diff --git a/src/types/preferences/index.ts b/src/types/preferences/index.ts
index aba7d2e..5361065 100644
--- a/src/types/preferences/index.ts
+++ b/src/types/preferences/index.ts
@@ -1,20 +1,8 @@
export type PreferencesSection = 'general';
-export type PreferencesSubsection = 'general';
export interface PreferencesSectionItem {
- section: PreferencesSection;
- subsection: PreferencesSubsection;
- isSubsection?: boolean;
+ id: PreferencesSection;
+ group?: string;
}
-export interface PreferencesActivePath {
- section: PreferencesSection;
- subsection: PreferencesSubsection;
-}
-
-export const PREFERENCES_DEFAULT_SECTIONS: PreferencesSectionItem[] = [
- {
- section: 'general',
- subsection: 'general',
- },
-];
+export const PREFERENCES_SECTIONS: PreferencesSectionItem[] = [{ id: 'general' }];
From 6e56c87fbfaac736c02c3ab31d3573974871854a Mon Sep 17 00:00:00 2001
From: Xavier Abad <77491413+xabg2@users.noreply.github.com>
Date: Tue, 24 Mar 2026 17:38:49 +0100
Subject: [PATCH 3/7] feat: add settings component to open Preferences Dialog
---
src/features/mail/MailView.tsx | 8 +++-
.../mail/components/settings/index.tsx | 45 +++++++++++++++++++
src/routes/layouts/SidebarAndHeaderLayout.tsx | 3 ++
3 files changed, 55 insertions(+), 1 deletion(-)
create mode 100644 src/features/mail/components/settings/index.tsx
diff --git a/src/features/mail/MailView.tsx b/src/features/mail/MailView.tsx
index 3603606..8c6590b 100644
--- a/src/features/mail/MailView.tsx
+++ b/src/features/mail/MailView.tsx
@@ -4,6 +4,7 @@ import { getMockedMail } from '@/test-utils/fixtures';
import PreviewMail from './components/mail-preview';
import type { User } from './components/mail-preview/header';
import TrayList from './components/tray';
+import Settings from './components/settings';
interface MailViewProps {
folder: FolderType;
@@ -24,7 +25,12 @@ const MailView = ({ folder }: MailViewProps) => {
{/* Tray */}
{/* Mail Preview */}
-
+
);
};
diff --git a/src/features/mail/components/settings/index.tsx b/src/features/mail/components/settings/index.tsx
new file mode 100644
index 0000000..4256b20
--- /dev/null
+++ b/src/features/mail/components/settings/index.tsx
@@ -0,0 +1,45 @@
+import { usePreferencesNavigation } from '@/hooks/preferences/usePreferencesNavigation';
+import { useAppDispatch, useAppSelector } from '@/store/hooks';
+import { logoutThunk } from '@/store/slices/user/thunks';
+import { Avatar, Button, Dropdown } from '@internxt/ui';
+import { GearIcon } from '@phosphor-icons/react';
+
+const Settings = () => {
+ const dispatch = useAppDispatch();
+ const user = useAppSelector((state) => state.user.user);
+ const { openSection } = usePreferencesNavigation();
+
+ if (!user) return null;
+
+ const openPreferences = () => {
+ openSection('general');
+ };
+
+ const onLogOut = async () => {
+ await dispatch(logoutThunk());
+ };
+
+ const dropdownActions = [
+ {
+ name: 'Settings',
+ onClick: openPreferences,
+ },
+ {
+ name: 'Logout',
+ onClick: onLogOut,
+ },
+ ];
+
+ return (
+
+ );
+};
+
+export default Settings;
diff --git a/src/routes/layouts/SidebarAndHeaderLayout.tsx b/src/routes/layouts/SidebarAndHeaderLayout.tsx
index 4a49ecb..22d5233 100644
--- a/src/routes/layouts/SidebarAndHeaderLayout.tsx
+++ b/src/routes/layouts/SidebarAndHeaderLayout.tsx
@@ -1,6 +1,7 @@
import { Outlet } from 'react-router-dom';
import { Suspense } from 'react';
import Sidenav from '@/components/Sidenav';
+import { PreferencesDialog } from '@/components/preferences';
/**
* App layout (contains the static components like the sidebar)
@@ -15,6 +16,8 @@ const SidebarAndHeaderLayout = () => {
+
+
);
};
From 834009867246722118209d4a49000290c1979ee2 Mon Sep 17 00:00:00 2001
From: Xavier Abad <77491413+xabg2@users.noreply.github.com>
Date: Tue, 24 Mar 2026 18:17:30 +0100
Subject: [PATCH 4/7] feat: account popover
---
.../components/account-popover/index.tsx | 86 +++++++++++++++++++
.../mail/components/settings/index.tsx | 34 ++++----
src/i18n/locales/en.json | 5 ++
3 files changed, 107 insertions(+), 18 deletions(-)
create mode 100644 src/features/mail/components/settings/components/account-popover/index.tsx
diff --git a/src/features/mail/components/settings/components/account-popover/index.tsx b/src/features/mail/components/settings/components/account-popover/index.tsx
new file mode 100644
index 0000000..2886f17
--- /dev/null
+++ b/src/features/mail/components/settings/components/account-popover/index.tsx
@@ -0,0 +1,86 @@
+import { useTranslationContext } from '@/i18n';
+import type { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings';
+import { Avatar, Popover } from '@internxt/ui';
+import { GearIcon, SignOutIcon } from '@phosphor-icons/react';
+import type { ReactNode } from 'react';
+
+interface AccountPopoverProps {
+ className?: string;
+ user: UserSettings;
+ percentageUsed?: number;
+ onLogout: () => void;
+ openPreferences: () => void;
+}
+
+export default function AccountPopover({
+ className = '',
+ user,
+ percentageUsed,
+ onLogout,
+ openPreferences,
+}: Readonly): JSX.Element {
+ const { translate } = useTranslationContext();
+ const name = user?.name ?? '';
+ const lastName = user?.lastname ?? '';
+ const fullName = name + ' ' + lastName;
+
+ const avatarWrapper = ;
+
+ const separator = ;
+
+ const panel = (
+
+
+ {avatarWrapper}
+
+
+ {fullName}
+
+
+ {user.email}
+
+
+
+
+
+
{translate('accountPopover.spaceUsed', { space: percentageUsed })}
+
+ {separator}
+
+
-
+
+
+ {translate('accountPopover.logout')}
+
+
+
+ );
+
+ return (
+ panel} data-test="app-header-dropdown" />
+ );
+}
+
+interface ItemProps {
+ children: ReactNode;
+ onClick: () => void;
+}
+
+function Item({ children, onClick }: Readonly) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/features/mail/components/settings/index.tsx b/src/features/mail/components/settings/index.tsx
index 4256b20..2078542 100644
--- a/src/features/mail/components/settings/index.tsx
+++ b/src/features/mail/components/settings/index.tsx
@@ -1,43 +1,41 @@
import { usePreferencesNavigation } from '@/hooks/preferences/usePreferencesNavigation';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
-import { logoutThunk } from '@/store/slices/user/thunks';
-import { Avatar, Button, Dropdown } from '@internxt/ui';
+import { useGetStorageLimitQuery, useGetStorageUsageQuery } from '@/store/queries/storage/storage.query';
+import { Button } from '@internxt/ui';
import { GearIcon } from '@phosphor-icons/react';
+import AccountPopover from './components/account-popover';
+import { logoutThunk } from '@/store/slices/user/thunks';
const Settings = () => {
const dispatch = useAppDispatch();
const user = useAppSelector((state) => state.user.user);
+ const { data: usage } = useGetStorageUsageQuery();
+ const { data: limit } = useGetStorageLimitQuery();
const { openSection } = usePreferencesNavigation();
+ const percentageUsed = usage != null && limit ? Math.round((usage / limit) * 100) : 0;
+
if (!user) return null;
const openPreferences = () => {
openSection('general');
};
- const onLogOut = async () => {
- await dispatch(logoutThunk());
+ const onLogout = () => {
+ dispatch(logoutThunk());
};
- const dropdownActions = [
- {
- name: 'Settings',
- onClick: openPreferences,
- },
- {
- name: 'Logout',
- onClick: onLogOut,
- },
- ];
-
return (
);
};
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 69ed961..d30ecb2 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -136,5 +136,10 @@
}
}
}
+ },
+ "accountPopover": {
+ "spaceUsed": "{{space}}% space used",
+ "settings": "Settings",
+ "logout": "Log out"
}
}
From df459d16054f597f556d8c119ed3da64daec787e Mon Sep 17 00:00:00 2001
From: Xavier Abad <77491413+xabg2@users.noreply.github.com>
Date: Tue, 24 Mar 2026 18:42:16 +0100
Subject: [PATCH 5/7] fix: make percentage used mandatory
---
src/components/preferences/components/SectionItem.tsx | 8 ++++----
src/components/preferences/index.tsx | 9 ++++++---
.../settings/components/account-popover/index.tsx | 7 +++----
src/features/mail/components/settings/index.tsx | 2 +-
4 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/src/components/preferences/components/SectionItem.tsx b/src/components/preferences/components/SectionItem.tsx
index 7836d33..0ff960d 100644
--- a/src/components/preferences/components/SectionItem.tsx
+++ b/src/components/preferences/components/SectionItem.tsx
@@ -17,8 +17,8 @@ const SectionItem = ({
notificationsNumber,
onClick,
}: SectionItemProps) => {
- const isClickable = !!onClick;
- const clickableContainerClass = isClickable ? 'hover:bg-gray-1 hover:bg-gray-5' : '';
+ const isClickable = !!onClick && !isDisabled;
+ const clickableContainerClass = isClickable ? 'hover:bg-gray-5' : '';
const activeContainerClass = isActive ? 'bg-primary' : clickableContainerClass;
const containerClass = isDisabled ? '' : activeContainerClass;
const clickableClass = isClickable ? 'hover:cursor-pointer' : '';
@@ -35,7 +35,7 @@ const SectionItem = ({
@@ -44,7 +44,7 @@ const SectionItem = ({
{notificationsNumber && notificationsNumber > 0 && (
-
+
{notificationsNumber}
)}
diff --git a/src/components/preferences/index.tsx b/src/components/preferences/index.tsx
index 5af2f6e..4116bd0 100644
--- a/src/components/preferences/index.tsx
+++ b/src/components/preferences/index.tsx
@@ -18,9 +18,12 @@ export const PreferencesDialog = () => {
const title = translate(`modals.preferences.sections.${activeSection}.title`);
useEffect(() => {
- if (isOpen) {
- document.title = `${title} | Internxt Mail`;
- }
+ if (!isOpen) return;
+ const previousTitle = document.title;
+ document.title = `${title} | Internxt Mail`;
+ return () => {
+ document.title = previousTitle;
+ };
}, [isOpen, title]);
const ActiveSectionComponent = SECTION_COMPONENTS[activeSection];
diff --git a/src/features/mail/components/settings/components/account-popover/index.tsx b/src/features/mail/components/settings/components/account-popover/index.tsx
index 2886f17..3a2befa 100644
--- a/src/features/mail/components/settings/components/account-popover/index.tsx
+++ b/src/features/mail/components/settings/components/account-popover/index.tsx
@@ -7,7 +7,7 @@ import type { ReactNode } from 'react';
interface AccountPopoverProps {
className?: string;
user: UserSettings;
- percentageUsed?: number;
+ percentageUsed: number;
onLogout: () => void;
openPreferences: () => void;
}
@@ -74,13 +74,12 @@ interface ItemProps {
function Item({ children, onClick }: Readonly
) {
return (
-
{children}
-
+
);
}
diff --git a/src/features/mail/components/settings/index.tsx b/src/features/mail/components/settings/index.tsx
index 2078542..b568bab 100644
--- a/src/features/mail/components/settings/index.tsx
+++ b/src/features/mail/components/settings/index.tsx
@@ -13,7 +13,7 @@ const Settings = () => {
const { data: limit } = useGetStorageLimitQuery();
const { openSection } = usePreferencesNavigation();
- const percentageUsed = usage != null && limit ? Math.round((usage / limit) * 100) : 0;
+ const percentageUsed = usage != null && limit != null && limit > 0 ? Math.round((usage / limit) * 100) : 0;
if (!user) return null;
From 33cdfcfe3500e55a10dfbe94e7fe521e6fa46b89 Mon Sep 17 00:00:00 2001
From: Xavier Abad <77491413+xabg2@users.noreply.github.com>
Date: Tue, 24 Mar 2026 18:48:32 +0100
Subject: [PATCH 6/7] fix: remove useless JSX.Element cast
---
src/components/preferences/components/Section.tsx | 12 ++++--------
.../settings/components/account-popover/index.tsx | 2 +-
src/features/mail/components/settings/index.tsx | 2 +-
3 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/src/components/preferences/components/Section.tsx b/src/components/preferences/components/Section.tsx
index c23896b..0eed257 100644
--- a/src/components/preferences/components/Section.tsx
+++ b/src/components/preferences/components/Section.tsx
@@ -1,19 +1,15 @@
import { type ReactNode } from 'react';
import { CaretLeftIcon, XIcon } from '@phosphor-icons/react';
-const Section = ({
- className = '',
- children,
- title,
- onBackButtonClicked,
- onClose,
-}: {
+interface SectionProps {
className?: string;
children: ReactNode;
title: string;
onBackButtonClicked?: () => void;
onClose: () => void;
-}): JSX.Element => {
+}
+
+const Section = ({ className = '', children, title, onBackButtonClicked, onClose }: SectionProps) => {
return (
diff --git a/src/features/mail/components/settings/components/account-popover/index.tsx b/src/features/mail/components/settings/components/account-popover/index.tsx
index 3a2befa..0d67b98 100644
--- a/src/features/mail/components/settings/components/account-popover/index.tsx
+++ b/src/features/mail/components/settings/components/account-popover/index.tsx
@@ -18,7 +18,7 @@ export default function AccountPopover({
percentageUsed,
onLogout,
openPreferences,
-}: Readonly
): JSX.Element {
+}: Readonly) {
const { translate } = useTranslationContext();
const name = user?.name ?? '';
const lastName = user?.lastname ?? '';
diff --git a/src/features/mail/components/settings/index.tsx b/src/features/mail/components/settings/index.tsx
index b568bab..7f55683 100644
--- a/src/features/mail/components/settings/index.tsx
+++ b/src/features/mail/components/settings/index.tsx
@@ -27,7 +27,7 @@ const Settings = () => {
return (
-