Skip to content

Commit a93bb38

Browse files
Merge branch 'main' into dev
2 parents 6ef3531 + 8115ed9 commit a93bb38

10 files changed

Lines changed: 339 additions & 256 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# TryCli Studio
22

3-
**TryCli Studio** is an open-source, full-stack platform that enables developers to host, demo, and share Command Line Interface (CLI) tools directly in the browser.
3+
**TryCli Studio** is an full-stack platform that enables developers to host, demo, and share Command Line Interface (CLI) tools directly in the browser.
44

55
It orchestrates isolated Docker environments on-demand, providing a seamless "Repl.it-like" experience specifically optimized for terminal applications.
66

client/src/components/hamburger.rs

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
use crate::api::api_base;
2+
use leptos::*;
3+
use leptos_router::A;
4+
use std::rc::Rc;
5+
use wasm_bindgen::JsCast;
6+
7+
const TWITTER_URL: &str = "https://x.com/tryclistudio";
8+
const SUPPORT_URL: &str = "https://ko-fi.com/tryclistudio";
9+
10+
#[component]
11+
pub fn HamburgerMenu(
12+
#[prop(into)] button_class: String,
13+
#[prop(into)] menu_class: String,
14+
#[prop(into)] item_class: String,
15+
#[prop(optional, into)] logout_class: String,
16+
#[prop(optional, into)] support_url: String,
17+
#[prop(optional, into)] link_style: String,
18+
#[prop(optional)] show_home: bool,
19+
#[prop(optional)] show_dashboard: bool,
20+
#[prop(optional, default = true)] show_docs: bool,
21+
#[prop(optional, default = true)] show_blogs: bool,
22+
#[prop(optional, default = true)] show_twitter: bool,
23+
#[prop(optional, default = true)] show_support: bool,
24+
#[prop(optional)] show_logout: bool,
25+
#[prop(optional)] use_open_class: bool,
26+
#[prop(optional)] stop_propagation: bool,
27+
#[prop(optional)] close_on_item_click: bool,
28+
#[prop(optional)] close_on_outside_click: bool,
29+
#[prop(optional, default = false)] support_target_blank: bool,
30+
) -> impl IntoView {
31+
let (menu_open, set_menu_open) = create_signal(false);
32+
33+
if close_on_outside_click {
34+
create_effect(move |_| {
35+
if menu_open.get() {
36+
if let Some(window) = web_sys::window() {
37+
if let Some(document) = window.document() {
38+
let closure = wasm_bindgen::closure::Closure::wrap(Box::new(move |_: web_sys::Event| {
39+
set_menu_open.set(false);
40+
}) as Box<dyn Fn(web_sys::Event)>);
41+
42+
let _ = document.add_event_listener_with_callback(
43+
"click",
44+
closure.as_ref().unchecked_ref(),
45+
);
46+
closure.forget();
47+
}
48+
}
49+
}
50+
});
51+
}
52+
53+
let logout_class = if logout_class.is_empty() {
54+
item_class.clone()
55+
} else {
56+
logout_class
57+
};
58+
59+
let support_url = if support_url.is_empty() {
60+
SUPPORT_URL.to_string()
61+
} else {
62+
support_url
63+
};
64+
65+
let menu_items: Rc<dyn Fn() -> View> = Rc::new(move || {
66+
let item_class_home = item_class.clone();
67+
let link_style_home = link_style.clone();
68+
let item_class_dashboard = item_class.clone();
69+
let link_style_dashboard = link_style.clone();
70+
let item_class_docs = item_class.clone();
71+
let link_style_docs = link_style.clone();
72+
let item_class_blogs = item_class.clone();
73+
let link_style_blogs = link_style.clone();
74+
let item_class_twitter = item_class.clone();
75+
let link_style_twitter = link_style.clone();
76+
let item_class_support = item_class.clone();
77+
let link_style_support = link_style.clone();
78+
let logout_class = logout_class.clone();
79+
let link_style_logout = link_style.clone();
80+
let support_url = support_url.clone();
81+
82+
view! {
83+
{move || if show_home {
84+
view! {
85+
<A href="/" class=item_class_home.clone() attr:style=link_style_home.clone() on:click=move |_| {
86+
if close_on_item_click {
87+
set_menu_open.set(false);
88+
}
89+
}>
90+
"Home"
91+
</A>
92+
}
93+
.into_view()
94+
} else {
95+
view! { <></> }.into_view()
96+
}}
97+
{move || if show_dashboard {
98+
view! {
99+
<A href="/dashboard" class=item_class_dashboard.clone() attr:style=link_style_dashboard.clone() on:click=move |_| {
100+
if close_on_item_click {
101+
set_menu_open.set(false);
102+
}
103+
}>
104+
"Dashboard"
105+
</A>
106+
}
107+
.into_view()
108+
} else {
109+
view! { <></> }.into_view()
110+
}}
111+
{move || if show_docs {
112+
view! {
113+
<A href="/docs" class=item_class_docs.clone() attr:style=link_style_docs.clone() on:click=move |_| {
114+
if close_on_item_click {
115+
set_menu_open.set(false);
116+
}
117+
}>
118+
"Docs"
119+
</A>
120+
}
121+
.into_view()
122+
} else {
123+
view! { <></> }.into_view()
124+
}}
125+
{move || if show_blogs {
126+
view! {
127+
<A href="/blogs" class=item_class_blogs.clone() attr:style=link_style_blogs.clone() on:click=move |_| {
128+
if close_on_item_click {
129+
set_menu_open.set(false);
130+
}
131+
}>
132+
"Blogs"
133+
</A>
134+
}
135+
.into_view()
136+
} else {
137+
view! { <></> }.into_view()
138+
}}
139+
{move || if show_twitter {
140+
view! {
141+
<a href=TWITTER_URL target="_blank" class=item_class_twitter.clone() style=link_style_twitter.clone() on:click=move |_| {
142+
if close_on_item_click {
143+
set_menu_open.set(false);
144+
}
145+
}>
146+
"Twitter"
147+
</a>
148+
}
149+
.into_view()
150+
} else {
151+
view! { <></> }.into_view()
152+
}}
153+
{move || if show_support {
154+
if support_target_blank {
155+
view! {
156+
<a href=support_url.clone() target="_blank" class=item_class_support.clone() style=link_style_support.clone() on:click=move |_| {
157+
if close_on_item_click {
158+
set_menu_open.set(false);
159+
}
160+
}>
161+
"Support Us"
162+
</a>
163+
}
164+
.into_view()
165+
} else {
166+
view! {
167+
<a href=support_url.clone() class=item_class_support.clone() style=link_style_support.clone() on:click=move |_| {
168+
if close_on_item_click {
169+
set_menu_open.set(false);
170+
}
171+
}>
172+
"Support Us"
173+
</a>
174+
}
175+
.into_view()
176+
}
177+
} else {
178+
view! { <></> }.into_view()
179+
}}
180+
{move || if show_logout {
181+
view! {
182+
<div style="border-top: 1px solid var(--border); margin: 4px 0;"></div>
183+
<a
184+
href=format!("{}/auth/logout", api_base())
185+
class=logout_class.clone()
186+
rel="external"
187+
style=link_style_logout.clone()
188+
on:click=move |_| {
189+
if close_on_item_click {
190+
set_menu_open.set(false);
191+
}
192+
}
193+
>
194+
"Logout"
195+
</a>
196+
}
197+
.into_view()
198+
} else {
199+
view! { <></> }.into_view()
200+
}}
201+
}
202+
.into_view()
203+
});
204+
205+
view! {
206+
<button
207+
class=button_class
208+
class:open=move || use_open_class && menu_open.get()
209+
on:click=move |e: ev::MouseEvent| {
210+
if stop_propagation {
211+
e.stop_propagation();
212+
}
213+
set_menu_open.update(|open| *open = !*open);
214+
}
215+
aria-label="Toggle menu"
216+
>
217+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
218+
<line x1="3" y1="12" x2="21" y2="12"></line>
219+
<line x1="3" y1="6" x2="21" y2="6"></line>
220+
<line x1="3" y1="18" x2="21" y2="18"></line>
221+
</svg>
222+
</button>
223+
{move || {
224+
let menu_items = menu_items.clone();
225+
if use_open_class {
226+
view! {
227+
<div class=menu_class.clone() class:open=move || menu_open.get() on:click=move |e: ev::MouseEvent| {
228+
if stop_propagation {
229+
e.stop_propagation();
230+
}
231+
}>
232+
{menu_items()}
233+
</div>
234+
}
235+
.into_view()
236+
} else if menu_open.get() {
237+
view! {
238+
<div class=menu_class.clone() on:click=move |e: ev::MouseEvent| {
239+
if stop_propagation {
240+
e.stop_propagation();
241+
}
242+
}>
243+
{menu_items()}
244+
</div>
245+
}
246+
.into_view()
247+
} else {
248+
view! { <></> }.into_view()
249+
}
250+
}}
251+
}
252+
}

