Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0f9844a
Implement retryableLazy-based loading
taneliang Oct 20, 2020
d077a22
Add React experimental and other deps
taneliang Oct 21, 2020
a76cab9
Big commit of many WIP things
taneliang Oct 21, 2020
b9eccea
Convert TimetableContainer to FC and fix sem switcher
taneliang Oct 21, 2020
f38c34f
Improve entry point component types
taneliang Oct 21, 2020
71fea5e
Improve router type annotations but TS still cannot infer
taneliang Oct 21, 2020
20a2889
Disable Sentry on this branch
taneliang Oct 22, 2020
5a99e30
Fix TS build errors in TodayContainer
taneliang Oct 22, 2020
2600a3c
Replace custom router with React Router v6/experimental
taneliang Oct 22, 2020
82bae87
Allow onMouseOver, onFocus and ref passthrough on Preloading(Nav)Link
taneliang Oct 22, 2020
4620ed5
Define entry points for Module{Finder,Page}
taneliang Oct 22, 2020
6f00456
Fix 404 route
taneliang Oct 24, 2020
21ebc33
Define entry points for ModuleArchive
taneliang Oct 24, 2020
cab0b55
Replace JSResource with DevTools Resource implementation for data loa…
taneliang Oct 24, 2020
b966326
Minor code tweaks
taneliang Oct 26, 2020
b6a1014
Implement EntryPoint.disposePreparedProps; preferentially use Redux d…
taneliang Oct 26, 2020
a878f03
Implement <ScrollToTop onPathChange>
taneliang Oct 26, 2020
4b7f0c4
Wrap EventMap* in Suspense
taneliang Oct 26, 2020
6f3ea31
Convert KeyboardShortcuts to FC to allow it to work with React Router 6
taneliang Oct 27, 2020
b69e361
Define entry point for TetrisContainer
taneliang Oct 27, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
"@types/react-loadable": "5.5.3",
"@types/react-modal": "3.10.6",
"@types/react-redux": "7.1.9",
"@types/react-router-dom": "5.1.6",
"@types/react-scrollspy": "3.3.3",
"@types/redux-mock-store": "1.0.2",
"@types/webpack-env": "1.15.3",
Expand Down Expand Up @@ -141,6 +140,7 @@
"core-js": "3.6.5",
"date-fns": "2.16.1",
"downshift": "5.4.7",
"history": "^5.0.0",
"ical-generator": "https://github.com/ZhangYiJiang/ical-generator.git#ed6928fe",
"immer": "7.0.9",
"json2mq": "0.2.0",
Expand All @@ -151,9 +151,9 @@
"no-scroll": "2.1.1",
"nusmoderator": "3.0.0",
"query-string": "5.0.0",
"react": "16.14.0",
"react": "^0.0.0-experimental-4ead6b530",
"react-beautiful-dnd": "13.0.0",
"react-dom": "16.14.0",
"react-dom": "^0.0.0-experimental-4ead6b530",
"react-feather": "2.0.8",
"react-helmet": "6.1.0",
"react-hot-loader": "4.13.0",
Expand All @@ -164,7 +164,7 @@
"react-modal": "3.11.2",
"react-qr-svg": "2.2.2",
"react-redux": "7.2.1",
"react-router-dom": "5.2.0",
"react-router-dom": "^0.0.0-experimental-ffd8c7d0",
"react-scrollspy": "3.4.3",
"redux": "4.0.5",
"redux-persist": "6.0.0",
Expand Down
28 changes: 14 additions & 14 deletions website/src/bootstrapping/configure-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,20 @@ export default function configureStore(defaultState?: State) {

const middlewares = [ravenMiddleware, thunk, requestsMiddleware];

if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, import/no-extraneous-dependencies
const { createLogger } = require('redux-logger');
const logger = createLogger({
level: 'info',
collapsed: true,
duration: true,
diff: true,
// Avoid diffing actions that insert a lot of stuff into the state to prevent console from lagging
diffPredicate: (getState: GetState, action: Actions) =>
!action.type.startsWith('FETCH_MODULE_LIST') && !action.type.startsWith('persist/'),
});
middlewares.push(logger);
}
// if (process.env.NODE_ENV === 'development') {
// // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, import/no-extraneous-dependencies
// const { createLogger } = require('redux-logger');
// const logger = createLogger({
// level: 'info',
// collapsed: true,
// duration: true,
// diff: true,
// // Avoid diffing actions that insert a lot of stuff into the state to prevent console from lagging
// diffPredicate: (getState: GetState, action: Actions) =>
// !action.type.startsWith('FETCH_MODULE_LIST') && !action.type.startsWith('persist/'),
// });
// middlewares.push(logger);
// }

