diff --git a/src/assets/appearance/dark.svg b/src/assets/appearance/dark.svg new file mode 100644 index 0000000..ada70d0 --- /dev/null +++ b/src/assets/appearance/dark.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/appearance/light.svg b/src/assets/appearance/light.svg new file mode 100644 index 0000000..e916047 --- /dev/null +++ b/src/assets/appearance/light.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/appearance/system.svg b/src/assets/appearance/system.svg new file mode 100644 index 0000000..549ee6c --- /dev/null +++ b/src/assets/appearance/system.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/preferences/components/Section.tsx b/src/components/preferences/components/PreferencesSection.tsx similarity index 88% rename from src/components/preferences/components/Section.tsx rename to src/components/preferences/components/PreferencesSection.tsx index 0eed257..852ed8f 100644 --- a/src/components/preferences/components/Section.tsx +++ b/src/components/preferences/components/PreferencesSection.tsx @@ -9,7 +9,7 @@ interface SectionProps { onClose: () => void; } -const Section = ({ className = '', children, title, onBackButtonClicked, onClose }: SectionProps) => { +const PreferencesSection = ({ className = '', children, title, onBackButtonClicked, onClose }: SectionProps) => { return (
@@ -25,7 +25,7 @@ const Section = ({ className = '', children, title, onBackButtonClicked, onClose
@@ -35,4 +35,4 @@ const Section = ({ className = '', children, title, onBackButtonClicked, onClose ); }; -export default Section; +export default PreferencesSection; diff --git a/src/components/preferences/sections/general/components/PreferenceSectionLayout.tsx b/src/components/preferences/sections/general/components/PreferenceSectionLayout.tsx new file mode 100644 index 0000000..986904d --- /dev/null +++ b/src/components/preferences/sections/general/components/PreferenceSectionLayout.tsx @@ -0,0 +1,29 @@ +import { CaretLeftIcon } from '@phosphor-icons/react'; +import { type ReactNode } from 'react'; + +interface SectionProps { + className?: string; + children: ReactNode; + title: string; + onBackButtonClicked?: () => void; +} + +const PreferenceSectionLayout = ({ className = '', children, title, onBackButtonClicked }: Readonly) => { + return ( +
+
+ {onBackButtonClicked && ( + + )} + {title} +
+ {children} +
+ ); +}; + +export default PreferenceSectionLayout; diff --git a/src/components/preferences/sections/general/components/appearance/index.tsx b/src/components/preferences/sections/general/components/appearance/index.tsx new file mode 100644 index 0000000..a7f35a1 --- /dev/null +++ b/src/components/preferences/sections/general/components/appearance/index.tsx @@ -0,0 +1,39 @@ +import { useThemeContext } from '@/context/theme/useThemeContext'; +import { useTranslationContext } from '@/i18n'; +import appearance_dark from '@/assets/appearance/dark.svg'; +import appearance_light from '@/assets/appearance/light.svg'; +import appearance_system from '@/assets/appearance/system.svg'; +import PreferenceSectionLayout from '../PreferenceSectionLayout'; +import ThemeButton from './theme-button'; +import type { Theme } from '@/context/theme/types'; + +const themes = [ + { theme: 'system', img: appearance_system }, + { theme: 'light', img: appearance_light }, + { theme: 'dark', img: appearance_dark }, +]; + +const Appearance = () => { + const { translate } = useTranslationContext(); + const { currentTheme, toggleTheme } = useThemeContext(); + + return ( + +
+
+ {themes.map((themeInfo) => ( + + ))} +
+
+
+ ); +}; + +export default Appearance; diff --git a/src/components/preferences/sections/general/components/appearance/theme-button/index.tsx b/src/components/preferences/sections/general/components/appearance/theme-button/index.tsx new file mode 100644 index 0000000..b733cc8 --- /dev/null +++ b/src/components/preferences/sections/general/components/appearance/theme-button/index.tsx @@ -0,0 +1,38 @@ +import type { Theme } from '@/context/theme/types'; +import { useTranslationContext } from '@/i18n'; + +interface ThemeButtonProps { + theme: Theme; + toggleTheme: (theme: Theme) => void; + isSelected: boolean; + img: string; +} + +const ThemeButton = ({ theme, toggleTheme, isSelected, img }: ThemeButtonProps) => { + const { translate } = useTranslationContext(); + + return ( + + ); +}; + +export default ThemeButton; diff --git a/src/components/preferences/sections/general/index.tsx b/src/components/preferences/sections/general/index.tsx index 765eb25..6bcf7bf 100644 --- a/src/components/preferences/sections/general/index.tsx +++ b/src/components/preferences/sections/general/index.tsx @@ -1,13 +1,18 @@ import { useTranslationContext } from '@/i18n'; -import Section from '../../components/Section'; +import PreferencesSection from '../../components/PreferencesSection'; +import Appearance from './components/appearance'; const GeneralSection = ({ onClose }: { onClose: () => void }) => { const { translate } = useTranslationContext(); return ( -
- {/* TODO: Add appearance, language and support components */} -

{translate('modals.preferences.sections.general.title')}

-
+ + {/* TODO: Add language and support components */} + + ); }; diff --git a/src/context/theme/ThemeProvider.tsx b/src/context/theme/ThemeProvider.tsx new file mode 100644 index 0000000..de841ed --- /dev/null +++ b/src/context/theme/ThemeProvider.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { LocalStorageService } from '@/services/local-storage'; +import { ThemeContext } from './useThemeContext'; +import type { Theme } from './types'; + +interface ThemeProviderProps { + children: React.ReactNode; +} + +export const ThemeProvider: React.FC = ({ children }) => { + const rootRef = useRef(document.getElementById('root')); + const stored = LocalStorageService.instance.get('theme') as Theme | null; + const defaultTheme = stored ?? 'system'; + const prefersDark = globalThis.matchMedia('(prefers-color-scheme: dark)').matches; + const [currentTheme, setCurrentTheme] = useState(defaultTheme); + const [checkoutTheme, setCheckoutTheme] = useState<'light' | 'dark'>(prefersDark ? 'dark' : 'light'); + + const toggleTheme = (theme: Theme) => setCurrentTheme(theme); + + const persistDarkTheme = (value: boolean) => { + LocalStorageService.instance.set('theme:isDark', value ? 'true' : 'false'); + }; + + useEffect(() => { + const root = rootRef.current; + if (!root || !currentTheme) return; + + const updateTheme = () => { + const prefersDark = globalThis.matchMedia('(prefers-color-scheme: dark)').matches; + + LocalStorageService.instance.set('theme', currentTheme); + + if (currentTheme === 'dark' || (currentTheme === 'system' && prefersDark)) { + root.style.backgroundImage = 'none'; + document.documentElement.classList.add('dark'); + setCheckoutTheme('dark'); + persistDarkTheme(true); + return; + } + + // fallback to light theme + root.style.backgroundImage = 'none'; + document.documentElement.classList.remove('dark'); + setCheckoutTheme('light'); + persistDarkTheme(false); + }; + + updateTheme(); + + const mediaQuery = globalThis.matchMedia('(prefers-color-scheme: dark)'); + mediaQuery.addEventListener('change', updateTheme); + + return () => mediaQuery.removeEventListener('change', updateTheme); + }, [currentTheme]); + + const themeContextValue = useMemo( + () => ({ + currentTheme, + checkoutTheme, + toggleTheme, + }), + [currentTheme, checkoutTheme], + ); + + return {children}; +}; diff --git a/src/context/theme/types/index.ts b/src/context/theme/types/index.ts new file mode 100644 index 0000000..a3d61e3 --- /dev/null +++ b/src/context/theme/types/index.ts @@ -0,0 +1 @@ +export type Theme = 'system' | 'light' | 'dark'; diff --git a/src/context/theme/useThemeContext.ts b/src/context/theme/useThemeContext.ts new file mode 100644 index 0000000..23bf211 --- /dev/null +++ b/src/context/theme/useThemeContext.ts @@ -0,0 +1,16 @@ +import { createContext, useContext } from 'react'; +import type { Theme } from './types'; + +interface ThemeContextProps { + currentTheme: Theme | undefined; + checkoutTheme: 'light' | 'dark' | undefined; + toggleTheme: (theme: Theme) => void; +} + +export const ThemeContext = createContext({ + currentTheme: undefined, + checkoutTheme: undefined, + toggleTheme: () => {}, +}); + +export const useThemeContext = (): ThemeContextProps => useContext(ThemeContext); diff --git a/src/features/mail/MailView.tsx b/src/features/mail/MailView.tsx index 8c6590b..6227bdb 100644 --- a/src/features/mail/MailView.tsx +++ b/src/features/mail/MailView.tsx @@ -25,7 +25,7 @@ const MailView = ({ folder }: MailViewProps) => { {/* Tray */} {/* Mail Preview */} -
+
diff --git a/src/features/welcome/index.tsx b/src/features/welcome/index.tsx index 72d10ea..35f0dcf 100644 --- a/src/features/welcome/index.tsx +++ b/src/features/welcome/index.tsx @@ -24,7 +24,7 @@ const WelcomePage = () => {
-

{translate('meet')}

+

{translate('title')}