v2: init -- UI is nuked from orbit, major app restructuring

This commit is contained in:
41666 2019-02-23 18:16:31 -06:00
parent c6f5b55c1c
commit b8da886601
108 changed files with 6717 additions and 17430 deletions

View file

@ -1,7 +1,3 @@
Server/.env .env
Server/public/
.git/ .git/
node_modules/
*/node_modules/
UI/node_modules/
Server/node_modules/

30
.env.example Normal file
View file

@ -0,0 +1,30 @@
# database url, we mostly use postgres. others may work, but who knows.
# this is set up for the docker-compose.yml file included.
DB_URL=postgres://roleypoly:19216801@localhost:5432/roleypoly
# debug=true will increase logging
DEBUG=true
# development vs production changes how the UI does it's rendering.
# do NOT leave this on development for prod. that's dumb.
NODE_ENV=development
# discord settings.
DISCORD_CLIENT_ID=36391647431320739
DISCORD_CLIENT_SECRET=PZd3u4RkokhnB8MVdALPz5
DISCORD_BOT_TOKEN=qgTk4wm9Q7ECmMCovpmJVNCBltzJhL
# URL to the app for OAuth callbacks
APP_URL=http://localhost:6769
# CHANGE THIS ALWAYS. BUT EXACTLY ONCE.
# CHANGE THIS ALWAYS. BUT EXACTLY ONCE.
# CHANGE THIS ALWAYS. BUT EXACTLY ONCE.
# signing key for sessions. changing this will invalidate all sessions.
APP_KEY=PJoayPGqi8vfYVFYBDgSeJSDYUpzBX
# does this instance start a bot?
IS_BOT=true
# your and your friend's user ID for using admin tools.
ROOT_USERS=62601275618889721

9
.gitignore vendored
View file

@ -1,8 +1,9 @@
Server/\.env .env
*.env
/docker-compose.test.yml /docker-compose.test.yml
Server/prod\.env
Server/test\.env node_modules
UI~ .vscode
.data

View file

