diff --git a/package-lock.json b/package-lock.json index c27e344..575f2ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2065,53 +2065,24 @@ "fastq": "^1.6.0" } }, - "@redux-saga/core": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.1.3.tgz", - "integrity": "sha512-8tInBftak8TPzE6X13ABmEtRJGjtK17w7VUs7qV17S8hCO5S3+aUTWZ/DBsBJPdE8Z5jOPwYALyvofgq1Ws+kg==", - "requires": { - "@babel/runtime": "^7.6.3", - "@redux-saga/deferred": "^1.1.2", - "@redux-saga/delay-p": "^1.1.2", - "@redux-saga/is": "^1.1.2", - "@redux-saga/symbols": "^1.1.2", - "@redux-saga/types": "^1.1.0", - "redux": "^4.0.4", - "typescript-tuple": "^2.2.1" - } - }, - "@redux-saga/deferred": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.1.2.tgz", - "integrity": "sha512-908rDLHFN2UUzt2jb4uOzj6afpjgJe3MjICaUNO3bvkV/kN/cNeI9PMr8BsFXB/MR8WTAZQq/PlTq8Kww3TBSQ==" - }, - "@redux-saga/delay-p": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.1.2.tgz", - "integrity": "sha512-ojc+1IoC6OP65Ts5+ZHbEYdrohmIw1j9P7HS9MOJezqMYtCDgpkoqB5enAAZrNtnbSL6gVCWPHaoaTY5KeO0/g==", - "requires": { - "@redux-saga/symbols": "^1.1.2" - } - }, - "@redux-saga/is": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.2.tgz", - "integrity": "sha512-OLbunKVsCVNTKEf2cH4TYyNbbPgvmZ52iaxBD4I1fTif4+MTXMa4/Z07L83zW/hTCXwpSZvXogqMqLfex2Tg6w==", + "@reduxjs/toolkit": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.5.0.tgz", + "integrity": "sha512-E/FUraRx+8guw9Hlg/Ja8jI/hwCrmIKed8Annt9YsZw3BQp+F24t5I5b2OWR6pkEHY4hn1BgP08FrTZFRKsdaQ==", "requires": { - "@redux-saga/symbols": "^1.1.2", - "@redux-saga/types": "^1.1.0" + "immer": "^8.0.0", + "redux": "^4.0.0", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0" + }, + "dependencies": { + "immer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.0.tgz", + "integrity": "sha512-jm87NNBAIG4fHwouilCHIecFXp5rMGkiFrAuhVO685UnMAlOneEAnOyzPt8OnP47TC11q/E7vpzZe0WvwepFTg==" + } } }, - "@redux-saga/symbols": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.2.tgz", - "integrity": "sha512-EfdGnF423glv3uMwLsGAtE6bg+R9MdqlHEzExnfagXPrIiuxwr3bdiAwz3gi+PsrQ3yBlaBpfGLtDG8rf3LgQQ==" - }, - "@redux-saga/types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.1.0.tgz", - "integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg==" - }, "@stylelint/postcss-css-in-js": { "version": "0.37.2", "resolved": "https://registry.npmjs.org/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz", @@ -15000,23 +14971,15 @@ "symbol-observable": "^1.2.0" } }, - "redux-devtools-extension": { - "version": "2.13.8", - "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz", - "integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==" - }, "redux-persist": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==" }, - "redux-saga": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.3.tgz", - "integrity": "sha512-RkSn/z0mwaSa5/xH/hQLo8gNf4tlvT18qXDNvedihLcfzh+jMchDgaariQoehCpgRltEm4zHKJyINEz6aqswTw==", - "requires": { - "@redux-saga/core": "^1.1.3" - } + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" }, "regenerate": { "version": "1.4.2", @@ -15286,6 +15249,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" + }, "resolve": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.0.tgz", @@ -17519,27 +17487,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz", "integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==" }, - "typescript-compare": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", - "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", - "requires": { - "typescript-logic": "^0.0.0" - } - }, - "typescript-logic": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", - "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" - }, - "typescript-tuple": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", - "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", - "requires": { - "typescript-compare": "^0.0.2" - } - }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", diff --git a/package.json b/package.json index b36995f..c782c6a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@ionic/react": "^5.5.2", "@ionic/react-router": "^5.5.2", "@ionic/storage": "^2.3.1", + "@reduxjs/toolkit": "^1.5.0", "@testing-library/jest-dom": "5.11.6", "@testing-library/react": "^11.2.2", "@testing-library/user-event": "^12.6.0", @@ -47,9 +48,7 @@ "react-router-dom": "5.2.0", "react-scripts": "3.4.3", "redux": "^4.0.5", - "redux-devtools-extension": "^2.13.8", "redux-persist": "^6.0.0", - "redux-saga": "^1.1.3", "typescript": "4.0.5" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index 4392fd0..f372e34 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import React, { Suspense } from 'react'; import { IonApp, IonRouterOutlet, IonSplitPane } from '@ionic/react'; import { IonReactRouter } from '@ionic/react-router'; import { Redirect, Route } from 'react-router-dom'; -import { connect } from 'react-redux'; +import { useSelector } from 'react-redux'; import { Menu } from './components/Menu'; import Page from './pages/Page'; import { ChapterPage } from './pages/Chapter'; @@ -24,20 +24,21 @@ import '@ionic/react/css/display.css'; import './theme/bright.theme.css'; import './theme/dark.theme.css'; // import Tutorial from './pages/Tutorial'; -import Settings from './pages/Settings'; +import { SettingsPage } from './pages/Settings'; import AboutPage from './pages/About'; import { SearchPage } from './pages/Search'; -import { Theme } from './utils/theme'; +import { THEMES } from './utils/theme'; +import { selectCurrentTheme } from './data/user/user.selector'; +import { RootState } from './store'; -interface ContainerProps { - hasSeenTutorial: boolean; - selectedTheme: Theme; -} +const App: React.FC = () => { + const currentTheme = useSelector((state) => + selectCurrentTheme(state as RootState), + ); -const App: React.FC = ({ selectedTheme }) => { return ( - + @@ -73,7 +74,7 @@ const App: React.FC = ({ selectedTheme }) => { { - return ; + return ; }} exact={true} /> @@ -90,17 +91,4 @@ const App: React.FC = ({ selectedTheme }) => { ); }; -const mapStateToProps = (state: any) => { - return { - hasSeenTutorial: state.user.hasSeenTutorial, - selectedTheme: state.user.selectedTheme, - }; -}; - -const mapDispatchToProps = (dispatch: any) => { - return { - // setSelectedPage: (page: string) => dispatch(setSelectedPage(page)), - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(App); +export default App; diff --git a/src/components/Chapters/01/01.tsx b/src/components/Chapters/01/01.tsx index 5c52838..074f0c2 100644 --- a/src/components/Chapters/01/01.tsx +++ b/src/components/Chapters/01/01.tsx @@ -11,7 +11,7 @@ import { } from '@ionic/react'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import ChapterFooter from '../footer'; +import { ChapterFooter } from '../footer'; import { ChapterProps } from '../types'; export const Chapter0101: React.FC = ({ isCard = false }) => { diff --git a/src/components/Chapters/01/02.tsx b/src/components/Chapters/01/02.tsx index 9341657..29af63c 100644 --- a/src/components/Chapters/01/02.tsx +++ b/src/components/Chapters/01/02.tsx @@ -11,7 +11,7 @@ import { IonText, } from '@ionic/react'; import { useTranslation } from 'react-i18next'; -import ChapterFooter from '../footer'; +import { ChapterFooter } from '../footer'; import { ChapterProps } from '../types'; export const Chapter0102: React.FC = ({ isCard = false }) => { diff --git a/src/components/Chapters/01/03.tsx b/src/components/Chapters/01/03.tsx index e246788..76f517b 100644 --- a/src/components/Chapters/01/03.tsx +++ b/src/components/Chapters/01/03.tsx @@ -11,7 +11,7 @@ import { IonText, } from '@ionic/react'; import { useTranslation } from 'react-i18next'; -import ChapterFooter from '../footer'; +import { ChapterFooter } from '../footer'; import { ChapterProps } from '../types'; export const Chapter0103: React.FC = ({ isCard = false }) => { diff --git a/src/components/Chapters/01/04.tsx b/src/components/Chapters/01/04.tsx index 21d859e..b5be928 100644 --- a/src/components/Chapters/01/04.tsx +++ b/src/components/Chapters/01/04.tsx @@ -11,7 +11,7 @@ import { IonText, } from '@ionic/react'; import { useTranslation } from 'react-i18next'; -import ChapterFooter from '../footer'; +import { ChapterFooter } from '../footer'; import { ChapterProps } from '../types'; export const Chapter0104: React.FC = ({ isCard = false }) => { diff --git a/src/components/Chapters/02/01.tsx b/src/components/Chapters/02/01.tsx index 96a23f1..6642655 100644 --- a/src/components/Chapters/02/01.tsx +++ b/src/components/Chapters/02/01.tsx @@ -11,7 +11,7 @@ import { IonText, } from '@ionic/react'; import { useTranslation } from 'react-i18next'; -import ChapterFooter from '../footer'; +import { ChapterFooter } from '../footer'; import { ChapterProps } from '../types'; export const Chapter0201: React.FC = (props) => { diff --git a/src/components/Chapters/02/02.tsx b/src/components/Chapters/02/02.tsx index 1a694b5..4288198 100644 --- a/src/components/Chapters/02/02.tsx +++ b/src/components/Chapters/02/02.tsx @@ -11,7 +11,7 @@ import { IonText, } from '@ionic/react'; import { useTranslation } from 'react-i18next'; -import ChapterFooter from '../footer'; +import { ChapterFooter } from '../footer'; import { ChapterProps } from '../types'; export const Chapter0202: React.FC = (props) => { diff --git a/src/components/Chapters/02/03.tsx b/src/components/Chapters/02/03.tsx index 348556f..4c8d447 100644 --- a/src/components/Chapters/02/03.tsx +++ b/src/components/Chapters/02/03.tsx @@ -11,7 +11,7 @@ import { IonText, } from '@ionic/react'; import { useTranslation } from 'react-i18next'; -import ChapterFooter from '../footer'; +import { ChapterFooter } from '../footer'; import { ChapterProps } from '../types'; export const Chapter0203: React.FC = (props) => { diff --git a/src/components/Chapters/02/04.tsx b/src/components/Chapters/02/04.tsx index 316b785..6e1650a 100644 --- a/src/components/Chapters/02/04.tsx +++ b/src/components/Chapters/02/04.tsx @@ -11,7 +11,7 @@ import { IonText, } from '@ionic/react'; import { useTranslation } from 'react-i18next'; -import ChapterFooter from '../footer'; +import { ChapterFooter } from '../footer'; import { ChapterProps } from '../types'; export const Chapter0204: React.FC = (props) => { diff --git a/src/components/Chapters/02/05.tsx b/src/components/Chapters/02/05.tsx index 7490d2f..fe9f92c 100644 --- a/src/components/Chapters/02/05.tsx +++ b/src/components/Chapters/02/05.tsx @@ -11,7 +11,7 @@ import { IonText, } from '@ionic/react'; import { useTranslation } from 'react-i18next'; -import ChapterFooter from '../footer'; +import { ChapterFooter } from '../footer'; import { ChapterProps } from '../types'; export const Chapter0205: React.FC = (props) => { diff --git a/src/components/Chapters/02/06.tsx b/src/components/Chapters/02/06.tsx index 568a8aa..f59729a 100644 --- a/src/components/Chapters/02/06.tsx +++ b/src/components/Chapters/02/06.tsx @@ -11,7 +11,7 @@ import { IonText, } from '@ionic/react'; import { useTranslation } from 'react-i18next'; -import ChapterFooter from '../footer'; +import { ChapterFooter } from '../footer'; import { ChapterProps } from '../types'; export const Chapter0206: React.FC = (props) => { diff --git a/src/components/Chapters/footer.tsx b/src/components/Chapters/footer.tsx index 51fe814..8c63972 100644 --- a/src/components/Chapters/footer.tsx +++ b/src/components/Chapters/footer.tsx @@ -8,33 +8,35 @@ import { } from '@ionic/react'; import { arrowBack, arrowForward, star, starOutline } from 'ionicons/icons'; import React from 'react'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router'; import { addFavorite, removeFavorite, } from '../../data/chapter/chapter.actions'; -import { selectChapterFavorites } from '../../data/chapter/chapter.select'; -import { RootState } from '../../reducers'; +import { selectIsChapterFavorite } from '../../data/chapter/chapter.select'; +import { RootState } from '../../store'; -type ContainerProps = ReturnType & - ReturnType & { - previousChapter: string; - nextChapter: string; - }; +type ContainerProps = { + previousChapter: string; + nextChapter: string; +}; -const ChapterFooter: React.FC = (props) => { - const { chapterFavorites, previousChapter, nextChapter } = props; +export const ChapterFooter: React.FC = (props) => { + const { previousChapter, nextChapter } = props; const history = useHistory(); const currentChapter = history.location.pathname; - const isFavorite = chapterFavorites.includes(currentChapter); + const isFavorite = useSelector((state) => + selectIsChapterFavorite(state as RootState, currentChapter), + ); + const dispatch = useDispatch(); const toggleFavorite = () => { if (isFavorite) { - return props.removeFavorite(currentChapter); + return dispatch(removeFavorite(currentChapter)); } - return props.addFavorite(currentChapter); + return dispatch(addFavorite(currentChapter)); }; return ( @@ -85,18 +87,3 @@ const ChapterFooter: React.FC = (props) => { ); }; - -const mapStateToProps = (state: RootState) => { - return { - chapterFavorites: selectChapterFavorites(state), - }; -}; - -const mapDispatchToProps = (dispatch: any) => { - return { - addFavorite: (chapterId: string) => dispatch(addFavorite(chapterId)), - removeFavorite: (chapterId: string) => dispatch(removeFavorite(chapterId)), - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ChapterFooter); diff --git a/src/configureStore.tsx b/src/configureStore.tsx deleted file mode 100644 index c4fa663..0000000 --- a/src/configureStore.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { routerMiddleware, RouterState } from 'connected-react-router'; -import { applyMiddleware, createStore } from 'redux'; -import { composeWithDevTools } from 'redux-devtools-extension'; -import { persistStore, persistReducer } from 'redux-persist'; -import createSagaMiddleware from 'redux-saga'; - -import createRootReducer from './reducers'; -import history from './utils/history'; -import createCapacitorStorage from './utils/capacitorStorage'; -import { GlobalState } from './data/global/global.reducer'; -import globalSaga from './data/global/global.saga'; - -export const persistConfig = { - blacklist: ['router'], - debug: false, - key: 'root', - storage: createCapacitorStorage(), - version: 1, -}; - -const persistedReducer = persistReducer(persistConfig, createRootReducer()); - -const configureStore = () => { - const sagaMiddleware = createSagaMiddleware(); - - // Create the store with two middlewares - // 1. sagaMiddleware: Makes redux-sagas work - // 2. routerMiddleware: Syncs the location/URL path to the state - const middlewares = [sagaMiddleware, routerMiddleware(history)]; - - let enhancer = applyMiddleware(...middlewares); - - // If Redux Dev Tools and Saga Dev Tools Extensions are installed, enable them - /* istanbul ignore next */ - if (process.env.NODE_ENV !== 'production' && typeof window === 'object') { - enhancer = composeWithDevTools(enhancer); - } - - const store = createStore( - persistedReducer, - enhancer, - ); - - sagaMiddleware.run(globalSaga); - - const persistor = persistStore(store); - - return { store, persistor }; -}; - -export default configureStore; diff --git a/src/data/chapter/chapter.select.tsx b/src/data/chapter/chapter.select.tsx index 74c0167..8eede5c 100644 --- a/src/data/chapter/chapter.select.tsx +++ b/src/data/chapter/chapter.select.tsx @@ -1,4 +1,4 @@ -import { RootState } from '../../reducers'; +import { RootState } from '../../store'; export const selectChapterState = (state: RootState) => { return state.chapter; @@ -12,7 +12,8 @@ export const selectChapterFavorites = (state: RootState) => { export const selectIsChapterFavorite = ( state: RootState, chapterId: string, -) => { +): boolean => { const chapterFavorites = selectChapterFavorites(state); - return chapterFavorites.includes(chapterId); + const isFavorite = chapterFavorites.includes(chapterId); + return isFavorite; }; diff --git a/src/data/global/global.actions.tsx b/src/data/global/global.actions.tsx deleted file mode 100644 index c82d898..0000000 --- a/src/data/global/global.actions.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/* - * - * GlobalProvider actions - * - */ - -import ActionTypes from './global.constants'; - -export const setActivePage = (page: string) => ({ - type: ActionTypes.SET_ACTIVE_PAGE, - payload: page, -}); - -export const resetApp = () => ({ - type: ActionTypes.RESET_APP, -}); diff --git a/src/data/global/global.constants.tsx b/src/data/global/global.constants.tsx deleted file mode 100644 index dfd3093..0000000 --- a/src/data/global/global.constants.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/* - * AppConstants - * Each action has a corresponding type, which the reducer knows and picks up on. - * To avoid weird typos between the reducer and the actions, we save them as - * constants here. We prefix them with 'yourproject/YourComponent' so we avoid - * reducers accidentally picking up actions they shouldn't. - * - */ - -enum ActionTypes { - RESET_APP = 'app/global/RESET_APP', - SET_ACTIVE_PAGE = 'app/global/SET_ACTIVE_PAGE', -} - -export default ActionTypes; diff --git a/src/data/global/global.reducer.tsx b/src/data/global/global.reducer.tsx deleted file mode 100644 index 3f71c2b..0000000 --- a/src/data/global/global.reducer.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import produce from 'immer'; -import ActionTypes from './global.constants'; - -interface GlobalAction { - readonly type: string; - readonly payload: any; -} - -export interface GlobalState { - readonly activePage: string; -} - -const initState: GlobalState = { - activePage: 'home', -}; - -const globalReducer = (state: GlobalState = initState, action: GlobalAction) => - produce(state, (draft) => { - switch (action.type) { - case ActionTypes.SET_ACTIVE_PAGE: - draft.activePage = action.payload; - break; - case ActionTypes.RESET_APP: - draft = initState; - break; - } - }); - -export default globalReducer; diff --git a/src/data/global/global.saga.tsx b/src/data/global/global.saga.tsx deleted file mode 100644 index 9e4c02d..0000000 --- a/src/data/global/global.saga.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { all } from 'redux-saga/effects'; - -function* actionWatcher() {} - -export default function* globalSaga() { - yield all([actionWatcher()]); -} diff --git a/src/data/user/user.actions.tsx b/src/data/user/user.actions.tsx deleted file mode 100644 index 33249fb..0000000 --- a/src/data/user/user.actions.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import ActionTypes from './user.constants'; - -export const setFontSize = (fontSize: string) => ({ - type: ActionTypes.SET_FONT_SIZE, - payload: fontSize, -}); - -export const setHasSeenTutorial = (hasSeenTutorial: boolean) => ({ - type: ActionTypes.SET_HAS_SEEN_TUTORIAL, - payload: hasSeenTutorial, -}); - -export const setTheme = (theme: string) => ({ - type: ActionTypes.SET_THEME, - payload: theme, -}); - -export const resetApp = () => ({ - type: ActionTypes.RESET_USER_STATE, -}); diff --git a/src/data/user/user.constants.tsx b/src/data/user/user.constants.tsx deleted file mode 100644 index 1967077..0000000 --- a/src/data/user/user.constants.tsx +++ /dev/null @@ -1,8 +0,0 @@ -enum ActionTypes { - SET_FONT_SIZE = 'boilerplate/user/SET_FONT_SIZE', - SET_HAS_SEEN_TUTORIAL = 'boilerplate/user/SET_HAS_SEEN_TUTORIAL', - SET_THEME = 'boilerplate/user/SET_THEME', - RESET_USER_STATE = 'boilerplate/user/RESET_USER_STATE', -} - -export default ActionTypes; diff --git a/src/data/user/user.reducer.tsx b/src/data/user/user.reducer.tsx deleted file mode 100644 index ec1ebf9..0000000 --- a/src/data/user/user.reducer.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import produce from 'immer'; -import ActionTypes from './user.constants'; -import { getSystemTheme, THEMES, Theme } from '../../utils/theme'; - -interface UserActions { - readonly type: string; - readonly payload: any; -} - -export interface UserState { - readonly fontSize: string; - readonly hasSeenTutorial: boolean; - readonly selectedTheme: Theme; -} - -const initState = { - fontSize: '', - hasSeenTutorial: false, - selectedTheme: getSystemTheme(), -}; - -const userReducer = (state: UserState = initState, action: UserActions) => - produce(state, (draft) => { - switch (action.type) { - case ActionTypes.SET_FONT_SIZE: - draft.fontSize = action.payload; - break; - case ActionTypes.SET_HAS_SEEN_TUTORIAL: - draft.hasSeenTutorial = action.payload; - break; - case ActionTypes.SET_THEME: - if (action.payload.name === THEMES.system.name) { - draft.selectedTheme = getSystemTheme(); - break; - } - draft.selectedTheme = action.payload; - break; - case ActionTypes.RESET_USER_STATE: - draft = initState; - break; - } - }); - -export default userReducer; diff --git a/src/data/user/user.selector.ts b/src/data/user/user.selector.ts new file mode 100644 index 0000000..8eda732 --- /dev/null +++ b/src/data/user/user.selector.ts @@ -0,0 +1,21 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from './../../store'; + +const selfUserState = (state: RootState) => state.user; + +export const selectUserState = createSelector(selfUserState, (state) => state); + +export const selectCurrentSearchView = createSelector( + selfUserState, + (state) => state.currentSearchView, +); + +export const selectCurrentTheme = createSelector( + selfUserState, + (state) => state.currentTheme, +); + +export const selectHasSeenTutorial = createSelector( + selfUserState, + (state) => state.hasSeenTutorial, +); diff --git a/src/data/user/user.slice.ts b/src/data/user/user.slice.ts new file mode 100644 index 0000000..cf16744 --- /dev/null +++ b/src/data/user/user.slice.ts @@ -0,0 +1,65 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface CurrentSearchView { + currentSearchView: 'card' | 'list'; +} + +interface CurrentSearchViewPayload { + currentSearchView: 'card' | 'list'; +} + +interface CurrentTheme { + currentTheme: string; +} + +interface CurrentThemePayload { + currentTheme: string; +} + +type UserState = { + hasSeenTutorial: boolean; +} & CurrentSearchView & + CurrentTheme; + +const initialState: UserState = { + currentSearchView: 'card', + currentTheme: 'system', + hasSeenTutorial: false, +}; + +const userSlice = createSlice({ + name: 'user', + initialState, + reducers: { + resetUserState() { + return initialState; + }, + setCurrentSearchView( + state, + action: PayloadAction, + ) { + const { currentSearchView } = action.payload; + state.currentSearchView = currentSearchView; + }, + setCurrentTheme(state, action: PayloadAction) { + const { currentTheme } = action.payload; + state.currentTheme = currentTheme; + }, + setHasSeenTutorial( + state, + action: PayloadAction<{ hasSeenTutorial: boolean }>, + ) { + const { hasSeenTutorial } = action.payload; + state.hasSeenTutorial = hasSeenTutorial; + }, + }, +}); + +export const { + resetUserState, + setCurrentSearchView, + setCurrentTheme, + setHasSeenTutorial, +} = userSlice.actions; + +export default userSlice.reducer; diff --git a/src/index.tsx b/src/index.tsx index 96ca6d6..439a329 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,13 +8,11 @@ import './i18n'; import App from './App'; import * as serviceWorker from './serviceWorker'; -import configureStore from './configureStore'; +import { store, persistor } from './store'; import { ConnectedRouter } from 'connected-react-router'; import history from './utils/history'; import './index.css'; -const { store, persistor } = configureStore(); - ReactDOM.render( diff --git a/src/pages/Settings/index.tsx b/src/pages/Settings/index.tsx index 6765bf6..782e5fb 100644 --- a/src/pages/Settings/index.tsx +++ b/src/pages/Settings/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { IonButtons, IonContent, @@ -21,23 +21,22 @@ import { IonItemDivider, } from '@ionic/react'; import './index.css'; -import { resetApp } from '../../data/global/global.actions'; import { useTranslation } from 'react-i18next'; import { I18N_LANGUAGES_SUPPORTED } from '../../i18n'; import { colorPaletteOutline, languageOutline } from 'ionicons/icons'; -import { setTheme } from '../../data/user/user.actions'; -import { THEMES, Theme } from '../../utils/theme'; +import { THEMES } from '../../utils/theme'; +import { selectCurrentTheme } from '../../data/user/user.selector'; +import { RootState } from '../../store'; +import { resetUserState, setCurrentTheme } from '../../data/user/user.slice'; -interface ContainerProps { - setTheme: Function; - selectedTheme: Theme; - resetApp: Function; -} - -const Page: React.FC = (props) => { - const { setTheme, selectedTheme, resetApp } = props; +export const SettingsPage: React.FC = () => { const { t, i18n } = useTranslation(); + const dispatch = useDispatch(); + const currentTheme = useSelector((state: RootState) => + selectCurrentTheme(state), + ); + return ( @@ -99,13 +98,13 @@ const Page: React.FC = (props) => { { - return setTheme(theme); - }} + onClick={() => + dispatch(setCurrentTheme({ currentTheme: theme.name })) + } > {t(theme.i18n)} ); @@ -123,7 +122,7 @@ const Page: React.FC = (props) => { {t('SETTINGS.GENERAL.ITEMS.COPYRIGHT.LABEL')} Simon Golms - resetApp()}> + dispatch(resetUserState())}> {t('SETTINGS.GENERAL.ITEMS.RESET.LABEL')} @@ -133,21 +132,3 @@ const Page: React.FC = (props) => { ); }; - -const mapStateToProps = (state: any) => { - return { - selectedTheme: state.user.selectedTheme, - }; -}; - -const mapDispatchToProps = (dispatch: any) => { - return { - setTheme: (theme: string) => { - dispatch(setTheme(theme)); - }, - resetApp: () => { - dispatch(resetApp()); - }, - }; -}; -export default connect(mapStateToProps, mapDispatchToProps)(Page); diff --git a/src/pages/Tutorial/index.tsx b/src/pages/Tutorial/index.tsx index 9259200..bb7e790 100644 --- a/src/pages/Tutorial/index.tsx +++ b/src/pages/Tutorial/index.tsx @@ -1,5 +1,3 @@ -import React, { useRef } from 'react'; -import { connect } from 'react-redux'; import { IonButton, IonContent, @@ -8,57 +6,45 @@ import { IonImg, IonText, } from '@ionic/react'; -import './index.css'; +import React, { useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import { setHasSeenTutorial } from '../../data/user/user.slice'; import imgIntro001 from '../../images/intro-001.png'; -import { setHasSeenTutorial } from '../../data/user/user.actions'; - -interface ContainerProps { - setHasSeenTutorial: Function; -} +import './index.css'; -const Tutorial: React.FC = (props) => { +export const Tutorial: React.FC = () => { + const dispatch = useDispatch(); const slidesRef = useRef(null); return ( - <> - - - - - -

