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

refactor: rewrite state management #29

Merged
merged 7 commits into from
Feb 1, 2022
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ Previous iteration: [v1](https://github.com/adamalston/v1)

<img float="left" height="370" src="images/desktop.png" alt="Desktop Preview" role="img" aria-label="desktop screenshot"> <img align="right" height="370" src="images/mobile.png" alt="Mobile Preview" role="img" aria-label="mobile screenshot">

This website's design is both simple and accessible. Dynamic particles create an experience that is interactive and visually inviting. The site offers two themes via a toggle, dark mode (default) and light mode. Once toggled, the selected theme should persist between tabs, windows, and page reloads.
I have designed this website to be simple and accessible. Dynamic particles create an experience that is interactive for visitors. The site offers two themes via a toggle, a dark theme (default) and a light theme. Once toggled, the selected theme should persist between tabs, windows, and page reloads.

Mobile support for the site ranges from 4 in. displays through 6.7 in. all the way up to 13 in. tablets.

## <img src="https://git.io/JUnUc" height="18"> Open Source

I made this website open source under the assumption that others would use the code to create their own websites. I only ask that this code be used with attribution as a significant amount of time was spent writing and optimizing it. Please give proper credit by linking back to [adamalston.com](https://www.adamalston.com/). Thanks!
I made this website open source under the assumption that others would use the code to create their own websites. I only ask that this code be used with attribution as a significant amount of time was spent writing and optimizing it. Please give proper credit by linking back to [adamalston.com](https://www.adamalston.com). Thanks!

<details>
<summary><b>Install &amp; Setup</b></summary>
Expand Down
1,617 changes: 857 additions & 760 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"homepage": "https://www.adamalston.com/",
"name": "v2",
"version": "2.1.0",
"version": "2.1.2",
"private": true,
"dependencies": {
"react": "^16.14.0",
Expand All @@ -14,9 +14,10 @@
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test:coverage": "react-scripts test --coverage --watchAll",
"eject": "react-scripts eject",
"lint": "eslint src --ext .js,.jsx --fix --max-warnings 0",
"format": "prettier --config ./.prettierrc --write \"./src/**/*.{js,jsx,css,scss,json}\" "
"lint": "eslint ./src --ext .js,.jsx --fix --max-warnings 0",
"format": "prettier --config ./.prettierrc --write \"./src/**/*.{js,jsx,css,scss,json}\""
},
"browserslist": {
"production": [
Expand Down Expand Up @@ -44,5 +45,11 @@
"hooks": {
"pre-commit": "npm run format && npm run lint"
}
},
"jest": {
"collectCoverageFrom": [
"**/*.{js,jsx}",
"!**/index.jsx"
]
}
}
40 changes: 36 additions & 4 deletions src/App/App.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,42 @@
import React from 'react';
import React, { useEffect, useState } from 'react';

import './App.scss';
import { AppProvider } from './AppContext';
import { Toggle, Content, Buttons, Footer, Particles } from 'components';
import './App.scss';
import config from './config';

const App = () => {
return (
<AppProvider>
const [isReady, setIsReady] = useState(false);
const [isMobile, setIsMobile] = useState(false);

const init = () => {
if (
window.matchMedia(
'(max-device-width: 820px) and (-webkit-min-device-pixel-ratio: 2)'
)?.matches
) {
setIsMobile(true);
}

// before the state refactoring, 'theme' had a boolean-ish ('true', 'false')
// value in localStorage, now 'theme' has a theme value ('dark', 'light'),
// to prevent the site from breaking, older 'theme' entries should be updated
const localStorageTheme = localStorage.getItem('theme');
if (localStorageTheme === 'true') {
localStorage.setItem('theme', 'dark');
} else if (localStorageTheme === 'false') {
localStorage.setItem('theme', 'light');
}

setIsReady(true);
};

useEffect(() => {
if (!isReady) init();
}, [isReady]);

return isReady ? (
<AppProvider config={config} isMobile={isMobile}>
<div className="app">
<Toggle />
<Content />
Expand All @@ -15,6 +45,8 @@ const App = () => {
<Particles />
</div>
</AppProvider>
) : (
<></>
);
};

Expand Down
56 changes: 41 additions & 15 deletions src/App/AppContext.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,48 @@
import React, { createContext } from 'react';
import React, { createContext, useReducer } from 'react';

import config from './config';
import usePersistentState from 'hooks/PersistentState';
import { dark, light } from 'themes/Theme';
import themes from 'appearance/themeOptions';

const AppContext = createContext({
isDark: Boolean,
setIsDark: () => {},
});
const initialState = {
config: {},
isMobile: false,
theme: themes.dark,
setTheme: () => {},
};

const actions = { SET_THEME: 'SET_THEME' };

const reducer = (state, action) => {
switch (action.type) {
case actions.SET_THEME:
return { ...state, theme: themes[action.value] };
default:
return state;
}
};

const AppContext = createContext(initialState);

const AppProvider = ({ config, isMobile, children }) => {
initialState.config = config;
initialState.isMobile = isMobile;

const supportedThemes = Object.keys(themes);
const localStorageTheme = localStorage.getItem('theme');

if (supportedThemes.includes(localStorageTheme)) {
initialState.theme = themes[localStorageTheme];
}

const AppProvider = ({ children }) => {
const [isDark, setIsDark] = usePersistentState('theme', true); // default: dark mode
const theme = isDark ? dark : light;
const isMobile = window.matchMedia(
'(max-device-width: 820px) and (-webkit-min-device-pixel-ratio: 2)'
)?.matches;
const [state, dispatch] = useReducer(reducer, initialState);

const value = { config, theme, isDark, setIsDark, isMobile };
const value = {
config: state.config,
isMobile: state.isMobile,
theme: state.theme,
setTheme: (value) => {
dispatch({ type: actions.SET_THEME, value });
},
};

return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
Expand Down
14 changes: 9 additions & 5 deletions src/App/config.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { GitHub, LinkedIn, Resume, Email } from 'icons';

const config = {
info: {
name: 'Adam Alston',
title: 'Software Engineer',
name: {
display: 'Adam Alston',
aria: 'My name is Adam Alston',
},
title: {
display: 'Software Engineer',
aria: 'I am a software engineer',
},
buttons: [
{
Expand All @@ -20,13 +24,13 @@ const config = {
},
{
href: 'https://drive.google.com/drive/folders/10k8NWflSYQ5laPzuWtK3bzUKzuOeas8i/',
aria: 'Visit Google Drive to view and download my resume',
aria: 'View my resume in Google Drive',
icon: <Resume />,
label: 'Resume',
},
{
href: 'mailto:aalston9@gmail.com',
aria: 'Send me an email with this template',
aria: 'Send me an email',
icon: <Email />,
label: 'Email',
},
Expand Down
48 changes: 44 additions & 4 deletions src/Index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe('application tests', () => {
expect(parent).toHaveAttribute('href', 'mailto:aalston9@gmail.com');
});

it('should toggle between dark and light themes', () => {
it('should toggle between the dark and light themes', () => {
const toggle = screen.getByTestId('toggle');
const particles = screen.getByTestId('particles');

Expand All @@ -111,16 +111,15 @@ describe('application tests', () => {

expect(particles).toBeVisible();
expect(particles).toHaveAccessibleName();
expect(particles).toHaveAccessibleDescription();

// site should default to dark theme
// site should default to the dark theme
expect(toggle).toBeChecked();
expect(particles).toHaveStyle({ backgroundColor: '#000' });

// click the toggle
fireEvent.click(toggle);

// light theme should be visible
// the light theme should be visible
expect(toggle).not.toBeChecked();
expect(particles).toHaveStyle({ backgroundColor: '#fff' });
});
Expand Down Expand Up @@ -162,3 +161,44 @@ describe('application tests', () => {
expect(footer).toHaveTextContent(/^Designed and built by Adam Alston$/);
});
});

describe('local storage tests', () => {
beforeEach(() => {
localStorage.clear();
});

it("should show the dark theme when 'theme' is set to 'true' in local storage", () => {
// set local storage item and render the app
localStorage.setItem('theme', true);
render(<App />);

// check that the local storage item has been updated correctly
expect(localStorage.getItem('theme')).toEqual('dark');
const particles = screen.getByTestId('particles');
expect(particles).toHaveStyle({ backgroundColor: '#000' });
});

it("should show the light theme when 'theme' is set to 'false' in local storage", () => {
// set local storage item and render the app
localStorage.setItem('theme', false);
render(<App />);

// check that the local storage item has been updated correctly
expect(localStorage.getItem('theme')).toEqual('light');
const particles = screen.getByTestId('particles');
expect(particles).toHaveStyle({ backgroundColor: '#fff' });
});

// https://testing-library.com/docs/react-testing-library/api/#rerender
it('should persist the light theme through an app re-render', () => {
const { rerender } = render(<App />);
localStorage.setItem('theme', 'light');

// re-render the app and check the theme
rerender(<App />);
const particles2 = screen.getByTestId('particles');

expect(localStorage.getItem('theme')).toEqual('light');
expect(particles2).toHaveStyle({ backgroundColor: '#fff' });
});
});
Loading