diff --git a/src/components/preferences/components/Section.tsx b/src/components/preferences/components/Section.tsx
new file mode 100644
index 0000000..0eed257
--- /dev/null
+++ b/src/components/preferences/components/Section.tsx
@@ -0,0 +1,38 @@
+import { type ReactNode } from 'react';
+import { CaretLeftIcon, XIcon } from '@phosphor-icons/react';
+
+interface SectionProps {
+ className?: string;
+ children: ReactNode;
+ title: string;
+ onBackButtonClicked?: () => void;
+ onClose: () => void;
+}
+
+const Section = ({ className = '', children, title, onBackButtonClicked, onClose }: SectionProps) => {
+ 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..be1cd5a
--- /dev/null
+++ b/src/components/preferences/components/SectionItem.tsx
@@ -0,0 +1,35 @@
+export interface SectionItemProps {
+ text: string;
+ isActive?: boolean;
+ isSection?: boolean;
+ isDisabled?: boolean;
+ onClick?: () => void;
+}
+
+const SectionItem = ({ text, isActive, isDisabled, isSection, onClick }: SectionItemProps) => {
+ 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' : '';
+ const activeTextClass = isActive ? 'text-white' : 'text-gray-80';
+ const disabledTextClass = isDisabled ? 'text-gray-40' : activeTextClass;
+ const sectionTextClass = isSection ? 'font-semibold' : '';
+
+ const Element = isClickable ? 'button' : 'div';
+
+ return (
+
+
+ {text}
+
+
+ );
+};
+
+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..7eaf9e1
--- /dev/null
+++ b/src/components/preferences/components/SectionList.tsx
@@ -0,0 +1,25 @@
+import type { PreferencesSection, PreferencesSectionItem } from '@/types/preferences';
+import SectionItem from './SectionItem';
+
+interface SectionListProps {
+ sectionItems: (PreferencesSectionItem & { text: string })[];
+ activeSection: PreferencesSection;
+ onSelectSection: (section: PreferencesSection) => void;
+}
+
+const SectionList = ({ sectionItems, activeSection, onSelectSection }: SectionListProps) => {
+ return (
+
+ {sectionItems.map((item) => (
+ onSelectSection(item.id)}
+ />
+ ))}
+
+ );
+};
+
+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..361ada6
--- /dev/null
+++ b/src/components/preferences/components/SectionListWrapper.tsx
@@ -0,0 +1,21 @@
+import { useTranslationContext } from '@/i18n';
+import { PREFERENCES_SECTIONS, type PreferencesSection } from '@/types/preferences';
+import SectionList from './SectionList';
+
+interface SectionListWrapperProps {
+ activeSection: PreferencesSection;
+ onSelectSection: (section: PreferencesSection) => void;
+}
+
+const SectionListWrapper = ({ activeSection, onSelectSection }: SectionListWrapperProps) => {
+ const { translate } = useTranslationContext();
+
+ const sectionItems = PREFERENCES_SECTIONS.map((item) => ({
+ ...item,
+ text: translate(`modals.preferences.sections.${item.id}.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..4116bd0
--- /dev/null
+++ b/src/components/preferences/index.tsx
@@ -0,0 +1,46 @@
+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, activeSection, openSection, close } = usePreferencesNavigation();
+
+ const title = translate(`modals.preferences.sections.${activeSection}.title`);
+
+ useEffect(() => {
+ if (!isOpen) return;
+ const previousTitle = document.title;
+ document.title = `${title} | Internxt Mail`;
+ return () => {
+ document.title = previousTitle;
+ };
+ }, [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/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/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/components/account-popover/index.tsx b/src/features/mail/components/settings/components/account-popover/index.tsx
new file mode 100644
index 0000000..0d67b98
--- /dev/null
+++ b/src/features/mail/components/settings/components/account-popover/index.tsx
@@ -0,0 +1,85 @@
+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) {
+ 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 (
+
+ );
+}
diff --git a/src/features/mail/components/settings/index.tsx b/src/features/mail/components/settings/index.tsx
new file mode 100644
index 0000000..7f55683
--- /dev/null
+++ b/src/features/mail/components/settings/index.tsx
@@ -0,0 +1,43 @@
+import { usePreferencesNavigation } from '@/hooks/preferences/usePreferencesNavigation';
+import { useAppDispatch, useAppSelector } from '@/store/hooks';
+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 != null && limit > 0 ? Math.round((usage / limit) * 100) : 0;
+
+ if (!user) return null;
+
+ const openPreferences = () => {
+ openSection('general');
+ };
+
+ const onLogout = () => {
+ dispatch(logoutThunk());
+ };
+
+ return (
+
+ );
+};
+
+export default Settings;
diff --git a/src/hooks/preferences/usePreferencesNavigation.tsx b/src/hooks/preferences/usePreferencesNavigation.tsx
new file mode 100644
index 0000000..189f51d
--- /dev/null
+++ b/src/hooks/preferences/usePreferencesNavigation.tsx
@@ -0,0 +1,38 @@
+import { useSearchParams } from 'react-router-dom';
+import { useCallback, useMemo } from 'react';
+import { PREFERENCES_SECTIONS, type PreferencesSection } from '@/types/preferences';
+
+const DEFAULT_SECTION: PreferencesSection = 'general';
+
+const isValidSection = (value: string): value is PreferencesSection => {
+ return PREFERENCES_SECTIONS.some((item) => item.id === value);
+};
+
+export const usePreferencesNavigation = () => {
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const isOpen = searchParams.get('preferences') === 'open';
+
+ const activeSection: PreferencesSection = useMemo(() => {
+ const section = searchParams.get('section');
+
+ if (section && isValidSection(section)) {
+ return section;
+ }
+
+ return DEFAULT_SECTION;
+ }, [searchParams]);
+
+ const openSection = useCallback(
+ (section: PreferencesSection) => {
+ setSearchParams({ preferences: 'open', section }, { replace: true });
+ },
+ [setSearchParams],
+ );
+
+ const close = useCallback(() => {
+ setSearchParams({}, { replace: true });
+ }, [setSearchParams]);
+
+ return { isOpen, activeSection, openSection, close };
+};
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 66fd3b9..d30ecb2 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -113,6 +113,33 @@
"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"
+ }
+ }
}
+ },
+ "accountPopover": {
+ "spaceUsed": "{{space}}% space used",
+ "settings": "Settings",
+ "logout": "Log out"
}
}
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 = () => {
+
+
);
};
diff --git a/src/types/preferences/index.ts b/src/types/preferences/index.ts
new file mode 100644
index 0000000..5361065
--- /dev/null
+++ b/src/types/preferences/index.ts
@@ -0,0 +1,8 @@
+export type PreferencesSection = 'general';
+
+export interface PreferencesSectionItem {
+ id: PreferencesSection;
+ group?: string;
+}
+
+export const PREFERENCES_SECTIONS: PreferencesSectionItem[] = [{ id: 'general' }];