@ -1 +1,56 @@
roleypoly # roleypoly
a discord bot & web ui for managing self-assignable roles.
**Most likely, you'll want to go here: https://rp.kat.cafe**. This app is already hosted, you don't need to deal with deploying it or anything, I've already done it for you.
If you're here to report a bug or develop on Roleypoly, the rest of this document is for you.
## developing/running your own
you'll need
- a discord app and bot token
- a node environment (maybe?)
- a docker environment
- a hard hat because it's time to go building!
Check `.env.example` for all the various possible configuration values. Roleypoly is configured entirely over environment variables. In development, you might want to copy `.env.example` to `.env` so you don't need to set this up in your shell.
### for developers
```
docker-compose up -d
yarn
yarn dev
```
### for production
If you want an unedited latest version of roleypoly, it is available on the Docker Hub ([katie/roleypoly](https://hub.docker.com/r/katie/roleypoly)) for your using pleasure. An example docker-compose.yml is provided in `docker-compose.example.yml`, and all relevant environment variables (see `.env.example`) may be set there.
If you're not into Docker and/or want to deploy your own, simply run
```
yarn start
```
and you're off to the production races (sort of, you'll want to set up a `.env` file.)
The relevant `Dockerfile` is also included, so `docker build` is a useful way to deploy this too.
## scope & goal of project
I wanted to create a bot that let servers fully express theirselves through roles. the primary goal is clear in that regard, and it started with a (desktop-only) web experience. originally, a command-based bot wasn't on the menu, but i've likened up to the idea; but the single requirement is it *must* work in a fuzzy-match situation.
One of the biggest problems I set out to solve was the problem of emojis. Discord supports them and bots try to, but the fact of the matter is, not every bot treats emojis as first class citizens; and users can't really remember roles either. The problem is fine with 10 roles that are easy to remember and explain. This is impossible to manage with 250, Discord's cap on roles.
The primary goal, all-in-all, is to provide the single, best, end-all user experience for a bot that manages roles; until Discord gives us this theirselves.
— kayteh
## need help? wanna help us?
If you need any help, [please join our discord](https://discord.gg/m4GpWYY). That is the best way to contact the developers.
If your server needs something in particular to accomodate your server's requirements of user-assignable roles, please reach out to me over DMs on Discord, or email at [roleypoly@kat.cafe](mailto:roleypoly@kat.cafe)
If you'd like to give us incentive to continue developing and hosting this bot, please consider supporting it through [Patreon](https://patreon.com/kata), or via [PayPal](https://paypal.me/kayteh). All support is extremely appreciated, and not required for the use of the service.

View file

@ -2,6 +2,8 @@ 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')
const Next = require('next')
class Roleypoly { class Roleypoly {
constructor (router, io, app) { constructor (router, io, app) {
@ -18,6 +20,10 @@ class Roleypoly {
if (log.debugOn) log.warn('debug mode is on') 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()
this.__initialized = this._mountServices() this.__initialized = this._mountServices()
} }
@ -47,7 +53,16 @@ class Roleypoly {
} }
async mountRoutes () { async mountRoutes () {
await this.ctx.ui.prepare()
fetchApis(this.router, this.ctx) fetchApis(this.router, this.ctx)
// after routing, add the * for ui handler
this.router.get('*', async ctx => {
await this.ctx.uiHandler(ctx.req, ctx.res)
ctx.respond = false
})
this.__app.use(this.router.middleware()) this.__app.use(this.router.middleware())
} }
} }

View file

@ -1,10 +0,0 @@
DB_URL=postgres://roleypoly:19216801@localhost:5432/roleypoly
DEBUG=true
NODE_ENV=development
DISCORD_CLIENT_ID=36391647431320739
DISCORD_CLIENT_SECRET=PZd3u4RkokhnB8MVdALPz5
DISCORD_BOT_TOKEN=qgTk4wm9Q7ECmMCovpmJVNCBltzJhL
APP_URL=http://localhost:6769
APP_KEY=PJoayPGqi8vfYVFYBDgSeJSDYUpzBX
IS_BOT=true
ROOT_USERS=62601275618889721

View file

@ -1,3 +0,0 @@
module.exports = {
"extends": "standard"
};

4
Server/.gitignore vendored
View file

@ -1,4 +0,0 @@
node_modules
.data
.env
public

View file

@ -1,38 +0,0 @@
{
"name": "backend",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "standard && node index.js",
"fix": "standard --fix",
"dev": "pm2 start index.js --watch",
"pm2": "pm2"
},
"dependencies": {
"@discordjs/uws": "^11.149.1",
"chalk": "^2.4.1",
"discord.js": "^11.4.2",
"dotenv": "^6.1.0",
"erlpack": "github:discordapp/erlpack",
"eslint": "^5.8.0",
"eslint-config-standard": "^12.0.0",
"glob": "^7.1.3",
"immutable": "^3.8.2",
"koa": "^2.6.2",
"koa-better-router": "^2.1.1",
"koa-bodyparser": "^4.2.1",
"koa-compress": "^3.0.0",
"koa-send": "latest-2",
"koa-session": "^5.10.0",
"koa-static": "^5.0.0",
"ksuid": "^1.1.3",
"lru-cache": "^4.1.3",
"pg": "^7.6.1",
"pg-hstore": "^2.3.2",
"pm2": "^2.10.4",
"sequelize": "^4.41.2",
"socket.io": "^2.2.0",
"superagent": "^4.0.0",
"uuid": "^3.3.2"
}
}

File diff suppressed because it is too large Load diff

12
UI/.gitignore vendored
View file

@ -1,23 +1,19 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies # dependencies
/node_modules /node_modules
/.pnp
.pnp.js
# testing # testing
/coverage /coverage
# production # production
/build /build
/dist
/.next
# misc # misc
.DS_Store .DS_Store
.env.local .env
.env.development.local
.env.test.local
.env.production.local
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*

View file

@ -1,44 +0,0 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br>
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br>
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br>
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br>
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

44
UI/components/head.js Normal file
View file

@ -0,0 +1,44 @@
import React from 'react'
import NextHead from 'next/head'
import { string } from 'prop-types'
const defaultDescription = ''
const defaultOGURL = ''
const defaultOGImage = ''
const Head = props => (
<NextHead>
<meta charSet="UTF-8" />
<title>{props.title || ''}</title>
<meta
name="description"
content={props.description || defaultDescription}
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" sizes="192x192" href="/static/touch-icon.png" />
<link rel="apple-touch-icon" href="/static/touch-icon.png" />
<link rel="mask-icon" href="/static/favicon-mask.svg" color="#49B882" />
<link rel="icon" href="/static/favicon.ico" />
<meta property="og:url" content={props.url || defaultOGURL} />
<meta property="og:title" content={props.title || ''} />
<meta
property="og:description"
content={props.description || defaultDescription}
/>
<meta name="twitter:site" content={props.url || defaultOGURL} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={props.ogImage || defaultOGImage} />
<meta property="og:image" content={props.ogImage || defaultOGImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
</NextHead>
)
Head.propTypes = {
title: string,
description: string,
url: string,
ogImage: string
}
export default Head

59
UI/components/nav.js Normal file
View file

@ -0,0 +1,59 @@
import React from 'react'
import Link from 'next/link'
const links = [
{ href: 'https://github.com/segmentio/create-next-app', label: 'Github' }
].map(link => {
link.key = `nav-link-${link.href}-${link.label}`
return link
})
const Nav = () => (
<nav>
<ul>
<li>
<Link prefetch href="/">
<a>Home</a>
</Link>
</li>
<ul>
{links.map(({ key, href, label }) => (
<li key={key}>
<Link href={href}>
<a>{label}</a>
</Link>
</li>
))}
</ul>
</ul>
<style jsx>{`
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Avenir Next, Avenir,
Helvetica, sans-serif;
}
nav {
text-align: center;
}
ul {
display: flex;
justify-content: space-between;
}
nav > ul {
padding: 4px 16px;
}
li {
display: flex;
padding: 6px 8px;
}
a {
color: #067df7;
text-decoration: none;
font-size: 13px;
}
`}</style>
</nav>
)
export default Nav

View file

@ -1,4 +0,0 @@
const { override, addDecoratorsLegacy } = require('customize-cra')
module.exports = override(
addDecoratorsLegacy()
)

View file

@ -1,62 +0,0 @@
{
"name": "crav2",
"version": "0.1.0",
"private": true,
"dependencies": {
"color": "^3.1.0",
"history": "^4.7.2",
"immutable": "^3.8.2",
"moment": "^2.22.2",
"prop-types": "^15.6.2",
"react": "^16.6.3",
"react-custom-scrollbars": "^4.2.1",
"react-dnd": "^7.0.0",
"react-dnd-html5-backend": "^7.0.0",
"react-dom": "^16.6.3",
"react-immutable-proptypes": "^2.1.0",
"react-redux": "^5.1.1",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-router-redux": "^5.0.0-alpha.8",
"react-scripts": "2.1.1",
"react-typist": "^2.0.4",
"react-typist-cycle": "^0.1.2",
"redux": "^4.0.1",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"superagent": "^4.0.0",
"uuid": "^3.3.2"
},
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"proxy": "http://localhost:6769",
"devDependencies": {
"@babel/plugin-proposal-decorators": "^7.1.2",
"customize-cra": "^0.2.1",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "^7.11.1",
"eslint-plugin-standard": "^4.0.0",
"node-sass-chokidar": "^1.3.4",
"react-app-rewire-scss": "^1.0.2",
"react-app-rewired": "^1.6.2",
"redux-devtools": "^3.4.1",
"redux-devtools-dock-monitor": "^1.1.3",
"redux-devtools-log-monitor": "^1.4.0"
}
}

91
UI/pages/index.js Normal file
View file

@ -0,0 +1,91 @@
import React from 'react'
import Link from 'next/link'
import Head from '../components/head'
import Nav from '../components/nav'
const Home = () => (
<div>
<Head title="Home" />
<Nav />
<div className="hero">
<h1 className="title">Welcome to Next!</h1>
<p className="description">
To get started, edit <code>pages/index.js</code> and save to reload.
</p>
<div className="row">
<Link href="https://github.com/zeit/next.js#getting-started">
<a className="card">
<h3>Getting Started &rarr;</h3>
<p>Learn more about Next on Github and in their examples</p>
</a>
</Link>
<Link href="https://open.segment.com/create-next-app">
<a className="card">
<h3>Examples &rarr;</h3>
<p>
Find other example boilerplates on the{' '}
<code>create-next-app</code> site
</p>
</a>
</Link>
<Link href="https://github.com/segmentio/create-next-app">
<a className="card">
<h3>Create Next App &rarr;</h3>
<p>Was this tool helpful? Let us know how we can improve it</p>
</a>
</Link>
</div>
</div>
<style jsx>{`
.hero {
width: 100%;
color: #333;
}
.title {
margin: 0;
width: 100%;
padding-top: 80px;
line-height: 1.15;
font-size: 48px;
}
.title,
.description {
text-align: center;
}
.row {
max-width: 880px;
margin: 80px auto 40px;
display: flex;
flex-direction: row;
justify-content: space-around;
}
.card {
padding: 18px 18px 24px;
width: 220px;
text-align: left;
text-decoration: none;
color: #434343;
border: 1px solid #9b9b9b;
}
.card:hover {
border-color: #067df7;
}
.card h3 {
margin: 0;
color: #067df7;
font-size: 18px;
}
.card p {
margin: 0;
padding: 12px 0 0;
font-size: 13px;
color: #333;
}
`}</style>
</div>
)
export default Home

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View file

@ -1,28 +0,0 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" sizes="any" href="%PUBLIC_URL%/favicon.png"/>
<title>Roleypoly</title>
<!-- <script src="https://use.typekit.net/bck0pci.js"></script>
<script>try{Typekit.load({ async: true });}catch(e){}</script> -->
<link rel="stylesheet" defer href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-rc.24/css/uikit.min.css" />
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-rc.24/js/uikit.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-rc.24/js/uikit-icons.min.js"></script>
<style>body{ background-color: #453F3E; }</style>
</head>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root" class=""></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->

View file

@ -1,15 +0,0 @@
{
"short_name": "roleypoly",
"name": "roleypoly",
"icons": [
{
"src": "favicon.png",
"sizes": "128x128 64x64 32x32 24x24 16x16",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#ab9b9a",
"background_color": "#453e3d"
}

View file

@ -1,28 +0,0 @@
.App {
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 80px;
}
.App-header {
background-color: #222;
height: 150px;
padding: 20px;
color: white;
}
.App-title {
font-size: 1.5em;
}
.App-intro {
font-size: large;
}
@keyframes App-logo-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}

View file

@ -1,39 +0,0 @@
import React, { Component } from 'react'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'react-router-redux'
import { DragDropContext } from 'react-dnd'
import HTML5Backend from 'react-dnd-html5-backend'
import createHistory from 'history/createBrowserHistory'
import configureStore from './store/configureStore'
import './App.css'
import './generic.sass'
import Wrapper from './components/wrapper'
import AppRouter from './router'
import { userInit } from './actions'
const history = createHistory()
const store = configureStore(undefined, history)
window.__APP_STORE__ = store
@DragDropContext(HTML5Backend)
class App extends Component {
componentWillMount () {
store.dispatch(userInit)
}
render () {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<Wrapper>
<AppRouter />
</Wrapper>
</ConnectedRouter>
</Provider>
)
}
}
export default App

View file

@ -1,8 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
});

View file

@ -1,87 +0,0 @@
import superagent from 'superagent'
import { push } from 'react-router-redux'
export const fetchServers = async dispatch => {
const rsp = await superagent.get('/api/servers')
dispatch({
type: Symbol.for('update servers'),
data: rsp.body
})
dispatch({
type: Symbol.for('app ready')
})
}
export const userInit = async dispatch => {
if (!window.location.pathname.startsWith('/oauth')) {
try {
const rsp = await superagent.get('/api/auth/user')
dispatch({
type: Symbol.for('set user'),
data: rsp.body
})
dispatch(fetchServers)
} catch (e) {
dispatch({
type: Symbol.for('app ready')
})
// window.location.href = '/oauth/flow'
}
} else {
dispatch({
type: Symbol.for('app ready')
})
}
}
export const userLogout = async dispatch => {
try {
await superagent.post('/api/auth/logout')
} catch (e) {
}
dispatch({
type: Symbol.for('reset user')
})
window.location.href = '/'
}
export const startServerPolling = dispatch => {
return poll(window.__APP_STORE__.dispatch, window.__APP_STORE__.getState) // let's not cheat... :c
}
const poll = (dispatch, getState) => {
const { servers } = getState()
let stop = false
const stopPolling = () => { stop = true }
const pollFunc = async () => {
if (stop) {
return
}
try {
await fetchServers(dispatch)
} catch (e) {
console.error(e)
setTimeout(pollFunc, 5000)
}
const newServers = getState().servers
if (servers.size >= newServers.size) {
setTimeout(pollFunc, 5000)
} else {
const old = servers.keySeq().toSet()
const upd = newServers.keySeq().toSet()
const newSrv = upd.subtract(old)
stopPolling()
dispatch(push(`/s/${newSrv.toJS()[0]}/edit`))
}
}
pollFunc()
return stopPolling
}

View file

@ -1,13 +0,0 @@
export const fadeOut = cb => dispatch => {
dispatch({
type: Symbol.for('app fade'),
data: true
})
setTimeout(cb, 300)
}
export const fadeIn = {
type: Symbol.for('app fade'),
data: false
}

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="350px" height="350px" viewBox="0 0 350 350" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 44.1 (41455) - http://www.bohemiancoding.com/sketch -->
<title>Slice</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M95.5351562,425.050781 C180.587008,425.050781 249.535156,356.102633 249.535156,271.050781 C249.535156,185.99893 172.535156,117.050781 95.5351562,117.050781 L95.5351562,425.050781 Z M95.5351563,387.050781 C159.600187,387.050781 211.535156,334.668097 211.535156,270.050781 C211.535156,205.433466 153.535156,153.050781 95.5351563,153.050781 C95.5351562,203.905895 95.5351563,337.260095 95.5351563,387.050781 Z" id="path-1"></path>
</defs>
<g id="Logomark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(35.000000, -46.000000)">
<text id="R" font-family="HelveticaNeue-Medium, Helvetica Neue" font-size="288" font-weight="400" fill="#000000">
<tspan x="0" y="281">R</tspan>
</text>
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Oval" fill="#000000" transform="translate(172.535156, 271.050781) rotate(45.000000) translate(-172.535156, -271.050781) " xlink:href="#path-1"></use>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,41 +0,0 @@
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import TypingDemo from '../demos/typing'
import RoleypolyDemo from '../demos/roleypoly'
import * as Actions from '../../actions'
import './styles.sass'
import discordLogo from '../../pages/images/discord-logo.svg'
export default class AddServer extends Component {
polling = null
componentDidMount () {
if (this.props.match.params.server !== undefined) {
this.pollingStop = Actions.startServerPolling(this.props.dispatch)
}
}
componentWillUnmount () {
if (this.pollingStop != null) {
this.pollingStop()
}
}
render () {
return <div className="inner add-server">
<h2>What is Roleypoly?</h2>
<p className="add-server__header">
Roleypoly is a helper bot to help server members assign themselves roles on Discord.
</p>
<div className="add-server__grid">
<div><TypingDemo /></div>
<div className="text">Could you easily remember 250 role names? You'd use images or bot commands to tell everyone what they can assign. This kinda limits how <i>many</i> roles you can reasonably have. And don't even start with emojis. <span alt="" role="img">💢</span></div>
<div className="text right">Just click. <span role="img">🌈 💖</span></div>
<div className="add-server__darkbg"><RoleypolyDemo /></div>
</div>
<div className="add-server__header">
<a href="/oauth/bot/flow" target="_blank" className="uk-button rp-button discord"><img src={discordLogo} className="rp-button-logo" alt=""/> Authorize via Discord</a>
</div>
</div>
}
}

View file

@ -1,29 +0,0 @@
.add-server
text-align: center
.paper
background-color: hsla(0,0%,100%,0.05)
padding: 30px
.text
font-size: 0.9rem
text-align: left
&.right
text-align: right
font-size: 1.1em
&__header
margin: 45px 0
&__grid
display: grid
grid-template-columns: 1fr 1fr
grid-template-rows: 1fr 1fr
grid-gap: 10px
>div
align-self: center
&__darkbg
background-color: var(--c-1)
padding: 20px

View file

@ -1,12 +0,0 @@
import React from 'react'
import RoleDemo from '../role/demo'
const RoleypolyDemo = () => <div className="demo__roleypoly">
<RoleDemo name="a cute role ♡" color="#3EC1BF" />
<RoleDemo name="a vanity role ♡" color="#F7335B" />
<RoleDemo name="a brave role ♡" color="#A8D0B8" />
<RoleDemo name="a proud role ♡" color="#5C8B88" />
<RoleDemo name="a wonderful role ♡" color="#D6B3C7" />
</div>
export default RoleypolyDemo

View file

@ -1,29 +0,0 @@
import React from 'react'
import moment from 'moment'
import Typist from 'react-typist'
import './typing.sass'
const Typing = () => <div className="demo__discord rp-discord">
<div className="discord__chat">
<span className="timestamp">{moment().format('LT')}</span>
<span className="username">Kata カタ</span>
<span className="text">Hey, I want some roles!</span>
</div>
<div className="discord__textarea">
<Typist cursor={{ blink: true }}>
<span>.iam a cute role </span>
<Typist.Backspace count={30} delay={1500} />
<span>.iam a vanity role </span>
<Typist.Backspace count={30} delay={1500} />
<span>.iam a brave role </span>
<Typist.Backspace count={30} delay={1500} />
<span>.iam a proud role </span>
<Typist.Backspace count={30} delay={1500} />
<span>.iam a wonderful role </span>
<Typist.Backspace count={30} delay={1500} />
<span>i have too many roles.</span>
</Typist>
</div>
</div>
export default Typing

View file

@ -1,48 +0,0 @@
.demo__discord
--not-quite-black: #23272A
--dark-but-not-black: #2C2F33
--greyple: #99AAB5
--blurple: var(--c-discord)
background-color: var(--dark-but-not-black)
padding: 10px
text-align: left
color: var(--c-white)
.Typist .Cursor
display: inline-block
color: transparent
border-left: 1px solid var(--c-white)
user-select: none
&--blinking
opacity: 1
animation: blink 2s ease-in-out infinite
@keyframes blink
0%
opacity: 1
50%
opacity: 0
100%
opacity: 1
.discord
&__chat
padding: 10px 0
span
display: inline-block
margin-left: 5px
.timestamp
font-size: 0.7em
color: hsla(0,0%,100%,.2)
.username
font-weight: bold
&__textarea
background-color: hsla(218,5%,47%,.3)
border-radius: 5px
padding: 10px

View file

@ -1,11 +0,0 @@
import React from 'react'
import { createDevTools } from 'redux-devtools'
import LogMonitor from 'redux-devtools-log-monitor'
import DockMonitor from 'redux-devtools-dock-monitor'
export default createDevTools(
<DockMonitor toggleVisibilityKey="`"
changePositionKey="ctrl-w">
<LogMonitor />
</DockMonitor>
)

File diff suppressed because one or more lines are too long

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1566px" height="298px" viewBox="0 0 1566 298" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 44.1 (41455) - http://www.bohemiancoding.com/sketch -->
<title>Untitled</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M86.5351562,318.050781 C171.587008,318.050781 240.535156,249.102633 240.535156,164.050781 C240.535156,78.9989298 163.535156,10.0507812 86.5351562,10.0507812 L86.5351562,318.050781 Z M86.5351563,280.050781 C150.600187,280.050781 202.535156,227.668097 202.535156,163.050781 C202.535156,98.4334655 144.535156,46.0507812 86.5351563,46.0507812 C86.5351562,96.9058949 86.5351563,230.260095 86.5351563,280.050781 Z" id="path-1"></path>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<text id="Roleypoly" font-family="HelveticaNeue-Medium, Helvetica Neue" font-size="288" font-weight="400" fill="#AB9C9A">
<tspan x="249" y="238">Roleypoly</tspan>
</text>
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Oval" fill="#AB9C9A" transform="translate(163.535156, 164.050781) rotate(45.000000) translate(-163.535156, -164.050781) " xlink:href="#path-1"></use>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,27 +0,0 @@
import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import superagent from 'superagent'
import { connect } from 'react-redux'
import { push } from 'react-router-redux'
import { fetchServers } from '../../actions'
@connect()
class OauthCallback extends Component {
state = {
notReady: true,
message: 'chotto matte kudasai...',
url: null
}
async componentDidMount () {
const { body: { url } } = await superagent.get('/api/oauth/bot?url=✔️')
this.setState({ url, notReady: false })
window.location.href = url
}
render () {
return (this.state.notReady) ? this.state.message : <a style={{zIndex: 10000}} href={this.state.url}>Something oopsed, click me to get to where you meant.</a>
}
}
export default OauthCallback

View file

@ -1,83 +0,0 @@
import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import superagent from 'superagent'
import { connect } from 'react-redux'
import { fetchServers } from '../../actions'
@connect()
class OauthCallback extends Component {
state = {
notReady: true,
message: 'chotto matte kudasai...',
redirect: '/s'
}
stopped = false
componentDidUnmount () {
this.stopped = true
}
async componentDidMount () {
// handle stuff in the url
const sp = new URLSearchParams(this.props.location.search)
const token = sp.get('code')
if (token === '' || token == null) {
this.setState({ message: 'token missing, what are you trying to do?!' })
return
}
const stateToken = sp.get('state')
const state = JSON.parse(window.sessionStorage.getItem('state') || 'null')
if (state !== null && state.state === stateToken && state.redirect != null) {
this.setState({ redirect: state.redirect })
}
this.props.history.replace(this.props.location.pathname)
let counter = 0
const retry = async () => {
if (this.stopped) return
try {
const rsp = await superagent.get('/api/auth/user')
this.props.dispatch({
type: Symbol.for('set user'),
data: rsp.body
})
this.props.dispatch(fetchServers)
this.setState({ notReady: false })
} catch (e) {
counter++
if (counter > 10) {
this.setState({ message: "i couldn't log you in. :c" })
} else {
setTimeout(() => { retry() }, 250)
}
}
}
// pass token to backend, await it to finish it's business.
try {
await superagent.post('/api/auth/token').send({ token })
// this.props.onLogin(rsp.body)
retry()
} catch (e) {
console.error('token pass error', e)
this.setState({ message: 'g-gomen nasai... i broke it...' })
return
}
// update user stuff here
}
render () {
return (this.state.notReady) ? this.state.message : <Redirect to={this.state.redirect} />
}
}
export default OauthCallback

View file

@ -1,73 +0,0 @@
import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import superagent from 'superagent'
import { connect } from 'react-redux'
import uuidv4 from 'uuid/v4'
import { fetchServers } from '../../actions'
@connect()
class OauthCallback extends Component {
state = {
notReady: true,
message: 'chotto matte kudasai...',
redirect: '/s',
url: null
}
async fetchUser () {
const rsp = await superagent.get('/api/auth/user')
sessionStorage.setItem('user', JSON.stringify(rsp.body))
sessionStorage.setItem('user.update', JSON.stringify(Date.now()))
this.props.dispatch({
type: Symbol.for('set user'),
data: rsp.body
})
}
setupUser () {
const userUpdateTime = sessionStorage.getItem('user.update') || 0
if (+userUpdateTime + (1000 * 60 * 10) > Date.now()) {
const user = sessionStorage.getItem('user')
if (user != null && user !== '') {
this.props.dispatch({
type: Symbol.for('set user'),
data: JSON.parse(user)
})
}
}
return this.fetchUser()
}
async componentDidMount () {
const state = uuidv4()
const oUrl = new URL(window.location.href)
if (oUrl.searchParams.has('r')) {
this.setState({ redirect: oUrl.searchParams.get('r') })
}
window.sessionStorage.setItem('state', JSON.stringify({ state, redirect: oUrl.searchParams.get('r') }))
try {
await this.setupUser()
this.props.dispatch(fetchServers)
this.setState({ notReady: false })
} catch (e) {
const { body: { url } } = await superagent.get('/api/auth/redirect?url=✔️')
const nUrl = new URL(url)
nUrl.searchParams.set('state', state)
this.setState({ url: nUrl.toString() })
window.location.href = nUrl.toString()
}
}
render () {
return (this.state.notReady) ? this.state.message : <><Redirect to={this.state.redirect} /><a style={{zIndex: 10000}} href={this.state.url}>Something oopsed, click me to get to where you meant.</a></>
}
}
export default OauthCallback

View file

@ -1,44 +0,0 @@
import React, { Component } from 'react'
import { DropTarget } from 'react-dnd'
import Role from '../role/draggable'
import CategoryEditor from './CategoryEditor'
@DropTarget(Symbol.for('dnd: role'), {
drop (props, monitor, element) {
props.onDrop(monitor.getItem())
},
canDrop (props, monitor) {
return (props.mode !== Symbol.for('edit') && monitor.getItem().category !== props.name)
}
}, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
isOverCurrent: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
itemType: monitor.getItemType()
}))
class Category extends Component {
render () {
const { category, name, isOver, canDrop, connectDropTarget, mode, onEditOpen, ...rest } = this.props
if (mode === Symbol.for('edit')) {
return <CategoryEditor category={category} name={name} {...rest} />
}
return connectDropTarget(<div key={name} className={`role-editor__category drop-zone ${(canDrop) ? 'can-drop' : ''} ${(isOver && canDrop) ? 'is-over' : ''}`}>
<div className='role-editor__category-header'>
<h4>{ category.get('name') }</h4>
<div uk-tooltip='' title='Edit' uk-icon="icon: file-edit" onClick={onEditOpen} />
</div>
{
category.get('roles_map')
.sortBy(r => r.get('position'))
.reverse()
.map((r, k) => <Role key={k} role={r} categoryId={name} />)
.toArray()
}
</div>)
}
}
export default Category

