diff --git a/package.json b/package.json
index 2b2414b..c89173a 100644
--- a/package.json
+++ b/package.json
@@ -36,11 +36,13 @@
"html-entities": "1.3.1",
"immer": "^7.0.1",
"memorystore": "^1.6.2",
+ "normalizr": "^3.6.0",
"prop-types": "^15",
"react": "16.8.6",
"react-dom": "16.8.6",
"react-hot-loader": "^4.12.21",
"react-markdown": "^4.3.1",
+ "react-redux": "^7.2.0",
"react-tabs": "^3.1.1",
"scwp": "1.x",
"socket.io-redis": "^5.3.0",
diff --git a/src/client/app.jsx b/src/client/app.jsx
index 6c530b7..38f520f 100644
--- a/src/client/app.jsx
+++ b/src/client/app.jsx
@@ -16,7 +16,7 @@ const App = () => {
const [typeRenderers] = useState(typeRenderMap());
return (
-
+
);
};
diff --git a/src/client/index.jsx b/src/client/index.jsx
index dd1fec4..9484a83 100644
--- a/src/client/index.jsx
+++ b/src/client/index.jsx
@@ -3,9 +3,18 @@ import './index.scss';
import {enableMapSet} from 'immer';
import React from 'react';
import {render} from 'react-dom';
+import {Provider} from 'react-redux';
import App from './app';
+import createStore from './store';
enableMapSet();
-render(, document.getElementById('root'));
+render(
+ (
+
+
+
+ ),
+ document.getElementById('root'),
+);
diff --git a/src/client/resources/entity/component/index.jsx b/src/client/resources/entity/entity.jsx
similarity index 56%
rename from src/client/resources/entity/component/index.jsx
rename to src/client/resources/entity/entity.jsx
index 317b562..0bb796c 100644
--- a/src/client/resources/entity/component/index.jsx
+++ b/src/client/resources/entity/entity.jsx
@@ -1,18 +1,28 @@
import {compose} from '@avocado/core';
import {Entity} from '@avocado/entity';
import contempo from 'contempo';
-import React from 'react';
+import React, {useEffect} from 'react';
+import {useDispatch, useSelector} from 'react-redux';
+import {fetchEntity, selectEntityByUri} from './state';
import Traits from './traits';
const decorate = compose(
- contempo(require('./index.raw.scss')),
+ contempo(require('./entity.raw.scss')),
);
-const json = require('~/../fixtures/rock-projectile.entity.json');
-
const EntityComponent = (props) => {
const {uri} = props;
+ const dispatch = useDispatch();
+ const json = useSelector((state) => selectEntityByUri(state, uri));
+ useEffect(() => {
+ if (!json) {
+ dispatch(fetchEntity(uri));
+ }
+ }, [json, uri]);
+ if (!json) {
+ return null;
+ }
const entity = new Entity(json);
const {traits} = json;
return (
diff --git a/src/client/resources/entity/component/index.raw.scss b/src/client/resources/entity/entity.raw.scss
similarity index 100%
rename from src/client/resources/entity/component/index.raw.scss
rename to src/client/resources/entity/entity.raw.scss
diff --git a/src/client/resources/entity/entity.resource.js b/src/client/resources/entity/entity.resource.js
index fbabc52..923bf99 100644
--- a/src/client/resources/entity/entity.resource.js
+++ b/src/client/resources/entity/entity.resource.js
@@ -1,6 +1,8 @@
-import EntityComponent from './component';
+import EntityComponent from './entity';
export default {
+ keys: ['entity', 'entities'],
matcher: /\.entity\.json$/,
Component: EntityComponent,
+ state: require('./state'),
};
diff --git a/src/client/resources/entity/slice.js b/src/client/resources/entity/slice.js
deleted file mode 100644
index e69de29..0000000
diff --git a/src/client/resources/entity/state.js b/src/client/resources/entity/state.js
new file mode 100644
index 0000000..173d2b8
--- /dev/null
+++ b/src/client/resources/entity/state.js
@@ -0,0 +1,59 @@
+import {normalize, schema} from 'normalizr';
+import {
+ createAsyncThunk,
+ createEntityAdapter,
+ createSelector,
+ createSlice,
+} from '@reduxjs/toolkit';
+
+const entitySchema = new schema.Entity('entities', {}, {idAttribute: 'uuid'});
+
+export const fetchEntity = createAsyncThunk(
+ 'entities/fetchEntity',
+ async (uri) => {
+ const data = await fetch(uri);
+ const json = await data.json();
+ const {entities} = normalize(json, entitySchema);
+ return entities;
+ }
+);
+
+const adapter = createEntityAdapter({
+ selectId: (entity) => entity.uuid,
+ sortComparer: (a, b) => a.uuid.localeCompare(b.uuid),
+});
+
+export const {
+ selectById: selectEntityById,
+ selectIds: selectEntityIds,
+ selectEntities: selectEntityEntities,
+ selectAll: selectAllEntities,
+ selectTotal: selectTotalEntities
+} = adapter.getSelectors((state) => state.entities)
+
+export const selectEntityUris = createSelector(
+ (state) => state.entities,
+ (entities) => entities.uris,
+);
+
+export const selectEntityByUri = createSelector(
+ (state) => state.entities,
+ selectEntityUris,
+ (_, uri) => uri,
+ ({entities}, uris, uri) => entities[uris[uri]],
+)
+
+const slice = createSlice({
+ name: 'entities',
+ initialState: adapter.getInitialState({uris: {}}),
+ reducers: {},
+ extraReducers: {
+ [fetchEntity.fulfilled]: (state, action) => {
+ adapter.upsertMany(state, action.payload.entities);
+ Object.entries(action.payload.entities)
+ .forEach(([id, entity]) => state.uris[entity.uri] = id);
+ },
+ },
+});
+
+export default slice.reducer;
diff --git a/src/client/resources/entity/component/traits.jsx b/src/client/resources/entity/traits.jsx
similarity index 100%
rename from src/client/resources/entity/component/traits.jsx
rename to src/client/resources/entity/traits.jsx
diff --git a/src/client/resources/entity/component/traits.raw.scss b/src/client/resources/entity/traits.raw.scss
similarity index 100%
rename from src/client/resources/entity/component/traits.raw.scss
rename to src/client/resources/entity/traits.raw.scss
diff --git a/src/client/store.js b/src/client/store.js
new file mode 100644
index 0000000..4d5a750
--- /dev/null
+++ b/src/client/store.js
@@ -0,0 +1,25 @@
+import merge from 'deepmerge';
+
+import {combineReducers} from 'redux';
+
+import createCommonStore from '~/common/store';
+
+import {all} from './resources.scwp';
+
+const resources = Object.values(all()).map((M) => M.default);
+const reducerMap = resources.reduce(
+ (r, {keys, state: {'default': reducer}}) => ({...r, [keys[1]]: reducer}),
+ {}
+);
+
+export default function createStore(options = {}) {
+ return createCommonStore(
+ merge(
+ options,
+ {
+ middleware: [],
+ reducer: combineReducers(reducerMap),
+ },
+ ),
+ );
+}
diff --git a/src/common/store/effects.js b/src/common/store/effects.js
new file mode 100644
index 0000000..02a61be
--- /dev/null
+++ b/src/common/store/effects.js
@@ -0,0 +1,2 @@
+export default {
+};
diff --git a/src/common/store/index.js b/src/common/store/index.js
new file mode 100644
index 0000000..ef3954f
--- /dev/null
+++ b/src/common/store/index.js
@@ -0,0 +1,18 @@
+import merge from 'deepmerge';
+import {configureStore, getDefaultMiddleware} from '@reduxjs/toolkit';
+
+import commonMiddleware from './middleware';
+
+export default function createStore(options = {}) {
+ return configureStore(
+ merge(
+ {
+ middleware: [
+ ...getDefaultMiddleware(),
+ commonMiddleware,
+ ],
+ },
+ options,
+ ),
+ );
+}
diff --git a/src/common/store/middleware.js b/src/common/store/middleware.js
new file mode 100644
index 0000000..55baa48
--- /dev/null
+++ b/src/common/store/middleware.js
@@ -0,0 +1,16 @@
+import effects from './effects';
+
+const debug = require('debug')('persea:common:store');
+
+export default (store) => (next) => (action) => {
+ const {meta, payload} = action;
+ debug("action '%s' dispatched: %o", action.type, {
+ payload,
+ meta,
+ });
+ const result = next(action);
+ if (effects[action.type]) {
+ setTimeout(() => effects[action.type](store, action), 0);
+ }
+ return result;
+};
diff --git a/src/server/app.js b/src/server/app.js
index cf02243..db296c7 100644
--- a/src/server/app.js
+++ b/src/server/app.js
@@ -1,6 +1,8 @@
import express from 'express';
import httpSession from 'express-session';
+import createRoutes from './routes/create';
+
const MemoryStore = require('memorystore')(httpSession);
let insideSession;
@@ -19,6 +21,7 @@ export default function createApp() {
}),
});
app.use(insideSession);
+ createRoutes(app);
return app;
}
diff --git a/src/server/routes/create.js b/src/server/routes/create.js
new file mode 100644
index 0000000..9bd7e0a
--- /dev/null
+++ b/src/server/routes/create.js
@@ -0,0 +1,5 @@
+import express from 'express';
+
+export default function createRoutes(app) {
+ app.use('/resources', express.static('resources'));
+}
diff --git a/yarn.lock b/yarn.lock
index 9f6c8f3..1c1ee28 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -989,6 +989,13 @@
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.5.5":
+ version "7.10.3"
+ resolved "https://npm.i12e.cha0s.io/@babel%2fruntime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364"
+ integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.10.1":
version "7.10.1"
resolved "https://npm.i12e.cha0s.io/@babel%2ftemplate/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
@@ -6598,6 +6605,11 @@ normalize-url@1.9.1:
query-string "^4.1.0"
sort-keys "^1.0.0"
+normalizr@^3.6.0:
+ version "3.6.0"
+ resolved "https://npm.i12e.cha0s.io/normalizr/-/normalizr-3.6.0.tgz#b8bbc4546ffe43c1c2200503041642915fcd3e1c"
+ integrity sha512-25cd8DiDu+pL46KIaxtVVvvEPjGacJgv0yUg950evr62dQ/ks2JO1kf7+Vi5/rMFjaSTSTls7aCnmRlUSljtiA==
+
notepack.io@~2.1.2:
version "2.1.3"
resolved "https://npm.i12e.cha0s.io/notepack.io/-/notepack.io-2.1.3.tgz#cc904045c751b1a27b2dcfd838d81d0bf3ced923"
@@ -7601,7 +7613,7 @@ react-hot-loader@^4.12.21:
shallowequal "^1.1.0"
source-map "^0.7.3"
-react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6:
+react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6, react-is@^16.9.0:
version "16.13.1"
resolved "https://npm.i12e.cha0s.io/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -7625,6 +7637,17 @@ react-markdown@^4.3.1:
unist-util-visit "^1.3.0"
xtend "^4.0.1"
+react-redux@^7.2.0:
+ version "7.2.0"
+ resolved "https://npm.i12e.cha0s.io/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"
+ integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ hoist-non-react-statics "^3.3.0"
+ loose-envify "^1.4.0"
+ prop-types "^15.7.2"
+ react-is "^16.9.0"
+
react-tabs@^3.1.1:
version "3.1.1"
resolved "https://npm.i12e.cha0s.io/react-tabs/-/react-tabs-3.1.1.tgz#b363a239f76046bb2158875a1e5921b11064052f"