Skip to content

Commit 80f4f78

Browse files
Merge pull request #161 from TryCli-Studio/dev
Dev
2 parents 8115ed9 + a93bb38 commit 80f4f78

8 files changed

Lines changed: 193 additions & 74 deletions

File tree

assets/octopus_terminal_opt.png

2.15 MB
Loading

client/src/components/modal.rs

Lines changed: 105 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ pub fn Modal(
2828
role="dialog"
2929
aria-modal="true"
3030
on:click=move |ev: ev::MouseEvent| {
31-
// Only close on overlay click if not a blocking modal
3231
if !is_blocking() {
3332
if let Some(target) = ev.target().and_then(|t| t.dyn_into::<HtmlElement>().ok()) {
3433
if target.class_list().contains("modal-overlay") {
@@ -80,6 +79,8 @@ pub fn EmbedModal(
8079
smart_link: MaybeSignal<String>,
8180
vip_link: MaybeSignal<String>,
8281
whitelist: MaybeSignal<Vec<String>>,
82+
is_public: MaybeSignal<bool>,
83+
on_toggle_public: Callback<bool>,
8384
on_add_url: Callback<String>,
8485
on_remove_url: Callback<String>,
8586
on_close: Callback<()>,
@@ -104,6 +105,8 @@ pub fn EmbedModal(
104105
let on_close = on_close.clone();
105106
let on_add_url = on_add_url.clone();
106107
let on_remove_url = on_remove_url.clone();
108+
let is_public = is_public.clone();
109+
let on_toggle_public = on_toggle_public.clone();
107110

108111
let iframe_code_for_click = iframe_code.clone();
109112
let smart_link_for_click = smart_link.clone();
@@ -118,6 +121,37 @@ pub fn EmbedModal(
118121
<button class="btn-nav" on:click=move |_| on_close.call(()) style="font-size: 1.5rem; line-height: 1;">"×"</button>
119122
</div>
120123

124+
// --- SECTION 0: VISIBILITY TOGGLE ---
125+
<div style="background: rgba(34, 197, 94, 0.05); border: 1px solid rgba(34, 197, 94, 0.2); border-radius: 8px; padding: 16px; margin-bottom: 24px; display: flex; align-items: center; justify-content: space-between;">
126+
<div>
127+
<div style="font-weight: 600; color: var(--text-main); margin-bottom: 4px;">"Public Access"</div>
128+
<div style="font-size: 0.85rem; color: var(--text-muted);">
129+
"Allow anyone to view/embed this project without whitelist restrictions."
130+
</div>
131+
</div>
132+
<label class="switch" style="position: relative; display: inline-block; width: 44px; height: 24px;">
133+
<input type="checkbox"
134+
checked=is_public.get()
135+
on:change=move |ev| {
136+
let checked = event_target_checked(&ev);
137+
on_toggle_public.call(checked);
138+
}
139+
style="opacity: 0; width: 0; height: 0;"
140+
/>
141+
<span class="slider"
142+
style=move || {
143+
let bg = if is_public.get() { "#22c55e" } else { "#3f3f46" };
144+
format!("position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: {}; transition: .4s; border-radius: 34px;", bg)
145+
}
146+
>
147+
<span style=move || {
148+
let transform = if is_public.get() { "translateX(20px)" } else { "translateX(0)" };
149+
format!("position: absolute; content: ''; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; transform: {};", transform)
150+
}></span>
151+
</span>
152+
</label>
153+
</div>
154+
121155
// --- SECTION 1: IFRAME ---
122156
<div style="margin-bottom: 24px; position: relative;">
123157
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
@@ -270,64 +304,75 @@ pub fn EmbedModal(
270304
</p>
271305
</div>
272306

273-
// --- SECTION 4: Guest List / Whitelist ---
274-
<div style="margin-bottom: 24px;">
275-
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
276-
<label style="color:var(--text-main); font-weight:600; font-size: 0.9rem;">
277-
"Guest List (Authorized URLs)"
278-
</label>
279-
<span style="font-size: 0.8rem; color: var(--text-muted);">
280-
"Only these pages can auto-launch your terminal."
281-
</span>
282-
</div>
283-
<div style="display: flex; gap: 10px; margin-bottom: 12px;">
284-
<input
285-
type="text"
286-
class="input-slug"
287-
style="flex: 1;"
288-
placeholder="https://medium.com/@user/article-slug"
289-
prop:value=new_url
290-
on:input=move |ev| set_new_url.set(event_target_value(&ev))
291-
/>
292-
<button
293-
class="btn-primary"
294-
on:click=move |_| {
295-
let url = new_url.get();
296-
if !url.is_empty() {
297-
on_add_url.call(url);
298-
set_new_url.set(String::new());
299-
}
300-
}
301-
prop:disabled=move || new_url.get().is_empty()
302-
>
303-
"Add URL"
304-
</button>
305-
</div>
306-
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
307-
<For
308-
each=move || whitelist.get()
309-
key=|u| u.clone()
310-
children=move |url| {
311-
let on_remove_url = on_remove_url.clone();
312-
view! {
313-
<span class="badge" style="margin: 0; display: flex; align-items: center; gap: 8px;">
314-
{url.clone()}
315-
<button
316-
class="btn-nav"
317-
style="padding: 0; color: #ef4444; font-weight: bold; font-size: 0.9rem;"
318-
aria-label="Remove URL"
319-
on:click=move |_| {
320-
on_remove_url.call(url.clone());
321-
}
322-
>
323-
"×"
324-
</button>
325-
</span>
326-
}
327-
}
328-
/>
329-
</div>
330-
</div>
307+
// --- SECTION 4: Guest List / Whitelist (CONDITIONAL) ---
308+
{move || if !is_public.get() {
309+
let whitelist = whitelist.clone(); // <--- Clone needed for Fn compliance
310+
view! {
311+
<div style="margin-bottom: 24px;">
312+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
313+
<label style="color:var(--text-main); font-weight:600; font-size: 0.9rem;">
314+
"Guest List (Authorized URLs)"
315+
</label>
316+
<span style="font-size: 0.8rem; color: var(--text-muted);">
317+
"Only these pages can auto-launch your terminal."
318+
</span>
319+
</div>
320+
<div style="display: flex; gap: 10px; margin-bottom: 12px;">
321+
<input
322+
type="text"
323+
class="input-slug"
324+
style="flex: 1;"
325+
placeholder="https://medium.com/@user/article-slug"
326+
prop:value=new_url
327+
on:input=move |ev| set_new_url.set(event_target_value(&ev))
328+
/>
329+
<button
330+
class="btn-primary"
331+
on:click=move |_| {
332+
let url = new_url.get();
333+
if !url.is_empty() {
334+
on_add_url.call(url);
335+
set_new_url.set(String::new());
336+
}
337+
}
338+
prop:disabled=move || new_url.get().is_empty()
339+
>
340+
"Add URL"
341+
</button>
342+
</div>
343+
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
344+
<For
345+
each=move || whitelist.get()
346+
key=|u| u.clone()
347+
children=move |url| {
348+
let on_remove_url = on_remove_url.clone();
349+
view! {
350+
<span class="badge" style="margin: 0; display: flex; align-items: center; gap: 8px;">
351+
{url.clone()}
352+
<button
353+
class="btn-nav"
354+
style="padding: 0; color: #ef4444; font-weight: bold; font-size: 0.9rem;"
355+
aria-label="Remove URL"
356+
on:click=move |_| {
357+
on_remove_url.call(url.clone());
358+
}
359+
>
360+
"×"
361+
</button>
362+
</span>
363+
}
364+
}
365+
/>
366+
</div>
367+
</div>
368+
}.into_view()
369+
} else {
370+
view! {
371+
<div style="margin-bottom: 24px; padding: 16px; border: 1px dashed var(--border); border-radius: 8px; text-align: center; color: var(--text-muted); font-size: 0.9rem;">
372+
"Project is Public. Guest List restrictions are disabled."
373+
</div>
374+
}.into_view()
375+
}}
331376

332377
<div class="modal-actions">
333378
<button class="btn-secondary btn-action" on:click=move |_| on_close.call(())>
@@ -414,4 +459,4 @@ pub fn ConfirmModal(
414459
}
415460
}}
416461
}
417-
}
462+
}

client/src/pages/view.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ enum ProjectState {
2020
Loading,
2121
NotFound,
2222
LimitReached,
23-
Unauthorized, // Security block state
23+
Unauthorized,
2424
Ready(serde_json::Value),
2525
}
2626

@@ -235,6 +235,10 @@ pub fn ViewPage() -> impl IntoView {
235235
let (iframe_code, set_iframe_code) = create_signal(String::new());
236236
let (smart_link, set_smart_link) = create_signal(String::new());
237237
let (vip_link, set_vip_link) = create_signal(String::new());
238+
let (menu_open, set_menu_open) = create_signal(false);
239+
240+
// NEW: Public toggle signal
241+
let (is_public, set_is_public) = create_signal(false);
238242

239243
let (whitelist, set_whitelist) = create_signal(Vec::<String>::new());
240244

@@ -284,10 +288,15 @@ pub fn ViewPage() -> impl IntoView {
284288
} else if resp.status() == 429 {
285289
ProjectState::LimitReached
286290
} else if resp.ok() {
287-
resp.json::<serde_json::Value>()
288-
.await
289-
.map(ProjectState::Ready)
290-
.unwrap_or(ProjectState::NotFound)
291+
if let Ok(json) = resp.json::<serde_json::Value>().await {
292+
// Extract is_public from the response
293+
if let Some(public_flag) = json.get("is_public").and_then(|v| v.as_bool()) {
294+
set_is_public.set(public_flag);
295+
}
296+
ProjectState::Ready(json)
297+
} else {
298+
ProjectState::NotFound
299+
}
291300
} else {
292301
ProjectState::NotFound
293302
}
@@ -334,6 +343,7 @@ pub fn ViewPage() -> impl IntoView {
334343
}
335344
};
336345

346+
// Actions
337347
let add_whitelist_item = create_action(move |url: &String| {
338348
let url = url.clone();
339349
let s = slug();
@@ -392,6 +402,22 @@ pub fn ViewPage() -> impl IntoView {
392402
}
393403
});
394404

405+
// NEW: Toggle public action
406+
let toggle_public_action = create_action(move |new_state: &bool| {
407+
let s = slug();
408+
let val = *new_state;
409+
async move {
410+
set_is_public.set(val); // Optimistic UI update
411+
let req = Request::post(&format!("{}/api/project/{}/visibility", api_base(), s))
412+
.credentials(RequestCredentials::Include)
413+
.json(&serde_json::json!({ "is_public": val }));
414+
415+
if let Ok(builder) = req {
416+
let _ = builder.send().await;
417+
}
418+
}
419+
});
420+
395421
view! {
396422
<>
397423
<EmbedModal
@@ -401,6 +427,9 @@ pub fn ViewPage() -> impl IntoView {
401427
smart_link=smart_link.into()
402428
vip_link=vip_link.into()
403429
whitelist=whitelist.into()
430+
// Pass new props
431+
is_public=is_public.into()
432+
on_toggle_public=Callback::new(move |val: bool| toggle_public_action.dispatch(val))
404433
on_add_url=Callback::new(move |url: String| add_whitelist_item.dispatch(url))
405434
on_remove_url=Callback::new(move |url: String| remove_whitelist_item.dispatch(url))
406435
on_close=Callback::new(move |_| set_embed_modal_open.set(false))
@@ -503,7 +532,6 @@ pub fn ViewPage() -> impl IntoView {
503532
let vip = if key.is_empty() {
504533
String::new()
505534
} else {
506-
// URL encode the key to handle special characters in base64
507535
let encoded_key = js_sys::encode_uri_component(key);
508536
format!("{}/{}/{}?key={}", origin_clone, username_clone, slug_clone, encoded_key)
509537
};

client/src/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ pub struct ProjectSummary {
88
pub view_count: i64,
99
#[serde(default)]
1010
pub owner_username: String,
11+
#[serde(default)]
12+
pub is_public: bool,
1113
}
1214

1315
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- Add down migration script here
2+
ALTER TABLE projects
3+
DROP COLUMN is_public;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- Add up migration script here
2+
ALTER TABLE projects
3+
ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE;

0 commit comments

Comments
 (0)