finish redux feature parity

This commit is contained in:
41666 2017-12-16 17:39:53 -06:00
parent f5220aa6dc
commit 5510d5a1c4
29 changed files with 220 additions and 100 deletions

View file

@ -1,4 +1,4 @@
const log = new (require('./logger'))('World') const log = new (require('./logger'))('Roleypoly')
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const fetchModels = require('./models') const fetchModels = require('./models')
const fetchApis = require('./api') const fetchApis = require('./api')
@ -40,9 +40,9 @@ class Roleypoly {
// enableReadyCheck: true, // enableReadyCheck: true,
// enableOfflineQueue: true // enableOfflineQueue: true
// }) // })
this.ctx.discord = new (require('./services/discord'))(this.ctx) this.ctx.discord = new (require('./services/discord'))(this.ctx)
this.ctx.sessions = new (require('./services/sessions'))(this.ctx) this.ctx.sessions = new (require('./services/sessions'))(this.ctx)
this.ctx.P = new (require('./services/presentation'))(this.ctx)
} }
async mountRoutes () { async mountRoutes () {

View file

@ -36,6 +36,7 @@ module.exports = (R, $) => {
if (ctx.session.accessToken === undefined) { if (ctx.session.accessToken === undefined) {
ctx.body = { err: 'not_logged_in' } ctx.body = { err: 'not_logged_in' }
ctx.status = 401 ctx.status = 401
return
} }
const user = await $.discord.getUser(ctx.session.accessToken) const user = await $.discord.getUser(ctx.session.accessToken)

View file

@ -2,7 +2,7 @@ module.exports = (R, $) => {
R.get('/api/servers', async (ctx) => { R.get('/api/servers', async (ctx) => {
const { userId } = ctx.session const { userId } = ctx.session
const srv = $.discord.getRelevantServers(userId) const srv = $.discord.getRelevantServers(userId)
const presentable = $.discord.presentableServers(srv, userId) const presentable = $.P.oldPresentableServers(srv, userId)
ctx.body = presentable ctx.body = presentable
}) })

View file

@ -37,9 +37,9 @@ async function start () {
await next() await next()
} catch (e) { } catch (e) {
log.error(e) log.error(e)
ctx.status = 500 ctx.status = ctx.status || 500
if (DEVEL) { if (DEVEL) {
ctx.body = e.stack ctx.body = ctx.body || e.stack
} else { } else {
ctx.body = { ctx.body = {
err: 'something terrible happened.' err: 'something terrible happened.'

View file

@ -7,7 +7,7 @@ module.exports = (sql, DataTypes) => {
categories: { categories: {
type: DataTypes.JSON type: DataTypes.JSON
}, },
note: { message: {
type: DataTypes.TEXT type: DataTypes.TEXT
} }
}) })

View file

@ -26,6 +26,7 @@
"koa-passport": "^4.0.1", "koa-passport": "^4.0.1",
"koa-session": "^5.5.1", "koa-session": "^5.5.1",
"ksuid": "^0.4.0", "ksuid": "^0.4.0",
"lru-cache": "^4.1.1",
"passport-discord": "^0.1.3", "passport-discord": "^0.1.3",
"passport-oauth2-refresh": "^1.0.0", "passport-oauth2-refresh": "^1.0.0",
"pg": "^7.4.0", "pg": "^7.4.0",

View file

@ -15,6 +15,7 @@ class DiscordService extends Service {
this.client = new discord.Client() this.client = new discord.Client()
this.startBot() this.startBot()
} }
async startBot () { async startBot () {
@ -25,42 +26,12 @@ class DiscordService extends Service {
return this.client.guilds.filter((g) => g.members.has(userId)) return this.client.guilds.filter((g) => g.members.has(userId))
} }
presentableServers (collection, userId) { gm (serverId, userId) {
return collection.map((server) => { return this.client.guilds.get(serverId).members.get(userId)
const gm = server.members.get(userId)
return {
id: server.id,
gm: {
nickname: gm.nickname,
color: gm.displayHexColor
},
server: {
id: server.id,
name: server.name,
ownerID: server.ownerID,
icon: server.icon
},
roles: this.presentableRoles(server.id, gm),
message: 'moe moe kyuuuuuuuuun~',
perms: this.getPermissions(gm)
}
})
} }
presentableRoles (serverId, gm) { getRoles (server) {
return this.client.guilds return this.client.guilds.get(server).roles
.get(serverId)
.roles
.filter(r => r.id !== serverId)
.map((role) => ({
color: role.hexColor,
position: role.position,
calculatedPosition: role.calculatedPosition,
id: role.id,
name: role.name,
selected: gm.roles.has(role.id)
}))
} }
getPermissions (gm) { getPermissions (gm) {

View file

@ -0,0 +1,49 @@
const Service = require('./Service')
const LRU = require('lru-cache')
class PresentationService extends Service {
constructor (ctx) {
super(ctx)
this.M = ctx.M
this.discord = ctx.discord
this.cache = LRU({ max: 500, maxAge: 100 * 60 * 5 })
}
oldPresentableServers (collection, userId) {
return collection.map((server) => {
const gm = server.members.get(userId)
return {
id: server.id,
gm: {
nickname: gm.nickname,
color: gm.displayHexColor
},
server: {
id: server.id,
name: server.name,
ownerID: server.ownerID,
icon: server.icon
},
roles: server.roles.filter(r => r.id !== server.id).map(r => ({
id: r.id,
color: r.color,
name: r.name,
selected: gm.roles.has(r.id),
position: r.position
})),
message: 'moe moe kyuuuuuuuuun~',
perms: this.discord.getPermissions(gm)
}
})
}
rolesByServer (serverId, userId) {
// get from discord, merge with server categories
}
}
module.exports = PresentationService

42
Server/services/server.js Normal file
View file

@ -0,0 +1,42 @@
const Service = require('./Service')
class ServerService extends Service {
constructor (ctx) {
super(ctx)
this.Server = ctx.M.Server
this.P = ctx.P
}
async ensure (server) {
const srv = await this.get(server.id)
if (srv == null) {
return this.create({
id: server.id,
message: '',
categories: {}
})
}
}
create ({ id, message, categories }) {
const srv = this.Server.build({ id, message, categories })
return srv.save()
}
update (id, newData) {
const srv = this.get(id)
return srv.update(newData)
}
get (id) {
return this.Server.findOne({
where: {
id
}
})
}
}
module.exports = ServerService

View file

@ -2022,6 +2022,13 @@ lru-cache@^4.0.1:
pseudomap "^1.0.1" pseudomap "^1.0.1"
yallist "^2.0.0" yallist "^2.0.0"
lru-cache@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"
media-typer@0.3.0: media-typer@0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@ -2635,7 +2642,7 @@ promptly@2.2.0:
dependencies: dependencies:
read "^1.0.4" read "^1.0.4"
pseudomap@^1.0.1: pseudomap@^1.0.1, pseudomap@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
@ -3441,7 +3448,7 @@ y18n@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
yallist@^2.0.0: yallist@^2.0.0, yallist@^2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"

View file

@ -6,6 +6,7 @@
"color": "^2.0.1", "color": "^2.0.1",
"custom-react-scripts": "0.2.1", "custom-react-scripts": "0.2.1",
"eslint": "^4.13.0", "eslint": "^4.13.0",
"history": "^4.7.2",
"immutable": "^3.8.2", "immutable": "^3.8.2",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"react": "^16.2.0", "react": "^16.2.0",
@ -14,6 +15,7 @@
"react-redux": "^5.0.6", "react-redux": "^5.0.6",
"react-router": "^4.2.0", "react-router": "^4.2.0",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-router-redux": "^5.0.0-alpha.8",
"redux": "^3.7.2", "redux": "^3.7.2",
"redux-devtools": "^3.4.1", "redux-devtools": "^3.4.1",
"redux-devtools-dock-monitor": "^1.1.2", "redux-devtools-dock-monitor": "^1.1.2",

View file

@ -4,7 +4,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" sizes="any" href="%PUBLIC_URL%/favicon.png"/> <link rel="icon" type="image/png" sizes="any" href="%PUBLIC_URL%/favicon.png"/>
<title>Roleypoly</title> <title>Roleypoly</title>
<link rel="stylesheet" href="https://use.typekit.net/bck0pci.css"> <script src="https://use.typekit.net/bck0pci.js"></script>
<script>try{Typekit.load({ async: true });}catch(e){}</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/css/uikit.min.css" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/css/uikit.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/js/uikit.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/js/uikit.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/js/uikit-icons.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/js/uikit-icons.min.js"></script>
@ -12,7 +13,7 @@
<noscript> <noscript>
You need to enable JavaScript to run this app. You need to enable JavaScript to run this app.
</noscript> </noscript>
<div id="root" class="tk-source-han-sans-japanese"></div> <div id="root" class=""></div>
<!-- <!--
This HTML file is a template. This HTML file is a template.
If you open it directly in the browser, you will see an empty page. If you open it directly in the browser, you will see an empty page.

View file

@ -1,14 +1,17 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import './App.css' import createHistory from 'history/createBrowserHistory'
import { ConnectedRouter } from 'react-router-redux'
import configureStore from './store/configureStore' import configureStore from './store/configureStore'
import './App.css'
import Wrapper from './components/wrapper' import Wrapper from './components/wrapper'
import AppRouter from './router' import AppRouter from './router'
import { userInit } from './actions' import { userInit } from './actions'
const store = configureStore() const history = createHistory()
const store = configureStore(undefined, history)
window.__APP_STORE__ = store window.__APP_STORE__ = store
@ -20,11 +23,11 @@ class App extends Component {
render () { render () {
return ( return (
<Provider store={store}> <Provider store={store}>
<BrowserRouter> <ConnectedRouter history={history}>
<Wrapper> <Wrapper>
<AppRouter /> <AppRouter />
</Wrapper> </Wrapper>
</BrowserRouter> </ConnectedRouter>
</Provider> </Provider>
) )
} }

View file

@ -14,19 +14,23 @@ export const fetchServers = async dispatch => {
} }
export const userInit = async dispatch => { export const userInit = async dispatch => {
try { if (!window.location.pathname.startsWith('/oauth')) {
const rsp = await superagent.get('/api/auth/user') try {
const rsp = await superagent.get('/api/auth/user')
dispatch({
type: Symbol.for('set user'),
data: rsp.body
})
dispatch(fetchServers) dispatch({
} catch (e) { type: Symbol.for('set user'),
if (!window.location.pathname.startsWith('/oauth')) { data: rsp.body
})
dispatch(fetchServers)
} catch (e) {
window.location.href = '/oauth/flow' window.location.href = '/oauth/flow'
} }
} else {
dispatch({
type: Symbol.for('app ready')
})
} }
} }

View file

@ -12,16 +12,16 @@ export const roleUpdate = (id, oldState) => (dispatch, getState) => {
} }
export const setup = id => async dispatch => { export const setup = id => async dispatch => {
const rsp = await superagent.get(`/api/server/${id}`) // const rsp = await superagent.get(`/api/server/${id}`)
const data = rsp.body // const data = rsp.body
dispatch({ // dispatch({
type: Symbol.for('update server roles'), // type: Symbol.for('update server roles'),
data: { // data: {
id, // id,
roles: data // roles: data
} // }
}) // })
dispatch(constructView(id)) dispatch(constructView(id))
} }
@ -31,9 +31,7 @@ export const constructView = id => (dispatch, getState) => {
const roles = server.get('roles') const roles = server.get('roles')
const categories = roles.groupBy(x => x.get('category')) const categories = roles.groupBy(x => x.get('category'))
const selected = roles.reduce((acc, r) => { const selected = roles.reduce((acc, r) => acc.set(r.get('id'), r.get('selected')), Map())
return acc.set(r.id, r.selected)
}, Map())
console.log(categories, selected) console.log(categories, selected)
dispatch({ dispatch({

View file

@ -7,7 +7,6 @@ import './RolePicker.sass'
import Role from '../role' import Role from '../role'
const mapState = ({ rolePicker, servers }, ownProps) => { const mapState = ({ rolePicker, servers }, ownProps) => {
console.log(servers)
return { return {
data: rolePicker, data: rolePicker,
server: servers.get(ownProps.match.params.server) server: servers.get(ownProps.match.params.server)
@ -21,8 +20,12 @@ class RolePicker extends Component {
dispatch(Actions.setup(server)) dispatch(Actions.setup(server))
} }
isSelected (id) {
return this.props.data.getIn([ 'rolesSelected', id ])
}
render () { render () {
console.log(this.props) console.log(this.constructor.name, this.props)
if (this.props.server === undefined) { if (this.props.server === undefined) {
return null return null
} }
@ -37,11 +40,11 @@ class RolePicker extends Component {
} }
<section> <section>
<h3>Roles</h3> <h3>Roles</h3>
{/* { {
this.props.data.roles.map((r, k) => { this.props.server.get('roles').map((r, k) => {
return <Role key={k} role={r} onToggle={this.dispatch(Actions.roleUpdate(r.id, r.selected))} /> return <Role key={k} role={r} selected={this.isSelected(r.get('id'))} onToggle={() => this.props.dispatch(Actions.roleUpdate(r.get('id'), this.isSelected(r.get('id'))))} />
}) })
} */} }
</section> </section>
</div> </div>
} }

View file

@ -20,7 +20,11 @@
border-color: var(--role-color-hover) border-color: var(--role-color-hover)
background-color: transparent background-color: transparent
&:active &:active
box-shadow: none box-shadow: none
&:active .role__option
box-shadow: none
&__option &__option
border-radius: 50% border-radius: 50%

View file

@ -9,12 +9,14 @@ class Role extends Component {
static propTypes = { static propTypes = {
role: PropTypes.object.isRequired, role: PropTypes.object.isRequired,
onToggle: PropTypes.func, onToggle: PropTypes.func,
type: PropTypes.string type: PropTypes.string,
selected: PropTypes.bool.isRequired
} }
render () { render () {
const { role } = this.props let { role, selected } = this.props
let color = Color(role.color)
let color = Color(role.get('color'))
if (color.rgbNumber() === 0) { if (color.rgbNumber() === 0) {
color = whiteColor color = whiteColor
@ -24,17 +26,16 @@ class Role extends Component {
let hc = color.lighten(0.1) let hc = color.lighten(0.1)
return <div return <div
onClick={this.props.onToggle.bind(null, !role.selected, role.selected)} onClick={this.props.onToggle.bind(null, !selected, selected)}
className='role' className='role font-sans-serif'
style={{ style={{
'--role-color-hex': c.string(), '--role-color-hex': c.string(),
'--role-color-hover': hc.string(), '--role-color-hover': hc.string(),
'--role-color-rgba': `rgba(${c.red()}, ${c.green()}, ${c.blue()}, 0.7)` '--role-color-rgba': `rgba(${c.red()}, ${c.green()}, ${c.blue()}, 0.7)`
}}> }}>
{/* circle svg */} <div className={`role__option ${(selected) ? 'selected' : ''}`}/>
<div className={`role__option ${(role.selected) ? 'selected' : ''}`}/>
<div className='role__name'> <div className='role__name'>
{role.name} {role.get('name')}
</div> </div>
</div> </div>
} }

View file

@ -5,10 +5,9 @@ import ServerCard from './ServerCard'
import UserCard from './UserCard' import UserCard from './UserCard'
class ServersNavigation extends Component { class ServersNavigation extends Component {
static propTypes = { static propTypes = {
user: ImmutablePropTypes.map.isRequired, user: ImmutablePropTypes.map.isRequired,
servers: ImmutablePropTypes.setOf(ImmutablePropTypes.orderedMap).isRequired, servers: ImmutablePropTypes.orderedMapOf(ImmutablePropTypes.map).isRequired,
className: PropTypes.string className: PropTypes.string
} }

View file

@ -1,8 +1,9 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { connect } from 'react-redux'
import ImmutablePropTypes from 'react-immutable-proptypes' import ImmutablePropTypes from 'react-immutable-proptypes'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import Radium from 'radium'
import './ServerCard.sass' import './ServerCard.sass'
import { withRouter } from 'react-router';
class ServerCard extends Component { class ServerCard extends Component {
static propTypes = { static propTypes = {

View file

@ -5,6 +5,7 @@ import './index.sass'
import Navigation from './Navigation' import Navigation from './Navigation'
import RolePicker from '../role-picker' import RolePicker from '../role-picker'
import { withRouter } from 'react-router';
// import mockData from './mockData' // import mockData from './mockData'

View file

@ -7,6 +7,10 @@ body {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.font-sans-serif {
font-family: sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
:root { :root {
--c-white: #efefef; --c-white: #efefef;
--c-9: #EBD6D4; --c-9: #EBD6D4;

View file

@ -3,6 +3,7 @@ import { combineReducers } from 'redux'
import servers from './servers' import servers from './servers'
import user from './user' import user from './user'
import rolePicker from './role-picker' import rolePicker from './role-picker'
import { routerMiddleware } from 'react-router-redux';
// import roles from './roles' // import roles from './roles'
const initialState = { const initialState = {
@ -25,6 +26,7 @@ const rootReducer = combineReducers({
appState, appState,
servers, servers,
user, user,
router: routerMiddleware,
// roles, // roles,
rolePicker rolePicker
}) })

View file

@ -1,9 +1,9 @@
import { Map, Set } from 'immutable' import { Map, OrderedMap } from 'immutable'
const initialState = Map({ const initialState = Map({
hidden: true, // should the view be hidden? hidden: true, // should the view be hidden?
emptyRoles: true, // helps derender roles so there's no visible element state change emptyRoles: true, // helps derender roles so there's no visible element state change
viewMap: Set([]), // roles in categories viewMap: OrderedMap({}), // roles in categories
originalRolesSelected: Map({}), // Map<role id, bool> -- original roles for diffing against selected originalRolesSelected: Map({}), // Map<role id, bool> -- original roles for diffing against selected
rolesSelected: Map({}) // Map<role id, bool> -- new roles for diffing rolesSelected: Map({}) // Map<role id, bool> -- new roles for diffing
}) })
@ -11,8 +11,8 @@ const initialState = Map({
export default (state = initialState, { type, data }) => { export default (state = initialState, { type, data }) => {
switch (type) { switch (type) {
case Symbol.for('setup role picker'): case Symbol.for('setup role picker'):
return state.merge(data) return state.mergeDeep(data)
case Symbol.for('hide role picker ui'): case Symbol.for('hide role picker ui'):
return { return {
...state, ...state,
@ -24,12 +24,12 @@ export default (state = initialState, { type, data }) => {
...state, ...state,
emptyRoles: data emptyRoles: data
} }
case Symbol.for('zero role picker'):
return initialState
case Symbol.for('update selected roles'): case Symbol.for('update selected roles'):
return state.set('rolesSelected', state.get('rolesSelected').set(data.id, data.state)) return state.setIn(['rolesSelected', data.id], data.state)
case Symbol.for('zero role picker'):
return initialState
default: default:
return state return state

View file

@ -5,9 +5,11 @@ import { connect } from 'react-redux'
import Servers from '../components/servers' import Servers from '../components/servers'
import OauthCallback from '../components/oauth-callback' import OauthCallback from '../components/oauth-callback'
import OauthFlow from '../components/oauth-flow' import OauthFlow from '../components/oauth-flow'
import { withRouter } from 'react-router';
const aaa = (props) => (<div>{ JSON.stringify(props) }</div>) const aaa = (props) => (<div>{ JSON.stringify(props) }</div>)
@withRouter
@connect(({ appState }) => ({ ready: appState.ready })) @connect(({ appState }) => ({ ready: appState.ready }))
export default class AppRouter extends Component { export default class AppRouter extends Component {
render () { render () {

View file

@ -5,13 +5,14 @@ import { createLogger } from 'redux-logger'
// import api from '../middleware/api' // import api from '../middleware/api'
import rootReducer from '../reducers' import rootReducer from '../reducers'
import DevTools from '../components/dev-tools' import DevTools from '../components/dev-tools'
import { routerMiddleware } from 'react-router-redux'
const configureStore = preloadedState => { const configureStore = (preloadedState, history) => {
const store = createStore( const store = createStore(
rootReducer, rootReducer,
preloadedState, preloadedState,
compose( compose(
applyMiddleware(thunk, createLogger()), applyMiddleware(thunk, routerMiddleware(history), createLogger()),
DevTools.instrument() DevTools.instrument()
) )
) )

View file

@ -1,12 +1,14 @@
import { createStore, applyMiddleware } from 'redux' import { createStore, applyMiddleware } from 'redux'
import { routerMiddleware } from 'react-router-redux'
import thunk from 'redux-thunk' import thunk from 'redux-thunk'
// import api from '../middleware/api' // import api from '../middleware/api'
import rootReducer from '../reducers' import rootReducer from '../reducers'
const configureStore = preloadedState => createStore( const configureStore = (preloadedState, history) => createStore(
rootReducer, rootReducer,
preloadedState, preloadedState,
applyMiddleware(thunk) applyMiddleware(thunk, routerMiddleware(history))
) )
export default configureStore export default configureStore

View file

@ -6094,6 +6094,14 @@ react-router-dom@^4.2.2:
react-router "^4.2.0" react-router "^4.2.0"
warning "^3.0.0" warning "^3.0.0"
react-router-redux@^5.0.0-alpha.8:
version "5.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-5.0.0-alpha.8.tgz#5242c705730b2ac862aff7a8e90f870d0cf45e12"
dependencies:
history "^4.7.2"
prop-types "^15.6.0"
react-router "^4.2.0"
react-router@^4.2.0: react-router@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.2.0.tgz#61f7b3e3770daeb24062dae3eedef1b054155986" resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.2.0.tgz#61f7b3e3770daeb24062dae3eedef1b054155986"

13
start.sh Executable file
View file

@ -0,0 +1,13 @@
#!/bin/sh
docker-compose up -d
cd Server
yarn
yarn dev
cd ..
cd UI
yarn
yarn start