From 3083fb17d9d5ef126c78f0ddd342c4fbda3a160b Mon Sep 17 00:00:00 2001 From: emilis Date: Sat, 3 Jan 2026 03:08:03 +0000 Subject: [PATCH] auto use pregenerated passwords (down to just 1 field now) --- Cargo.lock | 4 ++ plan/Cargo.toml | 4 ++ plan/index.scss | 59 +++++++++++++---- plan/src/components/dialog.rs | 54 ---------------- plan/src/components/nav.rs | 50 +++++---------- plan/src/main.rs | 1 - plan/src/pages/mainpage.rs | 19 ++++-- plan/src/pages/plan.rs | 117 +++++++++++++++++++++------------- plan/src/pages/signin.rs | 17 +++-- plan/src/pages/signup.rs | 54 ++++++++++------ plan/src/storage.rs | 21 +++++- 11 files changed, 221 insertions(+), 179 deletions(-) delete mode 100644 plan/src/components/dialog.rs diff --git a/Cargo.lock b/Cargo.lock index 68c7dbb..b75521c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -694,9 +694,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1786,12 +1788,14 @@ dependencies = [ "chrono-humanize", "ciborium", "futures", + "getrandom 0.3.4", "gloo 0.10.0", "instant", "log", "once_cell", "plan-proto", "postcard", + "rand 0.9.2", "serde", "thiserror 2.0.17", "wasm-bindgen", diff --git a/plan/Cargo.toml b/plan/Cargo.toml index 76d3784..f350219 100644 --- a/plan/Cargo.toml +++ b/plan/Cargo.toml @@ -24,6 +24,8 @@ web-sys = { version = "0.3.77", features = [ "ReadableStreamDefaultReader", "HtmlDialogElement", "DomRect", + "ShareData", + "Clipboard", ] } log = "0.4" yew = { version = "0.21", features = ["csr"] } @@ -42,3 +44,5 @@ thiserror = { version = "2" } plan-proto = { path = "../plan-proto", features = ["client"] } ciborium = { version = "0.2" } chrono-humanize = { version = "0.2.3", features = ["wasmbind"] } +rand = { version = "0.9" } +getrandom = { version = "0.3", features = ["wasm_js"] } diff --git a/plan/index.scss b/plan/index.scss index 68d653c..44b02db 100644 --- a/plan/index.scss +++ b/plan/index.scss @@ -356,9 +356,6 @@ dialog::backdrop { .dialog-box { - - - font-size: 2rem; border: 1px solid white; display: flex; flex-direction: column; @@ -372,6 +369,8 @@ dialog::backdrop { gap: 5px; color: white; + font-size: 1.5em; + .options { margin-top: 30px; display: flex; @@ -382,7 +381,6 @@ dialog::backdrop { &>button { min-width: 4cm; - font-size: 1em; } $close_color: rgba(255, 0, 0, 1); @@ -432,11 +430,13 @@ dialog::backdrop { flex-direction: column; align-items: center; font-size: 1.5rem; + gap: 10px; input { background-color: black; border: 1px solid white; color: white; + font-size: 1em; } .submit { @@ -458,6 +458,15 @@ dialog::backdrop { } } +.login-fields { + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-items: center; + gap: 10px; + width: 100%; +} + .fields { display: flex; flex-direction: row; @@ -547,20 +556,27 @@ dialog::backdrop { .splash { display: flex; - flex-direction: column; - flex-wrap: nowrap; + flex-direction: row; + flex-wrap: wrap; align-items: center; + user-select: none; + gap: 5vw; width: 100%; - .options { + .section { + flex-grow: 1; display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 1cm; + flex-direction: column; + flex-wrap: nowrap; + align-items: center; + min-height: 50vh; + } - & button { - font-size: 2rem; - } + .instruction { + margin: 0; + font-style: italic; + color: rgba(255, 255, 255, 0.7); + text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.3); } } @@ -727,3 +743,20 @@ select { } } } + +.notification { + border: 1px solid white; + padding: 20px; + // font-size: 2em; + color: white; + background-color: black; +} + +dialog:has(.notification) { + background-color: rgba(0, 0, 0, 0); + border: none; +} + +.share-button { + font-size: 1.5em; +} diff --git a/plan/src/components/dialog.rs b/plan/src/components/dialog.rs deleted file mode 100644 index 3d23d3c..0000000 --- a/plan/src/components/dialog.rs +++ /dev/null @@ -1,54 +0,0 @@ -use yew::prelude::*; - -#[derive(Debug, Clone, PartialEq, Properties)] -pub struct DialogProps { - #[prop_or_default] - pub children: Html, - pub options: Box<[String]>, - #[prop_or_default] - pub cancel_callback: Option>, - pub callback: Callback, -} - -#[function_component] -pub fn Dialog( - DialogProps { - children, - options, - cancel_callback, - callback, - }: &DialogProps, -) -> Html { - let options = options - .iter() - .map(|opt| { - let callback = callback.clone(); - let option = opt.clone(); - let cb = Callback::from(move |_| { - callback.emit(option.clone()); - }); - html! { - - } - }) - .collect::(); - let backdrop_click = cancel_callback.clone().map(|cancel_callback| { - Callback::from(move |_| { - cancel_callback.emit(()); - }) - }); - html! { -
-
-
-
- {children.clone()} -
-
- {options} -
-
-
-
- } -} diff --git a/plan/src/components/nav.rs b/plan/src/components/nav.rs index 8316189..1c5f622 100644 --- a/plan/src/components/nav.rs +++ b/plan/src/components/nav.rs @@ -1,7 +1,10 @@ use plan_proto::token::Token; use yew::prelude::*; -use crate::{components::dialog::Dialog, storage::StorageKey}; +use crate::{ + components::dialog_modal::{DialogModal, DialogMode}, + storage::StorageKey, +}; #[function_component] pub fn Nav() -> Html { @@ -23,51 +26,30 @@ pub fn Nav() -> Html { } }); - let dialog = use_state(|| false); let logged_in_buttons = token.is_some().then(|| { - let confirm_signout = { - let dialog = dialog.clone(); - Callback::from(move |_| { - dialog.set(true); - }) - }; - - let dialog = dialog.then(|| { - let cancel_signout = { - let dialog = dialog.clone(); - Callback::from(move |_| { - dialog.set(false); - }) - }; - let callback = Callback::from(move |option: String| match option.as_str() { - "yes" => { - Token::delete(); - let _ = gloo::utils::window().location().reload(); - } - "no" => { - dialog.set(false); - } - _ => {} + let signout = { + let callback = Callback::from(move |_| { + Token::delete(); + let _ = gloo::utils::window().location().reload(); }); - let options: Box<[String]> = Box::new([String::from("yes"), String::from("no")]); html! { -

{"really sign out?"}

-
+ } - }); + }; html! { <>
- + {signout}
- {dialog} } }); diff --git a/plan/src/main.rs b/plan/src/main.rs index e7c517b..43675ef 100644 --- a/plan/src/main.rs +++ b/plan/src/main.rs @@ -8,7 +8,6 @@ mod pages { pub mod user_settings_page; } mod components { - pub mod dialog; pub mod dialog_modal; pub mod error; pub mod half_hour_range_select; diff --git a/plan/src/pages/mainpage.rs b/plan/src/pages/mainpage.rs index 3114a3d..fd81ffc 100644 --- a/plan/src/pages/mainpage.rs +++ b/plan/src/pages/mainpage.rs @@ -1,7 +1,11 @@ use core::{num::NonZeroU8, time::Duration}; use std::rc::Rc; -use crate::{SERVER_URL, storage::StorageKey}; +use crate::{ + SERVER_URL, + pages::{signin::Signin, signup::Signup}, + storage::StorageKey, +}; use futures::{FutureExt, StreamExt, lock::Mutex, select}; use gloo::net::http::Request; use plan_proto::{ @@ -169,10 +173,15 @@ pub fn MainPage() -> Html { pub fn SignedOutMainPage() -> Html { html! {
-

{"plan"}

-
- - +
+

{"create new account"}

+

{"with just a username"}

+ +
+
+

{"or sign in with an account"}

+

{"that has a password set"}

+
} diff --git a/plan/src/pages/plan.rs b/plan/src/pages/plan.rs index 3953f35..0bab21e 100644 --- a/plan/src/pages/plan.rs +++ b/plan/src/pages/plan.rs @@ -1,27 +1,28 @@ -use core::{cell::RefCell, ops::Not, sync::atomic::AtomicBool, time::Duration}; +use core::{cell::RefCell, sync::atomic::AtomicBool, time::Duration}; use std::rc::Rc; use chrono::TimeDelta; -use chrono_humanize::{HumanTime, Humanize, Tense}; +use chrono_humanize::{HumanTime, Tense}; use core::ops::Deref; -use futures::{FutureExt, SinkExt, StreamExt, channel::mpsc::Receiver, stream::Fuse}; +use futures::{SinkExt, StreamExt, stream::Fuse}; use gloo::net::websocket::{Message, futures::WebSocket}; use plan_proto::{ error::ServerError, message::{ClientMessage, ServerMessage}, - plan::{Plan, PlanId, UpdateTiles}, + plan::{Plan, PlanId}, token::{Token, TokenLogin}, }; use serde::Serialize; use thiserror::Error; -use wasm_bindgen::JsError; +use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen_futures::JsFuture; +use web_sys::{HtmlDialogElement, ShareData, js_sys::Reflect}; use yew::{platform::pinned::mpsc::UnboundedReceiver, prelude::*}; use crate::{ WS_URL, components::{error::ErrorDisplay, planview::PlanView}, pages::{mainpage::SignedOutMainPage, not_found::NotFound}, - request::RequestError, storage::{LastPlanVisitedLoggedOut, StorageKey}, }; @@ -167,41 +168,6 @@ impl Session { let ws = WebSocket::open(url) .map_err(|err| ConnectionError::SocketOpeningError(err.to_string()))?; Ok(ws) - // struct WsConnectFuture(RefCell>); - // let ws_fut = WsConnectFuture(RefCell::new(Some(ws))); - // impl Future for WsConnectFuture { - // type Output = Result; - - // fn poll( - // self: std::pin::Pin<&mut Self>, - // cx: &mut std::task::Context<'_>, - // ) -> std::task::Poll { - // let ws = match self.0.try_borrow_mut() { - // Ok(ws) => ws, - // Err(_) => { - // log::warn!("ref mut"); - // cx.waker().wake_by_ref(); - // return std::task::Poll::Pending; - // } - // }; - // match ws.as_ref().map(|ws| ws.state()) { - // Some(gloo::net::websocket::State::Connecting) => { - // cx.waker().wake_by_ref(); - // std::task::Poll::Pending - // } - // Some(gloo::net::websocket::State::Open) => { - // std::task::Poll::Ready(Ok(self.0.take().unwrap())) - // } - // Some(gloo::net::websocket::State::Closing) - // | Some(gloo::net::websocket::State::Closed) => { - // std::task::Poll::Ready(Err(ConnectionError::SocketClosed)) - // } - // None => std::task::Poll::Pending, - // } - // } - // } - - // ws_fut.await } #[allow(clippy::await_holding_refcell_ref)] @@ -314,6 +280,70 @@ impl Session { #[function_component] pub fn PlanPage(PlanPageProps { id }: &PlanPageProps) -> Html { + const SHARE_SUCCESS_ID: &str = "share-link-copied"; + let share_button = { + let share_cb = { + Callback::from(move |_| { + if let Some(href) = gloo::utils::document() + .location() + .and_then(|a| a.href().ok()) + { + let share_data = ShareData::new(); + share_data.set_url(&href); + yew::platform::spawn_local(async move { + let nav = gloo::utils::window().navigator(); + let can_share = Reflect::get(&nav, &JsValue::from_str("share")) + .map(|v| !v.is_undefined()) + .unwrap_or_default(); + + if can_share && nav.can_share_with_data(&share_data) { + if let Err(err) = + Into::::into(nav.share_with_data(&share_data)).await + { + log::error!("share url [{href}]: {err:?}"); + } + return; + } + // not supported on this platform; + // copy url to clipboard and show the copied link modal + if let Err(err) = Into::::into( + gloo::utils::window() + .navigator() + .clipboard() + .write_text(&href), + ) + .await + { + log::error!("error writing to clipboard: {err:?}"); + return; + } + let Some(dialog) = gloo::utils::document() + .get_element_by_id(SHARE_SUCCESS_ID) + .and_then(|d| d.dyn_into::().ok()) + else { + return; + }; + if let Err(err) = dialog.show_modal() { + log::error!("show success modal: {err:?}"); + return; + } + gloo::timers::future::sleep(Duration::from_secs(1)).await; + dialog.close(); + }); + } + }) + }; + html! { + <> + + +
+

{"link copied to clipboard"}

+
+
+ + } + }; let plan_state = use_state_eq(|| PlanState::Loading); let token = match use_context::() { Some(token) => token, @@ -330,9 +360,7 @@ pub fn PlanPage(PlanPageProps { id }: &PlanPageProps) -> Html { let error_obj = html! { }; - let on_error = use_callback(error_state.setter(), |err: Option, err_state| { - err_state.set(err); - }); + let (send, recv) = yew::platform::pinned::mpsc::unbounded(); let send = use_state(|| send); let recv = use_mut_ref(|| recv); @@ -381,6 +409,7 @@ pub fn PlanPage(PlanPageProps { id }: &PlanPageProps) -> Html { html! {
+ {share_button} {content} {error_obj}
diff --git a/plan/src/pages/signin.rs b/plan/src/pages/signin.rs index 7fca7e7..b705c75 100644 --- a/plan/src/pages/signin.rs +++ b/plan/src/pages/signin.rs @@ -10,6 +10,7 @@ use crate::{ state_input::{InputType, StateInput}, }, error::JsError, + storage::{LoginState, StorageKey}, }; #[function_component] @@ -19,8 +20,17 @@ pub fn Signin() -> Html { let error_obj = html! { }; - let username_state = use_state(String::new); - let password_state = use_state(String::new); + + let (u, p) = if let Ok(LoginState::Passwordless { username, password }) = + LoginState::load_from_storage() + { + (username.to_string(), password.to_string()) + } else { + (String::new(), String::new()) + }; + + let username_state = use_state(|| u); + let password_state = use_state(|| p); let (submit_username, submit_password) = (username_state.clone(), password_state.clone()); let on_submit = { @@ -96,8 +106,7 @@ pub fn Signin() -> Html { }; html! {