typescript(lunargame/client): games page
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
parent
db4f749cd3
commit
ba844b45c1
50
typescript/lunargame/client/package-lock.json
generated
50
typescript/lunargame/client/package-lock.json
generated
|
@ -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": {
|
||||
"version": "0.6.6",
|
||||
"resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.6.6.tgz",
|
||||
|
@ -7988,6 +8010,22 @@
|
|||
"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": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
|
@ -8154,6 +8192,15 @@
|
|||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
|
||||
|
@ -9234,8 +9281,7 @@
|
|||
"tslib": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
|
||||
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
|
||||
},
|
||||
"tty-browserify": {
|
||||
"version": "0.0.0",
|
||||
|
|
|
@ -38,6 +38,10 @@
|
|||
"@material-ui/core": "^4.1.3",
|
||||
"@material-ui/icons": "^4.2.1",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -22,19 +22,35 @@ export interface ModalProps {
|
|||
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 = (
|
||||
title: string,
|
||||
description: string,
|
||||
url: string,
|
||||
fields: TextFieldData[],
|
||||
onSubmit?: (data: unknown) => void
|
||||
) => {
|
||||
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(
|
||||
|
@ -54,7 +70,7 @@ export const createFormModal = (
|
|||
props.onClose(event)
|
||||
}
|
||||
|
||||
const classes = useStyles()
|
||||
const classes = useStyles(props)
|
||||
|
||||
const textFields = fields.map((field, index) => {
|
||||
const fieldObject = formFields[index]
|
||||
|
|
|
@ -6,11 +6,11 @@ import {
|
|||
import { Account } from '../../network/types/Account'
|
||||
import { updateAccount } from '../../helpers/updateAccount'
|
||||
|
||||
export const LoginModal = createFormModal(
|
||||
'Login',
|
||||
`To subscribe to this website, please enter you r email address here. We will send updates occasionally.`,
|
||||
'auth/login',
|
||||
[
|
||||
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',
|
||||
|
@ -22,5 +22,5 @@ export const LoginModal = createFormModal(
|
|||
validators: passwordValidatorList()
|
||||
}
|
||||
],
|
||||
updateAccount
|
||||
)
|
||||
onSubmit: updateAccount
|
||||
})
|
||||
|
|
|
@ -10,11 +10,11 @@ import { DialogManager } from '../../../common/dom/dialogs/classes/DialogManager
|
|||
|
||||
const dialogManager = new DialogManager()
|
||||
|
||||
export const SignupModal = createFormModal(
|
||||
'Signup',
|
||||
`To create an account you need to provide an username, email and a password.`,
|
||||
'auth/create',
|
||||
[
|
||||
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',
|
||||
|
@ -31,7 +31,7 @@ export const SignupModal = createFormModal(
|
|||
validators: passwordValidatorList()
|
||||
}
|
||||
],
|
||||
(data: Account) => {
|
||||
onSubmit: (data: Account) => {
|
||||
updateAccount(data)
|
||||
dialogManager.add({
|
||||
title: 'Email verification',
|
||||
|
@ -41,4 +41,4 @@ export const SignupModal = createFormModal(
|
|||
onClose: () => {}
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
|
@ -5,7 +5,8 @@ import { Route } from 'react-router-dom'
|
|||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
padding: '5%'
|
||||
height: '90vh',
|
||||
display: 'block'
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
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[] = [
|
||||
{
|
||||
|
@ -11,13 +13,9 @@ export const routes: Route[] = [
|
|||
icon: <HomeIcon />
|
||||
},
|
||||
{
|
||||
name: 'about',
|
||||
url: '/about',
|
||||
content: () => (
|
||||
<>
|
||||
<h1>This is the about component</h1>
|
||||
</>
|
||||
),
|
||||
icon: <HomeIcon />
|
||||
name: 'games',
|
||||
url: '/games',
|
||||
content: Games,
|
||||
icon: <GamesIcon />
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
@mixin maxSize {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
|
@ -449,3 +449,8 @@ a {
|
|||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import React, { Component } from 'react'
|
||||
|
||||
export type acceptedContent = React.ComponentElement<unknown, Component>
|
||||
|
||||
export interface Route {
|
||||
name: string
|
||||
url: string
|
||||
content: () => acceptedContent
|
||||
icon: acceptedContent
|
||||
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
|
||||
}
|
||||
)
|
|
@ -26,14 +26,22 @@ export class BaseServer {
|
|||
public async request<T>(
|
||||
url: string,
|
||||
method = 'GET',
|
||||
body = {}
|
||||
body = {},
|
||||
queryParams: Record<string, string | number> = {}
|
||||
): Promise<T> {
|
||||
const noBody = ['GET', 'DELETE']
|
||||
const useBody = noBody.indexOf(method) === -1
|
||||
|
||||
const response = await fetch(`${this.path}/${url}`, {
|
||||
...(noBody.indexOf(method) === -1
|
||||
? { body: JSON.stringify(body) }
|
||||
: {}),
|
||||
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',
|
||||
|
@ -42,6 +50,7 @@ export class BaseServer {
|
|||
method,
|
||||
credentials: 'include'
|
||||
})
|
||||
|
||||
const parsed: Response<T> = await response.json()
|
||||
const status = response.status
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export interface GameChunkElementData {
|
||||
id: string
|
||||
thumbail: string
|
||||
avatar: string
|
||||
}
|
Loading…
Reference in a new issue