Ionic React Boilerplate

-
-

- A highly scalable, offline-first foundation with the best DX and a - focus on performance and best practices -

-
- -

Slide 2

-
- -

Slide 3

-
-
-
- props.setHasSeenTutorial(true)}> - Get Started - -
-
- + + + + + +

Ionic React Boilerplate

+
+

+ A highly scalable, offline-first foundation with the best DX and a + focus on performance and best practices +

+
+ +

Slide 2

+
+ +

Slide 3

+
+
+
+ + dispatch(setHasSeenTutorial({ hasSeenTutorial: true })) + } + > + Get Started + +
+
); }; - -const mapStateToProps = (state: any) => { - return {}; -}; - -const mapDispatchToProps = (dispatch: any) => { - return { - setHasSeenTutorial: (hasSeenTutorial: boolean) => - dispatch(setHasSeenTutorial(hasSeenTutorial)), - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Tutorial); diff --git a/src/reducers.ts b/src/reducers.ts new file mode 100644 index 0000000..0d2870d --- /dev/null +++ b/src/reducers.ts @@ -0,0 +1,25 @@ +/** + * Combine all reducers in this file and export the combined reducers. + */ + +import { combineReducers, Reducer } from 'redux'; +import { connectRouter } from 'connected-react-router'; +import chapterReducer from './data/chapter/chapter.reducer'; +import history from './utils/history'; +import userSlice from './data/user/user.slice'; + +/** + * Merges the main reducer with the router state and dynamically injected reducers + */ +const createReducer = (injectedReducers = {}) => { + const rootReducer = combineReducers({ + chapter: chapterReducer, + user: userSlice, + router: connectRouter(history) as Reducer, + ...injectedReducers, + }); + + return rootReducer; +}; + +export default createReducer; diff --git a/src/reducers.tsx b/src/reducers.tsx deleted file mode 100644 index 8f57a67..0000000 --- a/src/reducers.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Combine all reducers in this file and export the combined reducers. - */ - -import { combineReducers, Reducer } from 'redux'; -import { connectRouter } from 'connected-react-router'; -import chapterReducer from './data/chapter/chapter.reducer'; -import globalReducer from './data/global/global.reducer'; -import userReducer from './data/user/user.reducer'; -import history from './utils/history'; - -export type RootState = { - chapter: ReturnType; - global: ReturnType; - user: ReturnType; - router: ReturnType; -}; - -export type RootActions = - | Parameters[1] - | Parameters[1] - | Parameters[1] - | Parameters[0]; - -/** - * Merges the main reducer with the router state and dynamically injected reducers - */ -const createReducer = (injectedReducers = {}) => { - const rootReducer = combineReducers({ - chapter: chapterReducer, - global: globalReducer, - user: userReducer, - router: connectRouter(history) as Reducer, - ...injectedReducers, - }); - - return rootReducer; -}; - -export default createReducer; diff --git a/src/store.tsx b/src/store.tsx new file mode 100644 index 0000000..48959a7 --- /dev/null +++ b/src/store.tsx @@ -0,0 +1,25 @@ +import { routerMiddleware } from 'connected-react-router'; +import { configureStore } from '@reduxjs/toolkit'; +import { persistStore, persistReducer } from 'redux-persist'; +import createRootReducer from './reducers'; +import createCapacitorStorage from './utils/capacitorStorage'; +import history from './utils/history'; + +export type RootState = ReturnType; + +export const persistConfig = { + blacklist: ['router'], + debug: false, + key: 'root', + storage: createCapacitorStorage(), + version: 1, +}; + +const persistedReducer = persistReducer(persistConfig, createRootReducer()); + +export const store = configureStore({ + reducer: persistedReducer, + middleware: [routerMiddleware(history)], +}); + +export const persistor = persistStore(store);