mirror of
https://github.com/roleypoly/roleypoly-v1.git
synced 2025-06-16 10:19:10 +00:00
lerna: split up bulk of packages
This commit is contained in:
parent
cb0b1d2410
commit
47a2e5694e
90 changed files with 0 additions and 0 deletions
9
packages/roleypoly-ui/.babelrc
Normal file
9
packages/roleypoly-ui/.babelrc
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"presets": [
|
||||
"next/babel", "@babel/preset-flow"
|
||||
],
|
||||
"plugins": [
|
||||
[ "styled-components", { "ssr": true } ],
|
||||
"@babel/plugin-proposal-optional-chaining"
|
||||
]
|
||||
}
|
19
packages/roleypoly-ui/.gitignore
vendored
Normal file
19
packages/roleypoly-ui/.gitignore
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
/.next
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
|
@ -0,0 +1,11 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DiscordButton /> renders correctly 1`] = `
|
||||
<discord-button__Button>
|
||||
<discord-button__ButtonIcon
|
||||
src="/static/discord-logo.svg"
|
||||
/>
|
||||
|
||||
Hello!
|
||||
</discord-button__Button>
|
||||
`;
|
|
@ -0,0 +1,91 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DiscordGuildPic /> falls-back to a default when things go bad. 1`] = `
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
background-color: var(--fallback-color);
|
||||
}
|
||||
|
||||
<DiscordGuildPic
|
||||
icon="aaa"
|
||||
id="0000"
|
||||
name="Mock"
|
||||
>
|
||||
<discord-guild-pic__Fallback
|
||||
serverName="Mock"
|
||||
style={
|
||||
Object {
|
||||
"--fallback-color": "hsl(77,50%,50%)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledComponent
|
||||
forwardedComponent={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.forward_ref),
|
||||
"attrs": Array [],
|
||||
"componentStyle": ComponentStyle {
|
||||
"componentId": "discord-guild-pic__Fallback-d55lwu-0",
|
||||
"isStatic": true,
|
||||
"lastClassName": "c0",
|
||||
"rules": Array [
|
||||
"display:flex;justify-content:center;align-items:center;background-color:var(--fallback-color);",
|
||||
],
|
||||
},
|
||||
"displayName": "discord-guild-pic__Fallback",
|
||||
"foldedComponentIds": Array [],
|
||||
"render": [Function],
|
||||
"styledComponentId": "discord-guild-pic__Fallback-d55lwu-0",
|
||||
"target": "div",
|
||||
"toString": [Function],
|
||||
"warnTooManyClasses": [Function],
|
||||
"withComponent": [Function],
|
||||
}
|
||||
}
|
||||
forwardedRef={null}
|
||||
serverName="Mock"
|
||||
style={
|
||||
Object {
|
||||
"--fallback-color": "hsl(77,50%,50%)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="c0"
|
||||
style={
|
||||
Object {
|
||||
"--fallback-color": "hsl(77,50%,50%)",
|
||||
}
|
||||
}
|
||||
>
|
||||
M
|
||||
</div>
|
||||
</StyledComponent>
|
||||
</discord-guild-pic__Fallback>
|
||||
</DiscordGuildPic>
|
||||
`;
|
||||
|
||||
exports[`<DiscordGuildPic /> renders a snapshot 1`] = `
|
||||
<DiscordGuildPic
|
||||
icon="aaa"
|
||||
id="0000"
|
||||
name="Mock"
|
||||
>
|
||||
<img
|
||||
onError={[Function]}
|
||||
onLoad={[Function]}
|
||||
src="https://cdn.discordapp.com/icons/0000/aaa.png"
|
||||
/>
|
||||
</DiscordGuildPic>
|
||||
`;
|
|
@ -0,0 +1,14 @@
|
|||
/* eslint-env jest */
|
||||
import * as React from 'react'
|
||||
// import renderer from 'react-test-renderer'
|
||||
import { shallow } from 'enzyme'
|
||||
import DiscordButton from '../discord-button'
|
||||
import 'jest-styled-components'
|
||||
|
||||
describe('<DiscordButton />', () => {
|
||||
it('renders correctly', () => {
|
||||
const button = shallow(<DiscordButton>Hello!</DiscordButton>)
|
||||
expect(button).toMatchSnapshot()
|
||||
expect(button.text().trim()).toEqual('Hello!')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,33 @@
|
|||
/* eslint-env jest */
|
||||
import * as React from 'react'
|
||||
// import renderer from 'react-test-renderer'
|
||||
import { shallow, mount } from 'enzyme'
|
||||
import DiscordGuildPic from '../discord-guild-pic'
|
||||
import 'jest-styled-components'
|
||||
|
||||
describe('<DiscordGuildPic />', () => {
|
||||
const mockServer = {
|
||||
id: '0000',
|
||||
icon: 'aaa',
|
||||
name: 'Mock'
|
||||
}
|
||||
|
||||
it('renders a snapshot', () => {
|
||||
const pic = mount(<DiscordGuildPic {...mockServer} />)
|
||||
expect(pic).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a well-formatted guild pic correctly', () => {
|
||||
const pic = shallow(<DiscordGuildPic {...mockServer} />)
|
||||
const expectedSrc = `https://cdn.discordapp.com/icons/${mockServer.id}/${mockServer.icon}.png`
|
||||
expect(pic.find('img').prop('src')).toEqual(expectedSrc)
|
||||
})
|
||||
|
||||
it('falls-back to a default when things go bad.', () => {
|
||||
const pic = mount(<DiscordGuildPic {...mockServer} />)
|
||||
pic.find('img').simulate('error')
|
||||
expect(pic).toMatchSnapshot()
|
||||
|
||||
expect(pic.text()).toEqual(mockServer.name[0])
|
||||
})
|
||||
})
|
15
packages/roleypoly-ui/components/demos/tap.js
Normal file
15
packages/roleypoly-ui/components/demos/tap.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Role from '../role/demo'
|
||||
import demoRoles from '../../config/demo'
|
||||
|
||||
const DemoWrapper = styled.div`
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
|
||||
export default () => <DemoWrapper>
|
||||
{ demoRoles.map((v, i) => <Role key={i} role={v} />) }
|
||||
</DemoWrapper>
|
85
packages/roleypoly-ui/components/demos/typing.js
Normal file
85
packages/roleypoly-ui/components/demos/typing.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
import * as React from 'react'
|
||||
import moment from 'moment'
|
||||
import Typist from 'react-typist'
|
||||
import styled from 'styled-components'
|
||||
import MediaQuery from '../../kit/media'
|
||||
import demoRoles from '../../config/demo'
|
||||
|
||||
const Outer = styled.div`
|
||||
background-color: var(--dark-but-not-black);
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
color: var(--c-white);
|
||||
`
|
||||
|
||||
const Chat = styled.div`
|
||||
padding: 10px 0;
|
||||
font-size: 0.8em;
|
||||
${() => MediaQuery({ sm: 'font-size: 1em;' })}
|
||||
& span {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
}
|
||||
`
|
||||
|
||||
const TextArea = styled.div`
|
||||
background-color: hsla(218,5%,47%,.3);
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
font-size: 0.8em;
|
||||
${() => MediaQuery({ sm: 'font-size: 1em;' })}
|
||||
|
||||
& .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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Timestamp = styled.span`
|
||||
font-size: 0.7em;
|
||||
color: hsla(0,0%,100%,.2);
|
||||
`
|
||||
|
||||
const Username = styled.span`
|
||||
font-weight: bold;
|
||||
`
|
||||
|
||||
const Typing = () => <Outer>
|
||||
<Chat>
|
||||
<Timestamp className='timestamp'>{moment().format('LT')}</Timestamp>
|
||||
<Username className='username'>okano「岡野」</Username>
|
||||
<span className='text'>Hey, I want some roles!</span>
|
||||
</Chat>
|
||||
<TextArea>
|
||||
<Typist cursor={{ blink: true }}>
|
||||
{ demoRoles.map(({ name }) => [
|
||||
<span>.iam {name}</span>,
|
||||
<Typist.Backspace count={30} delay={1500} />
|
||||
]) }
|
||||
<span>i have too many roles.</span>
|
||||
</Typist>
|
||||
</TextArea>
|
||||
</Outer>
|
||||
|
||||
export default Typing
|
62
packages/roleypoly-ui/components/discord-button.js
Normal file
62
packages/roleypoly-ui/components/discord-button.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export type ButtonProps = {
|
||||
children: React.Node
|
||||
}
|
||||
|
||||
const Button = styled.a`
|
||||
background-color: var(--c-discord);
|
||||
color: var(--c-white);
|
||||
padding: 0.4em 1em;
|
||||
border-radius: 3px;
|
||||
border: 1px solid rgba(0,0,0,0.25);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translateY(0px);
|
||||
transition: all 0.3s ease-in-out;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
transition: all 0.35s ease-in-out;
|
||||
background-color: hsla(0,0%,100%,0.1);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.75);
|
||||
transform: translateY(-1px);
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0px);
|
||||
box-shadow: none;
|
||||
}
|
||||
`
|
||||
|
||||
const ButtonIcon = styled.img`
|
||||
height: 1.5em;
|
||||
`
|
||||
|
||||
const DiscordButton = ({ children, ...props }: ButtonProps) => (
|
||||
<Button {...props}>
|
||||
<ButtonIcon src='/static/discord-logo.svg' />
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
|
||||
export default DiscordButton
|
61
packages/roleypoly-ui/components/discord-guild-pic.js
Normal file
61
packages/roleypoly-ui/components/discord-guild-pic.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
// @flow
|
||||
// export default ({ id, icon, ...rest }) => <img src={`https://cdn.discordapp.com/icons/${id}/${icon}.png`} {...rest} />
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export type GuildPicProps = {
|
||||
id: string,
|
||||
icon: string,
|
||||
name: string
|
||||
}
|
||||
|
||||
export type GuildPicState = {
|
||||
src: ?string,
|
||||
ok: boolean
|
||||
}
|
||||
|
||||
const Fallback = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
background-color: var(--fallback-color);
|
||||
`
|
||||
|
||||
export default class DiscordGuildPic extends React.Component<GuildPicProps, GuildPicState> {
|
||||
state = {
|
||||
src: this.src,
|
||||
ok: false
|
||||
}
|
||||
|
||||
get src () {
|
||||
return `https://cdn.discordapp.com/icons/${this.props.id}/${this.props.icon}.png`
|
||||
}
|
||||
|
||||
renderFallback () {
|
||||
const { name, id, icon, ...rest } = this.props
|
||||
return <Fallback serverName={name} style={{ '--fallback-color': `hsl(${(name.codePointAt(0) % 360)},50%,50%)` }} {...rest}>{name[0]}</Fallback>
|
||||
}
|
||||
|
||||
onError = () => {
|
||||
// console.log('onError')
|
||||
this.setState({
|
||||
src: null
|
||||
})
|
||||
}
|
||||
|
||||
onLoad = () => {
|
||||
this.setState({
|
||||
ok: true
|
||||
})
|
||||
}
|
||||
|
||||
renderImg () {
|
||||
const { name, id, icon, ...rest } = this.props
|
||||
return <img src={this.state.src} onError={this.onError} onLoad={this.onLoad} {...rest} />
|
||||
}
|
||||
|
||||
render () {
|
||||
return (this.state.src === null) ? this.renderFallback() : this.renderImg()
|
||||
}
|
||||
}
|
91
packages/roleypoly-ui/components/global-colors.js
Normal file
91
packages/roleypoly-ui/components/global-colors.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
// @flow
|
||||
// import * as React from 'react'
|
||||
import { createGlobalStyle } from 'styled-components'
|
||||
|
||||
export const colors = {
|
||||
white: '#efefef',
|
||||
c9: '#EBD6D4',
|
||||
c7: '#ab9b9a',
|
||||
c5: '#756867',
|
||||
c3: '#5d5352',
|
||||
c1: '#453e3d',
|
||||
dark: '#332d2d',
|
||||
green: '#46b646',
|
||||
red: '#e95353',
|
||||
discord: '#7289da'
|
||||
}
|
||||
|
||||
const getColors = () => {
|
||||
return Object.keys(colors).map(key => {
|
||||
const nk = key.replace(/c([0-9])/, '$1')
|
||||
return `--c-${nk}: ${colors[key]};`
|
||||
}).join(' \n')
|
||||
}
|
||||
|
||||
export default createGlobalStyle`
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "source-han-sans-japanese", "Source Sans Pro", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/* prevent FOUC */
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.font-sans-serif {
|
||||
font-family: sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
}
|
||||
|
||||
:root {
|
||||
${() => getColors()}
|
||||
--not-quite-black: #23272A;
|
||||
--dark-but-not-black: #2C2F33;
|
||||
--greyple: #99AAB5;
|
||||
--blurple: var(--c-discord);
|
||||
}
|
||||
|
||||
::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 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
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);
|
||||
}
|
||||
|
||||
.fade-element {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.fade {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
`
|
100
packages/roleypoly-ui/components/header/auth.js
Normal file
100
packages/roleypoly-ui/components/header/auth.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import HeaderBarCommon, { Logomark } from './common'
|
||||
import { type User } from '../../stores/user'
|
||||
import DiscordIcon from '../discord-guild-pic'
|
||||
import styled from 'styled-components'
|
||||
import { Hide } from '../../kit/media'
|
||||
import Link from 'next/link'
|
||||
import { connect } from 'react-redux'
|
||||
import { getCurrentServerState } from '../../stores/currentServer'
|
||||
|
||||
const temporaryServer = {
|
||||
id: '423497622876061707',
|
||||
name: 'Placeholder',
|
||||
icon: '8d03476c186ec8b2f6a1a4f5e55b13fe'
|
||||
}
|
||||
|
||||
const LogoBox = styled.a`
|
||||
flex: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const StyledServerPic = styled(DiscordIcon)`
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 0 1px rgba(0,0,0,0.1);
|
||||
border: 1px solid rgba(0,0,0,0.25);
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const StyledAvatar = styled.img`
|
||||
border-radius: 100%;
|
||||
border: 1px solid var(--c-green);
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const ServerSelector = (props) => <div {...props}>
|
||||
<div>
|
||||
<StyledServerPic {...props} />
|
||||
</div>
|
||||
<div>
|
||||
{ props.name }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const StyledServerSelector = styled(ServerSelector)`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
`
|
||||
|
||||
const UserSection = ({ user, ...props }) => <div {...props}>
|
||||
<Hide.SM>{ user.username }</Hide.SM>
|
||||
<StyledAvatar src={user.avatar} />
|
||||
</div>
|
||||
|
||||
const StyledUserSection = styled(UserSection)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
`
|
||||
|
||||
const Spacer = styled.div`
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const HeaderBarAuth: React.StatelessFunctionalComponent<{ user: User, isOnServer: boolean, currentServer: * }> = ({ user, isOnServer, currentServer = temporaryServer }) => (
|
||||
<HeaderBarCommon noBackground={false}>
|
||||
<>
|
||||
<Link href='/'>
|
||||
<LogoBox>
|
||||
<Logomark />
|
||||
</LogoBox>
|
||||
</Link>
|
||||
{ isOnServer ? <StyledServerSelector name={currentServer.name} id={currentServer.id} icon={currentServer.icon} /> : <Spacer /> }
|
||||
<StyledUserSection user={user} />
|
||||
</>
|
||||
</HeaderBarCommon>
|
||||
)
|
||||
|
||||
const mapStateToProps = (state, { router }) => ({
|
||||
isOnServer: router.pathname === '/_internal/_server',
|
||||
currentServer: router.pathname === '/_internal/_server' ? getCurrentServerState(state, router.query.id).server : {}
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps)(HeaderBarAuth)
|
60
packages/roleypoly-ui/components/header/common.js
Normal file
60
packages/roleypoly-ui/components/header/common.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import styled from 'styled-components'
|
||||
import * as logo from '../logo'
|
||||
|
||||
export type CommonProps = {
|
||||
children: React.Element<any>,
|
||||
noBackground: boolean
|
||||
}
|
||||
|
||||
const Header = styled.div`
|
||||
background-color: ${({ noBackground }: any) => noBackground === false ? 'var(--c-dark);' : 'var(--c-1);'}
|
||||
position: relative;
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 5;
|
||||
`
|
||||
|
||||
const HeaderInner = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-width: 960px;
|
||||
width: 100vw;
|
||||
margin: 0 auto;
|
||||
height: 50px;
|
||||
padding: 3px 5px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
|
||||
& > div {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
`
|
||||
|
||||
export const Logotype = styled(logo.Logotype)`
|
||||
height: 30px;
|
||||
`
|
||||
|
||||
export const Logomark = styled(logo.Logomark)`
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
`
|
||||
//
|
||||
const DebugBreakpoints = dynamic(() => import('../../kit/debug-breakpoints'))
|
||||
|
||||
const HeaderBarCommon = ({ children, noBackground = false }: CommonProps) => (
|
||||
<Header noBackground={noBackground}>
|
||||
{ (process.env.NODE_ENV === 'development') && <DebugBreakpoints />}
|
||||
<HeaderInner>
|
||||
{ children }
|
||||
</HeaderInner>
|
||||
</Header>
|
||||
)
|
||||
|
||||
export default HeaderBarCommon
|
53
packages/roleypoly-ui/components/header/unauth.js
Normal file
53
packages/roleypoly-ui/components/header/unauth.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import HeaderBarCommon, { Logotype, type CommonProps } from './common'
|
||||
import styled from 'styled-components'
|
||||
import Link from 'next/link'
|
||||
|
||||
const LogoBox = styled.a`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const LoginButton = styled.a`
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
display: block;
|
||||
padding: 0.3em 1em;
|
||||
border-radius: 2px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.75);
|
||||
background-color: var(--c-green);
|
||||
border-color: rgba(0,0,0,0.25);
|
||||
text-shadow: 1px 1px 0px rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0px);
|
||||
box-shadow: none;
|
||||
}
|
||||
`
|
||||
|
||||
const HeaderBarUnauth: React.StatelessFunctionalComponent<CommonProps> = (props) => (
|
||||
<HeaderBarCommon {...props}>
|
||||
<>
|
||||
<Link href='/' prefetch>
|
||||
<LogoBox>
|
||||
<Logotype />
|
||||
</LogoBox>
|
||||
</Link>
|
||||
<Link href='/auth/login' prefetch>
|
||||
<LoginButton>Sign in →</LoginButton>
|
||||
</Link>
|
||||
</>
|
||||
</HeaderBarCommon>
|
||||
)
|
||||
|
||||
export default HeaderBarUnauth
|
37
packages/roleypoly-ui/components/layout.js
Normal file
37
packages/roleypoly-ui/components/layout.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import GlobalColors from './global-colors'
|
||||
import SocialCards from './social-cards'
|
||||
import HeaderBar from '../containers/header-bar'
|
||||
import { type User } from '../stores/user'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const LayoutWrapper = styled.div`
|
||||
transition: opacity: 0.1s ease-out;
|
||||
opacity: 0;
|
||||
.wf-active &, .force-active & {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const ContentBox = styled.div`
|
||||
margin: 0 auto;
|
||||
width: 960px;
|
||||
max-width: 100vw;
|
||||
padding: 5px;
|
||||
padding-top: 50px;
|
||||
/* max-height: calc(100vh - 50px); */
|
||||
`
|
||||
|
||||
const Layout = ({ children, user, noBackground, router }: {children: React.Element<any>, user: User, noBackground: boolean, router: * }) => <>
|
||||
<GlobalColors />
|
||||
<SocialCards />
|
||||
<LayoutWrapper>
|
||||
<HeaderBar user={user} noBackground={noBackground} router={router} />
|
||||
<ContentBox>
|
||||
{children}
|
||||
</ContentBox>
|
||||
</LayoutWrapper>
|
||||
</>
|
||||
|
||||
export default Layout
|
34
packages/roleypoly-ui/components/logo.js
Normal file
34
packages/roleypoly-ui/components/logo.js
Normal file
File diff suppressed because one or more lines are too long
78
packages/roleypoly-ui/components/role/__test__/Role.test.js
Normal file
78
packages/roleypoly-ui/components/role/__test__/Role.test.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
/* eslint-env jest */
|
||||
|
||||
import * as React from 'react'
|
||||
// import renderer from 'react-test-renderer'
|
||||
import { shallow } from 'enzyme'
|
||||
import Role from '../index'
|
||||
import 'jest-styled-components'
|
||||
|
||||
describe('<Role />', () => {
|
||||
it('renders correctly', () => {
|
||||
const role = shallow(<Role role={{ name: 'Test Role', color: '#ffffff' }} />)
|
||||
expect(role).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('triggers onToggle with new state', () => {
|
||||
let changed = false
|
||||
const role = shallow(
|
||||
<Role
|
||||
role={{ name: 'Test Role', color: '#ffffff' }}
|
||||
onToggle={(next) => { changed = next }}
|
||||
active={false}
|
||||
/>
|
||||
)
|
||||
role.simulate('click')
|
||||
expect(changed).toBe(true)
|
||||
|
||||
const role2 = shallow(
|
||||
<Role
|
||||
role={{ name: 'Test Role', color: '#ffffff' }}
|
||||
onToggle={(next) => { changed = next }}
|
||||
active
|
||||
/>
|
||||
)
|
||||
role2.simulate('click')
|
||||
expect(changed).toBe(false)
|
||||
})
|
||||
|
||||
it('fixes colors when they are not set', () => {
|
||||
const role = shallow(<Role role={{ name: 'Test Role', color: 0 }} />)
|
||||
expect(role.props().style['--role-color-base']).toEqual('hsl(0, 0%, 93.7%)')
|
||||
})
|
||||
|
||||
it('has a single space for a name when empty', () => {
|
||||
const role = shallow(<Role role={{ name: '', color: '#ffffff' }} />)
|
||||
expect(role.text()).toEqual(' ')
|
||||
})
|
||||
|
||||
describe('when disabled,', () => {
|
||||
it('handles touch hover events', () => {
|
||||
const role = shallow(<Role role={{ name: 'unsafe role', color: '#ffffff' }} disabled />)
|
||||
|
||||
role.simulate('touchstart')
|
||||
expect(role.state().hovering).toEqual(true)
|
||||
expect(role).toMatchSnapshot() // expecting tooltip
|
||||
expect(role.exists('tooltip')).toEqual(true)
|
||||
|
||||
role.simulate('touchend')
|
||||
expect(role.state().hovering).toEqual(false)
|
||||
})
|
||||
|
||||
it('does not trigger onToggle on click', () => {
|
||||
let changed = false
|
||||
const role = shallow(
|
||||
<Role
|
||||
role={{ name: 'Test Role', color: '#ffffff' }}
|
||||
onToggle={() => { changed = true }}
|
||||
active={changed}
|
||||
disabled
|
||||
/>
|
||||
)
|
||||
expect(role).toMatchSnapshot()
|
||||
role.simulate('click')
|
||||
|
||||
expect(role.html()).toBe(role.html())
|
||||
expect(changed).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,64 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Role /> renders correctly 1`] = `
|
||||
<rolestyled
|
||||
onClick={[Function]}
|
||||
onTouchEnd={null}
|
||||
onTouchStart={null}
|
||||
style={
|
||||
Object {
|
||||
"--role-color-active": "hsl(0, 0%, 100%)",
|
||||
"--role-color-base": "hsl(0, 0%, 100%)",
|
||||
"--role-color-outline": "hsla(0, 0%, 100%, 0.7)",
|
||||
"--role-color-outline-alt": "hsla(0, 0%, 100%, 0.4)",
|
||||
}
|
||||
}
|
||||
title={null}
|
||||
>
|
||||
Test Role
|
||||
</rolestyled>
|
||||
`;
|
||||
|
||||
exports[`<Role /> when disabled, does not trigger onToggle on click 1`] = `
|
||||
<rolestyled
|
||||
active={false}
|
||||
disabled={true}
|
||||
onClick={null}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"--role-color-active": "hsl(0, 0%, 100%)",
|
||||
"--role-color-base": "hsl(0, 0%, 100%)",
|
||||
"--role-color-outline": "hsla(0, 0%, 100%, 0.7)",
|
||||
"--role-color-outline-alt": "hsla(0, 0%, 100%, 0.4)",
|
||||
}
|
||||
}
|
||||
title="This role has unsafe permissions."
|
||||
>
|
||||
Test Role
|
||||
</rolestyled>
|
||||
`;
|
||||
|
||||
exports[`<Role /> when disabled, handles touch hover events 1`] = `
|
||||
<rolestyled
|
||||
disabled={true}
|
||||
onClick={null}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"--role-color-active": "hsl(0, 0%, 100%)",
|
||||
"--role-color-base": "hsl(0, 0%, 100%)",
|
||||
"--role-color-outline": "hsla(0, 0%, 100%, 0.7)",
|
||||
"--role-color-outline-alt": "hsla(0, 0%, 100%, 0.4)",
|
||||
}
|
||||
}
|
||||
title="This role has unsafe permissions."
|
||||
>
|
||||
unsafe role
|
||||
<tooltip>
|
||||
This role has unsafe permissions.
|
||||
</tooltip>
|
||||
</rolestyled>
|
||||
`;
|
|
@ -0,0 +1,14 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<RoleDemo /> renders 1`] = `
|
||||
<Role
|
||||
active={false}
|
||||
onToggle={[Function]}
|
||||
role={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"name": "test demo role",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
19
packages/roleypoly-ui/components/role/__test__/demo.test.js
Normal file
19
packages/roleypoly-ui/components/role/__test__/demo.test.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/* eslint-env jest */
|
||||
import * as React from 'react'
|
||||
import { shallow } from 'enzyme'
|
||||
import RoleDemo from '../demo'
|
||||
import 'jest-styled-components'
|
||||
|
||||
describe('<RoleDemo />', () => {
|
||||
it('renders', () => {
|
||||
const demo = shallow(<RoleDemo role={{ name: 'test demo role', color: '#ffffff' }} />)
|
||||
expect(demo).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('changes state when clicked', () => {
|
||||
const demo = shallow(<RoleDemo role={{ name: 'test demo role', color: '#ffffff' }} />)
|
||||
expect(demo.state().active).toEqual(false)
|
||||
demo.dive().simulate('click')
|
||||
expect(demo.state().active).toEqual(true)
|
||||
})
|
||||
})
|
25
packages/roleypoly-ui/components/role/demo.js
Normal file
25
packages/roleypoly-ui/components/role/demo.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import Role, { type RoleData } from './index'
|
||||
|
||||
export type DemoRoleProps = {
|
||||
role: RoleData
|
||||
}
|
||||
|
||||
type DemoRoleState = {
|
||||
active: boolean
|
||||
}
|
||||
|
||||
export default class RoleDemo extends React.Component<DemoRoleProps, DemoRoleState> {
|
||||
state = {
|
||||
active: false
|
||||
}
|
||||
|
||||
onToggle = (n: boolean) => {
|
||||
this.setState({ active: n })
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Role role={this.props.role} onToggle={this.onToggle} active={this.state.active} />
|
||||
}
|
||||
}
|
88
packages/roleypoly-ui/components/role/index.js
Normal file
88
packages/roleypoly-ui/components/role/index.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { colors } from '../global-colors'
|
||||
import Color from 'color'
|
||||
import RoleStyled from './role.styled'
|
||||
import Tooltip from '../tooltip'
|
||||
// import logger from '../../../logger'
|
||||
// const log = logger(__filename)
|
||||
|
||||
const fromColors = (colors) => Object.entries(colors).reduce(
|
||||
(acc, [v, val]) => ({ ...acc, [`--role-color-${v}`]: val })
|
||||
, {})
|
||||
|
||||
export type RoleData = {
|
||||
color: string,
|
||||
name: string,
|
||||
}
|
||||
|
||||
export type RoleProps = {
|
||||
active?: boolean, // is lit up as if it's in use
|
||||
disabled?: boolean, // is interaction-disabled
|
||||
type?: 'drag' | 'button',
|
||||
role: RoleData,
|
||||
isDragging?: boolean,
|
||||
onToggle?: (nextState: boolean, lastState: boolean) => void,
|
||||
connectDragSource?: (component: React.Node) => void
|
||||
}
|
||||
|
||||
// const tooltip = ({ show = true, text, ...rest }) => <div {...rest}>{text}</div>
|
||||
|
||||
type RoleState = {
|
||||
hovering: boolean
|
||||
}
|
||||
|
||||
export default class Role extends React.Component<RoleProps, RoleState> {
|
||||
state = {
|
||||
hovering: false
|
||||
}
|
||||
|
||||
onToggle = () => {
|
||||
if (!this.props.disabled && this.props.onToggle) {
|
||||
const { active = false } = this.props
|
||||
this.props.onToggle(!active, active)
|
||||
}
|
||||
}
|
||||
|
||||
onMouseOver = () => {
|
||||
// log.debug('caught hovering')
|
||||
if (this.props.disabled && this.state.hovering === false) {
|
||||
// log.debug('set hovering')
|
||||
this.setState({ hovering: true })
|
||||
}
|
||||
}
|
||||
|
||||
onMouseOut = () => {
|
||||
// log.debug('out hovering')
|
||||
this.setState({ hovering: false })
|
||||
}
|
||||
|
||||
render () {
|
||||
let color = Color(this.props.role.color)
|
||||
if (this.props.role.color === 0) {
|
||||
color = colors.white
|
||||
}
|
||||
|
||||
const roleColors = {
|
||||
'outline': Color(color).alpha(0.7).hsl().string(),
|
||||
'outline-alt': Color(color).alpha(0.4).hsl().string(),
|
||||
'active': Color(color).lighten(0.1).hsl().string(),
|
||||
'base': Color(color).hsl().string()
|
||||
}
|
||||
|
||||
const name = (this.props.role.name !== '') ? this.props.role.name : ' '
|
||||
|
||||
return <RoleStyled
|
||||
active={this.props.active}
|
||||
disabled={this.props.disabled}
|
||||
onClick={(this.props.disabled) ? null : this.onToggle}
|
||||
onTouchStart={(this.props.disabled) ? this.onMouseOver : null}
|
||||
onTouchEnd={(this.props.disabled) ? this.onMouseOut : null}
|
||||
style={fromColors(roleColors)}
|
||||
title={(this.props.disabled) ? 'This role has unsafe permissions.' : null}
|
||||
>
|
||||
{name}
|
||||
{ (this.props.disabled && this.state.hovering) && <Tooltip>This role has unsafe permissions.</Tooltip> }
|
||||
</RoleStyled>
|
||||
}
|
||||
}
|
124
packages/roleypoly-ui/components/role/role.styled.js
Normal file
124
packages/roleypoly-ui/components/role/role.styled.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
import styled from 'styled-components'
|
||||
import MediaQuery from '../../kit/media'
|
||||
|
||||
export default styled.div`
|
||||
border: solid 1px var(--role-color-outline);
|
||||
border-radius: 1.2em;
|
||||
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
|
||||
font-size: 1.2em;
|
||||
line-height: 20px;
|
||||
|
||||
margin: 0.3em;
|
||||
padding: 4px 0.5em;
|
||||
|
||||
min-height: 32px;
|
||||
max-width: 90vw;
|
||||
|
||||
transition: box-shadow 0.3s ease-in-out;
|
||||
|
||||
text-shadow: 1px 1px 1px rgba(0,0,0,0.45);
|
||||
text-overflow: ellipsis;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
transform: rotateZ(0);
|
||||
|
||||
${(props: any) => (props.active) ? `
|
||||
box-shadow: inset 0 0 0 3em var(--role-color-outline-alt);
|
||||
` : `
|
||||
`}
|
||||
|
||||
.wf-active & {
|
||||
/* padding-top: 4px; */
|
||||
}
|
||||
|
||||
&[disabled]:hover {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
transform: translateY(-1px) rotateZ(0);
|
||||
box-shadow: 0 0 1px rgba(0,0,0,0.75);
|
||||
border-color: var(--role-color-active);
|
||||
clip-path: border-box circle(50.2% at 49.6% 50%); /* firefox fix */
|
||||
}
|
||||
|
||||
&:active::after {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
bottom: 2px;
|
||||
top: 4px;
|
||||
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
|
||||
border: 1px solid var(--role-color-base);
|
||||
border-radius: 100%;
|
||||
|
||||
clip-path: border-box circle(50.2% at 50% 50%); /* this is just for you, firefox. */
|
||||
|
||||
transform: rotateZ(0);
|
||||
${(props: any) => (props.active) ? `
|
||||
transition: border 0.3s ease-in-out, transform 0.1s ease-in-out, background-color 1ms ease-in-out 0.29s;
|
||||
border-left-width: 21px;
|
||||
` : `
|
||||
transition: border 0.3s ease-in-out, transform 0.1s ease-in-out, background-color 0.3s ease-in-out;
|
||||
`}
|
||||
}
|
||||
|
||||
${(props: any) => MediaQuery({
|
||||
md: `
|
||||
font-size: 1em;
|
||||
text-shadow: none;
|
||||
padding-left: 32px;
|
||||
${(props.active) ? `box-shadow: none;` : ''}
|
||||
&::after {
|
||||
${(props.active) ? `background-color: var(--role-color-base);` : ''}
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
${(props.active) ? `background-color: var(--role-color-active);` : ''}
|
||||
}
|
||||
`
|
||||
})}
|
||||
|
||||
&[disabled] {
|
||||
border-color: hsl(0,0%,40%);
|
||||
color: hsla(0,0%,40%,0.7);
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
${(props: any) => (props.active) ? `
|
||||
box-shadow: inset 0 0 0 3em hsla(0,0%,40%,0.1);
|
||||
background-color: hsl(0,0%,40%);`
|
||||
: ``};
|
||||
|
||||
&::after {
|
||||
border-color: hsl(0,0%,40%);
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
border-color: hsl(0,0%,40%);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
`
|
36
packages/roleypoly-ui/components/social-cards.js
Normal file
36
packages/roleypoly-ui/components/social-cards.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import NextHead from 'next/head'
|
||||
|
||||
export type SocialCardProps = {
|
||||
title?: string,
|
||||
description?: string,
|
||||
image?: string,
|
||||
imageSize?: number
|
||||
}
|
||||
|
||||
const defaultProps: SocialCardProps = {
|
||||
title: 'Roleypoly',
|
||||
description: 'Tame your Discord roles.',
|
||||
image: 'https://rp.kat.cafe/static/social.png',
|
||||
imageSize: 200
|
||||
}
|
||||
|
||||
const SocialCards = (props: SocialCardProps) => {
|
||||
props = {
|
||||
...defaultProps,
|
||||
...props
|
||||
}
|
||||
|
||||
return <NextHead>
|
||||
<meta key='og:title' property='og:title' content={props.title} />
|
||||
<meta key='og:description' property='og:description' content={props.description} />
|
||||
<meta key='twitter:card' name='twitter:card' content='summary_large_image' />
|
||||
<meta key='twitter:image' name='twitter:image' content={props.image} />
|
||||
<meta key='og:image' property='og:image' content={props.image} />
|
||||
<meta key='og:image:width' property='og:image:width' content={props.imageSize} />
|
||||
<meta key='og:image:height' property='og:image:height' content={props.imageSize} />
|
||||
</NextHead>
|
||||
}
|
||||
|
||||
export default SocialCards
|
22
packages/roleypoly-ui/components/tooltip.js
Normal file
22
packages/roleypoly-ui/components/tooltip.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import styled from 'styled-components'
|
||||
import MediaQuery from '../kit/media'
|
||||
|
||||
export default styled.div`
|
||||
position: absolute;
|
||||
bottom: 35px;
|
||||
font-size: 0.9em;
|
||||
background-color: rgba(0,0,0,0.50);
|
||||
padding: 5px;
|
||||
color: var(--c-red);
|
||||
border-radius: 3px;
|
||||
border: 1px black solid;
|
||||
z-index: 10;
|
||||
opacity: 0.99;
|
||||
overflow: auto;
|
||||
pointer-events: none;
|
||||
/* max-width: 50vw; */
|
||||
white-space: normal;
|
||||
${() => MediaQuery({ md: `
|
||||
white-space: nowrap; `
|
||||
})}
|
||||
`
|
8
packages/roleypoly-ui/config/demo.js
Normal file
8
packages/roleypoly-ui/config/demo.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default [
|
||||
'cute',
|
||||
'vanity',
|
||||
'brave',
|
||||
'proud',
|
||||
'wonderful',
|
||||
'日本語'
|
||||
].map((name, i, roles) => ({ name: `a ${name} role ♡`, color: `hsl(${(360 / roles.length) * i},40%,70%)` }))
|
17
packages/roleypoly-ui/config/redux.js
Normal file
17
packages/roleypoly-ui/config/redux.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { createStore, applyMiddleware } from 'redux'
|
||||
import { composeWithDevTools } from 'redux-devtools-extension'
|
||||
import thunkMiddleware from 'redux-thunk'
|
||||
import withNextRedux from 'next-redux-wrapper'
|
||||
import { rootReducer } from 'fast-redux'
|
||||
|
||||
export const initStore = (initialState = {}) => {
|
||||
return createStore(
|
||||
rootReducer,
|
||||
initialState,
|
||||
composeWithDevTools(applyMiddleware(thunkMiddleware))
|
||||
)
|
||||
}
|
||||
|
||||
export const withRedux = (comp) => withNextRedux(initStore, {
|
||||
debug: process.env.NODE_ENV === 'development'
|
||||
})(comp)
|
13
packages/roleypoly-ui/config/rpc.js
Normal file
13
packages/roleypoly-ui/config/rpc.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
// @flow
|
||||
import RPCClient from '../rpc'
|
||||
|
||||
const client = new RPCClient({ forceDev: false })
|
||||
|
||||
export default client.rpc
|
||||
export const withCookies = (ctx: any) => {
|
||||
if (ctx.req != null) {
|
||||
return client.withCookies(ctx.req.headers.cookie)
|
||||
} else {
|
||||
return client.rpc
|
||||
}
|
||||
}
|
21
packages/roleypoly-ui/containers/header-bar.js
Normal file
21
packages/roleypoly-ui/containers/header-bar.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { type User } from '../stores/user'
|
||||
|
||||
type Props = {
|
||||
user: ?User
|
||||
}
|
||||
|
||||
const HeaderBarAuth = dynamic(() => import('../components/header/auth'))
|
||||
const HeaderBarUnauth = dynamic(() => import('../components/header/unauth'))
|
||||
|
||||
const HeaderBar = (props: Props) => {
|
||||
if (props.user == null) {
|
||||
return <HeaderBarUnauth {...props} />
|
||||
} else {
|
||||
return <HeaderBarAuth {...props} />
|
||||
}
|
||||
}
|
||||
|
||||
export default HeaderBar
|
|
@ -0,0 +1,19 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MediaQuery outputs media queries 1`] = `
|
||||
"@media screen and (min-width: 0px) {
|
||||
font-size: 0.5em;
|
||||
};
|
||||
@media screen and (min-width: 544px) {
|
||||
font-size: 1em;
|
||||
};
|
||||
@media screen and (min-width: 768px) {
|
||||
font-size: 1.5em;
|
||||
};
|
||||
@media screen and (min-width: 1012px) {
|
||||
font-size: 2em;
|
||||
};
|
||||
@media screen and (min-width: 1280px) {
|
||||
font-size: 2.5em;
|
||||
};"
|
||||
`;
|
16
packages/roleypoly-ui/kit/__test__/media.test.js
Normal file
16
packages/roleypoly-ui/kit/__test__/media.test.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/* eslint-env jest */
|
||||
import MediaQuery from '../media'
|
||||
|
||||
describe('MediaQuery', () => {
|
||||
it('outputs media queries', () => {
|
||||
const mq = MediaQuery({
|
||||
xs: 'font-size: 0.5em;',
|
||||
sm: 'font-size: 1em;',
|
||||
md: 'font-size: 1.5em;',
|
||||
lg: 'font-size: 2em;',
|
||||
xl: 'font-size: 2.5em;'
|
||||
})
|
||||
|
||||
expect(mq).toMatchSnapshot()
|
||||
})
|
||||
})
|
31
packages/roleypoly-ui/kit/debug-breakpoints.js
Normal file
31
packages/roleypoly-ui/kit/debug-breakpoints.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import MediaQuery, { breakpoints } from './media'
|
||||
|
||||
const BreakpointDebugFloat = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0em;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
height: 1.4em;
|
||||
opacity: 0.4;
|
||||
font-family: monospace;
|
||||
`
|
||||
|
||||
const Breakpoint = styled.div`
|
||||
padding: 0.1em;
|
||||
display: none;
|
||||
width: 1.5em;
|
||||
text-align: center;
|
||||
background-color: hsl(${(props: any) => props.hue}, 50%, 50%);
|
||||
${(props: any) => MediaQuery({ [props.bp]: `display: inline-block` })}
|
||||
`
|
||||
|
||||
const DebugFloater = () => {
|
||||
return <BreakpointDebugFloat>
|
||||
{ Object.keys(breakpoints).map((x, i, a) => <Breakpoint key={x} bp={x} hue={(360 / a.length) * i}>{x}</Breakpoint>) }
|
||||
</BreakpointDebugFloat>
|
||||
}
|
||||
|
||||
export default DebugFloater
|
74
packages/roleypoly-ui/kit/media.js
Normal file
74
packages/roleypoly-ui/kit/media.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
// @flow
|
||||
import styled from 'styled-components'
|
||||
|
||||
export type MediaQueryConfig = $Shape<{
|
||||
xs: string,
|
||||
sm: string,
|
||||
md: string,
|
||||
lg: string,
|
||||
xl: string
|
||||
}>
|
||||
|
||||
export const breakpoints = {
|
||||
xs: 0,
|
||||
sm: 544,
|
||||
md: 768,
|
||||
lg: 1012,
|
||||
xl: 1280
|
||||
}
|
||||
|
||||
const MediaQuery = (mq: MediaQueryConfig) => {
|
||||
const out = []
|
||||
|
||||
for (const size in mq) {
|
||||
if (breakpoints[size] == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
const inner = mq[size]
|
||||
|
||||
out.push(`@media screen and (min-width: ${breakpoints[size]}px) {\n${inner}\n};`)
|
||||
}
|
||||
|
||||
return out.join('\n')
|
||||
}
|
||||
|
||||
export default MediaQuery
|
||||
|
||||
export const Hide = {
|
||||
XS: styled.div`
|
||||
display: none;
|
||||
${() => MediaQuery({ sm: `display: block` })}
|
||||
`,
|
||||
SM: styled.div`
|
||||
display: none;
|
||||
${() => MediaQuery({ md: `display: block` })}
|
||||
`,
|
||||
MD: styled.div`
|
||||
display: none;
|
||||
${() => MediaQuery({ lg: `display: block` })}
|
||||
`,
|
||||
LG: styled.div`
|
||||
display: none;
|
||||
${() => MediaQuery({ xl: `display: block` })}
|
||||
`
|
||||
}
|
||||
|
||||
export const Show = {
|
||||
XS: styled.div`
|
||||
display: block;
|
||||
${() => MediaQuery({ sm: `display: none` })}
|
||||
`,
|
||||
SM: styled.div`
|
||||
display: block;
|
||||
${() => MediaQuery({ md: `display: none` })}
|
||||
`,
|
||||
MD: styled.div`
|
||||
display: block;
|
||||
${() => MediaQuery({ lg: `display: none` })}
|
||||
`,
|
||||
LG: styled.div`
|
||||
display: block;
|
||||
${() => MediaQuery({ xl: `display: none` })}
|
||||
`
|
||||
}
|
13
packages/roleypoly-ui/lib/redirect.js
Normal file
13
packages/roleypoly-ui/lib/redirect.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import Router from 'next/router'
|
||||
|
||||
export default (context, target) => {
|
||||
if (context && context.res) {
|
||||
// server
|
||||
// 303: "See other"
|
||||
context.res.writeHead(303, { Location: target })
|
||||
context.res.end()
|
||||
} else {
|
||||
// In the browser, we just pretend like this never even happened ;)
|
||||
Router.replace(target)
|
||||
}
|
||||
}
|
112
packages/roleypoly-ui/pages/_app.js
Normal file
112
packages/roleypoly-ui/pages/_app.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import App, { Container } from 'next/app'
|
||||
import Head from 'next/head'
|
||||
import Layout from '../components/layout'
|
||||
import { withCookies } from '../config/rpc'
|
||||
import { Provider } from 'react-redux'
|
||||
import ErrorP, { Overlay } from './_error'
|
||||
import styled from 'styled-components'
|
||||
import { withRedux } from '../config/redux'
|
||||
import type { UserPartial } from '../../services/discord'
|
||||
|
||||
type NextPage = React.Component<any> & React.StatelessFunctionalComponent<any> & {
|
||||
getInitialProps: (ctx: any, ...args: any) => any
|
||||
}
|
||||
|
||||
const MissingJS = styled.noscript`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
font-size: 1.3em;
|
||||
padding: 1em;
|
||||
`
|
||||
|
||||
class RoleypolyApp extends App {
|
||||
static async getInitialProps ({ Component, ctx, router }: { Component: NextPage, router: any, ctx: {[x:string]: any}}) {
|
||||
// Fix for next/error rendering instead of our error page.
|
||||
// Who knows why this would ever happen.
|
||||
if (Component.displayName === 'ErrorPage' || Component.constructor.name === 'ErrorPage') {
|
||||
Component = ErrorP
|
||||
}
|
||||
|
||||
// console.log({ Component })
|
||||
|
||||
let pageProps = {}
|
||||
const rpc = withCookies(ctx)
|
||||
let user: ?UserPartial
|
||||
try {
|
||||
user = await rpc.getCurrentUser()
|
||||
ctx.user = user
|
||||
} catch (e) {
|
||||
if (e.code === 403) {
|
||||
ctx.user = null
|
||||
} else {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
ctx.robots = 'INDEX, FOLLOW'
|
||||
|
||||
ctx.layout = {
|
||||
noBackground: false
|
||||
}
|
||||
|
||||
if (Component.getInitialProps) {
|
||||
pageProps = await Component.getInitialProps(ctx, rpc, router)
|
||||
}
|
||||
|
||||
// console.log({ pageProps })
|
||||
|
||||
return { pageProps, user, layout: ctx.layout, robots: ctx.robots }
|
||||
}
|
||||
|
||||
catchFOUC () {
|
||||
setTimeout(() => {
|
||||
if (document.documentElement) document.documentElement.className += ' force-active'
|
||||
}, 700)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.catchFOUC()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { Component, pageProps, router, user, layout, robots, store } = this.props
|
||||
// Fix for next/error rendering instead of our error page.
|
||||
// Who knows why this would ever happen.
|
||||
const ErrorCaughtComponent = (Component.displayName === 'ErrorPage' || Component.constructor.name === 'ErrorPage') ? ErrorP : Component
|
||||
return <Container>
|
||||
<MissingJS>
|
||||
<Overlay />
|
||||
Hey there... Unfortunately, we require JS for this app to work. Please take this rose as retribution. 🌹
|
||||
</MissingJS>
|
||||
<Head>
|
||||
<meta charSet='utf-8' />
|
||||
<title key='title'>Roleypoly</title>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
||||
<link rel='icon' href='/static/favicon.png' />
|
||||
<meta key='robots' name='robots' content={robots} />
|
||||
<script key='typekit' dangerouslySetInnerHTML={{ __html: `
|
||||
(function(d) {
|
||||
var config = {
|
||||
kitId: 'bck0pci',
|
||||
scriptTimeout: 1500,
|
||||
async: true
|
||||
},
|
||||
h=d.documentElement,t=setTimeout(function(){h.className=h.className.replace(/\bwf-loading\b/g,"")+" wf-inactive";},config.scriptTimeout),tk=d.createElement("script"),f=false,s=d.getElementsByTagName("script")[0],a;h.className+=" wf-loading";tk.src='https://use.typekit.net/'+config.kitId+'.js';tk.async=true;tk.onload=tk.onreadystatechange=function(){a=this.readyState;if(f||a&&a!="complete"&&a!="loaded")return;f=true;clearTimeout(t);try{Typekit.load(config)}catch(e){}};s.parentNode.insertBefore(tk,s)
|
||||
})(document);//
|
||||
` }} />
|
||||
</Head>
|
||||
<Provider store={store}>
|
||||
<Layout user={user} {...layout} router={router}>
|
||||
<ErrorCaughtComponent {...pageProps} router={router} originalName={Component.displayName || Component.constructor.name} />
|
||||
</Layout>
|
||||
</Provider>
|
||||
</Container>
|
||||
}
|
||||
}
|
||||
|
||||
export default withRedux(RoleypolyApp)
|
24
packages/roleypoly-ui/pages/_document.js
Normal file
24
packages/roleypoly-ui/pages/_document.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Document from 'next/document'
|
||||
import { ServerStyleSheet } from 'styled-components'
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
static async getInitialProps (ctx) {
|
||||
const sheet = new ServerStyleSheet()
|
||||
const originalRenderPage = ctx.renderPage
|
||||
|
||||
try {
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: App => props => sheet.collectStyles(<App {...props} />)
|
||||
})
|
||||
|
||||
const initialProps = await Document.getInitialProps(ctx)
|
||||
return {
|
||||
...initialProps,
|
||||
styles: <>{initialProps.styles}{sheet.getStyleElement()}</>
|
||||
}
|
||||
} finally {
|
||||
sheet.seal()
|
||||
}
|
||||
}
|
||||
}
|
110
packages/roleypoly-ui/pages/_error.js
Normal file
110
packages/roleypoly-ui/pages/_error.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import MediaQuery from '../kit/media'
|
||||
|
||||
export const Overlay = styled.div`
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: -10;
|
||||
background-image: radial-gradient(circle, var(--c-dark), var(--c-dark) 1px, transparent 1px, transparent);
|
||||
background-size: 27px 27px;
|
||||
`
|
||||
|
||||
const ResponsiveSplitter = styled.div`
|
||||
z-index: -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1.6;
|
||||
font-size: 1.3em;
|
||||
flex-direction: column;
|
||||
|
||||
${() => MediaQuery({
|
||||
md: `flex-direction: row; min-height: 100vh; position: relative; top: -50px;`
|
||||
})}
|
||||
|
||||
& > div {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
& section {
|
||||
text-align: center;
|
||||
${() => MediaQuery({
|
||||
md: `text-align: left;`
|
||||
})}
|
||||
}
|
||||
`
|
||||
|
||||
const JapaneseFlair = styled.section`
|
||||
color: var(--c-3);
|
||||
font-size: 0.9rem;
|
||||
`
|
||||
|
||||
const Code = styled.h1`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 4em;
|
||||
${() => MediaQuery({
|
||||
md: `font-size: 2em;`
|
||||
})}
|
||||
`
|
||||
|
||||
export default class CustomErrorPage extends React.Component {
|
||||
static getInitialProps ({ res, err, robots }) {
|
||||
const statusCode = res ? res.statusCode : err ? err.statusCode : null
|
||||
robots = 'NOINDEX, NOFOLLOW'
|
||||
return { statusCode }
|
||||
}
|
||||
|
||||
render400 = () => this.out('400', `Your client sent me something weird...`, '((((;゜Д゜)))')
|
||||
render403 = () => this.out('403', `You weren't allowed to access this.`, 'あなたはこの点に合格しないかもしれません')
|
||||
render404 = () => this.out('404', 'This page is in another castle.', 'お探しのページは見つかりませんでした')
|
||||
render419 = () => this.out('419', 'Something went too slowly...', 'おやすみなさい〜')
|
||||
render500 = () => this.out('500', `The server doesn't like you right now. Feed it a cookie.`, 'クッキーを送ってください〜 クッキーを送ってください〜')
|
||||
renderDefault = () => this.out('Oops', 'Something went bad. How could this happen?', 'おねがい?')
|
||||
renderServer = () => this.out('Oops.', 'Server was unhappy about this render. Try reloading or changing page.', 'クッキーを送ってください〜')
|
||||
renderAuthExpired = () => this.out('Woah.', 'That magic login link was expired.', 'What are you trying to do?')
|
||||
|
||||
out (code, description, flair) {
|
||||
return <div>
|
||||
<Overlay />
|
||||
<ResponsiveSplitter>
|
||||
<div>
|
||||
<Code>{code}</Code>
|
||||
</div>
|
||||
<div>
|
||||
<section>
|
||||
{description}
|
||||
</section>
|
||||
<JapaneseFlair>{flair}</JapaneseFlair>
|
||||
</div>
|
||||
</ResponsiveSplitter>
|
||||
</div>
|
||||
}
|
||||
|
||||
handlers = {
|
||||
400: this.render400,
|
||||
403: this.render403,
|
||||
404: this.render404,
|
||||
419: this.render419,
|
||||
500: this.render500,
|
||||
1001: this.renderAuthExpired
|
||||
}
|
||||
|
||||
render () {
|
||||
// if (this.props.originalName === 'ErrorPage') {
|
||||
// return this.renderServer()
|
||||
// }
|
||||
|
||||
if (this.props.statusCode in this.handlers) {
|
||||
return this.handlers[this.props.statusCode]()
|
||||
}
|
||||
|
||||
return this.renderDefault()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import SocialCards from '../../../components/social-cards'
|
||||
|
||||
export default () => <>
|
||||
<SocialCards title='Sign in on Roleypoly' />
|
||||
</>
|
|
@ -0,0 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import SocialCards from '../../../components/social-cards'
|
||||
|
||||
export default () => <>
|
||||
<SocialCards title='Sign in on Roleypoly' description="Click this link to log in. It's magic!" />
|
||||
</>
|
111
packages/roleypoly-ui/pages/_internal/_server.js
Normal file
111
packages/roleypoly-ui/pages/_internal/_server.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import Head from 'next/head'
|
||||
import type { PageProps } from '../../types'
|
||||
import SocialCards from '../../components/social-cards'
|
||||
import redirect from '../../lib/redirect'
|
||||
import { connect } from 'react-redux'
|
||||
import { fetchServerIfNeed, getCurrentServerState, type ServerState } from '../../stores/currentServer'
|
||||
import { renderRoles, getCategoryViewState, toggleRole, type ViewState } from '../../stores/roles'
|
||||
import styled from 'styled-components'
|
||||
import Role from '../../components/role'
|
||||
|
||||
type ServerPageProps = PageProps & {
|
||||
currentServer: ServerState,
|
||||
view: ViewState,
|
||||
isDiscordBot: boolean
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, { router: { query: { id } } }) => {
|
||||
return {
|
||||
currentServer: getCurrentServerState(state, id),
|
||||
view: getCategoryViewState(state)
|
||||
}
|
||||
}
|
||||
|
||||
const Category = styled.div``
|
||||
|
||||
const Hider = styled.div`
|
||||
/* opacity: ${(props: any) => props.visible ? '1' : '0'}; */
|
||||
/* opacity: 1; */
|
||||
/* transition: opacity 0.15s ease-out; */
|
||||
/* ${(props: any) => props.visible ? '' : 'display: none;'} */
|
||||
`
|
||||
|
||||
const RoleHolder = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
class Server extends React.PureComponent<ServerPageProps> {
|
||||
static async getInitialProps (ctx: *, rpc: *, router: *) {
|
||||
const isDiscordBot = ctx.req && ctx.req.headers['user-agent'].includes('Discordbot')
|
||||
if (ctx.user == null) {
|
||||
if (!isDiscordBot) {
|
||||
redirect(ctx, `/auth/login?r=${router.asPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.robots = 'NOINDEX, NOFOLLOW'
|
||||
await ctx.store.dispatch(fetchServerIfNeed(router.query.id, rpc))
|
||||
|
||||
if (!isDiscordBot) {
|
||||
await ctx.store.dispatch(renderRoles(router.query.id))
|
||||
}
|
||||
return { isDiscordBot }
|
||||
}
|
||||
|
||||
async componentDidMount () {
|
||||
const { currentServer, router: { query: { id } }, dispatch } = this.props
|
||||
if (currentServer == null) {
|
||||
this.props.router.push('/s/add')
|
||||
}
|
||||
|
||||
await dispatch(fetchServerIfNeed(id))
|
||||
await dispatch(renderRoles(id))
|
||||
}
|
||||
|
||||
onToggle = (role) => (nextState) => {
|
||||
if (role.safe) {
|
||||
this.props.dispatch(toggleRole(role.id, nextState))
|
||||
}
|
||||
}
|
||||
|
||||
renderSocial () {
|
||||
const { currentServer } = this.props
|
||||
return <SocialCards title={`${currentServer.server.name} on Roleypoly`} description='Manage your roles here.' />
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isDiscordBot, currentServer, view } = this.props
|
||||
// console.log({ currentServer })
|
||||
if (currentServer == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isDiscordBot) {
|
||||
return this.renderSocial()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title key='title'>{currentServer.server.name} - Roleypoly</title>
|
||||
</Head>
|
||||
{ this.renderSocial() }
|
||||
hello <span style={{ color: currentServer.gm?.color }}>{currentServer.gm?.nickname}</span> on {currentServer.server.name} ({ view.dirty ? 'dirty' : 'clean' })
|
||||
<Hider visible={true || currentServer.id !== null}>
|
||||
{ !view.invalidated && view.categories.map(c => <Category key={`cat__${c.name}__${c.id}`}>
|
||||
<div>{ c.name }</div>
|
||||
<RoleHolder>
|
||||
{
|
||||
c._roles && c._roles.map(r => <Role key={`role__${r.name}__${r.id}`} role={r} active={view.selected.includes(r.id)} onToggle={this.onToggle(r)} disabled={!r.safe} />)
|
||||
}
|
||||
</RoleHolder>
|
||||
</Category>) }
|
||||
</Hider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Server)
|
1
packages/roleypoly-ui/pages/_internal/_server_add.js
Normal file
1
packages/roleypoly-ui/pages/_internal/_server_add.js
Normal file
|
@ -0,0 +1 @@
|
|||
export default () => <h1>s/add</h1>
|
2
packages/roleypoly-ui/pages/_test/error.js
Normal file
2
packages/roleypoly-ui/pages/_test/error.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Error from '../_error'
|
||||
export default ({ router: { query: { code = 404 } } }) => <Error statusCode={code} />
|
23
packages/roleypoly-ui/pages/_test/landing-mock.js
Normal file
23
packages/roleypoly-ui/pages/_test/landing-mock.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import type { PageProps } from '../../types'
|
||||
|
||||
export default class LandingTest extends React.Component<PageProps> {
|
||||
render () {
|
||||
return <div>
|
||||
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit accusantium quidem adipisci excepturi, delectus iure omnis, eos corrupti, ea ex iusto magnam! Incidunt accusamus repellat natus esse facilis animi aut.</p>
|
||||
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit accusantium quidem adipisci excepturi, delectus iure omnis, eos corrupti, ea ex iusto magnam! Incidunt accusamus repellat natus esse facilis animi aut.</p>
|
||||
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit accusantium quidem adipisci excepturi, delectus iure omnis, eos corrupti, ea ex iusto magnam! Incidunt accusamus repellat natus esse facilis animi aut.</p>
|
||||
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit accusantium quidem adipisci excepturi, delectus iure omnis, eos corrupti, ea ex iusto magnam! Incidunt accusamus repellat natus esse facilis animi aut.</p>
|
||||
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit accusantium quidem adipisci excepturi, delectus iure omnis, eos corrupti, ea ex iusto magnam! Incidunt accusamus repellat natus esse facilis animi aut.</p>
|
||||
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit accusantium quidem adipisci excepturi, delectus iure omnis, eos corrupti, ea ex iusto magnam! Incidunt accusamus repellat natus esse facilis animi aut.</p>
|
||||
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit accusantium quidem adipisci excepturi, delectus iure omnis, eos corrupti, ea ex iusto magnam! Incidunt accusamus repellat natus esse facilis animi aut.</p>
|
||||
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit accusantium quidem adipisci excepturi, delectus iure omnis, eos corrupti, ea ex iusto magnam! Incidunt accusamus repellat natus esse facilis animi aut.</p>
|
||||
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit accusantium quidem adipisci excepturi, delectus iure omnis, eos corrupti, ea ex iusto magnam! Incidunt accusamus repellat natus esse facilis animi aut.</p>
|
||||
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit accusantium quidem adipisci excepturi, delectus iure omnis, eos corrupti, ea ex iusto magnam! Incidunt accusamus repellat natus esse facilis animi aut.</p>
|
||||
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit accusantium quidem adipisci excepturi, delectus iure omnis, eos corrupti, ea ex iusto magnam! Incidunt accusamus repellat natus esse facilis animi aut.</p>
|
||||
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit accusantium quidem adipisci excepturi, delectus iure omnis, eos corrupti, ea ex iusto magnam! Incidunt accusamus repellat natus esse facilis animi aut.</p>
|
||||
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Suscipit accusantium quidem adipisci excepturi, delectus iure omnis, eos corrupti, ea ex iusto magnam! Incidunt accusamus repellat natus esse facilis animi aut.</p>
|
||||
</div>
|
||||
}
|
||||
}
|
3
packages/roleypoly-ui/pages/auth/expired.js
Normal file
3
packages/roleypoly-ui/pages/auth/expired.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import ErrorPage from '../_error'
|
||||
|
||||
export default (props) => <ErrorPage {...props} statusCode={1001} />
|
192
packages/roleypoly-ui/pages/auth/login.js
Normal file
192
packages/roleypoly-ui/pages/auth/login.js
Normal file
|
@ -0,0 +1,192 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import MediaQuery from '../../kit/media'
|
||||
import DiscordButton from '../../components/discord-button'
|
||||
import RPC from '../../config/rpc'
|
||||
import redirect from '../../lib/redirect'
|
||||
import dynamic from 'next/dynamic'
|
||||
import type { PageProps, ServerSlug } from '../../types'
|
||||
import getConfig from 'next/config'
|
||||
const { publicRuntimeConfig: { BOT_HANDLE } } = getConfig()
|
||||
|
||||
type AuthLoginState = {
|
||||
humanCode: string,
|
||||
waiting: boolean
|
||||
}
|
||||
|
||||
type AuthLoginProps = PageProps & {
|
||||
redirect: ?string,
|
||||
redirectSlug: ?ServerSlug
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 3em;
|
||||
width: 400px;
|
||||
max-width: calc(98vw - 10px);
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
${() => MediaQuery({
|
||||
md: `
|
||||
padding-top: 0;
|
||||
align-items: center;
|
||||
min-height: 80vh;
|
||||
`
|
||||
})}
|
||||
`
|
||||
|
||||
const Line = styled.div`
|
||||
height: 1px;
|
||||
background-color: var(--c-9);
|
||||
margin: 1em 0.3em;
|
||||
`
|
||||
|
||||
const SecretCode = styled.input`
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
padding: 1em;
|
||||
color: var(--c-9);
|
||||
margin: 0.5rem 0;
|
||||
width: 100%;
|
||||
font-size: 0.9em;
|
||||
appearance: none;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&:focus, &:active, &:hover {
|
||||
background-color: var(--c-3);
|
||||
}
|
||||
|
||||
&:focus, &:active {
|
||||
& ::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
& ::placeholder {
|
||||
transition: all 0.3s ease-in-out;
|
||||
color: var(--c-7);
|
||||
text-align: center;
|
||||
}
|
||||
`
|
||||
|
||||
const HiderButton = styled.button`
|
||||
appearance: none;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
background-color: var(--c-3);
|
||||
color: var(--c-white);
|
||||
border: none;
|
||||
padding: 1em;
|
||||
font-size: 0.9em;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&[disabled] {
|
||||
cursor: default;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
`
|
||||
const SlugWrapper = styled.div`
|
||||
padding-bottom: 2em;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const DiscordGuildPic = dynamic(() => import('../../components/discord-guild-pic'))
|
||||
const StyledDGP = styled(DiscordGuildPic)`
|
||||
border-radius: 100%;
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
height: 4em;
|
||||
margin-top: 1em;
|
||||
`
|
||||
|
||||
const ServerName = styled.span`
|
||||
font-weight: bold;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 370px;
|
||||
display: block;
|
||||
`
|
||||
|
||||
const Slug = (slug: ServerSlug) => <SlugWrapper>
|
||||
<StyledDGP {...slug} />
|
||||
<br />Hey there.<br /><ServerName>{slug.name}</ServerName> uses Roleypoly to manage its roles.
|
||||
</SlugWrapper>
|
||||
|
||||
export default class AuthLogin extends React.Component<AuthLoginProps, AuthLoginState> {
|
||||
state = {
|
||||
humanCode: '',
|
||||
waiting: false
|
||||
}
|
||||
|
||||
static async getInitialProps (ctx: *, rpc: typeof RPC, router: *) {
|
||||
let { r } = (router.query: { r: string })
|
||||
|
||||
if (ctx.user != null) {
|
||||
redirect(ctx, r || '/')
|
||||
}
|
||||
|
||||
ctx.robots = 'NOINDEX, NOFOLLOW'
|
||||
|
||||
if (r != null) {
|
||||
let redirectSlug = null
|
||||
if (r.startsWith('/s/') && r !== '/s/add') {
|
||||
redirectSlug = await rpc.getServerSlug(r.replace('/s/', ''))
|
||||
}
|
||||
return { redirect: r, redirectSlug }
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (this.props.redirect != null) {
|
||||
this.props.router.replace(this.props.router.pathname)
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (event: any) => {
|
||||
this.setState({ humanCode: event.target.value })
|
||||
}
|
||||
|
||||
onSubmit = async () => {
|
||||
this.setState({ waiting: true })
|
||||
try {
|
||||
const result = await RPC.checkAuthChallenge(this.state.humanCode)
|
||||
if (result === true) {
|
||||
redirect(null, this.props.redirect || '/')
|
||||
}
|
||||
} catch (e) {
|
||||
this.setState({ waiting: false })
|
||||
}
|
||||
}
|
||||
|
||||
get dm () {
|
||||
if (BOT_HANDLE) {
|
||||
const [username, discrim] = BOT_HANDLE.split('#')
|
||||
return <><b>{ username }</b>#{discrim}</>
|
||||
}
|
||||
|
||||
return <><b>roleypoly</b>#3712</>
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Wrapper>
|
||||
<div>
|
||||
{(this.props.redirectSlug != null) ? <Slug {...this.props.redirectSlug} /> : null}
|
||||
<DiscordButton href={`/api/auth/redirect?r=${this.props.redirect || '/'}`}>Sign in with Discord</DiscordButton>
|
||||
<Line />
|
||||
<div>
|
||||
<i>Or, send a DM to {this.dm} saying: login</i>
|
||||
</div>
|
||||
<div>
|
||||
<SecretCode placeholder='click to enter super secret code' onChange={this.onChange} value={this.state.humanCode} />
|
||||
<HiderButton onClick={this.onSubmit} disabled={this.state.humanCode === ''}>{
|
||||
(this.state.waiting) ? 'One sec...' : 'Submit Code →'
|
||||
}</HiderButton>
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
}
|
||||
}
|
113
packages/roleypoly-ui/pages/help/why-no-roles.js
Normal file
113
packages/roleypoly-ui/pages/help/why-no-roles.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import demoRoles from '../../config/demo'
|
||||
import MediaQuery from '../../kit/media'
|
||||
|
||||
const admin = { name: 'admin', color: '#db2828' }
|
||||
const bot = { name: 'roleypoly', color: 'var(--c-5)' }
|
||||
|
||||
const exampleGood = [
|
||||
admin,
|
||||
bot,
|
||||
...demoRoles
|
||||
]
|
||||
|
||||
const exampleBad = [
|
||||
admin,
|
||||
...demoRoles,
|
||||
bot
|
||||
]
|
||||
|
||||
const DiscordOuter = styled.div`
|
||||
background-color: var(--dark-but-not-black);
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
color: var(--c-white);
|
||||
border: 1px solid rgba(0,0,0,0.25);
|
||||
width: 250px;
|
||||
margin: 0 auto;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const Collapser = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
${() => MediaQuery({
|
||||
md: `flex-direction: row;`
|
||||
})}
|
||||
`
|
||||
|
||||
const DiscordRole = styled.div`
|
||||
color: ${({ role: { color } }) => color};
|
||||
position: relative;
|
||||
padding: 0.3em 0.6em;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
transition: opacity 0.02s ease-in-out;
|
||||
opacity: 0;
|
||||
background-color: ${({ role: { color } }) => color};
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
${
|
||||
({ role }) => (role.name === 'roleypoly')
|
||||
? `
|
||||
background-color: ${role.color};
|
||||
color: var(--c-white);
|
||||
`
|
||||
: ``
|
||||
}
|
||||
`
|
||||
|
||||
const MicroHeader = styled.div`
|
||||
font-size: 0.7em;
|
||||
font-weight: bold;
|
||||
color: #72767d;
|
||||
padding: 0.3rem 0.6rem;
|
||||
`
|
||||
|
||||
const Center = styled.div`
|
||||
text-align: center;
|
||||
margin: 2em;
|
||||
`
|
||||
|
||||
const Example = ({ data }) => (
|
||||
<DiscordOuter>
|
||||
<MicroHeader>
|
||||
ROLES
|
||||
</MicroHeader>
|
||||
{
|
||||
data.map(role => <DiscordRole role={role} key={role.name}>{role.name}</DiscordRole>)
|
||||
}
|
||||
</DiscordOuter>
|
||||
)
|
||||
|
||||
export default () => <div>
|
||||
<h3>How come I can't see any of my roles?!</h3>
|
||||
<p>Discord doesn't let us act upon roles that are "higher" than Roleypoly's in the list. You must keep it's role higher than any role you may want to assign.</p>
|
||||
<Collapser>
|
||||
<Center>
|
||||
<h4 style={{ color: 'var(--c-red)' }}>Bad ❌</h4>
|
||||
<Example data={exampleBad} />
|
||||
</Center>
|
||||
<Center>
|
||||
<h4 style={{ color: 'var(--c-green)' }}>Good ✅</h4>
|
||||
<Example data={exampleGood} />
|
||||
</Center>
|
||||
</Collapser>
|
||||
</div>
|
119
packages/roleypoly-ui/pages/index.js
Normal file
119
packages/roleypoly-ui/pages/index.js
Normal file
|
@ -0,0 +1,119 @@
|
|||
import * as React from 'react'
|
||||
import redirect from '../lib/redirect'
|
||||
// import Link from 'next/link'
|
||||
// import Head from '../components/head'
|
||||
// import Nav from '../components/nav'
|
||||
import TypingDemo from '../components/demos/typing'
|
||||
import TapDemo from '../components/demos/tap'
|
||||
import styled from 'styled-components'
|
||||
import MediaQuery from '../kit/media'
|
||||
|
||||
const HeroBig = styled.h1`
|
||||
color: var(--c-7);
|
||||
font-size: 1.8em;
|
||||
`
|
||||
|
||||
const HeroSmol = styled.h1`
|
||||
color: var(--c-5);
|
||||
font-size: 1.1em;
|
||||
`
|
||||
|
||||
const Hero = styled.div`
|
||||
padding: 2em 0;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const Footer = styled.p`
|
||||
text-align: center;
|
||||
font-size: 0.7em;
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const FooterLink = styled.a`
|
||||
font-style: none;
|
||||
color: var(--c-7);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: var(--c-5);
|
||||
}
|
||||
`
|
||||
|
||||
const DemoArea = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
${() => MediaQuery({ md: `flex-direction: row;` })}
|
||||
|
||||
& > div {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
& > div > p {
|
||||
text-align: center;
|
||||
}
|
||||
`
|
||||
|
||||
const Wrapper = styled.div`
|
||||
flex-wrap: wrap;
|
||||
${() => MediaQuery({
|
||||
md: `
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80vh;
|
||||
min-height: 500px;
|
||||
`
|
||||
})}
|
||||
`
|
||||
|
||||
export default class Home extends React.Component {
|
||||
static async getInitialProps (ctx, rpc) {
|
||||
if (ctx.user != null) {
|
||||
redirect(ctx, '/s/add')
|
||||
}
|
||||
|
||||
ctx.layout.noBackground = true
|
||||
}
|
||||
|
||||
render () {
|
||||
return <div>
|
||||
<Wrapper>
|
||||
<div>
|
||||
<Hero>
|
||||
<HeroBig>Discord roles for humans.</HeroBig>
|
||||
<HeroSmol>Ditch bot commands once and for all.</HeroSmol>
|
||||
</Hero>
|
||||
<DemoArea>
|
||||
<div>
|
||||
<TypingDemo />
|
||||
<p>What is this? 2005?</p>
|
||||
</div>
|
||||
<div>
|
||||
<TapDemo />
|
||||
<p>Just click or tap.</p>
|
||||
</div>
|
||||
</DemoArea>
|
||||
</div>
|
||||
</Wrapper>
|
||||
<Footer>
|
||||
© {new Date().getFullYear()}<br />
|
||||
Made with ♡
|
||||
<img src='/static/flags.svg' style={{ height: '1em', opacity: 0.5 }} /><br />
|
||||
<FooterLink target='_blank' href='https://ko-fi.com/roleypoly'>Ko-Fi</FooterLink> -
|
||||
<FooterLink target='_blank' href='https://github.com/kayteh/roleypoly'>GitHub</FooterLink> -
|
||||
<FooterLink target='_blank' href='https://discord.gg/PWQUVsd'>Discord</FooterLink>
|
||||
</Footer>
|
||||
</div>
|
||||
}
|
||||
}
|
31
packages/roleypoly-ui/pages/testrpc.js
Normal file
31
packages/roleypoly-ui/pages/testrpc.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import * as React from 'react'
|
||||
import RPC, { withCookies } from '../config/rpc'
|
||||
|
||||
export default class TestRPC extends React.Component {
|
||||
static async getInitialProps (ctx) {
|
||||
const user = await withCookies(ctx).getCurrentUser()
|
||||
console.log(user)
|
||||
return {
|
||||
user
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount () {
|
||||
window.$RPC = RPC
|
||||
}
|
||||
|
||||
componentDidCatch (error, errorInfo) {
|
||||
if (error) {
|
||||
console.log(error, errorInfo)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.props.user == null) {
|
||||
return <div>hello stranger OwO</div>
|
||||
}
|
||||
|
||||
const { username, avatar, discriminator } = this.props.user
|
||||
return <div>hello, {username}#{discriminator} <img src={avatar} width={50} height={50} /></div>
|
||||
}
|
||||
}
|
119
packages/roleypoly-ui/rpc/index.js
Normal file
119
packages/roleypoly-ui/rpc/index.js
Normal file
|
@ -0,0 +1,119 @@
|
|||
// @flow
|
||||
import superagent from 'superagent'
|
||||
import RPCError from '../../rpc/_error'
|
||||
|
||||
export type RPCResponse = {
|
||||
response?: mixed,
|
||||
hash?: string,
|
||||
|
||||
// error stuff
|
||||
error?: boolean,
|
||||
msg?: string,
|
||||
trace?: string
|
||||
}
|
||||
|
||||
export type RPCRequest = {
|
||||
fn: string,
|
||||
args: mixed[]
|
||||
}
|
||||
|
||||
export default class RPCClient {
|
||||
dev: boolean = false
|
||||
baseUrl: string
|
||||
firstKnownHash: string
|
||||
recentHash: string
|
||||
cookieHeader: string
|
||||
|
||||
rpc: {
|
||||
[fn: string]: (...args: any[]) => Promise<any>
|
||||
} = {}
|
||||
|
||||
__rpcAvailable: Array<{
|
||||
name: string,
|
||||
args: number
|
||||
}> = []
|
||||
|
||||
constructor ({ forceDev, baseUrl = '/api/_rpc' }: { forceDev?: boolean, baseUrl?: string } = {}) {
|
||||
this.baseUrl = (process.env.APP_URL || '') + baseUrl
|
||||
|
||||
if (forceDev != null) {
|
||||
this.dev = forceDev
|
||||
} else {
|
||||
this.dev = process.env.NODE_ENV === 'development'
|
||||
}
|
||||
|
||||
this.rpc = new Proxy({}, {
|
||||
get: this.__rpcCall,
|
||||
has: this.__checkCall,
|
||||
ownKeys: this.__listCalls,
|
||||
delete: () => {}
|
||||
})
|
||||
|
||||
if (this.dev) {
|
||||
this.updateCalls()
|
||||
}
|
||||
}
|
||||
|
||||
withCookies = (h: string) => {
|
||||
this.cookieHeader = h
|
||||
return this.rpc
|
||||
}
|
||||
|
||||
async updateCalls () {
|
||||
// this is for development only. doing in prod is probably dumb.
|
||||
const rsp = await superagent.get(this.baseUrl)
|
||||
if (rsp.status !== 200) {
|
||||
console.error(rsp)
|
||||
return
|
||||
}
|
||||
|
||||
const { hash, available } = rsp.body
|
||||
|
||||
this.__rpcAvailable = available
|
||||
if (this.firstKnownHash == null) {
|
||||
this.firstKnownHash = hash
|
||||
}
|
||||
|
||||
this.recentHash = hash
|
||||
|
||||
// just kinda prefill. none of these get called anyway.
|
||||
// and don't matter in prod either.
|
||||
for (let { name } of available) {
|
||||
this.rpc[name] = async () => {}
|
||||
}
|
||||
}
|
||||
|
||||
async call (fn: string, ...args: any[]): mixed {
|
||||
const req: RPCRequest = { fn, args }
|
||||
const rq = superagent.post(this.baseUrl)
|
||||
if (this.cookieHeader != null) {
|
||||
rq.cookies = this.cookieHeader
|
||||
}
|
||||
const rsp = await rq.send(req).ok(() => true)
|
||||
const body: RPCResponse = rsp.body
|
||||
// console.log(body)
|
||||
if (body.error === true) {
|
||||
console.error(body)
|
||||
throw RPCError.fromResponse(body, rsp.status)
|
||||
}
|
||||
|
||||
if (body.hash != null) {
|
||||
if (this.firstKnownHash == null) {
|
||||
this.firstKnownHash = body.hash
|
||||
}
|
||||
|
||||
this.recentHash = body.hash
|
||||
|
||||
if (this.firstKnownHash !== this.recentHash) {
|
||||
this.updateCalls()
|
||||
}
|
||||
}
|
||||
|
||||
return body.response
|
||||
}
|
||||
|
||||
// PROXY HANDLERS
|
||||
__rpcCall = (_: {}, fn: string) => this.call.bind(this, fn)
|
||||
__checkCall = (_: {}, fn: string) => this.dev ? this.__listCalls(_).includes(fn) : true
|
||||
__listCalls = (_: {}): string[] => this.__rpcAvailable.map(x => x.name)
|
||||
}
|
4
packages/roleypoly-ui/setupTests.js
Normal file
4
packages/roleypoly-ui/setupTests.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { configure } from 'enzyme'
|
||||
import Adapter from 'enzyme-adapter-react-16'
|
||||
|
||||
configure({ adapter: new Adapter() })
|
21
packages/roleypoly-ui/static/discord-logo.svg
Normal file
21
packages/roleypoly-ui/static/discord-logo.svg
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?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>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
packages/roleypoly-ui/static/favicon.png
Normal file
BIN
packages/roleypoly-ui/static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
35
packages/roleypoly-ui/static/flags.svg
Normal file
35
packages/roleypoly-ui/static/flags.svg
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="3372px" height="900px" viewBox="0 0 3372 900" 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>Trans</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<rect id="path-1" x="0" y="0" width="1600" height="900" rx="100"></rect>
|
||||
<rect id="path-3" x="1772" y="0" width="1600" height="900" rx="100"></rect>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<g id="Rectangle-5"></g>
|
||||
<g id="Trans" mask="url(#mask-2)">
|
||||
<rect id="Rectangle" fill="#55CDFC" x="0" y="0" width="1600" height="900"></rect>
|
||||
<rect id="Rectangle-2" fill="#F7A8B8" x="0" y="170" width="1600" height="560"></rect>
|
||||
<rect id="Rectangle-3" fill="#FFFFFF" x="0" y="350" width="1600" height="200"></rect>
|
||||
</g>
|
||||
<mask id="mask-4" fill="white">
|
||||
<use xlink:href="#path-3"></use>
|
||||
</mask>
|
||||
<g id="Rectangle-5"></g>
|
||||
<g id="Geyy" mask="url(#mask-4)">
|
||||
<g transform="translate(1772.000000, 0.000000)" id="Rectangle-4">
|
||||
<rect fill="#F9238B" x="0" y="0" width="1600" height="151.006711"></rect>
|
||||
<rect fill="#FB7B04" x="0" y="150" width="1600" height="151.006711"></rect>
|
||||
<rect fill="#FFCA66" x="0" y="300" width="1600" height="151.006711"></rect>
|
||||
<rect fill="#00B289" x="0" y="450" width="1600" height="151.006711"></rect>
|
||||
<rect fill="#5A38B5" x="0" y="598.993289" width="1600" height="151.006711"></rect>
|
||||
<rect fill="#B413F5" x="0" y="748.993289" width="1600" height="151.006711"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
BIN
packages/roleypoly-ui/static/social.png
Normal file
BIN
packages/roleypoly-ui/static/social.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
58
packages/roleypoly-ui/stores/currentServer.js
Normal file
58
packages/roleypoly-ui/stores/currentServer.js
Normal file
File diff suppressed because one or more lines are too long
107
packages/roleypoly-ui/stores/roles.js
Normal file
107
packages/roleypoly-ui/stores/roles.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
// @flow
|
||||
// import { action } from './servers'
|
||||
import { namespaceConfig } from 'fast-redux'
|
||||
// $FlowFixMe
|
||||
import { OrderedMap, OrderedSet, Set } from 'immutable'
|
||||
import { getCurrentServerState, type ServerState } from './currentServer'
|
||||
|
||||
type RenderedRole = {
|
||||
id: string,
|
||||
name: string,
|
||||
color: string
|
||||
}
|
||||
|
||||
type RenderedCategory = {
|
||||
id: string,
|
||||
name: string,
|
||||
_roles?: RenderedRole[],
|
||||
roles: string[],
|
||||
type: 'single' | 'multi',
|
||||
hidden: boolean,
|
||||
position?: number,
|
||||
}
|
||||
|
||||
const MOCK_DATA: RenderedCategory = {
|
||||
id: '00000',
|
||||
name: 'Placeholder Category',
|
||||
hidden: false,
|
||||
type: 'multi',
|
||||
roles: [ '00000' ],
|
||||
_roles: OrderedSet([
|
||||
{ id: '00000', name: 'Placeholder Role', color: '#ff00ff' }
|
||||
])
|
||||
}
|
||||
|
||||
const DEFAULT_VIEW_STATE = { server: '000', invalidated: true, categories: [ MOCK_DATA ], selected: Set<string>([]), originalSelected: Set<string>([]), dirty: false }
|
||||
export type ViewState = typeof DEFAULT_VIEW_STATE
|
||||
|
||||
export const { action, getState: getCategoryViewState } = namespaceConfig('currentCategoryView', DEFAULT_VIEW_STATE)
|
||||
|
||||
export const toggleRole = action('toggleRole', (state: ViewState, role: string, nextState: boolean) => {
|
||||
let selected = state.selected
|
||||
|
||||
if (nextState === true) {
|
||||
selected = selected.add(role)
|
||||
} else {
|
||||
selected = selected.delete(role)
|
||||
}
|
||||
|
||||
const dirty = !selected.equals(state.originalSelected)
|
||||
|
||||
return {
|
||||
...state,
|
||||
selected,
|
||||
dirty
|
||||
}
|
||||
})
|
||||
|
||||
const getUncategorized = (roleMap: OrderedMap<RenderedRole>, allCategories: Set): RenderedCategory => {
|
||||
const roles = roleMap.keySeq().toSet()
|
||||
const knownRoles = allCategories.map(v => v.roles).flatMap(v => v)
|
||||
const rolesLeft = roles.subtract(knownRoles)
|
||||
|
||||
// console.log(Map({ roles, knownRoles, rolesLeft }).toJS())
|
||||
return {
|
||||
id: 'Uncategorized',
|
||||
name: 'Uncategorized',
|
||||
position: -1,
|
||||
roles: rolesLeft,
|
||||
_roles: rolesLeft.map(v => roleMap.get(v)).filter(v => v != null).sortBy(v => -v.position),
|
||||
hidden: true,
|
||||
type: 'multi'
|
||||
}
|
||||
}
|
||||
|
||||
export const updateCurrentView = action('updateCurrentView', (state, data) => ({ ...state, ...data }))
|
||||
export const invalidateView = action('invalidateView', (state, data) => ({ ...state, invalidated: true }))
|
||||
|
||||
export const renderRoles = (id: string) => (dispatch: *, getState: *) => {
|
||||
const active = getCategoryViewState(getState())
|
||||
const current: ServerState = getCurrentServerState(getState(), id)
|
||||
if (!active.invalidated && current.id === active.id && active.id === id) {
|
||||
return
|
||||
}
|
||||
|
||||
const { roles, categories } = current
|
||||
if (roles == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// console.log({ roles, categories })
|
||||
|
||||
const roleMap: OrderedMap<RenderedRole> = roles.reduce((acc: OrderedMap<RenderedRole>, r) => acc.set(r.id, r), OrderedMap())
|
||||
|
||||
let render = OrderedSet()
|
||||
for (let catId in categories) {
|
||||
const category = categories[catId]
|
||||
category._roles = OrderedSet(
|
||||
category.roles.map(r => roleMap.get(r))
|
||||
).filter(role => role != null).sortBy(role => role ? -(role.position || 0) : 0)
|
||||
render = render.add(category)
|
||||
}
|
||||
// console.log({id})
|
||||
render = render.add(getUncategorized(roleMap, render.toSet()))
|
||||
render = render.sortBy(h => (h.position) ? h.position : h.name)
|
||||
|
||||
dispatch(updateCurrentView({ server: id, categories: render, invalidated: false, selected: Set(current.gm?.roles), originalSelected: Set(current.gm?.roles) }))
|
||||
}
|
16
packages/roleypoly-ui/stores/servers.js
Normal file
16
packages/roleypoly-ui/stores/servers.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
// @flow
|
||||
import { namespaceConfig } from 'fast-redux'
|
||||
// import { Map } from 'immutable'
|
||||
|
||||
const DEFAULT_STATE = {}
|
||||
|
||||
export type ServersState = typeof DEFAULT_STATE
|
||||
|
||||
export const { action, getState: getServerState } = namespaceConfig('servers', DEFAULT_STATE)
|
||||
|
||||
export const updateServers = action('updateServers', (state: ServersState, serverData) => ({
|
||||
...state,
|
||||
servers: serverData
|
||||
}))
|
||||
|
||||
export const updateSingleServer = action('updateSingleServer', (state, data, server) => ({ ...state, [server]: data }))
|
32
packages/roleypoly-ui/stores/user.js
Normal file
32
packages/roleypoly-ui/stores/user.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
// @flow
|
||||
import { namespaceConfig } from 'fast-redux'
|
||||
|
||||
export type User = {
|
||||
id: string,
|
||||
username: string,
|
||||
discriminator: string,
|
||||
avatar: string,
|
||||
nicknameCache: {
|
||||
[server: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export type UserState = {
|
||||
currentUser: User | null,
|
||||
userCache: {
|
||||
[id: string]: User
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: UserState = {
|
||||
currentUser: null,
|
||||
userCache: {}
|
||||
}
|
||||
|
||||
export const {
|
||||
action, getState: getUserStore
|
||||
} = namespaceConfig('userStore', DEFAULT_STATE)
|
||||
|
||||
export const getCurrentUser = () => async (dispatch: Function) => {
|
||||
|
||||
}
|
10
packages/roleypoly-ui/types.js
Normal file
10
packages/roleypoly-ui/types.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
// @flow
|
||||
import type { ServerSlug as BackendServerSlug } from '../services/presentation'
|
||||
import type Router from 'next/router'
|
||||
|
||||
export type PageProps = {
|
||||
router: Router,
|
||||
dispatch: (...any) => mixed
|
||||
}
|
||||
|
||||
export type ServerSlug = BackendServerSlug
|
Loading…
Add table
Add a link
Reference in a new issue