diff --git a/typescript/lunargame/client/package-lock.json b/typescript/lunargame/client/package-lock.json index f8b6c64..75e6a1e 100644 --- a/typescript/lunargame/client/package-lock.json +++ b/typescript/lunargame/client/package-lock.json @@ -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", diff --git a/typescript/lunargame/client/package.json b/typescript/lunargame/client/package.json index 6e9fa65..814ca68 100644 --- a/typescript/lunargame/client/package.json +++ b/typescript/lunargame/client/package.json @@ -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" } } diff --git a/typescript/lunargame/client/src/common/dom/classes/InfiniteList.ts b/typescript/lunargame/client/src/common/dom/classes/InfiniteList.ts new file mode 100644 index 0000000..0cb0729 --- /dev/null +++ b/typescript/lunargame/client/src/common/dom/classes/InfiniteList.ts @@ -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 { + private static server = new BaseServer() + + private count = 0 + private page = 0 + + public elements = new Set() + 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( + 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()) + } +} diff --git a/typescript/lunargame/client/src/common/dom/forms/helpers/createFormModal.tsx b/typescript/lunargame/client/src/common/dom/forms/helpers/createFormModal.tsx index 42b2189..c8b727d 100644 --- a/typescript/lunargame/client/src/common/dom/forms/helpers/createFormModal.tsx +++ b/typescript/lunargame/client/src/common/dom/forms/helpers/createFormModal.tsx @@ -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 = {}) => { + // 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] diff --git a/typescript/lunargame/client/src/modules/account/components/LoginModal.ts b/typescript/lunargame/client/src/modules/account/components/LoginModal.ts index 33cf9d1..cd66637 100644 --- a/typescript/lunargame/client/src/modules/account/components/LoginModal.ts +++ b/typescript/lunargame/client/src/modules/account/components/LoginModal.ts @@ -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 +}) diff --git a/typescript/lunargame/client/src/modules/account/components/SignupModal.ts b/typescript/lunargame/client/src/modules/account/components/SignupModal.ts index 3041a0b..cf150b8 100644 --- a/typescript/lunargame/client/src/modules/account/components/SignupModal.ts +++ b/typescript/lunargame/client/src/modules/account/components/SignupModal.ts @@ -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: () => {} }) } -) +}) diff --git a/typescript/lunargame/client/src/modules/core/components/Body.tsx b/typescript/lunargame/client/src/modules/core/components/Body.tsx index 890b5b6..99fcaea 100644 --- a/typescript/lunargame/client/src/modules/core/components/Body.tsx +++ b/typescript/lunargame/client/src/modules/core/components/Body.tsx @@ -5,7 +5,8 @@ import { Route } from 'react-router-dom' const useStyles = makeStyles({ root: { - padding: '5%' + height: '90vh', + display: 'block' } }) diff --git a/typescript/lunargame/client/src/modules/core/components/SidebarRouteData.tsx b/typescript/lunargame/client/src/modules/core/components/SidebarRouteData.tsx index 4393b30..5b20048 100644 --- a/typescript/lunargame/client/src/modules/core/components/SidebarRouteData.tsx +++ b/typescript/lunargame/client/src/modules/core/components/SidebarRouteData.tsx @@ -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: }, { - name: 'about', - url: '/about', - content: () => ( - <> -

This is the about component

- - ), - icon: + name: 'games', + url: '/games', + content: Games, + icon: } ] diff --git a/typescript/lunargame/client/src/modules/core/styles/mixins/maxSize.scss b/typescript/lunargame/client/src/modules/core/styles/mixins/maxSize.scss new file mode 100644 index 0000000..6f1e10f --- /dev/null +++ b/typescript/lunargame/client/src/modules/core/styles/mixins/maxSize.scss @@ -0,0 +1,5 @@ +@mixin maxSize { + width: 100%; + height: 100%; + display: block; +} diff --git a/typescript/lunargame/client/src/modules/core/styles/reset.scss b/typescript/lunargame/client/src/modules/core/styles/reset.scss index 829396f..f1f84c7 100644 --- a/typescript/lunargame/client/src/modules/core/styles/reset.scss +++ b/typescript/lunargame/client/src/modules/core/styles/reset.scss @@ -449,3 +449,8 @@ a { text-decoration: none; color: inherit; } + +html, +body { + overflow: hidden; +} diff --git a/typescript/lunargame/client/src/modules/core/types/Route.ts b/typescript/lunargame/client/src/modules/core/types/Route.ts index 568bff8..67a36a9 100644 --- a/typescript/lunargame/client/src/modules/core/types/Route.ts +++ b/typescript/lunargame/client/src/modules/core/types/Route.ts @@ -1,10 +1,8 @@ import React, { Component } from 'react' -export type acceptedContent = React.ComponentElement - export interface Route { name: string url: string - content: () => acceptedContent - icon: acceptedContent + content: (props: unknown) => JSX.Element + icon: JSX.Element } diff --git a/typescript/lunargame/client/src/modules/games/components/GameListItem.scss b/typescript/lunargame/client/src/modules/games/components/GameListItem.scss new file mode 100644 index 0000000..34b64bd --- /dev/null +++ b/typescript/lunargame/client/src/modules/games/components/GameListItem.scss @@ -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(); +} diff --git a/typescript/lunargame/client/src/modules/games/components/GameListItem.tsx b/typescript/lunargame/client/src/modules/games/components/GameListItem.tsx new file mode 100644 index 0000000..106e1fc --- /dev/null +++ b/typescript/lunargame/client/src/modules/games/components/GameListItem.tsx @@ -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 ( +
+
+
+ ) +} diff --git a/typescript/lunargame/client/src/modules/games/components/GamePage.scss b/typescript/lunargame/client/src/modules/games/components/GamePage.scss new file mode 100644 index 0000000..6005f76 --- /dev/null +++ b/typescript/lunargame/client/src/modules/games/components/GamePage.scss @@ -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; +} diff --git a/typescript/lunargame/client/src/modules/games/components/GamePage.tsx b/typescript/lunargame/client/src/modules/games/components/GamePage.tsx new file mode 100644 index 0000000..4d86455 --- /dev/null +++ b/typescript/lunargame/client/src/modules/games/components/GamePage.tsx @@ -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

