Skip to content

Commit

Permalink
feat: Dark Theme feature (#9)
Browse files Browse the repository at this point in the history
* build: added progress-bar dependency

* feat: added useProgressBar hook

* feat: enabled link navigation

* build: updated path module

* feat: added new logo

* build: added required dependency

* style: implemented theme system

* feat: added poping component utiity

* feat: implemented theme switcher

* feat: added theme context

* style: added dark theme
  • Loading branch information
sboy99 committed Jan 12, 2023
1 parent d4d2ca6 commit 83010de
Show file tree
Hide file tree
Showing 22 changed files with 371 additions and 43 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"prettier": "prettier --write ."
},
"dependencies": {
"@badrap/bar-of-progress": "^0.2.2",
"@headlessui/react": "^1.7.7",
"@heroicons/react": "^2.0.13",
"@mdx-js/loader": "^2.2.1",
"@mdx-js/react": "^2.2.1",
Expand Down
4 changes: 2 additions & 2 deletions src/components/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import GradientBG from '../utilities/gradient-bg';

export default function Header() {
return (
<div className="relative border-b border-slate-200">
<div className="relative border-b border-skin-base ">
<Navbar />
<div className="pointer-events-none absolute inset-0 -z-10 bg-dot-slate-500/30 [mask-image:linear-gradient(-90deg,white,rgba(255,255,255,0))]"></div>
<div className="pointer-events-none absolute inset-0 -z-20 bg-gradient-to-r from-white to-white/80 backdrop-blur-md"></div>
<div className="pointer-events-none absolute inset-0 -z-20 bg-gradient-to-br from-primary via-primary/90 to-primary/50 backdrop-blur-md"></div>
<GradientBG
className="absolute inset-0 -z-30 h-full w-full animate-bg-shift"
colors={[
Expand Down
2 changes: 1 addition & 1 deletion src/components/hero/links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const HeroLinks = [

export default function Links() {
return (
<div className="flex flex-wrap-reverse items-center gap-y-4 gap-x-4 font-semibold sm:gap-x-6">
<div className="flex flex-wrap-reverse items-center gap-y-4 gap-x-4 font-semibold text-skin-base sm:gap-x-6">
{HeroLinks.map((link) => (
<a
href={link.href}
Expand Down
12 changes: 10 additions & 2 deletions src/components/hero/logo-section.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { useThemeContext } from '@/context/theme-context';
import ForkAltIcon from '../icons/fork-alt';
import GithubAltIcon from '../icons/github-alt';

export default function LogoSection() {
const { isDark } = useThemeContext();
return (
<section className="relative hidden lg:block">
<section className="relative hidden text-skin-base lg:block">
<div className="absolute bottom-4 right-16 ">
<GithubAltIcon className="h-36 w-36" />
</div>
<ForkAltIcon className="h-96 w-96 rotate-6 fill-transparent stroke-slate-900 stroke-1 " />
<ForkAltIcon
className={`h-96 w-96 rotate-6 ${
isDark
? `text-skin-base`
: ` fill-transparent stroke-skin-base stroke-1 `
}`}
/>
</section>
);
}
11 changes: 7 additions & 4 deletions src/components/hero/text-section.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import Dot from '@/utilities/dot';
import TypoComp from '@/utilities/typo-component';
import Dot from '@utilities/dot';
import TypoComp from '@utilities/typo-component';
import Links from './links';

export default function TextSection() {
return (
<section className="space-y-6">
{/* icons */}
<div className="flex flex-wrap items-center gap-x-4 text-sm font-medium capitalize">
<div className="flex flex-wrap items-center gap-x-4 text-sm font-medium capitalize text-skin-base">
1 contributors
<Dot />
Open Source
<Dot />
Student community
</div>
<TypoComp className="prose-sm prose-h1:capitalize xs:prose-base">
<h1>Great place to start Open-Source Journey. Contribute today</h1>
<h1>
Great place to start <span className="text-accent">Open-Source</span>{' '}
Journey. Contribute today
</h1>
<p>
An open source project focuses on student learning, If you want to get
started with open source with Github you are on the right spot. Just
Expand Down
22 changes: 22 additions & 0 deletions src/components/icons/git-opener.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentProps } from 'react';

type Props = ComponentProps<'svg'>;

const GitOpenerIcon = (props: Props) => {
return (
<svg
role="img"
viewBox="0 0 190 218"
{...props}
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
xmlns="http://www.w3.org/2000/svg"
>
<title>GitOpener</title>
<path d="M189.067 54.69 95 .382.934 54.691v108.618L95 217.619l94.067-54.31V54.691ZM80.441 18.025l14.56-8.405 15.438 8.913.002 56.698c-.172 1.977-7.31 6.644-15.002 9.912-7.691-3.262-14.809-7.926-14.994-9.896l-.004-57.222Zm-6.464 3.732L8.934 59.31v99.382L95 208.381l86.067-49.69V59.309l-64.162-37.044.002 52.979c-.003 2.276-1.326 4.467-3.334 6.475-3.917 3.917-10.448 7.159-14.9 9.077v43.877c5.547 1.439 9.657 6.49 9.662 12.48l-.004 29.502c0 3.44-1.34 6.679-3.775 9.113a12.802 12.802 0 0 1-9.113 3.775 12.79 12.79 0 0 1-9.115-3.774 12.798 12.798 0 0 1-3.772-9.117l-.007-29.496c.005-5.781 4.235-10.935 9.658-12.448l.002-43.912c-6.734-2.898-18.23-8.834-18.228-15.55l-.004-53.49ZM95.534 140.73l.042-.001c3.477.073 6.29 2.93 6.294 6.431l-.003 29.496c-.003 3.429-2.997 6.423-6.424 6.423a6.37 6.37 0 0 1-4.543-1.881 6.379 6.379 0 0 1-1.88-4.545l-.008-29.49a6.402 6.402 0 0 1 1.888-4.549 6.398 6.398 0 0 1 4.408-1.885l.132.002.094-.001Z"></path>
</svg>
);
};

export default GitOpenerIcon;
5 changes: 3 additions & 2 deletions src/components/layouts/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type LayoutProps = React.HTMLAttributes<HTMLDivElement> & {

const Layout: React.FC<LayoutProps> = ({
children,
title = 'PBC Life',
title = 'Git Opener',
description = 'Open source student community to learn and getting started with open-source',
...props
}) => {
Expand All @@ -25,7 +25,8 @@ const Layout: React.FC<LayoutProps> = ({
</Head>
<div {...props}>
<Header />
{children}
{/* outlet */}
<div className="bg-skin-base">{children}</div>
<Footer />
</div>
</>
Expand Down
31 changes: 16 additions & 15 deletions src/components/navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import Image from 'next/image';
import PBCLifeLogo from '~/images/pbclifelogo.png';
import Container from '../layouts/container';
import GitOpenerIcon from '@/components/icons/git-opener';
import { useThemeContext } from '@/context/theme-context';
import { MoonIcon, SunIcon } from '@heroicons/react/24/solid';
import Container from '@layouts/container';
import Pop from '@utilities/pop';
import Themes from './theme';

export default function Navbar() {
const { isDark } = useThemeContext();
return (
<Container>
<Container className="z-10 flex items-center justify-between">
{/* logo */}
<div className="flex items-center gap-x-2 py-4">
<Image
src={PBCLifeLogo}
alt="logo"
placeholder="blur"
width={48}
height={48}
className="h-10 w-10 rounded-full object-cover object-center"
/>
<span className="text-2xl font-bold tracking-tight">PBC LIFE</span>
<div className="flex items-center gap-x-2 py-4 text-skin-base">
<GitOpenerIcon className="h-10 w-10 rotate-90 " />
<span className="text-2xl font-bold tracking-tight">Git Opener</span>
</div>
{/* social icons */}
<div className=""></div>
<div className="">
<Pop Icon={isDark ? MoonIcon : SunIcon} className="!z-50">
<Themes />
</Pop>
</div>
</Container>
);
}
53 changes: 53 additions & 0 deletions src/components/navbar/theme.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useThemeContext } from '@/context/theme-context';
import { ComputerDesktopIcon } from '@heroicons/react/24/outline';
import PopButton from '@utilities/pop-button';
import { FC } from 'react';

const Themes: FC = () => {
const { setIsThemeChanged } = useThemeContext();

function setDarkTheme() {
window.localStorage.setItem('prefers-theme', 'dark');
setIsThemeChanged((cng) => cng + 1);
}
function setLightTheme() {
window.localStorage.setItem('prefers-theme', 'light');
setIsThemeChanged((cng) => cng + 1);
}
function setSystemTheme() {
window.localStorage.clear();
setIsThemeChanged((cng) => cng + 1);
}

return (
<div className=" w-screen max-w-[16rem] space-y-2 p-4">
<h2 className="text-2xl font-bold capitalize tracking-tight text-skin-base">
Choose Theme
</h2>
{/* two buttons */}
<div className="flex w-full flex-wrap items-center justify-between">
<PopButton
onClick={setLightTheme}
className="w-[48%] rounded-md border border-skin-base bg-white px-6 py-3 capitalize text-neutral-800"
>
light
</PopButton>
<PopButton
onClick={setDarkTheme}
className="w-[48%] rounded-md border border-skin-base bg-neutral-900 px-6 py-3 capitalize text-white"
>
dark
</PopButton>
</div>
{/* sync with system */}
<PopButton
onClick={setSystemTheme}
className="flex items-center justify-center gap-x-2 rounded-full border border-skin-base bg-skin-base px-4 py-3 text-skin-base hover:bg-skin-shine"
>
<ComputerDesktopIcon className="h-5 w-5" /> Sync with System
</PopButton>
</div>
);
};

export default Themes;
4 changes: 1 addition & 3 deletions src/components/utilities/dot.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export default function Dot() {
return (
<span className="h-1 w-1 flex-shrink-0 rounded-full bg-slate-800"></span>
);
return <span className="h-1 w-1 flex-shrink-0 rounded-full bg-accent"></span>;
}
24 changes: 24 additions & 0 deletions src/components/utilities/pop-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Menu } from '@headlessui/react';
import { FC, ReactNode } from 'react';

type Props = {
children: ReactNode;
className: string;
onClick?: () => void;
};

const PopButton: FC<Props> = ({ children, onClick, className }) => {
return (
<Menu.Item
as="button"
onClick={onClick}
className={`font-lexend rounded-full px-6 py-2 font-semibold capitalize outline-none ${
className || ``
}`}
>
{children}
</Menu.Item>
);
};

export default PopButton;
37 changes: 37 additions & 0 deletions src/components/utilities/pop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Menu, Transition } from '@headlessui/react';
import React, { ComponentProps, FC } from 'react';

type PopProps = ComponentProps<'div'> & {
Icon: React.FC<React.ComponentProps<'svg'>>;
};

const Pop: FC<PopProps> = ({ children, Icon, className }) => {
return (
<Menu as={`div`} className={`relative z-0 ${className}`}>
{/* icon */}
<Menu.Button
className={`group rounded-full border-none p-1 outline-none `}
>
{<Icon className={`h-8 w-8 text-skin-base group-hover:text-accent`} />}
</Menu.Button>
{/* panel */}
<Transition
enter="transition duration-500 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-300 ease-out"
leaveFrom="transform scale-300 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Menu.Items
className={`absolute top-4 -right-4 z-20 rounded-lg border border-skin-base bg-skin-base shadow-md `}
>
<div className="pointer-events-none absolute right-5 h-5 w-5 origin-top-right rotate-45 rounded bg-accent"></div>
{children}
</Menu.Items>
</Transition>
</Menu>
);
};

export default Pop;
14 changes: 10 additions & 4 deletions src/components/utilities/typo-component.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { HTMLAttributes } from 'react';
import { ComponentProps } from 'react';

type TypoCompType = HTMLAttributes<HTMLDivElement> & {};
type TypoCompType = ComponentProps<'div'>;

export default function TypoComp({ className, ...props }: TypoCompType) {
// todo: implement theme system
return <div className={` prose max-w-2xl ${className || ``}`} {...props} />;
return (
<div
className={` prose max-w-2xl prose-headings:text-skin-base prose-p:font-medium prose-p:text-skin-muted ${
className || ``
}`}
{...props}
/>
);
}
72 changes: 72 additions & 0 deletions src/context/theme-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
ComponentProps,
Dispatch,
FC,
SetStateAction,
createContext,
useContext,
useEffect,
useState,
} from 'react';

interface ThemeStateType {
setIsThemeChanged: Dispatch<SetStateAction<number>>;
isDark: boolean;
}

const ThemeContext = createContext<ThemeStateType>({
isDark: false,
setIsThemeChanged: () => {},
});

type ThemeProps = ComponentProps<'div'>;
type Mode = 'light' | 'dark';

const ThemeProvider: FC<ThemeProps> = ({ children, className, ...props }) => {
const [isDark, setIsDark] = useState<boolean>(false);
const [isThemeChanged, setIsThemeChanged] = useState<number>(0);
const [mode, setMode] = useState<Mode>(`light`);
function mediaQueryListener(e: MediaQueryListEvent) {
if (e.matches) {
setMode(`dark`);
setIsDark(true);
} else {
setMode(`light`);
setIsDark(false);
}
}
useEffect(() => {
const darkModeMediaQuery = window.matchMedia(`prefers-color-scheme:dark`);

const isTokenFound = window.localStorage.getItem('prefers-theme');
if (isTokenFound) {
if (isTokenFound === 'dark') {
setMode('dark');
setIsDark(true);
} else {
setMode('light');
setIsDark(false);
}
} else darkModeMediaQuery.addEventListener('change', mediaQueryListener);
return () =>
darkModeMediaQuery.removeEventListener('change', mediaQueryListener);

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isThemeChanged]);

const value: ThemeStateType = {
isDark,
setIsThemeChanged,
};
return (
<ThemeContext.Provider value={value}>
<div className={`${mode} ${className || ``}`} {...props}>
{children}
</div>
</ThemeContext.Provider>
);
};

export default ThemeProvider;

export const useThemeContext = () => useContext(ThemeContext);
17 changes: 17 additions & 0 deletions src/hooks/useProgressBar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ProgressBar from '@badrap/bar-of-progress';
import Router from 'next/router';
import { useEffect } from 'react';

export default function useProgressBar(color: string | undefined) {
useEffect(() => {
const progress = new ProgressBar({
className: 'bar-of-progress',
color: color ?? `#16A34A`,
delay: 80,
size: 2,
});
Router.events.on('routeChangeStart', () => progress.start());
Router.events.on('routeChangeComplete', () => progress.finish());
Router.events.on('routeChangeError', () => progress.finish());
});
}
Loading

0 comments on commit 83010de

Please sign in to comment.