Added redux. Moved theme loading/setting to redux actions. Added automatic theme loading based on localStorage value.
This commit is contained in:
parent
765418d3d2
commit
7199e296b8
14 changed files with 203 additions and 39 deletions
51
client/package-lock.json
generated
51
client/package-lock.json
generated
|
@ -2290,6 +2290,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz",
|
||||||
"integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA=="
|
"integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA=="
|
||||||
},
|
},
|
||||||
|
"@types/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"hoist-non-react-statics": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/html-minifier-terser": {
|
"@types/html-minifier-terser": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz",
|
||||||
|
@ -2388,6 +2397,17 @@
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-redux": {
|
||||||
|
"version": "7.1.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.16.tgz",
|
||||||
|
"integrity": "sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==",
|
||||||
|
"requires": {
|
||||||
|
"@types/hoist-non-react-statics": "^3.3.0",
|
||||||
|
"@types/react": "*",
|
||||||
|
"hoist-non-react-statics": "^3.3.0",
|
||||||
|
"redux": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/react-router": {
|
"@types/react-router": {
|
||||||
"version": "5.1.14",
|
"version": "5.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.14.tgz",
|
||||||
|
@ -12541,6 +12561,19 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"version": "7.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz",
|
||||||
|
"integrity": "sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.12.1",
|
||||||
|
"@types/react-redux": "^7.1.16",
|
||||||
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-refresh": {
|
"react-refresh": {
|
||||||
"version": "0.8.3",
|
"version": "0.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
|
||||||
|
@ -12773,6 +12806,24 @@
|
||||||
"strip-indent": "^3.0.0"
|
"strip-indent": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"redux": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"redux-devtools-extension": {
|
||||||
|
"version": "2.13.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz",
|
||||||
|
"integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A=="
|
||||||
|
},
|
||||||
|
"redux-thunk": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
|
||||||
|
},
|
||||||
"regenerate": {
|
"regenerate": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
||||||
|
|
|
@ -12,11 +12,16 @@
|
||||||
"@types/node": "^12.20.12",
|
"@types/node": "^12.20.12",
|
||||||
"@types/react": "^17.0.5",
|
"@types/react": "^17.0.5",
|
||||||
"@types/react-dom": "^17.0.3",
|
"@types/react-dom": "^17.0.3",
|
||||||
|
"@types/react-redux": "^7.1.16",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-redux": "^7.2.4",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
|
"redux": "^4.1.0",
|
||||||
|
"redux-devtools-extension": "^2.13.9",
|
||||||
|
"redux-thunk": "^2.3.0",
|
||||||
"typescript": "^4.2.4",
|
"typescript": "^4.2.4",
|
||||||
"web-vitals": "^1.1.2"
|
"web-vitals": "^1.1.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
.SettingsButton {
|
|
||||||
width: 35px;
|
|
||||||
height: 35px;
|
|
||||||
background-color: var(--color-accent);
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
left: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
opacity: 0.25;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.SettingsButton:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
|
@ -1,23 +1,30 @@
|
||||||
import { BrowserRouter, Route, Switch, Link } from 'react-router-dom';
|
import { BrowserRouter, Route, Switch } from 'react-router-dom';
|
||||||
|
import { setTheme } from './store/actions';
|
||||||
|
|
||||||
|
// Redux
|
||||||
|
import store from './store/store';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import classes from './App.module.css';
|
import classes from './App.module.css';
|
||||||
|
|
||||||
|
import Home from './components/Home/Home';
|
||||||
import Apps from './components/Apps/Apps';
|
import Apps from './components/Apps/Apps';
|
||||||
import Settings from './components/Settings/Settings';
|
import Settings from './components/Settings/Settings';
|
||||||
|
|
||||||
import Icon from './components/UI/Icon/Icon';
|
if (localStorage.theme) {
|
||||||
|
store.dispatch(setTheme(localStorage.theme));
|
||||||
|
}
|
||||||
|
|
||||||
const App = (): JSX.Element => {
|
const App = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<Provider store={store}>
|
||||||
<Switch>
|
<BrowserRouter>
|
||||||
<Route exact path='/' component={Apps} />
|
<Switch>
|
||||||
<Route exact path='/settings' component={Settings} />
|
<Route exact path='/' component={Home} />
|
||||||
</Switch>
|
<Route exact path='/settings' component={Settings} />
|
||||||
<Link to='/settings' className={classes.SettingsButton}>
|
</Switch>
|
||||||
<Icon icon='mdiCog' />
|
</BrowserRouter>
|
||||||
</Link>
|
</Provider>
|
||||||
</BrowserRouter>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
19
client/src/components/Home/Home.module.css
Normal file
19
client/src/components/Home/Home.module.css
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
.SettingsButton {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0.25;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SettingsButton:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
29
client/src/components/Home/Home.tsx
Normal file
29
client/src/components/Home/Home.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import Icon from '../UI/Icon/Icon';
|
||||||
|
|
||||||
|
import classes from './Home.module.css';
|
||||||
|
import { Container } from '../UI/Layout/Layout';
|
||||||
|
import Headline from '../UI/Headline/Headline';
|
||||||
|
|
||||||
|
const Home = (): JSX.Element => {
|
||||||
|
const dateAndTime = (): string => {
|
||||||
|
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||||
|
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
|
||||||
|
return `${days[date.getDay()]}, ${date.getDate()} of ${months[date.getMonth()]} ${date.getFullYear()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Headline title='Home' subtitle={dateAndTime()} />
|
||||||
|
<Link to='/settings' className={classes.SettingsButton}>
|
||||||
|
<Icon icon='mdiCog' />
|
||||||
|
</Link>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
|
@ -3,7 +3,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ThemePreview:hover {
|
.ThemePreview:hover {
|
||||||
|
@ -13,6 +13,7 @@
|
||||||
.ThemePreview p {
|
.ThemePreview p {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
|
color: var(--color-primary);
|
||||||
/* align-self: flex-start; */
|
/* align-self: flex-start; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect, Fragment } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import classes from './Themer.module.css';
|
import classes from './Themer.module.css';
|
||||||
|
|
||||||
|
@ -8,7 +9,13 @@ import ThemePreview from './ThemePreview';
|
||||||
import { Container } from '../UI/Layout/Layout';
|
import { Container } from '../UI/Layout/Layout';
|
||||||
import Headline from '../UI/Headline/Headline';
|
import Headline from '../UI/Headline/Headline';
|
||||||
|
|
||||||
const Themer = (): JSX.Element => {
|
import { setTheme } from '../../store/actions';
|
||||||
|
|
||||||
|
interface ComponentProps {
|
||||||
|
setTheme: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Themer = (props: ComponentProps): JSX.Element => {
|
||||||
useEffect((): void => {
|
useEffect((): void => {
|
||||||
if (localStorage.theme) {
|
if (localStorage.theme) {
|
||||||
applyTheme(localStorage.theme);
|
applyTheme(localStorage.theme);
|
||||||
|
@ -27,19 +34,20 @@ const Themer = (): JSX.Element => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Fragment>
|
||||||
<div>
|
<div>
|
||||||
<Headline
|
{/* <Headline
|
||||||
title='Themes'
|
title='Themes'
|
||||||
subtitle='Select new theme by clicking on it'
|
subtitle='Select new theme by clicking on it'
|
||||||
/>
|
/> */}
|
||||||
<div className={classes.ThemerGrid}>
|
<div className={classes.ThemerGrid}>
|
||||||
{themes.map((theme: Theme): JSX.Element => <ThemePreview theme={theme} applyTheme={applyTheme} />)}
|
{themes.map((theme: Theme): JSX.Element => <ThemePreview theme={theme} applyTheme={props.setTheme} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Fragment>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Themer;
|
|
||||||
|
export default connect(null, { setTheme })(Themer);
|
1
client/src/store/actions/actionTypes.ts
Normal file
1
client/src/store/actions/actionTypes.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const SET_THEME = 'SET_THEME';
|
2
client/src/store/actions/index.ts
Normal file
2
client/src/store/actions/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './theme';
|
||||||
|
export * from './actionTypes';
|
24
client/src/store/actions/theme.ts
Normal file
24
client/src/store/actions/theme.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { themes } from '../../components/Themer/themes.json';
|
||||||
|
import { Theme } from '../../interfaces/Theme';
|
||||||
|
import { SET_THEME } from './actionTypes';
|
||||||
|
|
||||||
|
export const setTheme = (themeName: string) => (dispatch: Dispatch) => {
|
||||||
|
const theme = themes.find((theme: Theme) => theme.name === themeName);
|
||||||
|
|
||||||
|
if (theme) {
|
||||||
|
localStorage.setItem('theme', themeName);
|
||||||
|
loadTheme(theme);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: SET_THEME,
|
||||||
|
payload: themeName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadTheme = (theme: Theme) => {
|
||||||
|
for (const [key, value] of Object.entries(theme.colors)) {
|
||||||
|
document.body.style.setProperty(`--color-${key}`, value);
|
||||||
|
}
|
||||||
|
}
|
9
client/src/store/reducers/index.ts
Normal file
9
client/src/store/reducers/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { combineReducers } from 'redux';
|
||||||
|
|
||||||
|
import themeReducer from './theme';
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
theme: themeReducer
|
||||||
|
})
|
||||||
|
|
||||||
|
export default rootReducer;
|
18
client/src/store/reducers/theme.ts
Normal file
18
client/src/store/reducers/theme.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { SET_THEME } from '../actions/actionTypes';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
theme: 'blues'
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeReducer = (state = initialState, action: any) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SET_THEME:
|
||||||
|
return {
|
||||||
|
theme: action.payload
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default themeReducer;
|
9
client/src/store/store.ts
Normal file
9
client/src/store/store.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { createStore, applyMiddleware } from 'redux';
|
||||||
|
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import rootReducer from './reducers';
|
||||||
|
const initialState = {};
|
||||||
|
|
||||||
|
const store = createStore(rootReducer, initialState, composeWithDevTools(applyMiddleware(thunk)));
|
||||||
|
|
||||||
|
export default store;
|
Loading…
Reference in a new issue