Skip to content

Commit 94ad5f5

Browse files
authored
Merge pull request #74 from webdevhome/add-options-menu
Add options menu
2 parents c6fbb02 + 69bf738 commit 94ad5f5

File tree

14 files changed

+1738
-742
lines changed

14 files changed

+1738
-742
lines changed

package-lock.json

Lines changed: 1433 additions & 607 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
{
22
"name": "webdevhome.github.io",
3-
"version": "2.2.1",
3+
"version": "2.3.0",
44
"scripts": {
55
"dev": "vite",
66
"build": "npm test && vite build",
77
"preview": "vite preview",
88
"test": "tsc && eslint src"
99
},
1010
"dependencies": {
11+
"@headlessui/react": "^2.1.2",
1112
"@mdi/react": "^1.4.0",
1213
"classnames": "^2.3.1",
1314
"react": "^18.0.0",

src/components/App/App.tsx

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import {
22
mdiArrowCollapseUp,
33
mdiArrowLeft,
44
mdiCheck,
5-
mdiFormatListChecks,
5+
mdiCogOutline,
6+
mdiListStatus,
67
mdiMagnify,
7-
mdiStickerTextOutline,
8+
mdiNoteTextOutline,
89
} from '@mdi/js'
910
import classNames from 'classnames'
1011
import { FC } from 'react'
@@ -18,24 +19,29 @@ import { FooterDivider } from '../Footer/FooterDivider'
1819
import { FooterGroup } from '../Footer/FooterGroup'
1920
import { AppAction } from '../Header/AppAction'
2021
import { AppHeader } from '../Header/AppHeader'
22+
import { AppMenu } from '../Header/AppMenu'
23+
import { AppMenuItem } from '../Header/AppMenuItem'
24+
import { MdiIcon } from '../Icon/MdiIcon'
2125
import { JumpLinks } from '../JumpLinks/JumpLinks'
2226
import { LinkGroup } from '../Links/LinkGroup'
2327
import { Search } from '../Search/Search'
2428
import { AppContent } from './AppContent'
29+
import { AppThemeSwitcher } from './AppThemeSwitcher'
2530
import { useCustomizeMode } from './useCustomizeMode'
2631
import { useSearchMode } from './useSearchMode'
27-
import { useThemeSwitcher } from './useThemeSwitcher'
32+
import { useTheme } from './useTheme'
2833
import { useToggleDescriptions } from './useToggleDescriptions'
2934

3035
export const WebdevHome: FC = () => {
3136
const customizeMode = useCustomizeMode()
3237
const searchMode = useSearchMode()
33-
const themeSwitcher = useThemeSwitcher()
3438
const toggleDescriptions = useToggleDescriptions()
3539
const isCurrentAppMode = useIsCurrentAppMode()
3640
const allLinks = useAllLinks()
3741
const hiddenLinksCount = useHiddenLinksCount()
3842

43+
useTheme()
44+
3945
function handleScrollTopClick() {
4046
const htmlEl = document.children.item(0)
4147
if (htmlEl === null) return
@@ -48,7 +54,7 @@ export const WebdevHome: FC = () => {
4854
<div
4955
className={classNames(
5056
'sticky top-0 left-0 right-0',
51-
'bg-gray-200 supports-backdrop:bg-gray-200/75',
57+
'bg-gray-200 supports-backdrop:bg-gray-100/75',
5258
'dark:bg-gray-900 dark:supports-backdrop:bg-gray-900/70',
5359
'border-b border-gray-300 dark:border-gray-600',
5460
'supports-backdrop:backdrop-blur',
@@ -69,23 +75,21 @@ export const WebdevHome: FC = () => {
6975
label="Search"
7076
action={searchMode.handleSearchAction}
7177
/>
72-
<AppAction
73-
icon={themeSwitcher.icon}
74-
active={false}
75-
label={themeSwitcher.title}
76-
action={themeSwitcher.switchTheme}
77-
/>
78-
<AppAction
79-
icon={mdiStickerTextOutline}
80-
active={toggleDescriptions.showDescriptions}
81-
label="Link info"
82-
action={toggleDescriptions.toggle}
83-
/>
84-
<AppAction
85-
icon={mdiFormatListChecks}
86-
label="Customize"
87-
action={customizeMode.handleCustomizeAction}
88-
/>
78+
<AppMenu icon={mdiCogOutline} label="Options">
79+
<AppMenuItem
80+
label="Customize links"
81+
icon={<MdiIcon path={mdiListStatus} />}
82+
action={customizeMode.handleCustomizeAction}
83+
/>
84+
<AppMenuItem
85+
label="Show link info"
86+
icon={<MdiIcon path={mdiNoteTextOutline} />}
87+
selected={toggleDescriptions.showDescriptions}
88+
action={toggleDescriptions.toggle}
89+
/>
90+
91+
<AppThemeSwitcher />
92+
</AppMenu>
8993
</>
9094
) : isCurrentAppMode(AppMode.search) ? (
9195
<>
@@ -95,39 +99,35 @@ export const WebdevHome: FC = () => {
9599
highlight
96100
action={searchMode.handleSearchAction}
97101
/>
98-
<AppAction
99-
icon={themeSwitcher.icon}
100-
active={false}
101-
label={themeSwitcher.title}
102-
action={themeSwitcher.switchTheme}
103-
/>
104-
<AppAction
105-
icon={mdiStickerTextOutline}
106-
active={toggleDescriptions.showDescriptions}
107-
label="Descriptions"
108-
action={toggleDescriptions.toggle}
109-
/>
102+
<AppMenu icon={mdiCogOutline} label="Options">
103+
<AppMenuItem
104+
label="Show link info"
105+
icon={<MdiIcon path={mdiNoteTextOutline} />}
106+
selected={toggleDescriptions.showDescriptions}
107+
action={toggleDescriptions.toggle}
108+
/>
109+
110+
<AppThemeSwitcher />
111+
</AppMenu>
110112
</>
111113
) : isCurrentAppMode(AppMode.customize) ? (
112114
<>
113-
<AppAction
114-
icon={themeSwitcher.icon}
115-
active={false}
116-
label={themeSwitcher.title}
117-
action={themeSwitcher.switchTheme}
118-
/>
119-
<AppAction
120-
icon={mdiStickerTextOutline}
121-
active={toggleDescriptions.showDescriptions}
122-
label="Descriptions"
123-
action={toggleDescriptions.toggle}
124-
/>
125115
<AppAction
126116
icon={mdiCheck}
127-
label="Save"
117+
label="Done"
128118
highlight
129119
action={customizeMode.handleCustomizeAction}
130120
/>
121+
<AppMenu icon={mdiCogOutline} label="Options">
122+
<AppMenuItem
123+
label="Show link info"
124+
icon={<MdiIcon path={mdiNoteTextOutline} />}
125+
selected={toggleDescriptions.showDescriptions}
126+
action={toggleDescriptions.toggle}
127+
/>
128+
129+
<AppThemeSwitcher />
130+
</AppMenu>
131131
</>
132132
) : null}
133133
</>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { MenuSection } from '@headlessui/react'
2+
import { mdiThemeLightDark, mdiWeatherNight, mdiWeatherSunny } from '@mdi/js'
3+
import { FC } from 'react'
4+
import { AppMenuHeader } from '../Header/AppMenuHeader'
5+
import { AppMenuItem } from '../Header/AppMenuItem'
6+
import { MdiIcon } from '../Icon/MdiIcon'
7+
import { useThemeSwitcher } from './useThemeSwitcher'
8+
9+
export const AppThemeSwitcher: FC = () => {
10+
const themeSwitcher = useThemeSwitcher()
11+
12+
return (
13+
<MenuSection className="flex flex-col gap-y-px">
14+
<AppMenuHeader title="Theme" />
15+
<AppMenuItem
16+
label="Light"
17+
icon={<MdiIcon path={mdiWeatherSunny} />}
18+
selected={themeSwitcher.isLightTheme}
19+
action={themeSwitcher.setLightTheme}
20+
/>
21+
<AppMenuItem
22+
label="Dark"
23+
icon={<MdiIcon path={mdiWeatherNight} />}
24+
selected={themeSwitcher.isDarkTheme}
25+
action={themeSwitcher.setDarkTheme}
26+
/>
27+
<AppMenuItem
28+
label="Use system theme"
29+
icon={<MdiIcon path={mdiThemeLightDark} />}
30+
selected={themeSwitcher.isAutoTheme}
31+
action={themeSwitcher.setAutoTheme}
32+
/>
33+
</MenuSection>
34+
)
35+
}

src/components/App/useTheme.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useEffect, useMemo, useState } from 'react'
2+
import { AppTheme } from '../../stores/appSettings/appSettingsReducer'
3+
import { useThemeSwitcher } from './useThemeSwitcher'
4+
5+
const prefersDarkQuery = '(prefers-color-scheme: dark)'
6+
7+
export function useTheme() {
8+
const { currentTheme } = useThemeSwitcher()
9+
10+
const [prefersDark, setPrefersDark] = useState<boolean>(
11+
() => matchMedia(prefersDarkQuery).matches,
12+
)
13+
14+
function handlePrefersColorSchemeChange(event: MediaQueryListEvent): void {
15+
setPrefersDark(event.matches)
16+
}
17+
18+
useEffect(() => {
19+
matchMedia(prefersDarkQuery).addEventListener(
20+
'change',
21+
handlePrefersColorSchemeChange,
22+
)
23+
24+
return () => {
25+
matchMedia(prefersDarkQuery).removeEventListener(
26+
'change',
27+
handlePrefersColorSchemeChange,
28+
)
29+
}
30+
}, [])
31+
32+
const effectiveTheme = useMemo((): AppTheme.light | AppTheme.dark => {
33+
if (currentTheme === AppTheme.auto) {
34+
return prefersDark ? AppTheme.dark : AppTheme.light
35+
}
36+
37+
return currentTheme
38+
}, [currentTheme, prefersDark])
39+
40+
useEffect(() => {
41+
const htmlElement = document.getElementsByTagName('html')[0]
42+
if (htmlElement === undefined) return
43+
44+
htmlElement.classList.toggle(
45+
AppTheme.light,
46+
effectiveTheme === AppTheme.light,
47+
)
48+
htmlElement.classList.toggle(
49+
AppTheme.dark,
50+
effectiveTheme === AppTheme.dark,
51+
)
52+
}, [effectiveTheme])
53+
}
Lines changed: 35 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,53 @@
1-
import { mdiThemeLightDark, mdiWeatherNight, mdiWeatherSunny } from '@mdi/js'
2-
import { useCallback, useEffect, useMemo, useState } from 'react'
1+
import { useCallback, useMemo } from 'react'
32
import { useAppDispatch, useAppSelector } from '../../stores'
4-
import {
5-
cycleTheme,
6-
setTheme,
7-
} from '../../stores/appSettings/appSettingsActions'
3+
import { setTheme } from '../../stores/appSettings/appSettingsActions'
84
import { AppTheme } from '../../stores/appSettings/appSettingsReducer'
95

106
export interface UseThemeSwitcherResult {
11-
icon: string
12-
title: string
13-
switchTheme: () => void
7+
currentTheme: AppTheme
8+
setLightTheme: () => void
9+
setDarkTheme: () => void
10+
setAutoTheme: () => void
11+
isLightTheme: boolean
12+
isDarkTheme: boolean
13+
isAutoTheme: boolean
1414
}
1515

16-
const prefersDarkQuery = '(prefers-color-scheme: dark)'
17-
1816
export function useThemeSwitcher(): UseThemeSwitcherResult {
1917
const dispatch = useAppDispatch()
2018
const currentTheme = useAppSelector((state) => state.appSettings.theme)
2119

22-
const [prefersDark, setPrefersDark] = useState<boolean>(
23-
() => matchMedia(prefersDarkQuery).matches,
24-
)
25-
26-
const [effectiveTheme, setEffectiveTheme] = useState<
27-
AppTheme.light | AppTheme.dark
28-
>(AppTheme.light)
29-
30-
function handlePrefersColorSchemeChange(event: MediaQueryListEvent): void {
31-
setPrefersDark(event.matches)
32-
}
33-
34-
useEffect(() => {
35-
matchMedia(prefersDarkQuery).addEventListener(
36-
'change',
37-
handlePrefersColorSchemeChange,
38-
)
39-
40-
return () => {
41-
matchMedia(prefersDarkQuery).removeEventListener(
42-
'change',
43-
handlePrefersColorSchemeChange,
44-
)
45-
}
46-
}, [])
20+
const setLightTheme = useCallback(() => {
21+
dispatch(setTheme(AppTheme.light))
22+
}, [dispatch])
4723

48-
// Set effectiveTheme
49-
useEffect(() => {
50-
if (currentTheme === AppTheme.auto) {
51-
setEffectiveTheme(prefersDark ? AppTheme.dark : AppTheme.light)
52-
return
53-
}
24+
const setDarkTheme = useCallback(() => {
25+
dispatch(setTheme(AppTheme.dark))
26+
}, [dispatch])
5427

55-
setEffectiveTheme(currentTheme)
56-
}, [currentTheme, prefersDark])
28+
const setAutoTheme = useCallback(() => {
29+
dispatch(setTheme(AppTheme.auto))
30+
}, [dispatch])
5731

58-
// Set className
59-
useEffect(() => {
60-
const htmlElement = document.getElementsByTagName('html')[0]
61-
if (htmlElement === undefined) return
62-
63-
htmlElement.classList.toggle(
64-
AppTheme.light,
65-
effectiveTheme === AppTheme.light,
66-
)
67-
htmlElement.classList.toggle(
68-
AppTheme.dark,
69-
effectiveTheme === AppTheme.dark,
70-
)
71-
}, [effectiveTheme])
72-
73-
const icon = useMemo((): string => {
74-
switch (currentTheme) {
75-
case AppTheme.light:
76-
return mdiWeatherSunny
77-
case AppTheme.dark:
78-
return mdiWeatherNight
79-
default:
80-
return mdiThemeLightDark
81-
}
32+
const isLightTheme = useMemo(() => {
33+
return currentTheme === AppTheme.light
8234
}, [currentTheme])
8335

84-
const title = useMemo(() => `Theme: ${currentTheme}`, [currentTheme])
36+
const isDarkTheme = useMemo(() => {
37+
return currentTheme === AppTheme.dark
38+
}, [currentTheme])
8539

86-
const switchTheme = useCallback((): void => {
87-
dispatch(setTheme(cycleTheme(currentTheme)))
88-
}, [currentTheme, dispatch])
40+
const isAutoTheme = useMemo(() => {
41+
return currentTheme === AppTheme.auto
42+
}, [currentTheme])
8943

90-
return { icon, title, switchTheme }
44+
return {
45+
currentTheme,
46+
setLightTheme,
47+
setDarkTheme,
48+
setAutoTheme,
49+
isLightTheme,
50+
isDarkTheme,
51+
isAutoTheme,
52+
}
9153
}

0 commit comments

Comments
 (0)