Browse Source

Migrated to react-redux.

gwillz 2 years ago
parent
commit
9b65556981
11 changed files with 159 additions and 121 deletions
  1. 1 1
      css/index.css
  2. 5 13
      package-lock.json
  3. 1 1
      package.json
  4. 41 0
      src/dark.tsx
  5. 26 16
      src/editor.tsx
  6. 20 47
      src/index.tsx
  7. 1 1
      src/markdown.tsx
  8. 39 30
      src/presentation.tsx
  9. 18 11
      src/store.ts
  10. 3 1
      src/toolbar.tsx
  11. 4 0
      types/globals.d.ts

+ 1 - 1
css/index.css

@@ -7,7 +7,7 @@
 @import "./dark.css";
 @import "./print.css";
 
-html, body, #root, #app {
+html, body, #root, #root > div, #app {
     height: 100%;
     width: 100%;
 }

+ 5 - 13
package-lock.json

@@ -5881,19 +5881,6 @@
       "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
       "dev": true
     },
-    "primer-markdown": {
-      "version": "3.7.12",
-      "resolved": "https://registry.npmjs.org/primer-markdown/-/primer-markdown-3.7.12.tgz",
-      "integrity": "sha512-cyWSxBQoNo7D5vjnf4eLvHmS1aQyDEmaKnJLiSoj1A9bq7IneAP/okLHRqiTXn7wy9MD/ixm4me9cVpuxQY9hw==",
-      "requires": {
-        "primer-support": "4.7.1"
-      }
-    },
-    "primer-support": {
-      "version": "4.7.1",
-      "resolved": "https://registry.npmjs.org/primer-support/-/primer-support-4.7.1.tgz",
-      "integrity": "sha512-BruDzdeTQW7UVsyR2eKxTWMHRzumCTs+Sc6I5reOwTmBIJ2o9CWtj1DvAO8l97+rzQiyajJWi/fJ7i2Zr9Uupg=="
-    },
     "process": {
       "version": "0.11.10",
       "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -6162,6 +6149,11 @@
         "symbol-observable": "^1.2.0"
       }
     },
+    "redux-persist": {
+      "version": "5.10.0",
+      "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-5.10.0.tgz",
+      "integrity": "sha512-sSJAzNq7zka3qVHKce1hbvqf0Vf5DuTVm7dr4GtsqQVOexnrvbV47RWFiPxQ8fscnyiuWyD2O92DOxPl0tGCRg=="
+    },
     "regenerate": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",

+ 1 - 1
package.json

@@ -21,12 +21,12 @@
     "draft-js": "^0.10.5",
     "express": "^4.16.4",
     "highlight.js": "^9.13.1",
-    "primer-markdown": "^3.7.12",
     "qs": "^6.6.0",
     "react": "^16.7.0",
     "react-dom": "^16.7.0",
     "react-redux": "^6.0.0",
     "redux": "^4.0.1",
+    "redux-persist": "^5.10.0",
     "reset.css": "^2.0.2",
     "showdown": "^1.9.0",
     "showdown-highlight": "^2.1.3"

+ 41 - 0
src/dark.tsx

@@ -0,0 +1,41 @@
+
+import * as React from 'react'
+import * as qs from 'qs'
+import style from './styles'
+import { connect, DispatchProp } from 'react-redux';
+import { Store } from './store';
+
+type Props = React.HTMLProps<HTMLDivElement> & DispatchProp & {
+    isDark?: boolean;
+}
+
+export class Dark extends React.PureComponent<Props> {
+    
+    private isDark: boolean;
+    
+    constructor(props: Props) {
+        super(props);
+        const params = qs.parse(window.location.search.slice(1));
+        this.isDark = params.dark != 'undefined';
+    }
+    
+    componentDidMount() {
+        this.isDark = false;
+    }
+    
+    render() {
+        const {isDark, dispatch, ...props} = this.props;
+        return (
+            <div
+                className={style({ dark: this.isDark || this.props.isDark })}
+                {...props}
+            />
+        )
+    }
+}
+
+const map = (store: Store) => ({
+    isDark: store.dark,
+})
+
+export default connect(map)(Dark)

