From 7d337dfeeef8e03522c6b979663d918a4d8a32ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:38:49 +0000 Subject: [PATCH 1/6] Initial plan From 350dd58a250003ee403d4bc0f257a0c543d5012d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:51:38 +0000 Subject: [PATCH 2/6] Show helpful loading message when VIP link is unavailable Co-authored-by: Yashb404 <139128977+Yashb404@users.noreply.github.com> --- client/src/components/modal.rs | 82 +++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/client/src/components/modal.rs b/client/src/components/modal.rs index 7e9d6d9..ccd5f57 100644 --- a/client/src/components/modal.rs +++ b/client/src/components/modal.rs @@ -134,36 +134,58 @@ pub fn EmbedModal( view! { "Placeholder" }.into_view() }} -
- - -
-

- "Share privately. This bypasses the Guest List and should never be embedded publicly." -

+ {move || { + let link = vip_link.get(); + let vip_link_for_input = vip_link.clone(); + let vip_link_for_button = vip_link_for_click.clone(); + if link.is_empty() { + view! { +
+

+ "🔒 VIP link is being generated..." +

+

+ "Refresh this page to see your private VIP link once it's ready." +

+
+ }.into_view() + } else { + view! { + <> +
+ + +
+

+ "Share privately. This bypasses the Guest List and should never be embedded publicly." +

+ + }.into_view() + } + }} // --- SECTION 3: SMART LINK --- From 3e820dbc049fcf7c220c2d221b0e79d6b5c79b6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:53:36 +0000 Subject: [PATCH 3/6] Run cargo fmt to fix formatting Co-authored-by: Yashb404 <139128977+Yashb404@users.noreply.github.com> --- client/src/api.rs | 2 +- client/src/app.rs | 12 +- client/src/components/limit.rs | 10 +- client/src/components/modal.rs | 10 +- client/src/components/protected.rs | 45 ++++---- client/src/components/terminal.rs | 48 ++++---- client/src/lib.rs | 24 ++-- client/src/pages/admin.rs | 70 ++++++++---- client/src/pages/analytics.rs | 125 +++++++++++---------- client/src/pages/blogs.rs | 43 +++---- client/src/pages/create.rs | 170 ++++++++++++++++------------ client/src/pages/dashboard.rs | 142 +++++++++++------------ client/src/pages/docs.rs | 63 ++++++----- client/src/pages/embed.rs | 38 ++++--- client/src/pages/home.rs | 91 +++++++-------- client/src/pages/policy.rs | 53 ++++----- client/src/pages/view.rs | 173 +++++++++++++++++------------ client/src/types.rs | 4 +- 18 files changed, 622 insertions(+), 501 deletions(-) diff --git a/client/src/api.rs b/client/src/api.rs index 4ca6589..5c23c84 100644 --- a/client/src/api.rs +++ b/client/src/api.rs @@ -1,4 +1,4 @@ -// CONFIGURATION HELPERS +// CONFIGURATION HELPERS pub fn api_base() -> &'static str { option_env!("API_URL").unwrap_or("http://localhost:3000") } diff --git a/client/src/app.rs b/client/src/app.rs index 776be65..b0cf2d2 100644 --- a/client/src/app.rs +++ b/client/src/app.rs @@ -1,7 +1,11 @@ +use crate::components::protected::ProtectedRoute; +use crate::pages::{ + admin::AdminPage, analytics::AnalyticsPage, blogs::BlogsPage, create::CreatePage, + dashboard::DashboardPage, docs::DocsPage, embed::EmbedPage, home::LandingPage, + policy::PolicyPage, view::ViewPage, +}; use leptos::*; use leptos_router::*; -use crate::components::protected::ProtectedRoute; -use crate::pages::{home::LandingPage, dashboard::DashboardPage, create::CreatePage, view::ViewPage, embed::EmbedPage, docs::DocsPage, blogs::BlogsPage, analytics::AnalyticsPage, admin::AdminPage, policy::PolicyPage}; #[component] pub fn App() -> impl IntoView { @@ -23,8 +27,8 @@ pub fn App() -> impl IntoView { } /> - - + + diff --git a/client/src/components/limit.rs b/client/src/components/limit.rs index 335c914..33462eb 100644 --- a/client/src/components/limit.rs +++ b/client/src/components/limit.rs @@ -13,18 +13,18 @@ pub fn LimitReached() -> impl IntoView {
"Please try again later or contact the owner."

- +

"Are you the owner?"

