typescript(lunargame/client): progress on user page
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
parent
2bc7d4e3e1
commit
db4f749cd3
|
@ -1,17 +1,17 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-typescript"
|
||||
'@babel/preset-env',
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript'
|
||||
],
|
||||
plugins: [
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
["@babel/plugin-proposal-decorators", { legacy: true }],
|
||||
["@babel/plugin-proposal-class-properties", { loose: true }],
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
['@babel/plugin-proposal-decorators', { legacy: true }],
|
||||
['@babel/plugin-proposal-class-properties', { loose: true }]
|
||||
],
|
||||
env: {
|
||||
test: {
|
||||
presets: [["@babel/preset-env", { targets: { node: "current" } }]],
|
||||
},
|
||||
},
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
51
typescript/lunargame/client/package-lock.json
generated
51
typescript/lunargame/client/package-lock.json
generated
|
@ -736,18 +736,6 @@
|
|||
"@babel/helper-plugin-utils": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-runtime": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.5.0.tgz",
|
||||
"integrity": "sha512-LmPIZOAgTLl+86gR9KjLXex6P/lRz1fWEjTz6V6QZMmKie51ja3tvzdwORqhHc4RWR8TcZ5pClpRWs0mlaA2ng==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
"resolve": "^1.8.1",
|
||||
"semver": "^5.5.1"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-shorthand-properties": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz",
|
||||
|
@ -1638,6 +1626,39 @@
|
|||
"object.assign": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"babel-plugin-transform-runtime": {
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz",
|
||||
"integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"babel-runtime": "^6.22.0"
|
||||
}
|
||||
},
|
||||
"babel-regenerator-runtime": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-regenerator-runtime/-/babel-regenerator-runtime-6.5.0.tgz",
|
||||
"integrity": "sha1-DkHNHJ+ARCRm8BXHSf/4upj44RA=",
|
||||
"dev": true
|
||||
},
|
||||
"babel-runtime": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-js": "^2.4.0",
|
||||
"regenerator-runtime": "^0.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"regenerator-runtime": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
|
@ -2489,6 +2510,12 @@
|
|||
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
|
||||
"dev": true
|
||||
},
|
||||
"core-js": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
|
||||
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==",
|
||||
"dev": true
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.1.4.tgz",
|
||||
|
|
|
@ -12,12 +12,13 @@
|
|||
"@babel/plugin-proposal-class-properties": "^7.5.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.4.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-transform-runtime": "^7.5.0",
|
||||
"@babel/preset-env": "^7.5.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/preset-typescript": "^7.3.3",
|
||||
"@types/react-router-dom": "^4.3.4",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-regenerator-runtime": "^6.5.0",
|
||||
"css-loader": "^3.0.0",
|
||||
"html-webpack-inline-source-plugin": "0.0.10",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
export const Login = () => {
|
||||
return <h1>This is the login component</h1>
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
export const Signup = () => {
|
||||
return <h1>This is the signup component</h1>
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { Singleton } from '@eix/utils'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
|
||||
export interface DialogAction {
|
||||
name: string
|
||||
variant: 'standard' | 'contained' | 'outlined'
|
||||
callback: Function
|
||||
}
|
||||
|
||||
export interface Dialog {
|
||||
title: string
|
||||
message: string
|
||||
actions: DialogAction[]
|
||||
onClose: (event: unknown) => void
|
||||
}
|
||||
|
||||
@Singleton
|
||||
export class DialogManager {
|
||||
public active = new BehaviorSubject<Dialog | null>(null)
|
||||
public queue: Dialog[] = []
|
||||
|
||||
public add(dialog: Dialog) {
|
||||
if (this.active.value !== null) {
|
||||
this.queue.push(dialog)
|
||||
} else {
|
||||
this.activate(dialog)
|
||||
}
|
||||
}
|
||||
|
||||
private activate(dialog: Dialog) {
|
||||
this.active.next({
|
||||
...dialog,
|
||||
onClose: (event: unknown) => {
|
||||
dialog.onClose(event)
|
||||
|
||||
if (this.queue.length) {
|
||||
const newDialog = this.queue.shift()
|
||||
this.activate(newDialog)
|
||||
} else {
|
||||
this.active.next(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import React from 'react'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||
import { DialogManager } from '../classes/DialogManager'
|
||||
import { useObservable } from 'rxjs-hooks'
|
||||
import DialogActions from '@material-ui/core/DialogActions'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
import DialogContentText from '@material-ui/core/DialogContentText'
|
||||
import { Button } from '@material-ui/core'
|
||||
|
||||
const manager = new DialogManager()
|
||||
|
||||
export const BaseDialogRenderer = () => {
|
||||
const activeDialog = useObservable(() => manager.active)
|
||||
|
||||
if (activeDialog !== null) {
|
||||
return (
|
||||
<Dialog
|
||||
onClose={activeDialog.onClose}
|
||||
open={true}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{activeDialog.title}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
{activeDialog.message}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
{activeDialog.actions.map((action, index) => {
|
||||
return (
|
||||
<Button
|
||||
onClick={event => {
|
||||
action.callback()
|
||||
activeDialog.onClose(event)
|
||||
}}
|
||||
key={index}
|
||||
>
|
||||
{action.name}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
return <></>
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { Subscription, BehaviorSubject } from 'rxjs'
|
||||
|
||||
export interface FormFieldSnapshot {
|
||||
passing: boolean
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
export interface FormValidator {
|
||||
validate: (input: string) => FormFieldSnapshot
|
||||
}
|
||||
|
||||
export class FormField {
|
||||
private subscription: Subscription
|
||||
|
||||
public constructor(
|
||||
public name: string,
|
||||
public input: BehaviorSubject<string>,
|
||||
public output: BehaviorSubject<FormFieldSnapshot>,
|
||||
private validators: FormValidator[]
|
||||
) {
|
||||
this.subscription = this.input.subscribe((text: string) => {
|
||||
for (const validator of this.validators) {
|
||||
const result = validator.validate(text)
|
||||
|
||||
if (!result.passing) {
|
||||
return this.output.next(result)
|
||||
}
|
||||
}
|
||||
|
||||
this.output.next({
|
||||
passing: true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
public passes() {
|
||||
return this.output.value.passing
|
||||
}
|
||||
|
||||
public get value() {
|
||||
return this.input.value
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { FormField } from './FormField'
|
||||
import { BaseServer } from '../../../../modules/network/classes/BaseServer'
|
||||
|
||||
const server = new BaseServer()
|
||||
|
||||
export class FormManager {
|
||||
constructor(public fields: FormField[]) {}
|
||||
|
||||
public collect() {
|
||||
const data: Record<string, string> = {}
|
||||
|
||||
for (const { name, value } of this.fields) {
|
||||
data[name] = value
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
public async submit(url: string) {
|
||||
if (this.validate()) {
|
||||
return server.request(url, 'POST', this.collect())
|
||||
}
|
||||
}
|
||||
|
||||
public validate() {
|
||||
for (const field of this.fields) {
|
||||
if (!field.passes()) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
for (const field of this.fields) {
|
||||
field.dispose()
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { FormFieldSnapshot } from '../classes/FormField'
|
||||
import { useObservable } from 'rxjs-hooks'
|
||||
|
||||
export interface TextFieldWithErrorsProps {
|
||||
name: string
|
||||
type: string
|
||||
input: BehaviorSubject<string>
|
||||
output: BehaviorSubject<FormFieldSnapshot>
|
||||
className: string
|
||||
}
|
||||
|
||||
const good = '✅'
|
||||
|
||||
export const TextFieldWithErrors = (props: TextFieldWithErrorsProps) => {
|
||||
const outputSnapshot = useObservable(() => props.output, {
|
||||
passing: true,
|
||||
errorMessage: good
|
||||
})
|
||||
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
className={props.className}
|
||||
label={props.name}
|
||||
type={props.type}
|
||||
error={!outputSnapshot.passing}
|
||||
helperText={
|
||||
outputSnapshot.passing ? good : outputSnapshot.errorMessage
|
||||
}
|
||||
onChange={event => {
|
||||
props.input.next(event.target.value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
import React from 'react'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
import DialogActions from '@material-ui/core/DialogActions'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
import DialogContentText from '@material-ui/core/DialogContentText'
|
||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||
import { FormValidator, FormField } from '../classes/FormField'
|
||||
import { Button } from '@material-ui/core'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { FormManager } from '../classes/FormManager'
|
||||
import { TextFieldWithErrors } from '../components/TextFieldWithError'
|
||||
import { makeStyles, Theme } from '@material-ui/core/styles'
|
||||
|
||||
export interface TextFieldData {
|
||||
name: string
|
||||
type: string
|
||||
validators: FormValidator[]
|
||||
}
|
||||
|
||||
export interface ModalProps {
|
||||
open: boolean
|
||||
onClose: Function
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
field: {
|
||||
marginTop: theme.spacing(2)
|
||||
}
|
||||
}))
|
||||
|
||||
export const createFormModal = (
|
||||
title: string,
|
||||
description: string,
|
||||
url: string,
|
||||
fields: TextFieldData[],
|
||||
onSubmit?: (data: unknown) => void
|
||||
) => {
|
||||
const formFields = fields.map(
|
||||
field =>
|
||||
new FormField(
|
||||
field.name,
|
||||
new BehaviorSubject(''),
|
||||
new BehaviorSubject({
|
||||
passing: true
|
||||
}),
|
||||
field.validators
|
||||
)
|
||||
)
|
||||
|
||||
const formManager = new FormManager(formFields)
|
||||
|
||||
return (props: ModalProps) => {
|
||||
const handleClose = (event: unknown) => {
|
||||
props.onClose(event)
|
||||
}
|
||||
|
||||
const classes = useStyles()
|
||||
|
||||
const textFields = fields.map((field, index) => {
|
||||
const fieldObject = formFields[index]
|
||||
|
||||
return (
|
||||
<TextFieldWithErrors
|
||||
className={classes.field}
|
||||
name={field.name}
|
||||
type={field.type}
|
||||
input={fieldObject.input}
|
||||
output={fieldObject.output}
|
||||
key={index}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
fullWidth={true}
|
||||
maxWidth="sm"
|
||||
open={props.open}
|
||||
aria-labelledby={title}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<DialogTitle id={title}>{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>{description}</DialogContentText>
|
||||
{textFields}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={event => {
|
||||
if (formManager.validate()) {
|
||||
formManager.submit(url).then(data => {
|
||||
handleClose(event)
|
||||
|
||||
if (onSubmit) onSubmit(data)
|
||||
})
|
||||
}
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { FormValidator, FormFieldSnapshot } from '../classes/FormField'
|
||||
|
||||
export interface ValidatorCondition {
|
||||
regex: RegExp
|
||||
message: string
|
||||
}
|
||||
|
||||
export const createValidator = (
|
||||
...conditions: ValidatorCondition[]
|
||||
): { new (): FormValidator } =>
|
||||
class implements FormValidator {
|
||||
public validate(text: string): FormFieldSnapshot {
|
||||
for (const condition of conditions) {
|
||||
if (!condition.regex.test(text)) {
|
||||
return {
|
||||
passing: false,
|
||||
errorMessage: condition.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
passing: true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { createValidator } from '../helpers/createValidator'
|
||||
|
||||
export const lengthValidator = (min: number, max: number) =>
|
||||
createValidator({
|
||||
regex: new RegExp(`^.{${min},${max}}$`),
|
||||
message: `Must contain between ${min} and ${max} characters.`
|
||||
})
|
|
@ -0,0 +1,38 @@
|
|||
import { decorable } from '@eix/utils'
|
||||
import { areEqual } from '../helpers/areEqual'
|
||||
|
||||
export interface ObjectArgumentsRef<T> {
|
||||
instance: T
|
||||
arguments: unknown[]
|
||||
}
|
||||
|
||||
export const cacheInstances = (argCount = Infinity) => {
|
||||
const objectMemory: ObjectArgumentsRef<unknown>[] = []
|
||||
|
||||
return <T extends { new (...args: any[]): { init: () => void } }>(
|
||||
toDecorate: T
|
||||
) => {
|
||||
return class extends toDecorate {
|
||||
constructor(...args: any[]) {
|
||||
super(...args)
|
||||
|
||||
const sliceParameters =
|
||||
argCount === Infinity ? [0] : [0, argCount]
|
||||
const argumentsToStore = args.slice(...sliceParameters)
|
||||
|
||||
const reference = objectMemory.find(instance =>
|
||||
areEqual(argumentsToStore, instance.arguments)
|
||||
)
|
||||
|
||||
if (reference) return reference.instance as this
|
||||
else
|
||||
objectMemory.push({
|
||||
instance: this,
|
||||
arguments: argumentsToStore
|
||||
})
|
||||
|
||||
if (super.init) super.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
export const areEqual = <T extends {}>(first: T, last: T) => {
|
||||
for (const key of Object.keys(first)) {
|
||||
// for ts to shut up
|
||||
const typedKey = key as keyof T
|
||||
|
||||
if (first[typedKey] !== last[typedKey]) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import { Account } from '../types/Account'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { Singleton } from '@eix/utils'
|
||||
import { defaultAvatar } from '../constants'
|
||||
|
||||
@Singleton
|
||||
export class BaseServer {
|
||||
public account = new BehaviorSubject<Account | null>(null)
|
||||
|
||||
constructor() {
|
||||
// mock account for now
|
||||
// this.account.next({
|
||||
// name: 'Mock',
|
||||
// email: 'mock@somethng.io',
|
||||
// avatar: defaultAvatar,
|
||||
// description: 'Just a random mock account',
|
||||
// uid: '1234',
|
||||
// verified: true
|
||||
// })
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { render } from 'react-dom'
|
||||
import React from 'react'
|
||||
import { App } from './common/core/components/App'
|
||||
import { App } from './modules/core/components/App'
|
||||
|
||||
render(<App />, document.querySelector('#app'))
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { createFormModal } from '../../../common/dom/forms/helpers/createFormModal'
|
||||
import {
|
||||
emailValidatorList,
|
||||
passwordValidatorList
|
||||
} from '../validators/authValidators'
|
||||
import { Account } from '../../network/types/Account'
|
||||
import { updateAccount } from '../../helpers/updateAccount'
|
||||
|
||||
export const LoginModal = createFormModal(
|
||||
'Login',
|
||||
`To subscribe to this website, please enter you r email address here. We will send updates occasionally.`,
|
||||
'auth/login',
|
||||
[
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
validators: emailValidatorList()
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
validators: passwordValidatorList()
|
||||
}
|
||||
],
|
||||
updateAccount
|
||||
)
|
|
@ -0,0 +1,27 @@
|
|||
import React, { useState } from 'react'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import { ModalProps } from '../../../common/dom/forms/helpers/createFormModal'
|
||||
|
||||
export interface ModalButtonProps {
|
||||
modal: (props: ModalProps) => JSX.Element
|
||||
children: string
|
||||
className?: string
|
||||
contained?: boolean
|
||||
}
|
||||
|
||||
export const ModalButton = (props: ModalButtonProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className={props.className}
|
||||
variant={props.contained ? 'contained' : 'text'}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
<props.modal open={open} onClose={() => setOpen(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { createFormModal } from '../../../common/dom/forms/helpers/createFormModal'
|
||||
import {
|
||||
usernameValidatorList,
|
||||
emailValidatorList,
|
||||
passwordValidatorList
|
||||
} from '../validators/authValidators'
|
||||
import { updateAccount } from '../../helpers/updateAccount'
|
||||
import { Account } from '../../network/types/Account'
|
||||
import { DialogManager } from '../../../common/dom/dialogs/classes/DialogManager'
|
||||
|
||||
const dialogManager = new DialogManager()
|
||||
|
||||
export const SignupModal = createFormModal(
|
||||
'Signup',
|
||||
`To create an account you need to provide an username, email and a password.`,
|
||||
'auth/create',
|
||||
[
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
validators: usernameValidatorList()
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
validators: emailValidatorList()
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
validators: passwordValidatorList()
|
||||
}
|
||||
],
|
||||
(data: Account) => {
|
||||
updateAccount(data)
|
||||
dialogManager.add({
|
||||
title: 'Email verification',
|
||||
message:
|
||||
'To unlock the full set of features offered by lunrabox, please verify your email',
|
||||
actions: [],
|
||||
onClose: () => {}
|
||||
})
|
||||
}
|
||||
)
|
|
@ -2,9 +2,10 @@ import React from 'react'
|
|||
import { useObservable } from 'rxjs-hooks'
|
||||
import { BaseServer } from '../../network/classes/BaseServer'
|
||||
import Avatar from '@material-ui/core/Avatar'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import { makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { LoginModal } from './LoginModal'
|
||||
import { ModalButton } from './ModalButton'
|
||||
import { SignupModal } from './SignupModal'
|
||||
|
||||
const { account } = new BaseServer()
|
||||
|
||||
|
@ -16,18 +17,18 @@ const useStyles = makeStyles((theme: Theme) => ({
|
|||
|
||||
export const TopbarAccount = (props: unknown) => {
|
||||
const accountSnapshot = useObservable(() => account, null)
|
||||
|
||||
const classes = useStyles(props)
|
||||
|
||||
const signup = (
|
||||
<>
|
||||
<Button>
|
||||
<Link to="/signup">Sign up</Link>
|
||||
</Button>
|
||||
|
||||
<Button variant="contained" className={classes.loginButton}>
|
||||
<Link to="/login"> Login</Link>
|
||||
</Button>
|
||||
<ModalButton modal={SignupModal}>Sign up</ModalButton>
|
||||
<ModalButton
|
||||
modal={LoginModal}
|
||||
className={classes.loginButton}
|
||||
contained
|
||||
>
|
||||
Log in
|
||||
</ModalButton>
|
||||
</>
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { createValidator } from '../../../common/dom/forms/helpers/createValidator'
|
||||
|
||||
export const alphaNumericValidator = createValidator({
|
||||
regex: /^[a-zA-Z0-9_]*$/,
|
||||
message: 'Must only contain alpha-numeric characters or underscores.'
|
||||
})
|
|
@ -0,0 +1,20 @@
|
|||
import { fieldLengthValidator } from './fieldLength'
|
||||
import { emailValidator } from './emailValidator'
|
||||
import { requiredValidator } from './requiredValidator'
|
||||
import { alphaNumericValidator } from './alphaNumeric'
|
||||
|
||||
export const usernameValidatorList = () => [
|
||||
new requiredValidator(),
|
||||
new fieldLengthValidator(),
|
||||
new alphaNumericValidator()
|
||||
]
|
||||
export const emailValidatorList = () => [
|
||||
new requiredValidator(),
|
||||
new fieldLengthValidator(),
|
||||
new emailValidator()
|
||||
]
|
||||
export const passwordValidatorList = () => [
|
||||
new requiredValidator(),
|
||||
new fieldLengthValidator(),
|
||||
new alphaNumericValidator()
|
||||
]
|
|
@ -0,0 +1,6 @@
|
|||
import { createValidator } from '../../../common/dom/forms/helpers/createValidator'
|
||||
|
||||
export const emailValidator = createValidator({
|
||||
regex: /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/,
|
||||
message: 'Must be a valid email'
|
||||
})
|
|
@ -0,0 +1,3 @@
|
|||
import { lengthValidator } from '../../../common/dom/forms/validators/lengthValidator'
|
||||
|
||||
export const fieldLengthValidator = lengthValidator(3, 30)
|
|
@ -0,0 +1,6 @@
|
|||
import { createValidator } from '../../../common/dom/forms/helpers/createValidator'
|
||||
|
||||
export const requiredValidator = createValidator({
|
||||
regex: /^.{1,}$/,
|
||||
message: 'Field is required'
|
||||
})
|
|
@ -8,6 +8,7 @@ import { theme as MuiTheme } from '../data/Theme'
|
|||
import { ThemeProvider as Theme } from '@material-ui/styles'
|
||||
import { AppBar } from './AppBar'
|
||||
import { Body } from './Body'
|
||||
import { BaseDialogRenderer } from '../../../common/dom/dialogs/components/BaseDialogRenderer'
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
|
@ -16,6 +17,7 @@ export const App = () => {
|
|||
<Router>
|
||||
<AppBar />
|
||||
<Body />
|
||||
<BaseDialogRenderer />
|
||||
</Router>
|
||||
</Theme>
|
||||
)
|
|
@ -2,8 +2,6 @@ import React from 'react'
|
|||
import { SiddebarRoutes } from './SidebarRouteList'
|
||||
import { makeStyles } from '@material-ui/styles'
|
||||
import { Route } from 'react-router-dom'
|
||||
import { Signup } from '../../account/components/Signup'
|
||||
import { Login } from '../../account/components/Login'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
|
@ -17,9 +15,6 @@ export const Body = (props: unknown) => {
|
|||
return (
|
||||
<div className={classes.root}>
|
||||
<SiddebarRoutes />
|
||||
|
||||
<Route component={Signup} path="/signup" />
|
||||
<Route component={Login} path="/login" />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -2,6 +2,7 @@ import React from 'react'
|
|||
import Typography from '@material-ui/core/Typography'
|
||||
import Divider from '@material-ui/core/Divider'
|
||||
import { makeStyles, Theme } from '@material-ui/core/styles'
|
||||
import { BaseServer } from '../../network/classes/BaseServer'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
root: {
|
||||
|
@ -10,6 +11,9 @@ const useStyles = makeStyles((theme: Theme) => ({
|
|||
divider: {
|
||||
marginBottom: theme.spacing(2),
|
||||
marginTop: theme.spacing(2)
|
||||
},
|
||||
a: {
|
||||
color: '#0000ff'
|
||||
}
|
||||
}))
|
||||
|
||||
|
@ -24,9 +28,34 @@ export const Home = (props: unknown) => {
|
|||
|
||||
<Typography variant="h6" color="textSecondary">
|
||||
Lunarbox is a game streaming website for games made with the eix
|
||||
game engine. The project is open source, and right now it's
|
||||
game engine. The project is open source, and right now also
|
||||
unusable.
|
||||
</Typography>
|
||||
|
||||
<br />
|
||||
|
||||
<Typography variant="h6" color="textSecondary">
|
||||
The project is open source on{' '}
|
||||
<a
|
||||
className={classes.a}
|
||||
href="https://github.com/Mateiadrielrafael/lunarbox-client"
|
||||
>
|
||||
github
|
||||
</a>
|
||||
.
|
||||
</Typography>
|
||||
|
||||
{/* Todo: remove */}
|
||||
<button
|
||||
onClick={async () => {
|
||||
const server = new BaseServer()
|
||||
await server.request('account/uid', 'DELETE')
|
||||
|
||||
server.account.next(null)
|
||||
}}
|
||||
>
|
||||
logout
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { BaseServer } from '../network/classes/BaseServer'
|
||||
import { Account } from '../network/types/Account'
|
||||
|
||||
const server = new BaseServer()
|
||||
|
||||
export const updateAccount = (data: Account | null) => {
|
||||
server.account.next(data)
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { Account } from '../types/Account'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { Singleton } from '@eix/utils'
|
||||
import { Response } from '../types/Response'
|
||||
|
||||
@Singleton
|
||||
export class BaseServer {
|
||||
public account = new BehaviorSubject<Account | null>(null)
|
||||
public path = 'http://localhost:8000'
|
||||
|
||||
constructor() {
|
||||
this.refreshAccount()
|
||||
}
|
||||
|
||||
public async refreshAccount(url = 'account', method = 'GET', body = {}) {
|
||||
try {
|
||||
const account = await this.request<Account>(url, method, body)
|
||||
this.account.next(account)
|
||||
return account
|
||||
} catch (err) {
|
||||
this.account.next(null)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
public async request<T>(
|
||||
url: string,
|
||||
method = 'GET',
|
||||
body = {}
|
||||
): Promise<T> {
|
||||
const noBody = ['GET', 'DELETE']
|
||||
|
||||
const response = await fetch(`${this.path}/${url}`, {
|
||||
...(noBody.indexOf(method) === -1
|
||||
? { body: JSON.stringify(body) }
|
||||
: {}),
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Credentials': 'true'
|
||||
},
|
||||
method,
|
||||
credentials: 'include'
|
||||
})
|
||||
const parsed: Response<T> = await response.json()
|
||||
const status = response.status
|
||||
|
||||
if (status !== 200) {
|
||||
console.warn(parsed.message)
|
||||
throw new Error(parsed.message)
|
||||
}
|
||||
|
||||
return parsed.data
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { cacheInstances } from '../../../common/lang/objects/decorators/cacheInstances'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { AccountPublicData } from '../types/AccountPublicData'
|
||||
import { BaseServer } from './BaseServer'
|
||||
|
||||
@cacheInstances()
|
||||
export class PublicAccount {
|
||||
public static server = new BaseServer()
|
||||
public account = new BehaviorSubject<AccountPublicData | null>(null)
|
||||
|
||||
public constructor(public name: string) {}
|
||||
|
||||
public init() {
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
public async refresh() {
|
||||
try {
|
||||
const account = await PublicAccount.server.request<
|
||||
AccountPublicData
|
||||
>(`user/name/${this.name}`)
|
||||
|
||||
this.account.next(account)
|
||||
return account
|
||||
} catch {
|
||||
this.account.next(null)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { AccountPublicData } from './AccountPublicData'
|
||||
|
||||
export interface Account extends AccountPublicData {
|
||||
uid: string
|
||||
verified: boolean
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
export interface Account {
|
||||
name: string
|
||||
export interface AccountPublicData {
|
||||
email: string
|
||||
avatar: string
|
||||
name: string
|
||||
description: string
|
||||
uid: string
|
||||
verified: boolean
|
||||
avatar: string
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface Response<T> {
|
||||
data: T
|
||||
message: string
|
||||
}
|
|
@ -4,7 +4,8 @@
|
|||
"esModuleInterop": true,
|
||||
"jsx": "preserve",
|
||||
"noImplicitAny": true,
|
||||
"experimentalDecorators": true
|
||||
"experimentalDecorators": true,
|
||||
"target": "esnext"
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["src"]
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
const { resolve } = require("path")
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin")
|
||||
const HtmlWebpackInlineSourcePlugin = require("html-webpack-inline-source-plugin")
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
|
||||
const OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin")
|
||||
const webpackMerge = require("webpack-merge")
|
||||
const { resolve } = require('path')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
const webpackMerge = require('webpack-merge')
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production"
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
const projectRoot = resolve(__dirname)
|
||||
const sourceFolder = resolve(projectRoot, "src")
|
||||
const buildFolder = resolve(projectRoot, "dist")
|
||||
const htmlTemplateFile = resolve(sourceFolder, "index.html")
|
||||
const sourceFolder = resolve(projectRoot, 'src')
|
||||
const buildFolder = resolve(projectRoot, 'dist')
|
||||
const htmlTemplateFile = resolve(sourceFolder, 'index.html')
|
||||
|
||||
const babelRule = {
|
||||
test: /\.(js|tsx?)$/,
|
||||
use: "babel-loader",
|
||||
use: 'babel-loader'
|
||||
}
|
||||
|
||||
const sassRule = {
|
||||
|
@ -23,62 +23,61 @@ const sassRule = {
|
|||
isProduction
|
||||
? MiniCssExtractPlugin.loader
|
||||
: {
|
||||
loader: "style-loader",
|
||||
loader: 'style-loader',
|
||||
options: {
|
||||
singleton: true,
|
||||
},
|
||||
},
|
||||
{ loader: "css-loader" },
|
||||
{
|
||||
loader: "sass-loader",
|
||||
options: {
|
||||
includePaths: [sourceFolder],
|
||||
},
|
||||
},
|
||||
],
|
||||
singleton: true
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
mode: "none",
|
||||
entry: [ resolve(sourceFolder, "main")],
|
||||
output: {
|
||||
filename: "js/[name].js",
|
||||
path: buildFolder,
|
||||
publicPath: "/",
|
||||
},
|
||||
module: {
|
||||
rules: [ babelRule, sassRule],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".js", ".ts", ".tsx", ".scss"],
|
||||
},
|
||||
plugins: [
|
||||
{ loader: 'css-loader' },
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
includePaths: [sourceFolder]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
mode: 'none',
|
||||
entry: ['babel-regenerator-runtime', resolve(sourceFolder, 'main')],
|
||||
output: {
|
||||
filename: 'js/[name].js',
|
||||
path: buildFolder,
|
||||
publicPath: '/'
|
||||
},
|
||||
module: {
|
||||
rules: [babelRule, sassRule]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.tsx', '.scss']
|
||||
},
|
||||
plugins: []
|
||||
}
|
||||
|
||||
const devConfig = {
|
||||
mode: "development",
|
||||
mode: 'development',
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: htmlTemplateFile,
|
||||
chunksSortMode: "dependency",
|
||||
}),
|
||||
chunksSortMode: 'dependency'
|
||||
})
|
||||
],
|
||||
devtool: "inline-source-map",
|
||||
devtool: 'inline-source-map',
|
||||
devServer: {
|
||||
historyApiFallback: true
|
||||
}
|
||||
}
|
||||
|
||||
const prodConfig = {
|
||||
mode: "production",
|
||||
mode: 'production',
|
||||
optimization: {
|
||||
minimize: true,
|
||||
nodeEnv: "production",
|
||||
nodeEnv: 'production'
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "css/[name].min.css",
|
||||
filename: 'css/[name].min.css'
|
||||
}),
|
||||
new OptimizeCssAssetsWebpackPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
|
@ -99,16 +98,16 @@ const prodConfig = {
|
|||
}),
|
||||
new HtmlWebpackInlineSourcePlugin()
|
||||
],
|
||||
devtool: "source-map"
|
||||
devtool: 'source-map'
|
||||
}
|
||||
|
||||
function getFinalConfig() {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
console.info("Running production config")
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.info('Running production config')
|
||||
return webpackMerge(baseConfig, prodConfig)
|
||||
}
|
||||
|
||||
console.info("Running development config")
|
||||
console.info('Running development config')
|
||||
return webpackMerge(baseConfig, devConfig)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue