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