View file

@ -1,64 +0,0 @@
import React, { Component } from 'react'
export default class CategoryEditor extends Component {
onKeyPress = (e) => {
const { onSave } = this.props
switch (e.key) {
case 'Enter':
case 'Escape':
return onSave()
}
}
render () {
const {
category
} = this.props
return <div className="role-editor__category editor" onKeyDown={this.onKeyPress}>
<div className="uk-form-stacked uk-light">
<div>
<label className="uk-form-label">Category Name</label>
<div className="uk-form-controls">
<input type="text" className="uk-input" placeholder='' value={category.get('name')} onChange={this.props.onEdit('name', Symbol.for('edit: text'))} />
</div>
</div>
<div style={{ marginTop: 10 }}>
<div className="uk-form-controls">
<label uk-tooltip="delay: 1s" title="Hides and disables roles in this category from being used.">
<input
style={{ marginRight: 5 }}
type="checkbox"
className="uk-checkbox uk-light"
checked={category.get('hidden')}
onChange={this.props.onEdit('hidden', Symbol.for('edit: bool'))}
/>
Hidden
</label>
</div>
</div>
<div style={{ marginTop: 10 }}>
<div className="uk-form-label">Type <i uk-icon="icon: info; ratio: 0.7" uk-tooltip="" title="Single mode only lets a user pick one role in this category." /></div>
<div className="uk-form-controls">
<select
className="uk-select"
value={category.get('type')}
onChange={this.props.onEdit('type', Symbol.for('edit: select'))}
>
<option value='multi'>Multiple</option>
<option value='single'>Single</option>
</select>
</div>
</div>
<div className='role-editor__actions'>
<button className="uk-button rp-button secondary role-editor__actions_delete" onClick={this.props.onDelete}>
<i uk-icon="icon: trash" />
</button>
<button className="uk-button rp-button primary role-editor__actions_save" onClick={this.props.onSave}>Save</button>
</div>
</div>
</div>
}
}

View file

@ -1,106 +0,0 @@
.role-editor
&__grid
display: grid
grid-template-areas: 'left right'
grid-template-columns: 1fr 1fr
grid-template-rows: 1fr
&__actions
display: flex
margin: 10px 0
button
padding: 0
&_delete
flex: 1
margin-right: 5px
&_save
flex: 4
&__uncat-zone
padding: 10px
&__alert
background-color: var(--c-5)
padding: 15px
a
color: var(--c-9)
text-decoration: none
font-style: italic
transition: color 0.15s ease-in-out
&:hover
color: var(--c-white)
.role-editor__category
box-sizing: border-box
background-color: var(--c-1)
padding: 15px
margin: 10px
min-width: 220px - 30px
position: relative
&.add-button
height: 100px
display: flex
align-items: center
justify-content: center
text-align: center
color: var(--c-5)
font-size: 2em
i
transition: transform 0.15s ease-in-out, color 0.15s ease-in-out
&:hover
i
transform: scale(1.1)
color: var(--c-7)
&-header
display: flex
align-items: center
justify-content: left
margin-bottom: 10px
color: var(--c-7)
svg
transition: transform 0.05s ease-in-out
transform: translateZ(0)
&:hover
transform: translateZ(0) scale(1.1)
color: var(--c-9)
h4
margin: 0
flex: 1
margin-right: 10px
.drop-zone
position: relative
&::after
content: ""
background-color: var(--c-7)
box-sizing: border-box
position: absolute
top: 0
right: 0
left: 0
bottom: 0
border: 5px dashed var(--c-3)
transition: opacity 0.15s ease-in-out
opacity: 0
pointer-events: none
&.is-over::after
opacity: 0.5
&.can-drop::after
opacity: 0.2

View file

