Move the assertNever utility to a utilities file
This commit is contained in:
parent
dc8cb834e0
commit
08462388ea
15
Cargo.lock
generated
15
Cargo.lock
generated
@ -4394,6 +4394,20 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"bytes",
|
||||||
|
"http 1.2.0",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-layer"
|
name = "tower-layer"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@ -4688,6 +4702,7 @@ dependencies = [
|
|||||||
"thiserror 2.0.3",
|
"thiserror 2.0.3",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
"tower-http",
|
||||||
"typeshare",
|
"typeshare",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid 1.11.0",
|
"uuid 1.11.0",
|
||||||
|
@ -24,6 +24,7 @@ serde_json = { version = "*" }
|
|||||||
thiserror = { version = "2.0.3" }
|
thiserror = { version = "2.0.3" }
|
||||||
tokio = { version = "1", features = [ "full" ] }
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
tokio-stream = { version = "0.1.16" }
|
tokio-stream = { version = "0.1.16" }
|
||||||
|
tower-http = { version = "0.6.2", features = ["cors"] }
|
||||||
typeshare = { version = "1.0.4" }
|
typeshare = { version = "1.0.4" }
|
||||||
urlencoding = { version = "2.1.3" }
|
urlencoding = { version = "2.1.3" }
|
||||||
uuid = { version = "1.11.0", features = ["v4"] }
|
uuid = { version = "1.11.0", features = ["v4"] }
|
||||||
|
@ -2,9 +2,11 @@ use std::fmt;
|
|||||||
|
|
||||||
use rusqlite::types::{FromSql, FromSqlResult, ValueRef};
|
use rusqlite::types::{FromSql, FromSqlResult, ValueRef};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use typeshare::typeshare;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
#[typeshare]
|
||||||
pub struct UserId(String);
|
pub struct UserId(String);
|
||||||
|
|
||||||
impl UserId {
|
impl UserId {
|
||||||
@ -47,6 +49,7 @@ impl fmt::Display for UserId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
#[typeshare]
|
||||||
pub struct SessionId(String);
|
pub struct SessionId(String);
|
||||||
|
|
||||||
impl SessionId {
|
impl SessionId {
|
||||||
@ -87,6 +90,7 @@ impl fmt::Display for SessionId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
#[typeshare]
|
||||||
pub struct GameId(String);
|
pub struct GameId(String);
|
||||||
|
|
||||||
impl GameId {
|
impl GameId {
|
||||||
@ -121,6 +125,7 @@ impl FromSql for GameId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
#[typeshare]
|
||||||
pub struct CharacterId(String);
|
pub struct CharacterId(String);
|
||||||
|
|
||||||
impl CharacterId {
|
impl CharacterId {
|
||||||
|
@ -16,7 +16,7 @@ use crate::{
|
|||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||||
pub struct HealthCheck {
|
pub struct HealthCheck {
|
||||||
pub ok: bool,
|
pub admin_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn wrap_handler<F, A, Fut>(f: F) -> (StatusCode, Json<Option<A>>)
|
pub async fn wrap_handler<F, A, Fut>(f: F) -> (StatusCode, Json<Option<A>>)
|
||||||
@ -46,10 +46,10 @@ where
|
|||||||
pub async fn healthcheck(core: Core) -> Vec<u8> {
|
pub async fn healthcheck(core: Core) -> Vec<u8> {
|
||||||
match core.status().await {
|
match core.status().await {
|
||||||
ResultExt::Ok(s) => serde_json::to_vec(&HealthCheck {
|
ResultExt::Ok(s) => serde_json::to_vec(&HealthCheck {
|
||||||
ok: s.admin_enabled,
|
admin_enabled: s.admin_enabled,
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
ResultExt::Err(_) => serde_json::to_vec(&HealthCheck { ok: false }).unwrap(),
|
ResultExt::Err(_) => serde_json::to_vec(&HealthCheck { admin_enabled: false }).unwrap(),
|
||||||
ResultExt::Fatal(err) => panic!("{}", err),
|
ResultExt::Fatal(err) => panic!("{}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::Path,
|
extract::Path,
|
||||||
http::{HeaderMap, StatusCode},
|
http::{header::CONTENT_TYPE, HeaderMap, Method, StatusCode},
|
||||||
routing::{get, post, put},
|
routing::{get, post, put},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::Core,
|
core::Core,
|
||||||
@ -21,60 +22,67 @@ pub fn routes(core: Core) -> Router {
|
|||||||
get({
|
get({
|
||||||
let core = core.clone();
|
let core = core.clone();
|
||||||
move || healthcheck(core)
|
move || healthcheck(core)
|
||||||
}),
|
})
|
||||||
|
.layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_methods([Method::GET])
|
||||||
|
.allow_origin(Any),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/auth",
|
"/api/v1/auth",
|
||||||
post({
|
post({
|
||||||
let core = core.clone();
|
let core = core.clone();
|
||||||
move |req: Json<AuthRequest>| wrap_handler(|| check_password(core, req))
|
move |req: Json<AuthRequest>| wrap_handler(|| check_password(core, req))
|
||||||
|
}).layer(
|
||||||
|
CorsLayer::new().allow_methods([Method::POST]).allow_headers([CONTENT_TYPE]).allow_origin(Any),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
// By default, just get the self user.
|
||||||
|
"/api/v1/user",
|
||||||
|
get({
|
||||||
|
let core = core.clone();
|
||||||
|
move |headers: HeaderMap| wrap_handler(|| get_user(core, headers, None))
|
||||||
|
})
|
||||||
|
.put({
|
||||||
|
let core = core.clone();
|
||||||
|
move |headers: HeaderMap, req: Json<CreateUserRequest>| {
|
||||||
|
let Json(req) = req;
|
||||||
|
wrap_handler(|| create_user(core, headers, req))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/v1/user/password",
|
||||||
|
put({
|
||||||
|
let core = core.clone();
|
||||||
|
move |headers: HeaderMap, req: Json<SetPasswordRequest>| {
|
||||||
|
let Json(req) = req;
|
||||||
|
wrap_handler(|| set_password(core, headers, req))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/v1/user/:user_id",
|
||||||
|
get({
|
||||||
|
let core = core.clone();
|
||||||
|
move |user_id: Path<UserId>, headers: HeaderMap| {
|
||||||
|
let Path(user_id) = user_id;
|
||||||
|
wrap_handler(|| get_user(core, headers, Some(user_id)))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/v1/games",
|
||||||
|
put({
|
||||||
|
let core = core.clone();
|
||||||
|
move |headers: HeaderMap, req: Json<CreateGameRequest>| {
|
||||||
|
let Json(req) = req;
|
||||||
|
wrap_handler(|| create_game(core, headers, req))
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
// By default, just get the self user.
|
|
||||||
"/api/v1/user",
|
|
||||||
get({
|
|
||||||
let core = core.clone();
|
|
||||||
move |headers: HeaderMap| wrap_handler(|| get_user(core, headers, None))
|
|
||||||
})
|
|
||||||
.put({
|
|
||||||
let core = core.clone();
|
|
||||||
move |headers: HeaderMap, req: Json<CreateUserRequest>| {
|
|
||||||
let Json(req) = req;
|
|
||||||
wrap_handler(|| create_user(core, headers, req))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/v1/user/password",
|
|
||||||
put({
|
|
||||||
let core = core.clone();
|
|
||||||
move |headers: HeaderMap, req: Json<SetPasswordRequest>| {
|
|
||||||
let Json(req) = req;
|
|
||||||
wrap_handler(|| set_password(core, headers, req))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/v1/user/:user_id",
|
|
||||||
get({
|
|
||||||
let core = core.clone();
|
|
||||||
move |user_id: Path<UserId>, headers: HeaderMap| {
|
|
||||||
let Path(user_id) = user_id;
|
|
||||||
wrap_handler(|| get_user(core, headers, Some(user_id)))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/v1/games",
|
|
||||||
put({
|
|
||||||
let core = core.clone();
|
|
||||||
move |headers: HeaderMap, req: Json<CreateGameRequest>| {
|
|
||||||
let Json(req) = req;
|
|
||||||
wrap_handler(|| create_game(core, headers, req))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -46,9 +46,9 @@ const App = ({ client }: AppProps) => {
|
|||||||
console.log("rendering app");
|
console.log("rendering app");
|
||||||
const [websocketUrl, setWebsocketUrl] = useState<string | undefined>(undefined);
|
const [websocketUrl, setWebsocketUrl] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
client.registerWebsocket().then((url) => setWebsocketUrl(url))
|
// client.registerWebsocket().then((url) => setWebsocketUrl(url))
|
||||||
}, [client]);
|
// }, [client]);
|
||||||
|
|
||||||
let router =
|
let router =
|
||||||
createBrowserRouter([
|
createBrowserRouter([
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { SessionId } from "visions-types";
|
||||||
|
|
||||||
export type PlayingField = {
|
export type PlayingField = {
|
||||||
backgroundImage: string;
|
backgroundImage: string;
|
||||||
}
|
}
|
||||||
@ -64,15 +66,15 @@ export class Client {
|
|||||||
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(password) });
|
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(password) });
|
||||||
}
|
}
|
||||||
|
|
||||||
async auth(username: string, password: string) {
|
async auth(username: string, password: string): Promise<SessionId | undefined> {
|
||||||
const url = new URL(this.base);
|
const url = new URL(this.base);
|
||||||
url.pathname = `api/v1/auth`
|
url.pathname = `/api/v1/auth`
|
||||||
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify({ 'username': username, 'password': password }) });
|
return fetch(url, { method: 'POST', headers: [['Content-Type', 'application/json']], body: JSON.stringify({ 'username': username, 'password': password }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
async status() {
|
async health() {
|
||||||
const url = new URL(this.base);
|
const url = new URL(this.base);
|
||||||
url.pathname = `/api/v1/status`;
|
url.pathname = `/api/v1/health`;
|
||||||
return fetch(url).then((response) => response.json());
|
return fetch(url).then((response) => response.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import './Tabletop.css';
|
import './Tabletop.css';
|
||||||
import { RGB } from 'visions-types';
|
import { Rgb } from 'visions-types';
|
||||||
|
|
||||||
interface TabletopElementProps {
|
interface TabletopElementProps {
|
||||||
backgroundColor: RGB;
|
backgroundColor: Rgb;
|
||||||
backgroundUrl: URL | undefined;
|
backgroundUrl: URL | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { assertNever } from '.';
|
import { assertNever } from '../../utils';
|
||||||
import './Charsheet.css';
|
import './Charsheet.css';
|
||||||
import { DriveGuage } from './DriveGuage/DriveGuage';
|
import { DriveGuage } from './DriveGuage/DriveGuage';
|
||||||
import { Charsheet, Nerve, Cunning, Intuition } from './types';
|
import { Charsheet, Nerve, Cunning, Intuition } from './types';
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { assertNever } from '.';
|
|
||||||
import { SimpleGuage } from '../../components/Guages/SimpleGuage';
|
import { SimpleGuage } from '../../components/Guages/SimpleGuage';
|
||||||
import { Charsheet, Nerve, Cunning, Intuition } from './types';
|
import { Charsheet, Nerve, Cunning, Intuition } from './types';
|
||||||
import './CharsheetPanel.css';
|
import './CharsheetPanel.css';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { assertNever } from '../../utils';
|
||||||
|
|
||||||
interface CharsheetPanelProps {
|
interface CharsheetPanelProps {
|
||||||
sheet: Charsheet;
|
sheet: Charsheet;
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import { CharsheetElement } from './Charsheet';
|
import { CharsheetElement } from './Charsheet';
|
||||||
import { CharsheetPanelElement } from './CharsheetPanel';
|
import { CharsheetPanelElement } from './CharsheetPanel';
|
||||||
|
|
||||||
export function assertNever(value: never) {
|
|
||||||
throw new Error("Unexpected value: " + value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default { CharsheetElement, CharsheetPanelElement };
|
export default { CharsheetElement, CharsheetPanelElement };
|
||||||
|
|
||||||
|
@ -1,26 +1,53 @@
|
|||||||
import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer, useRef } from "react";
|
import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer, useRef } from "react";
|
||||||
import { Status, Tabletop } from "visions-types";
|
import { Status, Tabletop } from "visions-types";
|
||||||
import { Client } from "../../client";
|
import { Client } from "../../client";
|
||||||
import { assertNever } from "../../plugins/Candela";
|
import { assertNever } from "../../utils";
|
||||||
|
|
||||||
type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", userid: string };
|
type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", userid: string };
|
||||||
|
|
||||||
type AppState = {
|
type LoadingState = { type: "Loading" }
|
||||||
auth: AuthState;
|
|
||||||
tabletop: Tabletop;
|
type ReadyState = {
|
||||||
|
type: "Ready",
|
||||||
|
auth: AuthState,
|
||||||
|
tabletop: Tabletop,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppState = LoadingState | ReadyState
|
||||||
|
|
||||||
type Action = { type: "SetAuthState", content: AuthState };
|
type Action = { type: "SetAuthState", content: AuthState };
|
||||||
|
|
||||||
|
/*
|
||||||
const initialState = (): AppState => (
|
const initialState = (): AppState => (
|
||||||
{
|
{
|
||||||
auth: { type: "NoAdmin" },
|
auth: { type: "NoAdmin" },
|
||||||
tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined }
|
tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
|
const initialState = (): AppState => ({ type: "Loading" })
|
||||||
|
|
||||||
|
const loadingReducer = (state: LoadingState, action: Action): AppState => {
|
||||||
|
return { type: "Ready", auth: action.content, tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined } }
|
||||||
|
}
|
||||||
|
|
||||||
|
const readyReducer = (state: ReadyState, action: Action): AppState => {
|
||||||
|
return { ...state, auth: action.content }
|
||||||
|
}
|
||||||
|
|
||||||
const stateReducer = (state: AppState, action: Action): AppState => {
|
const stateReducer = (state: AppState, action: Action): AppState => {
|
||||||
return { ...state, auth: action.content }
|
switch (state.type) {
|
||||||
|
case "Loading": {
|
||||||
|
return loadingReducer(state, action);
|
||||||
|
}
|
||||||
|
case "Ready": {
|
||||||
|
return readyReducer(state, action);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assertNever(state);
|
||||||
|
return { type: "Loading" };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StateManager {
|
class StateManager {
|
||||||
@ -35,7 +62,7 @@ class StateManager {
|
|||||||
async status() {
|
async status() {
|
||||||
if (!this.client || !this.dispatch) return;
|
if (!this.client || !this.dispatch) return;
|
||||||
|
|
||||||
const { admin_enabled } = await this.client.status();
|
const { admin_enabled } = await this.client.health();
|
||||||
if (!admin_enabled) {
|
if (!admin_enabled) {
|
||||||
this.dispatch({ type: "SetAuthState", content: { type: "NoAdmin" } });
|
this.dispatch({ type: "SetAuthState", content: { type: "NoAdmin" } });
|
||||||
} else {
|
} else {
|
||||||
@ -54,9 +81,9 @@ class StateManager {
|
|||||||
if (!this.client || !this.dispatch) return;
|
if (!this.client || !this.dispatch) return;
|
||||||
|
|
||||||
let resp = await this.client.auth(username, password);
|
let resp = await this.client.auth(username, password);
|
||||||
let userid = await resp.json();
|
let sessionid = await resp.json();
|
||||||
console.log("userid retrieved", userid);
|
console.log("sessionid retrieved", sessionid);
|
||||||
this.dispatch({ type: "SetAuthState", content: { type: "Authed", userid } });
|
this.dispatch({ type: "SetAuthState", content: { type: "Authed", sessionid } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
visions/ui/src/utils.ts
Normal file
4
visions/ui/src/utils.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export function assertNever(value: never) {
|
||||||
|
throw new Error("Unexpected value: " + value);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
import { PropsWithChildren, useContext, useState } from 'react';
|
import { PropsWithChildren, useContext, useState } from 'react';
|
||||||
import { StateContext } from '../../providers/StateProvider/StateProvider';
|
import { StateContext } from '../../providers/StateProvider/StateProvider';
|
||||||
import { assertNever } from '../../plugins/Candela';
|
import { assertNever } from '../../utils';
|
||||||
import './Authentication.css';
|
import './Authentication.css';
|
||||||
|
|
||||||
interface AuthenticationProps {
|
interface AuthenticationProps {
|
||||||
@ -17,35 +17,48 @@ export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithC
|
|||||||
let [pwField, setPwField] = useState<string>("");
|
let [pwField, setPwField] = useState<string>("");
|
||||||
let [state, _] = useContext(StateContext);
|
let [state, _] = useContext(StateContext);
|
||||||
|
|
||||||
switch (state.auth.type) {
|
switch (state.type) {
|
||||||
case "NoAdmin": {
|
case "Loading": {
|
||||||
return <div className="auth">
|
return <div>Loading</div>
|
||||||
<div className="card">
|
|
||||||
<h1> Welcome to your new Visions VTT Instance </h1>
|
|
||||||
<p> Set your admin password: </p>
|
|
||||||
<input type="password" placeholder="Password" onChange={(evt) => setPwField(evt.target.value)} />
|
|
||||||
<input type="submit" value="Submit" onClick={() => onAdminPassword(pwField)} />
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
case "Unauthed": {
|
case "Ready": {
|
||||||
return <div className="auth card">
|
switch (state.auth.type) {
|
||||||
<div className="card">
|
case "NoAdmin": {
|
||||||
<h1> Welcome to Visions VTT </h1>
|
return <div className="auth">
|
||||||
<div className="auth__input-line">
|
<div className="card">
|
||||||
<input type="text" placeholder="Username" onChange={(evt) => setUserField(evt.target.value)} />
|
<h1> Welcome to your new Visions VTT Instance </h1>
|
||||||
<input type="password" placeholder="Password" onChange={(evt) => setPwField(evt.target.value)} />
|
<p> Set your admin password: </p>
|
||||||
<input type="submit" value="Sign in" onClick={() => onAuth(userField, pwField)} />
|
<input type="password" placeholder="Password" onChange={(evt) => setPwField(evt.target.value)} />
|
||||||
</div>
|
<input type="submit" value="Submit" onClick={() => onAdminPassword(pwField)} />
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
case "Authed": {
|
case "Unauthed": {
|
||||||
return <div> {children} </div>;
|
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) => setPwField(evt.target.value)} />
|
||||||
|
<input type="submit" value="Sign in" onClick={() => onAuth(userField, pwField)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
case "Authed": {
|
||||||
|
return <div> {children} </div>;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assertNever(state.auth);
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
assertNever(state.auth);
|
assertNever(state);
|
||||||
return <div></div>;
|
return <div></div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { TabletopElement } from '../../components/Tabletop/Tabletop';
|
|||||||
import { ThumbnailElement } from '../../components/Thumbnail/Thumbnail';
|
import { ThumbnailElement } from '../../components/Thumbnail/Thumbnail';
|
||||||
import { WebsocketContext } from '../../components/WebsocketProvider';
|
import { WebsocketContext } from '../../components/WebsocketProvider';
|
||||||
import './GmView.css';
|
import './GmView.css';
|
||||||
|
import { assertNever } from '../../utils';
|
||||||
|
|
||||||
interface GmViewProps {
|
interface GmViewProps {
|
||||||
client: Client
|
client: Client
|
||||||
@ -18,12 +19,21 @@ export const GmView = ({ client }: GmViewProps) => {
|
|||||||
client.availableImages().then((images) => setImages(images));
|
client.availableImages().then((images) => setImages(images));
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
||||||
const backgroundUrl = state.tabletop.backgroundImage ? client.imageUrl(state.tabletop.backgroundImage) : undefined;
|
switch (state.type) {
|
||||||
return (<div className="gm-view">
|
case "Loading": return <div></div>;
|
||||||
<div>
|
case "Ready": {
|
||||||
{images.map((imageName) => <ThumbnailElement id={imageName} url={client.imageUrl(imageName)} onclick={() => { client.setBackgroundImage(imageName); }} />)}
|
const backgroundUrl = state.tabletop.backgroundImage ? client.imageUrl(state.tabletop.backgroundImage) : undefined;
|
||||||
</div>
|
return (<div className="gm-view">
|
||||||
<TabletopElement backgroundColor={state.tabletop.backgroundColor} backgroundUrl={backgroundUrl} />
|
<div>
|
||||||
</div>)
|
{images.map((imageName) => <ThumbnailElement id={imageName} url={client.imageUrl(imageName)} onclick={() => { client.setBackgroundImage(imageName); }} />)}
|
||||||
|
</div>
|
||||||
|
<TabletopElement backgroundColor={state.tabletop.backgroundColor} backgroundUrl={backgroundUrl} />
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assertNever(state);
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { Client } from '../../client';
|
|||||||
import { TabletopElement } from '../../components/Tabletop/Tabletop';
|
import { TabletopElement } from '../../components/Tabletop/Tabletop';
|
||||||
import Candela from '../../plugins/Candela';
|
import Candela from '../../plugins/Candela';
|
||||||
import { StateContext } from '../../providers/StateProvider/StateProvider';
|
import { StateContext } from '../../providers/StateProvider/StateProvider';
|
||||||
|
import { assertNever } from '../../utils';
|
||||||
|
|
||||||
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803";
|
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803";
|
||||||
|
|
||||||
@ -26,14 +27,23 @@ export const PlayerView = ({ client }: PlayerViewProps) => {
|
|||||||
[client, setCharsheet]
|
[client, setCharsheet]
|
||||||
);
|
);
|
||||||
|
|
||||||
const backgroundColor = state.tabletop.backgroundColor;
|
switch (state.type) {
|
||||||
const tabletopColorStyle = `rgb(${backgroundColor.red}, ${backgroundColor.green}, ${backgroundColor.blue})`;
|
case "Loading": return <div></div>;
|
||||||
const backgroundUrl = state.tabletop.backgroundImage ? client.imageUrl(state.tabletop.backgroundImage) : undefined;
|
case "Ready": {
|
||||||
|
const backgroundColor = state.tabletop.backgroundColor;
|
||||||
|
const tabletopColorStyle = `rgb(${backgroundColor.red}, ${backgroundColor.green}, ${backgroundColor.blue})`;
|
||||||
|
const backgroundUrl = state.tabletop.backgroundImage ? client.imageUrl(state.tabletop.backgroundImage) : undefined;
|
||||||
|
|
||||||
return (<div className="player-view" style={{ backgroundColor: tabletopColorStyle }}>
|
return (<div className="player-view" style={{ backgroundColor: tabletopColorStyle }}>
|
||||||
<div className="player-view__middle-panel"> <TabletopElement backgroundColor={backgroundColor} backgroundUrl={backgroundUrl} /> </div>
|
<div className="player-view__middle-panel"> <TabletopElement backgroundColor={backgroundColor} backgroundUrl={backgroundUrl} /> </div>
|
||||||
<div className="player-view__right-panel">
|
<div className="player-view__right-panel">
|
||||||
{charsheet ? <Candela.CharsheetPanelElement sheet={charsheet} /> : <div> </div>}</div>
|
{charsheet ? <Candela.CharsheetPanelElement sheet={charsheet} /> : <div> </div>}</div>
|
||||||
</div>)
|
</div>)
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assertNever(state);
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user