- "Request More Compute" - @@ -34,4 +34,4 @@ pub fn LimitReached() -> impl IntoView {
} -} \ No newline at end of file +} diff --git a/client/src/components/modal.rs b/client/src/components/modal.rs index ccd5f57..d0718da 100644 --- a/client/src/components/modal.rs +++ b/client/src/components/modal.rs @@ -52,7 +52,7 @@ pub fn EmbedModal( let (copied_link, set_copied_link) = create_signal(false); let (copied_vip, set_copied_vip) = create_signal(false); let (new_url, set_new_url) = create_signal(String::new()); - + let iframe_ref = create_node_ref::(); let link_ref = create_node_ref::(); let vip_ref = create_node_ref::(); @@ -81,7 +81,7 @@ pub fn EmbedModal( - + // --- SECTION 1: IFRAME ---
@@ -155,7 +155,7 @@ pub fn EmbedModal(
impl IntoView { let (user, set_user) = create_signal(None::); let (checked, set_checked) = create_signal(false); - create_resource(|| (), move |_| async move { - // FIX: Use dynamic API URL - let url = format!("{}/api/me", api_base()); - let auth_req = Request::get(&url) - .credentials(RequestCredentials::Include) - .send() - .await; + create_resource( + || (), + move |_| async move { + // FIX: Use dynamic API URL + let url = format!("{}/api/me", api_base()); + let auth_req = Request::get(&url) + .credentials(RequestCredentials::Include) + .send() + .await; - match auth_req { - Ok(resp) => { - if resp.ok() { - if let Ok(u) = resp.json::().await { - set_user.set(Some(u)); + match auth_req { + Ok(resp) => { + if resp.ok() { + if let Ok(u) = resp.json::().await { + set_user.set(Some(u)); + } } } + Err(_) => {} } - Err(_) => {} - } - set_checked.set(true); - }); + set_checked.set(true); + }, + ); let children_view = children(); @@ -49,13 +52,13 @@ pub fn ProtectedRoute(children: Children) -> impl IntoView { }.into_view() } diff --git a/client/src/components/terminal.rs b/client/src/components/terminal.rs index 3cf3ae2..935dd94 100644 --- a/client/src/components/terminal.rs +++ b/client/src/components/terminal.rs @@ -1,10 +1,10 @@ +use crate::api::ws_base; use leptos::*; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -use web_sys::{WebSocket, MessageEvent, ErrorEvent}; -use crate::api::ws_base; +use web_sys::{ErrorEvent, MessageEvent, WebSocket}; -// BINDING 1: FitAddon +// BINDING 1: FitAddon #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_name = FitAddon)] @@ -15,7 +15,7 @@ extern "C" { fn fit(this: &XtermFitAddon); } -// BINDING 2: Terminal +// BINDING 2: Terminal #[wasm_bindgen] extern "C" { type Terminal; @@ -39,12 +39,12 @@ pub fn TerminalView(container_id: String) -> impl IntoView { create_effect(move |_| { if let Some(div) = terminal_div_ref.get() { let term = Terminal::new(); - + let fit_addon = XtermFitAddon::new(); term.load_addon(&fit_addon); term.open(&div); - - fit_addon.fit(); + + fit_addon.fit(); let fit_addon_clone = fit_addon.clone().unchecked_into::(); let on_resize = Closure::::new(move || { fit_addon_clone.fit(); @@ -55,49 +55,53 @@ pub fn TerminalView(container_id: String) -> impl IntoView { window().set_onresize(None); }); term.write(&format!("Connecting to session {}...\r\n", id_for_effect)); - + let term_clone: Terminal = term.clone().unchecked_into(); let ws_url = format!("{}/ws/{}", ws_base(), id_for_effect); - + // FIX: Removed unwrap() on WebSocket::new match WebSocket::new(&ws_url) { Ok(ws) => { let ws_cleanup = ws.clone(); - + on_cleanup(move || { let _ = ws_cleanup.close(); }); - let onmessage = Closure::::new(move |e: MessageEvent| { - if let Ok(txt) = e.data().dyn_into::() { - term_clone.write(&String::from(txt)); - } - }); + let onmessage = + Closure::::new(move |e: MessageEvent| { + if let Ok(txt) = e.data().dyn_into::() { + term_clone.write(&String::from(txt)); + } + }); ws.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); onmessage.forget(); let ws_clone = ws.clone(); - let on_data_callback = Closure::::new(move |data: String| { - let _ = ws_clone.send_with_str(&data); - }); + let on_data_callback = + Closure::::new(move |data: String| { + let _ = ws_clone.send_with_str(&data); + }); term.on_data(&on_data_callback); on_data_callback.forget(); let term_err = term.clone().unchecked_into::(); let onerror = Closure::::new(move |_| { - term_err.write("\r\n\x1b[31m[!] Connection Error.\x1b[0m\r\n"); + term_err.write("\r\n\x1b[31m[!] Connection Error.\x1b[0m\r\n"); }); ws.set_onerror(Some(onerror.as_ref().unchecked_ref())); onerror.forget(); let term_close = term.clone().unchecked_into::(); let onclose = Closure::::new(move || { - term_close.write("\r\n\x1b[33m[!] Connection Closed.\x1b[0m\r\n"); + term_close.write("\r\n\x1b[33m[!] Connection Closed.\x1b[0m\r\n"); }); ws.set_onclose(Some(onclose.as_ref().unchecked_ref())); onclose.forget(); - }, + } Err(_) => { - term.write("\r\n\x1b[31m[!] Failed to initialize WebSocket connection.\x1b[0m\r\n"); + term.write( + "\r\n\x1b[31m[!] Failed to initialize WebSocket connection.\x1b[0m\r\n", + ); } } } diff --git a/client/src/lib.rs b/client/src/lib.rs index bf33e06..4600c25 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,26 +1,26 @@ use leptos::*; -pub mod types; pub mod api; pub mod app; +pub mod types; pub mod components { - pub mod terminal; - pub mod protected; pub mod limit; - pub mod navbar; pub mod modal; + pub mod navbar; + pub mod protected; + pub mod terminal; } pub mod pages { - pub mod home; - pub mod dashboard; + pub mod admin; + pub mod analytics; + pub mod blogs; pub mod create; - pub mod view; - pub mod embed; + pub mod dashboard; pub mod docs; - pub mod blogs; - pub mod analytics; - pub mod admin; + pub mod embed; + pub mod home; pub mod policy; + pub mod view; } pub use app::App; @@ -29,4 +29,4 @@ pub use app::App; pub fn main() { console_error_panic_hook::set_once(); leptos::mount_to_body(|| view! { }) -} \ No newline at end of file +} diff --git a/client/src/pages/admin.rs b/client/src/pages/admin.rs index cf3e8b2..761bbc2 100644 --- a/client/src/pages/admin.rs +++ b/client/src/pages/admin.rs @@ -1,11 +1,11 @@ -use leptos::*; -use leptos_router::use_navigate; -use gloo_net::http::Request; -use web_sys::RequestCredentials; -use serde::{Deserialize, Serialize}; use crate::api::api_base; use crate::components::navbar::Navbar; use crate::types::ProjectSummary; +use gloo_net::http::Request; +use leptos::*; +use leptos_router::use_navigate; +use serde::{Deserialize, Serialize}; +use web_sys::RequestCredentials; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ContainerInfo { @@ -35,11 +35,14 @@ pub fn AdminPage() -> impl IntoView { // Actions are Copy, so we can use 'refresh_action' everywhere without cloning let refresh_action = create_action(move |_: &()| { // FIX: Clone navigate here so the async block owns this copy - let navigate = navigate.clone(); - + let navigate = navigate.clone(); + async move { let url = format!("{}/api/admin/stats", api_base()); - let req = Request::get(&url).credentials(RequestCredentials::Include).send().await; + let req = Request::get(&url) + .credentials(RequestCredentials::Include) + .send() + .await; match req { Ok(resp) => { @@ -54,12 +57,16 @@ pub fn AdminPage() -> impl IntoView { set_stats.set(Some(data)); } } - }, + } Err(_) => {} } - + let p_url = format!("{}/api/admin/projects", api_base()); - if let Ok(resp) = Request::get(&p_url).credentials(RequestCredentials::Include).send().await { + if let Ok(resp) = Request::get(&p_url) + .credentials(RequestCredentials::Include) + .send() + .await + { if let Ok(data) = resp.json::>().await { set_projects.set(data); } @@ -76,9 +83,17 @@ pub fn AdminPage() -> impl IntoView { let kill_container = create_action(move |id: &String| { let id = id.clone(); async move { - if !window().confirm_with_message(&format!("Kill container {}?", id)).unwrap_or(false) { return; } + if !window() + .confirm_with_message(&format!("Kill container {}?", id)) + .unwrap_or(false) + { + return; + } let url = format!("{}/api/admin/container/{}", api_base(), id); - let _ = Request::delete(&url).credentials(RequestCredentials::Include).send().await; + let _ = Request::delete(&url) + .credentials(RequestCredentials::Include) + .send() + .await; refresh_action.dispatch(()); // Trigger refresh } }); @@ -87,9 +102,20 @@ pub fn AdminPage() -> impl IntoView { let delete_project = create_action(move |slug: &String| { let slug = slug.clone(); async move { - if !window().confirm_with_message(&format!("DELETE project '{}'?\nThis deletes the DB entry AND Docker image.", slug)).unwrap_or(false) { return; } + if !window() + .confirm_with_message(&format!( + "DELETE project '{}'?\nThis deletes the DB entry AND Docker image.", + slug + )) + .unwrap_or(false) + { + return; + } let url = format!("{}/api/admin/project/{}", api_base(), slug); - let _ = Request::delete(&url).credentials(RequestCredentials::Include).send().await; + let _ = Request::delete(&url) + .credentials(RequestCredentials::Include) + .send() + .await; refresh_action.dispatch(()); // Trigger refresh } }); @@ -107,11 +133,11 @@ pub fn AdminPage() -> impl IntoView {
- "ADMIN MODE" -
} -} \ No newline at end of file +} diff --git a/client/src/pages/blogs.rs b/client/src/pages/blogs.rs index 9e05a82..2dcedab 100644 --- a/client/src/pages/blogs.rs +++ b/client/src/pages/blogs.rs @@ -1,10 +1,10 @@ +use crate::api::api_base; +use crate::components::navbar::Navbar; +use crate::types::User; +use gloo_net::http::Request; use leptos::*; use leptos_router::A; -use gloo_net::http::Request; use web_sys::RequestCredentials; -use crate::components::navbar::Navbar; -use crate::api::api_base; -use crate::types::User; #[component] pub fn BlogsPage() -> impl IntoView { @@ -12,21 +12,24 @@ pub fn BlogsPage() -> impl IntoView { let (user, set_user) = create_signal(None::); let (auth_checked, set_auth_checked) = create_signal(false); - create_resource(|| (), move |_| async move { - let url = format!("{}/api/me", api_base()); - if let Ok(resp) = Request::get(&url) - .credentials(RequestCredentials::Include) - .send() - .await - { - if resp.ok() { - if let Ok(u) = resp.json::().await { - set_user.set(Some(u)); + create_resource( + || (), + move |_| async move { + let url = format!("{}/api/me", api_base()); + if let Ok(resp) = Request::get(&url) + .credentials(RequestCredentials::Include) + .send() + .await + { + if resp.ok() { + if let Ok(u) = resp.json::().await { + set_user.set(Some(u)); + } } } - } - set_auth_checked.set(true); - }); + set_auth_checked.set(true); + }, + ); view! {
@@ -37,8 +40,8 @@ pub fn BlogsPage() -> impl IntoView { if auth_checked.get() { if let Some(u) = user.get() { view! { - User Avatar }.into_view() } else { @@ -92,4 +95,4 @@ pub fn BlogsPage() -> impl IntoView {
} -} \ No newline at end of file +} diff --git a/client/src/pages/create.rs b/client/src/pages/create.rs index 813c9c6..e1db08a 100644 --- a/client/src/pages/create.rs +++ b/client/src/pages/create.rs @@ -1,16 +1,16 @@ +use crate::api::api_base; +use crate::components::modal::Modal; +use crate::components::navbar::Navbar; +use crate::components::terminal::TerminalView; +use crate::types::User; +use gloo_net::http::Request; use leptos::*; use leptos_router::*; -use gloo_net::http::Request; -use web_sys::RequestCredentials; -use wasm_bindgen::JsValue; -use wasm_bindgen::prelude::*; -use std::rc::Rc; use std::cell::RefCell; -use crate::api::api_base; -use crate::types::User; -use crate::components::terminal::TerminalView; -use crate::components::navbar::Navbar; -use crate::components::modal::Modal; +use std::rc::Rc; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; +use web_sys::RequestCredentials; // Simple resize divider setup fn setup_resize_divider() { @@ -20,21 +20,21 @@ fn setup_resize_divider() { .and_then(|e| e.dyn_into::().ok()) { let is_dragging = Rc::new(RefCell::new(false)); - + let on_mousedown = { let is_dragging = is_dragging.clone(); wasm_bindgen::closure::Closure::wrap(Box::new(move |_: web_sys::MouseEvent| { *is_dragging.borrow_mut() = true; }) as Box) }; - + let on_mousemove = { let is_dragging = is_dragging.clone(); wasm_bindgen::closure::Closure::wrap(Box::new(move |e: web_sys::MouseEvent| { if !*is_dragging.borrow() { return; } - + if let Some(workspace) = web_sys::window() .and_then(|w| w.document()) .and_then(|d| d.query_selector(".workspace").ok().flatten()) @@ -44,36 +44,57 @@ fn setup_resize_divider() { let workspace_left = workspace.offset_left() as f64; let relative_x = e.client_x() as f64 - workspace_left; let percentage = (relative_x / workspace_width * 100.0).max(20.0).min(80.0); - + if let Ok(panes) = workspace.query_selector_all(".pane") { if panes.length() >= 2 { - if let Some(first_pane) = panes.get(0).and_then(|e| e.dyn_into::().ok()) { + if let Some(first_pane) = panes + .get(0) + .and_then(|e| e.dyn_into::().ok()) + { first_pane.style().set_property("flex", "0 1 auto").ok(); - first_pane.style().set_property("width", &format!("{}%", percentage)).ok(); + first_pane + .style() + .set_property("width", &format!("{}%", percentage)) + .ok(); } - if let Some(second_pane) = panes.get(1).and_then(|e| e.dyn_into::().ok()) { + if let Some(second_pane) = panes + .get(1) + .and_then(|e| e.dyn_into::().ok()) + { second_pane.style().set_property("flex", "0 1 auto").ok(); - second_pane.style().set_property("width", &format!("{}%", 100.0 - percentage)).ok(); + second_pane + .style() + .set_property("width", &format!("{}%", 100.0 - percentage)) + .ok(); } } } } }) as Box) }; - + let on_mouseup = { let is_dragging = is_dragging.clone(); wasm_bindgen::closure::Closure::wrap(Box::new(move |_: web_sys::MouseEvent| { *is_dragging.borrow_mut() = false; }) as Box) }; - - divider.add_event_listener_with_callback("mousedown", on_mousedown.as_ref().unchecked_ref()).ok(); + + divider + .add_event_listener_with_callback("mousedown", on_mousedown.as_ref().unchecked_ref()) + .ok(); on_mousedown.forget(); - + if let Some(document) = web_sys::window().and_then(|w| w.document()) { - document.add_event_listener_with_callback("mousemove", on_mousemove.as_ref().unchecked_ref()).ok(); - document.add_event_listener_with_callback("mouseup", on_mouseup.as_ref().unchecked_ref()).ok(); + document + .add_event_listener_with_callback( + "mousemove", + on_mousemove.as_ref().unchecked_ref(), + ) + .ok(); + document + .add_event_listener_with_callback("mouseup", on_mouseup.as_ref().unchecked_ref()) + .ok(); on_mousemove.forget(); on_mouseup.forget(); } @@ -83,11 +104,8 @@ fn setup_resize_divider() { #[component] pub fn CreatePage() -> impl IntoView { let query_params = use_query_map(); - let pre_filled_name = move || { - query_params.with(|params| { - params.get("name").cloned().unwrap_or_default() - }) - }; + let pre_filled_name = + move || query_params.with(|params| params.get("name").cloned().unwrap_or_default()); let (container_id, set_container_id) = create_signal("".to_string()); let (markdown, set_markdown) = create_signal(r#"# Welcome to Your TryCLI Environment @@ -138,47 +156,52 @@ After publishing, you can easily distribute your interactive terminal: let (modal_body, set_modal_body) = create_signal(String::new()); let (modal_success, set_modal_success) = create_signal(false); - create_resource(|| (), move |_| async move { - let url = format!("{}/api/me", api_base()); - let auth_req = Request::get(&url) - .credentials(RequestCredentials::Include) - .send() - .await; - - match auth_req { - Ok(resp) => { - if resp.ok() { - if let Ok(u) = resp.json::().await { - set_user.set(Some(u)); - - let spawn_url = format!("{}/api/spawn", api_base()); - let spawn_req = Request::post(&spawn_url) - .credentials(RequestCredentials::Include) - .send() - .await; - - match spawn_req { - Ok(spawn_resp) => { - if spawn_resp.ok() { - if let Ok(id) = spawn_resp.json::().await { - set_container_id.set(id); + create_resource( + || (), + move |_| async move { + let url = format!("{}/api/me", api_base()); + let auth_req = Request::get(&url) + .credentials(RequestCredentials::Include) + .send() + .await; + + match auth_req { + Ok(resp) => { + if resp.ok() { + if let Ok(u) = resp.json::().await { + set_user.set(Some(u)); + + let spawn_url = format!("{}/api/spawn", api_base()); + let spawn_req = Request::post(&spawn_url) + .credentials(RequestCredentials::Include) + .send() + .await; + + match spawn_req { + Ok(spawn_resp) => { + if spawn_resp.ok() { + if let Ok(id) = spawn_resp.json::().await { + set_container_id.set(id); + } + } else { + let status = spawn_resp.status(); + let text = spawn_resp.text().await.unwrap_or_default(); + set_container_id.set(format!("ERROR {}: {}", status, text)); } - } else { - let status = spawn_resp.status(); - let text = spawn_resp.text().await.unwrap_or_default(); - set_container_id.set(format!("ERROR {}: {}", status, text)); } - } - Err(e) => { - set_container_id.set(format!("NETWORK_FAIL: {}", e)); + Err(e) => { + set_container_id.set(format!("NETWORK_FAIL: {}", e)); + } } } } } + Err(e) => { + web_sys::console::log_1(&JsValue::from_str(&format!("Auth Error: {}", e))) + } } - Err(e) => web_sys::console::log_1(&JsValue::from_str(&format!("Auth Error: {}", e))), - } - }); + }, + ); let navigate_modal = use_navigate(); let on_publish = Rc::new(move |_: ev::MouseEvent| { // Prevent concurrent publish requests @@ -219,14 +242,15 @@ After publishing, you can easily distribute your interactive terminal: if resp.ok() { publish_success = true; set_modal_title.set("Published".to_string()); - set_modal_body.set("Your project has been published successfully.".to_string()); + set_modal_body + .set("Your project has been published successfully.".to_string()); } else { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); set_modal_title.set("Publish failed".to_string()); set_modal_body.set(format!("{}: {}", status, text)); } - }, + } Err(_) => { set_modal_title.set("Publish failed".to_string()); set_modal_body.set("Network error while publishing.".to_string()); @@ -266,20 +290,20 @@ After publishing, you can easily distribute your interactive terminal: match user.get() { Some(u) => view! {
- {u.login.clone()}
- "Logout" "Project Slug:" - {err} })} -
} -} \ No newline at end of file +} diff --git a/client/src/pages/embed.rs b/client/src/pages/embed.rs index f83d2a4..b25201f 100644 --- a/client/src/pages/embed.rs +++ b/client/src/pages/embed.rs @@ -1,10 +1,10 @@ -use leptos::*; -use leptos_router::*; -use gloo_net::http::Request; use crate::api::api_base; -use crate::components::terminal::TerminalView; use crate::components::limit::LimitReached; -use serde::{Serialize, Deserialize}; +use crate::components::terminal::TerminalView; +use gloo_net::http::Request; +use leptos::*; +use leptos_router::*; +use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] enum ProjectState { @@ -24,13 +24,15 @@ pub fn EmbedPage() -> impl IntoView { // Resource now returns ProjectState let project_data = create_resource( - move || (started.get(), username(), slug()), + move || (started.get(), username(), slug()), |(is_started, u, s)| async move { - if !is_started { return ProjectState::Loading; } - + if !is_started { + return ProjectState::Loading; + } + let url = format!("{}/api/project/{}/{}", api_base(), u, s); let req = Request::get(&url).send().await; - + match req { Ok(resp) => { if resp.status() == 403 { @@ -46,21 +48,21 @@ pub fn EmbedPage() -> impl IntoView { } else { ProjectState::NotFound } - }, - Err(_) => ProjectState::NotFound + } + Err(_) => ProjectState::NotFound, } - } + }, ); view! {
{move || if !started.get() { view! { -

"TryCli Studio Demo"

-
}.into_view() } else { view! {}.into_view() @@ -107,4 +109,4 @@ pub fn EmbedPage() -> impl IntoView { }}
} -} \ No newline at end of file +} diff --git a/client/src/pages/home.rs b/client/src/pages/home.rs index fee7da9..7141748 100644 --- a/client/src/pages/home.rs +++ b/client/src/pages/home.rs @@ -1,32 +1,35 @@ +use crate::api::api_base; +use crate::components::navbar::Navbar; +use crate::types::User; +use gloo_net::http::Request; use leptos::*; +use leptos_meta::{Link, Meta, Script, Title}; use leptos_router::A; -use leptos_meta::{Title, Meta, Link, Script}; -use gloo_net::http::Request; use web_sys::RequestCredentials; -use crate::components::navbar::Navbar; -use crate::api::api_base; -use crate::types::User; #[component] pub fn LandingPage() -> impl IntoView { let (user, set_user) = create_signal(None::); let (auth_checked, set_auth_checked) = create_signal(false); - create_resource(|| (), move |_| async move { - let url = format!("{}/api/me", api_base()); - if let Ok(resp) = Request::get(&url) - .credentials(RequestCredentials::Include) - .send() - .await - { - if resp.ok() { - if let Ok(u) = resp.json::().await { - set_user.set(Some(u)); + create_resource( + || (), + move |_| async move { + let url = format!("{}/api/me", api_base()); + if let Ok(resp) = Request::get(&url) + .credentials(RequestCredentials::Include) + .send() + .await + { + if resp.ok() { + if let Ok(u) = resp.json::().await { + set_user.set(Some(u)); + } } } - } - set_auth_checked.set(true); - }); + set_auth_checked.set(true); + }, + ); let auth_github_url = move || format!("{}/auth/github", api_base()); @@ -50,45 +53,45 @@ pub fn LandingPage() -> impl IntoView { // 4. SEO METADATA <Meta name="description" content="Turn your static documentation into interactive live demos. Zero-config Docker sandboxes for onboarding users to your CLI tools instantly." /> - + <Link rel="canonical" href="https://trycli.com" /> <Script type_="application/ld+json"> {schema_json} </Script> - + <Meta property="og:type" content="website" /> <Meta property="og:title" content="TryCLI - The Standard for Interactive Documentation" /> <Meta property="og:description" content="Instantly spin up isolated Docker containers and share your CLI projects with a single link." /> <Meta property="og:url" content="https://trycli.com" /> - + <Meta name="twitter:card" content="summary_large_image" /> <Meta name="twitter:title" content="TryCLI Studio" /> <Meta name="twitter:description" content="Host, share, and embed fully interactive CLI demos directly in the browser." /> - + // MAIN CONTENT <div class="landing-container"> - + <Navbar> <div class="nav-actions"> {move || { let (menu_open, set_menu_open) = create_signal(false); - + if auth_checked.get() { if let Some(u) = user.get() { // LOGGED IN: Show Profile + Dashboard Button + Hamburger Menu view! { <div style="display: flex; align-items: center; gap: 20px; width: 100%;"> <div style="display: flex; align-items: center; gap: 12px;"> - <img src=u.avatar_url - style="width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--border);" + <img src=u.avatar_url + style="width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--border);" alt="User Avatar" /> <span style="color: var(--text-main); font-weight: 500; font-size: 0.95rem;"> {u.login} </span> </div> <A href="/dashboard" class="btn-secondary btn-action btn-dashboard">"Dashboard"</A> - + // Hamburger Menu Button <button class="hamburger-menu" @@ -117,7 +120,7 @@ pub fn LandingPage() -> impl IntoView { // LOGGED OUT let url = auth_github_url(); let (menu_open, set_menu_open) = create_signal(false); - + view! { <div style="display: flex; align-items: center; gap: 20px; width: 100%;"> <a href=url class="btn-secondary btn-action btn-login" rel="external" style="display: flex; align-items: center; gap: 8px;"> @@ -126,7 +129,7 @@ pub fn LandingPage() -> impl IntoView { </svg> "Login" </a> - + <button class="hamburger-menu" class:open=move || menu_open.get() @@ -160,12 +163,12 @@ pub fn LandingPage() -> impl IntoView { <main class="hero-main"> <div class="hero-content"> <div class="badge">"Now supporting Alpine, Debian & Fish Shell"</div> - + <h1 class="hero-title"> "Interactive CLI Demos"<br /> <span class="text-gradient">"for the Modern Web"</span> </h1> - + <p class="hero-subtitle"> "The modern way to showcase CLI tools. Spin up instant, sandboxed Linux environments directly in your browser. No downloads, no configuration, just code." </p> @@ -181,13 +184,13 @@ pub fn LandingPage() -> impl IntoView { </A> }.into_view() } else { - view! { + view! { <a href=url class="btn-primary btn-hero" rel="external" style="display: flex; align-items: center; gap: 10px;"> <svg height="24" width="24" viewBox="0 0 16 16" fill="currentColor" style="color: black;"> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path> </svg> "Start Building Free" - </a> + </a> }.into_view() } }} @@ -196,7 +199,7 @@ pub fn LandingPage() -> impl IntoView { </A> </div> - // TERMINAL PREVIEW + // TERMINAL PREVIEW <div class="terminal-preview" role="log" aria-label="Terminal Preview Demo"> <div class="terminal-header-preview" aria-hidden="true"> <div class="dot red"></div> @@ -206,14 +209,14 @@ pub fn LandingPage() -> impl IntoView { </div> <div class="terminal-body-preview"> <div class="line"> - <span class="prompt">"➜"</span> + <span class="prompt">"➜"</span> <span class="cmd">" curl -fsSL https://trycli.com/install.sh | sh"</span> </div> <div class="line output"><span>"→ Initializing environment (Ubuntu 22.04)..."</span></div> <div class="line output"><span>"→ Installing dependencies..."</span></div> <div class="line output"><span class="success">"✔ Environment Ready! Session ID: 9f8a-2b1c"</span></div> <div class="line"> - <span class="prompt">"➜"</span> + <span class="prompt">"➜"</span> <span class="cmd">" trycli publish --public"</span> </div> <div class="line output"><span>"Snapshotting container state... Done (1.2s)"</span></div> @@ -223,18 +226,18 @@ pub fn LandingPage() -> impl IntoView { </div> </main> - // FEATURES + // FEATURES <section class="section-features" style="background: rgba(255,255,255,0.01);"> <div class="container-narrow"> <h2 class="section-title">"Frictionless Onboarding"</h2> - + <p class="section-subtitle" style="text-align: left; margin-bottom: 3rem;"> "The biggest drop-off in developer adoption happens before the first command is ever run. " "TryCLI bridges the gap between reading about a tool and actually experiencing it." </p> <div style="display: flex; flex-wrap: wrap; gap: 40px; align-items: center;"> - + // Left Column: Text explanation <div style="flex: 1 1 400px; text-align: left;"> <h3 style="font-size: 1.5rem; margin-bottom: 1rem; color: #fff; font-weight: 700;">"Stop Losing Users at 'npm install'"</h3> @@ -313,7 +316,7 @@ pub fn LandingPage() -> impl IntoView { </section> - // FINAL CTA + // FINAL CTA <section class="section-usage" style="border-bottom: none;"> <div class="container-narrow"> <div class="final-cta"> @@ -329,13 +332,13 @@ pub fn LandingPage() -> impl IntoView { if auth_checked.get() && user.get().is_some() { view! { <A href="/new" class="btn-secondary btn-lg">"Create New Project"</A> }.into_view() } else { - view! { + view! { <a href=url class="btn-primary btn-hero btn-lg" rel="external" style="display: flex; align-items: center; gap: 10px;"> <svg height="24" width="24" viewBox="0 0 16 16" fill="currentColor" style="color: black;"> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path> </svg> "Sign Up with GitHub" - </a> + </a> }.into_view() } }} @@ -344,7 +347,7 @@ pub fn LandingPage() -> impl IntoView { </div> </section> - // FOOTER + // FOOTER <footer class="landing-footer"> <div class="footer-container"> <div class="footer-top"> @@ -369,4 +372,4 @@ pub fn LandingPage() -> impl IntoView { </div> </> } -} \ No newline at end of file +} diff --git a/client/src/pages/policy.rs b/client/src/pages/policy.rs index af1e1d8..39a3168 100644 --- a/client/src/pages/policy.rs +++ b/client/src/pages/policy.rs @@ -1,10 +1,10 @@ +use crate::api::api_base; +use crate::components::navbar::Navbar; +use crate::types::User; +use gloo_net::http::Request; use leptos::*; use leptos_router::A; -use gloo_net::http::Request; use web_sys::RequestCredentials; -use crate::components::navbar::Navbar; -use crate::api::api_base; -use crate::types::User; #[component] pub fn PolicyPage() -> impl IntoView { @@ -13,21 +13,24 @@ pub fn PolicyPage() -> impl IntoView { let (user, set_user) = create_signal(None::<User>); let (auth_checked, set_auth_checked) = create_signal(false); - create_resource(|| (), move |_| async move { - let url = format!("{}/api/me", api_base()); - if let Ok(resp) = Request::get(&url) - .credentials(RequestCredentials::Include) - .send() - .await - { - if resp.ok() { - if let Ok(u) = resp.json::<User>().await { - set_user.set(Some(u)); + create_resource( + || (), + move |_| async move { + let url = format!("{}/api/me", api_base()); + if let Ok(resp) = Request::get(&url) + .credentials(RequestCredentials::Include) + .send() + .await + { + if resp.ok() { + if let Ok(u) = resp.json::<User>().await { + set_user.set(Some(u)); + } } } - } - set_auth_checked.set(true); - }); + set_auth_checked.set(true); + }, + ); view! { <div class="docs-container"> @@ -37,8 +40,8 @@ pub fn PolicyPage() -> impl IntoView { if auth_checked.get() { if let Some(u) = user.get() { view! { - <img src=u.avatar_url - style="width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--border);" + <img src=u.avatar_url + style="width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--border);" alt="User Avatar" /> }.into_view() } else { @@ -120,7 +123,7 @@ pub fn PolicyPage() -> impl IntoView { <main class="docs-content"> <h1>"TryCLI Terms of Service"</h1> <p class="text-sm text-gray-500">"Last Updated: February 9, 2026"</p> - + <section id="introduction"> <h2>"1. Introduction and Acceptance of Terms"</h2> <p>"These Terms of Service (\"Terms\") constitute a binding legal agreement between you (\"Publisher,\" \"User,\" or \"You\") and TryCLI (\"Platform,\" \"We,\" or \"Us\"). By accessing, registering for, or using the TryCLI platform, including our browser-based sandbox environment, live-syncing Markdown editor, and related developer tools (collectively, the \"Services\"), you acknowledge that you have read, understood, and agree to be bound by these Terms."</p> @@ -129,7 +132,7 @@ pub fn PolicyPage() -> impl IntoView { <section id="nature-of-service"> <h2>"2. Nature of the Service: Passive Conduit"</h2> - + <h3>"2.1 Platform Status"</h3> <p>"You acknowledge and agree that TryCLI operates solely as a technological intermediary and hosting platform. We provide the infrastructure (containerized environments) for the execution of code and the display of documentation. We do not create, select, or modify the content, code, or applications (\"User Content\") uploaded or executed by Publishers."</p> @@ -160,7 +163,7 @@ pub fn PolicyPage() -> impl IntoView { <h2>"4. Proprietary Rights and License"</h2> <h3>"4.1 Your Content"</h3> <p>"You retain all ownership rights to the User Content you create, upload, or execute on TryCLI."</p> - + <h3>"4.2 License to Host"</h3> <p>"By submitting User Content to the Service, you grant TryCLI a worldwide, non-exclusive, royalty-free license to use, reproduce, modify, adapt, publish, and display such content solely for the purpose of providing the Services (e.g., running your code in a container, displaying your Markdown tutorial)."</p> </section> @@ -178,10 +181,10 @@ pub fn PolicyPage() -> impl IntoView { <section id="disclaimer"> <h2>"6. Disclaimer of Warranties"</h2> <p><strong>"THE SERVICES ARE PROVIDED ON AN \"AS IS\" AND \"AS AVAILABLE\" BASIS, WITHOUT ANY WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED."</strong></p> - + <h3>"6.1 No Warranty of Functionality"</h3> <p>"TryCLI does not warrant that the Services will be uninterrupted, secure, or error-free, or that any defects will be corrected. We do not guarantee that code which runs in our sandbox environment will function correctly in other environments or on local machines (\"Works on My Machine\" disclaimer)."</p> - + <h3>"6.2 Data Persistence"</h3> <p>"TryCLI is a sandbox environment designed for development and testing. We do not guarantee the permanent persistence of data, container states, or file systems. You are responsible for backing up your own data."</p> </section> @@ -204,4 +207,4 @@ pub fn PolicyPage() -> impl IntoView { </div> </div> } -} \ No newline at end of file +} diff --git a/client/src/pages/view.rs b/client/src/pages/view.rs index ec51a95..08e69e6 100644 --- a/client/src/pages/view.rs +++ b/client/src/pages/view.rs @@ -1,18 +1,18 @@ -use leptos::*; -use leptos_router::*; -use gloo_net::http::Request; -use web_sys::RequestCredentials; -use wasm_bindgen::prelude::*; -use std::rc::Rc; -use std::cell::RefCell; -use pulldown_cmark::{Parser, Options, html}; use crate::api::api_base; -use crate::components::terminal::TerminalView; use crate::components::limit::LimitReached; -use crate::components::navbar::Navbar; use crate::components::modal::EmbedModal; +use crate::components::navbar::Navbar; +use crate::components::terminal::TerminalView; use crate::types::User; -use serde::{Serialize, Deserialize}; +use gloo_net::http::Request; +use leptos::*; +use leptos_router::*; +use pulldown_cmark::{html, Options, Parser}; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::rc::Rc; +use wasm_bindgen::prelude::*; +use web_sys::RequestCredentials; #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] enum ProjectState { @@ -29,7 +29,7 @@ pub fn render_markdown(text: &str) -> String { options.insert(Options::ENABLE_TABLES); let parser = Parser::new_ext(text, options); let mut html_output = String::new(); - html::push_html(&mut html_output, parser); + html::push_html(&mut html_output, parser); html_output } @@ -40,18 +40,20 @@ fn setup_resize_divider() { .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok()) { let is_dragging = Rc::new(RefCell::new(false)); - + let on_mousedown = { let is_dragging = is_dragging.clone(); wasm_bindgen::closure::Closure::wrap(Box::new(move |_: web_sys::MouseEvent| { *is_dragging.borrow_mut() = true; }) as Box<dyn Fn(web_sys::MouseEvent)>) }; - + let on_mousemove = { let is_dragging = is_dragging.clone(); wasm_bindgen::closure::Closure::wrap(Box::new(move |e: web_sys::MouseEvent| { - if !*is_dragging.borrow() { return; } + if !*is_dragging.borrow() { + return; + } if let Some(workspace) = web_sys::window() .and_then(|w| w.document()) .and_then(|d| d.query_selector(".workspace").ok().flatten()) @@ -61,36 +63,55 @@ fn setup_resize_divider() { let workspace_left = workspace.offset_left() as f64; let relative_x = e.client_x() as f64 - workspace_left; let percentage = (relative_x / workspace_width * 100.0).max(20.0).min(80.0); - + if let Ok(panes) = workspace.query_selector_all(".pane") { if panes.length() >= 2 { - if let Some(p1) = panes.get(0).and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok()) { + if let Some(p1) = panes + .get(0) + .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok()) + { p1.style().set_property("flex", "0 1 auto").ok(); - p1.style().set_property("width", &format!("{}%", percentage)).ok(); + p1.style() + .set_property("width", &format!("{}%", percentage)) + .ok(); } - if let Some(p2) = panes.get(1).and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok()) { + if let Some(p2) = panes + .get(1) + .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok()) + { p2.style().set_property("flex", "0 1 auto").ok(); - p2.style().set_property("width", &format!("{}%", 100.0 - percentage)).ok(); + p2.style() + .set_property("width", &format!("{}%", 100.0 - percentage)) + .ok(); } } } } }) as Box<dyn Fn(web_sys::MouseEvent)>) }; - + let on_mouseup = { let is_dragging = is_dragging.clone(); wasm_bindgen::closure::Closure::wrap(Box::new(move |_: web_sys::MouseEvent| { *is_dragging.borrow_mut() = false; }) as Box<dyn Fn(web_sys::MouseEvent)>) }; - - divider.add_event_listener_with_callback("mousedown", on_mousedown.as_ref().unchecked_ref()).ok(); + + divider + .add_event_listener_with_callback("mousedown", on_mousedown.as_ref().unchecked_ref()) + .ok(); on_mousedown.forget(); - + if let Some(document) = web_sys::window().and_then(|w| w.document()) { - document.add_event_listener_with_callback("mousemove", on_mousemove.as_ref().unchecked_ref()).ok(); - document.add_event_listener_with_callback("mouseup", on_mouseup.as_ref().unchecked_ref()).ok(); + document + .add_event_listener_with_callback( + "mousemove", + on_mousemove.as_ref().unchecked_ref(), + ) + .ok(); + document + .add_event_listener_with_callback("mouseup", on_mouseup.as_ref().unchecked_ref()) + .ok(); on_mousemove.forget(); on_mouseup.forget(); } @@ -103,43 +124,54 @@ pub fn ViewPage() -> impl IntoView { let query_params = use_query_map(); let username = move || params.get().get("username").cloned().unwrap_or_default(); let slug = move || params.get().get("slug").cloned().unwrap_or_default(); - + let (user, set_user) = create_signal(None::<User>); let (embed_modal_open, set_embed_modal_open) = create_signal(false); let (iframe_code, set_iframe_code) = create_signal(String::new()); let (smart_link, set_smart_link) = create_signal(String::new()); let (vip_link, set_vip_link) = create_signal(String::new()); - + let (whitelist, set_whitelist) = create_signal(Vec::<String>::new()); // Auth Resource - let auth_resource = create_resource(|| (), move |_| async move { - let req = Request::get(&format!("{}/api/me", api_base())) - .credentials(RequestCredentials::Include) - .send().await; + let auth_resource = create_resource( + || (), + move |_| async move { + let req = Request::get(&format!("{}/api/me", api_base())) + .credentials(RequestCredentials::Include) + .send() + .await; - if let Ok(resp) = req { - if resp.ok() { - if let Ok(u) = resp.json::<User>().await { - set_user.set(Some(u)); - } + if let Ok(resp) = req { + if resp.ok() { + if let Ok(u) = resp.json::<User>().await { + set_user.set(Some(u)); + } + } } - } - }); + }, + ); // Project Data Resource (with VIP key and Referer security) let project_resource = create_resource( - move || (username(), slug(), auth_resource.get()), + move || (username(), slug(), auth_resource.get()), move |(u, s, _)| async move { - let key = query_params.get_untracked().get("key").cloned().unwrap_or_default(); + let key = query_params + .get_untracked() + .get("key") + .cloned() + .unwrap_or_default(); let url = if key.is_empty() { format!("{}/api/project/{}/{}", api_base(), u, s) } else { format!("{}/api/project/{}/{}?key={}", api_base(), u, s, key) }; - let req = Request::get(&url).credentials(RequestCredentials::Include).send().await; - + let req = Request::get(&url) + .credentials(RequestCredentials::Include) + .send() + .await; + match req { Ok(resp) => { if resp.status() == 403 { @@ -147,16 +179,17 @@ pub fn ViewPage() -> impl IntoView { } else if resp.status() == 429 { ProjectState::LimitReached } else if resp.ok() { - resp.json::<serde_json::Value>().await + resp.json::<serde_json::Value>() + .await .map(ProjectState::Ready) .unwrap_or(ProjectState::NotFound) } else { ProjectState::NotFound } - }, - Err(_) => ProjectState::NotFound + } + Err(_) => ProjectState::NotFound, } - } + }, ); // Whitelist Resource for Owners @@ -165,23 +198,27 @@ pub fn ViewPage() -> impl IntoView { move |(state, s)| async move { if let Some(ProjectState::Ready(_)) = state { let url = format!("{}/api/project/{}/whitelist", api_base(), s); - if let Ok(resp) = Request::get(&url).credentials(RequestCredentials::Include).send().await { + if let Ok(resp) = Request::get(&url) + .credentials(RequestCredentials::Include) + .send() + .await + { if let Ok(list) = resp.json::<Vec<String>>().await { set_whitelist.set(list); } } } - } + }, ); let is_owner = move || { let current_user = user.get(); if let Some(ProjectState::Ready(p)) = project_resource.get() { - let project_owner_id = p.get("owner_id").and_then(|id| id.as_i64()); - match current_user { - Some(u) => Some(u.id) == project_owner_id, - None => false - } + let project_owner_id = p.get("owner_id").and_then(|id| id.as_i64()); + match current_user { + Some(u) => Some(u.id) == project_owner_id, + None => false, + } } else { false } @@ -219,18 +256,18 @@ pub fn ViewPage() -> impl IntoView { view! { <> - <EmbedModal - show=embed_modal_open.into() - title="Share Project".to_string().into() - iframe_code=iframe_code.into() + <EmbedModal + show=embed_modal_open.into() + title="Share Project".to_string().into() + iframe_code=iframe_code.into() smart_link=smart_link.into() vip_link=vip_link.into() whitelist=whitelist.into() on_add_url=Callback::new(move |url: String| add_whitelist_item.dispatch(url)) on_remove_url=Callback::new(move |url: String| remove_whitelist_item.dispatch(url)) - on_close=Callback::new(move |_| set_embed_modal_open.set(false)) + on_close=Callback::new(move |_| set_embed_modal_open.set(false)) /> - + <Navbar> <div class="controls"> {move || if is_owner() { @@ -245,7 +282,7 @@ pub fn ViewPage() -> impl IntoView { if let Some(ProjectState::Ready(data)) = project_resource.get() { let token = data.get("embed_token").and_then(|v| v.as_str()).unwrap_or_default(); let key = data.get("embed_key").and_then(|v| v.as_str()).unwrap_or_default(); - + // Public embed uses whitelist + domain check only let public_url = format!("{}/embed/{}/{}", origin, username(), slug()); let smart_url = format!("{}/e/{}", api_base(), token); @@ -278,7 +315,7 @@ pub fn ViewPage() -> impl IntoView { let cid = data["container_id"].as_str().unwrap_or_default().to_string(); let md_raw = data["markdown"].as_str().unwrap_or_default().to_string(); let html_output = render_markdown(&md_raw); - + create_effect(move |_| { if let Some(window) = web_sys::window() { let callback = wasm_bindgen::closure::Closure::once(move || { @@ -288,7 +325,7 @@ pub fn ViewPage() -> impl IntoView { callback.forget(); } }); - + view! { <div style="display: flex; flex-direction: column; height: calc(100vh - 60px);"> <div class="workspace" style="flex: 1;"> @@ -320,16 +357,16 @@ pub fn ViewPage() -> impl IntoView { </div> }.into_view(), Some(ProjectState::LimitReached) => view! { <LimitReached /> }.into_view(), - Some(ProjectState::NotFound) => view! { - <div style="color: var(--text-muted); text-align: center; margin-top: 100px;">"Project not found."</div> + Some(ProjectState::NotFound) => view! { + <div style="color: var(--text-muted); text-align: center; margin-top: 100px;">"Project not found."</div> }.into_view(), - _ => view! { + _ => view! { <div style="padding: 50px; text-align: center;"> <div class="spinner" style="margin: 0 auto;"></div> <p style="margin-top: 1rem; color: var(--text-muted);">"PREPARING ENVIRONMENT..."</p> - </div> + </div> }.into_view() }} </> } -} \ No newline at end of file +} diff --git a/client/src/types.rs b/client/src/types.rs index 903d5c9..8c51301 100644 --- a/client/src/types.rs +++ b/client/src/types.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct ProjectSummary { pub slug: String, pub image_tag: String, @@ -54,4 +54,4 @@ pub struct User { pub id: i64, pub login: String, pub avatar_url: String, -} \ No newline at end of file +} From f14900333c93472b656ca1dec919c0cda4d7196c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 06:42:55 +0000 Subject: [PATCH 4/6] Fix error handling in dashboard and create pages - dashboard.rs: Handle JSON parse failure explicitly to prevent stuck loading state - create.rs: Separate spawn error state from container_id to avoid invalid WebSocket connections Co-authored-by: Yashb404 <139128977+Yashb404@users.noreply.github.com> --- client/src/pages/create.rs | 25 ++++++++++++--- client/src/pages/dashboard.rs | 58 +++++++++++++++++++---------------- 2 files changed, 52 insertions(+), 31 deletions(-) diff --git a/client/src/pages/create.rs b/client/src/pages/create.rs index e1db08a..a712008 100644 --- a/client/src/pages/create.rs +++ b/client/src/pages/create.rs @@ -108,6 +108,7 @@ pub fn CreatePage() -> impl IntoView { move || query_params.with(|params| params.get("name").cloned().unwrap_or_default()); let (container_id, set_container_id) = create_signal("".to_string()); + let (spawn_error, set_spawn_error) = create_signal(None::<String>); let (markdown, set_markdown) = create_signal(r#"# Welcome to Your TryCLI Environment This interactive workspace is your project's staging area. On the left is your live terminal, and here on the right is your editable documentation panel. @@ -182,15 +183,18 @@ After publishing, you can easily distribute your interactive terminal: if spawn_resp.ok() { if let Ok(id) = spawn_resp.json::<String>().await { set_container_id.set(id); + set_spawn_error.set(None); + } else { + set_spawn_error.set(Some("Failed to parse container ID".to_string())); } } else { let status = spawn_resp.status(); let text = spawn_resp.text().await.unwrap_or_default(); - set_container_id.set(format!("ERROR {}: {}", status, text)); + set_spawn_error.set(Some(format!("Spawn failed ({}): {}", status, text))); } } Err(e) => { - set_container_id.set(format!("NETWORK_FAIL: {}", e)); + set_spawn_error.set(Some(format!("Network error: {}", e))); } } } @@ -361,9 +365,20 @@ After publishing, you can easily distribute your interactive terminal: <span class="terminal-title">"bash — interactive"</span> </div> <div class="terminal-body"> - {move || match container_id.get().as_str() { - "" => view! { <div style="padding: 20px; color: #666;">"Initializing Environment..."</div> }.into_view(), - id => view! { <TerminalView container_id=id.to_string() /> }.into_view() + {move || { + if let Some(error) = spawn_error.get() { + view! { + <div style="padding: 20px; color: #ef4444;"> + <div style="margin-bottom: 10px; font-weight: bold;">"⚠️ Environment Spawn Error"</div> + <div style="color: #666;">{error}</div> + </div> + }.into_view() + } else { + match container_id.get().as_str() { + "" => view! { <div style="padding: 20px; color: #666;">"Initializing Environment..."</div> }.into_view(), + id => view! { <TerminalView container_id=id.to_string() /> }.into_view() + } + } }} </div> </div> diff --git a/client/src/pages/dashboard.rs b/client/src/pages/dashboard.rs index 6cfadf0..c489432 100644 --- a/client/src/pages/dashboard.rs +++ b/client/src/pages/dashboard.rs @@ -29,40 +29,46 @@ pub fn DashboardPage() -> impl IntoView { match auth_req { Ok(resp) => { if resp.ok() { - if let Ok(u) = resp.json::<User>().await { - // user authenticated - set_user.set(Some(u.clone())); + match resp.json::<User>().await { + Ok(u) => { + // user authenticated + set_user.set(Some(u.clone())); - let proj_url = format!("{}/api/my-projects", api_base()); - let projects_req = Request::get(&proj_url) - .credentials(RequestCredentials::Include) - .send() - .await; + let proj_url = format!("{}/api/my-projects", api_base()); + let projects_req = Request::get(&proj_url) + .credentials(RequestCredentials::Include) + .send() + .await; - match projects_req { - Ok(p_resp) => { - if p_resp.ok() { - if let Ok(projs) = - p_resp.json::<Vec<ProjectSummary>>().await - { - set_projects.set(projs); - set_error.set(None); + match projects_req { + Ok(p_resp) => { + if p_resp.ok() { + if let Ok(projs) = + p_resp.json::<Vec<ProjectSummary>>().await + { + set_projects.set(projs); + set_error.set(None); + } else { + set_error.set(Some( + "Failed to parse project list".to_string(), + )); + } } else { - set_error.set(Some( - "Failed to parse project list".to_string(), - )); + set_error + .set(Some("Failed to fetch deployments".to_string())); } - } else { + } + Err(_) => { set_error - .set(Some("Failed to fetch deployments".to_string())); + .set(Some("Network error connecting to API".to_string())); } } - Err(_) => { - set_error - .set(Some("Network error connecting to API".to_string())); - } + set_loading.set(false); + } + Err(_) => { + set_loading.set(false); + set_error.set(Some("Failed to parse user data".to_string())); } - set_loading.set(false); } } else { set_loading.set(false); From 705a0bfd0e2b4b8c1f4ee009b0fc93c1bb678897 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 06:43:10 +0000 Subject: [PATCH 5/6] Run cargo fmt Co-authored-by: Yashb404 <139128977+Yashb404@users.noreply.github.com> --- client/src/pages/create.rs | 9 +++++++-- client/src/pages/dashboard.rs | 10 ++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/client/src/pages/create.rs b/client/src/pages/create.rs index a712008..fc0a05d 100644 --- a/client/src/pages/create.rs +++ b/client/src/pages/create.rs @@ -185,12 +185,17 @@ After publishing, you can easily distribute your interactive terminal: set_container_id.set(id); set_spawn_error.set(None); } else { - set_spawn_error.set(Some("Failed to parse container ID".to_string())); + set_spawn_error.set(Some( + "Failed to parse container ID".to_string(), + )); } } else { let status = spawn_resp.status(); let text = spawn_resp.text().await.unwrap_or_default(); - set_spawn_error.set(Some(format!("Spawn failed ({}): {}", status, text))); + set_spawn_error.set(Some(format!( + "Spawn failed ({}): {}", + status, text + ))); } } Err(e) => { diff --git a/client/src/pages/dashboard.rs b/client/src/pages/dashboard.rs index c489432..53082f8 100644 --- a/client/src/pages/dashboard.rs +++ b/client/src/pages/dashboard.rs @@ -54,13 +54,15 @@ pub fn DashboardPage() -> impl IntoView { )); } } else { - set_error - .set(Some("Failed to fetch deployments".to_string())); + set_error.set(Some( + "Failed to fetch deployments".to_string(), + )); } } Err(_) => { - set_error - .set(Some("Network error connecting to API".to_string())); + set_error.set(Some( + "Network error connecting to API".to_string(), + )); } } set_loading.set(false); From 33a77cb66a7ab8d6ee4d9b0dea2b46512ae3225f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 06:44:12 +0000 Subject: [PATCH 6/6] Use CSS variables for consistent styling in error messages Co-authored-by: Yashb404 <139128977+Yashb404@users.noreply.github.com> --- client/src/pages/create.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/pages/create.rs b/client/src/pages/create.rs index fc0a05d..b6a6e69 100644 --- a/client/src/pages/create.rs +++ b/client/src/pages/create.rs @@ -373,14 +373,14 @@ After publishing, you can easily distribute your interactive terminal: {move || { if let Some(error) = spawn_error.get() { view! { - <div style="padding: 20px; color: #ef4444;"> - <div style="margin-bottom: 10px; font-weight: bold;">"⚠️ Environment Spawn Error"</div> - <div style="color: #666;">{error}</div> + <div style="padding: 20px; color: var(--text-main);"> + <div style="margin-bottom: 10px; font-weight: bold; color: #ef4444;">"⚠️ Environment Spawn Error"</div> + <div style="color: var(--text-muted);">{error}</div> </div> }.into_view() } else { match container_id.get().as_str() { - "" => view! { <div style="padding: 20px; color: #666;">"Initializing Environment..."</div> }.into_view(), + "" => view! { <div style="padding: 20px; color: var(--text-muted);">"Initializing Environment..."</div> }.into_view(), id => view! { <TerminalView container_id=id.to_string() /> }.into_view() } }