@ -1,146 +0,0 @@
import { Set } from 'immutable'
import * as UIActions from '../../actions/ui'
import { getViewMap, setup } from '../role-picker/actions'
import uuidv4 from 'uuid/v4'
import superagent from 'superagent'
export const constructView = id => async (dispatch, getState) => {
await setup(id)(dispatch)
const server = getState().servers.get(id)
let { viewMap, hasSafeRoles } = getViewMap(server)
viewMap = viewMap.map(c => c.set('mode', Symbol.for('drop')))
dispatch({
type: Symbol.for('re: setup'),
data: {
hasSafeRoles,
viewMap,
originalSnapshot: viewMap
}
})
dispatch(UIActions.fadeIn)
}
export const addRoleToCategory = (id, oldId, role, flip = true) => (dispatch) => {
dispatch({
type: Symbol.for('re: add role to category'),
data: {
id,
role
}
})
if (flip) {
dispatch(removeRoleFromCategory(oldId, id, role, false))
}
}
export const removeRoleFromCategory = (id, oldId, role, flip = true) => (dispatch) => {
dispatch({
type: Symbol.for('re: remove role from category'),
data: {
id,
role
}
})
if (flip) {
dispatch(addRoleToCategory(oldId, id, role, false))
}
}
export const editCategory = ({ id, key, value }) => dispatch => {
dispatch({
type: Symbol.for('re: edit category'),
data: {
id,
key,
value
}
})
}
export const saveCategory = (id, category) => (dispatch) => {
if (category.get('name') === '') {
return
}
dispatch({
type: Symbol.for('re: switch category mode'),
data: {
id,
mode: Symbol.for('drop')
}
})
}
export const openEditor = (id) => ({
type: Symbol.for('re: switch category mode'),
data: {
id,
mode: Symbol.for('edit')
}
})
export const deleteCategory = (id, category) => (dispatch, getState) => {
const roles = category.get('roles')
const rolesMap = category.get('roles_map')
let uncategorized = getState().roleEditor.getIn(['viewMap', 'Uncategorized'])
dispatch({
type: Symbol.for('re: set category'),
data: {
id: 'Uncategorized',
name: '',
roles: uncategorized.get('roles').union(roles),
roles_map: uncategorized.get('roles_map').union(rolesMap),
hidden: true,
type: 'multi',
mode: null
}
})
dispatch({
type: Symbol.for('re: delete category'),
data: id
})
}
export const createCategory = (dispatch, getState) => {
const { roleEditor } = getState()
const vm = roleEditor.get('viewMap')
let name = 'New Category'
let idx = 1
while (vm.find(c => c.get('name') === name) !== undefined) {
idx++
name = `New Category ${idx}`
}
const id = uuidv4()
dispatch({
type: Symbol.for('re: set category'),
data: {
id,
name,
roles: Set([]),
roles_map: Set([]),
hidden: true,
type: 'multi',
mode: Symbol.for('edit')
}
})
}
export const saveServer = id => async (dispatch, getState) => {
const viewMap = getState().roleEditor.get('viewMap')
.filterNot((_, k) => k === 'Uncategorized')
.map(v => v.delete('roles_map').delete('mode').delete('id'))
await superagent.patch(`/api/server/${id}`).send({ categories: viewMap.toJS() })
dispatch({ type: Symbol.for('re: swap original state') })
}

View file

@ -1,192 +0,0 @@
import React, { Component } from 'react'
import { Set } from 'immutable'
import { connect } from 'react-redux'
import { DropTarget } from 'react-dnd'
import { Link, Prompt, Redirect } from 'react-router-dom'
import { Scrollbars } from 'react-custom-scrollbars'
import * as Actions from './actions'
import * as PickerActions from '../role-picker/actions'
import * as UIActions from '../../actions/ui'
import './RoleEditor.sass'
import Category from './Category'
import CategoryEditor from './CategoryEditor'
import Role from '../role/draggable'
const mapState = ({ rolePicker, roleEditor, servers }, ownProps) => ({
rp: rolePicker,
editor: roleEditor,
server: servers.get(ownProps.match.params.server)
})
@connect(mapState)
@DropTarget(Symbol.for('dnd: role'), {
drop (props, monitor, element) {
element.dropRole({}, 'Uncategorized')(monitor.getItem())
},
canDrop (props, monitor) {
return (monitor.getItem().category !== 'Uncategorized')
}
}, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
isOverCurrent: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
itemType: monitor.getItemType()
}))
class RoleEditor extends Component {
componentWillMount () {
const { dispatch, match: { params: { server } } } = this.props
dispatch(Actions.constructView(server))
}
componentWillReceiveProps (nextProps) {
if (this.props.match.params.server !== nextProps.match.params.server) {
const { dispatch } = this.props
dispatch(UIActions.fadeOut(() => dispatch(Actions.constructView(nextProps.match.params.server))))
}
}
dropRole = (category, name) => ({role, category}) => {
const { dispatch } = this.props
console.log(role)
dispatch(Actions.addRoleToCategory(name, category, role))
}
createCategory = () => {
const { dispatch } = this.props
dispatch(Actions.createCategory)
}
saveCategory = (category, name) => () => {
const { dispatch } = this.props
dispatch(Actions.saveCategory(name, category))
}
deleteCategory = (category, id) => () => {
const { dispatch } = this.props
dispatch(Actions.deleteCategory(id, category))
}
openEditor = (category, name) => () => {
const { dispatch } = this.props
dispatch(Actions.openEditor(name))
}
editCategory = (category, id) => (key, type) => event => {
const { dispatch } = this.props
let value
switch (type) {
case Symbol.for('edit: text'):
value = event.target.value
break
case Symbol.for('edit: bool'):
value = event.target.checked
break
case Symbol.for('edit: select'):
value = event.target.value
break
default:
value = null
}
dispatch(Actions.editCategory({ category, id, key, type, value }))
}
resetServer = () => {
const { dispatch } = this.props
dispatch({ type: Symbol.for('re: reset') })
}
saveServer = () => {
const { dispatch, match: { params: { server } } } = this.props
dispatch(Actions.saveServer(server))
}
get hasChanged () {
return this.props.editor.get('originalSnapshot').hashCode() !== this.props.editor.get('viewMap').hashCode()
}
render () {
const { server } = this.props
if (server == null) {
return null
}
if (server.getIn(['perms', 'canManageRoles']) !== true) {
return <Redirect to={`/s/${server.get('id')}`} />
}
const vm = this.props.editor.get('viewMap')
return <div className="inner role-editor">
<Prompt when={this.hasChanged} message="Are you sure you want to leave? You have unsaved changes that will be lost." />
<div className="role-picker__header" style={{ marginBottom: 10 }}>
<h3>{this.props.server.getIn(['server','name'])}</h3>
<div className="role-picker__spacer"></div>
<div className={`role-picker__actions ${(!this.hasChanged) ? 'hidden' : ''}`}>
<button onClick={this.resetServer} disabled={!this.hasChanged} className="uk-button rp-button secondary">
Reset
</button>
<button onClick={this.saveServer} disabled={!this.hasChanged} className="uk-button rp-button primary">
Save Changes
</button>
</div>
</div>
<div className="role-editor__grid">
<div className="role-editor__grid__left">
<Scrollbars autoHeight autoHeightMax='calc(100vh - 110px)'>
{
vm
.filter((_, k) => k !== 'Uncategorized')
.map((c, name) => <Category
key={name}
name={name}
category={c}
mode={c.get('mode')}
onDrop={this.dropRole(c, name)}
onEdit={this.editCategory(c, name)}
onEditOpen={this.openEditor(c, name)}
onSave={this.saveCategory(c, name)}
onDelete={this.deleteCategory(c, name)}
/>)
.toArray()
}
<div onClick={this.createCategory} uk-tooltip="pos: bottom" title="Add new category" className="role-editor__category add-button">
<i uk-icon="icon: plus" />
</div>
</Scrollbars>
</div>
{
this.props.connectDropTarget(
<div className={`role-editor__grid__right drop-zone ${(this.props.canDrop) ? 'can-drop' : ''} ${(this.props.isOver && this.props.canDrop) ? 'is-over' : ''}`}>
<Scrollbars autoHeight autoHeightMax='calc(100vh - 145px)'>
<div className="role-editor__uncat-zone">
{
(vm.getIn(['Uncategorized', 'roles_map']) || Set())
.sortBy(r => r.get('position'))
.reverse()
.map((r, k) => <Role key={k} categoryId='Uncategorized' role={r} />)
.toArray()
}
{
(this.props.editor.get('hasSafeRoles') !== true)
? <div className="role-editor__alert">
<Link to="/help/why-no-roles">Why are there no roles here? <i uk-icon="icon: info" /></Link>
</div>
: null
}
</div>
</Scrollbars>
</div>)
}
</div>
</div>
}
}
export default RoleEditor

View file

@ -1,53 +0,0 @@
import React, { Component } from 'react'
import { Map } from 'immutable'
import Role from '../role'
class Category extends Component {
toggleRoleMulti (id, next) {
this.props.onChange(Map({ [id]: next }))
}
toggleRoleSingle (id, next) {
this.props.onChange(this.props.category.get('roles').reduce((acc, i) => acc.set(i, false), Map()).set(id, next))
}
onRoleToggle = id => (next, old) => {
const type = this.props.category.get('type')
switch (type) {
case 'single': return this.toggleRoleSingle(id, next)
case 'multi': return this.toggleRoleMulti(id, next)
default:
console.warn('DEFAULTING TO MULTI', id, next, old)
return this.toggleRoleMulti(id, next)
}
}
render () {
const { category, name, isSelected } = this.props
if (category.get('hidden')) {
return null
}
if (category.get('roles').count() === 0) {
return null
}
return <div key={name} className="role-picker__category">
<h4>{ category.get('name') }</h4>
{
category.get('roles_map')
.sortBy(r => r.get('position'))
.reverse()
.map((r, k) => {
const id = r.get('id')
return <Role key={k} role={r} disabled={!r.get('safe')} selected={isSelected(id)} onToggle={this.onRoleToggle(id)}/>
})
.toArray()
}
</div>
}
}
export default Category

View file

@ -1,63 +0,0 @@
.role-picker
transition: opacity 0.3s ease-in-out
opacity: 1
&.hidden
opacity: 0
&__categories
display: flex
align-items: flex-start
flex-wrap: wrap
flex-direction: row
&__category
// flex: 1 3 33%
box-sizing: border-box
background-color: var(--c-1)
padding: 15px
margin: 10px
min-width: 220px - 30px
&__header
display: flex
align-items: center
justify-content: left
color: var(--c-7)
svg
transition: transform 0.05s ease-in-out
transform: translateZ(0)
&:hover
transform: translateZ(0) scale(1.1)
color: var(--c-9)
h3
margin: 0
margin-right: 10px
&__spacer
flex: 1
&__actions
opacity: 1
transition: opacity 0.3s ease-in-out
button
margin-left: 5px
&.hidden
opacity: 0
// display: none
&__msg-editor
background-color: rgba(0,0,0,0.2)
border-color: rgba(0,0,0,0.1)
color: var(--c-white)
margin: 10px 0
&:focus, &:active
color: var(--c-white)
background-color: rgba(0,0,0,0.2)
border-color: var(--c-7)

View file

