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

✨ feat: add light/dark theme switch #68

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
17 changes: 17 additions & 0 deletions src/app/events/handles/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { themes } from "styles"
import { Events } from 'types';

import { BaseEventHandle } from './base';

class AppHandleEvents extends BaseEventHandle {
constructor() {
super();

}

theme = (theme: keyof typeof themes) => {
this.emit(Events.APP_SET_THEME, theme);
}
}

export { AppHandleEvents };
2 changes: 2 additions & 0 deletions src/app/events/handles/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AppHandleEvents } from './app';
import { CanvasHandleEvents } from './canvas';
import { ContextMenuHandleEvents } from './context-menu';
import { SettingsHandleEvents } from './settings';
Expand All @@ -8,6 +9,7 @@ import { TemplateHandleEvents } from './template';
import { ExtensionsHandleEvents } from './extensions';

class Handles {
app = new AppHandleEvents();
canvas = new CanvasHandleEvents();
contextmenu = new ContextMenuHandleEvents();
settings = new SettingsHandleEvents();
Expand Down
55 changes: 42 additions & 13 deletions src/components/canvas/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { MouseEvent, useMemo, useState } from 'react';
import { useTheme } from 'styled-components';
import { Reorder } from 'framer-motion';

import {
Trash as TrashIcon,
Check as CheckIcon,
Moon as MoonIcon,
Sun as SunIcon,
X as CloseIcon,
} from '@styled-icons/feather';

Expand All @@ -27,6 +30,8 @@ const Canvas = () => {
const { sections, currentSection, previewMode } = useCanvas();
const [hasError, setHasError] = useState(false);

const currentTheme = useTheme();

const sectionIds = sections.map(section => section.id);
const hasSection = !!sections.length;

Expand All @@ -36,25 +41,49 @@ const Canvas = () => {
!previewMode && events.contextmenu.open(ContextMenus.SECTION, e);
};

const getNextThemeName = (themeName: string) => {
if(themeName === 'dark') return 'light';
return 'dark';
}

const handleSetTheme = () => {
events.app.theme(getNextThemeName(currentTheme.NAME));
}

return (
<OnlyClientSide>
<S.Container
onContextMenu={handleOpenContextMenu}
fullHeight={hasError || !hasSection}
>
{hasSection && !previewMode && (
<S.Wrapper>
<Tooltip position="left" content="Clear canvas" variant="danger">
<S.Button
aria-label="Clear canvas"
onClick={events.canvas.clear}
variant="warn"
>
<TrashIcon size={16} />
</S.Button>
</Tooltip>
</S.Wrapper>
)}
<S.Wrapper>
<Tooltip
position="left"
content={`Preview: ${getNextThemeName(currentTheme.NAME)} mode`}
variant="info"
>
<S.Button
aria-label={`Preview: ${getNextThemeName(currentTheme.NAME)} mode`}
onClick={() => handleSetTheme()}
>
{getNextThemeName(currentTheme.NAME) === 'dark' ? <MoonIcon size={16} /> : <SunIcon size={16} />}
</S.Button>
</Tooltip>

{hasSection && !previewMode && (

<Tooltip position="left" content="Clear canvas" variant="danger">
<S.Button
aria-label="Clear canvas"
onClick={events.canvas.clear}
variant="warn"
>
<TrashIcon size={16} />
</S.Button>
</Tooltip>

)}
</S.Wrapper>

<ErrorBoundary
fallback={<CanvasErrorFallback />}
Expand Down
8 changes: 8 additions & 0 deletions src/components/canvas/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const Container = styled.div<ContainerProps>`
padding-right: ${theme.spacings.small};
height: ${fullHeight ? '100%' : 'auto'};

border: 2px solid ${theme.colors.border};

&::-webkit-scrollbar {
width: 0.8rem;
overflow: hidden;
Expand Down Expand Up @@ -75,6 +77,12 @@ const buttonModifiers = {
color: ${theme.colors.secondary};
}
`,

info: (theme: DefaultTheme) => css`
&:hover {
color: ${theme.colors.primary};
}
`,
};

export const Button = styled.button<ButtonsProps>`
Expand Down
6 changes: 5 additions & 1 deletion src/contexts/canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@ const CanvasProvider = ({ children }: CanvasProviderProps) => {

return (
<CanvasContext.Provider
value={{ sections: canvas, currentSection, previewMode }}
value={{
sections: canvas,
currentSection,
previewMode,
}}
>
{children}
</CanvasContext.Provider>
Expand Down
25 changes: 22 additions & 3 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MouseEvent, useEffect } from 'react';
import React, { MouseEvent, useEffect, useState, useRef } from 'react';
import Head from 'next/head';

import { AppProps } from 'next/app';
Expand All @@ -10,22 +10,41 @@ import { config, events } from 'app';
import { ContextMenu, Modal } from 'components';

import { Features } from 'features';
import { theme, GlobalStyles } from 'styles';
import { themes, GlobalStyles } from 'styles';
import { Events } from 'types';

const App = ({ Component, pageProps }: AppProps) => {
const appUrl = config.general.urls.app;

const [currTheme, setCurrTheme] = useState(themes['dark']);
const isTransition = useRef(false);

const handlePreventRightClick = (e: MouseEvent) => {
e.preventDefault();

events.contextmenu.close();
};

const handleSetTheme = async(e: CustomEvent) => {
document.querySelectorAll(`ul`).forEach(el => el.classList.add('no-animate'));
if(!isTransition.current) {
isTransition.current = true;
setCurrTheme(themes[e.detail]);
document.body.animate([{}], { duration: 450, iterations: 1, direction: 'alternate'});
Promise.all(document.body.getAnimations().map((animation) => animation.finished)).then(async() => {
return new Promise((resolve) =>
resolve(document.querySelectorAll(`ul`).forEach(el => el.classList.remove('no-animate')))
).then(() => isTransition.current = false)
});
}}

useEffect(() => {
events.on('contextmenu', handlePreventRightClick);
events.on(Events.APP_SET_THEME, handleSetTheme);

return () => {
events.on('contextmenu', handlePreventRightClick);
events.off(Events.APP_SET_THEME, handleSetTheme);
};
}, []);

Expand All @@ -34,7 +53,7 @@ const App = ({ Component, pageProps }: AppProps) => {
'Beautify your github profile with this amazing tool, creating the readme your way in a simple and fast way! The best profile readme generator you will find!';

return (
<ThemeProvider theme={theme}>
<ThemeProvider theme={currTheme}>
<Head>
<title>{title}</title>

Expand Down
10 changes: 10 additions & 0 deletions src/styles/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,21 @@ const GlobalStyles = createGlobalStyle`

html {
font-size: 10px;
transition: background-color .45s linear !important;
}

ul.no-animate li {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
transition: none !important;
transform: none !important;
}

ul,
li {
list-style: none;
transition: 0 all linear;
}

a {
Expand Down
2 changes: 2 additions & 0 deletions src/styles/themes/default.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const defaultTheme = {
NAME: 'dark',

grid: {
container: '104rem',
},
Expand Down
17 changes: 16 additions & 1 deletion src/styles/themes/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
export { defaultTheme as theme } from './default';
import { DefaultTheme } from 'styled-components';
import { defaultTheme } from './default';
import { lightTheme } from './light';

const themes: ThemeObject = {
dark: defaultTheme,
light: lightTheme
}

type ThemeObject = {
[key: string]: DefaultTheme;
dark: DefaultTheme,
light: DefaultTheme
}

export { themes }
50 changes: 50 additions & 0 deletions src/styles/themes/light.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const lightTheme = {
NAME: 'light',

grid: {
container: '104rem',
},

border: {
width: '1px',
radius: '6px',
},

font: {
family:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'",

weights: {
normal: 400,
bold: 600,
},

sizes: {
xsmall: '1.2rem',
small: '1.4rem',
medium: '1.6rem',
large: '2.0rem',
xlarge: '2.6rem',
},
},

colors: {
primary: '#1f75d7',
secondary: '#3dd264',
tertiary: '#f78166',
border: '#30363d',
text: '#0d1117', // #0d1117 #c9d1d9
bg: '#eee',
error: '#f85149',
},

spacings: {
xsmall: '0.8rem',
small: '1.2rem',
medium: '1.6rem',
large: '2.0rem',
xlarge: '2.4rem',
},
}

export { lightTheme };
1 change: 1 addition & 0 deletions src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum Events {
CANVAS_REORDER_SECTIONS = 'canvas.section.reorder',
CANVAS_DUPLICATE_SECTION = 'canvas.section.duplicate',
CANVAS_CLEAR_SECTIONS = 'canvas.clear',
APP_SET_THEME = 'app.set.theme',

TEMPLATE_USE = 'template.use',
TEMPLATE_PREVIEW = 'template.preview',
Expand Down
43 changes: 41 additions & 2 deletions src/types/styled-components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,44 @@ type Theme = typeof theme;

declare module 'styled-components' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DefaultTheme extends Theme {}
}
export interface DefaultTheme extends Theme {
NAME: string,
grid: {
container: string
},
border: {
width: string,
radius: string
},
font: {
family: string,
weights: {
normal: number,
bold: number
},
sizes: {
xsmall: string,
small: string,
medium: string,
large: string,
xlarge: string
}
},
colors: {
primary: string,
secondary: string,
tertiary: string,
border: string,
text: string,
bg: string,
error: string
},
spacings: {
xsmall: string,
small: string,
medium: string,
large: string,
xlarge: string
}
}
};