1
Fork 0

typescript(lunargame/client): games page

Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
Matei Adriel 2019-07-09 18:03:28 +03:00 committed by prescientmoon
parent db4f749cd3
commit ba844b45c1
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
18 changed files with 302 additions and 43 deletions

View file

@ -7546,6 +7546,28 @@
} }
} }
}, },
"react": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz",
"integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.13.6"
}
},
"react-dom": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz",
"integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.13.6"
}
},
"react-event-listener": { "react-event-listener": {
"version": "0.6.6", "version": "0.6.6",
"resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.6.6.tgz", "resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.6.6.tgz",
@ -7988,6 +8010,22 @@
"aproba": "^1.1.1" "aproba": "^1.1.1"
} }
}, },
"rxjs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz",
"integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==",
"requires": {
"tslib": "^1.9.0"
}
},
"rxjs-hooks": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/rxjs-hooks/-/rxjs-hooks-0.5.1.tgz",
"integrity": "sha512-UVF2PH6PdzGr1FPgRljzNtOxu8Yt3J5S2cM2KCv6ZRs3E/XaRI7/qiQySZlp9YIMpFVOuYeJDHgePkSLatY22g==",
"requires": {
"tslib": "^1.9.3"
}
},
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@ -8154,6 +8192,15 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true "dev": true
}, },
"scheduler": {
"version": "0.13.6",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
"integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"schema-utils": { "schema-utils": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
@ -9234,8 +9281,7 @@
"tslib": { "tslib": {
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
"dev": true
}, },
"tty-browserify": { "tty-browserify": {
"version": "0.0.0", "version": "0.0.0",

View file

@ -38,6 +38,10 @@
"@material-ui/core": "^4.1.3", "@material-ui/core": "^4.1.3",
"@material-ui/icons": "^4.2.1", "@material-ui/icons": "^4.2.1",
"@material-ui/styles": "^4.2.0", "@material-ui/styles": "^4.2.0",
"react-router-dom": "^5.0.1" "react": "^16.8.6",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.1",
"rxjs": "^6.5.2",
"rxjs-hooks": "^0.5.1"
} }
} }

View file

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

View file

