Skip to content

Commit

Permalink
Merge pull request #32 from sereneinserenade/feat/system-theme-option
Browse files Browse the repository at this point in the history
feat: option to select system theme in theme dropdown
  • Loading branch information
jeelmakani authored Nov 6, 2022
2 parents 076169f + dd2d2bb commit bf56dce
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 48 deletions.
41 changes: 21 additions & 20 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { createTheme, NextUIProvider, getDocumentTheme } from '@nextui-org/react';
import { RecoilRoot } from 'recoil';
import { useRecoilState } from 'recoil';

import 'tippy.js/animations/shift-toward-subtle.css';

import Main from './Main'

import { themeState } from './Store'

import './Newtab.scss';
import { useThemeDetector } from './hooks';

const Newtab = () => {
const [isDark, setIsDark] = useState(false);
const prefersDarkTheme = useThemeDetector()

const [theme, setTheme] = useRecoilState(themeState)

const isDark = useMemo(() => theme === 'dark-theme', [theme])

const lightTheme = createTheme({ type: 'light' })

Expand All @@ -23,29 +30,23 @@ const Newtab = () => {
})

useEffect(() => {
// you can use any storage
let theme = window.localStorage.getItem('data-theme');
setIsDark(theme === 'dark-theme');
setTheme(theme!);
}, []);

const observer = new MutationObserver(() => {
let newTheme = getDocumentTheme(document?.documentElement);
setIsDark(newTheme === 'dark-theme');
});
useEffect(() => {
window.localStorage.setItem('data-theme', theme);
}, [theme])

// Observe the document theme changes
observer.observe(document?.documentElement, {
attributes: true,
attributeFilter: ['data-theme', 'style']
});
const calculatedTheme = useMemo(() => {
if (theme === 'system-theme') return prefersDarkTheme ? darkTheme : lightTheme

return () => observer.disconnect();
}, []);
return isDark ? darkTheme : lightTheme
}, [isDark, prefersDarkTheme, darkTheme, lightTheme, theme])

return (
<NextUIProvider theme={isDark ? darkTheme : lightTheme}>
<RecoilRoot>
<Main />
</RecoilRoot>
<NextUIProvider theme={calculatedTheme}>
<Main />
</NextUIProvider>
);
};
Expand Down
5 changes: 5 additions & 0 deletions src/Store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { atom } from 'recoil';

import { Note, QuickLink } from './types'

export const themeState = atom<string>({
key: 'themeState',
default: 'system-theme'
})

