tallyweb_frontend/pages/
edit.rs

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    // we need to render the outlet first since it sets the selection key from the url
42    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        // we check whether a signal is disposed so we know the node_ref is disposed as well
351        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        // hunt_option(Hunttype::DexNav).into(),
445    ];
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}