1#![allow(non_snake_case)]
2use components::{
3 MessageJar, Select, SelectionModel, ShowSidebar, Sidebar, SidebarLayout, TreeViewWidget,
4};
5use elements::{Navbar, SortMethod, SortSearch};
6use leptos::*;
7use leptos_router::{use_params, ActionForm, Outlet, Params, A};
8
9use super::*;
10
11stylance::import_style!(
12 #[allow(dead_code)]
13 style,
14 "../../style/edit.module.scss"
15);
16
17#[component]
18pub fn EditWindow() -> impl IntoView {
19 let preferences = expect_context::<RwSignal<Preferences>>();
20 let store = expect_context::<RwSignal<CountableStore>>();
21 let screen = expect_context::<Screen>();
22 let sort_method = expect_context::<RwSignal<SortMethod>>();
23
24 let sidebar_layout: Signal<SidebarLayout> = create_read_slice(screen.style, |s| (*s).into());
25
26 let selection = create_rw_signal(SelectionModel::<uuid::Uuid, Countable>::new());
27 provide_context(selection);
28
29 let width = create_rw_signal(400);
30 let show_sort_search = create_rw_signal(true);
31 let show_sep = create_read_slice(preferences, |pref| pref.show_separator);
32
33 let show_sidebar = create_rw_signal(ShowSidebar(false));
34
35 let min_height = create_memo(move |_| match (screen.style)() {
36 ScreenStyle::Portrait => Some("110vh"),
37 ScreenStyle::Small => None,
38 ScreenStyle::Big => None,
39 });
40
41 let outlet_view = view! { <Outlet /> };
43
44 let sidebar_update_memo =
45 create_memo(move |_| ((screen.style)(), selection().get_owned_selected_keys()));
46
47 create_isomorphic_effect(move |_| match sidebar_update_memo.get() {
48 (ScreenStyle::Portrait, e) => {
49 width.set(0);
50 logging::log!("{}", e.is_empty());
51 show_sidebar.set(ShowSidebar(e.is_empty()));
52 }
53 (ScreenStyle::Small, e) => {
54 width.set(0);
55 show_sidebar.set(ShowSidebar(e.is_empty()));
56 }
57 (ScreenStyle::Big, _) => {
58 width.set(400);
59 show_sidebar.set(ShowSidebar(true));
60 }
61 });
62
63 view! {
64 <div style:display="flex">
65 <Sidebar display=show_sidebar layout=sidebar_layout width>
66 <nav>
67 <SortSearch
68 shown=show_sort_search
69 search=create_rw_signal(String::new())
70 on_keydown=|_| ()
71 />
72 </nav>
73 <TreeViewWidget
74 each=move || {
75 let mut root_nodes = store().root_nodes();
76 root_nodes
77 .sort_by(|a, b| sort_method()
78 .sort_by()(
79 &store.get_untracked(),
80 &a.uuid().into(),
81 &b.uuid().into(),
82 ));
83 root_nodes
84 }
85
86 key=|countable| countable.uuid()
87 each_child=move |countable| {
88 let mut children = store().children(&countable.uuid().into());
89 children
90 .sort_by(|a, b| sort_method()
91 .sort_by()(
92 &store.get_untracked(),
93 &a.uuid().into(),
94 &b.uuid().into(),
95 ));
96 children
97 }
98
99 view=|countable| view! { <TreeViewRow key=countable.uuid() /> }
100 show_separator=show_sep
101 selection_model=selection
102 />
103 </Sidebar>
104 <section
105 style:width=move || format!("calc(100vw - {}px)", width())
106 style:min-height=min_height
107 >
108 <Navbar show_sidebar />
109 {outlet_view}
110 </section>
111 </div>
112 }
113}
114
115#[component]
116fn TreeViewRow(key: uuid::Uuid) -> impl IntoView {
117 let store = expect_context::<RwSignal<CountableStore>>();
118 let name = move || store().name(&key.into());
119
120 view! {
121 <A href=move || key.to_string()>
122 <div class=style::tree_row>
123 <span>{name}</span>
124 </div>
125 </A>
126 }
127}
128
129#[component]
130pub fn EditCountableWindow() -> impl IntoView {
131 let selection = expect_context::<SelectionSignal>();
132 let store = expect_context::<RwSignal<CountableStore>>();
133 let screen = expect_context::<Screen>();
134
135 let key_memo = create_memo(move |old_key| {
136 let new_key = use_params::<Key>()()
137 .ok()
138 .and_then(|p| uuid::Uuid::parse_str(&p.key).ok());
139
140 if let Some(key) = new_key
141 && new_key != old_key.copied()
142 {
143 selection.update(|sel| sel.select(&key))
144 }
145
146 new_key.unwrap_or_default()
147 });
148
149 let form_style = move || {
150 stylance::classes!(
151 style::form,
152 match (screen.style)() {
153 ScreenStyle::Portrait => Some(style::portrait),
154 ScreenStyle::Small => Some(style::small),
155 ScreenStyle::Big => Some(style::big),
156 }
157 )
158 };
159
160 let valid = move || store().contains(&key_memo().into());
161
162 view! {
163 <h1 style:color="white" style:padding="12px 48px">
164 Edit
165 </h1>
166 <div style:display="flex" style:justify-content="center">
167 <Show when=valid>
168 <edit-form class=form_style>
169 <EditCounterBox key=key_memo />
170 </edit-form>
171 </Show>
172 </div>
173 }
174}
175
176#[derive(Debug, Clone, Params, PartialEq, Eq, Default)]
177struct Key {
178 key: String,
179}
180
181#[component]
182fn EditCounterBox(#[prop(into)] key: MaybeSignal<uuid::Uuid>) -> impl IntoView {
183 let rs = expect_context::<StateResource>();
184 let session = expect_context::<RwSignal<UserSession>>();
185 let store = expect_context::<RwSignal<CountableStore>>();
186 let msg = expect_context::<MessageJar>();
187 let action = create_server_action::<api::EditCountableForm>();
188 let screen = expect_context::<Screen>();
189
190 let kind = create_read_slice(store, move |s| s.kind(&key().into()));
191
192 create_effect(move |_| match action.value()() {
193 Some(Ok(_)) => {
194 leptos_router::use_navigate()(format!("/{}", key()).as_str(), Default::default())
195 }
196 Some(Err(err)) => {
197 match err {
198 ServerFnError::WrappedServerError(err) => msg.set_err(err),
199 ServerFnError::Registration(err) => msg.set_err(err),
200 ServerFnError::Request(_) => msg.set_err("Could not reach server"),
201 ServerFnError::Response(err) => msg.set_err(err),
202 ServerFnError::ServerError(err) => msg.set_err(err),
203 ServerFnError::Deserialization(err) => msg.set_err(err),
204 ServerFnError::Serialization(err) => msg.set_err(err),
205 ServerFnError::Args(err) => msg.set_err(err),
206 ServerFnError::MissingArg(err) => msg.set_err(err),
207 };
208 }
209 None => {}
210 });
211
212 create_effect(move |_| {
213 if let Some(Ok(_)) = action.value()() {
214 rs.refetch();
215 leptos_router::use_navigate()(format!("/{}", key()).as_str(), Default::default())
216 }
217 });
218
219 let undo = move |_| {
220 rs.refetch();
221 };
222
223 view! {
224 <ActionForm action>
225 <SessionFormInput session />
226 <input type="hidden" name="countable_key" value=move || key().to_string() />
227 <input type="hidden" name="countable_kind" value=move || kind().to_string() />
228 <table style:display="flex" style:flex-flow="column" class=style::content>
229 <tbody>
230 <tr class=stylance::classes!(style::row, style::text_row)>
231 <EditName key />
232 </tr>
233 <tr class=stylance::classes!(style::row, style::text_row)>
234 <EditCount key />
235 </tr>
236 <tr class=stylance::classes!(style::row, style::text_row)>
237 <EditTime key />
238 </tr>
239 <tr class=stylance::classes!(style::row, style::text_row)>
240 <EditHunttype key />
241 </tr>
242 <tr class=style::row>
243 <EditCharm key />
244 </tr>
245 </tbody>
246 </table>
247 <action-buttons class=move || {
248 stylance::classes!(
249 style::action_buttons, match (screen.style) () { ScreenStyle::Portrait =>
250 Some(style::fixed), ScreenStyle::Small => None, ScreenStyle::Big => None, }
251 )
252 }>
253
254 <action-start></action-start>
255 <action-end>
256 <button type="button" on:click=undo>
257 Undo
258 </button>
259 <button type="submit" class=style::confirm>
260 Submit
261 </button>
262 </action-end>
263 </action-buttons>
264 </ActionForm>
265 }
266}
267
268#[component]
269fn EditName(#[prop(into)] key: MaybeSignal<uuid::Uuid>) -> impl IntoView {
270 let store = expect_context::<RwSignal<CountableStore>>();
271 let (name, set_name) = create_slice(
272 store,
273 move |s| s.name(&key().into()),
274 move |s, name: String| s.set_name(&key().into(), &name),
275 );
276
277 let on_input = move |ev| set_name(event_target_value(&ev));
278
279 view! {
280 <td>
281 <label for="change-name">Name</label>
282 </td>
283 <td>
284 <div class=style::boxed>
285 <input
286 type="text"
287 value=name
288 prop:value=name
289 id="change-name"
290 name="countable_name"
291 on:input=on_input
292 style:text-align="end"
293 />
294 </div>
295 </td>
296 }
297}
298
299#[component]
300fn EditCount(#[prop(into)] key: MaybeSignal<uuid::Uuid>) -> impl IntoView {
301 let store = expect_context::<RwSignal<CountableStore>>();
302 let count = create_read_slice(store, move |s| s.count(&key().into()));
303
304 view! {
305 <td>
306 <label for="change-count">Count</label>
307 </td>
308 <td>
309 <div class=style::boxed>
310 <input
311 type="number"
312 value=count
313 prop:value=count
314 id="change-count"
315 name="countable_count"
316 style:text-align="end"
317 />
318 </div>
319 </td>
320 }
321}
322
323#[component]
324fn EditTime(#[prop(into)] key: MaybeSignal<uuid::Uuid>) -> impl IntoView {
325 let store = expect_context::<RwSignal<CountableStore>>();
326 let time = create_read_slice(store, move |s| s.time(&key().into()));
327
328 let hour_ref = create_node_ref::<html::Input>();
329 let min_ref = create_node_ref::<html::Input>();
330 let sec_ref = create_node_ref::<html::Input>();
331 let millis_ref = create_node_ref::<html::Input>();
332
333 let limit_num = |ev: ev::Event, node_ref: NodeRef<html::Input>, min, max| {
334 if let Some(node) = node_ref() {
335 let mut new_val = event_target_value(&ev);
336 if new_val.parse::<i64>().is_ok_and(|v| min <= v && v < max) {
337 } else if !new_val.is_empty() {
338 new_val.remove(new_val.len() - 1);
339 node.set_value(&new_val);
340 }
341 }
342 };
343
344 let pad_hours = move || format!("{:02}", time().num_hours());
345 let pad_mins = move || format!("{:02}", time().num_minutes() % 60);
346 let pad_secs = move || format!("{:02}", time().num_seconds() % 60);
347 let pad_millis = move || format!("{:03}", time().num_milliseconds() % 1000);
348
349 let pad_input = move |node_ref: NodeRef<html::Input>, w| {
350 if time.try_get().is_none() {
352 return;
353 }
354 if let Some(node) = node_ref() {
355 if let Ok(num) = node.value().parse::<i32>() {
356 node.set_value(format!("{:0w$}", num, w = w).as_str());
357 } else if node.value() == "" {
358 node.set_value("0".repeat(w).as_str())
359 }
360 }
361 };
362
363 view! {
364 <td>Time</td>
365 <td>
366 <div class=style::boxed style:text-align="end">
367 <label for="change-hours">
368 <input
369 type="number"
370 value=pad_hours
371 id="change-hours"
372 name="countable_hours"
373 style:width="4ch"
374 style:text-align="end"
375 node_ref=hour_ref
376 on:focusout=move |_| pad_input(hour_ref, 2)
377 />
378 :
379 </label>
380 <label for="change-mins">
381 <input
382 type="number"
383 value=pad_mins
384 id="change-mins"
385 name="countable_mins"
386 max="59"
387 style:width="2ch"
388 style:text-align="end"
389 node_ref=min_ref
390 on:input=move |ev| limit_num(ev, min_ref, 0, 59)
391 on:focusout=move |_| pad_input(min_ref, 2)
392 />
393 :
394 </label>
395 <label for="change-secs">
396 <input
397 type="number"
398 value=pad_secs
399 id="change-secs"
400 name="countable_secs"
401 max="59"
402 style:width="2ch"
403 style:text-align="end"
404 node_ref=sec_ref
405 on:input=move |ev| limit_num(ev, sec_ref, 0, 59)
406 on:focusout=move |_| pad_input(sec_ref, 2)
407 />
408 .
409 </label>
410 <label for="change-millis">
411 <input
412 type="number"
413 value=pad_millis
414 id="change-millis"
415 name="countable_millis"
416 max="999"
417 style:width="3ch"
418 style:text-align="end"
419 node_ref=millis_ref
420 on:input=move |ev| limit_num(ev, millis_ref, 0, 999)
421 on:focusout=move |_| pad_input(millis_ref, 3)
422 />
423 </label>
424 </div>
425 </td>
426 }
427}
428
429#[component]
430fn EditHunttype(#[prop(into)] key: MaybeSignal<uuid::Uuid>) -> impl IntoView {
431 let store = expect_context::<RwSignal<CountableStore>>();
432 let hunt_type = move || store().hunttype(&key().into());
433 let selected = create_memo(move |_| hunt_type().into());
434
435 let hunt_option = |ht: Hunttype| -> (&'static str, &'static str) { (ht.repr(), ht.into()) };
436
437 let options = vec![
438 hunt_option(Hunttype::OldOdds).into(),
439 hunt_option(Hunttype::NewOdds).into(),
440 hunt_option(Hunttype::Masuda(Masuda::GenIV)).into(),
441 hunt_option(Hunttype::Masuda(Masuda::GenV)).into(),
442 hunt_option(Hunttype::Masuda(Masuda::GenVI)).into(),
443 hunt_option(Hunttype::SOS).into(),
444 ];
446
447 view! {
448 <td>
449 <label for="change-hunttype">Method</label>
450 </td>
451 <td style:text-align="start">
452 <div class=style::boxed>
453 <Select
454 attr:id="change-hunttype"
455 attr:name="countable_hunttype"
456 attr:value=hunt_type
457 selected
458 options
459 />
460 </div>
461 </td>
462 }
463}
464
465#[component]
466fn EditCharm(#[prop(into)] key: MaybeSignal<uuid::Uuid>) -> impl IntoView {
467 let store = expect_context::<RwSignal<CountableStore>>();
468 let checked = create_read_slice(store, move |s| s.has_charm(&key().into()));
469
470 view! {
471 <td>
472 <label for="has-charm">Has Charm</label>
473 </td>
474 <td>
475 <components::Slider
476 attr:id="has-charm"
477 attr:name="countable_charm"
478 checked
479 ></components::Slider>
480 </td>
481 }
482}