Skip to content

Commit

Permalink
refactor: rewrite state management (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamalston authored Feb 1, 2022
1 parent df89e4a commit 051990e
Show file tree
Hide file tree
Showing 22 changed files with 1,168 additions and 982 deletions.
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

0 comments on commit 051990e

Please sign in to comment.