Skip to content

Commit 9cd4e8b

Browse files
committed
Merge remote-tracking branch 'origin/deploy2'
2 parents c0b8ddb + e3f33a2 commit 9cd4e8b

14 files changed

Lines changed: 217 additions & 95 deletions

File tree

.codex

Whitespace-only changes.

crates/app/src/api.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
22
use shared::{
33
client::{ClientError, HttpMethod, fetch, fetch_typed, send},
44
dto::UpdateDemoRequest,
5+
dto::PublicDemoResponse,
56
models::demo::{Demo, DemoSettings, Step, Theme},
67
};
78
use time::OffsetDateTime;
@@ -489,6 +490,16 @@ pub async fn get_demo_detail(id: &str) -> Result<Demo, String> {
489490
.await
490491
}
491492

493+
pub async fn get_public_demo(reference: &str) -> Result<PublicDemoResponse, String> {
494+
fetch(
495+
HttpMethod::Get,
496+
&api_url(&format!("/api/public/demos/{reference}")),
497+
None,
498+
false,
499+
)
500+
.await
501+
}
502+
492503
pub async fn create_demo(title: &str, project_id: Option<&str>) -> Result<DashboardDemo, String> {
493504
let payload = CreateDemoRequest { title, project_id };
494505
let body = serde_json::to_string(&payload).map_err(|e| format!("serialize body: {e}"))?;

crates/app/src/app.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ use crate::components::shell::AppShell;
1010
use crate::pages::{
1111
analytics::AnalyticsPage, demo_editor::DemoEditorPage, demo_share::ShareDemoPage,
1212
demo_view::DemoViewPage, demos::DemosPage, docs::DocsPage, docs::DocsSectionPage,
13-
landing::LandingPage, publish::PublishPage, settings::SettingsPage,
13+
embed_fallback::EmbedFallbackPage, landing::LandingPage, publish::PublishPage,
14+
settings::SettingsPage,
1415
};
1516

1617
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -104,6 +105,7 @@ pub fn App() -> impl IntoView {
104105
<Route path=path!("/docs/:section") view=DocsSectionPage />
105106
<Route path=path!("/docs/:category/:section") view=DocsSectionPage />
106107
<Route path=path!("/d/:slug") view=ShareDemoPage />
108+
<Route path=path!("/embed/:id") view=EmbedFallbackPage />
107109
<Route path=path!("/demo/view") view=DemoViewPage />
108110

109111
<ParentRoute path=path!("") view=AppShell>

crates/app/src/pages/demo_editor.rs

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -56,73 +56,6 @@ fn normalize_command_match_patterns(steps: &mut [Step]) {
5656
}
5757
}
5858

59-
/// Produces a list of (command, description) pairs for command steps.
60-
///
61-
/// Scans the provided steps for those whose `step_type` is `StepType::Command`,
62-
/// selects a command string from `input` if present or `match_pattern` otherwise
63-
/// (ignoring empty/whitespace values), and pairs it with a short description
64-
/// (using `short_description`, falling back to `description`, or a sensible
65-
/// default when both are empty).
66-
///
67-
/// # Parameters
68-
///
69-
/// - `steps`: Slice of steps to extract command guide entries from.
70-
///
71-
/// # Returns
72-
///
73-
/// A vector of `(command, description)` tuples for each command step with a
74-
/// non-empty command string.
75-
///
76-
/// # Examples
77-
///
78-
/// ```rust,no_run
79-
/// // Construct demo steps and produce guide entries.
80-
/// // Assume `Step` and `StepType` are available in scope.
81-
/// let steps = vec![
82-
/// Step {
83-
/// step_type: StepType::Command,
84-
/// input: Some("echo hello".to_string()),
85-
/// match_pattern: None,
86-
/// short_description: Some("Greet".to_string()),
87-
/// description: None,
88-
/// ..Default::default()
89-
/// },
90-
/// Step {
91-
/// step_type: StepType::Command,
92-
/// input: None,
93-
/// match_pattern: Some("ls -la".to_string()),
94-
/// short_description: None,
95-
/// description: Some("List files".to_string()),
96-
/// ..Default::default()
97-
/// },
98-
/// ];
99-
///
100-
/// let entries = command_guide_entries(&steps);
101-
/// assert_eq!(entries.len(), 2);
102-
/// ```
103-
fn command_guide_entries(steps: &[Step]) -> Vec<(String, String)> {
104-
steps
105-
.iter()
106-
.filter(|step| matches!(step.step_type, StepType::Command))
107-
.filter_map(|step| {
108-
let command = step
109-
.input
110-
.clone()
111-
.or_else(|| step.match_pattern.clone())
112-
.filter(|v| !v.trim().is_empty())?;
113-
114-
let description = step
115-
.short_description
116-
.clone()
117-
.or_else(|| step.description.clone())
118-
.filter(|v| !v.trim().is_empty())
119-
.unwrap_or_else(|| "Run this command in the terminal preview.".to_string());
120-
121-
Some((command, description))
122-
})
123-
.collect()
124-
}
125-
12659
/// Constructs a namespaced demo path using router params, the demo ID, and an optional suffix.
12760
12861
///

crates/app/src/pages/demo_share.rs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use leptos::prelude::*;
22
use leptos_router::hooks::use_params_map;
33

4+
use crate::api;
5+
46
#[component]
57
pub fn ShareDemoPage() -> impl IntoView {
68
let params = use_params_map();
@@ -11,11 +13,65 @@ pub fn ShareDemoPage() -> impl IntoView {
1113
.unwrap_or_else(|| "unknown".to_string())
1214
};
1315

16+
Effect::new(move |_| {
17+
let current_slug = slug();
18+
if current_slug == "unknown" {
19+
return;
20+
}
21+
22+
let api_base = api::api_base();
23+
let runtime_url = format!(
24+
"{api_base}/embed-runtime/index.html?demo_id={current_slug}&api_base={api_base}"
25+
);
26+
27+
if let Some(window) = web_sys::window() {
28+
let in_iframe = window
29+
.parent()
30+
.ok()
31+
.flatten()
32+
.map(|parent| !js_sys::Object::is(window.as_ref(), parent.as_ref()))
33+
.unwrap_or(false);
34+
35+
if in_iframe {
36+
let _ = window.location().set_href(&runtime_url);
37+
}
38+
}
39+
});
40+
41+
let embed_src = move || {
42+
let api_base = api::api_base();
43+
format!(
44+
"{api_base}/embed-runtime/index.html?demo_id={}&api_base={api_base}",
45+
slug()
46+
)
47+
};
48+
1449
view! {
15-
<section class="page demo-share-page">
16-
<h2>"Shared Demo"</h2>
17-
<p>{move || format!("Public slug: {}", slug())}</p>
18-
<div class="demo-frame-placeholder">"Terminal render will mount here."</div>
50+
<section class="page demo-share-page" style="min-height:100vh;padding:24px;background:#0e0e10;color:#e7e4ec;">
51+
<div style="max-width:1200px;margin:0 auto;display:grid;gap:18px;">
52+
<header style="display:flex;flex-wrap:wrap;align-items:end;justify-content:space-between;gap:12px;">
53+
<div>
54+
<p style="margin:0 0 8px;font-size:12px;font-weight:700;letter-spacing:0.18em;text-transform:uppercase;color:#4ae176;">"Shared Demo"</p>
55+
<h2 style="margin:0;font-size:clamp(2rem,4vw,3.5rem);line-height:0.95;letter-spacing:-0.05em;">"Terminal render"</h2>
56+
<p style="margin:10px 0 0;color:#acaab1;">{move || format!("Public slug: {}", slug())}</p>
57+
</div>
58+
<a
59+
href="/"
60+
style="display:inline-flex;align-items:center;gap:8px;border:1px solid rgba(255,255,255,0.12);border-radius:999px;padding:10px 14px;color:#e7e4ec;text-decoration:none;background:rgba(255,255,255,0.03);"
61+
>
62+
"Back to home"
63+
</a>
64+
</header>
65+
66+
<div style="overflow:hidden;border:1px solid rgba(255,255,255,0.08);border-radius:20px;background:#11151b;box-shadow:0 30px 80px rgba(0,0,0,0.35);">
67+
<iframe
68+
src=embed_src
69+
title="Shared terminal demo"
70+
referrerpolicy="no-referrer"
71+
style="display:block;width:100%;height:min(78vh,760px);border:0;background:#000;"
72+
/>
73+
</div>
74+
</div>
1975
</section>
2076
}
2177
}

crates/app/src/pages/demo_view.rs

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@ pub fn DemoViewPage() -> impl IntoView {
1414
.unwrap_or_else(|| "demo-id".to_string())
1515
});
1616
let (demo_id, set_demo_id) = signal(initial_demo_id.get());
17-
let (api_base, set_api_base) = signal(api::api_base());
18-
let page_origin = api::browser_origin();
19-
let iframe_origin = page_origin.clone();
20-
let script_origin = page_origin.clone();
17+
let api_base = api::api_base();
18+
let iframe_origin = api_base.clone();
19+
let script_origin = api_base.clone();
2120
let embed_src = Signal::derive(move || {
2221
format!(
23-
"/embed-runtime/index.html?demo_id={}&api_base={}",
22+
"{}/embed-runtime/index.html?demo_id={}&api_base={}",
23+
api::api_base(),
2424
demo_id.get(),
25-
api_base.get()
25+
api::api_base()
2626
)
2727
});
2828

