1
Fork 0

typescript(lunargame/client): progress on user page

Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
Matei Adriel 2019-07-08 15:04:19 +03:00 committed by prescientmoon
parent 2bc7d4e3e1
commit db4f749cd3
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
45 changed files with 853 additions and 173 deletions

View file

@ -1,17 +1,17 @@
module.exports = { module.exports = {
presets: [ presets: [
"@babel/preset-env", '@babel/preset-env',
"@babel/preset-react", '@babel/preset-react',
"@babel/preset-typescript" '@babel/preset-typescript'
], ],
plugins: [ plugins: [
"@babel/plugin-syntax-dynamic-import", '@babel/plugin-syntax-dynamic-import',
["@babel/plugin-proposal-decorators", { legacy: true }], ['@babel/plugin-proposal-decorators', { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }], ['@babel/plugin-proposal-class-properties', { loose: true }]
], ],
env: { env: {
test: { test: {
presets: [["@babel/preset-env", { targets: { node: "current" } }]], presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
}, }
}, }
} }

View file

@ -736,18 +736,6 @@
"@babel/helper-plugin-utils": "^7.0.0" "@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": { "@babel/plugin-transform-shorthand-properties": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", "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" "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": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@ -2489,6 +2510,12 @@
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
"dev": true "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": { "core-js-compat": {
"version": "3.1.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.1.4.tgz", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.1.4.tgz",

View file

@ -12,12 +12,13 @@
"@babel/plugin-proposal-class-properties": "^7.5.0", "@babel/plugin-proposal-class-properties": "^7.5.0",
"@babel/plugin-proposal-decorators": "^7.4.4", "@babel/plugin-proposal-decorators": "^7.4.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.5.0",
"@babel/preset-env": "^7.5.0", "@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.3.3", "@babel/preset-typescript": "^7.3.3",
"@types/react-router-dom": "^4.3.4", "@types/react-router-dom": "^4.3.4",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-regenerator-runtime": "^6.5.0",
"css-loader": "^3.0.0", "css-loader": "^3.0.0",
"html-webpack-inline-source-plugin": "0.0.10", "html-webpack-inline-source-plugin": "0.0.10",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",

View file

@ -1,5 +0,0 @@
import React from 'react'
export const Login = () => {
return <h1>This is the login component</h1>
}

View file

@ -1,5 +0,0 @@
import React from 'react'
export const Signup = () => {
return <h1>This is the signup component</h1>
}

View file

@ -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)
}
}
})
}
}

View file

@ -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 <></>
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}}
/>
)
}

View file

@ -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>
)
}
}

View file

@ -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
}
}
}

View file

