From bd89ba48b0042d14462ca15f05371232ba1e9551 Mon Sep 17 00:00:00 2001
From: Marc-Antoine Manningham <marc-antoine.m@outlook.com>
Date: Tue, 10 Dec 2024 22:53:47 -0500
Subject: [PATCH] fix: make the generator work again

---
 .../src/frontend/components/options/form.rs   | 17 +----
 .../src/frontend/components/options/search.rs |  5 +-
 .../src/frontend/components/options/todo.rs   |  8 +-
 .../src/frontend/components/schedules.rs      | 73 ++++++++-----------
 .../src/frontend/pages/generator.rs           | 35 ++++-----
 .../src/frontend/state/action_add_course.rs   | 31 ++++++++
 .../src/frontend/state/mod.rs                 | 61 +++++++++-------
 .../src/frontend/state/reactive_course.rs     |  4 +-
 8 files changed, 125 insertions(+), 109 deletions(-)
 create mode 100644 aep-schedule-website/src/frontend/state/action_add_course.rs

diff --git a/aep-schedule-website/src/frontend/components/options/form.rs b/aep-schedule-website/src/frontend/components/options/form.rs
index 0e1f693..f8e91c1 100644
--- a/aep-schedule-website/src/frontend/components/options/form.rs
+++ b/aep-schedule-website/src/frontend/components/options/form.rs
@@ -3,31 +3,20 @@ use crate::frontend::{
         common::number_input::NumberInput,
         options::{courses_selector::CoursesSelector, optimizations::SelectOptimizations},
     },
