Add
This commit is contained in:
commit
4ea503d68b
61
typescript/lunargame/client/.gitignore
vendored
Normal file
61
typescript/lunargame/client/.gitignore
vendored
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
5
typescript/lunargame/client/.prettierignore
Normal file
5
typescript/lunargame/client/.prettierignore
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
docs
|
||||||
|
node_modules
|
||||||
|
*.md
|
7
typescript/lunargame/client/.prettierrc.json
Normal file
7
typescript/lunargame/client/.prettierrc.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": false
|
||||||
|
}
|
6
typescript/lunargame/client/.vscode/settings.json
vendored
Normal file
6
typescript/lunargame/client/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"eslint.enable": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"prettier.eslintIntegration": true,
|
||||||
|
"explorer.autoReveal": false
|
||||||
|
}
|
21
typescript/lunargame/client/LICENSE
Normal file
21
typescript/lunargame/client/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Matei Adriel
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
1
typescript/lunargame/client/README.md
Normal file
1
typescript/lunargame/client/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# lunarbox-client
|
17
typescript/lunargame/client/babel.config.js
Normal file
17
typescript/lunargame/client/babel.config.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@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 }]
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
test: {
|
||||||
|
presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10066
typescript/lunargame/client/package-lock.json
generated
Normal file
10066
typescript/lunargame/client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
47
typescript/lunargame/client/package.json
Normal file
47
typescript/lunargame/client/package.json
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "webpack-dev-server",
|
||||||
|
"build": "cross-env NODE_ENV=production webpack",
|
||||||
|
"build:dev": "webpack",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.5.0",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.5.0",
|
||||||
|
"@babel/plugin-proposal-decorators": "^7.4.4",
|
||||||
|
"@babel/plugin-syntax-dynamic-import": "^7.2.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",
|
||||||
|
"mini-css-extract-plugin": "^0.7.0",
|
||||||
|
"node-sass": "^4.12.0",
|
||||||
|
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||||
|
"sass-loader": "^7.1.0",
|
||||||
|
"style-loader": "^0.23.1",
|
||||||
|
"typescript": "^3.5.2",
|
||||||
|
"webpack": "^4.35.2",
|
||||||
|
"webpack-cli": "^3.3.5",
|
||||||
|
"webpack-dev-server": "^3.7.2",
|
||||||
|
"webpack-merge": "^4.2.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@eix/utils": "git+https://github.com/eix-js/utils.git",
|
||||||
|
"@material-ui/core": "^4.1.3",
|
||||||
|
"@material-ui/icons": "^4.2.1",
|
||||||
|
"@material-ui/styles": "^4.2.0",
|
||||||
|
"react": "^16.8.6",
|
||||||
|
"react-dom": "^16.8.6",
|
||||||
|
"react-router-dom": "^5.0.1",
|
||||||
|
"rxjs": "^6.5.2",
|
||||||
|
"rxjs-hooks": "^0.5.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { cacheInstances } from '../../lang/objects/decorators/cacheInstances'
|
||||||
|
import { BehaviorSubject } from 'rxjs'
|
||||||
|
import { BaseServer } from '../../../modules/network/classes/BaseServer'
|
||||||
|
|
||||||
|
export interface InfiniteListConfig {
|
||||||
|
urls: {
|
||||||
|
chunk: string
|
||||||
|
count: string
|
||||||
|
}
|
||||||
|
pageSize: number
|
||||||
|
initialLoads?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
@cacheInstances(1)
|
||||||
|
export class InfiniteList<T> {
|
||||||
|
private static server = new BaseServer()
|
||||||
|
|
||||||
|
private count = 0
|
||||||
|
private page = 0
|
||||||
|
|
||||||
|
public elements = new Set<T>()
|
||||||
|
public refresh = new BehaviorSubject(0)
|
||||||
|
public ready = new BehaviorSubject(false)
|
||||||
|
|
||||||
|
constructor(public name: string, private config: InfiniteListConfig) {}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this.count = await InfiniteList.server.request(this.config.urls.count)
|
||||||
|
|
||||||
|
this.ready.next(true)
|
||||||
|
this.update()
|
||||||
|
|
||||||
|
for (let index = 0; index < this.config.initialLoads; index++) {
|
||||||
|
this.loadChunk()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadChunk() {
|
||||||
|
if (this.elements.size >= this.count) return
|
||||||
|
|
||||||
|
const chunk = await InfiniteList.server.request<T[]>(
|
||||||
|
this.config.urls.chunk,
|
||||||
|
'GET',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
page: this.page++,
|
||||||
|
pageSize: this.config.pageSize
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const element of chunk) {
|
||||||
|
this.elements.add(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
private update() {
|
||||||
|
this.refresh.next(this.refresh.value + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
get data() {
|
||||||
|
return Array.from(this.elements.values())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,126 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormModalOptions {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
url: string
|
||||||
|
fields: TextFieldData[]
|
||||||
|
onSubmit: (data: unknown) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultFormModalOptions: FormModalOptions = {
|
||||||
|
title: 'Mymodal',
|
||||||
|
description: 'This is a modal',
|
||||||
|
url: '',
|
||||||
|
fields: [],
|
||||||
|
onSubmit: () => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
field: {
|
||||||
|
marginTop: theme.spacing(2)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const createFormModal = (options: Partial<FormModalOptions> = {}) => {
|
||||||
|
// This merges all options
|
||||||
|
const { fields, title, description, onSubmit, url } = {
|
||||||
|
...defaultFormModalOptions,
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
|
||||||
|
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(props)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
22
typescript/lunargame/client/src/index.html
Normal file
22
typescript/lunargame/client/src/index.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="theme-color" content="#091F35" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
|
||||||
|
/>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<title>LunarBox</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
5
typescript/lunargame/client/src/main.tsx
Normal file
5
typescript/lunargame/client/src/main.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { render } from 'react-dom'
|
||||||
|
import React from 'react'
|
||||||
|
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({
|
||||||
|
title: 'Login',
|
||||||
|
description: `To subscribe to this website, please enter you r email address here. We will send updates occasionally.`,
|
||||||
|
url: 'auth/login',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
type: 'email',
|
||||||
|
validators: emailValidatorList()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'password',
|
||||||
|
type: 'password',
|
||||||
|
validators: passwordValidatorList()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
onSubmit: 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({
|
||||||
|
title: 'Signup',
|
||||||
|
description: `To create an account you need to provide an username, email and a password.`,
|
||||||
|
url: 'auth/create',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
validators: usernameValidatorList()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
type: 'email',
|
||||||
|
validators: emailValidatorList()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'password',
|
||||||
|
type: 'password',
|
||||||
|
validators: passwordValidatorList()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
onSubmit: (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: () => {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { useObservable } from 'rxjs-hooks'
|
||||||
|
import { BaseServer } from '../../network/classes/BaseServer'
|
||||||
|
import Avatar from '@material-ui/core/Avatar'
|
||||||
|
import { makeStyles, Theme } from '@material-ui/core/styles'
|
||||||
|
import { LoginModal } from './LoginModal'
|
||||||
|
import { ModalButton } from './ModalButton'
|
||||||
|
import { SignupModal } from './SignupModal'
|
||||||
|
|
||||||
|
const { account } = new BaseServer()
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
loginButton: {
|
||||||
|
marginLeft: theme.spacing(2)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const TopbarAccount = (props: unknown) => {
|
||||||
|
const accountSnapshot = useObservable(() => account, null)
|
||||||
|
const classes = useStyles(props)
|
||||||
|
|
||||||
|
const signup = (
|
||||||
|
<>
|
||||||
|
<ModalButton modal={SignupModal}>Sign up</ModalButton>
|
||||||
|
<ModalButton
|
||||||
|
modal={LoginModal}
|
||||||
|
className={classes.loginButton}
|
||||||
|
contained
|
||||||
|
>
|
||||||
|
Log in
|
||||||
|
</ModalButton>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
return accountSnapshot ? (
|
||||||
|
<Avatar src={accountSnapshot.avatar} alt={accountSnapshot.name} />
|
||||||
|
) : (
|
||||||
|
signup
|
||||||
|
)
|
||||||
|
}
|
|
@ -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'
|
||||||
|
})
|
|
@ -0,0 +1,24 @@
|
||||||
|
import '../styles/reset.scss'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||||
|
|
||||||
|
import { BrowserRouter as Router } from 'react-router-dom'
|
||||||
|
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 (
|
||||||
|
<Theme theme={MuiTheme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<Router>
|
||||||
|
<AppBar />
|
||||||
|
<Body />
|
||||||
|
<BaseDialogRenderer />
|
||||||
|
</Router>
|
||||||
|
</Theme>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React, { useState, KeyboardEvent } from 'react'
|
||||||
|
import MuiAppBar from '@material-ui/core/AppBar'
|
||||||
|
import Toolbar from '@material-ui/core/Toolbar'
|
||||||
|
import Typography from '@material-ui/core/Typography'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import MenuIcon from '@material-ui/icons/Menu'
|
||||||
|
import SwipeableDrawer from '@material-ui/core/SwipeableDrawer'
|
||||||
|
import { Sidebar } from './Sidebar'
|
||||||
|
import { makeStyles, Theme } from '@material-ui/core/styles'
|
||||||
|
import { TopbarAccount } from '../../account/components/TopbarAccount'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
root: {
|
||||||
|
flexGrow: 1
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
flexGrow: 1
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
width: 250
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const AppBar = (props: unknown) => {
|
||||||
|
const [sidebar, setSidebar] = useState(false)
|
||||||
|
const classes = useStyles(props)
|
||||||
|
|
||||||
|
const closeSidebar = () => setSidebar(false)
|
||||||
|
const openSidebar = () => setSidebar(true)
|
||||||
|
|
||||||
|
const handleKeydown = (open: boolean) => (
|
||||||
|
event: KeyboardEvent<HTMLDivElement>
|
||||||
|
) => {
|
||||||
|
if (event.key === 'Tab' || event.key === 'Shift') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSidebar(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classes.root}>
|
||||||
|
<MuiAppBar position="static">
|
||||||
|
<Toolbar>
|
||||||
|
<IconButton
|
||||||
|
edge="start"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="Menu"
|
||||||
|
onClick={() => setSidebar(true)}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h6" className={classes.title} />
|
||||||
|
|
||||||
|
<TopbarAccount />
|
||||||
|
</Toolbar>
|
||||||
|
</MuiAppBar>
|
||||||
|
</div>
|
||||||
|
<SwipeableDrawer
|
||||||
|
open={sidebar}
|
||||||
|
onClose={closeSidebar}
|
||||||
|
onOpen={openSidebar}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
role="presentation"
|
||||||
|
onClick={closeSidebar}
|
||||||
|
onKeyDown={handleKeydown(false)}
|
||||||
|
className={classes.list}
|
||||||
|
>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</SwipeableDrawer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { SiddebarRoutes } from './SidebarRouteList'
|
||||||
|
import { makeStyles } from '@material-ui/styles'
|
||||||
|
import { Route } from 'react-router-dom'
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: {
|
||||||
|
height: '90vh',
|
||||||
|
display: 'block'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Body = (props: unknown) => {
|
||||||
|
const classes = useStyles(props)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<SiddebarRoutes />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
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: {
|
||||||
|
flexGrow: 1
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
marginTop: theme.spacing(2)
|
||||||
|
},
|
||||||
|
a: {
|
||||||
|
color: '#0000ff'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const Home = (props: unknown) => {
|
||||||
|
const classes = useStyles(props)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography variant="h4">This is Lunarbox</Typography>
|
||||||
|
|
||||||
|
<Divider className={classes.divider} />
|
||||||
|
|
||||||
|
<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 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,26 @@
|
||||||
|
import React from 'react'
|
||||||
|
import List from '@material-ui/core/List'
|
||||||
|
import ListItem from '@material-ui/core/ListItem'
|
||||||
|
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||||
|
import ListItemText from '@material-ui/core/ListItemText'
|
||||||
|
import { routes } from './SidebarRouteData'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
export const Sidebar = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<List>
|
||||||
|
{routes.map((route, index) => {
|
||||||
|
return (
|
||||||
|
<Link className="routeLink" to={route.url} key={index}>
|
||||||
|
<ListItem button>
|
||||||
|
<ListItemIcon>{route.icon}</ListItemIcon>
|
||||||
|
<ListItemText primary={route.name} />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react'
|
||||||
|
import HomeIcon from '@material-ui/icons/Home'
|
||||||
|
import GamesIcon from '@material-ui/icons/Games'
|
||||||
|
import { Route } from '../types/Route'
|
||||||
|
import { Home } from './Home'
|
||||||
|
import { Games } from '../../games/components/GamePage'
|
||||||
|
|
||||||
|
export const routes: Route[] = [
|
||||||
|
{
|
||||||
|
name: 'home',
|
||||||
|
url: '/',
|
||||||
|
content: Home,
|
||||||
|
icon: <HomeIcon />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'games',
|
||||||
|
url: '/games',
|
||||||
|
content: Games,
|
||||||
|
icon: <GamesIcon />
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { routes } from './SidebarRouteData'
|
||||||
|
import { Route } from 'react-router-dom'
|
||||||
|
|
||||||
|
export const SiddebarRoutes = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{routes.map((route, index) => (
|
||||||
|
<Route
|
||||||
|
key={index}
|
||||||
|
path={route.url}
|
||||||
|
component={route.content}
|
||||||
|
exact={route.url === '/'}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
10
typescript/lunargame/client/src/modules/core/data/Theme.ts
Normal file
10
typescript/lunargame/client/src/modules/core/data/Theme.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { createMuiTheme } from '@material-ui/core/styles'
|
||||||
|
import * as Colors from '@material-ui/core/colors'
|
||||||
|
|
||||||
|
export const theme = createMuiTheme({
|
||||||
|
palette: {
|
||||||
|
type: 'dark',
|
||||||
|
primary: Colors.deepPurple,
|
||||||
|
secondary: Colors.red
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
@mixin maxSize {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
456
typescript/lunargame/client/src/modules/core/styles/reset.scss
Normal file
456
typescript/lunargame/client/src/modules/core/styles/reset.scss
Normal file
|
@ -0,0 +1,456 @@
|
||||||
|
/* http://meyerweb.com/eric/tools/css/reset/
|
||||||
|
v2.0-modified | 20110126
|
||||||
|
License: none (public domain)
|
||||||
|
*/
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
div,
|
||||||
|
span,
|
||||||
|
applet,
|
||||||
|
object,
|
||||||
|
iframe,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
blockquote,
|
||||||
|
pre,
|
||||||
|
a,
|
||||||
|
abbr,
|
||||||
|
acronym,
|
||||||
|
address,
|
||||||
|
big,
|
||||||
|
cite,
|
||||||
|
code,
|
||||||
|
del,
|
||||||
|
dfn,
|
||||||
|
em,
|
||||||
|
img,
|
||||||
|
ins,
|
||||||
|
kbd,
|
||||||
|
q,
|
||||||
|
s,
|
||||||
|
samp,
|
||||||
|
small,
|
||||||
|
strike,
|
||||||
|
strong,
|
||||||
|
sub,
|
||||||
|
sup,
|
||||||
|
tt,
|
||||||
|
var,
|
||||||
|
b,
|
||||||
|
u,
|
||||||
|
i,
|
||||||
|
center,
|
||||||
|
dl,
|
||||||
|
dt,
|
||||||
|
dd,
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
li,
|
||||||
|
fieldset,
|
||||||
|
form,
|
||||||
|
label,
|
||||||
|
legend,
|
||||||
|
table,
|
||||||
|
caption,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
thead,
|
||||||
|
tr,
|
||||||
|
th,
|
||||||
|
td,
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
canvas,
|
||||||
|
details,
|
||||||
|
embed,
|
||||||
|
figure,
|
||||||
|
figcaption,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
output,
|
||||||
|
ruby,
|
||||||
|
section,
|
||||||
|
summary,
|
||||||
|
time,
|
||||||
|
mark,
|
||||||
|
audio,
|
||||||
|
video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
font: inherit;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* make sure to set some focus styles for accessibility */
|
||||||
|
:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
details,
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote:before,
|
||||||
|
blockquote:after,
|
||||||
|
q:before,
|
||||||
|
q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='search']::-webkit-search-cancel-button,
|
||||||
|
input[type='search']::-webkit-search-decoration,
|
||||||
|
input[type='search']::-webkit-search-results-button,
|
||||||
|
input[type='search']::-webkit-search-results-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='search'] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
-webkit-box-sizing: content-box;
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
vertical-align: top;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio,
|
||||||
|
canvas,
|
||||||
|
video {
|
||||||
|
display: inline-block;
|
||||||
|
*display: inline;
|
||||||
|
*zoom: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent modern browsers from displaying `audio` without controls.
|
||||||
|
* Remove excess height in iOS 5 devices.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio:not([controls]) {
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
|
||||||
|
* Known issue: no IE 6 support.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
|
||||||
|
* `em` units.
|
||||||
|
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||||
|
* user zoom.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
-ms-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address `outline` inconsistency between Chrome and other browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a:focus {
|
||||||
|
outline: thin dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Improve readability when focused and also mouse hovered in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a:active,
|
||||||
|
a:hover {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
|
||||||
|
* 2. Improve image quality when scaled in IE 7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0; /* 1 */
|
||||||
|
-ms-interpolation-mode: bicubic; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct margin displayed oddly in IE 6/7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define consistent border, margin, and padding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 0.35em 0.625em 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct color not being inherited in IE 6/7/8/9.
|
||||||
|
* 2. Correct text not wrapping in Firefox 3.
|
||||||
|
* 3. Correct alignment displayed oddly in IE 6/7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
border: 0; /* 1 */
|
||||||
|
padding: 0;
|
||||||
|
white-space: normal; /* 2 */
|
||||||
|
*margin-left: -7px; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct font size not being inherited in all browsers.
|
||||||
|
* 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
|
||||||
|
* and Chrome.
|
||||||
|
* 3. Improve appearance and consistency in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
vertical-align: baseline; /* 3 */
|
||||||
|
*vertical-align: middle; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address Firefox 3+ setting `line-height` on `input` using `!important` in
|
||||||
|
* the UA stylesheet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||||
|
* All other form control elements do not inherit `text-transform` values.
|
||||||
|
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
|
||||||
|
* Correct `select` style inheritance in Firefox 4+ and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||||
|
* and `video` controls.
|
||||||
|
* 2. Correct inability to style clickable `input` types in iOS.
|
||||||
|
* 3. Improve usability and consistency of cursor style between image-type
|
||||||
|
* `input` and others.
|
||||||
|
* 4. Remove inner spacing in IE 7 without affecting normal text inputs.
|
||||||
|
* Known issue: inner spacing remains in IE 6.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
html input[type="button"], /* 1 */
|
||||||
|
input[type="reset"],
|
||||||
|
input[type="submit"] {
|
||||||
|
-webkit-appearance: button; /* 2 */
|
||||||
|
cursor: pointer; /* 3 */
|
||||||
|
*overflow: visible; /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-set default cursor for disabled elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button[disabled],
|
||||||
|
html input[disabled] {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Address box sizing set to content-box in IE 8/9.
|
||||||
|
* 2. Remove excess padding in IE 8/9.
|
||||||
|
* 3. Remove excess padding in IE 7.
|
||||||
|
* Known issue: excess padding remains in IE 6.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type='checkbox'],
|
||||||
|
input[type='radio'] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
*height: 13px; /* 3 */
|
||||||
|
*width: 13px; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
|
||||||
|
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
|
||||||
|
* (include `-moz` to future-proof).
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type='search'] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
-webkit-box-sizing: content-box; /* 2 */
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and search cancel button in Safari 5 and Chrome
|
||||||
|
* on OS X.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type='search']::-webkit-search-cancel-button,
|
||||||
|
input[type='search']::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and border in Firefox 3+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
input::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove default vertical scrollbar in IE 6/7/8/9.
|
||||||
|
* 2. Improve readability and alignment in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto; /* 1 */
|
||||||
|
vertical-align: top; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove most spacing between table cells.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: #b3d4fc;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: #b3d4fc;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chromeframe {
|
||||||
|
margin: 0.2em 0;
|
||||||
|
background: #ccc;
|
||||||
|
color: #000;
|
||||||
|
padding: 0.2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import React, { Component } from 'react'
|
||||||
|
|
||||||
|
export interface Route {
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
content: (props: unknown) => JSX.Element
|
||||||
|
icon: JSX.Element
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
@import '../../core/styles/mixins/maxSize.scss';
|
||||||
|
|
||||||
|
.gameListItemContainer {
|
||||||
|
padding: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameListItem {
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameListItem,
|
||||||
|
.gameListItemContainer {
|
||||||
|
@include maxSize();
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import React, { CSSProperties } from 'react'
|
||||||
|
import './GameListItem.scss'
|
||||||
|
import { GameChunkElementData } from '../../network/types/GameChunkElementData'
|
||||||
|
|
||||||
|
export const GameListItem = (props: { game: GameChunkElementData }) => {
|
||||||
|
const styles: CSSProperties = {
|
||||||
|
backgroundImage: `url(${props.game.thumbail})`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gameListItemContainer">
|
||||||
|
<div className="gameListItem" style={styles} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
.gameListContainer {
|
||||||
|
overflow-y: scroll;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
padding: 5vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameListContainer > .gameListGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
grid-auto-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameListGrid::before {
|
||||||
|
content: '';
|
||||||
|
width: 0;
|
||||||
|
padding-bottom: 100%;
|
||||||
|
grid-row: 1 / 1;
|
||||||
|
grid-column: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gameListGrid > *:first-child {
|
||||||
|
grid-row: 1 / 1;
|
||||||
|
grid-column: 1 / 1;
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import React, { UIEvent } from 'react'
|
||||||
|
import './GamePage.scss'
|
||||||
|
import { useObservable } from 'rxjs-hooks'
|
||||||
|
import { GameInfiniteList } from '../constants'
|
||||||
|
import { GameListItem } from './GameListItem'
|
||||||
|
|
||||||
|
export const Games = () => {
|
||||||
|
useObservable(() => GameInfiniteList.refresh)
|
||||||
|
|
||||||
|
const ready = useObservable(() => GameInfiniteList.ready)
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
return <h1>Loading...</h1>
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScroll = (e: UIEvent<HTMLDivElement>) => {
|
||||||
|
const element = e.target as HTMLDivElement
|
||||||
|
|
||||||
|
if (
|
||||||
|
element.scrollHeight - element.scrollTop <
|
||||||
|
element.clientHeight + 50
|
||||||
|
) {
|
||||||
|
GameInfiniteList.loadChunk()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onScroll={handleScroll} className="gameListContainer">
|
||||||
|
<div className="gameListGrid">
|
||||||
|
{GameInfiniteList.data.map(game => (
|
||||||
|
<GameListItem key={game.id} game={game} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
14
typescript/lunargame/client/src/modules/games/constants.ts
Normal file
14
typescript/lunargame/client/src/modules/games/constants.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { InfiniteList } from '../../common/dom/classes/InfiniteList'
|
||||||
|
import { GameChunkElementData } from '../network/types/GameChunkElementData'
|
||||||
|
|
||||||
|
export const GameInfiniteList = new InfiniteList<GameChunkElementData>(
|
||||||
|
'gameList',
|
||||||
|
{
|
||||||
|
pageSize: 5,
|
||||||
|
urls: {
|
||||||
|
chunk: 'game/chunk',
|
||||||
|
count: 'game/count'
|
||||||
|
},
|
||||||
|
initialLoads: 6
|
||||||
|
}
|
||||||
|
)
|
|
@ -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,64 @@
|
||||||
|
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 = {},
|
||||||
|
queryParams: Record<string, string | number> = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const noBody = ['GET', 'DELETE']
|
||||||
|
const useBody = noBody.indexOf(method) === -1
|
||||||
|
|
||||||
|
const params = Object.keys(queryParams).map(
|
||||||
|
key => `${key}=${queryParams[key]}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const finalUrl = `${this.path}/${url}${
|
||||||
|
params.length ? '?' : ''
|
||||||
|
}${params.join('&')}`
|
||||||
|
|
||||||
|
const response = await fetch(finalUrl, {
|
||||||
|
...(useBody ? { 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,2 @@
|
||||||
|
export const defaultAvatar =
|
||||||
|
'https://cdn-images-1.medium.com/max/1200/1*MccriYX-ciBniUzRKAUsAw.png'
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { AccountPublicData } from './AccountPublicData'
|
||||||
|
|
||||||
|
export interface Account extends AccountPublicData {
|
||||||
|
uid: string
|
||||||
|
verified: boolean
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface AccountPublicData {
|
||||||
|
email: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
avatar: string
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface GameChunkElementData {
|
||||||
|
id: string
|
||||||
|
thumbail: string
|
||||||
|
avatar: string
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface Response<T> {
|
||||||
|
data: T
|
||||||
|
message: string
|
||||||
|
}
|
12
typescript/lunargame/client/tsconfig.json
Normal file
12
typescript/lunargame/client/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"target": "esnext"
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"],
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
114
typescript/lunargame/client/webpack.config.js
Normal file
114
typescript/lunargame/client/webpack.config.js
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
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 projectRoot = resolve(__dirname)
|
||||||
|
const sourceFolder = resolve(projectRoot, 'src')
|
||||||
|
const buildFolder = resolve(projectRoot, 'dist')
|
||||||
|
const htmlTemplateFile = resolve(sourceFolder, 'index.html')
|
||||||
|
|
||||||
|
const babelRule = {
|
||||||
|
test: /\.(js|tsx?)$/,
|
||||||
|
use: 'babel-loader'
|
||||||
|
}
|
||||||
|
|
||||||
|
const sassRule = {
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: [
|
||||||
|
isProduction
|
||||||
|
? MiniCssExtractPlugin.loader
|
||||||
|
: {
|
||||||
|
loader: 'style-loader',
|
||||||
|
options: {
|
||||||
|
singleton: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 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',
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: htmlTemplateFile,
|
||||||
|
chunksSortMode: 'dependency'
|
||||||
|
})
|
||||||
|
],
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
devServer: {
|
||||||
|
historyApiFallback: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prodConfig = {
|
||||||
|
mode: 'production',
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
nodeEnv: 'production'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: 'css/[name].min.css'
|
||||||
|
}),
|
||||||
|
new OptimizeCssAssetsWebpackPlugin(),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: htmlTemplateFile,
|
||||||
|
minify: {
|
||||||
|
removeComments: true,
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeRedundantAttributes: true,
|
||||||
|
useShortDoctype: true,
|
||||||
|
removeEmptyAttributes: true,
|
||||||
|
removeStyleLinkTypeAttributes: true,
|
||||||
|
keepClosingSlash: true,
|
||||||
|
minifyJS: true,
|
||||||
|
minifyCSS: true,
|
||||||
|
minifyURLs: true
|
||||||
|
},
|
||||||
|
inject: true
|
||||||
|
}),
|
||||||
|
new HtmlWebpackInlineSourcePlugin()
|
||||||
|
],
|
||||||
|
devtool: 'source-map'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFinalConfig() {
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
console.info('Running production config')
|
||||||
|
return webpackMerge(baseConfig, prodConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info('Running development config')
|
||||||
|
return webpackMerge(baseConfig, devConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getFinalConfig()
|
Loading…
Reference in a new issue