Loading...

+ } + + const handleScroll = (e: UIEvent) => { + const element = e.target as HTMLDivElement + + if ( + element.scrollHeight - element.scrollTop < + element.clientHeight + 50 + ) { + GameInfiniteList.loadChunk() + e.preventDefault() + } + } + + return ( +
+
+ {GameInfiniteList.data.map(game => ( + + ))} +
+
+ ) +} diff --git a/typescript/lunargame/client/src/modules/games/constants.ts b/typescript/lunargame/client/src/modules/games/constants.ts new file mode 100644 index 0000000..acc22ad --- /dev/null +++ b/typescript/lunargame/client/src/modules/games/constants.ts @@ -0,0 +1,14 @@ +import { InfiniteList } from '../../common/dom/classes/InfiniteList' +import { GameChunkElementData } from '../network/types/GameChunkElementData' + +export const GameInfiniteList = new InfiniteList( + 'gameList', + { + pageSize: 5, + urls: { + chunk: 'game/chunk', + count: 'game/count' + }, + initialLoads: 6 + } +) diff --git a/typescript/lunargame/client/src/modules/network/classes/BaseServer.ts b/typescript/lunargame/client/src/modules/network/classes/BaseServer.ts index b9c4844..ee72452 100644 --- a/typescript/lunargame/client/src/modules/network/classes/BaseServer.ts +++ b/typescript/lunargame/client/src/modules/network/classes/BaseServer.ts @@ -26,14 +26,22 @@ export class BaseServer { public async request( url: string, method = 'GET', - body = {} + body = {}, + queryParams: Record = {} ): Promise { 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 = await response.json() const status = response.status diff --git a/typescript/lunargame/client/src/modules/network/types/GameChunkElementData.ts b/typescript/lunargame/client/src/modules/network/types/GameChunkElementData.ts new file mode 100644 index 0000000..7c5a83c --- /dev/null +++ b/typescript/lunargame/client/src/modules/network/types/GameChunkElementData.ts @@ -0,0 +1,5 @@ +export interface GameChunkElementData { + id: string + thumbail: string + avatar: string +}