1#![recursion_limit = "512"]
2
3#[cfg(any(
4 all(feature = "ssr", feature = "hydrate", not(feature = "docs-only")),
5 not(any(feature = "ssr", feature = "hydrate"))
6))]
7compile_error!("exactly one of the features \"ssr\" or \"hydrate\" must be enabled");
8
9pub mod query;
10mod settings;
11
12pub mod ws {
13 pub use flams_flodown::math::MathSocket;
14 #[cfg(feature = "ssr")]
15 pub use flams_flodown::math::TeXSocket;
16 pub use flams_router_base::ws::*;
17 pub use flams_router_buildqueue_components::QueueSocket;
18 pub use flams_router_logging::LogSocket;
19}
20
21pub mod server_fns {
22 pub mod content {
23 pub use flams_router_content::server_fns::*;
24 }
25 pub mod backend {
26 pub use flams_router_backend::server_fns::*;
27 }
28 pub mod buildqueue {
29 pub use flams_router_buildqueue_base::server_fns::*;
30 }
31 pub mod git {
32 pub use flams_router_git_base::server_fns::*;
33 }
34 pub mod login {
35 pub use flams_router_login::server_fns::*;
36 }
37 pub mod search {
38 pub use flams_router_search::{search_query, search_symbols};
39 }
40 pub use super::query::query_api as query;
41 pub use super::settings::{get_settings as settings, reload};
42}
43
44pub use flams_router_base::LoginState;
45use ftml_dom::FtmlViews;
46use leptos::{
47 either::{Either, EitherOf4},
48 prelude::*,
49};
50use leptos_meta::{Stylesheet, Title, provide_meta_context};
51use leptos_router::{
52 components::{Outlet, ParentRoute, Redirect, Route, Router, Routes},
53 hooks::use_query_map,
54 path,
55};
56use thaw::{Divider, Grid, GridItem, Layout, LayoutHeader, LayoutPosition, LayoutSider};
57
58#[component]
59pub fn Main() -> AnyView {
60 provide_meta_context();
61 view! {
62 <Title text="𝖥𝖫∀𝖬∫"/>
63 <Router>{
64 let has_params = Memo::new(move |_| use_query_map().with(|p| p.get_str("a").is_some() || p.get_str("uri").is_some()));
65 view!{<Routes fallback=|| NotFound()>
66 <ParentRoutepath=() view=Top>
67 <ParentRoute path=path!("/dashboard") view=Dashboard>
68 <Route path=path!("mathhub") view=|| view!(<MainPage page=Page::MathHub/>).into_any()/>
69 <Route path=path!("log") view=|| view!(<MainPage page=Page::Log/>).into_any()/>
71 <Route path=path!("queue") view=|| view!(<MainPage page=Page::Queue/>).into_any()/>
72 <Route path=path!("settings") view=|| view!(<MainPage page=Page::Settings/>).into_any()/>
73 <Route path=path!("query") view=|| view!(<MainPage page=Page::Query/>).into_any()/>
74 <Route path=path!("archives") view=|| view!(<MainPage page=Page::MyArchives/>).into_any()/>
75 <Route path=path!("users") view=|| view!(<MainPage page=Page::Users/>).into_any()/>
76 <Route path=path!("search") view=|| view!(<MainPage page=Page::Search/>).into_any()/>
77 <Route path=path!("flodown") view=|| view!(<MainPage page=Page::FloDown/>).into_any()/>
78 <Route path=path!("") view=|| view!(<MainPage page=Page::Home/>).into_any()/>
79 <Route path=path!("*any") view=|| view!(<MainPage page=Page::NotFound/>).into_any()/>
80 </ParentRoute>
81 <ParentRoute path=path!("/vscode") view= flams_router_vscode::VSCodeWrap>
82 <Route path=path!("search") view=flams_router_search::vscode::VSCodeSearch/>
83 </ParentRoute>
84 <Route path=path!("/document") view={move || {
85 use flams_router_content::components::{DocumentOfTop,DocumentOfTopProps};
86 let params = use_query_map().get_untracked();
87 if let Some(p) = params.get_str("uri") {
88 let Ok(uri) = <ftml_uris::Uri as std::str::FromStr>::from_str(p) else {
89 return view! { <Redirect path="/dashboard"/> }.into_any()
90 };
91 DocumentOfTop(DocumentOfTopProps{uri}).into_any()
92 } else {
93 view! { <Redirect path="/dashboard"/> }.into_any()
94 }
95 }.into_any()}/>
96 <Route path=path!("/") view={move || if has_params.get() {
97 view! { <flams_router_content::components::URITop/> }.into_any()
98 } else {
99 view! { <Redirect path="/dashboard"/> }.into_any()
100 }}
101 />
102 </ParentRoute>
103 </Routes>}
104 }</Router>
105 }.into_any()
106}
107
108#[component(transparent)]
109fn Top() -> AnyView {
110 use flams_router_login::components::LoginProvider;
111 view!(<LoginProvider><leptos_router::components::Outlet/></LoginProvider>).into_any()
112}
113
114#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
115enum Page {
116 Home,
117 MathHub,
118 Log,
120 NotFound,
121 Queue,
122 Settings,
123 Login,
124 Query,
125 Search,
126 FloDown,
127 MyArchives,
128 Users,
129}
130impl Page {
131 pub const fn key(self) -> &'static str {
132 use Page::*;
133 match self {
134 Home => "home",
135 MathHub => "mathhub",
136 Log => "log",
138 Login => "login",
139 Queue => "queue",
140 Settings => "settings",
141 Query => "query",
142 MyArchives => "archives",
143 Search => "search",
144 FloDown => "flodown",
145 Users => "users",
146 NotFound => "notfound",
147 }
148 }
149}
150impl std::fmt::Display for Page {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 f.write_str(self.key())
153 }
154}
155
156#[component(transparent)]
157pub fn Dashboard() -> AnyView {
158 view! {
159 <Outlet/>
160 }
161 .into_any()
162}
163
164#[component]
165fn MainPage(page: Page) -> AnyView {
166 ftml_dom::global_setup(move || flams_router_content::Views::top(move || {
171 view! {
172 <Layout position=LayoutPosition::Absolute>
173 <LayoutHeader class="flams-header">
175 <div style="width:100%">
176 <Grid cols=3>
177 <GridItem>""</GridItem>
178 <GridItem>
179 <svg xmlns="http://www.w3.org/2000/svg" width="120px" height="60px" viewBox="0 -805.5 2248.7 1111" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" style="color:var(--colorBrandForeground1)"><defs><path id="MJX-5-TEX-SS-1D5A5" d="M86 0V691H526V611H358L190 612V384H485V308H190V0H86Z"></path><path id="MJX-5-TEX-SS-1D5AB" d="M87 0V694H191V79L297 80H451L499 81V0H87Z"></path><path id="MJX-5-TEX-N-2200" d="M0 673Q0 684 7 689T20 694Q32 694 38 680T82 567L126 451H430L473 566Q483 593 494 622T512 668T519 685Q524 694 538 694Q556 692 556 674Q556 670 426 329T293 -15Q288 -22 278 -22T263 -15Q260 -11 131 328T0 673ZM414 410Q414 411 278 411T142 410L278 55L414 410Z"></path><path id="MJX-5-TEX-SS-1D5AC" d="M92 0V694H228L233 680Q236 675 284 547T382 275T436 106Q446 149 497 292T594 558L640 680L645 694H782V0H689V305L688 606Q688 577 500 78L479 23H392L364 96Q364 97 342 156T296 280T246 418T203 544T186 609V588Q185 568 185 517T185 427T185 305V0H92Z"></path><path id="MJX-5-TEX-SO-222B" d="M113 -244Q113 -246 119 -251T139 -263T167 -269Q186 -269 199 -260Q220 -247 232 -218T251 -133T262 -15T276 155T297 367Q300 390 305 438T314 512T325 580T340 647T361 703T390 751T428 784T479 804Q481 804 488 804T501 805Q552 802 581 769T610 695Q610 669 594 657T561 645Q542 645 527 658T512 694Q512 705 516 714T526 729T538 737T548 742L552 743Q552 745 545 751T525 762T498 768Q475 768 460 756T434 716T418 652T407 559T398 444T387 300T369 133Q349 -38 337 -102T303 -207Q256 -306 169 -306Q119 -306 87 -272T55 -196Q55 -170 71 -158T104 -146Q123 -146 138 -159T153 -195Q153 -206 149 -215T139 -230T127 -238T117 -242L113 -244Z"></path></defs><g stroke="currentcolor" fill="currentcolor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mstyle"><g data-mml-node="TeXAtom" data-mjx-texclass="ORD"><g data-mml-node="mi"><use data-c="1D5A5" xlink:href="#MJX-5-TEX-SS-1D5A5"></use></g></g><g data-mml-node="mspace" transform="translate(569,0)"></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(469,0)"><g data-mml-node="mi"><use data-c="1D5AB" xlink:href="#MJX-5-TEX-SS-1D5AB"></use></g></g><g data-mml-node="mspace" transform="translate(1011,0)"></g><g data-mml-node="mpadded" transform="translate(651,0)"><g transform="translate(0,23)"><g data-mml-node="mi"><use data-c="2200" xlink:href="#MJX-5-TEX-N-2200"></use></g></g></g><g data-mml-node="mspace" transform="translate(1207,0)"></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(1097,0)"><g data-mml-node="mi"><use data-c="1D5AC" xlink:href="#MJX-5-TEX-SS-1D5AC"></use></g></g><g data-mml-node="mspace" transform="translate(1972,0)"></g><g data-mml-node="mo" transform="translate(1638.7,0) translate(0 0.5)"><use data-c="222B" xlink:href="#MJX-5-TEX-SO-222B"></use></g></g></g></g></svg>
180 </GridItem>
184 <GridItem>
185 <div style="width:calc(100% - 20px);text-align:right;padding:10px">
186 {user_field().into_any()}
187 </div>
188 </GridItem>
189 </Grid>
190 <Divider/>
191 </div>
192 </LayoutHeader>
193 <Layout position=LayoutPosition::Absolute class="flams-main" content_style="height:100%" has_sider=true>
194 <LayoutSider class="flams-menu" content_style="width:100%;height:100%">
195 {side_menu(page).into_any()}
196 </LayoutSider>
197 <Layout>
198 <div style="width:calc(100% - 10px);padding-left:5px;height:calc(100vh - 67px)">
199 {do_main(page).into_any()}
200 </div>
201 </Layout>
202 </Layout>
203 </Layout>
205 }
206 })).into_any()
207}
208
209fn do_main(page: Page) -> AnyView {
210 let inner = || match page {
212 Page::Home => view!(<flams_router_backend::index_components::Index/>).into_any(),
213 Page::MathHub => view! {<flams_router_backend::components::ArchivesTop/>}.into_any(),
214 Page::Log => view! {<flams_router_logging::Logger/>}.into_any(),
216 Page::Queue => view! {<flams_router_buildqueue_components::QueuesTop/>}.into_any(),
217 Page::Query => view! {<query::Query/>}.into_any(),
218 Page::Settings => view! {<settings::Settings/>}.into_any(),
219 Page::MyArchives => view! {<flams_router_git_components::Archives/>}.into_any(),
220 Page::Search => view! {<flams_router_search::components::SearchTop/>}.into_any(),
221 Page::FloDown => view! {<flams_flodown::FloDownEditor/>}.into_any(),
222 Page::Users => view! {<flams_router_login::components::Users/>}.into_any(),
223 _ => view!(<span>"TODO"</span>).into_any(),
224 };
226 view!(<main style="height:100%">{inner()}</main>).into_any()
227}
228
229#[component]
230fn NotFound() -> AnyView {
231 #[cfg(feature = "ssr")]
232 {
233 let resp = expect_context::<leptos_axum::ResponseOptions>();
234 resp.set_status(axum::http::StatusCode::NOT_FOUND);
235 }
236
237 view! {
238 <h3>"Not Found"</h3>
239 }
240 .into_any()
241}
242
243fn side_menu(page: Page) -> AnyView {
244 use thaw::{NavDrawer, NavItem};
245 view! {
246 <NavDrawer selected_value=page.to_string() class="flams-menu-inner">
247 <NavItem value="home" href="/">"Home"</NavItem>
248 <NavItem value="mathhub" href="/dashboard/mathhub">"MathHub"</NavItem>
249 <NavItem value="query" href="/dashboard/query">"Queries"</NavItem>
250 <NavItem value="search" href="/dashboard/search">"Search Content"</NavItem>
251 {move || {let s = LoginState::get(); match s {
252 LoginState::NoAccounts => view!{
253 <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
254 <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
255 <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
256 <NavItem value="flodown" href="/dashboard/flodown">"FloDown"</NavItem>
257 }.into_any(),
258 LoginState::Admin => view!{
259 <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
260 <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
261 <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
262 <NavItem value="flodown" href="/dashboard/flodown">"FloDown"</NavItem>
263 <NavItem value="users" href="/dashboard/users">"Manage Users"</NavItem>
264 }.into_any(),
265 LoginState::User{is_admin:true,..} => view!{
266 <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
267 <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
268 <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
269 <NavItem value="archives" href="/dashboard/archives">"My Archives"</NavItem>
270 <NavItem value="flodown" href="/dashboard/flodown">"FloDown"</NavItem>
271 }.into_any(),
272 LoginState::User{..} => view!{
273 <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
274 <NavItem value="archives" href="/dashboard/archives">"My Archives"</NavItem>
275 <NavItem value="flodown" href="/dashboard/flodown">"FloDown"</NavItem>
276 }.into_any(),
277 LoginState::None | LoginState::Loading => ().into_any()
278 }}}
279 </NavDrawer>
280 }
281 .into_any()
282}
283
284fn user_field() -> AnyView {
285 use flams_web_utils::components::ClientOnly;
286 use flams_web_utils::components::{Spinner, SpinnerSize};
287 use thaw::{Menu, MenuItem, MenuPosition, MenuTrigger, MenuTriggerType};
288
289 view! {<div class="flams-user-menu-trigger">{
291 let theme = expect_context::<RwSignal<thaw::Theme>>();
292 let on_select = move |key: &'static str| match key {
293 "theme" => {
294 theme.update(|v| {
295 if v.name == "dark" {
296 *v = thaw::Theme::light();
297 } else {
298 *v = thaw::Theme::dark();
299 }
300 });
301 }
302 _ => unreachable!(),
303 };
304 let src = Memo::new(|_| match LoginState::get() {
305 LoginState::User { avatar, .. } => Some(avatar),
306 LoginState::Admin => Some("/admin.png".to_string()),
307 _ => None,
308 });
309 let icon = Memo::new(move |_| if theme.with(|v| v.name == "dark")
310 {icondata_bi::BiSunRegular} else {icondata_bi::BiMoonSolid}
311 );
312 let text = Memo::new(move |_| if theme.with(|v| v.name == "dark")
313 {"Light Mode"} else {"Dark Mode"}
314 );
315 view!{
316 <Menu on_select trigger_type=MenuTriggerType::Hover position=MenuPosition::Bottom>
317 <MenuTrigger slot>
318 <thaw::Avatar src />
319 </MenuTrigger>
320 <MenuItem value="theme" icon=icon>{text}</MenuItem>
322 <Divider/>
323 {move || match LoginState::get() {
324 LoginState::None => login_form().into_any(),
325 LoginState::NoAccounts => view!(<span>"Admin"</span>).into_any(),
326 LoginState::Admin => logout_form("admin".to_string()).into_any(),
327 LoginState::User{name,..} => logout_form(name).into_any(),
328 LoginState::Loading => view!(<Spinner size=SpinnerSize::Tiny/>).into_any()
329 }}
330 </Menu>
331 }
332 }</div>
333 }
335 .into_any()
336}
337
338fn logout_form(user: String) -> AnyView {
339 use thaw::Button;
340 let login = expect_context::<RwSignal<LoginState>>();
341 let action = Action::new(move |_| {
342 login.set(LoginState::None);
343 flams_router_login::server_fns::logout()
344 });
345 view!(<span>{user}" "<Button on_click=move |_| {action.dispatch(());}>Logout</Button></span>)
346 .into_any()
347}
348
349fn login_form() -> AnyView {
350 use thaw::{Button, Input, InputType};
351 let login = expect_context();
352 let action = Action::new(move |pwd: &String| do_login(pwd.clone(), login));
353 let value = RwSignal::<String>::new(String::new());
354 view! {
355 <Button on_click=move |_| {action.dispatch(value.get_untracked());}>Login</Button>
356 <Input placeholder="admin pwd" value input_type=InputType::Password/>
357 }
358 .into_any()
359}
360
361#[allow(unused_variables)]
362async fn do_login(pw: String, login: RwSignal<LoginState>) {
363 let pwd = if pw.is_empty() { None } else { Some(pw) };
364 match flams_router_login::server_fns::login(pwd).await {
365 Ok(Some(u @ (LoginState::Admin | LoginState::User { .. }))) => login.set(u),
366 Ok(_) => (),
367 Err(e) => {
368 #[cfg(feature = "hydrate")]
369 flams_web_utils::components::display_error(std::borrow::Cow::Owned(format!(
370 "Error: {e}"
371 )));
372 }
373 }
374 let _ = view!(<Redirect path="/dashboard"/>);
375}