Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: User setting to select preferred theme for the user - dark, light, system #1032

Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"jotai": "^2.9.3",
"js-cookie": "^3.0.5",
"lowlight": "^3.1.0",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.3",
Expand Down Expand Up @@ -71,4 +72,4 @@
"@types/turndown": "^5.0.4",
"typescript": "^5.3.3"
}
}
}
40 changes: 26 additions & 14 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { ChannelRedirect } from './utils/channel/ChannelRedirect'
import "cal-sans";
import { ThemeProvider } from './ThemeProvider'
import { Toaster } from 'sonner'
import { useStickyState } from './hooks/useStickyState'
import MobileTabsPage from './pages/MobileTabsPage'
import { UserProfile } from './components/feature/userSettings/UserProfile/UserProfile'

import { Appearance } from './components/feature/userSettings/Appearance/Appearance'
import { ThemeProvider as NextThemeProvider } from "next-themes"

const router = createBrowserRouter(
createRoutesFromElements(
Expand All @@ -27,6 +27,7 @@ const router = createBrowserRouter(
<Route path="settings" lazy={() => import('./pages/settings/Settings')}>
<Route index element={<UserProfile />} />
<Route path="profile" element={<UserProfile />} />
<Route path="appearance" element={<Appearance />} />
<Route path="users" lazy={() => import('./components/feature/userSettings/Users/AddUsers')} />
<Route path="frappe-hr" lazy={() => import('./pages/settings/Integrations/FrappeHR')} />
{/* <Route path="bots" lazy={() => import('./components/feature/userSettings/Bots')} /> */}
Expand All @@ -52,11 +53,22 @@ const router = createBrowserRouter(
)
function App() {

const [appearance, setAppearance] = useStickyState<'light' | 'dark'>('dark', 'appearance');
// const [appearance, setAppearance] = useStickyState<'light' | 'dark'>('dark', 'appearance');

// const toggleTheme = () => {
// setAppearance(appearance === 'dark' ? 'light' : 'dark');
// };

// const { theme, setTheme, resolvedTheme } = useTheme()

const toggleTheme = () => {
setAppearance(appearance === 'dark' ? 'light' : 'dark');
};
// const toggleTheme = () => {
// setTheme(theme === 'light' ? 'dark' : 'light');
// };
// console.log(resolvedTheme);

// useEffect(() => {
// setTheme(resolvedTheme)
// }, [])

// We need to pass sitename only if the Frappe version is v15 or above.

Expand All @@ -82,14 +94,14 @@ function App() {
>
<UserProvider>
<Toaster richColors />
<ThemeProvider
appearance={appearance}
// grayColor='slate'
accentColor='iris'
panelBackground='translucent'
toggleTheme={toggleTheme}>
<RouterProvider router={router} />
</ThemeProvider>
<NextThemeProvider attribute="class" defaultTheme='system' enableSystem={true} storageKey="app-theme">
<ThemeProvider
// grayColor='slate'
accentColor='iris'
panelBackground='translucent'>
<RouterProvider router={router} />
</ThemeProvider>
</NextThemeProvider>
</UserProvider>
</FrappeProvider>
)
Expand Down
42 changes: 15 additions & 27 deletions frontend/src/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,42 @@
/* eslint-disable react/jsx-no-useless-fragment */
import { Theme } from '@radix-ui/themes';
import { Theme, ThemeProps } from '@radix-ui/themes';
import React from 'react';
import { ThemeProps } from '@radix-ui/themes/dist/cjs/theme';
import { PropsWithChildren, useEffect } from 'react';
import { useTheme as useNextTheme } from "next-themes"

export type Theme = 'light' | 'dark'

export const ThemeProvider: React.FC<PropsWithChildren<ThemeProps>> = ({ children }) => {
const { theme } = useNextTheme()

interface ThemeProviderProps extends ThemeProps {
toggleTheme: () => void;
}
export const ThemeProvider: React.FC<PropsWithChildren<ThemeProviderProps>> = ({ children, toggleTheme, ...props }) => {
useEffect(() => {
const metaThemeColor = document.querySelector("meta[name=theme-color]");
switch (props.appearance) {
switch (theme) {
case 'light': {
if (document?.body) {
document.body.classList.remove('light', 'dark');
document.body.classList.add('light');
pranavmene2000 marked this conversation as resolved.
Show resolved Hide resolved
metaThemeColor?.setAttribute('content', '#FFFFFF');

}

if (document?.body) metaThemeColor?.setAttribute('content', '#FFFFFF');
break;
}
case 'dark': {
if (document?.body) {
document.body.classList.remove('light', 'dark');
document.body.classList.add('dark');
pranavmene2000 marked this conversation as resolved.
Show resolved Hide resolved
metaThemeColor?.setAttribute('content', '#191919');
}

if (document?.body) metaThemeColor?.setAttribute('content', '#191919');
break;
}
}
}, [props.appearance]);
}, [theme]);

return (
<Theme {...props}>
<ThemeContext.Provider value={{ appearance: props.appearance as 'light' | 'dark', toggleTheme }}>
<Theme>
<ThemeContext.Provider value={{ appearance: theme as Theme | 'system' }}>
{children}
</ThemeContext.Provider>
</Theme>
);
};

interface ThemeContextType {
appearance: 'light' | 'dark';
toggleTheme: () => void;
appearance: Theme | 'system';
}
export const ThemeContext = React.createContext<ThemeContextType>({
appearance: 'dark',
toggleTheme: () => { },
appearance: 'system',
});

export const useTheme = () => React.useContext(ThemeContext);
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useState } from "react"
import { Box, Button, Flex, Text, Card, RadioCards } from "@radix-ui/themes"
import { clsx } from 'clsx'
import { useTheme as useNextTheme } from "next-themes"
import type { Theme } from "@/ThemeProvider";
import { toast } from 'sonner'

export const Appearance = () => {
const { theme, setTheme, systemTheme } = useNextTheme();
const [localAppearance, setLocalAppearance] = useState<Theme | 'system'>(theme as Theme | 'system');

const handleLocalApperanceChange = (value: Theme | 'system') => {
setLocalAppearance(value);
}

const saveTheme = () => {
setTheme(localAppearance)
toast.success(`Theme switched to ${localAppearance === 'system' ? `${localAppearance}(${systemTheme})` : localAppearance}`)
}

return (
<Flex direction='column' gap='4' px='6' py='4'>
<Flex direction={'column'} gap='4'>
<Flex justify={'between'} align={'center'}>
<Flex direction='column' gap='0'>
<Text size='3' className={'font-semibold'}>Appearance</Text>
<Text size='1' color='gray'>Manage your Raven appearance</Text>
</Flex>
<Button onClick={saveTheme} type='submit'>
Save
</Button>
</Flex>

<Card className="p-0">
<Box className="flex justify-center items-center bg-slate-2 dark:bg-slate-3 py-10 px-6">
<RadioCards.Root onValueChange={handleLocalApperanceChange} defaultValue={localAppearance || theme} columns={{ initial: '1', sm: '1', md: '2', lg: '3' }} gap="6" size="3">
<RadioItem value="system" localAppearance={localAppearance} label="System default" imgURL="https://app.cal.com/theme-system.svg" />
<RadioItem value="light" localAppearance={localAppearance} label="Light" imgURL="https://app.cal.com/theme-light.svg" />
<RadioItem value="dark" localAppearance={localAppearance} label="Dark" imgURL="https://app.cal.com/theme-dark.svg" />
</RadioCards.Root>
</Box>
</Card>
</Flex>
</Flex>
)
}


interface RadioItemProps {
value: string,
localAppearance: string,
label: string,
imgURL: string
}

export const RadioItem = ({ value, localAppearance, label, imgURL }: RadioItemProps) => {
return (
<Flex direction="column" align="center" className={clsx("transition-transform duration-300", value === localAppearance ? 'scale-110' : '')}>
<RadioCards.Item className="p-0 cursor-pointer" value={value}>
<img src={imgURL} className="w-full h-auto" />
</RadioCards.Item>
<Text className="mt-2 text-[14px]" weight='medium' as="p">{label}</Text>
</Flex>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const SettingsSidebar = () => {
<Flex direction="column" gap='2' className='px-4'>
<SettingsGroup title="My Account" icon={LuUserCircle2}>
<SettingsSidebarItem title="Profile" to='profile' />
<SettingsSidebarItem title="Appearance" to='appearance' />
{/* <SettingsSidebarItem title="Preferences" to='preferences' /> */}
</SettingsGroup>
<SettingsSeparator />
Expand Down
22 changes: 1 addition & 21 deletions frontend/src/components/layout/Sidebar/SidebarHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useTheme } from '@/ThemeProvider'
import { commandMenuOpenAtom } from '@/components/feature/CommandMenu/CommandMenu'
import { Flex, IconButton, Text } from '@radix-ui/themes'
import { BiCommand, BiMoon, BiSun } from 'react-icons/bi'
import { BiCommand } from 'react-icons/bi'
import { useSetAtom } from 'jotai'

export const SidebarHeader = () => {
Expand All @@ -16,7 +15,6 @@ export const SidebarHeader = () => {
<Text as='span' size='6' className='cal-sans pl-1'>raven</Text>
<Flex align='center' gap='4' className='pr-1 sm:pr-0'>
<SearchButton />
<ColorModeToggleButton />
</Flex>
</Flex>
</header>
Expand All @@ -40,22 +38,4 @@ const SearchButton = () => {
<BiCommand className='text-lg' />
</IconButton>
)
}

const ColorModeToggleButton = () => {

const { appearance, toggleTheme } = useTheme()

return <Flex align='center' justify='center' pr='1'>
<IconButton
size={{ initial: '2', md: '1' }}
aria-label='Toggle theme'
title='Toggle theme'
color='gray'
className='text-gray-11 sm:hover:text-gray-12'
variant='ghost'
onClick={toggleTheme}>
{appearance === 'light' ? <BiMoon className='text-lg sm:text-base' /> : <BiSun className='text-lg sm:text-base' />}
</IconButton>
</Flex>
}
Loading