-
Notifications
You must be signed in to change notification settings - Fork 5
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
Feature/dark mode swithc #8
Changes from all commits
c88473a
a73aa25
bf0addd
ee5d33d
4ce94c6
9c77b32
b6f0ca9
83679e6
0617f72
a22b781
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
'use client'; | ||
|
||
import { | ||
Menu, MenuHandler, MenuList | ||
} from '@material-tailwind/react'; | ||
import React, { useEffect, useState } from 'react'; | ||
import { UserCircleIcon } from '@heroicons/react/24/outline'; | ||
import { useTheme } from '@/utils/theme'; | ||
import Link from 'next/link'; | ||
import { useClickOutside } from '@/utils/clickAway'; | ||
import Icon, { ICON_SIZE } from '../icon'; | ||
import ThemeSwitch from '../theme-switch'; | ||
import { LinkText } from '../text'; | ||
|
||
const ProfileMenu = () => { | ||
const [theme, setTheme] = useTheme(); | ||
const [open, setOpen] = useState<boolean>(false); | ||
|
||
const ref = useClickOutside<HTMLUListElement>(() => open && setOpen(false)); | ||
|
||
useEffect(() => { | ||
document.documentElement.classList.remove('dark'); | ||
if (theme === 'dark') { | ||
document.documentElement.classList.add('dark'); | ||
} | ||
}, [theme]); | ||
|
||
const items: { | ||
item: React.JSX.Element | ||
}[] = [ | ||
{ | ||
item: | ||
( | ||
<Link href="/user/todo" className="justify-center flex"> | ||
<LinkText className="text-center w-full"> | ||
Profile | ||
</LinkText> | ||
</Link> | ||
) | ||
}, | ||
{ | ||
item: <ThemeSwitch key="theme-switcher" mode={theme as 'dark' | 'light'} onSwitch={setTheme} /> | ||
} | ||
]; | ||
|
||
return ( | ||
<div className="flex justify-center items-center"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please make this a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would not do that to the whole menu i would do a aria label to the button and the menu There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, good thought. Putting it under However, since we want it to be an invisible button it'd be easier to use the default |
||
<Menu | ||
placement="bottom" | ||
open={open} | ||
handler={setOpen} | ||
> | ||
<MenuHandler> | ||
<button | ||
type="button" | ||
aria-label="ProfileButton" | ||
className="bg-transparent" | ||
> | ||
<Icon IconType={UserCircleIcon} size={ICON_SIZE.MEDIUM} title="User icon" isInteractable /> | ||
</button> | ||
</MenuHandler> | ||
<MenuList ref={ref} className="w-full relative md:w-80 flex flex-col bg-primary dark:bg-primary-dark dark:border-gray-800 rounded-b-none rounded-t-md md:rounded-b-md md:rounded-t-none" aria-label="ProfileMenu"> | ||
{items.map(({ item }) => ( | ||
<div className="w-full mt-0 top-0 pt-0 bg-none border-t first:border-t-0 border-gray-500 z-50" key={item.key}> | ||
{item} | ||
</div> | ||
))} | ||
</MenuList> | ||
</Menu> | ||
</div> | ||
); | ||
}; | ||
|
||
export default ProfileMenu; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
'use client'; | ||
|
||
import { | ||
Button | ||
} from '@/components/TailwindMaterial'; | ||
import React from 'react'; | ||
import { H2 } from '../text'; | ||
|
||
const Switch = ({ | ||
active, onSwitch, icon, label | ||
}: | ||
{ | ||
active: boolean, onSwitch: (newState: boolean) => void, icon: React.JSX.Element, label: string | ||
}) => ( | ||
<Button | ||
onClick={() => { | ||
onSwitch(!active); | ||
}} | ||
className="w-full h-full bg-transparent dark:text-gray-200 text-gray-800 relative flex justify-center items-center py-8 z-10" | ||
> | ||
<div className="w-32 bg-gray-500 rounded-xl mr-8"> | ||
{icon} | ||
</div> | ||
<H2 className="text-gray-600 dark:text-gray-200 text-md"> | ||
{label} | ||
</H2> | ||
</Button> | ||
); | ||
|
||
export default Switch; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
'use client'; | ||
|
||
import React from 'react'; | ||
import { MoonIcon } from '@heroicons/react/24/outline'; | ||
import { MoonIcon as MoonIconSolid } from '@heroicons/react/24/solid'; | ||
import Switch from '../switch'; | ||
|
||
const ThemeSwitch = ({ mode, onSwitch }: { mode: 'dark' | 'light', onSwitch: (newState: 'dark' | 'light') => void }) => ( | ||
Pdzly marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<Switch | ||
active={mode === 'dark'} | ||
icon={( | ||
<div className="w-16 h-16 dark:translate-x-3/4 bg-gray-400 dark:bg-green-800 rounded-xl m-2 transition duration-300 p-2"> | ||
{mode === 'light' ? <MoonIcon className="w-full h-full" /> : <MoonIconSolid className="w-full h-full" />} | ||
</div> | ||
)} | ||
label="Dark Mode" | ||
onSwitch={() => onSwitch(mode === 'dark' ? 'light' : 'dark')} | ||
/> | ||
); | ||
|
||
export default ThemeSwitch; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { useEffect, useRef } from 'react'; | ||
|
||
export function useClickOutside<T extends HTMLElement>(callback: () => void) { | ||
const ref = useRef<T | null>(null); | ||
|
||
useEffect(() => { | ||
function handleClick(event: MouseEvent) { | ||
const el = ref?.current; | ||
// Do nothing if clicking ref's element or descendent elements | ||
if (!el || el.contains(event.target as Node)) { | ||
return; | ||
} | ||
|
||
callback(); | ||
} | ||
|
||
window?.addEventListener('mousedown', handleClick, { capture: true }); | ||
return () => { | ||
window?.removeEventListener('mousedown', handleClick, { capture: true }); | ||
}; | ||
}, [callback, ref]); | ||
|
||
return ref; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
'use client'; | ||
|
||
import { useEffect, useState } from 'react'; | ||
|
||
export function useLocalStorage(key: string, fallbackValue: string) { | ||
const [value, setValue] = useState(typeof localStorage === 'undefined' ? '' : (localStorage.getItem(key) || fallbackValue)); | ||
|
||
useEffect(() => { | ||
localStorage.setItem(key, value); | ||
}, [key, value]); | ||
|
||
return [value, setValue] as const; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
'use client'; | ||
|
||
import { useLocalStorage } from './localstorage'; | ||
|
||
export function useTheme() { | ||
return useLocalStorage('theme', typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Testing this with dark mode, I notice a delay as we always show light mode on initial page load and then half a second later it changes to dark mode. Almost like a flashbang effect. While we should definitely keep this switch, we need to find a way to allow tailwind to listen for the browser mode setting again. Something broke when adding MT but it makes no sense for them to block tailwind's default dark mode rules. Had to be some way around it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Weirdly i dont get it. Or what do you mean it directly? |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The menu dropdown stays open after page navigation. I'd prefer it closes when a user clicks on an option that takes them to another page. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be annoying for the user if he changes the darkmode. And i rather like that it stays open, if you didnt wanted to go there you can click to go somewhere else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right we don't close it when they change modes. I was thinking about when they navigate to their profile page from the menu dropdown for example. If they use the profile menu to navigate somewhere else then they're done using it.
It's also staying open if I click to show it but then click another link on the page, like "Create post".