-    pages::generator::FirstGenerationDone,
     state::OptionState,
 };
 use leptos::prelude::*;
 
 #[component]
 pub fn OptionsForms() -> impl IntoView {
-    let first_generation_done: FirstGenerationDone = use_context().unwrap();
-
     let state = OptionState::from_context();
+
     let submit = move || {
-        state.validate();
-        if !first_generation_done.0.get() || state.step.get() < 5 {
-            return;
-        }
-        state.generate();
+        state.submit();
     };
 
     let submit_mobile = move |_| {
-        state.validate();
-        if state.step.get() < 5 {
-            state.hide.set(true);
-            return;
-        }
-        state.generate();
+        state.submit_mobile();
     };
 
     view! {
diff --git a/aep-schedule-website/src/frontend/components/options/search.rs b/aep-schedule-website/src/frontend/components/options/search.rs
index 43d5070..3b66f1a 100644
--- a/aep-schedule-website/src/frontend/components/options/search.rs
+++ b/aep-schedule-website/src/frontend/components/options/search.rs
@@ -1,6 +1,6 @@
 use crate::frontend::{
     components::common::autocomplete::{AutoComplete, AutoCompleteOption},
-    state::OptionState,
+    state::action_add_course::ActionAddCourse,
 };
 use aep_schedule_generator::data::course::CourseName;
 use leptos::prelude::*;
@@ -13,13 +13,12 @@ pub fn SearchCourse(
     let Ok(courses) = all_courses else {
         return None;
     };
-    let state = OptionState::from_context();
     let courses = courses
         .into_iter()
         .map(|c| AutoCompleteOption::new(c.sigle.clone(), c.sigle + " - " + &c.name))
         .collect();
 
-    let action_courses = state.action_courses;
+    let action_courses = ActionAddCourse::from_context().0;
     let on_submit = move |sigle: String| {
         set_active_tab(sigle.clone());
         action_courses.dispatch(sigle);
diff --git a/aep-schedule-website/src/frontend/components/options/todo.rs b/aep-schedule-website/src/frontend/components/options/todo.rs
index 50acbbb..8104fd7 100644
--- a/aep-schedule-website/src/frontend/components/options/todo.rs
+++ b/aep-schedule-website/src/frontend/components/options/todo.rs
@@ -1,7 +1,7 @@
 use std::cmp::Ordering;
 
 use crate::frontend::components::icons::warning_circle::WarningCircle;
-use crate::frontend::{pages::generator::FirstGenerationDone, state::OptionState};
+use crate::frontend::state::OptionState;
 use leptos::prelude::*;
 
 #[component]
@@ -62,12 +62,10 @@ pub fn Step(
 
 #[component]
 pub fn Todo() -> impl IntoView {
-    let first_generation_done: FirstGenerationDone = use_context().unwrap();
-
     let state = OptionState::from_context();
     let submit = move |_| {
-        first_generation_done.0.set(true);
-        state.generate();
+        state.first_generation_done.set_value(true);
+        state.submit();
     };
 
     let step: RwSignal<u8> = state.step;
diff --git a/aep-schedule-website/src/frontend/components/schedules.rs b/aep-schedule-website/src/frontend/components/schedules.rs
index 2ebe5ca..4b661e4 100644
--- a/aep-schedule-website/src/frontend/components/schedules.rs
+++ b/aep-schedule-website/src/frontend/components/schedules.rs
@@ -17,49 +17,40 @@ pub fn SchedulesComponent() -> impl IntoView {
                 let calendar = Arc::new(calendar.clone().unwrap());
                 let bad_generation = state.schedule.get().is_empty();
                 let generated = state.step.get() == 6;
-                match generated && !bad_generation {
-                    true => {
-                        view! {
-                            <For
-                                each=move || state.schedule.get()
-                                key=|course| course.id
-                                children=move |schedule| {
-                                    let calendar = Arc::clone(&calendar);
-                                    view! { <ScheduleComponent schedule calendar /> }
-                                }
-                            />
+                view! {
+                    {move || {
+                        let bad_generation = state.schedule.get().is_empty();
+                        let generated = state.step.get() == 6;
+                        if !(generated && !bad_generation) {
+                            Some(view! { <Todo /> })
+                        } else {
+                            None
                         }
-                            .into_any()
-                    }
-                    _ => {
-                        view! {
-                            <Todo />
-                            <For
-                                each=move || state.schedule.get()
-                                key=|course| course.id
-                                children=move |schedule| {
-                                    let calendar = Arc::clone(&calendar);
-                                    view! { <ScheduleComponent schedule calendar /> }
-                                }
-                            />
-                            {match generated && bad_generation {
-                                true => {
-                                    Some(
-                                        view! {
-                                            <div class="warning-box">
-                                                <WarningCircle size="4em" />
-                                                <span>
-                                                    "Aucun horaire n'a pu être généré, augmentez le nombre de conflits ou ouvrez des sections. Probablement que deux groupes sont toujours en conflits."
-                                                </span>
-                                            </div>
-                                        },
-                                    )
-                                }
-                                false => None,
-                            }}
+                    }}
+                    {move || {
+                        if generated && bad_generation {
+                            Some(
+                                view! {
+                                    <div class="warning-box">
+                                        <WarningCircle size="4em" />
+                                        <span>
+                                            "Aucun horaire n'a pu être généré, augmentez le nombre de conflits ou ouvrez des sections. Probablement que deux groupes sont toujours en conflits."
+                                        </span>
+                                    </div>
+                                },
+                            )
+                        } else {
+                            None
                         }
-                            .into_any()
-                    }
+                    }}
+                    <For
+                        each=move || state.schedule.get()
+                        key=|course| course.id
+                        children=move |schedule| {
+                            let calendar = Arc::clone(&calendar);
+                            view! { <ScheduleComponent schedule calendar /> }
+                        }
+                    />
                 }
             }
         />
diff --git a/aep-schedule-website/src/frontend/pages/generator.rs b/aep-schedule-website/src/frontend/pages/generator.rs
index 8babed2..c51087b 100644
--- a/aep-schedule-website/src/frontend/pages/generator.rs
+++ b/aep-schedule-website/src/frontend/pages/generator.rs
@@ -1,9 +1,12 @@
 use crate::frontend::components::icons::{caret_double_right::CaretDoubleRight, IconWeight};
 use crate::frontend::components::notifications::Notifications;
 use crate::frontend::components::{options::form::OptionsForms, schedules::SchedulesComponent};
+use crate::frontend::state::action_add_course::ActionAddCourse;
 use crate::frontend::state::OptionState;
 use aep_schedule_generator::data::group_sigle::SigleGroup;
 use leptos::prelude::*;
+#[cfg(feature = "hydrate")]
+use web_sys::Event;
 
 #[derive(Clone, Copy)]
 pub struct SetModal(WriteSignal<Option<SigleGroup>>);
@@ -14,38 +17,32 @@ impl SetModal {
     }
 }
 
-#[derive(Clone, Copy)]
-pub struct FirstGenerationDone(pub RwSignal<bool>);
-
 #[component]
 pub fn GeneratorPage() -> impl IntoView {
-    let first_generation_done = RwSignal::new(false);
     let (modal, set_modal) = signal(None);
     let state = OptionState::default();
     let hide = state.hide;
     provide_context(state);
     provide_context(SetModal(set_modal));
-    provide_context(FirstGenerationDone(first_generation_done));
+    provide_context(ActionAddCourse::new(state));
 
+    #[cfg(feature = "hydrate")]
+    let onscroll = move |ev: Event| {
+        use web_sys::wasm_bindgen::JsCast;
+        let target = ev.target().unwrap().dyn_into::<web_sys::Element>().unwrap();
+        let scroll_top = target.scroll_top() as f64;
+        let client_height = target.client_height() as f64;
+        let scroll_height = target.scroll_height() as f64;
+        if (scroll_top + client_height >= scroll_height - 500.0) && state.step.get() == 6 {
+            state.regenerate();
+        }
+    };
     view! {
         <aside class="left-panel" class=("hide-left-panel", hide)>
             <OptionsForms />
         </aside>
         <section class="right-panel">
-            // on:scroll=move |ev| {
-            // use web_sys::wasm_bindgen::JsCast;
-            // let target = ev
-            // .target()
-            // .unwrap()
-            // .dyn_into::<web_sys::Element>()
-            // .unwrap();
-            // let scroll_top = target.scroll_top() as f64;
-            // let client_height = target.client_height() as f64;
-            // let scroll_height = target.scroll_height() as f64;
-            // if (scroll_top + client_height >= scroll_height - 500.0) && state.step.get() == 6 {
-            // state.regenerate();
-            // }
-
+            on:scroll=onscroll
             <SchedulesComponent />
         </section>
         <Notifications modal set_modal />
diff --git a/aep-schedule-website/src/frontend/state/action_add_course.rs b/aep-schedule-website/src/frontend/state/action_add_course.rs
new file mode 100644
index 0000000..8bd7241
--- /dev/null
+++ b/aep-schedule-website/src/frontend/state/action_add_course.rs
@@ -0,0 +1,31 @@
+use leptos::prelude::*;
+
+use crate::backend::routes::get_course;
+
+use super::OptionState;
+
+#[derive(Clone)]
+pub struct ActionAddCourse(pub Action<String, ()>);
+
+impl ActionAddCourse {
+    pub fn from_context() -> Self {
+        use_context().unwrap()
+    }
+
+    pub fn new(state: OptionState) -> Self {
+        let action_courses = Action::new(move |sigle: &String| {
+            let sigle = sigle.clone();
+            async move {
+                if let Ok(c) = get_course(sigle.clone()).await {
+                    state.courses.update(|courses| {
+                        if !courses.iter().any(|c| c.sigle == sigle) {
+                            courses.push(c.into());
+                        }
+                    });
+                    state.submit();
+                }
+            }
+        });
+        Self(action_courses)
+    }
+}
diff --git a/aep-schedule-website/src/frontend/state/mod.rs b/aep-schedule-website/src/frontend/state/mod.rs
index c51a6e6..fd687f9 100644
--- a/aep-schedule-website/src/frontend/state/mod.rs
+++ b/aep-schedule-website/src/frontend/state/mod.rs
@@ -7,14 +7,13 @@ use aep_schedule_generator::{
 use leptos::prelude::*;
 use reactive_course::ReactiveCourse;
 
-use crate::backend::routes::get_course;
-
+pub mod action_add_course;
 pub mod reactive_course;
 
 #[derive(Copy, Clone)]
 pub struct OptionState {
+    pub first_generation_done: StoredValue<bool>,
     pub courses: RwSignal<Vec<ReactiveCourse>>,
-    pub action_courses: Action<String, ()>,
     pub week: [RwSignal<u64>; 5],
     pub max_nb_conflicts: RwSignal<u8>,
     pub day_off: RwSignal<u8>,
@@ -33,10 +32,28 @@ impl OptionState {
         use_context().unwrap()
     }
 
-    pub fn validate(&self) {
+    pub fn submit(&self) {
+        self.validate();
+        if !self.first_generation_done.get_value() || self.step.get() < 5 {
+            return;
+        }
+        self.generate();
+    }
+
+    pub fn submit_mobile(&self) {
+        self.validate();
+        if self.step.get() < 5 {
+            self.hide.set(true);
+            return;
+        }
+        self.generate();
+    }
+
+    fn validate(&self) {
         let mut options: SchedulesOptions = self.into();
         if options.courses_to_take.is_empty() {
             self.step.set(1);
+            self.schedule.set(vec![]);
             return;
         }
         let mut impossible_courses = options.get_impossible_course().into_iter();
@@ -49,6 +66,7 @@ impl OptionState {
             error.push_str(" sont toutes fermées.");
             self.section_error.set(error);
             self.step.set(2);
+            self.schedule.set(vec![]);
             return;
         }
         self.section_error.set("".to_string());
@@ -63,6 +81,7 @@ impl OptionState {
             error.push_str(" sont en conflits avec les heures libres sélectionnées.");
             self.personal_error.set(error);
             self.step.set(3);
+            self.schedule.set(vec![]);
             return;
         }
         self.personal_error.set("".to_string());
@@ -73,7 +92,7 @@ impl OptionState {
         });
     }
 
-    pub fn generate(&self) {
+    fn generate(&self) {
         self.max_size
             .update_value(|v| v.store(8, Ordering::Relaxed));
         self.hide.set(true);
@@ -102,22 +121,9 @@ impl Default for OptionState {
     fn default() -> Self {
         let courses: RwSignal<Vec<ReactiveCourse>> = RwSignal::new(vec![]);
 
-        let action_courses = Action::new(move |sigle: &String| {
-            let sigle = sigle.clone();
-            async move {
-                if let Ok(c) = get_course(sigle.clone()).await {
-                    courses.update(|courses| {
-                        if !courses.iter().any(|c| c.sigle == sigle) {
-                            courses.push(c.into());
-                        }
-                    });
-                }
-            }
-        });
-
         Self {
+            first_generation_done: StoredValue::new(false),
             courses,
-            action_courses,
             max_nb_conflicts: RwSignal::new(0),
             week: std::array::from_fn(|_i| RwSignal::new(0)),
             day_off: RwSignal::new(3),
@@ -135,18 +141,23 @@ impl Default for OptionState {
 
 impl From<&OptionState> for SchedulesOptions {
     fn from(state: &OptionState) -> Self {
-        let courses_to_take = state.courses.get().into_iter().map(|c| c.into()).collect();
+        let courses_to_take = state
+            .courses
+            .get_untracked()
+            .into_iter()
+            .map(|c| c.into())
+            .collect();
         let mut max_size = 8;
         state
             .max_size
             .update_value(|v| max_size = v.load(Ordering::Relaxed));
-        let max_nb_conflicts = state.max_nb_conflicts.get();
+        let max_nb_conflicts = state.max_nb_conflicts.get_untracked();
         let evaluation = EvaluationOption {
-            day_off: state.day_off.get(),
-            morning: state.morning.get(),
-            finish_early: state.finish_early.get(),
+            day_off: state.day_off.get_untracked(),
+            morning: state.morning.get_untracked(),
+            finish_early: state.finish_early.get_untracked(),
         };
-        let user_conflicts = Week::new(state.week.map(|s| s.get() << 2));
+        let user_conflicts = Week::new(state.week.map(|s| s.get_untracked() << 2));
         Self {
             courses_to_take,
             max_nb_conflicts,
diff --git a/aep-schedule-website/src/frontend/state/reactive_course.rs b/aep-schedule-website/src/frontend/state/reactive_course.rs
index 2bda6cd..8f18bb4 100644
--- a/aep-schedule-website/src/frontend/state/reactive_course.rs
+++ b/aep-schedule-website/src/frontend/state/reactive_course.rs
@@ -61,10 +61,10 @@ impl From<ReactiveCourse> for Course {
                 mut lab_groups,
             } => {
                 for (i, theo) in theo_open.iter().enumerate() {
-                    theo_groups.get_mut(i.into()).unwrap().open = theo.get();
+                    theo_groups.get_mut(i.into()).unwrap().open = theo.get_untracked();
                 }
                 for (i, lab) in lab_open.iter().enumerate() {
-                    lab_groups.get_mut(i.into()).unwrap().open = lab.get();
+                    lab_groups.get_mut(i.into()).unwrap().open = lab.get_untracked();
                 }
                 CourseType::Both {
                     theo_groups,
-- 
GitLab