Delete old visions and yew prototypes
This commit is contained in:
parent
0387429021
commit
bf6cb0f054
23
visions-prototype/ui/.gitignore
vendored
23
visions-prototype/ui/.gitignore
vendored
@ -1,23 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@ -1,46 +0,0 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
@ -1,14 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
dev:
|
||||
cmds:
|
||||
- cd ../visions-types && task build
|
||||
- npm install
|
||||
- npm run start
|
||||
|
||||
test:
|
||||
cmds:
|
||||
- cd ../visions-types && task build
|
||||
- npm install
|
||||
- npm run test
|
||||
14754
visions-prototype/ui/package-lock.json
generated
14754
visions-prototype/ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,51 +0,0 @@
|
||||
{
|
||||
"name": "ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.119",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-router": "^5.1.20",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"classnames": "^2.5.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router": "^6.28.0",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-use-websocket": "^4.11.1",
|
||||
"typescript": "^4.9.5",
|
||||
"visions-types": "../visions-types",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB |
@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@ -1,37 +0,0 @@
|
||||
.App {
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
import React, { PropsWithChildren, useContext, useEffect, useState } from 'react'
|
||||
import './App.css'
|
||||
import { Client } from './client'
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
||||
import { DesignPage } from './views/Design/Design'
|
||||
import Candela from './plugins/Candela'
|
||||
import { Authentication } from './views/Authentication/Authentication'
|
||||
import { StateContext, StateProvider } from './providers/StateProvider/StateProvider'
|
||||
import { MainView } from './views'
|
||||
import { AdminView } from './views/Admin/Admin'
|
||||
|
||||
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"
|
||||
|
||||
interface AppProps {
|
||||
client: Client
|
||||
}
|
||||
|
||||
/*
|
||||
const CandelaCharsheet = ({ client }: { client: Client }) => {
|
||||
let [sheet, setSheet] = useState(undefined)
|
||||
useEffect(
|
||||
() => { client.charsheet(TEST_CHARSHEET_UUID).then((c) => setSheet(c)) },
|
||||
[client, setSheet]
|
||||
)
|
||||
|
||||
return sheet ? <Candela.CharsheetElement sheet={sheet} /> : <div> </div>
|
||||
}
|
||||
*/
|
||||
|
||||
const App = ({ client }: AppProps) => {
|
||||
console.log("rendering app")
|
||||
const [websocketUrl, setWebsocketUrl] = useState<string | undefined>(undefined)
|
||||
|
||||
// useEffect(() => {
|
||||
// client.registerWebsocket().then((url) => setWebsocketUrl(url))
|
||||
// }, [client])
|
||||
|
||||
let router =
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: (
|
||||
<StateProvider client={client}>
|
||||
<AuthedView client={client}>
|
||||
<MainView client={client} />
|
||||
</AuthedView>
|
||||
</StateProvider>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
element: <AdminView client={client} />
|
||||
},
|
||||
/*
|
||||
{
|
||||
path: "/candela",
|
||||
element: <CandelaCharsheet client={client} />
|
||||
},
|
||||
*/
|
||||
{
|
||||
path: "/design",
|
||||
element: <DesignPage />
|
||||
}
|
||||
])
|
||||
return (
|
||||
<div className="App">
|
||||
<RouterProvider router={router} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@ -1,149 +0,0 @@
|
||||
import { AuthResponse, SessionId, UserId, UserOverview } from "visions-types";
|
||||
|
||||
export type PlayingField = {
|
||||
backgroundImage: string;
|
||||
}
|
||||
|
||||
export type ServerResponse<A> = { type: "Unauthorized" } | { type: "Unexpected", status: number } | A;
|
||||
|
||||
export interface Client {
|
||||
users: (sessionId: SessionId) => Promise<Array<UserOverview>>;
|
||||
createUser: (sessionId: SessionId, username: string) => Promise<UserId>;
|
||||
auth: (username: string, password: string) => Promise<ServerResponse<AuthResponse>>;
|
||||
setPassword: (sessionId: SessionId, password_1: string, password_2: string) => Promise<void>;
|
||||
logout: (sessionId: SessionId) => Promise<void>;
|
||||
}
|
||||
|
||||
export class Connection implements Client {
|
||||
private base: URL;
|
||||
private sessionId: string | undefined;
|
||||
|
||||
constructor() {
|
||||
this.base = new URL("http://localhost:8001");
|
||||
}
|
||||
|
||||
registerWebsocket() {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `api/v1/client`;
|
||||
return fetch(url, { method: 'POST' }).then((response) => response.json()).then((ws) => ws.url);
|
||||
}
|
||||
|
||||
/*
|
||||
unregisterWebsocket() {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `api/v1/client`;
|
||||
return fetch(url, { method: 'POST' }).then((response => response.json()));
|
||||
}
|
||||
*/
|
||||
|
||||
imageUrl(imageId: string) {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/image/${imageId}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
async playingField(): Promise<PlayingField> {
|
||||
return { backgroundImage: "trans-ferris.jpg" };
|
||||
}
|
||||
|
||||
async availableImages(): Promise<string[]> {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/image`;
|
||||
return fetch(url).then((response) => response.json());
|
||||
}
|
||||
|
||||
async setBackgroundImage(name: string) {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/tabletop/bg_image`;
|
||||
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(name) });
|
||||
}
|
||||
|
||||
async users(sessionId: string): Promise<Array<UserOverview>> {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = '/api/v1/users';
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: [['Authorization', `Bearer ${sessionId}`]]
|
||||
}).then((response) => response.json());
|
||||
}
|
||||
|
||||
async charsheet(id: string) {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/charsheet/${id}`;
|
||||
return fetch(url).then((response) => response.json());
|
||||
}
|
||||
|
||||
async createUser(sessionId: string, username: string): Promise<UserId> {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = '/api/v1/user';
|
||||
const response: Response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: [['Authorization', `Bearer ${sessionId}`],
|
||||
['Content-Type', 'application/json']],
|
||||
body: JSON.stringify({ username }),
|
||||
});
|
||||
const userId: UserId = await response.json();
|
||||
return userId;
|
||||
}
|
||||
|
||||
async setPassword(sessionId: string, password_1: string, password_2: string) {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/user/password`;
|
||||
|
||||
await fetch(url, {
|
||||
method: 'PUT', headers: [['Authorization', `Bearer ${sessionId}`], ['Content-Type', 'application/json']], body: JSON.stringify({
|
||||
password_1, password_2,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async auth(username: string, password: string): Promise<ServerResponse<AuthResponse>> {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/auth`
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: [['Content-Type', 'application/json']],
|
||||
body: JSON.stringify({ 'username': username, 'password': password })
|
||||
});
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else if (response.status == 401) {
|
||||
return await response.json().then(() => ({ type: "Unauthorized" }));
|
||||
} else {
|
||||
return await response.json().then(() => ({ type: "Unexpected", status: response.status }));
|
||||
}
|
||||
}
|
||||
|
||||
async logout(sessionId: string) {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/auth`
|
||||
await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: [['Authorization', `Bearer ${sessionId}`]],
|
||||
});
|
||||
}
|
||||
|
||||
async profile(sessionId: SessionId, userId: UserId | undefined): Promise<UserOverview | undefined> {
|
||||
const url = new URL(this.base);
|
||||
if (userId) {
|
||||
url.pathname = `/api/v1/user${userId}`
|
||||
} else {
|
||||
url.pathname = `/api/v1/user`
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: [['Authorization', `Bearer ${sessionId}`]],
|
||||
});
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
async health() {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/health`;
|
||||
return fetch(url).then((response) => response.json()).then((response) => {
|
||||
console.log("health response: ", response);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
.card {
|
||||
border: var(--border-standard);
|
||||
border-radius: var(--border-radius-standard);
|
||||
box-shadow: var(--border-shadow-shallow);
|
||||
padding: var(--padding-m);
|
||||
}
|
||||
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import './Card.css';
|
||||
|
||||
interface CardElementProps {
|
||||
name?: string,
|
||||
}
|
||||
|
||||
export const CardElement = ({ name, children }: PropsWithChildren<CardElementProps>) => (
|
||||
<div className="card">
|
||||
{name && <h1 className="card__title">{name}</h1> }
|
||||
<div className="card__body">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import { GameOverview } from "visions-types"
|
||||
import { CardElement } from '../Card/Card';
|
||||
|
||||
|
||||
export const GameOverviewElement = ({ name, gm, players }: GameOverview) => {
|
||||
return (<CardElement name={name}>
|
||||
<p><i>GM</i> {gm}</p>
|
||||
<ul>
|
||||
{players.map((player) => player)}
|
||||
</ul>
|
||||
</CardElement>)
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
interface GuageProps {
|
||||
current: number,
|
||||
max: number,
|
||||
}
|
||||
|
||||
export const SimpleGuage = ({ current, max }: GuageProps) => <> {current} / {max}</>
|
||||
@ -1,24 +0,0 @@
|
||||
.modal {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(128, 128, 128, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal_window {
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.modal_title {
|
||||
background-color: skyblue;
|
||||
}
|
||||
|
||||
.modal_cta {
|
||||
margin-top: var(--margin-s);
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import { PropsWithChildren } from "react";
|
||||
import "./Modal.css";
|
||||
|
||||
export type Action = {
|
||||
name: string
|
||||
action: () => void
|
||||
}
|
||||
|
||||
export interface ModalProps {
|
||||
title: string
|
||||
|
||||
onCancel: Action
|
||||
onPrimary: Action
|
||||
onSecondary?: Action
|
||||
}
|
||||
|
||||
export const Modal = ({ title, children, onCancel, onPrimary, onSecondary }: PropsWithChildren<ModalProps>) => (
|
||||
<div className="modal">
|
||||
<div className="modal_window">
|
||||
<h1 className="modal_title"> {title} </h1>
|
||||
<div className="modal_body">
|
||||
{children}
|
||||
</div>
|
||||
<footer className="modal_cta">
|
||||
<button onClick={() => onCancel.action()}>{onCancel.name}</button>
|
||||
{onSecondary ? <button onClick={() => onSecondary.action()}>{onSecondary.name}</button> : <></>}
|
||||
<button onClick={() => onPrimary.action()}>{onPrimary.name}</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
.profile {
|
||||
margin: var(--margin-s);
|
||||
}
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import { GameOverview, UserOverview } from 'visions-types';
|
||||
import { CardElement, GameOverviewElement, UserManagementElement } from '..';
|
||||
import './Profile.css';
|
||||
|
||||
interface ProfileProps {
|
||||
profile: UserOverview,
|
||||
games: GameOverview[],
|
||||
}
|
||||
|
||||
export const ProfileElement = ({ profile, games }: ProfileProps) => {
|
||||
const adminNote = profile.isAdmin ? <div> <i>Note: this user is an admin</i> </div> : <></>;
|
||||
|
||||
return (<div className="profile profile_columns">
|
||||
<CardElement name={profile.name}>
|
||||
<div>Games: {games.map((game) => {
|
||||
return <span key={game.id}>{game.name} ({game.type})</span>;
|
||||
}) }</div>
|
||||
{adminNote}
|
||||
</CardElement>
|
||||
</div>)
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
.tabletop > img {
|
||||
max-width: 100%;
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import './Tabletop.css';
|
||||
import { Rgb } from 'visions-types';
|
||||
|
||||
interface TabletopElementProps {
|
||||
backgroundColor: Rgb;
|
||||
backgroundUrl: URL | undefined;
|
||||
}
|
||||
|
||||
export const TabletopElement = ({ backgroundColor, backgroundUrl }: TabletopElementProps) => {
|
||||
const tabletopColorStyle = `rgb(${backgroundColor.red}, ${backgroundColor.green}, ${backgroundColor.blue})`;
|
||||
|
||||
return <div className="tabletop"> {backgroundUrl && <img src={backgroundUrl.toString()} alt="playing field" />} </div>
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
.thumbnail > img {
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Client } from '../../client';
|
||||
import './Thumbnail.css';
|
||||
|
||||
interface ThumbnailProps {
|
||||
id: string;
|
||||
url: URL;
|
||||
onclick?: () => void;
|
||||
}
|
||||
|
||||
export const ThumbnailElement = ({ id, url, onclick }: ThumbnailProps) => {
|
||||
const clickHandler = () => {
|
||||
if (onclick) { onclick(); }
|
||||
}
|
||||
return (<div id={id} className="thumbnail" onClick={clickHandler}> <img src={url.toString()} /> </div>)
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
import { PropsWithChildren, useState } from "react"
|
||||
import { AccountState, UserOverview } from "visions-types"
|
||||
import { CardElement} from ".."
|
||||
|
||||
interface UserManagementProps {
|
||||
users: UserOverview[]
|
||||
onShowCreateUser: () => void
|
||||
}
|
||||
|
||||
export const UserManagementElement = ({ users, onShowCreateUser }: UserManagementProps) => {
|
||||
return (
|
||||
<>
|
||||
<CardElement name="Users">
|
||||
<table>
|
||||
<thead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user) => <tr key={user.id}>
|
||||
<td> {user.name} </td>
|
||||
<td> {formatAccountState(user.state)} </td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
<button onClick={() => onShowCreateUser()}>Create User</button>
|
||||
</CardElement>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const formatAccountState = (state: AccountState): string => {
|
||||
switch (state.type) {
|
||||
case "Normal": return "";
|
||||
case "PasswordReset": return "password reset";
|
||||
case "Locked": return "locked";
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
import React, { createContext, PropsWithChildren, useEffect, useReducer } from "react";
|
||||
import useWebSocket from "react-use-websocket";
|
||||
import { Message, Tabletop } from "visions-types";
|
||||
|
||||
type WebsocketState = { }
|
||||
|
||||
export const WebsocketContext = createContext<WebsocketState>({});
|
||||
|
||||
interface WebsocketProviderProps {
|
||||
websocketUrl: string;
|
||||
}
|
||||
|
||||
export const WebsocketProvider = ({ websocketUrl, children }: PropsWithChildren<WebsocketProviderProps>) => {
|
||||
return <div> {children} </div>;
|
||||
/*
|
||||
const { lastMessage } = useWebSocket(websocketUrl);
|
||||
|
||||
const [state, dispatch] = useReducer(handleMessage, initialState());
|
||||
|
||||
useEffect(() => {
|
||||
if (lastMessage !== null) {
|
||||
const message: Message = JSON.parse(lastMessage.data);
|
||||
dispatch(message);
|
||||
}
|
||||
}, [lastMessage]);
|
||||
|
||||
return (<WebsocketContext.Provider value={state}>
|
||||
{children}
|
||||
</WebsocketContext.Provider>);
|
||||
*/
|
||||
}
|
||||
|
||||
/*
|
||||
const handleMessage = (state: TabletopState, message: Message): TabletopState => {
|
||||
console.log(message);
|
||||
switch (message.type) {
|
||||
case "UpdateTabletop": {
|
||||
return {
|
||||
...state,
|
||||
tabletop: message.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
@ -1,10 +0,0 @@
|
||||
import { CardElement } from './Card/Card'
|
||||
import { GameOverviewElement } from './GameOverview/GameOverview'
|
||||
import { Modal, ModalProps } from './Modal/Modal'
|
||||
import { ProfileElement } from './Profile/Profile'
|
||||
import { SimpleGuage } from './Guages/SimpleGuage'
|
||||
import { ThumbnailElement } from './Thumbnail/Thumbnail'
|
||||
import { TabletopElement } from './Tabletop/Tabletop'
|
||||
import { UserManagementElement } from './UserManagement/UserManagement'
|
||||
|
||||
export { CardElement, GameOverviewElement, Modal, type ModalProps, UserManagementElement, ProfileElement, ThumbnailElement, TabletopElement, SimpleGuage }
|
||||
@ -1,8 +0,0 @@
|
||||
:root {
|
||||
--border-standard: 2px solid black;
|
||||
--border-radius-standard: 4px;
|
||||
--border-shadow-shallow: 1px 1px 2px black;
|
||||
--padding-m: 8px;
|
||||
--margin-s: 4px;
|
||||
}
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { Connection } from './client';
|
||||
|
||||
const client = new Connection();
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App client={client} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@ -1,110 +0,0 @@
|
||||
.charsheet__header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.charsheet__header > div {
|
||||
margin: 8px;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.charsheet__body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.charsheet__body > div {
|
||||
margin: 8px;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
position: relative;
|
||||
border: 2px solid black;
|
||||
border-radius: 4px;
|
||||
|
||||
padding-left: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.action-group:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
.action-group__header {
|
||||
display: flex;
|
||||
font-size: xx-large;
|
||||
justify-content: space-between;
|
||||
margin: 4px;
|
||||
margin-left: -4px;
|
||||
padding-left: 4px;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-group__action {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.action-group__dots {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.action-group__guage-column {
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.action-group__guage-top {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.action-group__guage-top:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
left: 2px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.action-group__guage-top_filled {
|
||||
background-color: black;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.action-group__guage-bottom {
|
||||
background-color: white;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.action-group__guage-bottom_filled {
|
||||
background-color: black;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.action-group__guage-bottom:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
bottom: 1px;
|
||||
}
|
||||
|
||||
.action-group__guage {
|
||||
display: flex;
|
||||
}
|
||||
@ -1,164 +0,0 @@
|
||||
import React from 'react';
|
||||
import { assertNever } from '../../utils';
|
||||
import './Charsheet.css';
|
||||
import { DriveGuage } from './DriveGuage/DriveGuage';
|
||||
import { Charsheet, Nerve, Cunning, Intuition } from './types';
|
||||
|
||||
interface CharsheetProps {
|
||||
sheet: Charsheet,
|
||||
}
|
||||
|
||||
interface ActionElementProps {
|
||||
name: string,
|
||||
gilded: boolean,
|
||||
value: number,
|
||||
}
|
||||
|
||||
const ActionElement = ({ name, gilded, value }: ActionElementProps) => {
|
||||
let dots = [];
|
||||
for (let i = 0; i < value; i++) {
|
||||
dots.push("\u25ef");
|
||||
}
|
||||
let diamond = gilded ? "\u25c6" : "\u25c7";
|
||||
return (<div>
|
||||
<h2 className="action-group__action"> {diamond} {name} </h2>
|
||||
<div className="action-group__dots"> {dots} </div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
interface ActionGroupElementProps {
|
||||
group: Nerve | Cunning | Intuition;
|
||||
}
|
||||
|
||||
const ActionGroupElement = ({ group }: ActionGroupElementProps) => {
|
||||
var title;
|
||||
var elements = [];
|
||||
|
||||
switch (group.type_) {
|
||||
case "nerve": {
|
||||
title = <div className="action-group__header"> Nerve <DriveGuage current={group.drives.current} max={group.drives.max} /> </div>
|
||||
elements.push(<ActionElement name="Move" gilded={group.move.gilded} value={group.move.score} />);
|
||||
elements.push(<ActionElement name="Strike" gilded={group.strike.gilded} value={group.strike.score} />);
|
||||
elements.push(<ActionElement name="Control" gilded={group.control.gilded} value={group.control.score} />);
|
||||
break
|
||||
}
|
||||
case "cunning": {
|
||||
title = <div className="action-group__header"> Cunning <DriveGuage current={group.drives.current} max={group.drives.max} /> </div>
|
||||
elements.push(<ActionElement name="Sway" gilded={group.sway.gilded} value={group.sway.score} />);
|
||||
elements.push(<ActionElement name="Read" gilded={group.read.gilded} value={group.read.score} />);
|
||||
elements.push(<ActionElement name="Hide" gilded={group.hide.gilded} value={group.hide.score} />);
|
||||
break
|
||||
}
|
||||
case "intuition": {
|
||||
title = <div className="action-group__header"> Intuition <DriveGuage current={group.drives.current} max={group.drives.max} /> </div>
|
||||
elements.push(<ActionElement name="Survey" gilded={group.survey.gilded} value={group.survey.score} />);
|
||||
elements.push(<ActionElement name="Focus" gilded={group.focus.gilded} value={group.focus.score} />);
|
||||
elements.push(<ActionElement name="Sense" gilded={group.sense.gilded} value={group.sense.score} />);
|
||||
break
|
||||
}
|
||||
default: {
|
||||
assertNever(group);
|
||||
}
|
||||
}
|
||||
|
||||
return (<div className="action-group">
|
||||
{title}
|
||||
{elements}
|
||||
</div>)
|
||||
}
|
||||
|
||||
interface AbilitiesElementProps {
|
||||
role: string
|
||||
role_abilities: string[]
|
||||
specialty: string
|
||||
specialty_abilities: string[]
|
||||
}
|
||||
|
||||
const AbilitiesElement = ({ role, role_abilities, specialty, specialty_abilities }: AbilitiesElementProps) => {
|
||||
return (<div>
|
||||
<h1> ROLE: {role} </h1>
|
||||
<ul>
|
||||
{role_abilities.map((ability) => <li>{ability}</li>)}
|
||||
</ul>
|
||||
<h1> SPECIALTY: {role} </h1>
|
||||
<ul>
|
||||
{specialty_abilities.map((ability) => <li>{ability}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const CharsheetElement = ({ sheet }: CharsheetProps) => {
|
||||
return (<div>
|
||||
<div className="charsheet__header">
|
||||
<div> Candela Obscura </div>
|
||||
<div>
|
||||
<p> {sheet.name} </p>
|
||||
<p> {sheet.pronouns} </p>
|
||||
<p> {sheet.circle} </p>
|
||||
</div>
|
||||
<div>
|
||||
<p> {sheet.style} </p>
|
||||
<p> {sheet.catalyst} </p>
|
||||
<p> {sheet.question} </p>
|
||||
</div>
|
||||
</div >
|
||||
<div className="charsheet__body">
|
||||
<div>
|
||||
<ActionGroupElement group={sheet.nerve} />
|
||||
<ActionGroupElement group={sheet.cunning} />
|
||||
<ActionGroupElement group={sheet.intuition} />
|
||||
</div>
|
||||
<div> <AbilitiesElement {...sheet} /> </div>
|
||||
<div> Marks, Scars, Relationships </div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
/*
|
||||
export const CharsheetElement = () => {
|
||||
const sheet = {
|
||||
type_: 'Candela',
|
||||
name: "Soren Jensen",
|
||||
pronouns: 'he/him',
|
||||
circle: 'Circle of the Bluest Sky',
|
||||
style: 'dapper gentleman',
|
||||
catalyst: 'a cursed book',
|
||||
question: 'What were the contents of that book?',
|
||||
nerve: {
|
||||
type_: "nerve",
|
||||
drives: { current: 1, max: 2 },
|
||||
resistances: { current: 0, max: 3 },
|
||||
move: { gilded: false, score: 2 },
|
||||
strike: { gilded: false, score: 1 },
|
||||
control: { gilded: true, score: 0 },
|
||||
} as Nerve,
|
||||
cunning: {
|
||||
type_: "cunning",
|
||||
drives: { current: 1, max: 1 },
|
||||
resistances: { current: 0, max: 3 },
|
||||
sway: { gilded: false, score: 0 },
|
||||
read: { gilded: false, score: 0 },
|
||||
hide: { gilded: false, score: 0 },
|
||||
} as Cunning,
|
||||
intuition: {
|
||||
type_: "intuition",
|
||||
drives: { current: 0, max: 0 },
|
||||
resistances: { current: 0, max: 3 },
|
||||
survey: { gilded: false, score: 0 },
|
||||
focus: { gilded: false, score: 0 },
|
||||
sense: { gilded: false, score: 0 },
|
||||
} as Intuition,
|
||||
role: 'Slink',
|
||||
role_abilities: [
|
||||
'Scout: If you have time to observe a location, you can spend 1 Intuition to ask a question: What do I notice here that others do not see? What in this place might be of use to us? What path should we follow?',
|
||||
],
|
||||
specialty: 'Detective',
|
||||
specialty_abilities: [
|
||||
"Mind Palace: When you want to figure out how two clues might relate or what path they should point you towards, burn 1 Intution resistance. The GM will give you the information you've deduced.",
|
||||
],
|
||||
};
|
||||
|
||||
return <CharsheetElement_ sheet={sheet} />
|
||||
}
|
||||
*/
|
||||
@ -1,24 +0,0 @@
|
||||
.candela-panel-actions {
|
||||
border: 1px solid black;
|
||||
padding: 4px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.candela-panel-actions__header {
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
justify-content: space-between;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.candela-panel-actions__action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.candela-panel-actions__action_gilded {
|
||||
font-weight: bold;
|
||||
}
|
||||
@ -1,137 +0,0 @@
|
||||
import React from 'react';
|
||||
import { SimpleGuage } from '../../components/Guages/SimpleGuage';
|
||||
import { Charsheet, Nerve, Cunning, Intuition } from './types';
|
||||
import './CharsheetPanel.css';
|
||||
import classNames from 'classnames';
|
||||
import { assertNever } from '../../utils';
|
||||
|
||||
interface CharsheetPanelProps {
|
||||
sheet: Charsheet;
|
||||
}
|
||||
|
||||
interface ActionElementProps {
|
||||
name: string,
|
||||
gilded: boolean,
|
||||
value: number,
|
||||
}
|
||||
|
||||
const ActionElement = ({ name, gilded, value }: ActionElementProps) => {
|
||||
const className = gilded ? "candela-panel-actions__action_gilded" : "candela-panel-actions__action";
|
||||
return (<div className={classNames({
|
||||
"candela-panel-actions__action": true,
|
||||
"candela-panel-actions__action_gilded": gilded,
|
||||
})}> <div> {name} </div> <div> {value} </div> </div>);
|
||||
}
|
||||
|
||||
|
||||
interface ActionGroupElementProps {
|
||||
group: Nerve | Cunning | Intuition;
|
||||
}
|
||||
|
||||
const ActionGroupElement = ({ group }: ActionGroupElementProps) => {
|
||||
var title;
|
||||
var elements = [];
|
||||
|
||||
switch (group.type_) {
|
||||
case "nerve": {
|
||||
title = <div className="candela-panel-actions__header"> <div> Nerve </div> <SimpleGuage current={group.drives.current} max={group.drives.max} /> </div>
|
||||
elements.push(<ActionElement name="Move" gilded={group.move.gilded} value={group.move.score} />);
|
||||
elements.push(<ActionElement name="Strike" gilded={group.strike.gilded} value={group.strike.score} />);
|
||||
elements.push(<ActionElement name="Control" gilded={group.control.gilded} value={group.control.score} />);
|
||||
break
|
||||
}
|
||||
case "cunning": {
|
||||
title = <div className="candela-panel-actions__header"> <div> Cunning </div> <SimpleGuage current={group.drives.current} max={group.drives.max} /> </div>
|
||||
elements.push(<ActionElement name="Sway" gilded={group.sway.gilded} value={group.sway.score} />);
|
||||
elements.push(<ActionElement name="Read" gilded={group.read.gilded} value={group.read.score} />);
|
||||
elements.push(<ActionElement name="Hide" gilded={group.hide.gilded} value={group.hide.score} />);
|
||||
break
|
||||
}
|
||||
case "intuition": {
|
||||
title = <div className="candela-panel-actions__header"> <div> Intuition </div> <SimpleGuage current={group.drives.current} max={group.drives.max} /> </div>
|
||||
elements.push(<ActionElement name="Survey" gilded={group.survey.gilded} value={group.survey.score} />);
|
||||
elements.push(<ActionElement name="Focus" gilded={group.focus.gilded} value={group.focus.score} />);
|
||||
elements.push(<ActionElement name="Sense" gilded={group.sense.gilded} value={group.sense.score} />);
|
||||
break
|
||||
}
|
||||
default: {
|
||||
assertNever(group);
|
||||
}
|
||||
}
|
||||
|
||||
return (<div className="candela-panel-actions">
|
||||
{title}
|
||||
{elements}
|
||||
</div>)
|
||||
}
|
||||
|
||||
|
||||
export const CharsheetPanelElement = ({ sheet }: CharsheetPanelProps) => {
|
||||
return (<div className="candela-panel">
|
||||
<div className="candela-panel__header">
|
||||
<p> {sheet.name} ({sheet.pronouns}) </p>
|
||||
<p> {sheet.specialty} </p>
|
||||
</div>
|
||||
|
||||
<div className="candela-panel__action-groups">
|
||||
<ActionGroupElement group={sheet.nerve} />
|
||||
<ActionGroupElement group={sheet.cunning} />
|
||||
<ActionGroupElement group={sheet.intuition} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ul>
|
||||
{sheet.role_abilities.map((ability) => <li> {ability} </li>)}
|
||||
{sheet.specialty_abilities.map((ability) => <li> {ability} </li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
/*
|
||||
export const CharsheetPanelElement = () => {
|
||||
const sheet = {
|
||||
type_: 'Candela',
|
||||
name: "Soren Jensen",
|
||||
pronouns: 'he/him',
|
||||
circle: 'Circle of the Bluest Sky',
|
||||
style: 'dapper gentleman',
|
||||
catalyst: 'a cursed book',
|
||||
question: 'What were the contents of that book?',
|
||||
nerve: {
|
||||
type_: "nerve",
|
||||
drives: { current: 1, max: 2 },
|
||||
resistances: { current: 0, max: 3 },
|
||||
move: { gilded: false, score: 2 },
|
||||
strike: { gilded: false, score: 1 },
|
||||
control: { gilded: true, score: 0 },
|
||||
} as Nerve,
|
||||
cunning: {
|
||||
type_: "cunning",
|
||||
drives: { current: 1, max: 1 },
|
||||
resistances: { current: 0, max: 3 },
|
||||
sway: { gilded: false, score: 0 },
|
||||
read: { gilded: false, score: 0 },
|
||||
hide: { gilded: false, score: 0 },
|
||||
} as Cunning,
|
||||
intuition: {
|
||||
type_: "intuition",
|
||||
drives: { current: 0, max: 0 },
|
||||
resistances: { current: 0, max: 3 },
|
||||
survey: { gilded: false, score: 0 },
|
||||
focus: { gilded: false, score: 0 },
|
||||
sense: { gilded: false, score: 0 },
|
||||
} as Intuition,
|
||||
role: 'Slink',
|
||||
role_abilities: [
|
||||
'Scout: If you have time to observe a location, you can spend 1 Intuition to ask a question: What do I notice here that others do not see? What in this place might be of use to us? What path should we follow?',
|
||||
],
|
||||
specialty: 'Detective',
|
||||
specialty_abilities: [
|
||||
"Mind Palace: When you want to figure out how two clues might relate or what path they should point you towards, burn 1 Intution resistance. The GM will give you the information you've deduced.",
|
||||
],
|
||||
};
|
||||
|
||||
return <CharsheetPanelElement_ sheet={sheet} />
|
||||
}
|
||||
*/
|
||||
@ -1,44 +0,0 @@
|
||||
.drive-guages_on-light {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.drive-guage {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.drive-guage__element {
|
||||
border: 1px solid black;
|
||||
margin: 1px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.drive-guage__spacer {
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.drive-guage__top {
|
||||
margin: 1px;
|
||||
width: 8px;
|
||||
border: 1px solid black;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.drive-guage__top_filled {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.drive-guage__bottom {
|
||||
margin: 1px;
|
||||
border: 1px solid black;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.drive-guage__bottom_filled {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.drive-guages_on-dark {
|
||||
background-color: black;
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import './DriveGuage.css';
|
||||
|
||||
interface Guage {
|
||||
current: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export const DriveGuage = ({ current, max }: Guage) => {
|
||||
let components = [];
|
||||
for (let i = 0; i < 9; i++) {
|
||||
components.push(<div className="drive-guage__element">
|
||||
{i < current ? <div className="drive-guage__top drive-guage__top_filled"> </div> :
|
||||
<div className="drive-guage__top"> </div>}
|
||||
{i < max ? <div className="drive-guage__bottom drive-guage__bottom_filled"> </div> :
|
||||
<div className="drive-guage__bottom"> </div>}
|
||||
</div>)
|
||||
}
|
||||
components.splice(3, 0, <div className="drive-guage__spacer"> </div>);
|
||||
components.splice(7, 0, <div className="drive-guage__spacer"> </div>);
|
||||
return (<div className="drive-guage">
|
||||
{components}
|
||||
</div>)
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import { CharsheetElement } from './Charsheet';
|
||||
import { CharsheetPanelElement } from './CharsheetPanel';
|
||||
|
||||
export default { CharsheetElement, CharsheetPanelElement };
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
|
||||
export type Guage = {
|
||||
current: number,
|
||||
max: number,
|
||||
}
|
||||
|
||||
export type Action = {
|
||||
gilded: boolean,
|
||||
score: number,
|
||||
}
|
||||
|
||||
export type Actions = { [key: string]: Action }
|
||||
|
||||
export type ActionGroup = {
|
||||
drives: Guage,
|
||||
resistances: Guage,
|
||||
actions: Actions,
|
||||
}
|
||||
|
||||
export type Nerve = {
|
||||
type_: "nerve",
|
||||
drives: Guage,
|
||||
resistances: Guage,
|
||||
move: Action,
|
||||
strike: Action,
|
||||
control: Action,
|
||||
}
|
||||
|
||||
export type Cunning = {
|
||||
type_: "cunning",
|
||||
drives: Guage,
|
||||
resistances: Guage,
|
||||
sway: Action,
|
||||
read: Action,
|
||||
hide: Action,
|
||||
}
|
||||
|
||||
export type Intuition = {
|
||||
type_: "intuition",
|
||||
drives: Guage,
|
||||
resistances: Guage,
|
||||
survey: Action,
|
||||
focus: Action,
|
||||
sense: Action,
|
||||
}
|
||||
|
||||
export type Charsheet = {
|
||||
type_: string,
|
||||
name: string,
|
||||
pronouns: string
|
||||
circle: string
|
||||
style: string,
|
||||
catalyst: string,
|
||||
question: string,
|
||||
|
||||
nerve: Nerve,
|
||||
cunning: Cunning,
|
||||
intuition: Intuition,
|
||||
|
||||
role: string,
|
||||
role_abilities: string[],
|
||||
specialty: string,
|
||||
specialty_abilities: string[],
|
||||
}
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { UserOverview, AuthResponse, SessionId, UserId } from "visions-types";
|
||||
import { Client, ServerResponse } from "../../client";
|
||||
import { StateProvider } from "./StateProvider";
|
||||
import { AuthedView, Authentication } from '../../views/Authentication/Authentication';
|
||||
|
||||
class MockClient implements Client {
|
||||
validUsers: {[username: string]: string};
|
||||
constructor() {
|
||||
this.validUsers = { "vakarian": "aoeu", "shephard": "rubbish" }
|
||||
}
|
||||
|
||||
async users(sessionId: SessionId): Promise<UserOverview[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async createUser(sessionId: SessionId, username: string): Promise<UserId> {
|
||||
return "abcdefg";
|
||||
}
|
||||
|
||||
async setPassword(sessionId: SessionId, password_1: string, password_2: string): Promise<void> {
|
||||
}
|
||||
|
||||
async auth(username: string, password: string): Promise<ServerResponse<AuthResponse>> {
|
||||
if (this.validUsers[username] === password) {
|
||||
return { type: "Success", content: "session-id" };
|
||||
} else {
|
||||
return { type: "Unauthorized" };
|
||||
}
|
||||
}
|
||||
|
||||
async logout(sessionId: SessionId): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
test('a user is able to authenticate', async () => {
|
||||
const client = new MockClient;
|
||||
render(<StateProvider client={client}>
|
||||
<AuthedView>
|
||||
<p>Hi, authentication complete</p>
|
||||
</AuthedView>
|
||||
</StateProvider>);
|
||||
expect(screen.getByText(/Welcome to Visions VTT/i)).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.getByPlaceholderText("Username"), { target: { value: "vakarian" } });
|
||||
fireEvent.change(screen.getByPlaceholderText("Password"), { target: { value: "aoeu" } });
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
});
|
||||
|
||||
expect(screen.getByText(/Hi, authentication complete/i)).toBeInTheDocument();
|
||||
})
|
||||
@ -1,156 +0,0 @@
|
||||
import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer, useRef } from "react";
|
||||
import { SessionId, Status, Tabletop } from "visions-types";
|
||||
import { Client } from "../../client";
|
||||
import { assertNever } from "../../utils";
|
||||
|
||||
type AuthState = { type: "Unauthed" } | { type: "Authed", sessionId: string } | { type: "PasswordReset", sessionId: string };
|
||||
|
||||
export enum LoadingState {
|
||||
Loading,
|
||||
Ready,
|
||||
}
|
||||
|
||||
type AppState = {
|
||||
state: LoadingState,
|
||||
auth: AuthState,
|
||||
tabletop: Tabletop,
|
||||
}
|
||||
|
||||
type Action = { type: "SetAuthState", content: AuthState };
|
||||
|
||||
const initialState = (): AppState => {
|
||||
let state: AppState = {
|
||||
state: LoadingState.Ready,
|
||||
auth: { type: "Unauthed" },
|
||||
tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined },
|
||||
}
|
||||
|
||||
const sessionId = window.localStorage.getItem("sessionId")
|
||||
if (sessionId) {
|
||||
return { ...state, auth: { type: "Authed", sessionId } }
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const stateReducer = (state: AppState, action: Action): AppState => {
|
||||
switch (action.type) {
|
||||
case "SetAuthState": {
|
||||
console.log("setReducer: ", action);
|
||||
return { ...state, auth: action.content }
|
||||
}
|
||||
/*
|
||||
default: {
|
||||
assertNever(action)
|
||||
return state
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
export const authState = (state: AppState): AuthState => state.auth
|
||||
|
||||
export const getSessionId = (state: AppState): SessionId | undefined => {
|
||||
switch (state.auth.type) {
|
||||
case "Unauthed": return undefined
|
||||
case "Authed": return state.auth.sessionId
|
||||
case "PasswordReset": return state.auth.sessionId
|
||||
default: {
|
||||
assertNever(state.auth)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface StateManagerInterface {
|
||||
setPassword: (password1: string, password2: string) => void;
|
||||
auth: (username: string, password: string) => void;
|
||||
logout: () => void;
|
||||
createUser: (username: string) => void;
|
||||
}
|
||||
|
||||
class NullManager implements StateManagerInterface {
|
||||
constructor() { }
|
||||
|
||||
async setPassword(_password1: string, _password2: string) { }
|
||||
|
||||
async auth(_username: string, _password: string) { }
|
||||
|
||||
async logout() { }
|
||||
|
||||
async createUser(_username: string) { }
|
||||
}
|
||||
|
||||
class StateManager implements StateManagerInterface{
|
||||
client: Client;
|
||||
state: AppState;
|
||||
dispatch: React.Dispatch<Action>;
|
||||
|
||||
constructor(client: Client, state: AppState, dispatch: React.Dispatch<any>) {
|
||||
this.client = client;
|
||||
this.state = state;
|
||||
this.dispatch = dispatch;
|
||||
}
|
||||
|
||||
async setPassword(password1: string, password2: string) {
|
||||
let sessionId = getSessionId(this.state);
|
||||
console.log(`StateManager.setPassword: ${sessionId}`);
|
||||
if (sessionId) {
|
||||
await this.client.setPassword(sessionId, password1, password2);
|
||||
}
|
||||
}
|
||||
|
||||
async auth(username: string, password: string) {
|
||||
let authResponse = await this.client.auth(username, password);
|
||||
switch (authResponse.type) {
|
||||
case "Unauthorized": break;
|
||||
case "Unexpected": break;
|
||||
case "Success": {
|
||||
window.localStorage.setItem("sessionId", authResponse.content);
|
||||
this.dispatch({ type: "SetAuthState", content: { type: "Authed", sessionId: authResponse.content } });
|
||||
break;
|
||||
}
|
||||
case "PasswordReset": {
|
||||
window.localStorage.setItem("sessionId", authResponse.content);
|
||||
this.dispatch({ type: "SetAuthState", content: { type: "PasswordReset", sessionId: authResponse.content } });
|
||||
break;
|
||||
}
|
||||
case "Locked": {
|
||||
this.dispatch({ type: "SetAuthState", content: { type: "Unauthed" } });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
const sessionId = getSessionId(this.state);
|
||||
if (sessionId) {
|
||||
await this.client.logout(sessionId);
|
||||
window.localStorage.removeItem("sessionId");
|
||||
this.dispatch({ type: "SetAuthState", content: { type: "Unauthed" } });
|
||||
}
|
||||
}
|
||||
|
||||
async createUser(username: string) {
|
||||
console.log("order to createUser", username);
|
||||
const sessionId = getSessionId(this.state);
|
||||
if (sessionId) {
|
||||
let createUserResponse = await this.client.createUser(sessionId, username);
|
||||
console.log("createUser: ", createUserResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const StateContext = createContext<[AppState, StateManagerInterface]>([initialState(), new NullManager()]);
|
||||
|
||||
interface StateProviderProps { client: Client; }
|
||||
|
||||
export const StateProvider = ({ client, children }: PropsWithChildren<StateProviderProps>) => {
|
||||
const [state, dispatch] = useReducer(stateReducer, initialState());
|
||||
|
||||
const stateManager = useRef(new StateManager(client, state, dispatch));
|
||||
|
||||
return <StateContext.Provider value={[state, stateManager.current]}>
|
||||
{children}
|
||||
</StateContext.Provider>;
|
||||
}
|
||||
1
visions-prototype/ui/src/react-app-env.d.ts
vendored
1
visions-prototype/ui/src/react-app-env.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@ -1,15 +0,0 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@ -1,5 +0,0 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
@ -1,4 +0,0 @@
|
||||
export function assertNever(value: never) {
|
||||
throw new Error("Unexpected value: " + value);
|
||||
}
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Game, User } from 'visions-types';
|
||||
import { Client } from '../../client';
|
||||
|
||||
interface UserRowProps {
|
||||
user: User,
|
||||
}
|
||||
|
||||
const UserRow = ({ user }: UserRowProps) => {
|
||||
let accountState = "Normal";
|
||||
|
||||
switch (user.state.type) {
|
||||
case "Normal": {
|
||||
accountState = "Normal";
|
||||
break;
|
||||
}
|
||||
case "PasswordReset": {
|
||||
accountState = `PasswordReset until ${user.state.content}`;
|
||||
break;
|
||||
}
|
||||
case "Locked": {
|
||||
accountState = "Locked";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (<tr>
|
||||
<td> {user.name} </td>
|
||||
<td> {user.admin && "admin"} </td>
|
||||
<td> {accountState} </td>
|
||||
</tr>);
|
||||
}
|
||||
|
||||
interface GameRowProps {
|
||||
game: Game,
|
||||
}
|
||||
|
||||
const GameRow = ({ game }: GameRowProps) => {
|
||||
return (<tr>
|
||||
<td> {game.name} </td>
|
||||
</tr>);
|
||||
}
|
||||
|
||||
interface AdminProps {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
export const AdminView = ({ client }: AdminProps) => {
|
||||
const [users, setUsers] = useState<Array<User>>([]);
|
||||
|
||||
/*
|
||||
useEffect(() => {
|
||||
client.users("aoeu").then((u) => {
|
||||
console.log(u);
|
||||
setUsers(u);
|
||||
});
|
||||
}, [client]);
|
||||
*/
|
||||
return (<table>
|
||||
<tbody>
|
||||
{users.map((user) => <UserRow user={user} />)}
|
||||
</tbody>
|
||||
</table>);
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
@import '../../design.css';
|
||||
|
||||
.auth {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.auth__input-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.auth__input-line > * {
|
||||
margin: var(--margin-s);
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
import { PropsWithChildren, useContext, useState } from 'react';
|
||||
import { LoadingState, StateContext } from '../../providers/StateProvider/StateProvider';
|
||||
import { assertNever } from '../../utils';
|
||||
import './Authentication.css';
|
||||
|
||||
interface AuthenticationProps {
|
||||
onSetPassword: (password1: string, password2: string) => void;
|
||||
onAuth: (username: string, password: string) => void;
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
export const Authentication = ({ onSetPassword, onAuth, onLogout, children }: PropsWithChildren<AuthenticationProps>) => {
|
||||
// No admin password set: prompt for the admin password
|
||||
// Password set, nobody logged in: prompt for login
|
||||
// User logged in: show the children
|
||||
|
||||
let [userField, setUserField] = useState<string>("");
|
||||
let [pwField1, setPwField1] = useState<string>("");
|
||||
let [pwField2, setPwField2] = useState<string>("");
|
||||
let [state, _] = useContext(StateContext);
|
||||
|
||||
switch (state.state) {
|
||||
case LoadingState.Loading: {
|
||||
return <div>Loading</div>
|
||||
}
|
||||
case LoadingState.Ready: {
|
||||
switch (state.auth.type) {
|
||||
case "Unauthed": {
|
||||
return <div className="auth card">
|
||||
<div className="card">
|
||||
<h1> Welcome to Visions VTT </h1>
|
||||
<div className="auth__input-line">
|
||||
<input type="text" placeholder="Username" onChange={(evt) => setUserField(evt.target.value)} />
|
||||
<input type="password" placeholder="Password" onChange={(evt) => setPwField1(evt.target.value)} />
|
||||
<input type="submit" value="Sign in" onClick={() => onAuth(userField, pwField1)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
case "Authed": {
|
||||
return (<div>
|
||||
<div> <button onClick={onLogout}>Logout</button> </div>
|
||||
<div> {children} </div>
|
||||
</div>);
|
||||
}
|
||||
case "PasswordReset": {
|
||||
return <div className="auth">
|
||||
<div className="card">
|
||||
<h1> Password Reset </h1>
|
||||
<p> Your password currently requires a reset. </p>
|
||||
<input type="password" placeholder="Password" onChange={(evt) => setPwField1(evt.target.value)} />
|
||||
<input type="password" placeholder="Retype your Password" onChange={(evt) => setPwField2(evt.target.value)} />
|
||||
<input type="submit" value="Submit" onClick={() => {
|
||||
onSetPassword(pwField1, pwField2);
|
||||
}} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
default: {
|
||||
assertNever(state.auth);
|
||||
return <div></div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthedViewProps {}
|
||||
|
||||
export const AuthedView = ({ children }: PropsWithChildren<AuthedViewProps>) => {
|
||||
const [_, manager] = useContext(StateContext)
|
||||
return (
|
||||
<Authentication onSetPassword={(password1, password2) => {
|
||||
manager.setPassword(password1, password2)
|
||||
}} onAuth={(username, password) => manager.auth(username, password)}
|
||||
onLogout={() => manager.logout()}>
|
||||
{children}
|
||||
</Authentication>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
.section {
|
||||
border: 2px solid black;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DriveGuage } from '../../plugins/Candela/DriveGuage/DriveGuage';
|
||||
|
||||
const DriveGuages = () => {
|
||||
return (<div className="section">
|
||||
<div className="drive-guages_on-light">
|
||||
<DriveGuage current={2} max={4} />
|
||||
</div>
|
||||
<div className="drive-guages_on-dark">
|
||||
<DriveGuage current={2} max={4} />
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
export const DesignPage = () => {
|
||||
return (<div>
|
||||
<DriveGuages />
|
||||
</div>);
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { UserOverview } from 'visions-types';
|
||||
import { Client } from '../../client';
|
||||
import { ProfileElement } from '../../components';
|
||||
import { getSessionId, StateContext, StateProvider } from '../../providers/StateProvider/StateProvider';
|
||||
import { ProfileView } from '../Profile/Profile';
|
||||
|
||||
interface MainProps {
|
||||
client: Client
|
||||
}
|
||||
|
||||
export const MainView = ({ client }: MainProps) => {
|
||||
const [state, _manager] = useContext(StateContext)
|
||||
const [profile, setProfile] = useState<UserOverview | undefined>(undefined)
|
||||
const [users, setUsers] = useState<UserOverview[]>([])
|
||||
|
||||
const sessionId = getSessionId(state)
|
||||
/*
|
||||
useEffect(() => {
|
||||
if (sessionId) {
|
||||
client.profile(sessionId, undefined).then((profile) => setProfile(profile))
|
||||
client.users(sessionId).then((users) => {
|
||||
console.log(users);
|
||||
setUsers(users);
|
||||
})
|
||||
}
|
||||
}, [sessionId, client])
|
||||
*/
|
||||
|
||||
return (
|
||||
<div>
|
||||
{profile && <ProfileView profile={profile} users={users} games={[]} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
.profile-view_columns {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.profile-view_columns > div {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
import { useContext, useState } from "react"
|
||||
import { GameOverview, UserOverview } from "visions-types"
|
||||
import { Modal, ModalProps, ProfileElement, UserManagementElement } from "../../components"
|
||||
import { StateContext } from "../../providers/StateProvider/StateProvider"
|
||||
import "./Profile.css"
|
||||
|
||||
interface ProfileProps {
|
||||
profile: UserOverview,
|
||||
users: UserOverview[],
|
||||
games: GameOverview[],
|
||||
}
|
||||
|
||||
interface CreateUserModalProps {
|
||||
onCancel: () => void
|
||||
onCreateUser: (name: string) => void
|
||||
}
|
||||
|
||||
const CreateUserModal = ({ onCancel, onCreateUser }: CreateUserModalProps) => {
|
||||
const [userName, setUserName] = useState("");
|
||||
|
||||
return <Modal title="Create User" onCancel={{ name: "Cancel", action: onCancel }}
|
||||
onPrimary={{ name: "Create User", action: () => onCreateUser(userName) }}>
|
||||
<input type="text" placeholder="username" onChange={(evt) => {
|
||||
setUserName(evt.target.value);
|
||||
}} />
|
||||
</Modal>
|
||||
}
|
||||
|
||||
export const ProfileView = ({ profile, users, games, }: ProfileProps) => {
|
||||
const [_state, manager] = useContext(StateContext)
|
||||
const [showUser, setShowUser] = useState(false)
|
||||
|
||||
const userList = profile.isAdmin && <UserManagementElement users={users} onShowCreateUser={() => setShowUser(true)} />
|
||||
|
||||
return (
|
||||
<div className="profile-view">
|
||||
{showUser && <CreateUserModal onCancel={() => setShowUser(false)} onCreateUser={(username) => manager.createUser(username)} />}
|
||||
<div className="profile-view_columns">
|
||||
<ProfileElement profile={profile} games={games} />
|
||||
|
||||
{userList}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
import { MainView } from './Main/Main'
|
||||
import { ProfileView } from './Profile/Profile'
|
||||
|
||||
export { MainView, ProfileView }
|
||||
@ -1,27 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"gen"
|
||||
]
|
||||
}
|
||||
2
visions-prototype/visions-types/.gitignore
vendored
2
visions-prototype/visions-types/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
dist
|
||||
visions.ts
|
||||
@ -1,8 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- npm install typescript
|
||||
- typeshare --lang typescript --output-file visions.ts ../server/src
|
||||
- npx tsc
|
||||
28
visions-prototype/visions-types/package-lock.json
generated
28
visions-prototype/visions-types/package-lock.json
generated
@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "visions-types",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "visions-types",
|
||||
"version": "0.0.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "visions-types",
|
||||
"version": "0.0.1",
|
||||
"description": "Shared data types for Visions",
|
||||
"main": "visions.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./visions.ts"]
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "yew-app"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
gloo-net = "0.6.0"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
wasm-bindgen-futures = "0.4.50"
|
||||
yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] }
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
body {
|
||||
background-color: hsl(0, 0%, 50%);
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head></head>
|
||||
<body></body>
|
||||
</html>
|
||||
@ -1,126 +0,0 @@
|
||||
use yew::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use gloo_net::http::Request;
|
||||
|
||||
#[derive(Clone, PartialEq, Deserialize)]
|
||||
struct Video {
|
||||
id: usize,
|
||||
title: String,
|
||||
speaker: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct VideosListProps {
|
||||
videos: Vec<Video>,
|
||||
on_click: Callback<Video>,
|
||||
}
|
||||
|
||||
/*
|
||||
fn videos() -> Vec<Video> {
|
||||
vec![
|
||||
Video {
|
||||
id: 1,
|
||||
title: "Building and breaking things".to_string(),
|
||||
speaker: "John Doe".to_string(),
|
||||
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
},
|
||||
Video {
|
||||
id: 2,
|
||||
title: "The development process".to_string(),
|
||||
speaker: "Jane Smith".to_string(),
|
||||
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
},
|
||||
Video {
|
||||
id: 3,
|
||||
title: "The Web 7.0".to_string(),
|
||||
speaker: "Matt Miller".to_string(),
|
||||
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
},
|
||||
Video {
|
||||
id: 4,
|
||||
title: "Mouseless development".to_string(),
|
||||
speaker: "Tom Jerry".to_string(),
|
||||
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
},
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
#[function_component(VideosList)]
|
||||
fn videos_list(VideosListProps { videos, on_click }: &VideosListProps) -> Html {
|
||||
let on_click = on_click.clone();
|
||||
videos
|
||||
.iter()
|
||||
.map(|video| {
|
||||
let on_video_select = {
|
||||
let on_click = on_click.clone();
|
||||
let video = video.clone();
|
||||
Callback::from(move |_| on_click.emit(video.clone()))
|
||||
};
|
||||
html! {
|
||||
<p key={video.id} onclick={on_video_select}>{format!("{}: {}", video.speaker, video.title)}</p>
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct VideosDetailsProps {
|
||||
video: Video,
|
||||
}
|
||||
|
||||
#[function_component(VideoDetails)]
|
||||
fn video_details(VideosDetailsProps { video }: &VideosDetailsProps) -> Html {
|
||||
html! {
|
||||
<div>
|
||||
<h3>{video.title.clone()}</h3>
|
||||
<img src="https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
let videos = use_state(|| vec![]);
|
||||
{
|
||||
let videos = videos.clone();
|
||||
use_effect_with((), move |_| {
|
||||
let videos = videos.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let response = Request::get("/tutorial/data.json").send().await;
|
||||
println!("response: {:?}", response);
|
||||
let response = response.unwrap();
|
||||
let fetched_videos: Vec<Video> = response.json().await.unwrap();
|
||||
videos.set(fetched_videos);
|
||||
});
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
let selected_video = use_state(|| None);
|
||||
|
||||
let on_video_select = {
|
||||
let selected_video = selected_video.clone();
|
||||
Callback::from(move |video: Video| selected_video.set(Some(video)))
|
||||
};
|
||||
|
||||
let details = selected_video.as_ref().map(|video| html! {
|
||||
<VideoDetails video={video.clone()} />
|
||||
});
|
||||
|
||||
html! {
|
||||
<>
|
||||
<h1>{ "RustConf Explorer" }</h1>
|
||||
<div>
|
||||
<h3>{"Videos to watch"}</h3>
|
||||
<VideosList videos={(*videos).clone()} on_click={on_video_select.clone()} />
|
||||
</div>
|
||||
{ for details }
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user