1#![recursion_limit = "256"]
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_router_base::ws::*;
14 pub use flams_router_buildqueue_components::QueueSocket;
15 pub use flams_router_logging::LogSocket;
16}
17
18pub mod server_fns {
19 pub mod content {
20 pub use flams_router_content::server_fns::*;
21 }
22 pub mod backend {
23 pub use flams_router_backend::server_fns::*;
24 }
25 pub mod buildqueue {
26 pub use flams_router_buildqueue_base::server_fns::*;
27 }
28 pub mod git {
29 pub use flams_router_git_base::server_fns::*;
30 }
31 pub mod login {
32 pub use flams_router_login::server_fns::*;
33 }
34 pub mod search {
35 pub use flams_router_search::{search_query, search_symbols};
36 }
37 pub use super::query::query_api as query;
38 pub use super::settings::{get_settings as settings, reload};
39}
40
41pub use flams_router_base::LoginState;
42use leptos::{
43 either::{Either, EitherOf4},
44 prelude::*,
45};
46use leptos_meta::{Stylesheet, Title, provide_meta_context};
47use leptos_router::{
48 components::{Outlet, ParentRoute, Redirect, Route, Router, Routes},
49 hooks::use_query_map,
50 path,
51};
52use thaw::{Divider, Grid, GridItem, Layout, LayoutHeader, LayoutPosition, LayoutSider};
53
54#[component]
55pub fn Main() -> impl IntoView {
56 provide_meta_context();
57 #[cfg(feature = "ssr")]
58 provide_context(flams_web_utils::CssIds::default());
59 view! {
60 <Title text="𝖥𝖫∀𝖬∫"/>
61 <Router>{
62 let params = use_query_map();
63 let has_params = move || params.with(|p| p.get_str("a").is_some() || p.get_str("uri").is_some());
64 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/>)/>
69 <Route path=path!("log") view=|| view!(<MainPage page=Page::Log/>)/>
71 <Route path=path!("queue") view=|| view!(<MainPage page=Page::Queue/>)/>
72 <Route path=path!("settings") view=|| view!(<MainPage page=Page::Settings/>)/>
73 <Route path=path!("query") view=|| view!(<MainPage page=Page::Query/>)/>
74 <Route path=path!("archives") view=|| view!(<MainPage page=Page::MyArchives/>)/>
75 <Route path=path!("users") view=|| view!(<MainPage page=Page::Users/>)/>
76 <Route path=path!("search") view=|| view!(<MainPage page=Page::Search/>)/>
77 <Route path=path!("") view=|| view!(<MainPage page=Page::Home/>)/>
78 <Route path=path!("*any") view=|| view!(<MainPage page=Page::NotFound/>)/>
79 </ParentRoute>
80 <ParentRoute path=path!("/vscode") view= flams_router_vscode::VSCodeWrap>
81 <Route path=path!("search") view=flams_router_search::vscode::VSCodeSearch/>
82 </ParentRoute>
83 <Route path=path!("/document") view={move || {
84 use flams_router_content::components::{DocumentOfTop,DocumentOfTopProps};
85 let params = params.get();
86 if let Some(p) = params.get_str("uri") {
87 let Ok(uri) = <flams_ontology::uris::URI as std::str::FromStr>::from_str(p) else {
88 return Either::Right(view! { <Redirect path="/dashboard"/> })
89 };
90 Either::Left(DocumentOfTop(DocumentOfTopProps{uri}))
91 } else {
92 Either::Right(view! { <Redirect path="/dashboard"/> })
93 }
94 }}/>
95 <Route path=path!("/") view={move || if has_params() {
96 Either::Left(view! { <flams_router_content::components::URITop/> })
97 } else {
98 Either::Right(view! { <Redirect path="/dashboard"/> })
99 }}
100 />
101 </ParentRoute>
102 </Routes>}
103 }</Router>
104 }
105}
106
107#[component(transparent)]
108fn Top() -> impl IntoView {
109 use flams_router_login::components::LoginProvider;
110 view! {<LoginProvider><leptos_router::components::Outlet/></LoginProvider>}
111}
112
113#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
114enum Page {
115 Home,
116 MathHub,
117 Log,
119 NotFound,
120 Queue,
121 Settings,
122 Login,
123 Query,
124 Search,
125 MyArchives,
126 Users,
127}
128impl Page {
129 pub const fn key(self) -> &'static str {
130 use Page::*;
131 match self {
132 Home => "home",
133 MathHub => "mathhub",
134 Log => "log",
136 Login => "login",
137 Queue => "queue",
138 Settings => "settings",
139 Query => "query",
140 MyArchives => "archives",
141 Search => "search",
142 Users => "users",
143 NotFound => "notfound",
144 }
145 }
146}
147impl std::fmt::Display for Page {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.write_str(self.key())
150 }
151}
152
153#[component(transparent)]
154pub fn Dashboard() -> impl IntoView {
155 view! {
156 <Stylesheet id="leptos" href="/pkg/flams.css"/>
157 <Outlet/>
158 }
159}
160
161#[component]
162fn MainPage(page: Page) -> impl IntoView {
163 use flams_web_utils::components::Themer;
164 use ftml_viewer_components::FTMLGlobalSetup;
165 view! {
166 <Themer><FTMLGlobalSetup>
167 <Layout position=LayoutPosition::Absolute>
168 <LayoutHeader class="flams-header">
170 <div style="width:100%">
171 <Grid cols=3>
172 <GridItem>""</GridItem>
173 <GridItem>
174 <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>
175 </GridItem>
179 <GridItem>
180 <div style="width:calc(100% - 20px);text-align:right;padding:10px">
181 {user_field()}
182 </div>
183 </GridItem>
184 </Grid>
185 <Divider/>
186 </div>
187 </LayoutHeader>
188 <Layout position=LayoutPosition::Absolute class="flams-main" content_style="height:100%" has_sider=true>
189 <LayoutSider class="flams-menu" content_style="width:100%;height:100%">
190 {side_menu(page)}
191 </LayoutSider>
192 <Layout>
193 <div style="width:calc(100% - 10px);padding-left:5px;height:calc(100vh - 67px)">
194 {do_main(page)}
195 </div>
196 </Layout>
197 </Layout>
198 </Layout>
200 </FTMLGlobalSetup></Themer>
201 }
202}
203
204fn do_main(page: Page) -> impl IntoView {
205 use leptos::either::EitherOf10::*;
206 let inner = || match page {
207 Page::Home => A(view!(<flams_router_backend::index_components::Index/>)),
208 Page::MathHub => B(view! {<flams_router_backend::components::ArchivesTop/>}),
209 Page::Log => C(view! {<flams_router_logging::Logger/>}),
211 Page::Queue => D(view! {<flams_router_buildqueue_components::QueuesTop/>}),
212 Page::Query => E(view! {<query::Query/>}),
213 Page::Settings => F(view! {<settings::Settings/>}),
214 Page::MyArchives => G(view! {<flams_router_git_components::Archives/>}),
215 Page::Search => H(view! {<flams_router_search::components::SearchTop/>}),
216 Page::Users => I(view! {<flams_router_login::components::Users/>}),
217 _ => J(view!(<span>"TODO"</span>)),
218 };
220 view!(<main style="height:100%">{inner()}</main>)
221}
222
223#[component]
224fn NotFound() -> impl IntoView {
225 #[cfg(feature = "ssr")]
226 {
227 let resp = expect_context::<leptos_axum::ResponseOptions>();
228 resp.set_status(axum::http::StatusCode::NOT_FOUND);
229 }
230
231 view! {
232 <h3>"Not Found"</h3>
233 }
234}
235
236fn side_menu(page: Page) -> impl IntoView {
237 use thaw::{NavDrawer, NavItem};
238 view! {
239 <NavDrawer selected_value=page.to_string() class="flams-menu-inner">
240 <NavItem value="home" href="/">"Home"</NavItem>
241 <NavItem value="mathhub" href="/dashboard/mathhub">"MathHub"</NavItem>
242 <NavItem value="query" href="/dashboard/query">"Queries"</NavItem>
243 <NavItem value="search" href="/dashboard/search">"Search Content"</NavItem>
244 {move || {let s = LoginState::get(); match s {
245 LoginState::NoAccounts => leptos::either::EitherOf5::A(view!{
246 <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
247 <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
248 <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
249 }),
250 LoginState::Admin => leptos::either::EitherOf5::B(view!{
251 <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
252 <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
253 <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
254 <NavItem value="users" href="/dashboard/users">"Manage Users"</NavItem>
255 }),
256 LoginState::User{is_admin:true,..} => leptos::either::EitherOf5::C(view!{
257 <NavItem value="log" href="/dashboard/log">"Logs"</NavItem>
258 <NavItem value="settings" href="/dashboard/settings">"Settings"</NavItem>
259 <NavItem value="queue" href="/dashboard/queue">"Queue"</NavItem>
260 <NavItem value="archives" href="/dashboard/archives">"My Archives"</NavItem>
261 }),
262 LoginState::User{..} => leptos::either::EitherOf5::D(view!{
263 <NavItem value="archives" href="/dashboard/archives">"My Archives"</NavItem>
264 }),
265 LoginState::None | LoginState::Loading => leptos::either::EitherOf5::E(())
266 }}}
267 </NavDrawer>
268 }
269}
270
271fn user_field() -> impl IntoView {
272 use flams_web_utils::components::ClientOnly;
273 use flams_web_utils::components::{Spinner, SpinnerSize};
274 use thaw::{Menu, MenuItem, MenuPosition, MenuTrigger, MenuTriggerType};
275
276 view! {<div class="flams-user-menu-trigger">{
278 let theme = expect_context::<RwSignal<thaw::Theme>>();
279 let on_select = move |key: &'static str| match key {
280 "theme" => {
281 theme.update(|v| {
282 if v.name == "dark" {
283 *v = thaw::Theme::light();
284 } else {
285 *v = thaw::Theme::dark();
286 }
287 });
288 }
289 _ => unreachable!(),
290 };
291 let src = Memo::new(|_| match LoginState::get() {
292 LoginState::User { avatar, .. } => Some(avatar),
293 LoginState::Admin => Some("/admin.png".to_string()),
294 _ => None,
295 });
296 let icon = Memo::new(move |_| if theme.with(|v| v.name == "dark")
297 {icondata_bi::BiSunRegular} else {icondata_bi::BiMoonSolid}
298 );
299 let text = Memo::new(move |_| if theme.with(|v| v.name == "dark")
300 {"Light Mode"} else {"Dark Mode"}
301 );
302 view!{
303 <Menu on_select trigger_type=MenuTriggerType::Hover position=MenuPosition::Bottom>
304 <MenuTrigger slot>
305 <thaw::Avatar src />
306 </MenuTrigger>
307 <MenuItem value="theme" icon=icon>{text}</MenuItem>
309 <Divider/>
310 {move || match LoginState::get() {
311 LoginState::None => EitherOf4::A(login_form()),
312 LoginState::NoAccounts => EitherOf4::B(view!(<span>"Admin"</span>)),
313 LoginState::Admin => EitherOf4::C(logout_form("admin".to_string())),
314 LoginState::User{name,..} => EitherOf4::C(logout_form(name)),
315 LoginState::Loading => EitherOf4::D(view!(<Spinner size=SpinnerSize::Tiny/>))
316 }}
317 </Menu>
318 }
319 }</div>
320 }
322}
323
324fn logout_form(user: String) -> impl IntoView {
325 use thaw::Button;
326 let login = expect_context::<RwSignal<LoginState>>();
327 let action = Action::new(move |_| {
328 login.set(LoginState::None);
329 flams_router_login::server_fns::logout()
330 });
331 view!(<span>{user}" "<Button on_click=move |_| {action.dispatch(());}>Logout</Button></span>)
332}
333
334fn login_form() -> impl IntoView {
335 use thaw::{Button, Input, InputType};
336 let login = expect_context();
337 let action = Action::new(move |pwd: &String| do_login(pwd.clone(), login));
338 let value = RwSignal::<String>::new(String::new());
339 view! {
340 <Button on_click=move |_| {action.dispatch(value.get_untracked());}>Login</Button>
341 <Input placeholder="admin pwd" value input_type=InputType::Password/>
342 }
343}
344
345#[allow(unused_variables)]
346async fn do_login(pw: String, login: RwSignal<LoginState>) {
347 let pwd = if pw.is_empty() { None } else { Some(pw) };
348 match flams_router_login::server_fns::login(pwd).await {
349 Ok(Some(u @ (LoginState::Admin | LoginState::User { .. }))) => login.set(u),
350 Ok(_) => (),
351 Err(e) => {
352 #[cfg(feature = "hydrate")]
353 flams_web_utils::components::display_error(std::borrow::Cow::Owned(format!(
354 "Error: {e}"
355 )));
356 }
357 }
358 let _ = view!(<Redirect path="/dashboard"/>);
359}