1#![recursion_limit = "512"]
2#![allow(clippy::must_use_candidate)]
3
4pub mod query;
13mod settings;
14
15pub mod ws {
16 pub use flams_router_base::ws::*;
20 pub use flams_router_buildqueue_components::QueueSocket;
21 pub use flams_router_logging::LogSocket;
22}
23
24pub mod server_fns {
25 pub mod content {
26 pub use flams_router_content::server_fns::*;
27 }
28 pub mod backend {
29 pub use flams_router_backend::server_fns::*;
30 }
31 pub mod buildqueue {
32 pub use flams_router_buildqueue_base::server_fns::*;
33 }
34 pub mod git {
35 pub use flams_router_git_base::server_fns::*;
36 }
37 pub mod login {
38 pub use flams_router_login::server_fns::*;
39 }
40 pub mod search {
41 pub use flams_router_search::{search_query, search_symbols};
42 }
43 pub use super::query::query_api as query;
44 pub use super::settings::{get_settings as settings, reload};
45}
46
47pub use flams_router_base::LoginState;
48use flams_router_base::maybe_lazy;
49use flams_web_utils::components::{Layout, LayoutHeader, LayoutSider, wait_and_then_fn};
50use ftml_component_utils::{Divider, Grid, GridItem, inject_css_after};
51use ftml_components::config::AllowSubterms;
52use ftml_dom::FtmlViews;
53use ftml_uris::DocumentUri;
54use leptos::prelude::*;
55use leptos_meta::{Title, provide_meta_context};
56use leptos_router::{
57 components::{Outlet, ParentRoute, Redirect, Route, Router, Routes},
58 path,
59};
60
61#[component]
62pub fn Main() -> AnyView {
63 provide_meta_context();
64 let (_, set_is_routing) = signal(false);
65 view! {
66 <Title text="𝖥𝖫∀𝖬∫"/>
67 <Router set_is_routing>{
68 view!{<Routes fallback=|| NotFound()>
70 <ParentRoutepath=() view=Top>
71 <ParentRoute path=path!("/dashboard") view=Dashboard>
72 <ParentRoute path=path!("mathhub") view={|| main_page(Page::MathHub)}>
73 <Route path=path!("") view={maybe_lazy!(flams_router_backend::components::ArchivesTop)}/>
74 </ParentRoute>
75 <ParentRoute path=path!("log") view={|| main_page(Page::Log)}>
77 <Route path=path!("") view={maybe_lazy!(flams_router_logging::Logger)}/>
78 </ParentRoute>
79 <ParentRoute path=path!("queue") view={|| main_page(Page::Queue)}>
80 <Route path=path!("") view={maybe_lazy!(flams_router_buildqueue_components::QueuesTop)}/>
81 </ParentRoute>
82 <ParentRoute path=path!("settings") view={|| main_page(Page::Settings)}>
83 <Route path=path!("") view={maybe_lazy!(settings::Settings)}/>
84 </ParentRoute>
85 <ParentRoute path=path!("query") view={|| main_page(Page::Query)}>
86 <Route path=path!("") view={maybe_lazy!(query::Query)}/>
87 </ParentRoute>
88 <ParentRoute path=path!("archives") view={|| main_page(Page::MyArchives)}>
89 <Route path=path!("") view={maybe_lazy!(flams_router_git_components::Archives)}/>
90 </ParentRoute>
91 <ParentRoute path=path!("users") view={|| main_page(Page::Users)}>
92 <Route path=path!("") view={maybe_lazy!(flams_router_login::components::Users)}/>
93 </ParentRoute>
94 <ParentRoute path=path!("search") view={|| main_page(Page::Search)}>
95 <Route path=path!("") view={maybe_lazy!(flams_router_search::components::SearchTop)}/>
96 </ParentRoute>
97 <ParentRoute path=path!("") view={|| main_page(Page::Home)}>
101 <Route path=path!("") view={maybe_lazy!(Home)}/>
102 </ParentRoute>
103 <ParentRoute path=path!("*any") view={|| main_page(Page::NotFound)}>
104 <Route path=path!("") view=||view!(<NotFound/>)/>
105 </ParentRoute>
106 </ParentRoute>
107 <ParentRoute path=path!("/vscode") view={maybe_lazy!(flams_router_vscode::VSCWrap)}><Route path=path!("search") view={maybe_lazy!(flams_router_search::vscode::VSCSearch)}/>
109 <Route path=path!("proofs") view={maybe_lazy!(flams_router_vscode::checks::Checks)}/>
110 </ParentRoute>
111 <Route path=path!("/document") view={maybe_lazy!(flams_router_content::components::TopDocRouter)}/>
112 <Route path=path!("/") view={maybe_lazy!(flams_router_content::components::UriTopRouter)}/>
113 </ParentRoute>
114 </Routes>}
115 }</Router>
116 }.into_any()
117}
118
119maybe_lazy!(Home = home());
120
121fn home() -> AnyView {
122 let index = flams_router_backend::index_components::index();
123 let header = wait_and_then_fn(get_home, |(css, top)| {
124 if let Some(css) = css {
125 inject_css_after("home-main", css);
126 }
127 top.map(|top| {
128 let d = ftml_dom::global_setup(move || {
129 flams_router_content::Views::top(move || {
130 flams_router_content::Views::setup_document(
131 DocumentUri::no_doc().clone(),
132 ftml_components::SidebarPosition::None,
133 true,
134 ftml_dom::toc::TocSource::None,
135 move || flams_router_content::Views::render_ftml(top, None).into_any(),
136 )
137 })
138 });
139 view! {
140 {d}
141 <hr/>
142 }
143 })
144 .into_any()
145 });
146 view! {
147 {header}
148 {index}
149 }
150 .into_any()
151}
152
153#[cfg(feature = "ssr")]
154static MAIN_DIRS: std::sync::LazyLock<(
155 std::path::PathBuf,
156 std::path::PathBuf,
157 std::path::PathBuf,
158)> = std::sync::LazyLock::new(|| {
159 let ce = std::env::current_exe().expect("failed to determine current executable path");
160 let cedir = ce
161 .parent()
162 .expect("failed to determine current executable path");
163 let main_css = cedir.join("templates/main.css");
164 let main_html = cedir.join("templates/main.html");
165 let logo = cedir.join("templates/logo.html");
166 (main_css, main_html, logo)
167});
168
169#[server]
170pub async fn get_logo() -> Result<Option<String>, ServerFnError<String>> {
171 let (_, _, logo) = &*MAIN_DIRS;
172 Ok(
173 if logo.exists()
174 && let Ok(s) = std::fs::read_to_string(logo)
175 {
176 Some(s)
177 } else {
178 None
179 },
180 )
181}
182
183#[server]
184pub async fn get_home() -> Result<(Option<String>, Option<String>), ServerFnError<String>> {
185 let (css, main, _) = &*MAIN_DIRS;
186 let css = if css.exists() {
187 Some(
188 tokio::fs::read_to_string(css)
189 .await
190 .map_err(|e| ServerFnError::from(e.to_string()))?,
191 )
192 } else {
193 None
194 };
195 let html = if main.exists() {
196 Some(
197 tokio::fs::read_to_string(main)
198 .await
199 .map_err(|e| ServerFnError::from(e.to_string()))?,
200 )
201 } else {
202 None
203 };
204 Ok((css, html))
205}
206
207#[component(transparent)]
208fn Top() -> AnyView {
209 use flams_router_login::components::LoginProvider;
210 provide_context(AllowSubterms(true));
211 view!(<LoginProvider><leptos_router::components::Outlet/></LoginProvider>).into_any()
212}
213
214#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
215enum Page {
216 Home,
217 MathHub,
218 Log,
220 NotFound,
221 Queue,
222 Settings,
223 Login,
224 Query,
225 Search,
226 MyArchives,
228 Users,
229}
230impl Page {
231 pub const fn key(self) -> &'static str {
232 match self {
233 Self::Home => "home",
234 Self::MathHub => "mathhub",
235 Self::Log => "log",
237 Self::Login => "login",
238 Self::Queue => "queue",
239 Self::Settings => "settings",
240 Self::Query => "query",
241 Self::MyArchives => "archives",
242 Self::Search => "search",
243 Self::Users => "users",
245 Self::NotFound => "notfound",
246 }
247 }
248}
249impl std::fmt::Display for Page {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 f.write_str(self.key())
252 }
253}
254
255#[component(transparent)]
256pub fn Dashboard() -> impl IntoView {
257 view! {
258 <Outlet/>
259 }
260}
261
262fn main_page(page: Page) -> AnyView {
263 fn logo_default() -> impl IntoView {
264 view! {
265 <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)">
266 <defs>
267 <path id="MJX-5-TEX-SS-1D5A5" d="M86 0V691H526V611H358L190 612V384H485V308H190V0H86Z"></path>
268 <path id="MJX-5-TEX-SS-1D5AB" d="M87 0V694H191V79L297 80H451L499 81V0H87Z"></path>
269 <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>
270 <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>
271 <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>
272 </defs>
273 <g stroke="currentcolor" fill="currentcolor" stroke-width="0" transform="scale(1,-1)">
274 <g data-mml-node="math">
275 <g data-mml-node="mstyle">
276 <g data-mml-node="TeXAtom" data-mjx-texclass="ORD">
277 <g data-mml-node="mi">
278 <use data-c="1D5A5" xlink:href="#MJX-5-TEX-SS-1D5A5"></use>
279 </g>
280 </g>
281 <g data-mml-node="mspace" transform="translate(569,0)"></g>
282 <g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(469,0)">
283 <g data-mml-node="mi">
284 <use data-c="1D5AB" xlink:href="#MJX-5-TEX-SS-1D5AB"></use>
285 </g>
286 </g>
287 <g data-mml-node="mspace" transform="translate(1011,0)"></g>
288 <g data-mml-node="mpadded" transform="translate(651,0)">
289 <g transform="translate(0,23)">
290 <g data-mml-node="mi">
291 <use data-c="2200" xlink:href="#MJX-5-TEX-N-2200"></use>
292 </g>
293 </g>
294 </g>
295 <g data-mml-node="mspace" transform="translate(1207,0)"></g>
296 <g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(1097,0)">
297 <g data-mml-node="mi">
298 <use data-c="1D5AC" xlink:href="#MJX-5-TEX-SS-1D5AC"></use>
299 </g>
300 </g>
301 <g data-mml-node="mspace" transform="translate(1972,0)"></g>
302 <g data-mml-node="mo" transform="translate(1638.7,0) translate(0 0.5)">
303 <use data-c="222B" xlink:href="#MJX-5-TEX-SO-222B"></use>
304 </g>
305 </g>
306 </g>
307 </g>
308 </svg>}
309 }
310
311 let logo = wait_and_then_fn(get_logo, |logo| {
312 logo.map_or_else(
313 || leptos::either::Either::Left(logo_default()),
314 |s| leptos::either::Either::Right(flams_router_content::Views::render_ftml(s, None)),
315 )
316 .into_any()
317 });
318
319 ftml_dom::global_setup(move || {
320 flams_router_content::Views::top(move || {
321 view! {
322 <div style="width:100vw;height:100vh;"><Layout>
323 <LayoutHeader style="text-align:center;" slot>
324 <div style="width:100%">
325 <Grid cols=3>
326 <GridItem>""</GridItem>
327 <GridItem>{logo}</GridItem>
328 <GridItem>
329 <div style="width:calc(100% - 20px);text-align:right;padding:10px">
330 {user_field().into_any()}
331 </div>
332 </GridItem>
333 </Grid>
334 <Divider/>
335 </div>
336 </LayoutHeader>
337 <LayoutSider class="flams-menu" slot>
338 {side_menu(page)}
339 </LayoutSider>
340 <Outlet/>
341 </Layout></div>
342 }
343 })
344 })
345 .into_any()
346}
347
348#[component]
349fn NotFound() -> AnyView {
350 #[cfg(feature = "ssr")]
351 {
352 let resp = expect_context::<leptos_axum::ResponseOptions>();
353 resp.set_status(axum::http::StatusCode::NOT_FOUND);
354 }
355
356 view! {
357 <h3>"Not Found"</h3>
358 }
359 .into_any()
360}
361
362fn side_menu(page: Page) -> AnyView {
363 use ftml_component_utils::{NavDrawer, NavItem};
364 view! {
365 <NavDrawer selected_value=page.to_string() class="flams-menu-inner">
366 <NavItem value="home" href="/">"Home"</NavItem>
367 <NavItem value="mathhub" href="/dashboard/mathhub">"MathHub"</NavItem>
368 <NavItem value="query" href="/dashboard/query">"Queries"</NavItem>
369 <NavItem value="search" href="/dashboard/search">"Search Content"</NavItem>
370 {move || {let s = LoginState::get(); match s {
371 LoginState::NoAccounts => view!{
372 <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
373 <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
374 <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
375 }.into_any(),
377 LoginState::Admin => view!{
378 <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
379 <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
380 <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
381 <NavItem value="users" href="/dashboard/users">"Manage Users"</NavItem>
383 }.into_any(),
384 LoginState::User{is_admin:true,..} => view!{
385 <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
386 <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
387 <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
388 <NavItem value="archives" href="/dashboard/archives">"My Archives"</NavItem>
389 }.into_any(),
391 LoginState::User{..} => view!{
392 <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
393 <NavItem value="archives" href="/dashboard/archives">"My Archives"</NavItem>
394 }.into_any(),
396 LoginState::None | LoginState::Loading => ().into_any()
397 }}}
398 </NavDrawer>
399 }
400 .into_any()
401}
402
403fn user_field() -> AnyView {
404 use ftml_component_utils::{
405 Menu, MenuItem, MenuPosition, MenuTrigger, MenuTriggerType, Spinner, theming::Theme,
406 };
407
408 view! {<div class="flams-user-menu-trigger">{
410 let theme = expect_context::<RwSignal<Theme>>();
411 let on_select = move |key: &'static str| match key {
412 "theme" => {
413 theme.update(|v| {
414 if v.name == "dark" {
415 *v = Theme::light();
416 } else {
417 *v = Theme::dark();
418 }
419 });
420 }
421 _ => unreachable!(),
422 };
423 let src = Memo::new(|_| match LoginState::get() {
424 LoginState::User { avatar, .. } => Some(avatar),
425 LoginState::Admin => Some("/admin.png".to_string()),
426 _ => None,
427 });
428 let icon = Memo::new(move |_| if theme.with(|v| v.name == "dark")
429 {icondata_bi::BiSunRegular} else {icondata_bi::BiMoonSolid}
430 );
431 let text = Memo::new(move |_| if theme.with(|v| v.name == "dark")
432 {"Light Mode"} else {"Dark Mode"}
433 );
434 view!{
435 <Menu on_select trigger_type=MenuTriggerType::Hover position=MenuPosition::Bottom>
436 <MenuTrigger slot>
437 <ftml_component_utils::Avatar src />
438 </MenuTrigger>
439 <MenuItem value="theme" icon=icon>{text}</MenuItem>
441 <Divider/>
442 {move || match LoginState::get() {
443 LoginState::None => login_form().into_any(),
444 LoginState::NoAccounts => view!(<span>"Admin"</span>).into_any(),
445 LoginState::Admin => logout_form("admin".to_string()).into_any(),
446 LoginState::User{name,..} => logout_form(name).into_any(),
447 LoginState::Loading => view!(<Spinner small=true/>).into_any()
448 }}
449 </Menu>
450 }
451 }</div>
452 }
454 .into_any()
455}
456
457fn logout_form(user: String) -> AnyView {
458 use ftml_component_utils::Button;
459 let login = expect_context::<RwSignal<LoginState>>();
460 let action = Action::new(move |_| {
461 login.set(LoginState::None);
462 flams_router_login::server_fns::logout()
463 });
464 view!(<span>{user}" "<Button on_click=move |_| {action.dispatch(());}>Logout</Button></span>)
465 .into_any()
466}
467
468fn login_form() -> AnyView {
469 use ftml_component_utils::{Button, Input, InputType};
470 let login = expect_context();
471 let action = Action::new(move |pwd: &String| do_login(pwd.clone(), login));
472 let value = RwSignal::<String>::new(String::new());
473 view! {
474 <Button on_click=move |_| {action.dispatch(value.get_untracked());}>Login</Button>
475 <Input placeholder="admin pwd" value input_type=InputType::Password/>
476 }
477 .into_any()
478}
479
480#[allow(unused_variables)]
481async fn do_login(pw: String, login: RwSignal<LoginState>) {
482 let pwd = if pw.is_empty() { None } else { Some(pw) };
483 match flams_router_login::server_fns::login(pwd).await {
484 Ok(Some(u @ (LoginState::Admin | LoginState::User { .. }))) => login.set(u),
485 Ok(_) => (),
486 Err(e) => {
487 #[cfg(feature = "hydrate")]
488 flams_web_utils::components::display_error(std::borrow::Cow::Owned(format!(
489 "Error: {e}"
490 )));
491 }
492 }
493 let _ = view!(<Redirect path="/dashboard"/>);
494}