index.js 8.3 KB
Newer Older
J
Jason Park 已提交
1
import React from 'react';
2
import Cookies from 'js-cookie';
J
Jason Park 已提交
3
import { connect } from 'react-redux';
4
import Promise from 'bluebird';
J
Jason Park 已提交
5
import { Helmet } from 'react-helmet';
J
Jason Park 已提交
6
import queryString from 'query-string';
7
import {
J
Jason Park 已提交
8
  BaseComponent,
9 10 11 12 13 14
  CodeEditor,
  Header,
  Navigator,
  ResizableContainer,
  TabContainer,
  ToastContainer,
15
  VisualizationViewer,
16 17 18 19 20 21
} from 'components';
import { AlgorithmApi, GitHubApi, VisualizationApi } from 'apis';
import { actions } from 'reducers';
import { createUserFile, extension, refineGist } from 'common/util';
import { exts, languages } from 'common/config';
import { CONTRIBUTING_MD } from 'files';
J
Jason Park 已提交
22
import styles from './App.module.scss';
23

J
Jason Park 已提交
24
class App extends BaseComponent {
J
Jason Park 已提交
25 26 27
  constructor(props) {
    super(props);

28
    this.state = {
J
Jason Park 已提交
29
      workspaceVisibles: [true, true, true],
30
      workspaceWeights: [1, 2, 2],
31
    };
J
Jason Park 已提交
32 33

    this.codeEditorRef = React.createRef();
J
Jason Park 已提交
34 35

    this.ignoreHistoryBlock = this.ignoreHistoryBlock.bind(this);
J
Jason Park 已提交
36 37 38
    this.handleClickTitleBar = this.handleClickTitleBar.bind(this);
    this.loadScratchPapers = this.loadScratchPapers.bind(this);
    this.handleChangeWorkspaceWeights = this.handleChangeWorkspaceWeights.bind(this);
J
Jason Park 已提交
39 40 41
  }

  componentDidMount() {
42 43 44
    window.signIn = this.signIn.bind(this);
    window.signOut = this.signOut.bind(this);

J
Jason Park 已提交
45 46 47
    const { params } = this.props.match;
    const { search } = this.props.location;
    this.loadAlgorithm(params, queryString.parse(search));
J
Jason Park 已提交
48

49 50
    const accessToken = Cookies.get('access_token');
    if (accessToken) this.signIn(accessToken);
J
Jason Park 已提交
51

J
Jason Park 已提交
52
    AlgorithmApi.getCategories()
53
      .then(({ categories }) => this.props.setCategories(categories))
J
Jason Park 已提交
54
      .catch(this.handleError);
J
Jason Park 已提交
55

J
Jason Park 已提交
56
    this.toggleHistoryBlock(true);
57 58 59
  }

  componentWillUnmount() {
60 61
    delete window.signIn;
    delete window.signOut;
J
Jason Park 已提交
62

J
Jason Park 已提交
63
    this.toggleHistoryBlock(false);
J
Jason Park 已提交
64 65
  }

J
Jason Park 已提交
66
  componentWillReceiveProps(nextProps) {
J
Jason Park 已提交
67
    const { params } = nextProps.match;
J
Jason Park 已提交
68 69
    const { search } = nextProps.location;
    if (params !== this.props.match.params || search !== this.props.location.search) {
70 71 72 73
      const { categoryKey, algorithmKey, gistId } = params;
      const { algorithm, scratchPaper } = nextProps.current;
      if (algorithm && algorithm.categoryKey === categoryKey && algorithm.algorithmKey === algorithmKey) return;
      if (scratchPaper && scratchPaper.gistId === gistId) return;
J
Jason Park 已提交
74
      this.loadAlgorithm(params, queryString.parse(search));
J
Jason Park 已提交
75 76 77
    }
  }

J
Jason Park 已提交
78 79
  toggleHistoryBlock(enable = !this.unblock) {
    if (enable) {
J
Jason Park 已提交
80
      const warningMessage = 'Are you sure you want to discard changes?';
J
Jason Park 已提交
81 82 83 84
      window.onbeforeunload = () => {
        const { saved } = this.props.current;
        if (!saved) return warningMessage;
      };
J
Jason Park 已提交
85 86
      this.unblock = this.props.history.block((location) => {
        if (location.pathname === this.props.location.pathname) return;
J
Jason Park 已提交
87
        const { saved } = this.props.current;
J
Jason Park 已提交
88
        if (!saved) return warningMessage;
J
Jason Park 已提交
89 90
      });
    } else {
J
Jason Park 已提交
91
      window.onbeforeunload = undefined;
J
Jason Park 已提交
92 93 94 95 96 97 98 99 100 101 102
      this.unblock();
      this.unblock = undefined;
    }
  }