const storeEnhancer = applyMiddleware(...middlewares);

Expand Down
3 changes: 1 addition & 2 deletions website/src/bootstrapping/matomo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { History } from 'history';
import { each } from 'lodash';

Expand Down Expand Up @@ -68,7 +67,7 @@ export function setCustomDimensions(dimensions: { [id: number]: string }) {
}

export function trackPageView(history: History) {
history.listen((location, action) => {
return history.listen(({ action }) => {
if (action === 'PUSH') {
// Wait a bit for the page title to update
setTimeout(() => {
Expand Down
3 changes: 2 additions & 1 deletion website/src/bootstrapping/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import * as Sentry from '@sentry/browser';
import { isBrowserSupported } from './browser';

// Configure Raven - the client for Sentry, which we use to handle errors
const loadRaven = process.env.NODE_ENV === 'production';
// const loadRaven = process.env.NODE_ENV === 'production';
const loadRaven = false; // TODO: Reenable before merging
if (loadRaven) {
Sentry.init({
dsn: 'https://4b4fe71954424fd39ac88a4f889ffe20@sentry.io/213986',
Expand Down
19 changes: 13 additions & 6 deletions website/src/entry/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,48 @@ import { Store } from 'redux';
import { State } from 'types/state';
import { Persistor } from 'storage/persistReducer';

import React from 'react';
import React, { Suspense, useCallback, useMemo } from 'react';
import { hot } from 'react-hot-loader/root';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';

import AppShell from 'views/AppShell';
// import AppShell from 'views/AppShell';
import Routes from 'views/routes/Routes';
import { DIMENSIONS, setCustomDimensions } from 'bootstrapping/matomo';
import ErrorBoundary from 'views/errors/ErrorBoundary';
import ErrorPage from 'views/errors/ErrorPage';
import LoadingSpinner from 'views/components/LoadingSpinner';
import ApiError from 'views/errors/ApiError';

type Props = {
store: Store<State>;
persistor: Persistor;
};

const App: React.FC<Props> = ({ store, persistor }) => {
const onBeforeLift = () => {
const onBeforeLift = useCallback(() => {
const { theme, settings } = store.getState();

setCustomDimensions({
[DIMENSIONS.theme]: theme.id,
[DIMENSIONS.beta]: String(!!settings.beta),
});
};
}, [store]);

// <Router>
// <AppShell>
// <Routes />
// </AppShell>
// </Router>
return (
<ErrorBoundary errorPage={() => <ErrorPage showReportDialog />}>
<Provider store={store}>
<PersistGate persistor={persistor} onBeforeLift={onBeforeLift}>
<Router>
<AppShell>
<Suspense fallback={<LoadingSpinner />}>
<Routes />
</AppShell>
</Suspense>
</Router>
</PersistGate>
</Provider>
Expand Down
7 changes: 5 additions & 2 deletions website/src/entry/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import 'bootstrapping/sentry';
import 'core-js/es/promise/finally';

import React from 'react';
import ReactDOM from 'react-dom';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { unstable_createRoot as createRoot } from 'react-dom';
import ReactModal from 'react-modal';

import configureStore from 'bootstrapping/configure-store';
Expand All @@ -26,7 +28,8 @@ subscribeOnlineEvents(store);
// Initialize ReactModal
ReactModal.setAppElement('#app');

ReactDOM.render(<App store={store} persistor={persistor} />, document.getElementById('app'));
// ReactDOM.render(<App store={store} persistor={persistor} />, document.getElementById('app'));
createRoot(document.getElementById('app')).render(<App store={store} persistor={persistor} />);

if (
('serviceWorker' in navigator &&
Expand Down
169 changes: 169 additions & 0 deletions website/src/utils/JSResource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
type Module<T> = { default: T };
type Loader<T> = () => Promise<Module<T>>;

export interface JSResourceReference<T> {
/**
* Returns the module's id
*/
getModuleId(): string;

/**
* Gets a module if it is already loaded, undefined otherwise.
*/
getModuleIfRequired(): T | undefined;

/**
* Loads the resource if necessary
*/
preload(): Promise<T>;

preloadOrReloadIfError(): void;

/**
* This is the key method for integrating with React Suspense. Read will:
* - "Suspend" if the resource is still pending (currently implemented as
* throwing a Promise, though this is subject to change in future
* versions of React)
* - Throw an error if the resource failed to load.
* - Return the data of the resource if available.
*/
read(): T;

/**
* Convenience function that preloads and reads this resource.
*/
fetch(): T;
}

/**
* A cache of resources to avoid loading the same module twice. This is important
* because Webpack dynamic imports only expose an asynchronous API for loading
* modules, so to be able to access already-loaded modules synchronously we
* must have stored the previous result somewhere.
*/
const resourceMap = new Map<string, JSResourceImpl<unknown>>();

/**
* A generic resource: given some method to asynchronously load a value -- the
* loader() argument -- it allows accessing the state of the resource.
*
* The main differences between this and `Resource` are:
* - `JSResourceImpl` returns a promise on preload.
* - `Resource` allows a single resource to load values for different keys.
*/
class JSResourceImpl<T> implements JSResourceReference<T> {
private error: Error | undefined;

private promise: Promise<T> | undefined;

private result: T | undefined;

private moduleId: string;

private loader: Loader<T>;

constructor(moduleId: string, loader: Loader<T>) {
this.moduleId = moduleId;
this.loader = loader;
}

getModuleId() {
return this.moduleId;
}

getModuleIfRequired() {
return this.result;
}

/**
* Loads the resource if necessary.
*/
preload() {
let { promise } = this;
if (promise == null) {
promise = this.loader()
.then(({ default: result }) => {
this.result = result;
return result;
})
.catch((error) => {
this.error = error;
throw error;
});
this.promise = promise;
}
return promise;
}

preloadOrReloadIfError() {
if (this.error !== undefined) {
this.error = undefined;
this.promise = undefined;
this.result = undefined;
}
this.preload();
}

/**
* Returns the result, if available. This can be useful to check if the value
* is resolved yet.
*/
get() {
if (this.result !== undefined) {
return this.result;
}
return undefined;
}

/**
* This is the key method for integrating with React Suspense. Read will:
* - "Suspend" if the resource is still pending (currently implemented as
* throwing a Promise, though this is subject to change in future
* versions of React)
* - Throw an error if the resource failed to load.
* - Return the data of the resource if available.
*/
read() {
if (this.result !== undefined) {
return this.result;
}
if (this.error !== undefined) {
throw this.error;
}
if (this.promise === undefined) {
throw new Error('preload() must be called before read().');
}
throw this.promise;
}

fetch() {
this.preload();
return this.read();
}
}

/**
* A helper method to create a resource, intended for dynamically loading code.
*
* Example:
* ```
* // Before rendering, ie in an event handler:
* const resource = JSResource('Foo', () => import('./Foo.js));
* resource.load();
*
* // in a React component:
* const Foo = resource.read();
* return <Foo ... />;
* ```
*
* @param {*} moduleId A globally unique identifier for the resource used for caching
* @param {*} loader A method to load the resource's data if necessary
*/
export function JSResource<T>(moduleId: string, loader: Loader<T>): JSResourceReference<T> {
let resource = resourceMap.get(moduleId);
if (resource == null) {
resource = new JSResourceImpl(moduleId, loader);
resourceMap.set(moduleId, resource);
}
return resource as JSResourceReference<T>;
}
Loading