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

Feature/dark mode swithc #8

Merged
merged 10 commits into from
Dec 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 13 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 26 additions & 15 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,32 @@ const RootLayout = ({
children
}: {
children: React.ReactNode
}) => (
<html lang="en" className="h-full">
<ThemeProvider>
<body className={cx(inter.className, 'flex flex-col h-full bg-secondary dark:bg-secondary-dark max-md:pb-48')}>
<Header />
<BottomNav />
<main className="relative flex-grow bg-primary dark:bg-primary-dark w-full l:max-w-[1352px] xl:max-w-[1524px] m-auto md:pt-24">
<CurvedCorner left />
<CurvedCorner right />
{children}
</main>
</body>
</ThemeProvider>
}) => {
const theme = {
menu: {
defaultProps: {
dismiss: {
itemPress: false
}
}
}
};

</html>
);
return (
<ThemeProvider value={theme}>
<html lang="en" className="h-full dark">
<body className={cx(inter.className, 'flex flex-col h-full bg-secondary dark:bg-secondary-dark max-md:pb-48')}>
<Header />
<BottomNav />
<main className="relative flex-grow bg-primary dark:bg-primary-dark w-full l:max-w-[1352px] xl:max-w-[1524px] m-auto md:pt-24">
<CurvedCorner left />
<CurvedCorner right />
{children}
</main>
</body>
</html>
</ThemeProvider>
);
};

export default RootLayout;
6 changes: 4 additions & 2 deletions src/components/TailwindMaterial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
TabsHeader,
TabsBody,
Tab,
TabPanel
TabPanel,
Switch
} from '@material-tailwind/react';

export {
Expand All @@ -17,5 +18,6 @@ export {
TabsHeader,
TabsBody,
Tab,
TabPanel
TabPanel,
Switch
};
5 changes: 3 additions & 2 deletions src/components/bottom-nav/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import React from 'react';
import Link from 'next/link';
import {
DocumentPlusIcon, MagnifyingGlassIcon, StarIcon, UserCircleIcon, UserGroupIcon
DocumentPlusIcon, MagnifyingGlassIcon, StarIcon, UserGroupIcon
} from '@heroicons/react/24/outline';

import Icon, { ICON_SIZE } from '../icon';
import ProfileMenu from '../profile-menu';

const BottomNav = () => (
<div className="fixed bottom-0 w-full h-48 flex items-center justify-around py-8 px-8 md:px-16 border-t bg-white z-10 dark:bg-gray-500 dark:border-gray-900 md:hidden">
<Link href="/communities"><Icon IconType={UserGroupIcon} size={ICON_SIZE.MEDIUM} title="Communities icon" isInteractable /></Link>
<Link href="/p"><Icon IconType={DocumentPlusIcon} size={ICON_SIZE.MEDIUM} title="Create post icon" isInteractable /></Link>
<Link href="/favorites"><Icon IconType={StarIcon} size={ICON_SIZE.MEDIUM} title="Favorites icon" isInteractable /></Link>
<button type="button" aria-label="Search"><Icon IconType={MagnifyingGlassIcon} size={ICON_SIZE.MEDIUM} title="Search icon" isInteractable /></button>
<Link href="/user"><Icon IconType={UserCircleIcon} size={ICON_SIZE.MEDIUM} title="User icon" isInteractable /></Link>
<ProfileMenu />
</div>
);

Expand Down
7 changes: 3 additions & 4 deletions src/components/header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { MagnifyingGlassIcon, StarIcon, UserCircleIcon } from '@heroicons/react/24/outline';
import { MagnifyingGlassIcon, StarIcon } from '@heroicons/react/24/outline';

import Icon, { ICON_SIZE } from '../icon';
import { InputField } from '../input';
import { LinkText } from '../text';
import ProfileMenu from '../profile-menu';

const Header = () => (
<header className="hidden md:flex items-center justify-between py-8 px-8 md:px-16 bg-primary dark:bg-primary-dark">
Expand Down Expand Up @@ -43,9 +44,7 @@ const Header = () => (
LeftIcon={MagnifyingGlassIcon}
className="w-240 lg:hover:w-500 lg:focus-within:w-500 transition-all"
/>
<button type="button" aria-label="User profile">
<Icon IconType={UserCircleIcon} size={ICON_SIZE.MEDIUM} title="User icon" isInteractable />
</button>
<ProfileMenu />
</div>
</header>
);
Expand Down
74 changes: 74 additions & 0 deletions src/components/profile-menu/index.tsx
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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".

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">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this a button with an aria-label to make it more accessible.

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good thought. Putting it under MenuHandler like you did makes a lot more sense.

However, since we want it to be an invisible button it'd be easier to use the default button element rather than one from MT that requires us to style it to make it invisible.

<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;
30 changes: 30 additions & 0 deletions src/components/switch/index.tsx
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;
3 changes: 3 additions & 0 deletions src/components/text/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ interface TextProps {

const H1 = ({ children, className, title }: TextProps) => <span className={cx('text-lg text-gray-900 dark:text-gray-100', className)} title={title}>{children}</span>;

const H2 = ({ children, className, title }: TextProps) => <span className={cx('text-md text-gray-900 dark:text-gray-100', className)} title={title}>{children}</span>;

const BodyTitle = ({ children, className, title }: TextProps) => <span className={cx('text-gray-900 dark:text-gray-100', className)} title={title}>{children}</span>;

const BodyText = ({ children, className, title }: TextProps) => <span className={cx('text-gray-600 dark:text-gray-200', className)} title={title}>{children}</span>;
Expand All @@ -21,6 +23,7 @@ const PaleLinkText = ({ children, className, title }: TextProps) => <span classN

export {
H1,
H2,
BodyTitle,
BodyText,
PaleBodyText,
Expand Down
21 changes: 21 additions & 0 deletions src/components/theme-switch/index.tsx
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 }) => (
<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;
24 changes: 24 additions & 0 deletions src/utils/clickAway.ts
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;
}
13 changes: 13 additions & 0 deletions src/utils/localstorage.ts
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;
}
7 changes: 7 additions & 0 deletions src/utils/theme.ts
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');
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's also preventing certain tailwind response threshold classes from working, it causes the bottom nav bar to overflow in narrow screens.
image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weirdly i dont get it. Or what do you mean it directly?

}
10 changes: 6 additions & 4 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import withMT from '@material-tailwind/react/utils/withMT';
import type { Config } from 'tailwindcss';

const spacing = {
0: '0',
Expand Down Expand Up @@ -32,7 +32,8 @@ const spacing = {
500: '500px'
};

const config = withMT({
const config: Config = {
darkMode: "class",
content: [
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
Expand Down Expand Up @@ -79,7 +80,8 @@ const config = withMT({
}
}
},
plugins: []
});
plugins: [
]
};

export default config;