@ -22,19 +22,35 @@ export interface ModalProps {
onClose: Function 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) => ({ const useStyles = makeStyles((theme: Theme) => ({
field: { field: {
marginTop: theme.spacing(2) marginTop: theme.spacing(2)
} }
})) }))
export const createFormModal = ( export const createFormModal = (options: Partial<FormModalOptions> = {}) => {
title: string, // This merges all options
description: string, const { fields, title, description, onSubmit, url } = {
url: string, ...defaultFormModalOptions,
fields: TextFieldData[], ...options
onSubmit?: (data: unknown) => void }
) => {
const formFields = fields.map( const formFields = fields.map(
field => field =>
new FormField( new FormField(
@ -54,7 +70,7 @@ export const createFormModal = (
props.onClose(event) props.onClose(event)
} }
const classes = useStyles() const classes = useStyles(props)
const textFields = fields.map((field, index) => { const textFields = fields.map((field, index) => {
const fieldObject = formFields[index] const fieldObject = formFields[index]

View file

@ -6,11 +6,11 @@ import {
import { Account } from '../../network/types/Account' import { Account } from '../../network/types/Account'
import { updateAccount } from '../../helpers/updateAccount' import { updateAccount } from '../../helpers/updateAccount'
export const LoginModal = createFormModal( export const LoginModal = createFormModal({
'Login', title: 'Login',
`To subscribe to this website, please enter you r email address here. We will send updates occasionally.`, description: `To subscribe to this website, please enter you r email address here. We will send updates occasionally.`,
'auth/login', url: 'auth/login',
[ fields: [
{ {
name: 'email', name: 'email',
type: 'email', type: 'email',
@ -22,5 +22,5 @@ export const LoginModal = createFormModal(
validators: passwordValidatorList() validators: passwordValidatorList()
} }
], ],
updateAccount onSubmit: updateAccount
) })

View file

@ -10,11 +10,11 @@ import { DialogManager } from '../../../common/dom/dialogs/classes/DialogManager
const dialogManager = new DialogManager() const dialogManager = new DialogManager()
export const SignupModal = createFormModal( export const SignupModal = createFormModal({
'Signup', title: 'Signup',
`To create an account you need to provide an username, email and a password.`, description: `To create an account you need to provide an username, email and a password.`,
'auth/create', url: 'auth/create',
[ fields: [
{ {
name: 'name', name: 'name',
type: 'text', type: 'text',
@ -31,7 +31,7 @@ export const SignupModal = createFormModal(
validators: passwordValidatorList() validators: passwordValidatorList()
} }
], ],
(data: Account) => { onSubmit: (data: Account) => {
updateAccount(data) updateAccount(data)
dialogManager.add({ dialogManager.add({
title: 'Email verification', title: 'Email verification',
@ -41,4 +41,4 @@ export const SignupModal = createFormModal(
onClose: () => {} onClose: () => {}
}) })
} }
) })

View file

@ -5,7 +5,8 @@ import { Route } from 'react-router-dom'
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
padding: '5%' height: '90vh',
display: 'block'
} }
}) })

View file

@ -1,7 +1,9 @@
import React from 'react' import React from 'react'
import HomeIcon from '@material-ui/icons/Home' import HomeIcon from '@material-ui/icons/Home'
import GamesIcon from '@material-ui/icons/Games'
import { Route } from '../types/Route' import { Route } from '../types/Route'
import { Home } from './Home' import { Home } from './Home'
import { Games } from '../../games/components/GamePage'
export const routes: Route[] = [ export const routes: Route[] = [
{ {
@ -11,13 +13,9 @@ export const routes: Route[] = [
icon: <HomeIcon /> icon: <HomeIcon />
}, },
{ {
name: 'about', name: 'games',
url: '/about', url: '/games',
content: () => ( content: Games,
<> icon: <GamesIcon />
<h1>This is the about component</h1>
</>
),
icon: <HomeIcon />
} }
] ]

View file

@ -0,0 +1,5 @@
@mixin maxSize {
width: 100%;
height: 100%;
display: block;
}

View file

@ -449,3 +449,8 @@ a {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
html,
body {
overflow: hidden;
}

View file

@ -1,10 +1,8 @@
import React, { Component } from 'react' import React, { Component } from 'react'
export type acceptedContent = React.ComponentElement<unknown, Component>
export interface Route { export interface Route {
name: string name: string
url: string url: string
content: () => acceptedContent content: (props: unknown) => JSX.Element
icon: acceptedContent icon: JSX.Element
} }

View file

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

View file

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

View file

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

View file

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

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

View file

@ -26,14 +26,22 @@ export class BaseServer {
public async request<T>( public async request<T>(
url: string, url: string,
method = 'GET', method = 'GET',
body = {} body = {},
queryParams: Record<string, string | number> = {}
): Promise<T> { ): Promise<T> {
const noBody = ['GET', 'DELETE'] const noBody = ['GET', 'DELETE']
const useBody = noBody.indexOf(method) === -1
const response = await fetch(`${this.path}/${url}`, { const params = Object.keys(queryParams).map(
...(noBody.indexOf(method) === -1 key => `${key}=${queryParams[key]}`
? { body: JSON.stringify(body) } )
: {}),
const finalUrl = `${this.path}/${url}${
params.length ? '?' : ''
}${params.join('&')}`
const response = await fetch(finalUrl, {
...(useBody ? { body: JSON.stringify(body) } : {}),
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -42,6 +50,7 @@ export class BaseServer {
method, method,
credentials: 'include' credentials: 'include'
}) })
const parsed: Response<T> = await response.json() const parsed: Response<T> = await response.json()
const status = response.status const status = response.status

View file

@ -0,0 +1,5 @@
export interface GameChunkElementData {
id: string
thumbail: string
avatar: string
}