@ -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.`
})

View file

@ -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()
}
}
}
}

View file

@ -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
}

View file

@ -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
// })
}
}

View file

@ -1,5 +1,5 @@
import { render } from 'react-dom' import { render } from 'react-dom'
import React from 'react' import React from 'react'
import { App } from './common/core/components/App' import { App } from './modules/core/components/App'
render(<App />, document.querySelector('#app')) render(<App />, document.querySelector('#app'))

View file

@ -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
)

View file

@ -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)} />
</>
)
}

View file

@ -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: () => {}
})
}
)

View file

@ -2,9 +2,10 @@ import React from 'react'
import { useObservable } from 'rxjs-hooks' import { useObservable } from 'rxjs-hooks'
import { BaseServer } from '../../network/classes/BaseServer' import { BaseServer } from '../../network/classes/BaseServer'
import Avatar from '@material-ui/core/Avatar' import Avatar from '@material-ui/core/Avatar'
import Button from '@material-ui/core/Button'
import { makeStyles, Theme } from '@material-ui/core/styles' 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() const { account } = new BaseServer()
@ -16,18 +17,18 @@ const useStyles = makeStyles((theme: Theme) => ({
export const TopbarAccount = (props: unknown) => { export const TopbarAccount = (props: unknown) => {
const accountSnapshot = useObservable(() => account, null) const accountSnapshot = useObservable(() => account, null)
const classes = useStyles(props) const classes = useStyles(props)
const signup = ( const signup = (
<> <>
<Button> <ModalButton modal={SignupModal}>Sign up</ModalButton>
<Link to="/signup">Sign up</Link> <ModalButton
</Button> modal={LoginModal}
className={classes.loginButton}
<Button variant="contained" className={classes.loginButton}> contained
<Link to="/login"> Login</Link> >
</Button> Log in
</ModalButton>
</> </>
) )

View file

@ -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.'
})

View file

@ -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()
]

View file

@ -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'
})

View file

@ -0,0 +1,3 @@
import { lengthValidator } from '../../../common/dom/forms/validators/lengthValidator'
export const fieldLengthValidator = lengthValidator(3, 30)

View file

@ -0,0 +1,6 @@
import { createValidator } from '../../../common/dom/forms/helpers/createValidator'
export const requiredValidator = createValidator({
regex: /^.{1,}$/,
message: 'Field is required'
})

View file

@ -8,6 +8,7 @@ import { theme as MuiTheme } from '../data/Theme'
import { ThemeProvider as Theme } from '@material-ui/styles' import { ThemeProvider as Theme } from '@material-ui/styles'
import { AppBar } from './AppBar' import { AppBar } from './AppBar'
import { Body } from './Body' import { Body } from './Body'
import { BaseDialogRenderer } from '../../../common/dom/dialogs/components/BaseDialogRenderer'
export const App = () => { export const App = () => {
return ( return (
@ -16,6 +17,7 @@ export const App = () => {
<Router> <Router>
<AppBar /> <AppBar />
<Body /> <Body />
<BaseDialogRenderer />
</Router> </Router>
</Theme> </Theme>
) )

View file

@ -2,8 +2,6 @@ import React from 'react'
import { SiddebarRoutes } from './SidebarRouteList' import { SiddebarRoutes } from './SidebarRouteList'
import { makeStyles } from '@material-ui/styles' import { makeStyles } from '@material-ui/styles'
import { Route } from 'react-router-dom' import { Route } from 'react-router-dom'
import { Signup } from '../../account/components/Signup'
import { Login } from '../../account/components/Login'
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
@ -17,9 +15,6 @@ export const Body = (props: unknown) => {
return ( return (
<div className={classes.root}> <div className={classes.root}>
<SiddebarRoutes /> <SiddebarRoutes />
<Route component={Signup} path="/signup" />
<Route component={Login} path="/login" />
</div> </div>
) )
} }

View file

@ -2,6 +2,7 @@ import React from 'react'
import Typography from '@material-ui/core/Typography' import Typography from '@material-ui/core/Typography'
import Divider from '@material-ui/core/Divider' import Divider from '@material-ui/core/Divider'
import { makeStyles, Theme } from '@material-ui/core/styles' import { makeStyles, Theme } from '@material-ui/core/styles'
import { BaseServer } from '../../network/classes/BaseServer'
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
root: { root: {
@ -10,6 +11,9 @@ const useStyles = makeStyles((theme: Theme) => ({
divider: { divider: {
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
marginTop: theme.spacing(2) marginTop: theme.spacing(2)
},
a: {
color: '#0000ff'
} }
})) }))
@ -24,9 +28,34 @@ export const Home = (props: unknown) => {
<Typography variant="h6" color="textSecondary"> <Typography variant="h6" color="textSecondary">
Lunarbox is a game streaming website for games made with the eix 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. unusable.
</Typography> </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>
</> </>
) )
} }

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -0,0 +1,6 @@
import { AccountPublicData } from './AccountPublicData'
export interface Account extends AccountPublicData {
uid: string
verified: boolean
}

View file

@ -1,8 +1,6 @@
export interface Account { export interface AccountPublicData {
name: string
email: string email: string
avatar: string name: string
description: string description: string
uid: string avatar: string
verified: boolean
} }

View file

@ -0,0 +1,4 @@
export interface Response<T> {
data: T
message: string
}

View file

@ -4,7 +4,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"jsx": "preserve", "jsx": "preserve",
"noImplicitAny": true, "noImplicitAny": true,
"experimentalDecorators": true "experimentalDecorators": true,
"target": "esnext"
}, },
"exclude": ["node_modules"], "exclude": ["node_modules"],
"include": ["src"] "include": ["src"]

View file

@ -1,115 +1,114 @@
const { resolve } = require("path") const { resolve } = require('path')
const HtmlWebpackPlugin = require("html-webpack-plugin") const HtmlWebpackPlugin = require('html-webpack-plugin')
const HtmlWebpackInlineSourcePlugin = require("html-webpack-inline-source-plugin") const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin") const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin") const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const webpackMerge = require("webpack-merge") const webpackMerge = require('webpack-merge')
const isProduction = process.env.NODE_ENV === "production" const isProduction = process.env.NODE_ENV === 'production'
const projectRoot = resolve(__dirname) const projectRoot = resolve(__dirname)
const sourceFolder = resolve(projectRoot, "src") const sourceFolder = resolve(projectRoot, 'src')
const buildFolder = resolve(projectRoot, "dist") const buildFolder = resolve(projectRoot, 'dist')
const htmlTemplateFile = resolve(sourceFolder, "index.html") const htmlTemplateFile = resolve(sourceFolder, 'index.html')
const babelRule = { const babelRule = {
test: /\.(js|tsx?)$/, test: /\.(js|tsx?)$/,
use: "babel-loader", use: 'babel-loader'
} }
const sassRule = { const sassRule = {
test: /\.scss$/, test: /\.scss$/,
use: [ use: [
isProduction isProduction
? MiniCssExtractPlugin.loader ? MiniCssExtractPlugin.loader
: { : {
loader: "style-loader", loader: 'style-loader',
options: { options: {
singleton: true, singleton: true
}, }
}, },
{ loader: "css-loader" }, { loader: 'css-loader' },
{ {
loader: "sass-loader", loader: 'sass-loader',
options: { options: {
includePaths: [sourceFolder], includePaths: [sourceFolder]
}, }
}, }
], ]
} }
const baseConfig = { const baseConfig = {
mode: "none", mode: 'none',
entry: [ resolve(sourceFolder, "main")], entry: ['babel-regenerator-runtime', resolve(sourceFolder, 'main')],
output: { output: {
filename: "js/[name].js", filename: 'js/[name].js',
path: buildFolder, path: buildFolder,
publicPath: "/", publicPath: '/'
}, },
module: { module: {
rules: [ babelRule, sassRule], rules: [babelRule, sassRule]
}, },
resolve: { resolve: {
extensions: [".js", ".ts", ".tsx", ".scss"], extensions: ['.js', '.ts', '.tsx', '.scss']
}, },
plugins: [ plugins: []
]
} }
const devConfig = { const devConfig = {
mode: "development", mode: 'development',
plugins: [ plugins: [
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: htmlTemplateFile, template: htmlTemplateFile,
chunksSortMode: "dependency", chunksSortMode: 'dependency'
}), })
], ],
devtool: "inline-source-map", devtool: 'inline-source-map',
devServer: { devServer: {
historyApiFallback: true historyApiFallback: true
} }
} }
const prodConfig = { const prodConfig = {
mode: "production", mode: 'production',
optimization: { optimization: {
minimize: true, minimize: true,
nodeEnv: "production", nodeEnv: 'production'
}, },
plugins: [ plugins: [
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: "css/[name].min.css", filename: 'css/[name].min.css'
}), }),
new OptimizeCssAssetsWebpackPlugin(), new OptimizeCssAssetsWebpackPlugin(),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: htmlTemplateFile, template: htmlTemplateFile,
minify: { minify: {
removeComments: true, removeComments: true,
collapseWhitespace: true, collapseWhitespace: true,
removeRedundantAttributes: true, removeRedundantAttributes: true,
useShortDoctype: true, useShortDoctype: true,
removeEmptyAttributes: true, removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true, removeStyleLinkTypeAttributes: true,
keepClosingSlash: true, keepClosingSlash: true,
minifyJS: true, minifyJS: true,
minifyCSS: true, minifyCSS: true,
minifyURLs: true minifyURLs: true
}, },
inject: true inject: true
}), }),
new HtmlWebpackInlineSourcePlugin() new HtmlWebpackInlineSourcePlugin()
], ],
devtool: "source-map" devtool: 'source-map'
} }
function getFinalConfig() { function getFinalConfig() {
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === 'production') {
console.info("Running production config") console.info('Running production config')
return webpackMerge(baseConfig, prodConfig) return webpackMerge(baseConfig, prodConfig)
} }
console.info("Running development config") console.info('Running development config')
return webpackMerge(baseConfig, devConfig) return webpackMerge(baseConfig, devConfig)
} }
module.exports = getFinalConfig() module.exports = getFinalConfig()