@ -1,146 +0,0 @@
import { Map, Set, fromJS } from 'immutable'
import superagent from 'superagent'
import * as UIActions from '../../actions/ui'
export const setup = id => async dispatch => {
const rsp = await superagent.get(`/api/server/${id}`)
const data = rsp.body
dispatch({
type: Symbol.for('server: set'),
data: {
id,
...data
}
})
dispatch(constructView(id))
}
export const getViewMap = server => {
const roles = server.get('roles')
const categories = server.get('categories')
const allRoles = server.get('roles').filter(v => v.get('safe')).map(r => r.get('id')).toSet()
const accountedRoles = categories.map(c => c.get('roles')).toSet().flatten()
const unaccountedRoles = allRoles.subtract(accountedRoles)
// console.log('roles', allRoles.toJS(), accountedRoles.toJS(), unaccountedRoles.toJS())
const viewMap = categories.set('Uncategorized', fromJS({
roles: unaccountedRoles,
hidden: true,
type: 'multi',
name: 'Uncategorized'
})).map(c => {
const roles = c.get('roles')
// fill in roles_map
.map(r =>
server.get('roles').find(sr => sr.get('id') === r)
)
.filter(r => r != null)
// sort by server position, backwards.
.sort((a, b) => a.position > b.position)
// force data to sets
return c.set('roles_map', Set(roles)).set('roles', Set(c.get('roles')))
})
const selected = roles.reduce((acc, r) => acc.set(r.get('id'), r.get('selected')), Map())
const hasSafeRoles = allRoles.size > 0
return {
viewMap,
selected,
hasSafeRoles
}
}
export const constructView = id => (dispatch, getState) => {
const server = getState().servers.get(id)
const { viewMap, selected } = getViewMap(server)
dispatch({
type: Symbol.for('rp: setup role picker'),
data: {
viewMap: viewMap,
rolesSelected: selected,
originalRolesSelected: selected,
hidden: false,
isEditingMessage: false,
messageBuffer: ''
}
})
dispatch(UIActions.fadeIn)
}
export const resetSelected = (dispatch) => {
dispatch({
type: Symbol.for('rp: reset selected')
})
}
export const submitSelected = serverId => async (dispatch, getState) => {
const { rolePicker } = getState()
const original = rolePicker.get('originalRolesSelected')
const current = rolePicker.get('rolesSelected')
const diff = original.reduce((acc, v, k) => {
if (current.get(k) !== v) {
// if original value is false, then we know we're adding, otherwise removing.
if (v !== true) {
return acc.set('added', acc.get('added').add(k))
} else {
return acc.set('removed', acc.get('removed').add(k))
}
}
return acc
}, Map({ added: Set(), removed: Set() }))
await superagent.patch(`/api/servers/${serverId}/roles`).send(diff.toJS())
dispatch({
type: Symbol.for('rp: sync selected roles')
})
}
export const updateRoles = roles => ({
type: Symbol.for('rp: update selected roles'),
data: roles
})
export const openMessageEditor = id => (dispatch, getState) => {
const message = getState().servers.getIn([id, 'message'])
dispatch(editServerMessage(id, message))
dispatch({
type: Symbol.for('rp: set message editor state'),
data: true
})
}
export const saveServerMessage = id => async (dispatch, getState) => {
const message = getState().rolePicker.get('messageBuffer')
await superagent.patch(`/api/server/${id}`).send({ message })
dispatch(closeMessageEditor)
dispatch({
type: Symbol.for('server: edit message'),
data: {
id,
message
}
})
}
export const editServerMessage = (id, message) => ({
type: Symbol.for('rp: edit message buffer'),
data: message
})
export const closeMessageEditor = ({
type: Symbol.for('rp: set message editor state'),
data: false
})

View file

@ -1,145 +0,0 @@
import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux'
import { Prompt } from 'react-router-dom'
import superagent from 'superagent'
import * as Actions from './actions'
import * as UIActions from '../../actions/ui'
import { msgToReal } from '../../utils'
import './RolePicker.sass'
import Category from './Category'
import { Scrollbars } from 'react-custom-scrollbars';
import { Link } from 'react-router-dom';
const mapState = ({ rolePicker, servers }, ownProps) => {
return {
data: rolePicker,
server: servers.get(ownProps.match.params.server)
}
}
@connect(mapState)
class RolePicker extends Component {
componentWillMount () {
const { dispatch, match: { params: { server } } } = this.props
dispatch(Actions.setup(server))
}
componentWillReceiveProps (nextProps) {
if (this.props.match.params.server !== nextProps.match.params.server) {
const { dispatch } = this.props
dispatch(UIActions.fadeOut(() => dispatch(Actions.setup(nextProps.match.params.server))))
}
}
get serverId () {
return this.props.server.get('id')
}
isSelected = id => {
return this.props.data.getIn([ 'rolesSelected', id ])
}
get rolesHaveChanged () {
const { data } = this.props
return !data.get('rolesSelected').equals(data.get('originalRolesSelected'))
}
editServerMessage = (e) => {
const { dispatch } = this.props
dispatch(Actions.editServerMessage(this.serverId, e.target.value))
}
saveServerMessage = (e) => {
const { dispatch } = this.props
dispatch(Actions.saveServerMessage(this.serverId))
}
openMessageEditor = () => {
const { dispatch } = this.props
dispatch(Actions.openMessageEditor(this.serverId))
}
closeMessageEditor = () => {
const { dispatch } = this.props
dispatch(Actions.closeMessageEditor)
}
renderServerMessage (server) {
const isEditing = this.props.data.get('isEditingMessage')
const roleManager = server.getIn(['perms', 'canManageRoles'])
const msg = server.get('message')
const msgBuffer = this.props.data.get('messageBuffer')
console.log(msg, roleManager, isEditing, this.props.data.toJS())
if (!roleManager && msg !== '') {
return <section>
<h3>Server Message</h3>
<p>{msgToReal(msg)}</p>
</section>
}
if (roleManager && !isEditing) {
return <section>
<div className="role-picker__header">
<h3>Server Message</h3>
<div uk-tooltip='' title='Edit Server Message' uk-icon="icon: pencil" onClick={this.openMessageEditor} />
</div>
<p dangerouslySetInnerHTML={{__html: msgToReal(msg) || '<i>no server message</i>'}}></p>
</section>
}
if (roleManager && isEditing) {
return <section>
<div className="role-picker__header">
<h3>Server Message</h3>
<div uk-tooltip='' title='Save Server Message' onClick={this.saveServerMessage} style={{cursor: 'pointer', color: 'var(--c-green)'}} uk-icon="icon: check; ratio: 1.4" />
<div uk-tooltip='' title='Discard Edits' onClick={this.closeMessageEditor} style={{cursor: 'pointer', color: 'var(--c-red)', marginLeft: 10}} uk-icon="icon: trash; ratio: 0.9" />
</div>
<textarea className="uk-width-1-2 uk-textarea role-picker__msg-editor" rows="3" onChange={this.editServerMessage} value={msgBuffer} />
</section>
}
return null
}
render () {
const { data, server, dispatch } = this.props
const vm = data.get('viewMap')
if (server === undefined) {
return null
}
return <div className={`inner role-picker ${(data.get('hidden')) ? 'hidden' : ''}`}>
<Prompt when={this.rolesHaveChanged} message="Are you sure you want to leave? You have unsaved changes that will be lost." />
{ this.renderServerMessage(server) }
<section>
<div className="role-picker__header">
<h3>Roles</h3>
{ server.getIn(['perms', 'canManageRoles']) === true
? <Link to={`/s/${server.get('id')}/edit`} uk-tooltip='' title='Edit Categories' uk-icon="icon: file-edit"></Link>
: null
}
<div className="role-picker__spacer"></div>
<div className={`role-picker__actions ${(!this.rolesHaveChanged) ? 'hidden' : ''}`}>
<button disabled={!this.rolesHaveChanged} onClick={() => dispatch(Actions.resetSelected)} className="uk-button rp-button secondary">
Reset
</button>
<button disabled={!this.rolesHaveChanged} onClick={() => dispatch(Actions.submitSelected(this.props.match.params.server))} className="uk-button rp-button primary">
Save Changes
</button>
</div>
</div>
<div className="role-picker__categories">
{
vm.map((c, name) => <Category key={name} name={name} category={c} isSelected={this.isSelected} onChange={(roles) => dispatch(Actions.updateRoles(roles))} />).toArray()
}
</div>
</section>
</div>
}
}
export default RolePicker

View file

@ -1,104 +0,0 @@
.role
border: 1px solid var(--role-color-rgba)
border-radius: 32px
box-sizing: border-box
height: 32px
padding: 4px
display: inline-flex
font-weight: 600
align-content: center
justify-content: center
font-size: 16px
vertical-align: baseline
transition: background-color 0.15s ease-in-out, transform 0.15s ease-in-out, box-shadow 0.15s ease-in-out
transform: translateZ(0)
margin: 0 6px 6px 0
position: relative
&.role__button
.role__option
&:hover
cursor: inherit
&:hover:not(.disabled)
cursor: pointer
.role__option
transform: translateY(-1px) translateZ(0px)
box-shadow: 0 1px 1px var(--c-dark)
border-color: var(--role-color-hover)
background-color: transparent
&:active
box-shadow: none
&:active .role__option
box-shadow: none
&.disabled
.role__option
border-color: var(--c-7)
transition: none
transform: translateZ(0)
&:hover
cursor: default
&__name
margin-right: 6px
max-width: 200px
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
user-select: none
&__option
border-radius: 50%
height: 22px
margin-right: 6px
width: 22px
clip-path: border-box circle(50% at 50% 50%) // fix for firefox and other odd things.
box-sizing: border-box
/* display: inline-block */
background-color: transparent
overflow: hidden
transform: translateZ(0)
border: 1px solid var(--role-color-hex)
transition: background-color 0.1s ease-in-out, border-left-width 0.3s ease-in-out, border-color 0.1s ease-in-out, transform 0.1s ease-in-out, box-shadow 0.1s ease-out
&.selected
// This **must** be width-1, otherwise blink adds width to the boundaries.
border-left-width: 21px
&.role__drag
box-shadow: none
&:hover, &.is-dragging
transform: translateZ(0) translateY(-1px)
box-shadow: 0 1px 2px rgba(0,0,0,0.3)
background-color: rgba(#ccc,0.03)
cursor: grab
&:active
cursor: grabbing
&::after
position: absolute
top: -1px
bottom: -1px
left: -1px
right: -1px
border-radius: 32px
background-color: var(--c-3)
border: 2px dashed var(--c-7)
content: ""
opacity: 0
transition: opacity 0.15s ease-in-out
box-sizing: border-box
&.is-dragging
transition: border-color 0s ease-out 0.15s
border-color: transparent
&::after
opacity: 1
.role__option:active, .role:active .role__option:not(:active)
transform: translateY(0) translateZ(0px) !important

View file

@ -1,19 +0,0 @@
import React, { Component } from 'react'
import { Map } from 'immutable'
import Role from './index'
export default class DemoRole extends Component {
state = {
isSelected: false
}
handleToggle = () => {
this.setState({ isSelected: !this.state.isSelected })
}
render () {
return <Role selected={this.state.isSelected} role={Map({ name: this.props.name, color: this.props.color })} onToggle={this.handleToggle} type='button' />
}
}

View file

@ -1,32 +0,0 @@
import React, { Component } from 'react'
import { DragSource } from 'react-dnd'
import Role from './index'
// @DragSource(Symbol.for('dnd: role'), {
// beginDrag ({ role, categoryId }) {
// return { role, category: categoryId }
// }
// },
// (connect, monitor) => ({
// connectDragSource: connect.dragSource(),
// isDragging: monitor.isDragging()
// }))
export default
@DragSource(
Symbol.for('dnd: role'),
{
beginDrag ({ role, categoryId }) {
return { role, category: categoryId }
}
},
(connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
})
)
class DraggableRole extends Component {
render () {
return <Role {...this.props} type='drag' />
}
}

View file

