Compare commits

...

4 Commits

16 changed files with 182 additions and 367 deletions

10
Cargo.lock generated
View File

@ -5475,6 +5475,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "visions-client"
version = "0.1.0"
dependencies = [
"gloo-net 0.6.0",
"serde 1.0.217",
"wasm-bindgen-futures",
"yew",
]
[[package]]
name = "wait-timeout"
version = "0.2.0"

View File

@ -33,4 +33,4 @@ members = [
"tree",
"visions/server",
"gm-dash/server"
, "visions/yew-app"]
, "visions/yew-app", "visions/ui"]

24
visions/ui/.gitignore vendored
View File

@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

11
visions/ui/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "visions-client"
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,50 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

View File

@ -1,17 +0,0 @@
version: "3"
tasks:
fmt:
cmds:
- npm run fmt
build:
cmds:
- npm install
- npx tsc --watch
test:
cmds:
# - cd ../visions-types && task build
- npm install
- npm run test

36
visions/ui/design.css Normal file
View File

@ -0,0 +1,36 @@
:root {
--spacing-s: 4px;
--spacing-m: 8px;
--shadow-shallow: 2px 2px 1px;
}
body {
background-color: hsl(0, 0%, 95%);
font-family: Ariel, sans-serif;
}
.card {
display: flex;
flex-direction: column;
align-items: space-between;
border: 1px solid black;
box-shadow: var(--shadow-shallow);
border-radius: var(--spacing-s);
padding: var(--spacing-m);
}
.card > h1 {
margin: 0px;
}
.card > * {
margin-top: var(--spacing-s);
margin-bottom: var(--spacing-s);
}
.login-form {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}

View File

@ -1,29 +0,0 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended, eslintConfigPrettier],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
},
);

View File

@ -1,11 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> My first web component with typescript </title>
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Visions Client Demo</title>
<link data-trunk rel="css" href="design.css" />
</head>
<body>
<app-component name="TypeScript"></app-component>
<script type="module" src="./dist/app-component.js"></script>
</body>
<body></body>
</html>

View File

@ -1,111 +0,0 @@
{
"name": "ui",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ui",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"lit": "^3.2.1",
"visions-client": "file:../client",
"visions-types": "file:../types"
},
"devDependencies": {
"typescript": "^5.7.3"
}
},
"../client": {
"name": "visions-client",
"version": "0.0.1",
"license": "ISC",
"dependencies": {
"visions-types": "file:../types"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"prettier": "^3.5.1",
"ts-jest": "^29.2.5",
"typescript": "^5.7.3"
}
},
"../types": {
"name": "visions-types",
"version": "0.0.1",
"license": "ISC",
"dependencies": {
"typescript": "^5.7.3"
}
},
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
"integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ=="
},
"node_modules/@lit/reactive-element": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz",
"integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.2.0"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
"node_modules/lit": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz",
"integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==",
"dependencies": {
"@lit/reactive-element": "^2.0.4",
"lit-element": "^4.1.0",
"lit-html": "^3.2.0"
}
},
"node_modules/lit-element": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.1.tgz",
"integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.2.0",
"@lit/reactive-element": "^2.0.4",
"lit-html": "^3.2.0"
}
},
"node_modules/lit-html": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.1.tgz",
"integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==",
"dependencies": {
"@types/trusted-types": "^2.0.2"
}
},
"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==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/visions-client": {
"resolved": "../client",
"link": true
},
"node_modules/visions-types": {
"resolved": "../types",
"link": true
}
}
}

View File

@ -1,20 +0,0 @@
{
"name": "ui",
"version": "1.0.0",
"description": "This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.",
"main": "eslint.config.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"typescript": "^5.7.3"
},
"dependencies": {
"lit": "^3.2.1",
"visions-client": "file:../client",
"visions-types": "file:../types"
}
}

View File

