Compare commits
25 Commits
main
...
visions-ui
Author | SHA1 | Date | |
---|---|---|---|
11e33eca2f | |||
fd3ca9f561 | |||
e8a8a12de3 | |||
7f0b7982ec | |||
5e4fd97aca | |||
1c4894df9a | |||
20b214df10 | |||
ca89455d4d | |||
2ff981e28a | |||
672578b9a9 | |||
fb2fcf4d36 | |||
a1dc573fc5 | |||
0d39690560 | |||
9439cfea34 | |||
1d050f014a | |||
df1dfeaae3 | |||
8ab8cd0684 | |||
aa7229eae4 | |||
0663a70c97 | |||
41bb21c254 | |||
182020e136 | |||
79af050f53 | |||
dca9c3c39e | |||
e9f89e1bdb | |||
f6534d5d05 |
738
Cargo.lock
generated
738
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@ -2,24 +2,24 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"authdb",
|
||||
# "bike-lights/bike",
|
||||
"bike-lights/core",
|
||||
"bike-lights/simulator",
|
||||
"changeset",
|
||||
"config",
|
||||
"config-derive",
|
||||
"coordinates",
|
||||
"cyberpunk",
|
||||
"cyber-slides",
|
||||
"cyberpunk",
|
||||
"cyberpunk-splash",
|
||||
"dashboard",
|
||||
"emseries",
|
||||
"file-service",
|
||||
"fitnesstrax/core",
|
||||
"fitnesstrax/app",
|
||||
"fitnesstrax/core",
|
||||
"fluent-ergonomics",
|
||||
"geo-types",
|
||||
"gm-control-panel",
|
||||
"gm-dash/server",
|
||||
"hex-grid",
|
||||
"icon-test",
|
||||
"memorycache",
|
||||
@ -32,5 +32,8 @@ members = [
|
||||
"timezone-testing",
|
||||
"tree",
|
||||
"visions/server",
|
||||
"gm-dash/server"
|
||||
"visions/types",
|
||||
"visions/ui",
|
||||
"visions/yew-app",
|
||||
# "bike-lights/bike",
|
||||
]
|
||||
|
@ -25,6 +25,7 @@
|
||||
pkgs.cargo-watch
|
||||
pkgs.clang
|
||||
pkgs.crate2nix
|
||||
pkgs.trunk
|
||||
pkgs.glib
|
||||
pkgs.gst_all_1.gst-plugins-bad
|
||||
pkgs.gst_all_1.gst-plugins-base
|
||||
|
@ -4,3 +4,12 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8.1", features = ["macros"] }
|
||||
visions-types = { path = "../types" }
|
||||
serde = { version = "1.0.217", features = ["derive", "serde_derive"] }
|
||||
tokio = { version = "1.43.0", features = ["full", "rt"] }
|
||||
tower-http = { version = "0.6.2", features = ["cors"] }
|
||||
typeshare = "1.0.4"
|
||||
uuid = { version = "1.13.1", features = ["v4"] }
|
||||
result-extended = { path = "../../result-extended" }
|
||||
thiserror = "2.0.11"
|
||||
|
@ -1,3 +1,155 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
use std::future::Future;
|
||||
|
||||
use axum::{
|
||||
http::{
|
||||
header::{AUTHORIZATION, CONTENT_TYPE},
|
||||
HeaderMap, Method, StatusCode,
|
||||
},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use visions_types::{AccountStatus, AuthRequest, AuthResponse, SessionId, UserOverview};
|
||||
use result_extended::{error, ok, ResultExt};
|
||||
use thiserror::Error;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn check_password(
|
||||
request: Json<AuthRequest>,
|
||||
) -> (StatusCode, Json<Option<AuthResponse>>) {
|
||||
let Json(request) = request;
|
||||
if request.username == "vakarian" && request.password == "aoeu" {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(Some(AuthResponse::Success("vakarian-session-id".into()))),
|
||||
)
|
||||
} else if request.username == "shephard" && request.password == "aoeu" {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(Some(AuthResponse::PasswordReset(
|
||||
"shephard-session-id".into(),
|
||||
))),
|
||||
)
|
||||
} else {
|
||||
(StatusCode::UNAUTHORIZED, Json(None))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum AppError {
|
||||
#[error("no user authorized")]
|
||||
Unauthorized,
|
||||
#[error("bad request")]
|
||||
BadRequest,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum FatalError {
|
||||
#[error("on unknown fatal error occurred")]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl result_extended::FatalError for FatalError {}
|
||||
|
||||
fn parse_session_header(headers: HeaderMap) -> ResultExt<Option<SessionId>, AppError, FatalError> {
|
||||
match headers.get("Authorization") {
|
||||
Some(token) => {
|
||||
match token
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.split(" ")
|
||||
.collect::<Vec<&str>>()
|
||||
.as_slice()
|
||||
{
|
||||
[_schema, token] => ok(Some(SessionId::from(*token))),
|
||||
_ => error(AppError::BadRequest),
|
||||
}
|
||||
}
|
||||
None => ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn auth_required<B, F, Fut>(headers: HeaderMap, f: F) -> (StatusCode, Json<Option<B>>)
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = (StatusCode, B)>,
|
||||
{
|
||||
match parse_session_header(headers) {
|
||||
ResultExt::Ok(Some(session_id)) => {
|
||||
if session_id == "vakarian-session-id".into() {
|
||||
let (code, result) = f().await;
|
||||
(code, Json(Some(result)))
|
||||
} else {
|
||||
(StatusCode::UNAUTHORIZED, Json(None))
|
||||
}
|
||||
}
|
||||
ResultExt::Ok(None) => (StatusCode::UNAUTHORIZED, Json(None)),
|
||||
ResultExt::Err(AppError::Unauthorized) => (StatusCode::UNAUTHORIZED, Json(None)),
|
||||
ResultExt::Err(AppError::BadRequest) => (StatusCode::BAD_REQUEST, Json(None)),
|
||||
ResultExt::Fatal(err) => {
|
||||
panic!("{}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/api/test/health",
|
||||
get(|| async { (StatusCode::OK, Json(None::<String>)) }).layer(
|
||||
CorsLayer::new()
|
||||
.allow_methods([Method::GET])
|
||||
.allow_origin(Any),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/api/test/auth",
|
||||
post(|req: Json<AuthRequest>| check_password(req)).layer(
|
||||
CorsLayer::new()
|
||||
.allow_methods([Method::POST])
|
||||
.allow_headers([CONTENT_TYPE])
|
||||
.allow_origin(Any),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/api/test/list-users",
|
||||
get(|headers: HeaderMap| {
|
||||
auth_required(headers, || async {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Some(vec![
|
||||
UserOverview {
|
||||
id: "vakarian-id".into(),
|
||||
name: "vakarian".to_owned(),
|
||||
status: AccountStatus::Ok,
|
||||
},
|
||||
UserOverview {
|
||||
id: "shephard-id".into(),
|
||||
name: "shephard".to_owned(),
|
||||
status: AccountStatus::PasswordReset(
|
||||
"2050-01-01 00:00:00".to_owned(),
|
||||
),
|
||||
},
|
||||
UserOverview {
|
||||
id: "tali-id".into(),
|
||||
name: "tali".to_owned(),
|
||||
status: AccountStatus::Locked,
|
||||
},
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_headers([AUTHORIZATION])
|
||||
.allow_methods([Method::GET])
|
||||
.allow_origin(Any),
|
||||
),
|
||||
);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:8001")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
2
visions/types/.gitignore
vendored
Normal file
2
visions/types/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
gen/
|
||||
dist/
|
8
visions/types/Cargo.toml
Normal file
8
visions/types/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "visions-types"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.218", features = ["derive"] }
|
||||
uuid = { version = "1.13.2", features = ["v4", "js"] }
|
@ -1,8 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- npm install typescript
|
||||
- typeshare --lang typescript --output-file visions.ts ../server/src
|
||||
- npx tsc
|
@ -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"
|
||||
}
|
||||
}
|
81
visions/types/src/lib.rs
Normal file
81
visions/types/src/lib.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(tag = "type", content = "content", rename_all = "kebab-case")]
|
||||
pub enum AccountStatus {
|
||||
Ok,
|
||||
PasswordReset(String),
|
||||
Locked,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct AuthRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub struct SessionId(String);
|
||||
|
||||
impl SessionId {
|
||||
pub fn new() -> Self {
|
||||
Self(format!("{}", Uuid::new_v4().hyphenated()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for SessionId {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for SessionId {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub struct UserId(String);
|
||||
|
||||
impl UserId {
|
||||
pub fn new() -> Self {
|
||||
Self(format!("{}", Uuid::new_v4().hyphenated()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for UserId {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for UserId {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct UserOverview {
|
||||
pub id: UserId,
|
||||
pub name: String,
|
||||
pub status: AccountStatus,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(tag = "type", content = "content", rename_all = "kebab-case")]
|
||||
pub enum AuthResponse {
|
||||
Success(SessionId),
|
||||
PasswordReset(SessionId),
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
23
visions/ui/.gitignore
vendored
23
visions/ui/.gitignore
vendored
@ -1,23 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
17
visions/ui/Cargo.toml
Normal file
17
visions/ui/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "visions-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
visions-types = { path = "../types" }
|
||||
gloo-console = "0.3.0"
|
||||
gloo-net = "0.6.0"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.6.5"
|
||||
serde_json = "1.0.138"
|
||||
wasm-bindgen = "0.2.100"
|
||||
wasm-bindgen-futures = "0.4.50"
|
||||
web-sys = "0.3.77"
|
||||
yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] }
|
||||
|
@ -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
|
3
visions/ui/Trunk.toml
Normal file
3
visions/ui/Trunk.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[[proxy]]
|
||||
backend = "http://localhost:8001/api"
|
||||
insecure = true
|
36
visions/ui/design.css
Normal file
36
visions/ui/design.css
Normal 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;
|
||||
}
|
9
visions/ui/index.html
Normal file
9
visions/ui/index.html
Normal file
@ -0,0 +1,9 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Visions Client Demo</title>
|
||||
<link data-trunk rel="css" href="design.css" />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
44
visions/ui/src/client.rs
Normal file
44
visions/ui/src/client.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use std::future::Future;
|
||||
|
||||
use gloo_console::log;
|
||||
use gloo_net::http::{Request, Response};
|
||||
use visions_types::{AuthRequest, AuthResponse, SessionId, UserOverview};
|
||||
|
||||
pub enum ClientError {
|
||||
Unauthorized,
|
||||
Err(u16),
|
||||
}
|
||||
|
||||
pub trait Client {
|
||||
fn auth(&self, username: String, password: String) -> impl Future<Output = Result<AuthResponse, ClientError>>;
|
||||
fn list_users(&self, session_id: SessionId) -> impl Future<Output = Result<Vec<UserOverview>, ClientError>>;
|
||||
}
|
||||
|
||||
pub struct Connection;
|
||||
|
||||
impl Connection {
|
||||
pub fn new() -> Self { Self }
|
||||
}
|
||||
|
||||
impl Client for Connection {
|
||||
async fn auth(&self, username: String, password: String) -> Result<AuthResponse, ClientError> {
|
||||
log!("authenticating: ", &username, &password);
|
||||
let response: Response = Request::post("/api/test/auth")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(serde_wasm_bindgen::to_value(&serde_json::to_string(&AuthRequest{ username, password }).unwrap()).unwrap())
|
||||
.unwrap()
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if response.ok() {
|
||||
Ok(serde_json::from_slice(&response.binary().await.unwrap()).unwrap())
|
||||
} else {
|
||||
Err(ClientError::Err(response.status()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_users(&self, session_id: SessionId) -> Result<Vec<UserOverview>, ClientError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
128
visions/ui/src/main.rs
Normal file
128
visions/ui/src/main.rs
Normal file
@ -0,0 +1,128 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gloo_console::log;
|
||||
use visions_types::AuthResponse;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::HtmlInputElement;
|
||||
use yew::prelude::*;
|
||||
|
||||
mod client;
|
||||
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> {
|
||||
// log!("reduce", action);
|
||||
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_login: Callback<(String, String)>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn Login(LoginProps { on_login }: &LoginProps) -> Html {
|
||||
let username = use_state(|| "".to_owned());
|
||||
let password = use_state(|| "".to_owned());
|
||||
|
||||
let on_click = {
|
||||
let on_login = on_login.clone();
|
||||
let username = username.clone();
|
||||
let password = password.clone();
|
||||
Callback::from(move |_| on_login.emit((username.to_string(), password.to_string())))
|
||||
};
|
||||
|
||||
let on_username_changed = {
|
||||
let username = username.clone();
|
||||
Callback::from(move |event: Event| {
|
||||
let input = event
|
||||
.target()
|
||||
.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
|
||||
if let Some(input) = input {
|
||||
username.set(input.value());
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_password_changed = {
|
||||
let password = password.clone();
|
||||
Callback::from(move |event: Event| {
|
||||
let input = event
|
||||
.target()
|
||||
.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
|
||||
if let Some(input) = input {
|
||||
password.set(input.value());
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="login-form">
|
||||
<div class="card">
|
||||
<h1>{"Welcome to Visions VTT"}</h1>
|
||||
<input type="text" name="username" placeholder="username" onchange={on_username_changed} />
|
||||
<input type="password" name="password" placeholder="password" onchange={on_password_changed} />
|
||||
<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 |(username, password)| {
|
||||
let auth_info = auth_info.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let client = Connection::new();
|
||||
match client.auth(username, password).await {
|
||||
Ok(AuthResponse::Success(session_id)) => {
|
||||
auth_info.dispatch(AuthAction::Auth(session_id.as_str().to_owned()))
|
||||
}
|
||||
Ok(AuthResponse::PasswordReset(session_id)) => {
|
||||
auth_info.dispatch(AuthAction::Auth(session_id.as_str().to_owned()))
|
||||
}
|
||||
Err(ClientError::Unauthorized) => todo!(),
|
||||
Err(ClientError::Err(status)) => todo!(),
|
||||
};
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
if auth_info.session_id.is_some() {
|
||||
html! { <p>{ "this is just a thing" }</p> }
|
||||
} else {
|
||||
html! { <Login on_login={on_login.clone()} /> }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
@ -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"
|
||||
]
|
||||
}
|
11
visions/yew-app/Cargo.toml
Normal file
11
visions/yew-app/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[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"] }
|
||||
|
3
visions/yew-app/design.css
Normal file
3
visions/yew-app/design.css
Normal file
@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: hsl(0, 0%, 50%);
|
||||
}
|
5
visions/yew-app/index.html
Normal file
5
visions/yew-app/index.html
Normal file
@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head></head>
|
||||
<body></body>
|
||||
</html>
|
126
visions/yew-app/src/main.rs
Normal file
126
visions/yew-app/src/main.rs
Normal file
@ -0,0 +1,126 @@
|
||||
use yew::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use gloo_net::http::Request;
|
||||
|
||||
#[derive(Clone, PartialEq, Deserialize)]
|
||||
struct Video {
|
||||
id: usize,
|
||||
title: String,
|
||||
speaker: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct VideosListProps {
|
||||
videos: Vec<Video>,
|
||||
on_click: Callback<Video>,
|
||||
}
|
||||
|
||||
/*
|
||||
fn videos() -> Vec<Video> {
|
||||
vec![
|
||||
Video {
|
||||
id: 1,
|
||||
title: "Building and breaking things".to_string(),
|
||||
speaker: "John Doe".to_string(),
|
||||
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
},
|
||||
Video {
|
||||
id: 2,
|
||||
title: "The development process".to_string(),
|
||||
speaker: "Jane Smith".to_string(),
|
||||
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
},
|
||||
Video {
|
||||
id: 3,
|
||||
title: "The Web 7.0".to_string(),
|
||||
speaker: "Matt Miller".to_string(),
|
||||
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
},
|
||||
Video {
|
||||
id: 4,
|
||||
title: "Mouseless development".to_string(),
|
||||
speaker: "Tom Jerry".to_string(),
|
||||
url: "https://youtu.be/PsaFVLr8t4E".to_string(),
|
||||
},
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
#[function_component(VideosList)]
|
||||
fn videos_list(VideosListProps { videos, on_click }: &VideosListProps) -> Html {
|
||||
let on_click = on_click.clone();
|
||||
videos
|
||||
.iter()
|
||||
.map(|video| {
|
||||
let on_video_select = {
|
||||
let on_click = on_click.clone();
|
||||
let video = video.clone();
|
||||
Callback::from(move |_| on_click.emit(video.clone()))
|
||||
};
|
||||
html! {
|
||||
<p key={video.id} onclick={on_video_select}>{format!("{}: {}", video.speaker, video.title)}</p>
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct VideosDetailsProps {
|
||||
video: Video,
|
||||
}
|
||||
|
||||
#[function_component(VideoDetails)]
|
||||
fn video_details(VideosDetailsProps { video }: &VideosDetailsProps) -> Html {
|
||||
html! {
|
||||
<div>
|
||||
<h3>{video.title.clone()}</h3>
|
||||
<img src="https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder" alt="video thumbnail" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
let videos = use_state(|| vec![]);
|
||||
{
|
||||
let videos = videos.clone();
|
||||
use_effect_with((), move |_| {
|
||||
let videos = videos.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let response = Request::get("/tutorial/data.json").send().await;
|
||||
println!("response: {:?}", response);
|
||||
let response = response.unwrap();
|
||||
let fetched_videos: Vec<Video> = response.json().await.unwrap();
|
||||
videos.set(fetched_videos);
|
||||
});
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
let selected_video = use_state(|| None);
|
||||
|
||||
let on_video_select = {
|
||||
let selected_video = selected_video.clone();
|
||||
Callback::from(move |video: Video| selected_video.set(Some(video)))
|
||||
};
|
||||
|
||||
let details = selected_video.as_ref().map(|video| html! {
|
||||
<VideoDetails video={video.clone()} />
|
||||
});
|
||||
|
||||
html! {
|
||||
<>
|
||||
<h1>{ "RustConf Explorer" }</h1>
|
||||
<div>
|
||||
<h3>{"Videos to watch"}</h3>
|
||||
<VideosList videos={(*videos).clone()} on_click={on_video_select.clone()} />
|
||||
</div>
|
||||
{ for details }
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
Loading…
Reference in New Issue
Block a user