1
0
Fork 0

improve web client

This commit is contained in:
Sean Sube 2024-05-04 17:18:25 -05:00
parent 3574542a94
commit dc00a78b57
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
3 changed files with 106 additions and 43 deletions

View File

@ -1,8 +1,7 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef, MutableRefObject, Fragment } from 'react';
import useWebSocketModule, { ReadyState } from 'react-use-websocket'; import useWebSocketModule, { ReadyState } from 'react-use-websocket';
import { Maybe, Optional, doesExist } from '@apextoaster/js-utils'; import { Maybe, doesExist } from '@apextoaster/js-utils';
import List from '@mui/material/List'; import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem'; import ListItem from '@mui/material/ListItem';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
@ -16,7 +15,7 @@ import Alert from '@mui/material/Alert';
import Switch from '@mui/material/Switch'; import Switch from '@mui/material/Switch';
import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
import { TreeItem } from '@mui/x-tree-view/TreeItem'; import { TreeItem } from '@mui/x-tree-view/TreeItem';
import { CssBaseline, PaletteMode, ThemeProvider, createTheme } from '@mui/material'; import { Button, CssBaseline, Dialog, DialogContent, DialogTitle, PaletteMode, ThemeProvider, createTheme } from '@mui/material';
import { formatters } from './format.js'; import { formatters } from './format.js';
@ -74,7 +73,7 @@ export function WorldItem(props: EventItemProps) {
return <ListItem alignItems="flex-start"> return <ListItem alignItems="flex-start">
<ListItemAvatar> <ListItemAvatar>
<Avatar alt={step} src="/static/images/avatar/1.jpg" /> <Avatar alt={step.toString()} src="/static/images/avatar/1.jpg" />
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={theme} primary={theme}
@ -165,32 +164,94 @@ export interface World {
theme: string; theme: string;
} }
export function WorldPanel(props: { world: Maybe<World> }) { export type SetDetails = (entity: Maybe<Item | Actor | Room>) => void;
const { world } = props;
return <Stack direction="column"> export function ItemItem(props: { item: Item; setDetails: SetDetails }) {
const { item, setDetails } = props;
return <TreeItem itemId={item.name} label={item.name}>
<TreeItem itemId={`${item.name}-details`} label="Details" onClick={() => setDetails(item)} />
</TreeItem>;
}
export function ActorItem(props: { actor: Actor; setDetails: SetDetails }) {
const { actor, setDetails } = props;
return <TreeItem itemId={actor.name} label={actor.name}>
<TreeItem itemId={`${actor.name}-details`} label="Details" onClick={() => setDetails(actor)} />
<TreeItem itemId={`${actor.name}-items`} label="Items">
{actor.items.map((item) => <ItemItem item={item} setDetails={setDetails} />)}
</TreeItem>
</TreeItem>;
}
export function RoomItem(props: { room: Room; setDetails: SetDetails }) {
const { room, setDetails } = props;
return <TreeItem itemId={room.name} label={room.name}>
<TreeItem itemId={`${room.name}-details`} label="Details" onClick={() => setDetails(room)} />
<TreeItem itemId={`${room.name}-actors`} label="Actors">
{room.actors.map((actor) => <ActorItem actor={actor} setDetails={setDetails} />)}
</TreeItem>
<TreeItem itemId={`${room.name}-items`} label="Items">
{room.items.map((item) => <ItemItem item={item} setDetails={setDetails} />)}
</TreeItem>
</TreeItem>;
}
export function WorldPanel(props: { world: Maybe<World>; setDetails: SetDetails }) {
const { world, setDetails } = props;
// eslint-disable-next-line no-restricted-syntax
if (!doesExist(world)) {
return <Typography variant="h4">
No world data available
</Typography>;
}
return <Stack direction="column" sx={{ minWidth: 600 }}>
<Typography variant="h4"> <Typography variant="h4">
World: {world?.name} World: {world.name}
</Typography> </Typography>
<Typography variant="h6"> <Typography variant="h6">
Theme: {world?.theme} Theme: {world.theme}
</Typography> </Typography>
<SimpleTreeView> <SimpleTreeView>
{world?.rooms.map((room) => <TreeItem itemId={room.name} label={room.name}> {world.rooms.map((room) => <RoomItem room={room} setDetails={setDetails} />)}
{room.actors.map((actor) => <TreeItem itemId={actor.name} label={actor.name}>
{actor.items.map((item) => <TreeItem itemId={item.name} label={item.name} />
)}
</TreeItem>
)}
{room.items.map((item) => <TreeItem itemId={item.name} label={item.name} />
)}
</TreeItem>
)}
</SimpleTreeView> </SimpleTreeView>
</Stack>; </Stack>;
} }
export function EntityDetails(props: { entity: Maybe<Item | Actor | Room>; close: () => void }) {
// eslint-disable-next-line no-restricted-syntax
if (!doesExist(props.entity)) {
return <Fragment />;
}
return <Fragment>
<DialogTitle>{props.entity.name}</DialogTitle>
<DialogContent>
<Typography>
{props.entity.description}
</Typography>
<Button onClick={() => props.close()}>Close</Button>
</DialogContent>
</Fragment>;
}
export function DetailDialog(props: { setDetails: SetDetails; details: Maybe<Item | Actor | Room> }) {
const { details, setDetails } = props;
return <Dialog
open={doesExist(details)}
onClose={() => setDetails(undefined)}
>
<EntityDetails entity={details} close={() => setDetails(undefined)} />
</Dialog>;
}
export function App(props: AppProps) { export function App(props: AppProps) {
const [ detailEntity, setDetailEntity ] = useState<Maybe<Item | Actor | Room>>(undefined);
const [ world, setWorld ] = useState<Maybe<World>>(undefined); const [ world, setWorld ] = useState<Maybe<World>>(undefined);
const [ themeMode, setThemeMode ] = useState('light'); const [ themeMode, setThemeMode ] = useState('light');
const [ history, setHistory ] = useState<Array<string>>([]); const [ history, setHistory ] = useState<Array<string>>([]);
@ -220,26 +281,29 @@ export function App(props: AppProps) {
return <ThemeProvider theme={theme}> return <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
<Container maxWidth='lg'> <DetailDialog details={detailEntity} setDetails={setDetailEntity} />
<Stack direction="row"> <Container maxWidth='xl'>
<WorldPanel world={world} /> <Stack direction="column">
<Stack direction="column"> <Alert icon={false} severity="success">
<Alert icon={false} severity="success"> <Stack direction="row" alignItems="center" gap={4}>
<Stack direction="row" alignItems="center" gap={4}> <Typography>
<Typography> Status: {connectionStatus}
Status: {connectionStatus} </Typography>
</Typography> <Switch
<Switch checked={themeMode === 'dark'}
checked={themeMode === 'dark'} onChange={() => setThemeMode(themeMode === 'dark' ? 'light' : 'dark')}
onChange={() => setThemeMode(themeMode === 'dark' ? 'light' : 'dark')} inputProps={{ 'aria-label': 'controlled' }}
inputProps={{ 'aria-label': 'controlled' }} sx={{ marginLeft: 'auto' }}
sx={{ marginLeft: 'auto' }} />
/> </Stack>
</Stack> </Alert>
</Alert> <Stack direction="row">
<List sx={{ width: '100%', bgcolor: 'background.paper' }}> <WorldPanel world={world} setDetails={setDetailEntity} />
{interleave(items)} <Stack direction="column" sx={{ minWidth: 800 }}>
</List> <List sx={{ width: '100%', bgcolor: 'background.paper' }}>
{interleave(items)}
</List>
</Stack>
</Stack> </Stack>
</Stack> </Stack>
</Container> </Container>

View File

@ -26,7 +26,7 @@ export function formatWorld(data: any) {
} }
export const formatters: Record<string, any> = { export const formatters: Record<string, any> = {
input: formatInput, action: formatInput,
result: formatResult, result: formatResult,
world: formatWorld, world: formatWorld,
}; };

View File

@ -4,8 +4,7 @@
<title>Text World</title> <title>Text World</title>
</head> </head>
<body> <body>
<div class="history"> <div id="history">
<ol id="history"></ol>
</div> </div>
<script src="./bundle/main.js" type="module"></script> <script src="./bundle/main.js" type="module"></script>
</body> </body>