+ 26 - 16
src/editor.tsx

@@ -1,9 +1,9 @@
 
 import * as React from 'react';
-import { Unsubscribe } from 'redux';
 import { EditorState, Editor, ContentState, getDefaultKeyBinding, Modifier } from 'draft-js';
 import styles from './styles';
-import store from './store';
+import { connect, DispatchProp } from 'react-redux';
+import { Store, ActionTypes } from './store';
 
 const TAB = "    ";
 
@@ -22,13 +22,17 @@ function keyBinding(e: React.KeyboardEvent): string | null {
     return getDefaultKeyBinding(e);
 }
 
+type Props = DispatchProp & {
+    action: ActionTypes;
+    content: string;
+}
+
 type State = {
     draft: EditorState;
 }
 
-export class EditorView extends React.PureComponent<{}, State> {
+export class EditorView extends React.PureComponent<Props, State> {
     
-    private unsubscribe: Unsubscribe;
     private timer: number;
     private view: HTMLElement | null;
     private editor: Editor | null;
@@ -37,16 +41,17 @@ export class EditorView extends React.PureComponent<{}, State> {
         draft: EditorState.createEmpty(),
     }
     
-    // @todo I hate this. Why is state management such a bitch?
-    private handleStore = () => {
-        const state = store.getState();
-        if (state.action == "LOAD") {
-            this.setState(({draft}) => ({
-                draft: EditorState.push(draft,
-                    ContentState.createFromText(state.content), 
-                    "insert-characters"),
-            }))
+    static getDerivedStateFromProps(props: Props, state: State) {
+        switch (props.action) {
+            case 'LOAD':
+            case 'persist/REHYDRATE':
+                return {
+                    draft: EditorState.push(state.draft,
+                        ContentState.createFromText(props.content), 
+                        "insert-characters"),
+                }
         }
+        return null;
     }
     
     private handleKey = (event: KeyboardEvent) => {
@@ -72,7 +77,7 @@ export class EditorView extends React.PureComponent<{}, State> {
         clearTimeout(this.timer);
         this.timer = setTimeout(() => {
             const content = draft.getCurrentContent().getPlainText('');
-            store.dispatch({ type: 'EDIT', content });
+            this.props.dispatch({ type: 'EDIT', content });
         }, 350);
     }
     
@@ -104,12 +109,10 @@ export class EditorView extends React.PureComponent<{}, State> {
     }
     
     componentDidMount() {
-        this.unsubscribe = store.subscribe(this.handleStore);
         window.addEventListener("keyup", this.handleKey);
     }
     
     componentWillUnmount() {
-        this.unsubscribe();
         window.addEventListener("keyup", this.handleKey);
     }
     
@@ -130,3 +133,10 @@ export class EditorView extends React.PureComponent<{}, State> {
         )
     }
 }
+
+const map = (store: Store) => ({
+    action: store.action,
+    content: store.content,
+})
+
+export default connect(map)(EditorView);

+ 20 - 47
src/index.tsx

@@ -1,71 +1,44 @@
 
 import * as React from 'react';
 import { render } from 'react-dom';
+import { PersistGate } from 'redux-persist/integration/react';
 import { Provider } from 'react-redux';
-import { Unsubscribe } from 'redux';
 import * as qs from 'qs'
 
 import style from './styles';
-import store from './store'
+import { store, persistor } from './store'
 import './icons';
 
-import {Toolbar} from './toolbar'
-import {EditorView} from './editor'
-import {PresentView} from './presentation'
+import Dark from './dark'
+import Toolbar from './toolbar'
+import EditorView from './editor'
+import PresentView from './presentation'
 
-const params = qs.parse(window.location.search.slice(1));
 
-const IS_DARK = typeof params.dark != 'undefined';
-const LOAD_URL = params.url as string;
-
-type State = {
-    dark: boolean;
-}
-
-class App extends React.Component<{}, State> {
-    state: State = {
-        dark: IS_DARK,
-    }
-    
-    private unsubscribe: Unsubscribe;
-    
-    componentDidMount() {
-        this.unsubscribe = store.subscribe(this.handleStore);
-    }
-    
-    private handleStore = () => {
-        let state = store.getState();
-        if (state.action == "DARK") {
-            this.setState({
-                dark: state.dark,
-            })
-        }
-    }
+class App extends React.Component {
     
     render() {
         return (
             <Provider store={store}>
-                <div id={style('app')} className={style({
-                    dark: this.state.dark,
-                })}>
-                    <Toolbar/>
-                    <div className={style("split-view")}>
-                        <EditorView/>
-                        <PresentView/>
-                    </div>
-                </div>
+                <PersistGate loading={null} persistor={persistor}>
+                    <Dark id={style('app')}>
+                        <Toolbar/>
+                        <div className={style("split-view")}>
+                            <EditorView/>
+                            <PresentView/>
+                        </div>
+                    </Dark>
+                </PersistGate>
             </Provider>
         )
     }
 }
 
 async function loadParams() {
-    if (IS_DARK) {
-        store.dispatch({type: 'DARK'});
-    }
-    
-    if (LOAD_URL) {
-        let url = window.location.protocol + "//" + LOAD_URL.replace(/^http:/, '');
+    const params = qs.parse(window.location.search.slice(1));
+    let url = params.url as string;
+    if (url) {
+        url = window.location.protocol + "//" + url.replace(/^http:/, '');
         
         const req = await fetch(url, {
             mode: 'cors',

+ 1 - 1
src/markdown.tsx

@@ -2,7 +2,7 @@
 import * as React from 'react'
 import * as showdown from 'showdown'
 import styles from './styles'
-import store from './store';
+import {store} from './store'
 
 type Props = {
     className?: string;

+ 39 - 30
src/presentation.tsx

@@ -1,9 +1,9 @@
 
 import * as React from 'react';
 import styles from './styles';
-import store from './store';
+import {ActionTypes, Store} from './store';
 import { Markdown } from './markdown';
-import { Unsubscribe } from 'redux';
+import { connect, DispatchProp } from 'react-redux';
 
 const FILTER_NOTES = /\s*\[\/\/\]:\s*#\s*\(([^\n]+)\)/g;
 
@@ -18,6 +18,10 @@ function recurseRegex(expr: RegExp, src: string, index = 0) {
     return result;
 }
 
+type Props = DispatchProp & {
+    action: ActionTypes;
+    content: string;
+}
 
 type State = {
     slides: string[];
@@ -25,7 +29,7 @@ type State = {
     active: number;
 }
 
-export class PresentView extends React.PureComponent<{}, State> {
+export class PresentView extends React.PureComponent<Props, State> {
     state: State = {
         slides: [],
         notes: [],
@@ -34,21 +38,6 @@ export class PresentView extends React.PureComponent<{}, State> {
     
     private lastSelection = "";
     private element: HTMLElement | null;
-    private unsubscribe: Unsubscribe;
-    
-    private handleStore = () => {
-        const state = store.getState();
-        
-        switch (state.action) {
-            case "FULLSCREEN":
-                this.goFullscreen();
-                // fallthrough
-            case "RENDER":
-            case "LOAD":
-                this.doRender();
-                break;
-        }
-    }
     
     private handleKey = (event: KeyboardEvent) => {
         switch (event.key) {
@@ -103,19 +92,8 @@ export class PresentView extends React.PureComponent<{}, State> {
         }
     }
     
-    componentDidMount() {
-        this.unsubscribe = store.subscribe(this.handleStore);
-        window.addEventListener("keyup", this.handleKey);
-    }
-    
-    componentWillUnmount() {
-        this.unsubscribe();
-        window.addEventListener("keyup", this.handleKey);
-    }
-    
     public doRender() {
-        const state = store.getState();
-        const slides = state.content.split(/\n\s*---+\s*\n/);
+        const slides = this.props.content.split(/\n\s*---+\s*\n/);
         const notes = slides.map(slide => (
             recurseRegex(FILTER_NOTES, slide, 1)
             .map(note => " + " + note)
@@ -146,6 +124,31 @@ export class PresentView extends React.PureComponent<{}, State> {
         }))
     }
     
+    componentDidUpdate(props: Props) {
+        // console.log(this.props.action)
+        if (this.props.action !== props.action) return;
+        switch (this.props.action) {
+            case "FULLSCREEN":
+                this.goFullscreen();
+                // fallthrough
+            case "RENDER":
+            case "LOAD":
+                this.doRender();
+                break;
+        }
+    }
+    
+    componentDidMount() {
+        window.addEventListener("keyup", this.handleKey);
+        if (this.props.content) {
+            this.doRender();
+            }
+    }
+    
+    componentWillUnmount() {
+        window.addEventListener("keyup", this.handleKey);
+    }
+    
     render() {
         return (
             <div ref={r => this.element = r}
@@ -170,3 +173,9 @@ export class PresentView extends React.PureComponent<{}, State> {
     }
 }
 
+const map = (store: Store) => ({
+    action: store.action,
+    content: store.content,
+})
+
+export default connect(map)(PresentView);

+ 18 - 11
src/store.ts

@@ -1,14 +1,18 @@
 
-import { createStore } from 'redux';
 import * as React from 'react'
+import { createStore } from 'redux';
+import {persistStore, persistReducer, PersistConfig, REHYDRATE} from 'redux-persist'
+import storage from 'redux-persist/lib/storage'
+
+export type ActionTypes = 'LOAD' | 'EDIT' | 'SAVE' | 'FULLSCREEN' | 'RENDER' | 'DARK' | typeof REHYDRATE | null;
 
-export type State = {
-    action: 'LOAD' | 'EDIT' | 'SAVE' | 'FULLSCREEN' | 'RENDER' | 'DARK' | null;
+type State = {
+    action: ActionTypes;
     dark: boolean;
     content: string;
 }
 
-export type Action = {
+type Action = {
     type: 'EDIT';
     content: string;
 } | {
@@ -18,6 +22,11 @@ export type Action = {
     type: 'SAVE' | 'RENDER' | 'FULLSCREEN' | 'DARK';
 }
 
+const config: PersistConfig = {
+    key: 'root',
+    storage,
+}
+
 const INIT_STATE: State = {
     action: null,
     dark: false,
@@ -46,13 +55,11 @@ function reducer(state = INIT_STATE, action: Action) {
     }
 }
 
-const store = createStore(
-    reducer,
-    // @ts-ignore
+export const store = createStore(
+    persistReducer(config, reducer),
     window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
 );
-export default store;
 
-export type Store = typeof store;
-export const context = React.createContext(store);
-export const {Provider, Consumer} = context;
+export const persistor = persistStore(store);
+
+export type Store = State;

+ 3 - 1
src/toolbar.tsx

@@ -2,7 +2,7 @@
 import * as React from 'react';
 import styles from './styles';
 import {Button} from './button';
-import store from './store';
+import {store} from './store'
 
 export class Toolbar extends React.PureComponent {
     private file: HTMLInputElement | null;
@@ -139,3 +139,5 @@ export class Toolbar extends React.PureComponent {
         )
     }
 }
+
+export default Toolbar;

+ 4 - 0
types/globals.d.ts

@@ -0,0 +1,4 @@
+
+interface Window {
+    __REDUX_DEVTOOLS_EXTENSION__?: Function;
+}