@ -1,60 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Color from 'color'
import './Role.sass'
const whiteColor = Color('#efefef')
class Role extends Component {
static propTypes = {
role: PropTypes.object.isRequired,
onToggle: PropTypes.func,
type: PropTypes.string,
selected: PropTypes.bool,
disabled: PropTypes.bool
}
render () {
let { role, selected, disabled, type, isDragging } = this.props
type = type || 'button'
// console.log(this.props)
let color = Color(role.get('color'))
if (color.rgbNumber() === 0) {
color = whiteColor
}
const c = color
let hc = color.lighten(0.1)
const out = <div
onClick={() => {
if (!disabled && this.props.onToggle != null) {
this.props.onToggle(!selected, selected) }
}
}
{...((disabled) ? { 'uk-tooltip': '', title: "I don't have permissions to grant this." } : {})}
className={`role font-sans-serif ${(disabled) ? 'disabled' : ''} ${(isDragging) ? 'is-dragging' : ''} role__${type}`}
title={role.get('name')}
style={{
'--role-color-hex': c.string(),
'--role-color-hover': hc.string(),
'--role-color-rgba': `rgba(${c.red()}, ${c.green()}, ${c.blue()}, 0.7)`
}}>
<div className={`role__option ${(selected) ? 'selected' : ''}`}/>
<div className='role__name'>
{role.get('name')}
</div>
</div>
if (type === 'drag' && this.props.connectDragSource != null) {
return this.props.connectDragSource(out)
}
return out
}
}
export default Role

View file

@ -1,40 +0,0 @@
import React, { Component, Fragment } from 'react'
import ImmutablePropTypes from 'react-immutable-proptypes'
import PropTypes from 'prop-types'
import ServerCard from './ServerCard'
import UserCard from './UserCard'
import { Scrollbars } from 'react-custom-scrollbars'
import { NavLink } from 'react-router-dom'
class ServersNavigation extends Component {
static propTypes = {
user: ImmutablePropTypes.map.isRequired,
servers: ImmutablePropTypes.orderedMapOf(ImmutablePropTypes.map).isRequired,
className: PropTypes.string
}
render () {
// console.log(this.props.servers)
return <Fragment>
<UserCard user={this.props.user} />
<div className={this.props.className}>
<Scrollbars autoHeight autoHeightMax='calc(100vh - 180px)'>
{
this.props.servers.reduce((acc, s, i) => {
acc.push(<ServerCard server={s} user={this.props.user} key={i} />)
return acc
}, [])
}
<NavLink className='server-list__item add-new' activeClassName='active' to={`/s/add`}>
<div className='server-list__item__info'>
<i uk-icon="icon: plus; ratio: 0.9"></i>&nbsp;
Add to your server
</div>
</NavLink>
</Scrollbars>
</div>
</Fragment>
}
}
export default ServersNavigation

View file

@ -1,45 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { NavLink } from 'react-router-dom'
import './ServerCard.sass'
import { withRouter } from 'react-router';
class ServerCard extends Component {
static propTypes = {
user: ImmutablePropTypes.map.isRequired,
server: ImmutablePropTypes.map.isRequired,
}
render () {
const { server, user } = this.props
let icon = ''
console.log(__filename, server)
const s = server.get('server')
const gm = server.get('gm')
const perms = server.get('perms')
if (perms.get('canManageRoles')) {
icon = <span title='Role Manager' uk-tooltip='' role='img' aria-label='Role Manager' className="server-list__item__tag" uk-icon="icon: bolt; ratio: 0.7" />
}
if (perms.get('isAdmin')) {
icon = <span title='Server Admin' uk-tooltip='' role='img' aria-label='Server Admin' className="server-list__item__tag" uk-icon="icon: star; ratio: 0.7" />
}
return <NavLink className='server-list__item' activeClassName='active' to={`/s/${s.get('id')}`}>
<div className='server-list__item__icon'>
<img src={`https://cdn.discordapp.com/icons/${s.get('id')}/${s.get('icon')}.png`} alt={s.name} />
</div>
<div className='server-list__item__info'>
<b>{s.get('name')}</b><br />
<span style={{ color: gm.get('color') }}>{ gm.get('nickname') || user.get('username') }</span> { icon }
</div>
</NavLink>
}
}
export default ServerCard

View file

@ -1,50 +0,0 @@
.server-list
&__item
display: flex
/* border-bottom: 1px solid var(--c-3) */
padding: 25px 15px
padding-right: 0
align-items: center
/* justify-content: center */
&.add-new
color: var(--c-7)
&.active
background-color: var(--c-3)
&:not(.active)
&:hover
background-color: rgba(0,0,0,0.25) !important
cursor: pointer
transform: translateX(1px)
transition: background 0.05s ease-in-out, transform 0.1s ease-in-out
&:nth-of-type(even)
background-color: rgba(0,0,0,0.1)
&:last-of-type
border: 0
&__icon img
border-radius: 100%
max-width: 50px
max-height: 50px
border: 2px solid transparent
&__info
padding: 0 10px
&__tag
color: var(--c-9)
position: relative
top: -2px
// position: inline-block
a.server-list__item
color: var(--c-white)
text-decoration: none
.active
cursor: default

View file

@ -1,47 +0,0 @@
import React, { Component } from 'react'
import { Link, Redirect } from 'react-router-dom'
import superagent from 'superagent'
import discordLogo from '../../pages/images/discord-logo.svg'
export default class ServerLanding extends Component {
state = {
server: null,
exit: false
}
async componentWillMount () {
console.log(this.props)
try {
const rsp = await superagent.get(`/api/server/${this.props.match.params.server}/slug`)
this.setState({ server: rsp.body })
} catch (e) {
this.setState({ exit: true })
return
}
}
render () {
if (this.state.exit === true) {
return <Redirect to="/" />
}
if (this.state.server === null) {
return null //SPINNER
}
return <div className="landing uk-width-1-1 uk-text-center">
<div className="uk-container">
<section>
<h1>Hey there.</h1>
<h4>{this.state.server.name} uses Roleypoly to manage self-assignable roles.</h4>
<h5><span role="img">💖</span></h5>
</section>
<section>
<Link to={`/oauth/flow?r=${window.location.pathname}`} className="uk-button rp-button discord"><img src={discordLogo} className="rp-button-logo"/> Sign in with Discord</Link>
</section>
</div>
</div>
}
}

View file

@ -1,51 +0,0 @@
import React, { Component } from 'react'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { NavLink } from 'react-router-dom'
import { connect } from 'react-redux'
import * as Actions from '../../actions'
import './UserCard.sass'
@connect()
class UserCard extends Component {
static propTypes = {
user: ImmutablePropTypes.map
}
get avatar () {
const { user } = this.props
const avatar = user.get('avatar')
if (avatar === '' || avatar == null) {
return `https://cdn.discordapp.com/embed/avatars/${Math.ceil(Math.random() * 9999) % 5}.png`
}
return `https://cdn.discordapp.com/avatars/${user.get('id')}/${avatar}.png`
}
render () {
const { user } = this.props
// console.log(this.props)
return <div className='user-card'>
<div className='user-card__icon'>
<img src={this.avatar} alt={user.get('username')} />
</div>
<div className='user-card__info'>
<span className='user-card__info__name'>{user.get('username')}</span><span className='user-card__info__discrim'>#{user.get('discriminator')}</span>
</div>
<div className='user-card__actions'>
<ul className='uk-iconnav uk-iconnav-vertical'>
<li><a uk-tooltip='' title='Sign out' uk-icon='icon: sign-out' onClick={() => { this.props.dispatch(Actions.userLogout) }} /></li>
{
(this.props.user.isRoot === true)
? <li><NavLink uk-tooltip='' title='Root' uk-icon='icon: bolt' to='/root/' activeClassName='uk-active' /></li>
: null
}
</ul>
</div>
</div>
}
}
export default UserCard

View file

@ -1,44 +0,0 @@
.user-card
position: relative
display: flex
background-color: var(--c-dark)
/* border-bottom: 1px solid var(--c-3) */
padding: 25px 15px
grid-area: user
&__icon img
width: 50px
height: 50px
border-radius: 100%
border: 2px solid #5fc66d
&__info
padding: 0 10px
line-height: 50px
&__discrim
font-weight: 100
font-size: 0.7rem
color: var(--c-7)
&__name
/* font-size */
/* font-weight: */
&__actions
position: absolute
right: 5px
top: 5px
bottom: 5px
display: flex
align-items: center
justify-content: center
svg
transition: transform 0.05s ease-in-out
svg:hover
transform: scale(1.15)
polygon
fill: var(--c-7)

View file

@ -1,55 +0,0 @@
import React, { Component } from 'react'
import { Route, Switch } from 'react-router-dom'
import { Scrollbars } from 'react-custom-scrollbars'
import { connect } from 'react-redux'
import { withRouter, Redirect } from 'react-router'
import './index.sass'
import Navigation from './Navigation'
import RolePicker from '../role-picker'
import RoleEditor from '../role-editor'
import AddServer from '../add-server'
import Error404 from '../../pages/Error404'
// import mockData from './mockData'
const mapState = ({ servers, user, appState }) => {
return {
servers,
user,
fade: appState.fade
}
}
@connect(mapState)
class Servers extends Component {
get defaultPath () {
console.log(this.props.servers.toJS())
const first = this.props.servers.first()
if (first != null) {
return first.get('id')
}
return 'add'
}
render () {
return <div className="servers">
<Navigation className="servers__nav" servers={this.props.servers} user={this.props.user} />
<div className='servers__content'>
<Scrollbars className={`fade-element ${(this.props.fade) ? 'fade' : ''}`} autoHeight autoHeightMax='calc(100vh - 80px)'>
<Switch>
<Route path='/s/add' component={AddServer} exact />
<Route path='/s/:server/edit' component={RoleEditor} />
<Route path='/s/:server' component={RolePicker} />
<Route path='/s' exact render={() => <Redirect to={`/s/${this.defaultPath}`} />} />
<Route component={Error404} />
</Switch>
</Scrollbars>
</div>
</div>
}
}
export default Servers

View file

@ -1,26 +0,0 @@
.servers
$fullH: calc(100vh - 180px)
display: grid
grid-template-rows: 100px $fullH
grid-template-columns: 300px 1fr
grid-template-areas: "user content" "listing content"
&__nav
grid-area: listing
overflow: hidden
// height: $fullH
&__content
background-color: var(--c-3)
// padding: 15px
grid-area: content
position: relative
// height: $fullH
overflow: hidden
box-sizing: border-box
.inner
padding: 15px

View file

@ -1,40 +0,0 @@
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import Scrollbars from 'react-custom-scrollbars'
import Logotype from '../logotype'
import './wrapper.sass'
import discordLogo from '../../pages/images/discord-logo.svg'
class Wrapper extends Component {
render () {
return <div className='wrapper'>
<Scrollbars autoHeight autoHeightMax='calc(100vh + 2px)'>
<div className='wrapper__background' />
<div className='wrapper__container'>
<nav uk-navbar='' className='uk-navbar-transparent wrapper__nav'>
<div className='uk-navbar-left'>
<Link to="/">
<Logotype style={{ height: '2rem' }} className='wrapper__logotype' />
</Link>
</div>
<div className='uk-navbar-right'>
<ul className='uk-navbar-nav'>
<li><div className='wrapper__nav__button'>
<a href="/oauth/bot/flow" target="_blank" className="uk-button rp-button discord-alt"><img src={discordLogo} className="rp-button-logo" alt=""/> Add Roleypoly</a>
</div></li>
<li><a href='https://discord.gg/PWQUVsd'>Support Discord</a></li>
</ul>
</div>
</nav>
<main className="wrapper__content">
{
this.props.children
}
</main>
</div>
</Scrollbars>
</div>
}
}
export default Wrapper

