1#![allow(non_snake_case)]
2use components::*;
3use leptos::*;
4use leptos_meta::*;
5use leptos_router::*;
6
7use super::{elements::*, pages::*, preferences::ProvidePreferences, session::*, *};
8
9pub const SIDEBAR_MIN_WIDTH: usize = 280;
10
11#[component]
12pub fn App() -> impl IntoView {
13 provide_meta_context();
15
16 let close_overlay_signal = create_rw_signal(CloseOverlays());
17 provide_context(close_overlay_signal);
18
19 let close_overlays = move |_| {
20 close_overlay_signal.update(|_| ());
21 };
22
23 let show_sidebar = create_rw_signal(ShowSidebar(true));
24 provide_context(show_sidebar);
25
26 view! {
27 <Stylesheet href=format!("/pkg/{LEPTOS_OUTPUT_NAME}.css") />
28 <Stylesheet href="/fa/css/all.css" />
29
30 <Link rel="shortcut icon" type_="image/ico" href="/favicon.svg" />
31 <Link href="https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet" />
32
33 <Meta name="viewport" content="width=device-width, initial-scale=1.0" />
34 <Meta name="apple-mobile-web-app-capable" content="yes" />
35
36 <Title text="TallyWeb" />
37
38 <ProvideMessageSystem />
39 <Router>
40 <main on:click=close_overlays>
41 <Routes>
42 <Route
43 path=""
44 ssr=SsrMode::Async
45 view=|| {
46 view! {
47 <ProvideSessionSignal>
48 <ProvideScreenSignal>
49 <ProvidePreferences>
50 <ProvideStore>
51 <ProvideCountableSignals>
52 <Outlet />
53 </ProvideCountableSignals>
54 </ProvideStore>
55 </ProvidePreferences>
56 </ProvideScreenSignal>
57 </ProvideSessionSignal>
58 }
59 }
60 >
61
62 <Route
63 path="/"
64 view=|| {
65 view! {
66 <Outlet />
67 <HomePage />
68 }
69 }
70 >
71
72 <Route path="" view=UnsetCountable />
73 <Route path=":key" view=SetCountable />
74 </Route>
75 <Route path="/edit" view=EditWindow>
76 <Route path=":key" view=move || view! { <EditCountableWindow /> } />
77 </Route>
78
79 <Route path="/preferences" view=move || view! { <PreferencesWindow /> } />
80
81 <Route
82 path="/change-username"
83 view=move || view! { <ChangeAccountInfo /> }
84 />
85 <Route path="/change-password" view=ChangePassword />
86 </Route>
87 <TestRoutes />
88 <Route path="/login" view=LoginPage />
89 <Route path="/create-account" view=CreateAccount />
90 <Route path="/*any" view=NotFound />
91 </Routes>
92 </main>
93 </Router>
94 }
95}
96
97#[component]
99fn NotFound() -> impl IntoView {
100 #[cfg(feature = "ssr")]
107 {
108 let resp = expect_context::<leptos_actix::ResponseOptions>();
111 resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
112 }
113
114 view! { <h1>"Not Found"</h1> }
115}
116
117#[component]
118fn RouteSidebar(children: ChildrenFn) -> impl IntoView {
119 let selection = expect_context::<SelectionSignal>();
120 let show_sidebar = expect_context::<RwSignal<ShowSidebar>>();
121 let screen = expect_context::<Screen>();
122
123 let sidebar_layout: Signal<SidebarLayout> = create_read_slice(screen.style, |s| (*s).into());
124
125 let sidebar_width = create_rw_signal(400);
126 provide_context(sidebar_width);
127
128 let section_width = create_memo(move |_| {
129 if show_sidebar().0 {
130 format!("calc(100vw - {}px)", sidebar_width())
131 } else {
132 String::from("100vw")
133 }
134 });
135
136 create_isomorphic_effect(move |_| {
137 if screen.style.get() != ScreenStyle::Big {
138 let sel_memo = create_read_slice(selection, |sel| sel.is_empty());
139 sel_memo.with(|sel| show_sidebar.update(|s| *s = ShowSidebar(*sel)));
140 }
141 });
142
143 let suppress_transition = create_rw_signal(false);
144 let trans_class = move || (!suppress_transition()).then_some("transition-width");
145
146 let on_resize = move |ev: ev::DragEvent| {
147 if ev.client_x() as usize > SIDEBAR_MIN_WIDTH {
148 suppress_transition.set(true);
149 sidebar_width.update(|w| *w = ev.client_x() as usize);
150 } else {
151 suppress_transition.set(false);
152 }
153 };
154
155 view! {
156 <div style:display="flex">
157 <Sidebar
158 display=show_sidebar
159 layout=sidebar_layout
160 width=sidebar_width
161 attr:class=trans_class
162 >
163 <SidebarContent />
164 <Show when=move || (screen.style)() != ScreenStyle::Portrait>
165 <ResizeBar
166 position=sidebar_width
167 direction=Direction::Vertical
168 on:drag=on_resize
169 />
170 </Show>
171 </Sidebar>
172 <section style:flex-grow="1" class=trans_class style:width=section_width>
173 {children}
174 </section>
175 </div>
176 }
177}
178
179#[component]
180pub fn HomePage() -> impl IntoView {
181 let selection_signal = expect_context::<SelectionSignal>();
182 let show_sidebar = expect_context::<RwSignal<ShowSidebar>>();
183
184 let active = create_memo(move |_| {
185 selection_signal
186 .get()
187 .get_selected_keys()
188 .into_iter()
189 .copied()
190 .collect()
191 });
192
193 view! {
194 <RouteSidebar>
195 <div id="HomeGrid">
196 <Navbar show_sidebar />
197 <InfoBox countable_list=active />
198 </div>
199 </RouteSidebar>
200 }
201}
202
203#[component(transparent)]
204fn SidebarContent() -> impl IntoView {
205 let selection_signal = expect_context::<SelectionSignal>();
206 let preferences = expect_context::<RwSignal<Preferences>>();
207 let store = expect_context::<RwSignal<CountableStore>>();
208 let sort_method = expect_context::<RwSignal<SortMethod>>();
209
210 let show_sort_search = create_rw_signal(true);
211 let show_sep = create_read_slice(preferences, |pref| pref.show_separator);
212 let search = create_rw_signal(String::new());
213 provide_context(search);
214
215 let each = create_memo(move |_| {
216 let mut root_nodes = store()
217 .filter(move |c| c.name().to_lowercase().contains(&search().to_lowercase()))
218 .raw_filter(move |c| !c.is_archived())
219 .root_nodes();
220 root_nodes.sort_by(|a, b| {
221 sort_method().sort_by()(&store.get_untracked(), &a.uuid().into(), &b.uuid().into())
222 });
223 root_nodes
224 });
225
226 #[allow(clippy::single_match)]
227 let on_sort_key = move |ev: ev::KeyboardEvent| match ev.key().as_str() {
228 "Enter" => {
229 let mut nodes = store()
230 .raw_filter(move |c| c.name().to_lowercase().contains(&search().to_lowercase()))
231 .nodes();
232 nodes.sort_by(|a, b| {
233 sort_method().sort_by()(&store.get_untracked(), &a.uuid().into(), &b.uuid().into())
234 });
235 if let Some(first) = nodes.first() {
236 leptos_router::use_navigate()(&first.uuid().to_string(), Default::default());
237 }
238 }
239 _ => {}
240 };
241
242 view! {
243 <nav>
244 <SortSearch shown=show_sort_search search on_keydown=on_sort_key />
245 </nav>
246 <TreeViewWidget
247 each
248 key=|countable| countable.uuid()
249 each_child=move |countable| {
250 let key = countable.uuid().into();
251 let children = create_read_slice(
252 store,
253 move |s| {
254 let mut children = s.children(&key);
255 children
256 .sort_by(|a, b| sort_method()
257 .sort_by()(
258 &store.get_untracked(),
259 &a.uuid().into(),
260 &b.uuid().into(),
261 ));
262 children
263 },
264 );
265 children()
266 }
267
268 view=|countable| view! { <TreeViewRow key=countable.uuid() /> }
269 show_separator=show_sep
270 selection_model=selection_signal
271 on_click=|_, _| ()
272 />
273
274 <NewCounterButton />
275 }
276}
277
278#[component]
279fn TreeViewRow(key: uuid::Uuid) -> impl IntoView {
280 let selection = expect_context::<SelectionSignal>();
281 let data_resource = expect_context::<StateResource>();
282 let store = expect_context::<RwSignal<CountableStore>>();
283 let save_handler = expect_context::<RwSignal<SaveHandlers>>();
284 let search = create_memo(move |_| expect_context::<RwSignal<String>>()());
285
286 let expand_node = move |key: uuid::Uuid, expand: bool| {
287 selection.update(|s| {
288 if let Some(node) = s.get_node_mut(&key) {
289 node.set_expand(expand)
290 }
291 })
292 };
293
294 let includes_search = create_memo(move |_| {
295 !search().is_empty()
296 && store
297 .get_untracked()
298 .name(&key.into())
299 .to_lowercase()
300 .contains(&search().to_lowercase())
301 });
302 let selected = create_memo(move |_| selection().is_selected(&key));
303 let parent = store.get_untracked().parent(&key.into());
304
305 create_isomorphic_effect(move |_| {
306 if let Some(p) = parent.clone() {
307 if includes_search() || selected() {
308 expand_node(p.uuid(), true)
309 } else {
310 expand_node(p.uuid(), false)
311 }
312 }
313 });
314
315 let click_new_phase = move |ev: ev::MouseEvent| {
316 ev.stop_propagation();
317
318 let phase_number = store.get_untracked().children(&key.into()).len();
319 let name = format!("Phase {}", phase_number + 1);
320
321 store.update(move |s| {
322 let id = s.new_countable(&name, CountableKind::Phase, Some(key.into()));
323 let _ = save_handler().save(
324 Box::new([s.get(&id).unwrap()].to_vec()),
325 Box::new(move |_| data_resource.refetch()),
326 );
327 });
328
329 request_animation_frame(move || expand_node(key, true))
330 };
331
332 let show_context_menu = create_rw_signal(false);
333 let (click_location, set_click_location) = create_signal((0, 0));
334 let on_right_click = move |ev: web_sys::MouseEvent| {
335 ev.prevent_default();
336 expect_context::<RwSignal<CloseOverlays>>().update(|_| ());
337 show_context_menu.set(!show_context_menu());
338 set_click_location((ev.x(), ev.y()))
339 };
340
341 let has_children = move || matches!(store().get(&key.into()), Some(Countable::Counter(_)));
342
343 let search_split = create_memo(move |_| {
344 if search().is_empty() {
345 return None;
346 }
347 let name = store().name(&key.into()).to_lowercase();
348 if let Some(idx) = name.find(&search().to_lowercase()) {
349 let (first, rest) = name.split_at(idx);
350 let (_, last) = rest.split_at(search().len());
351 Some((first.to_string(), last.to_string()))
352 } else {
353 None
354 }
355 });
356
357 view! {
358 <A href=move || key.to_string()>
359 <div class="row-body" on:contextmenu=on_right_click>
360 <Show
361 when=move || search_split().is_some()
362 fallback=move || view! { <span>{move || store().name(&key.into())}</span> }
363 >
364 <div>
365 <span>{move || search_split().unwrap().0}</span>
366 <span style:background="var(--accent)" style:color="black">
367 {search}
368 </span>
369 <span>{move || search_split().unwrap().1}</span>
370 </div>
371 </Show>
372 <Show when=has_children>
373 <button on:click=click_new_phase>+</button>
374 </Show>
375 </div>
376 </A>
377 <CountableContextMenu show_overlay=show_context_menu location=click_location key />
378 }
379}
380
381#[component]
382fn SetCountable() -> impl IntoView {
383 #[derive(Debug, PartialEq, Params, Clone)]
384 struct Key {
385 key: String,
386 }
387
388 let selection = expect_context::<SelectionSignal>();
389
390 let key_memo = create_memo(move |old_key| {
391 let new_key = use_params::<Key>()
392 .get()
393 .ok()
394 .and_then(|p| uuid::Uuid::parse_str(&p.key).ok());
395
396 if let Some(key) = new_key
397 && old_key != Some(&new_key)
398 {
399 selection.update(|sel| sel.select(&key));
400 }
401
402 new_key
403 });
404
405 create_isomorphic_effect(move |_| key_memo.track());
406}
407
408#[component]
409fn UnsetCountable() -> impl IntoView {
410 let selection = expect_context::<SelectionSignal>();
411 selection.update(|sel| sel.clear_selection())
412}
413
414#[component(transparent)]
415fn ProvideCountableSignals(children: ChildrenFn) -> impl IntoView {
416 let msg = expect_context::<MessageJar>();
417 let store = expect_context::<RwSignal<CountableStore>>();
418
419 let selection = SelectionModel::<uuid::Uuid, Countable>::new();
420 let selection_signal = create_rw_signal(selection);
421 provide_context(selection_signal);
422
423 provide_context(create_rw_signal(SortMethod::default()));
424
425 let save_handlers = create_rw_signal(SaveHandlers::new());
426
427 let server_handler = Box::new(ServerSaveHandler::new());
428 save_handlers.update(|sh| sh.connect_handler(server_handler.clone()));
429
430 window_event_listener(ev::blur, move |_| {
432 if let Err(err) = save_handlers
433 .get_untracked()
434 .save(Box::new(store), Box::new(|_| ()))
435 {
436 msg.set_err(err)
437 }
438 });
439
440 create_effect(move |_| {
441 spawn_local(async move {
442 let indexed_handler = indexed::IndexedSaveHandler::new().await;
443 match indexed_handler {
444 Ok(ih) => {
445 let mut s = store.get_untracked();
446 if let Err(err) = ih.sync_store(&mut s).await {
447 msg.set_err(err);
448 };
449 if let Err(err) = save_handlers
450 .get_untracked()
451 .save(Box::new(s.clone()), Box::new(|_| ()))
452 {
453 msg.set_err(err);
454 }
455 store.set(s.clone());
456 save_handlers.update(|sh| sh.connect_handler(Box::new(ih)));
457 if let Err(err) = save_handlers
458 .get_untracked()
459 .save(Box::new(store), Box::new(|_| ()))
460 {
461 msg.set_err(err)
462 }
463 }
464 Err(err) => msg.set_msg(format!(
465 "Local saving could not be initialised\nGot error: {}",
466 err
467 )),
468 }
469 })
470 });
471 provide_context(save_handlers);
472
473 children()
474}