client/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod api;
44
pub mod app;
55
pub mod types;
66
pub mod components {
7+
pub mod hamburger;
78
pub mod limit;
89
pub mod modal;
910
pub mod navbar;

client/src/pages/blogs.rs

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::api::api_base;
2+
use crate::components::hamburger::HamburgerMenu;
23
use crate::components::navbar::Navbar;
34
use crate::types::User;
45
use gloo_net::http::Request;
@@ -8,7 +9,6 @@ use web_sys::RequestCredentials;
89

910
#[component]
1011
pub fn BlogsPage() -> impl IntoView {
11-
let (menu_open, set_menu_open) = create_signal(false);
1212
let (user, set_user) = create_signal(None::<User>);
1313
let (auth_checked, set_auth_checked) = create_signal(false);
1414

@@ -52,28 +52,16 @@ pub fn BlogsPage() -> impl IntoView {
5252
}
5353
}}
5454

55-
// Hamburger Menu Button
56-
<button
57-
class="hamburger-menu dashboard-hamburger"
58-
class:open=move || menu_open.get()
59-
on:click=move |_| set_menu_open.update(|open| *open = !*open)
60-
aria-label="Toggle menu"
61-
>
62-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
63-
<line x1="3" y1="12" x2="21" y2="12"></line>
64-
<line x1="3" y1="6" x2="21" y2="6"></line>
65-
<line x1="3" y1="18" x2="21" y2="18"></line>
66-
</svg>
67-
</button>
68-
69-
// Mobile Menu Dropdown
70-
<div class="mobile-menu" class:open=move || menu_open.get()>
71-
<A href="/" class="menu-item" on:click=move |_| set_menu_open.set(false)>"Home"</A>
72-
<A href="/dashboard" class="menu-item" on:click=move |_| set_menu_open.set(false)>"Dashboard"</A>
73-
<A href="/docs" class="menu-item" on:click=move |_| set_menu_open.set(false)>"Docs"</A>
74-
<a href="https://twitter.com" target="_blank" class="menu-item" on:click=move |_| set_menu_open.set(false)>"Twitter"</a>
75-
<a href="https://ko-fi.com/tryclistudio" class="menu-item" on:click=move |_| set_menu_open.set(false)>"Support Us"</a>
76-
</div>
55+
<HamburgerMenu
56+
button_class="hamburger-menu dashboard-hamburger"
57+
menu_class="mobile-menu"
58+
item_class="menu-item"
59+
show_home=true
60+
show_dashboard=true
61+
show_blogs=false
62+
use_open_class=true
63+
close_on_item_click=true
64+
/>
7765
</div>
7866
</Navbar>
7967

