Set up the user interface state model and set up the admin user onboarding #283
|
@ -20,6 +20,7 @@
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-router": "^6.28.0",
|
"react-router": "^6.28.0",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
@ -3747,6 +3748,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
|
||||||
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.5.13",
|
"version": "8.5.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
|
||||||
|
@ -12920,6 +12926,28 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||||
|
@ -15257,6 +15285,14 @@
|
||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-router": "^6.28.0",
|
"react-router": "^6.28.0",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { PropsWithChildren, useEffect, useState } from 'react';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import { Client } from './client';
|
import { Client } from './client';
|
||||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||||
|
@ -10,6 +10,7 @@ import { Admin } from './views/Admin/Admin';
|
||||||
import Candela from './plugins/Candela';
|
import Candela from './plugins/Candela';
|
||||||
import { Authentication } from './views/Authentication/Authentication';
|
import { Authentication } from './views/Authentication/Authentication';
|
||||||
import { StateProvider } from './components/StateProvider';
|
import { StateProvider } from './components/StateProvider';
|
||||||
|
import { AuthProvider } from './providers/AuthProvider/AuthProvider';
|
||||||
|
|
||||||
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803";
|
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803";
|
||||||
|
|
||||||
|
@ -27,6 +28,20 @@ const CandelaCharsheet = ({ client }: { client: Client }) => {
|
||||||
return sheet ? <Candela.CharsheetElement sheet={sheet} /> : <div> </div>
|
return sheet ? <Candela.CharsheetElement sheet={sheet} /> : <div> </div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AuthedViewProps {
|
||||||
|
client: Client;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthedView = ({ client, children }: PropsWithChildren<AuthedViewProps>) => {
|
||||||
|
return (<AuthProvider client={client}>
|
||||||
|
<StateProvider client={client}>
|
||||||
|
<Authentication onAdminPassword={(password) => console.log(password)} onAuth={(username, password) => console.log(username, password)}>
|
||||||
|
{children}
|
||||||
|
</Authentication>
|
||||||
|
</StateProvider>
|
||||||
|
</AuthProvider >);
|
||||||
|
}
|
||||||
|
|
||||||
const App = ({ client }: AppProps) => {
|
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);
|
||||||
|
@ -39,7 +54,7 @@ const App = ({ client }: AppProps) => {
|
||||||
createBrowserRouter([
|
createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <StateProvider client={client}> <Authentication> <PlayerView client={client} /> </Authentication> </StateProvider>
|
element: <AuthedView client={client}> <PlayerView client={client} /> </AuthedView>
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,19 +3,14 @@ import { Status, Tabletop } from "visions-types";
|
||||||
import { Client } from "../client";
|
import { Client } from "../client";
|
||||||
import { assertNever } from "../plugins/Candela";
|
import { assertNever } from "../plugins/Candela";
|
||||||
|
|
||||||
type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", username: string };
|
|
||||||
|
|
||||||
type TabletopState = {
|
type TabletopState = {
|
||||||
auth: AuthState;
|
|
||||||
tabletop: Tabletop;
|
tabletop: Tabletop;
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateAction = { type: "SetAuthState", state: AuthState }
|
type Action = {};
|
||||||
| { type: "HandleMessage" };
|
|
||||||
|
|
||||||
const initialState = (): TabletopState => (
|
const initialState = (): TabletopState => (
|
||||||
{
|
{
|
||||||
auth: { type: "Unauthed" },
|
|
||||||
tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined }
|
tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -28,37 +23,10 @@ export const StateProvider = ({ client, children }: PropsWithChildren<StateProvi
|
||||||
console.log("StateProvider");
|
console.log("StateProvider");
|
||||||
const [state, dispatch] = useReducer(stateReducer, initialState());
|
const [state, dispatch] = useReducer(stateReducer, initialState());
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("useCallback");
|
|
||||||
client.status().then((status: Status) => {
|
|
||||||
console.log("status: ", status);
|
|
||||||
if (status.admin_enabled) {
|
|
||||||
dispatch({ type: "SetAuthState", state: { type: "Unauthed" } });
|
|
||||||
} else {
|
|
||||||
dispatch({ type: "SetAuthState", state: { type: "NoAdmin" } });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[client]
|
|
||||||
);
|
|
||||||
|
|
||||||
return <AppContext.Provider value={state}>
|
return <AppContext.Provider value={state}>
|
||||||
{children}
|
{children}
|
||||||
</AppContext.Provider>;
|
</AppContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateReducer = (state: TabletopState, action: StateAction): TabletopState => {
|
const stateReducer = (state: TabletopState, _action: Action): TabletopState => state;
|
||||||
switch (action.type) {
|
|
||||||
case "SetAuthState": {
|
|
||||||
return { ...state, auth: action.state };
|
|
||||||
}
|
|
||||||
case "HandleMessage": {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
assertNever(action);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { createContext, PropsWithChildren, useEffect, useReducer } from "react";
|
||||||
|
import { Status } from "visions-types";
|
||||||
|
import { Client } from "../../client";
|
||||||
|
|
||||||
|
type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", username: string };
|
||||||
|
|
||||||
|
type Action = { type: "SetAuthState", state: AuthState };
|
||||||
|
|
||||||
|
export const AuthContext = createContext<AuthState>({ type: "NoAdmin" });
|
||||||
|
|
||||||
|
interface AuthProviderProps {
|
||||||
|
client: Client;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthProvider = ({ client, children }: PropsWithChildren<AuthProviderProps>) => {
|
||||||
|
const [authState, dispatch] = useReducer(stateReducer, { type: "NoAdmin" });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client.status().then((status: Status) => {
|
||||||
|
if (status.admin_enabled) {
|
||||||
|
dispatch({ type: "SetAuthState", state: { type: "Unauthed" } });
|
||||||
|
} else {
|
||||||
|
dispatch({ type: "SetAuthState", state: { type: "NoAdmin" } });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[client]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (<AuthContext.Provider value={authState}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateReducer = (_state: AuthState, action: Action) => action.state;
|
|
@ -1,17 +1,22 @@
|
||||||
import React, { PropsWithChildren, useContext } from 'react';
|
import React, { PropsWithChildren, useContext, useState } from 'react';
|
||||||
import { AppContext } from '../../components/StateProvider';
|
import { AppContext } from '../../components/StateProvider';
|
||||||
import { assertNever } from '../../plugins/Candela';
|
import { assertNever } from '../../plugins/Candela';
|
||||||
|
import { AuthContext } from '../../providers/AuthProvider/AuthProvider';
|
||||||
import './Authentication.css';
|
import './Authentication.css';
|
||||||
|
|
||||||
interface AuthenticationProps {
|
interface AuthenticationProps {
|
||||||
|
onAdminPassword: (password: string) => void;
|
||||||
|
onAuth: (username: string, password: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Authentication = ({ children }: PropsWithChildren<AuthenticationProps>) => {
|
export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithChildren<AuthenticationProps>) => {
|
||||||
// No admin password set: prompt for the admin password
|
// No admin password set: prompt for the admin password
|
||||||
// Password set, nobody logged in: prompt for login
|
// Password set, nobody logged in: prompt for login
|
||||||
// User logged in: show the children
|
// User logged in: show the children
|
||||||
|
|
||||||
let { auth } = useContext(AppContext);
|
let [userField, setUserField] = useState<string>("");
|
||||||
|
let [pwField, setPwField] = useState<string>("");
|
||||||
|
let auth = useContext(AuthContext);
|
||||||
|
|
||||||
switch (auth.type) {
|
switch (auth.type) {
|
||||||
case "NoAdmin": {
|
case "NoAdmin": {
|
||||||
|
@ -19,8 +24,8 @@ export const Authentication = ({ children }: PropsWithChildren<AuthenticationPro
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h1> Welcome to your new Visions VTT Instance </h1>
|
<h1> Welcome to your new Visions VTT Instance </h1>
|
||||||
<p> Set your admin password: </p>
|
<p> Set your admin password: </p>
|
||||||
<input type="password" placeholder="Password" />
|
<input type="password" placeholder="Password" onChange={(evt) => setPwField(evt.target.value)} />
|
||||||
<input type="submit" value="Submit" />
|
<input type="submit" value="Submit" onClick={() => onAdminPassword(pwField)} />
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -29,9 +34,9 @@ export const Authentication = ({ children }: PropsWithChildren<AuthenticationPro
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h1> Welcome to Visions VTT </h1>
|
<h1> Welcome to Visions VTT </h1>
|
||||||
<div className="auth__input-line">
|
<div className="auth__input-line">
|
||||||
<input type="text" placeholder="Username" />
|
<input type="text" placeholder="Username" onChange={(evt) => setUserField(evt.target.value)} />
|
||||||
<input type="password" placeholder="Password" />
|
<input type="password" placeholder="Password" onChange={(evt) => setPwField(evt.target.value)} />
|
||||||
<input type="submit" value="Sign in" />
|
<input type="submit" value="Sign in" onClick={() => onAuth(userField, pwField)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
Loading…
Reference in New Issue