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' }];