client/src/pages/create.rs

Lines changed: 12 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::api::api_base;
2+
use crate::components::hamburger::HamburgerMenu;
23
use crate::components::modal::Modal;
34
use crate::components::navbar::Navbar;
45
use crate::components::terminal::TerminalView;
@@ -260,7 +261,6 @@ After publishing, you can easily distribute your interactive terminal:
260261
let (modal_body, set_modal_body) = create_signal(String::new());
261262
let (modal_success, set_modal_success) = create_signal(false);
262263
let (publish_modal_open, set_publish_modal_open) = create_signal(false);
263-
let (menu_open, set_menu_open) = create_signal(false);
264264

265265
create_resource(
266266
|| (),
@@ -533,50 +533,17 @@ After publishing, you can easily distribute your interactive terminal:
533533
style="width: 24px; height: 24px; border-radius: 50%; border: 1px solid var(--border);" />
534534
}}
535535
<div style="position: relative;">
536-
<button
537-
class="hamburger-menu create-hamburger"
538-
on:click=move |e: ev::MouseEvent| {
539-
e.stop_propagation();
540-
set_menu_open.set(!menu_open.get());
541-
}
542-
aria-label="Toggle menu"
543-
>
544-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
545-
<line x1="3" y1="12" x2="21" y2="12"></line>
546-
<line x1="3" y1="6" x2="21" y2="6"></line>
547-
<line x1="3" y1="18" x2="21" y2="18"></line>
548-
</svg>
549-
</button>
550-
{move || if menu_open.get() {
551-
view! {
552-
<div class="dropdown-menu" on:click=move |e: ev::MouseEvent| e.stop_propagation()>
553-
<a href="/dashboard" class="dropdown-item" style="text-decoration: none;">
554-
"Dashboard"
555-
</a>
556-
<a href="/docs" class="dropdown-item" style="text-decoration: none;">
557-
"Docs"
558-
</a>
559-
<a href="/blogs" class="dropdown-item" style="text-decoration: none;">
560-
"Blogs"
561-
</a>
562-
<a href="https://twitter.com" target="_blank" class="dropdown-item" style="text-decoration: none;">
563-
"Twitter"
564-
</a>
565-
<a href="https://ko-fi.com/tryclistudio" target="_blank" class="dropdown-item" style="text-decoration: none;">
566-
"Support Us"
567-
</a>
568-
<div style="border-top: 1px solid var(--border); margin: 4px 0;"></div>
569-
<a href=format!("{}/auth/logout", api_base())
570-
class="dropdown-item dropdown-item-danger"
571-
rel="external"
572-
style="text-decoration: none;">
573-
"Logout"
574-
</a>
575-
</div>
576-
}.into_view()
577-
} else {
578-
view! { <></> }.into_view()
579-
}}
536+
<HamburgerMenu
537+
button_class="hamburger-menu create-hamburger"
538+
menu_class="dropdown-menu"
539+
item_class="dropdown-item"
540+
logout_class="dropdown-item dropdown-item-danger"
541+
link_style="text-decoration: none;"
542+
show_dashboard=true
543+
show_logout=true
544+
stop_propagation=true
545+
support_target_blank=true
546+
/>
580547
</div>
581548
</div>
582549
}.into_view(),

0 commit comments

Comments
 (0)