View file

@ -1,35 +0,0 @@
// .wrapper__logo:hover mask
// /* transform: translate 1s ease-in-out repeat; */
// transform: rotate(30deg)
.wrapper
&__background
background-color: var(--c-1)
position: fixed
top: 0
left: 0
right: 0
bottom: 0
z-index: -1000
&__container
width: 960px
margin: 0 auto
&__nav
padding: 0 20px
z-index: 1000
&__button
box-sizing: border-box
height: 82px
display: flex
align-items: center
justify-content: center
&>.rp-button
padding-left: 15px
padding-right: 15px

View file

@ -1,59 +0,0 @@
.rp-button
border: 0
border-radius: 2px
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out
position: relative
box-sizing: border-box
transform: translateZ(0)
&::after
content: ""
position: absolute
top: 0
bottom: 0
right: 0
left: 0
background-color: rgba(0,0,0,0.1)
border-radius: 2px
transform: translateZ(0)
opacity: 0
transition: opacity 0.15s ease-in-out
&:hover
transform: translateZ(0) translateY(-1px)
box-shadow: 0 1px 2px rgba(0,0,0,0.3)
&::after
opacity: 0.7
&:active
transform: translateZ(0) translateY(0px)
box-shadow: none
&::after
opacity: 1
&.primary
background-color: var(--c-5)
color: var(--c-9)
&.secondary
background-color: var(--c-3)
color: var(--c-7)
&.discord
background-color: var(--c-discord)
color: var(--c-white)
&.discord-alt
background-color: transparent
color: var(--c-white)
border: 1px solid var(--c-discord)
&::after
background-color: hsla(0,0%,100%,0.05)
transition: opacity 0.3s ease-in-out
&-logo
height: 1.6rem
.rp-discord
border: 1px solid var(--c-discord-dark)
box-shadow: 0 0 1px rgba(0,0,0,0.3)

View file

@ -1,65 +0,0 @@
body {
margin: 0;
padding: 0;
font-family: "source-han-sans-japanese", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.font-sans-serif {
font-family: sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
:root {
--c-white: #efefef;
--c-9: #EBD6D4;
--c-7: #ab9b9a;
--c-5: #756867;
--c-3: #5d5352;
--c-1: #453e3d;
--c-dark: #332d2d;
--c-green: #46b646;
--c-red: #e95353;
--c-discord: #7289da;
}
::selection {
background: var(--c-9);
color: var(--c-1);
}
::-moz-selection {
background: var(--c-9);
color: var(--c-1);
}
html {
overflow: hidden;
height: 100%;
}
body {
height: 100%;
overflow: auto;
color: var(--c-white);
background-color: var(--c-1);
overflow-y: hidden;
}
h1,h2,h3,h4,h5,h6 {
color: var(--c-9);
}
.uk-navbar-nav>li>a {
color: var(--c-7);
}
.fade-element {
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
.fade {
opacity: 0;
}

View file

@ -1,8 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import {unregister} from './registerServiceWorker'
ReactDOM.render(<App />, document.getElementById('root'))
unregister()

View file

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -1,14 +0,0 @@
import React from 'react'
import './landing.sass'
const Error404 = ({ root = false }) =>
<div className="landing uk-width-1-1 uk-text-center">
<div className="uk-container">
<section>
<h1><span role="img">💔</span> g-gomen nasai</h1>
<h4>I'm not sure what page you were looking for <span role="img">😢</span></h4>
</section>
</div>
</div>
export default Error404

View file

@ -1,19 +0,0 @@
import React, { Fragment } from 'react'
import goodImg from './images/whynoroles-good.png'
import badImg from './images/whynoroles-bad.png'
const WhyNoRoles = (props) => {
return <Fragment>
<h2>Why don't I see any roles in my editor?</h2>
<p>Roleypoly needs to be a higher role position than other roles in order to assign them to anyone.</p>
<h3 className="pages__bad">Bad <i uk-icon="icon: ban"></i></h3>
<img src={badImg} className="rp-discord" alt="Bad example"/>
<p>In this example, Roleypoly is at the bottom of the list. It can't assign anyone any roles above it.</p>
<h3 className="pages__good">Good <i uk-icon="icon: check"></i></h3>
<img src={goodImg} className="rp-discord" alt="Good example"/>
<p>In this example, Roleypoly is above other roles, and will be able to assign them.</p>
</Fragment>
}
export default WhyNoRoles

View file

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 245 240" style="enable-background:new 0 0 245 240;" xml:space="preserve">
<style type="text/css">
.st0{fill:#efefef;}
</style>
<g>
<path class="st0" d="M104.4,103.9c-5.7,0-10.2,5-10.2,11.1c0,6.1,4.6,11.1,10.2,11.1c5.7,0,10.2-5,10.2-11.1
C114.7,108.9,110.1,103.9,104.4,103.9z"/>
<path class="st0" d="M140.9,103.9c-5.7,0-10.2,5-10.2,11.1c0,6.1,4.6,11.1,10.2,11.1c5.7,0,10.2-5,10.2-11.1
C151.1,108.9,146.6,103.9,140.9,103.9z"/>
<path class="st0" d="M189.5,20H55.5C44.2,20,35,29.2,35,40.6v135.2c0,11.4,9.2,20.6,20.5,20.6h113.4l-5.3-18.5l12.8,11.9l12.1,11.2
L210,220v-44.2v-10V40.6C210,29.2,200.8,20,189.5,20z M150.9,150.6c0,0-3.6-4.3-6.6-8.1c13.1-3.7,18.1-11.9,18.1-11.9
c-4.1,2.7-8,4.6-11.5,5.9c-5,2.1-9.8,3.5-14.5,4.3c-9.6,1.8-18.4,1.3-25.9-0.1c-5.7-1.1-10.6-2.7-14.7-4.3c-2.3-0.9-4.8-2-7.3-3.4
c-0.3-0.2-0.6-0.3-0.9-0.5c-0.2-0.1-0.3-0.2-0.4-0.3c-1.8-1-2.8-1.7-2.8-1.7s4.8,8,17.5,11.8c-3,3.8-6.7,8.3-6.7,8.3
c-22.1-0.7-30.5-15.2-30.5-15.2c0-32.2,14.4-58.3,14.4-58.3c14.4-10.8,28.1-10.5,28.1-10.5l1,1.2c-18,5.2-26.3,13.1-26.3,13.1
s2.2-1.2,5.9-2.9c10.7-4.7,19.2-6,22.7-6.3c0.6-0.1,1.1-0.2,1.7-0.2c6.1-0.8,13-1,20.2-0.2c9.5,1.1,19.7,3.9,30.1,9.6
c0,0-7.9-7.5-24.9-12.7l1.4-1.6c0,0,13.7-0.3,28.1,10.5c0,0,14.4,26.1,14.4,58.3C181.5,135.4,173,149.9,150.9,150.6z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View file

@ -1,25 +0,0 @@
import React from 'react'
import { Route, Switch } from 'react-router-dom'
import Scrollbars from 'react-custom-scrollbars'
import './pages.sass'
import WhyNoRoles from './WhyNoRoles'
import Error404 from './Error404'
export { default as Landing } from './Landing'
export { default as Error404 } from './Error404'
const Pages = (props) => {
return <div className="pages">
<Scrollbars autoHeight autoHeightMax='calc(100vh - 80px)'>
<div className="pages-inner">
<Switch>
<Route path="/help/why-no-roles" component={WhyNoRoles} />
{/* { isDev ? <Route path="/p/landing" component={Landing} /> : null } */}
<Route component={Error404} />
</Switch>
</div>
</Scrollbars>
</div>
}
export default Pages

View file

@ -1,76 +0,0 @@
.landing
min-height: calc(100vh)
position: relative
top: -80px
display: flex
align-items: center
justify-content: center
.Typist .Cursor
display: inline-block
color: transparent
border-left: 1px solid var(--c-white)
&--blinking
opacity: 1
animation: blink 2s ease-in-out infinite
@keyframes blink
0%
opacity: 1
50%
opacity: 0
100%
opacity: 1
&__roleypoly
&__discord
--not-quite-black: #23272A
--dark-but-not-black: #2C2F33
--greyple: #99AAB5
--blurple: var(--c-discord)
background-color: var(--dark-but-not-black)
padding: 10px
text-align: left
color: var(--c-white)
.discord
&__chat
padding: 10px 0
span
display: inline-block
margin-left: 5px
.timestamp
font-size: 0.7em
color: hsla(0,0%,100%,.2)
.username
font-weight: bold
&__textarea
background-color: hsla(218,5%,47%,.3)
border-radius: 5px
padding: 10px
section
margin-top: 50px
h1
color: var(--c-7)
h4
color: var(--c-9)
h1,h2,h3,h4,h5,h6
margin: 0.3em
.subtext
color: hsla(0,0%,100%,0.8)
font-size: 0.9em
line-height: 1.6

View file

@ -1,14 +0,0 @@
.pages
background-color: var(--c-3)
&-inner
padding: 45px 35px
&__bad
color: var(--c-red)
&__good
color: var(--c-green)
img
max-width: 75%

View file

@ -1,45 +0,0 @@
import { combineReducers } from 'redux'
import servers from './servers'
import user from './user'
import rolePicker from './role-picker'
import roleEditor from './role-editor'
import { routerMiddleware } from 'react-router-redux'
// import roles from './roles'
const initialState = {
ready: false,
fade: true
}
const appState = (state = initialState, { type, data }) => {
switch (type) {
case Symbol.for('app ready'):
return {
...state,
ready: true,
fade: false
}
case Symbol.for('app fade'):
return {
...state,
fade: data
}
default:
return state
}
}
const rootReducer = combineReducers({
appState,
servers,
user,
router: routerMiddleware,
// roles,
rolePicker,
roleEditor
})
export default rootReducer

View file

@ -1,54 +0,0 @@
import { Map, OrderedMap, fromJS } from 'immutable'
const initialState = Map({
viewMap: OrderedMap({}),
originalSnapshot: OrderedMap({}),
hasAvailableRoles: true
})
const reducer = (state = initialState, { type, data }) => {
switch (type) {
case Symbol.for('re: setup'):
const { viewMap, originalSnapshot, ...rest } = data
return state.merge({ viewMap: OrderedMap(viewMap), originalSnapshot: OrderedMap(originalSnapshot), ...rest })
case Symbol.for('re: set category'):
return state.setIn(['viewMap', data.id], Map(data))
case Symbol.for('re: edit category'):
return state.setIn(['viewMap', data.id, data.key], data.value)
case Symbol.for('re: delete category'):
return state.deleteIn(['viewMap', data])
case Symbol.for('re: switch category mode'):
return state.setIn(['viewMap', data.id, 'mode'], data.mode)
case Symbol.for('re: add role to category'):
const category = state.getIn(['viewMap', data.id])
return state.setIn(['viewMap', data.id],
category
.set('roles', category.get('roles').add(data.role.get('id')))
.set('roles_map', category.get('roles_map').add(data.role))
)
case Symbol.for('re: remove role from category'):
const rmCat = state.getIn(['viewMap', data.id])
return state.setIn(['viewMap', data.id],
rmCat
.set('roles', rmCat.get('roles').filterNot(r => r === data.role.get('id')))
.set('roles_map', rmCat.get('roles_map').filterNot(r => r.get('id') === data.role.get('id')))
)
case Symbol.for('re: reset'):
return state.set('viewMap', state.get('originalSnapshot'))
case Symbol.for('re: swap original state'):
return state.set('originalSnapshot', state.get('viewMap'))
default:
return state
}
}
export default reducer

View file

@ -1,44 +0,0 @@
import { Map, OrderedMap } from 'immutable'
const initialState = Map({
hidden: true, // should the view be hidden?
// emptyRoles: true, // helps derender roles so there's no visible element state change
isEditingMessage: false,
messageBuffer: '',
viewMap: OrderedMap({}), // roles in categories
originalRolesSelected: Map({}), // Map<role id, bool> -- original roles for diffing against selected
rolesSelected: Map({}) // Map<role id, bool> -- new roles for diffing
})
export default (state = initialState, { type, data }) => {
switch (type) {
case Symbol.for('rp: setup role picker'):
return Map(data)
case Symbol.for('rp: hide role picker ui'):
return state.set('hidden', data)
case Symbol.for('rp: reset role picker ui'):
return state.set('emptyRoles', data)
case Symbol.for('rp: update selected roles'):
return state.mergeIn(['rolesSelected'], data)
case Symbol.for('rp: sync selected roles'):
return state.set('originalRolesSelected', state.get('rolesSelected'))
case Symbol.for('rp: reset selected'):
return state.set('rolesSelected', state.get('originalRolesSelected'))
case Symbol.for('rp: set message editor state'):
return state.set('isEditingMessage', data)
case Symbol.for('rp: edit message buffer'):
return state.set('messageBuffer', data)
// case Symbol.for('rp: zero role picker'):
// return initialState
default:
return state
}
}

View file

@ -1,48 +0,0 @@
import { Set, OrderedMap, Map, fromJS } from 'immutable'
const blankServer = Map({
id: '386659935687147521',
gm: {
nickname: null,
color: '#cca1a1'
},
message: 'Hey hey!',
server: {
id: '386659935687147521',
name: 'Roleypoly',
ownerID: '62601275618889728',
icon: '4fa0c1063649a739f3fe1a0589aa2c03'
},
roles: Set([]),
categories: OrderedMap(),
perms: {
isAdmin: true,
canManageRoles: true
}
})
const initialState = OrderedMap({})
export default (state = initialState, { type, data }) => {
switch (type) {
case Symbol.for('update servers'):
return data.reduce((acc, s) => acc.set(s.id, fromJS(s)), OrderedMap())
// case Symbol.for('update server roles'):
// return state.set(data.id,
// state.get(data.id).set('roles', Set(data.roles))
// )
case Symbol.for('server: set'):
return state.set(data.id, fromJS(data))
case Symbol.for('server: edit message'):
return state.setIn([data.id, 'message'], data.message)
case Symbol.for('add debug server'):
return state.set('0', blankServer)
default:
return state
}
}

View file

@ -1,22 +0,0 @@
import { Map } from 'immutable'
const initialState = Map({
isLoggedIn: false,
username: 'あたし',
discriminator: '0001',
id: '',
avatar: null
})
export default (state = initialState, { type, data }) => {
switch (type) {
case Symbol.for('set user'):
return Map({...data, isLoggedIn: true})
case Symbol.for('reset user'):
return initialState
default:
return state
}
}

View file

@ -1,108 +0,0 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

View file

@ -1,55 +0,0 @@
import React, { Component, Fragment } from 'react'
import { Route, Switch, Redirect } from 'react-router-dom'
import { connect } from 'react-redux'
import { withRouter } from 'react-router'
import Servers from '../components/servers'
import OauthCallback from '../components/oauth-callback'
import OauthFlow from '../components/oauth-flow'
import OauthBotFlow from '../components/oauth-bot-flow'
import Pages, { Landing, Error404 } from '../pages'
import ServerLanding from '../components/servers/ServerLanding'
const aaa = (props) => (<div>{ JSON.stringify(props) }</div>)
export default
@withRouter
@connect(({ appState, user }) => ({ ready: appState.ready, user }))
class AppRouter extends Component {
render () {
const isLoggedIn = this.props.user.get('isLoggedIn')
if (!this.props.ready) {
return null
}
return <Switch>
{ (isLoggedIn === true)
// YES LOGGED IN
? <Route path='/s' component={Servers} />
// NOT LOGGED IN
: [<Route path='/s/:server' key={1} component={ServerLanding} />, <Route path='/s' key={2} render={() => <Redirect to="/" />} />]
}
{/* GENERAL ROUTES */}
<Route path='/oauth/callback' component={OauthCallback} />
<Route path='/oauth/flow' component={OauthFlow} />
<Route path='/oauth/bot/flow' component={OauthBotFlow} />
<Route path="/p/landing" exact component={Landing} />
<Route path='/p' component={Pages} />
<Route path='/help' component={Pages} />
<Route exact path='/' render={() =>
isLoggedIn
? <Redirect to="/s" />
: <Landing root={true} />
} />
<Route component={Error404} />
</Switch>
}
}

View file

@ -1,30 +0,0 @@
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import { createLogger } from 'redux-logger'
// import api from '../middleware/api'
import rootReducer from '../reducers'
import DevTools from '../components/dev-tools'
import { routerMiddleware } from 'react-router-redux'
const configureStore = (preloadedState, history) => {
const store = createStore(
rootReducer,
preloadedState,
compose(
applyMiddleware(thunk, routerMiddleware(history), createLogger()),
// DevTools.instrument()
)
)
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
store.replaceReducer(rootReducer)
})
}
return store
}
export default configureStore

View file

@ -1,5 +0,0 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./configureStore.prod')
} else {
module.exports = require('./configureStore.dev')
}

