Delete old visions and yew prototypes

This commit is contained in:
Savanni D'Gerinel 2025-07-11 17:41:53 -04:00
parent 0387429021
commit bf6cb0f054
71 changed files with 0 additions and 16841 deletions

View File

@ -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*

View File

@ -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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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/).

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
import React from 'react';
interface GuageProps {
current: number,
max: number,
}
export const SimpleGuage = ({ current, max }: GuageProps) => <> {current} / {max}</>

View File

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

View File

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

View File

@ -1,4 +0,0 @@
.profile {
margin: var(--margin-s);
}

View File

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

View File

@ -1,3 +0,0 @@
.tabletop > img {
max-width: 100%;
}

View File

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

View File

@ -1,4 +0,0 @@
.thumbnail > img {
max-width: 300px;
max-height: 300px;
}

View File

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

View File

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

View File

@ -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,
}
}
}
}
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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} />
}
*/

View File

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

View File

@ -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} />
}
*/

View File

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

View File

@ -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">&nbsp;</div> :
<div className="drive-guage__top">&nbsp;</div>}
{i < max ? <div className="drive-guage__bottom drive-guage__bottom_filled">&nbsp;</div> :
<div className="drive-guage__bottom">&nbsp;</div>}
</div>)
}
components.splice(3, 0, <div className="drive-guage__spacer">&nbsp;</div>);
components.splice(7, 0, <div className="drive-guage__spacer">&nbsp;</div>);
return (<div className="drive-guage">
{components}
</div>)
}

View File

@ -1,5 +0,0 @@
import { CharsheetElement } from './Charsheet';
import { CharsheetPanelElement } from './CharsheetPanel';
export default { CharsheetElement, CharsheetPanelElement };

View File

@ -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[],
}

View File

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

View File

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

View File

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

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

View File

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

View File

@ -1,4 +0,0 @@
export function assertNever(value: never) {
throw new Error("Unexpected value: " + value);
}

View File

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

View File

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

View File

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

View File

@ -1,4 +0,0 @@
.section {
border: 2px solid black;
border-radius: 4px;
}

View File

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

View File

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

View File

@ -1,9 +0,0 @@
.profile-view_columns {
display: flex;
justify-content: space-between;
}
.profile-view_columns > div {
width: 45%;
}

View File

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

View File

@ -1,4 +0,0 @@
import { MainView } from './Main/Main'
import { ProfileView } from './Profile/Profile'
export { MainView, ProfileView }

View File

@ -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"
]
}

View File

@ -1,2 +0,0 @@
dist
visions.ts

View File

@ -1,8 +0,0 @@
version: '3'
tasks:
build:
cmds:
- npm install typescript
- typeshare --lang typescript --output-file visions.ts ../server/src
- npx tsc

View File

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

View File

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

View File

@ -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"]
}

View File

@ -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"] }

View File

@ -1,3 +0,0 @@
body {
background-color: hsl(0, 0%, 50%);
}

View File

@ -1,5 +0,0 @@
<!doctype html>
<html lang="en">
<head></head>
<body></body>
</html>

View File

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