@ -1,21 +0,0 @@
import {html, css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
export class SimpleGreeting extends LitElement {
static styles = css`p { color: blue }`;
name: String;
static properties = {
name: { type: String }
};
constructor() {
super();
this.name = 'Somebody';
}
render() {
return html`<p>Hello, ${this.name}!</p>`;
}
}

39
visions/ui/src/client.rs Normal file
View File

@ -0,0 +1,39 @@
use gloo_net::http::Request;
use serde::Deserialize;
#[derive(Deserialize)]
enum AuthResponse {
Ok(SessionId),
PasswordReset(SessionId),
}
#[derive(Deserialize)]
struct SessionId(String);
enum ClientError {
Unauthorized,
}
#[derive(Deserialize)]
struct UserId(String);
#[derive(Deserialize)]
struct UserInfo {
id: UserId,
name: String,
}
trait Client {
async fn auth(username: String, password: String) -> Result<AuthResponse, ClientError>;
async fn list_users(session_id: SessionId) -> Result<Vec<UserInfo>, ClientError>;
}
impl Client for Connection {
async fn auth(username: String, password: String) -> Result<AuthResponse, ClientError> {
let request = Request::post("http://localhost:8001")
.body().unwrap();
}
async fn list_users(session_id: SessionId) -> Result<Vec<UserInfo>, ClientError> {
}
}

78
visions/ui/src/main.rs Normal file
View File

@ -0,0 +1,78 @@
use std::rc::Rc;
use yew::prelude::*;
mod client;
pub use client::*;
struct AuthInfo {
session_id: Option<String>,
}
impl Default for AuthInfo {
fn default() -> Self {
Self { session_id: None }
}
}
enum AuthAction {
Auth(String),
Unauth,
}
impl Reducible for AuthInfo {
type Action = AuthAction;
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
match action {
AuthAction::Auth(session_id) => Self {
session_id: Some(session_id),
}
.into(),
AuthAction::Unauth => Self { session_id: None }.into(),
}
}
}
#[derive(Properties, PartialEq)]
struct LoginProps {
on_click: Callback<()>,
}
#[function_component]
fn Login(LoginProps { on_click }: &LoginProps) -> Html {
let on_click = {
let on_click = on_click.clone();
Callback::from(move |_| on_click.emit(()))
};
html! {
<div class="login-form">
<div class="card">
<h1>{"Welcome to Visions VTT"}</h1>
<input type="text" name="username" placeholder="username" />
<input type="password" name="password" placeholder="password" />
<button onclick={on_click}>{"Login"}</button>
</div>
</div>
}
}
#[function_component]
fn App() -> Html {
let auth_info = use_reducer(AuthInfo::default);
let on_login = {
let auth_info = auth_info.clone();
Callback::from(move |_| auth_info.dispatch(AuthAction::Auth("abcdefg".into())))
};
if auth_info.session_id.is_some() {
html! { <p>{ "this is just a thing" }</p> }
} else {
html! { <Login on_click={on_login.clone()} /> }
}
}
fn main() {
yew::Renderer::<App>::new().render();
}

View File

@ -1,70 +0,0 @@
import { Client } from 'visions-client'
export type AuthState =
| { type: 'unauthed' }
| { type: 'authed'; sessionId: string }
export type State = {
auth: AuthState
}
export const initialState = (): State => ({
auth: { type: 'unauthed' },
})
export const sessionId = (state: State): string | undefined => {
if (state.auth.type === 'authed') {
return state.auth.sessionId
} else {
return undefined
}
}
export type Action = { type: 'set-auth'; content: AuthState }
export const reducer = (state: State, action: Action) => {
switch (action.type) {
case 'set-auth': {
return { ...state, auth: action.content }
}
default: {
return state
}
}
}
export class Controller {
client: Client
state: State
constructor(
client: Client,
state: State,
) {
this.client = client
this.state = state
}
// On any request, there are four options.
// The request succeeds. No problem.
// The request succeeds, but the user needs to reset their password.
// The action fails.
// The HTTP request itself fails.
async auth(username: string, password: string) {
let response = await this.client.auth(username, password)
switch (response.status) {
case 'ok': {
/*
this.dispatch({
type: 'set-auth',
content: {
type: 'authed',
sessionId: response.content.content,
},
})
*/
return
}
}
}
}

View File

@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"module": "es6",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src"
}
}