View file

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

View file

@ -1 +0,0 @@
export const msgToReal = (msg) => (msg.replace(/</g, '&lt;').replace(/\n/g, '<br />'))

BIN
UI/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

10704
UI/yarn.lock

File diff suppressed because it is too large Load diff

17
api/ui.js Normal file
View file

@ -0,0 +1,17 @@
// note, this file only contains stuff for complicated routes.
// next.js will handle anything beyond this.
module.exports = (R, $) => {
const processMappings = mapping => {
for (let p in mapping) {
R.get(p, ctx => {
$.ui.render(ctx.req, ctx.res, mapping[p], {...ctx.params, ...ctx.query})
})
}
}
processMappings({
"/s/add": "/_server_add",
"/s/:id": "/_server",
"/help/:page": "/_help"
})
}

View file

@ -0,0 +1,28 @@
version: '2.1'
services:
app:
image: katie/roleypoly
ports:
- 6000:6769
links:
- pg
environment:
DB_URL: postgres://roleypoly:19216801@pg:5432/roleypoly
NODE_ENV: production
DISCORD_CLIENT_ID: '396227611649'
DISCORD_CLIENT_SECRET: RnX8pcH5FrDcs26su-
DISCORD_BOT_TOKEN: Mzk2MjI3MTM0Mjo7ROXbvyW09y0
APP_URL: http://localhost:6000
OAUTH_AUTH_CALLBACK: http://localhost:6000/oauth/callback
APP_KEY: 9vJrVFXHazgDo0RuF
pg:
image: postgres:10-alpine
ports:
- 5432
environment:
POSTGRES_PASSWORD: 19216801
POSTGRES_DB: roleypoly
POSTGRES_USER: roleypoly
POSTGRES_INITDB_ARGS: -A trust

View file

@ -28,6 +28,8 @@ Array.prototype.filterNot = Array.prototype.filterNot || function (predicate) {
const server = http.createServer(app.callback()) const server = http.createServer(app.callback())
const io = _io(server, { transports: ['websocket'], path: '/api/socket.io' }) const io = _io(server, { transports: ['websocket'], path: '/api/socket.io' })
const M = new Roleypoly(router, io, app) // eslint-disable-line no-unused-vars const M = new Roleypoly(router, io, app) // eslint-disable-line no-unused-vars
app.keys = [ process.env.APP_KEY ] app.keys = [ process.env.APP_KEY ]
@ -45,80 +47,6 @@ async function start () {
const compress = require('koa-compress') const compress = require('koa-compress')
app.use(compress()) app.use(compress())
// SPA + Static
if (process.env.NODE_ENV === 'production') {
const pub = path.join(__dirname, 'public')
log.info('public path', pub)
const staticFiles = require('koa-static')
// app.use(staticFiles(pub, { defer: true, gzip: true, br: true }))
const send = require('koa-send')
app.use(async (ctx, next) => {
if (ctx.path.startsWith('/api')) {
log.info('api breakout')
return next()
}
const chkPath = path.resolve(path.join(pub, ctx.path))
log.info('chkPath', chkPath)
if (!chkPath.startsWith(pub)) {
return next()
}
try {
fs.statSync(chkPath)
log.info('sync pass')
ctx.body = fs.readFileSync(chkPath, { encoding: 'utf-8' })
ctx.type = path.extname(ctx.path)
log.info('body sent')
ctx.status = 200
return next()
} catch (e) {
log.warn('failed')
if (ctx.path.startsWith('/static/')) {
return next()
}
try {
ctx.body = fs.readFileSync(path.join(pub, 'index.html'), { encoding: 'utf-8' })
} catch (e) {
ctx.body = e.stack || e.trace
ctx.status = 500
}
log.info('index sent')
ctx.status = 200
return next()
}
// try {
// await next()
// } catch (e) {
// send(ctx, 'index.html', { root: pub })
// }
})
// const sendOpts = {root: pub, index: 'index.html'}
// // const sendOpts = {}
// app.use(async (ctx, next) => {
// if (ctx.path.startsWith('/api')) {
// return await next()
// }
// try {
// log.info('pass 1', ctx.path, __dirname+'/public'+ctx.path)
// await send(ctx, __dirname+'/public'+ctx.path, sendOpts)
// } catch (error) {
// try {
// log.info('pass 2 /', ctx.path, __dirname+'/public/index.html')
// await send(ctx, __dirname+'/public/index.html', sendOpts)
// } catch (error) {
// log.info('exit to next', ctx.path)
// await next()
// }
// }
// })
}
// Request logger // Request logger
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
let timeStart = new Date() let timeStart = new Date()

10
next.config.js Normal file
View file

@ -0,0 +1,10 @@
module.exports = {
webpack: config => {
// Fixes npm packages that depend on `fs` module
config.node = {
fs: 'empty'
}
return config
}
}

36
package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "roleypoly",
"version": "2.0.0",
"main": "index.js",
"scripts": {
"start": "pm2 start index.js",
"dev": "pm2 start index.js --watch",
"build": "next build"
},
"dependencies": {
"@discordjs/uws": "^11.149.1",
"chalk": "^2.4.2",
"discord.js": "^11.4.2",
"dotenv": "^6.2.0",
"erlpack": "github:discordapp/erlpack",
"glob": "^7.1.3",
"immutable": "^3.8.2",
"koa": "^2.7.0",
"koa-better-router": "^2.1.1",
"koa-bodyparser": "^4.2.1",
"koa-compress": "^3.0.0",
"koa-session": "^5.10.1",
"ksuid": "^1.1.3",
"lru-cache": "^5.1.1",
"next": "^8.0.3",
"pg": "^7.8.1",
"pg-hstore": "^2.3.2",
"pm2": "^3.3.1",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"sequelize": "^4.42.0",
"socket.io": "^2.2.0",
"superagent": "^4.1.0",
"uuid": "^3.3.2"
}
}

Some files were not shown because too many files have changed in this diff Show more