//! Share creation dialog and the "Your shares" management panel (milestone 6). use leptos::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_futures::spawn_local; use crate::api::{self, ShareInfo}; use crate::components::toast::{show, ToastMsg}; use crate::router::share_url; use crate::util::format_date; // --------------------------------------------------------------------------- // Expiry helpers (compute the RFC 3339 expiry client-side) // --------------------------------------------------------------------------- /// Format a millisecond timestamp as an RFC 3339 UTC string. fn ms_to_rfc3339(ms: f64) -> Option { if ms.is_nan() { return None; } let secs = (ms / 1000.0) as i64; chrono::DateTime::from_timestamp(secs, 0) .map(|dt| dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)) } /// Convert the user's expiry choice into an RFC 3339 timestamp, or `None` for /// "never". Presets are computed from "now"; "custom" parses the /// datetime-local value as the browser's local time. fn expiry_to_rfc3339(choice: &str, custom: &str) -> Option { let now = js_sys::Date::now(); let h = 60.0 * 60.0 * 1000.0; let target_ms: Option = match choice { "never" => return None, "1h" => Some(now + h), "1d" => Some(now + 24.0 * h), "1w" => Some(now + 7.0 * 24.0 * h), "1mo" => Some(now + 30.0 * 24.0 * h), "custom" => { if custom.is_empty() { None } else { // `Date.parse` treats a bare datetime as local time, so the // result is already the correct UTC instant. Some(js_sys::Date::parse(custom)) } } _ => return None, }; target_ms.and_then(ms_to_rfc3339) } fn copy_to_clipboard(text: &str, toast: ToastMsg) { let Some(win) = web_sys::window() else { show(toast, "Copy the link manually".to_string()); return; }; let cb = win.navigator().clipboard(); let t = text.to_string(); spawn_local(async move { let _ = wasm_bindgen_futures::JsFuture::from(cb.write_text(&t)).await; show(toast, "Link copied to clipboard".to_string()); }); } fn select_value(ev: &web_sys::Event) -> Option { ev.target() .and_then(|t| t.dyn_into::().ok()) .map(|s| s.value()) } // --------------------------------------------------------------------------- // Share creation dialog (two-phase: form -> link) // --------------------------------------------------------------------------- #[component] pub fn ShareDialog( name: String, root_id: i64, path: String, allow_writable: bool, close: Callback<()>, ) -> impl IntoView { let toast = use_context::().expect("toast context"); let (writable, set_writable) = signal(false); let (choice, set_choice) = signal("never".to_string()); let (custom, set_custom) = signal(String::new()); let (created, set_created) = signal(Option::::None); let (busy, set_busy) = signal(false); let do_create = move |_| { if busy.get() { return; } set_busy.set(true); let exp = expiry_to_rfc3339(&choice.get(), &custom.get()); let wr = writable.get(); let rid = root_id; let p = path.clone(); spawn_local(async move { match api::create_share(rid, &p, wr, exp.as_deref()).await { Ok(info) => set_created.set(Some(info)), Err(e) => { show(toast, format!("Could not create share: {e}")); set_busy.set(false); } } }); }; view! { } } // --------------------------------------------------------------------------- // "Your shares" management panel // --------------------------------------------------------------------------- #[component] pub fn SharesPanel(close: Callback<()>) -> impl IntoView { let toast = use_context::().expect("toast context"); let (shares, set_shares) = signal(Option::>::None); { let set = set_shares; let toast2 = toast; spawn_local(async move { match api::list_shares().await { Ok(s) => set.set(Some(s)), Err(e) => show(toast2, e.to_string()), } }); } view! { } }