  ignoreHistoryBlock(process) {
    this.toggleHistoryBlock(false);
    process();
    this.toggleHistoryBlock(true);
  }

103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
  signIn(accessToken) {
    Cookies.set('access_token', accessToken);
    GitHubApi.auth(accessToken)
      .then(() => GitHubApi.getUser())
      .then(user => {
        const { login, avatar_url } = user;
        this.props.setUser({ login, avatar_url });
      })
      .then(() => this.loadScratchPapers())
      .catch(() => this.signOut());
  }

  signOut() {
    Cookies.remove('access_token');
    GitHubApi.auth(undefined)
118 119 120
      .then(() => {
        this.props.setUser(undefined);
      })
121 122 123
      .then(() => this.props.setScratchPapers([]));
  }

J
Jason Park 已提交
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
  loadScratchPapers() {
    const per_page = 100;
    const paginateGists = (page = 1, scratchPapers = []) => GitHubApi.listGists({
      per_page,
      page,
      timestamp: Date.now(),
    }).then(gists => {
      scratchPapers.push(...gists.filter(gist => 'algorithm-visualizer' in gist.files).map(gist => ({
        key: gist.id,
        name: gist.description,
        files: Object.keys(gist.files),
      })));
      if (gists.length < per_page) {
        return scratchPapers;
      } else {
        return paginateGists(page + 1, scratchPapers);
      }
    });
142 143
    return paginateGists()
      .then(scratchPapers => this.props.setScratchPapers(scratchPapers))
J
Jason Park 已提交
144
      .catch(this.handleError);
J
Jason Park 已提交
145 146
  }

J
Jason Park 已提交
147
  loadAlgorithm({ categoryKey, algorithmKey, gistId }, { visualizationId }) {
J
Jason Park 已提交
148
    const { ext } = this.props.env;
149
    const fetch = () => {
150 151 152
      if (window.__PRELOADED_ALGORITHM__) {
        this.props.setAlgorithm(window.__PRELOADED_ALGORITHM__);
        delete window.__PRELOADED_ALGORITHM__;
153 154 155
      } else if (window.__PRELOADED_ALGORITHM__ === null) {
        delete window.__PRELOADED_ALGORITHM__;
        return Promise.reject(new Error('Algorithm Not Found'));
156
      } else if (categoryKey && algorithmKey) {
157 158
        return AlgorithmApi.getAlgorithm(categoryKey, algorithmKey)
          .then(({ algorithm }) => this.props.setAlgorithm(algorithm));
J
Jason Park 已提交
159 160 161 162 163 164 165
      } else if (gistId === 'new' && visualizationId) {
        return VisualizationApi.getVisualization(visualizationId)
          .then(content => {
            this.props.setScratchPaper({
              login: undefined,
              gistId,
              title: 'Untitled',
J
Jason Park 已提交
166
              files: [CONTRIBUTING_MD, createUserFile('visualization.json', JSON.stringify(content))],
J
Jason Park 已提交
167 168
            });
          });
169 170 171 172 173 174
      } else if (gistId === 'new') {
        const language = languages.find(language => language.ext === ext);
        this.props.setScratchPaper({
          login: undefined,
          gistId,
          title: 'Untitled',
J
Jason Park 已提交
175
          files: [CONTRIBUTING_MD, language.skeleton],
176 177 178 179 180 181 182 183
        });
      } else if (gistId) {
        return GitHubApi.getGist(gistId, { timestamp: Date.now() })
          .then(refineGist)
          .then(this.props.setScratchPaper);
      } else {
        this.props.setHome();
      }
184
      return Promise.resolve();
185 186
    };
    fetch()
J
Jason Park 已提交
187 188 189 190
      .then(() => {
        this.selectDefaultTab();
        return null; // to suppress unnecessary bluebird warning
      })
191
      .catch(error => {
J
Jason Park 已提交
192
        this.handleError(error);
193
        this.props.history.push('/');
J
Jason Park 已提交
194
      });
J
Jason Park 已提交
195 196
  }

197 198 199
  selectDefaultTab() {
    const { ext } = this.props.env;
    const { files } = this.props.current;
J
Jason Park 已提交
200 201 202 203 204
    const editingFile = files.find(file => extension(file.name) === 'json') ||
      files.find(file => extension(file.name) === ext) ||
      files.find(file => exts.includes(extension(file.name))) ||
      files[files.length - 1];
    this.props.setEditingFile(editingFile);
205 206
  }

207 208
  handleChangeWorkspaceWeights(workspaceWeights) {
    this.setState({ workspaceWeights });
209
    this.codeEditorRef.current.handleResize();
210 211
  }

J
Jason Park 已提交
212 213 214 215
  toggleNavigatorOpened(navigatorOpened = !this.state.workspaceVisibles[0]) {
    const workspaceVisibles = [...this.state.workspaceVisibles];
    workspaceVisibles[0] = navigatorOpened;
    this.setState({ workspaceVisibles });
216 217
  }

J
Jason Park 已提交
218 219 220
  handleClickTitleBar() {
    this.toggleNavigatorOpened();
  }
J
Jason Park 已提交
221

J
Jason Park 已提交
222 223
  render() {
    const { workspaceVisibles, workspaceWeights } = this.state;
J
Jason Park 已提交
224
    const { titles, description, saved } = this.props.current;
J
Jason Park 已提交
225

226
    const title = `${saved ? '' : '(Unsaved) '}${titles.join(' - ')}`;
J
Jason Park 已提交
227
    const [navigatorOpened] = workspaceVisibles;
228

229
    return (
J
Jason Park 已提交
230
      <div className={styles.app}>
J
Jason Park 已提交
231
        <Helmet>
J
Jason Park 已提交
232
          <title>{title}</title>
233
          <meta name="description" content={description}/>
J
Jason Park 已提交
234
        </Helmet>
J
Jason Park 已提交
235 236
        <Header className={styles.header} onClickTitleBar={this.handleClickTitleBar}
                navigatorOpened={navigatorOpened} loadScratchPapers={this.loadScratchPapers}
237
                ignoreHistoryBlock={this.ignoreHistoryBlock}/>
238
        <ResizableContainer className={styles.workspace} horizontal weights={workspaceWeights}
J
Jason Park 已提交
239
                            visibles={workspaceVisibles} onChangeWeights={this.handleChangeWorkspaceWeights}>
240 241
          <Navigator/>
          <VisualizationViewer className={styles.visualization_viewer}/>
J
Jason Park 已提交
242
          <TabContainer className={styles.editor_tab_container}>
243
            <CodeEditor ref={this.codeEditorRef}/>
244 245
          </TabContainer>
        </ResizableContainer>
246
        <ToastContainer className={styles.toast_container}/>
J
Jason Park 已提交
247 248 249 250 251
      </div>
    );
  }
}

252 253 254
export default connect(({ current, env }) => ({ current, env }), actions)(
  App,
);