2929
let iframe_snippet = Signal::derive(move || {
3030
generate_iframe_snippet(
31-
&format!("{}/demo/view?id={}", iframe_origin, demo_id.get()),
31+
&format!("{}/embed/{}", iframe_origin, demo_id.get()),
3232
"100%",
3333
"480px",
3434
)
@@ -53,16 +53,9 @@ pub fn DemoViewPage() -> impl IntoView {
5353
on:input=move |ev| set_demo_id.set(event_target_value(&ev))
5454
/>
5555
</label>
56-
<label>
57-
"API Base"
58-
<input
59-
prop:value=move || api_base.get()
60-
on:input=move |ev| set_api_base.set(event_target_value(&ev))
61-
/>
62-
</label>
6356
<p>
64-
"Open with query param: "
65-
<code>"/demo/view?id=YOUR_DEMO_ID"</code>
57+
"Open with path: "
58+
<code>"/embed/YOUR_DEMO_ID"</code>
6659
</p>
6760
<p>
6861
"Quick smoke test commands (fallback demo): "
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use leptos::prelude::*;
2+
use leptos_router::hooks::use_params_map;
3+
4+
use crate::api;
5+
6+
#[component]
7+
pub fn EmbedFallbackPage() -> impl IntoView {
8+
let params = use_params_map();
9+
let demo_id = move || {
10+
params
11+
.read()
12+
.get("id")
13+
.unwrap_or_else(|| "unknown".to_string())
14+
};
15+
16+
let embed_src = Signal::derive(move || {
17+
let base = api::api_base();
18+
let id = demo_id();
19+
format!("{base}/embed-runtime/index.html?demo_id={id}&api_base={base}")
20+
});
21+
22+
view! {
23+
<main style="width:100vw;height:100vh;overflow:hidden;background:#050505;">
24+
<iframe
25+
src=move || embed_src.get()
26+
title="SimuCLI Embed"
27+
referrerpolicy="strict-origin-when-cross-origin"
28+
style="display:block;width:100%;height:100%;border:0;background:#050505;"
29+
/>
30+
</main>
31+
}
32+
}

crates/app/src/pages/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod analytics;
22
pub mod demo_editor;
3+
pub mod embed_fallback;
34
pub mod demo_share;
45
pub mod demo_view;
56
pub mod demos;

crates/app/src/pages/publish.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,13 @@ pub fn PublishPage() -> impl IntoView {
187187
<span class="material-symbols-outlined text-on-surface-variant text-sm">"code"</span>
188188
</div>
189189
<EmbedCodeGenerator
190-
demo_url=public_url.get()
191-
script_url=format!("{}/static/embed.js", api::browser_origin())
190+
demo_url=format!(
191+
"{}/embed-runtime/index.html?demo_id={}&api_base={}",
192+
api::api_base(),
193+
published_slug.get(),
194+
api::api_base()
195+
)
196+
script_url=format!("{}/static/embed.js", api::api_base())
192197
demo_id=published_slug.get()
193198
/>
194199
</section>

crates/embed/src/lib.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,26 @@ fn query_param_value(key: &str) -> Option<String> {
3131
None
3232
}
3333

34+
#[cfg(target_arch = "wasm32")]
35+
fn path_segment_value() -> Option<String> {
36+
fn decode_component(value: &str) -> String {
37+
js_sys::decode_uri_component(value)
38+
.ok()
39+
.and_then(|s| s.as_string())
40+
.unwrap_or_else(|| value.to_string())
41+
}
42+
43+
let path = web_sys::window()?.location().pathname().ok()?;
44+
let trimmed = path.trim_matches('/');
45+
let last_segment = trimmed.rsplit('/').find(|segment| !segment.is_empty())?;
46+
let decoded = decode_component(last_segment);
47+
if decoded.is_empty() {
48+
None
49+
} else {
50+
Some(decoded)
51+
}
52+
}
53+
3454
#[derive(Clone, Debug, PartialEq, Eq)]
3555
pub struct EmbedConfig {
3656
pub demo_id: String,
@@ -55,7 +75,7 @@ pub fn EmbedApp() -> impl IntoView {
5575
let set_demo = _set_demo;
5676
let set_status = _set_status;
5777
let set_config = _set_config;
58-
let Some(demo_id) = query_param_value("demo_id") else {
78+
let Some(demo_id) = query_param_value("demo_id").or_else(path_segment_value) else {
5979
set_status.set("Missing demo_id query parameter".to_string());
6080
return;
6181
};

0 commit comments

Comments
 (0)