export const notesState = atom<Note[]>({
key: 'notesState',
default: []
Expand Down
74 changes: 52 additions & 22 deletions src/components/MainTop.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React, { useCallback, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'

import { Button, changeTheme, Tooltip, useTheme } from '@nextui-org/react'
import { Button, Dropdown, Tooltip, useTheme } from '@nextui-org/react'
import { saveAs } from 'file-saver'
import { BiExport, BiImport } from 'react-icons/bi'
import { FiHome } from 'react-icons/fi'
import { RiAddLine, RiMenuFoldFill, RiMenuUnfoldFill, RiMoonFill, RiPrinterLine, RiSunFill } from 'react-icons/ri'
import { RiAddLine, RiComputerLine, RiMenuFoldFill, RiMenuUnfoldFill, RiMoonFill, RiPrinterLine, RiSunFill } from 'react-icons/ri'
import { useRecoilState, useRecoilValue } from 'recoil'
import { v4 as uuidv4 } from 'uuid'

import { activeNoteState, binNotesState, notesState, sidebarActiveState } from '../Store'
import { activeNoteState, binNotesState, notesState, sidebarActiveState, themeState } from '../Store'
import { Note, QuickLink } from '../types'
import { ImportDataModal } from './data'

Expand Down Expand Up @@ -64,14 +64,6 @@ const MainTop = () => {
setActiveNote(undefined)
}

const onThemeChange = () => {
const nextTheme = isDark ? 'light-theme' : 'dark-theme';

changeTheme(nextTheme);

window.localStorage.setItem('data-theme', nextTheme); // I think local storage is good enough for this
}

const printEditorContent = () => {
const printContents = document.querySelector(".editor-content")?.innerHTML;

Expand Down Expand Up @@ -113,14 +105,33 @@ const MainTop = () => {
})
}, [])

const [themeSelection, setThemeSelection] = useState<string>('system-theme')

const onThemeChange = (theme: string) => {
setTheme(theme)
}

const onDropdownThemeChange = (keys: any) => {
let theme = [...keys][0]

setThemeSelection(theme)
onThemeChange(theme)
}

const [theme, setTheme] = useRecoilState(themeState)

useEffect(() => {
setThemeSelection(theme)
}, [theme])

return (
<section className={`main-top flex`} aria-label="main-top-section">
<section className='left-controls flex' aria-label='left-controls-section'>
<Tooltip placement='bottomStart' content={sidebarActive ? 'Close Sidebar' : 'Open Sidebar'}>
<Button color="primary" auto ghost size='sm' onClick={onSidebarControlButtonClicked} className={`sidebar-control-button flex`} icon={sidebarActive ? <RiMenuFoldFill /> : <RiMenuUnfoldFill />} />
</Tooltip>
<Tooltip placement='bottomStart' content={'Home'}>
<Button color="primary" auto ghost size='sm' onClick={() => goHome()} className="sidebar-control-button flex" icon={< FiHome />} />
<Button color="primary" auto ghost size='sm' onClick={goHome} className="sidebar-control-button flex" icon={< FiHome />} />
</Tooltip>
<Tooltip placement='bottomStart' content={'Create new note'}>
<Button
Expand Down Expand Up @@ -157,15 +168,34 @@ const MainTop = () => {
</>)
}

<Button
color="success"
size='sm'
auto
flat
onClick={onThemeChange}
rounded
icon={isDark ? <RiSunFill /> : <RiMoonFill />}
/>
<Dropdown>
<Dropdown.Button
size='sm'
auto
flat
rounded
icon={isDark ? <RiSunFill /> : <RiMoonFill />}
/>

<Dropdown.Menu
aria-label='Theme Selection Dropdown'
color="primary"
disallowEmptySelection
selectionMode='single'
selectedKeys={[themeSelection]}
onSelectionChange={onDropdownThemeChange}
>
<Dropdown.Item key={'system-theme'} icon={<RiComputerLine />}>
System
</Dropdown.Item>
<Dropdown.Item key={'light-theme'} icon={<RiSunFill />}>
Light
</Dropdown.Item>
<Dropdown.Item key={'dark-theme'} icon={<RiMoonFill />}>
Dark
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</section>
</section>
)
Expand Down
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useThemeDetector'
18 changes: 18 additions & 0 deletions src/hooks/useThemeDetector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useEffect, useState } from 'react'

export const useThemeDetector = () => {
const getCurrentTheme = () => window.matchMedia("(prefers-color-scheme: dark)").matches
const [isDarkTheme, setIsDarkTheme] = useState(getCurrentTheme())

const mqListener = (e: MediaQueryListEvent) => setIsDarkTheme(e.matches)

useEffect(() => {
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)")

darkThemeMq.addEventListener('change', mqListener)

return () => darkThemeMq.removeEventListener('change', mqListener)
}, [])

return isDarkTheme
}
17 changes: 11 additions & 6 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { createRoot } from 'react-dom/client';
import { createRoot } from 'react-dom/client'
import { RecoilRoot } from 'recoil'

import App from './App';
import './index.scss';
import App from './App'
import './index.scss'

const container = document.getElementById('root');
const container = document.getElementById('root')

const root = createRoot(container!); // createRoot(container!) if you use TypeScript
root.render(<App />);
const root = createRoot(container!)
root.render(
<RecoilRoot>
<App />
</RecoilRoot>
);

0 comments on commit bf56dce

Please sign in to comment.