diff --git a/.babelrc b/.babelrc index 2507b5a..1d42ce1 100644 --- a/.babelrc +++ b/.babelrc @@ -1,8 +1,14 @@ { - "presets": [ - "next/babel" - ], + "presets": [ ["@babel/preset-env", { + "targets": { + "node": true + } + }], "@babel/preset-flow" ], "plugins": [ - "transform-flow-strip-types" - ] + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-proposal-class-properties", + ["@babel/plugin-transform-runtime", + { "helpers": false }] + ], + "ignore": ["ui/**/*"] } diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index b4a10d9..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": [ "standard" ], - "parser": "babel-eslint" -} diff --git a/.gitignore b/.gitignore index 23f699f..f5de1ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ .env *.env - +dist /docker-compose.test.yml node_modules .vscode .data + +yarn-error\.log diff --git a/Dockerfile b/Dockerfile index a677dd1..1808301 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,4 +10,4 @@ FROM mhart/alpine-node:10 ENV NODE_ENV production WORKDIR /dist COPY --from=builder /src /dist -CMD node index.js +CMD npm start diff --git a/Roleypoly.js b/Roleypoly.js index 61b8f71..c8d1c99 100644 --- a/Roleypoly.js +++ b/Roleypoly.js @@ -1,29 +1,94 @@ -const log = new (require('./logger'))('Roleypoly') -const Sequelize = require('sequelize') -const fetchModels = require('./models') -const fetchApis = require('./api') -const Next = require('next') -const betterRouter = require('koa-better-router') +// @flow +import Sequelize from 'sequelize' +import Next from 'next' +import betterRouter from 'koa-better-router' +import EventEmitter from 'events' +import fs from 'fs' +import logger from './logger' +import ServerService from './services/server' +import DiscordService from './services/discord' +import SessionService from './services/sessions' +import PresentationService from './services/presentation' +import RPCServer from './rpc' +import fetchModels, { type Models } from './models' +import fetchApis from './api' + +import type SocketIO from 'socket.io' +import type KoaApp, { Context } from 'koa' + +const log = logger(__filename) + +type HTTPHandler = (path: string, handler: (ctx: Context, next: () => void) => any) => void +export type Router = { + get: HTTPHandler, + post: HTTPHandler, + patch: HTTPHandler, + delete: HTTPHandler, + put: HTTPHandler, + middleware: () => any +} +export type AppContext = { + config: { + appUrl: string, + dev: boolean, + hotReload: boolean + }, + ui: Next, + uiHandler: Next.Handler, + io: SocketIO, + + server: ServerService, + discord: DiscordService, + sessions: SessionService, + P: PresentationService, + RPC: RPCServer, + M: Models, + sql: Sequelize +} class Roleypoly { - constructor (io, app) { + ctx: AppContext|any + io: SocketIO + router: Router + + M: Models + + __app: KoaApp + __initialized: Promise + __apiWatcher: EventEmitter + __rpcWatcher: EventEmitter + constructor (io: SocketIO, app: KoaApp) { this.io = io - this.ctx = {} - - this.ctx.config = { - appUrl: process.env.APP_URL, - dev: process.env.NODE_ENV !== 'production', - hotReload: process.env.NO_HOT_RELOAD !== '1' - } - - this.ctx.io = io this.__app = app if (log.debugOn) log.warn('debug mode is on') const dev = process.env.NODE_ENV !== 'production' - this.ctx.ui = Next({ dev, dir: './ui' }) - this.ctx.uiHandler = this.ctx.ui.getRequestHandler() + + // simple check if we're in a compiled situation or not. + let uiDir = './ui' + if (!fs.existsSync(uiDir) && fs.existsSync('../ui')) { + uiDir = '../ui' + } + + const ui = Next({ dev, dir: uiDir }) + const uiHandler = ui.getRequestHandler() + + const appUrl = process.env.APP_URL + if (appUrl == null) { + throw new Error('APP_URL was unset.') + } + + this.ctx = { + config: { + appUrl, + dev, + hotReload: process.env.NO_HOT_RELOAD !== '1' + }, + io, + ui, + uiHandler + } this.__initialized = this._mountServices() } @@ -33,31 +98,30 @@ class Roleypoly { } async _mountServices () { - const sequelize = new Sequelize(process.env.DB_URL, { logging: log.sql.bind(log, log) }) + const dbUrl: ?string = process.env.DB_URL + if (dbUrl == null) { + throw log.fatal('DB_URL not set.') + } + + const sequelize = new Sequelize(dbUrl, { logging: log.sql.bind(log, log) }) this.ctx.sql = sequelize this.M = fetchModels(sequelize) this.ctx.M = this.M await sequelize.sync() - // this.ctx.redis = new (require('ioredis'))({ - // port: process.env.REDIS_PORT || '6379', - // host: process.env.REDIS_HOST || 'localhost', - // parser: 'hiredis', - // dropBufferSupport: true, - // enableReadyCheck: true, - // enableOfflineQueue: true - // }) - this.ctx.server = new (require('./services/server'))(this.ctx) - this.ctx.discord = new (require('./services/discord'))(this.ctx) - this.ctx.sessions = new (require('./services/sessions'))(this.ctx) - this.ctx.P = new (require('./services/presentation'))(this.ctx) + this.ctx.server = new ServerService(this.ctx) + this.ctx.discord = new DiscordService(this.ctx) + this.ctx.sessions = new SessionService(this.ctx) + this.ctx.P = new PresentationService(this.ctx) + this.ctx.RPC = new RPCServer(this) } - async loadRoutes (forceClear = false) { + async loadRoutes (forceClear: boolean = false) { await this.ctx.ui.prepare() this.router = betterRouter().loadMethods() fetchApis(this.router, this.ctx, { forceClear }) + this.ctx.RPC.hookRoutes(this.router) // after routing, add the * for ui handler this.router.get('*', async ctx => { @@ -83,6 +147,12 @@ class Roleypoly { hotMiddleware = await this.loadRoutes(true) }) + this.__rpcWatcher = chokidar.watch('rpc/**') + this.__rpcWatcher.on('all', (path) => { + log.info('reloading RPCs...', path) + this.ctx.RPC.reload() + }) + // custom passthrough so we use a specially scoped middleware. mw = (ctx, next) => { return hotMiddleware(ctx, next) diff --git a/UI/.babelrc b/UI/.babelrc new file mode 100644 index 0000000..571c6da --- /dev/null +++ b/UI/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": [ + "next/babel", "@babel/preset-flow" + ] +} diff --git a/UI/components/global-colors.js b/UI/components/global-colors.js index 8d3f5de..938e9f2 100644 --- a/UI/components/global-colors.js +++ b/UI/components/global-colors.js @@ -1,3 +1,6 @@ +// @flow +import * as React from 'react' + export const colors = { white: '#efefef', c9: '#EBD6D4', @@ -12,7 +15,7 @@ export const colors = { } const getColors = () => { - Object.keys(colors).map(key => { + return Object.keys(colors).map(key => { const nk = key.replace(/c([0-9])/, '$1') return `--c-${nk}: ${colors[key]};` }).join(' \n') @@ -26,6 +29,18 @@ body { text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + /* prevent FOUC */ + transition: opacity 0.2s ease-in-out; + opacity: 0; +} + +.wf-active body { + opacity: 1; +} + +// FOUC guard if we take too long +.force-active body { + opacity: 1; } .font-sans-serif { diff --git a/UI/components/header/auth.js b/UI/components/header/auth.js new file mode 100644 index 0000000..a265421 --- /dev/null +++ b/UI/components/header/auth.js @@ -0,0 +1,11 @@ +// @flow +import * as React from 'react' +import HeaderBarCommon from './common' + +const HeaderBarAuth: React.StatelessFunctionalComponent<{}> = () => ( + + hi + +) + +export default HeaderBarAuth diff --git a/UI/components/header/common.js b/UI/components/header/common.js new file mode 100644 index 0000000..f80c712 --- /dev/null +++ b/UI/components/header/common.js @@ -0,0 +1,14 @@ +// @flow +import * as React from 'react' + +export type CommonProps = { + children: React.Element +} + +const HeaderBarCommon: React.StatelessFunctionalComponent = ({ children }) => ( +
+ { children } +
+) + +export default HeaderBarCommon diff --git a/UI/components/header/unauth.js b/UI/components/header/unauth.js new file mode 100644 index 0000000..0155a84 --- /dev/null +++ b/UI/components/header/unauth.js @@ -0,0 +1,11 @@ +// @flow +import * as React from 'react' +import HeaderBarCommon from './common' + +const HeaderBarUnauth: React.StatelessFunctionalComponent<{}> = () => ( + + hi + +) + +export default HeaderBarUnauth diff --git a/UI/components/social-cards.js b/UI/components/social-cards.js new file mode 100644 index 0000000..1a72cd7 --- /dev/null +++ b/UI/components/social-cards.js @@ -0,0 +1,36 @@ +// @flow +import * as React from 'react' +import NextHead from 'next/head' + +export type SocialCardProps = { + title?: string, + description?: string, + image?: string, + imageSize?: number +} + +const defaultProps: SocialCardProps = { + title: 'Roleypoly', + description: 'Tame your Discord roles.', + image: 'https://rp.kat.cafe/static/social.png', + imageSize: 200 +} + +const SocialCards: React.StatelessFunctionalComponent = (props) => { + props = { + ...defaultProps, + ...props + } + + return + + + + + + + + +} + +export default SocialCards diff --git a/UI/config/redux.js b/UI/config/redux.js new file mode 100644 index 0000000..dd43cb3 --- /dev/null +++ b/UI/config/redux.js @@ -0,0 +1,15 @@ +import { createStore, applyMiddleware } from 'redux' +import { composeWithDevTools } from 'redux-devtools-extension' +import thunkMiddleware from 'redux-thunk' +import withNextRedux from 'next-redux-wrapper' +import { rootReducer } from 'fast-redux' + +export const initStore = (initialState = {}) => { + return createStore( + rootReducer, + initialState, + composeWithDevTools(applyMiddleware(thunkMiddleware)) + ) +} + +export const withRedux = (comp) => withNextRedux(initStore)(comp) diff --git a/UI/config/rpc.js b/UI/config/rpc.js new file mode 100644 index 0000000..707e231 --- /dev/null +++ b/UI/config/rpc.js @@ -0,0 +1,4 @@ +// @flow +import RPCClient from '../rpc' + +export default (new RPCClient({ forceDev: false })).rpc diff --git a/UI/containers/header-bar.js b/UI/containers/header-bar.js new file mode 100644 index 0000000..8a751f3 --- /dev/null +++ b/UI/containers/header-bar.js @@ -0,0 +1,30 @@ +// @flow +import * as React from 'react' +import dynamic from 'next/dynamic' +import { withRedux } from '../config/redux' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import { namespaceConfig } from 'fast-redux' +import * as User from './user' + +type Props = { + user: User.User +} + +const HeaderBarAuth = dynamic(() => import('../components/header/auth')) +const HeaderBarUnauth = dynamic(() => import('../components/header/unauth')) + +const HeaderBar: React.StatelessFunctionalComponent = () => { + // if () + return null +} + +const mapStateToProps = (state): Props => { + return {} +} + +function mapDispatchToProps (dispatch) { + return bindActionCreators({ }, dispatch) +} + +export default withRedux(connect(mapStateToProps, mapDispatchToProps)(HeaderBar)) diff --git a/UI/containers/user.js b/UI/containers/user.js new file mode 100644 index 0000000..117f2f7 --- /dev/null +++ b/UI/containers/user.js @@ -0,0 +1,32 @@ +// @flow +import { namespaceConfig } from 'fast-redux' + +export type User = { + id: string, + username: string, + discriminator: string, + avatar: string, + nicknameCache: { + [server: string]: string + } +} + +export type UserState = { + currentUser: User | null, + userCache: { + [id: string]: User + } +} + +const DEFAULT_STATE: UserState = { + currentUser: null, + userCache: {} +} + +export const { + action, getState: getUserStore +} = namespaceConfig('userStore', DEFAULT_STATE) + +export const getCurrentUser = () => async (dispatch: Function) => { + +} diff --git a/UI/pages/_app.js b/UI/pages/_app.js index 8944c01..36cb762 100644 --- a/UI/pages/_app.js +++ b/UI/pages/_app.js @@ -1,6 +1,9 @@ +import * as React from 'react' import App, { Container } from 'next/app' import Head from 'next/head' import GlobalColors from '../components/global-colors' +import SocialCards from '../components/social-cards' +// import RPCClient from '../rpc' class RoleypolyApp extends App { static async getInitialProps ({ Component, ctx }) { @@ -15,6 +18,7 @@ class RoleypolyApp extends App { componentDidMount () { this.loadTypekit(document) + this.waitForFOUC() } loadTypekit (d) { @@ -42,14 +46,26 @@ class RoleypolyApp extends App { s.parentNode.insertBefore(tk, s) } + // wait one second, add FOUC de-protection. + waitForFOUC () { + setTimeout(() => { + document.documentElement.className += ' force-active'// + }, 2000) + } + render () { - const { Component, pageProps, router } = this.props + const { Component, pageProps, router, rpc } = this.props return ( - + + + Roleypoly + + - + + ) } diff --git a/UI/pages/_internal/_server.js b/UI/pages/_internal/_server.js index e1745ab..a54434a 100644 --- a/UI/pages/_internal/_server.js +++ b/UI/pages/_internal/_server.js @@ -1,7 +1,19 @@ -const Server = ({ router: { query: { id } } }) => ( -
- {id} -
-) +// @flow +import * as React from 'react' +import Head from 'next/head' +import type { PageProps } from '../../types' +import SocialCards from '../../components/social-cards' -export default Server +export default class Server extends React.Component { + render () { + return ( +
+ + server name! + + + hello {this.props.router.query.id} +
+ ) + } +} diff --git a/UI/pages/index.js b/UI/pages/index.js index 2bb702b..1d1ef8b 100644 --- a/UI/pages/index.js +++ b/UI/pages/index.js @@ -5,24 +5,24 @@ import Nav from '../components/nav' const Home = () => (
- +