diff --git a/aep-schedule-website/src/frontend/components/common/checkbox.rs b/aep-schedule-website/src/frontend/components/common/checkbox.rs index f8b24c7ea5a7bed02af571886fc2fbf54302e285..7e689b3dc4d7d3a4625a7eaee76bf53a2dc15a6b 100644 --- a/aep-schedule-website/src/frontend/components/common/checkbox.rs +++ b/aep-schedule-website/src/frontend/components/common/checkbox.rs @@ -1,13 +1,20 @@ use leptos::*; #[component] -pub fn CheckboxChip( +pub fn CheckboxChip<F>( #[prop(optional, into)] value: RwSignal<bool>, #[prop(optional, into)] class: &'static str, + submit: F, children: Children, -) -> impl IntoView { +) -> impl IntoView +where + F: Fn() + Copy + 'static, +{ view! { - <div on:click=move |_| {value.update(|b| *b = !*b)} class="chips ".to_owned() + class> + <div on:click=move |_| { + value.update(|b| *b = !*b); + submit(); + } class="chips ".to_owned() + class> <input type="checkbox" prop:checked=value diff --git a/aep-schedule-website/src/frontend/components/options/courses_selector.rs b/aep-schedule-website/src/frontend/components/options/courses_selector.rs index 58602e7cb7110828303175038edd71a83fa070e7..f1d46f5e6555c834c1daf22b9a2493c30da13230 100644 --- a/aep-schedule-website/src/frontend/components/options/courses_selector.rs +++ b/aep-schedule-website/src/frontend/components/options/courses_selector.rs @@ -19,17 +19,21 @@ use compact_str::CompactString; use leptos::*; #[component] -fn GroupsChips( +fn GroupsChips<F>( open: RwSignal<bool>, open_style: &'static str, group: Group, course_sigle: CompactString, group_type: GroupType, -) -> impl IntoView { + submit: F, +) -> impl IntoView +where + F: Fn() + Copy + 'static, +{ let set_modal = use_context::<SetModal>().unwrap().0; view! { - <CheckboxChip value=open class=open_style> + <CheckboxChip value=open class=open_style submit> <span>{group.number.to_string()}</span> <div class="col-container group-text-col"> {group.periods.iter().map(|p| { @@ -58,18 +62,22 @@ fn GroupsChips( } #[component] -fn GroupsSettings( +fn GroupsSettings<F>( groups: Groups, open: Vec<RwSignal<bool>>, course_sigle: CompactString, group_type: GroupType, -) -> impl IntoView { + submit: F, +) -> impl IntoView +where + F: Fn() + Copy + 'static, +{ view! { {groups.into_iter().enumerate().map(|(i, group)| { let open_style = if group.open {"group-chip"} else {"group-chip closed-group"}; let open = open[i]; view! { - <GroupsChips open open_style group course_sigle=course_sigle.clone() group_type/> + <GroupsChips open open_style group course_sigle=course_sigle.clone() group_type submit/> } }).collect_view() } @@ -77,7 +85,10 @@ fn GroupsSettings( } #[component] -fn CourseTab(course: ReactiveCourse, active_tab: ReadSignal<String>) -> impl IntoView { +fn CourseTab<F>(course: ReactiveCourse, active_tab: ReadSignal<String>, submit: F) -> impl IntoView +where + F: Fn() + Copy + 'static, +{ let course_sigle = course.sigle.clone(); view! { <Tab active_tab tab_id=course.sigle.to_string()> @@ -90,7 +101,7 @@ fn CourseTab(course: ReactiveCourse, active_tab: ReadSignal<String>) -> impl Int view!{ <div> <h3>"Théorie"</h3> - <GroupsSettings groups open=theo_open course_sigle group_type=GroupType::TheoGroup/> + <GroupsSettings groups open=theo_open course_sigle group_type=GroupType::TheoGroup submit/> </div> }.into_view() }, @@ -99,7 +110,7 @@ fn CourseTab(course: ReactiveCourse, active_tab: ReadSignal<String>) -> impl Int view!{ <div> <h3>"Laboratoire"</h3> - <GroupsSettings groups open=lab_open course_sigle=course_sigle.clone() group_type=GroupType::LabGroup/> + <GroupsSettings groups open=lab_open course_sigle=course_sigle.clone() group_type=GroupType::LabGroup submit/> </div> }.into_view() }, @@ -109,12 +120,12 @@ fn CourseTab(course: ReactiveCourse, active_tab: ReadSignal<String>) -> impl Int view!{ <div> <h3>"Théorie"</h3> - <GroupsSettings groups=theo_groups open=theo_open course_sigle=course_sigle.clone() group_type=GroupType::TheoGroup/> + <GroupsSettings groups=theo_groups open=theo_open course_sigle=course_sigle.clone() group_type=GroupType::TheoGroup submit/> </div> <div class="vertical-bar"></div> <div> <h3>"Laboratoire"</h3> - <GroupsSettings groups=lab_groups open=lab_open course_sigle=course_sigle.clone() group_type=GroupType::LabGroup/> + <GroupsSettings groups=lab_groups open=lab_open course_sigle=course_sigle.clone() group_type=GroupType::LabGroup submit/> </div> }.into_view() }, @@ -123,7 +134,7 @@ fn CourseTab(course: ReactiveCourse, active_tab: ReadSignal<String>) -> impl Int view!{ <div> <h3>"Théorie et laboratoire lié"</h3> - <GroupsSettings groups open=both_open course_sigle=course_sigle group_type=GroupType::LabGroup/> + <GroupsSettings groups open=both_open course_sigle=course_sigle group_type=GroupType::LabGroup submit/> </div> }.into_view() }, @@ -146,7 +157,7 @@ where future=get_courses let:courses > - <SearchCourse courses=courses.clone() set_selections set_active_tab/> + <SearchCourse courses=courses.clone() set_selections set_active_tab submit/> </Await> <div class="row-container tab-width"> <button class="tab-button chips" class=("tab-selected", move || active_tab.get() == "") id="personal" on:click={ @@ -171,7 +182,8 @@ where <button class="close" on:click={ let sigle = sigle.clone(); move |_| { - set_selections.update(|courses| courses.retain(|c| c.sigle.as_str() != sigle)) + set_selections.update(|courses| courses.retain(|c| c.sigle.as_str() != sigle)); + submit(); } }><X weight=IconWeight::Regular size="16px"/></button> </button> @@ -187,7 +199,7 @@ where key=|c| c.sigle.clone() let:course > - <CourseTab course active_tab/> + <CourseTab course active_tab submit/> </For> } } diff --git a/aep-schedule-website/src/frontend/components/options/form.rs b/aep-schedule-website/src/frontend/components/options/form.rs index 11fe24e9bbc1b3fdcc8be9c76ebee8f44b78c6c8..01cf4fc12989f19c9171f3debcc87b07d3f03aa3 100644 --- a/aep-schedule-website/src/frontend/components/options/form.rs +++ b/aep-schedule-website/src/frontend/components/options/form.rs @@ -1,23 +1,35 @@ -use crate::frontend::components::{ - common::number_input::NumberInput, - options::{ - courses_selector::CoursesSelector, optimizations::SelectOptimizations, state::OptionState, +use crate::frontend::{ + components::{ + common::number_input::NumberInput, + options::{ + courses_selector::CoursesSelector, optimizations::SelectOptimizations, + state::OptionState, + }, }, + pages::generator::FirstGenerationDone, }; use aep_schedule_generator::algorithm::{generation::SchedulesOptions, schedule::Schedule}; use leptos::*; #[component] pub fn OptionsForms(action: Action<SchedulesOptions, Vec<Schedule>>) -> impl IntoView { - let state = OptionState::default(); + let state: OptionState = use_context().unwrap(); - let submit = move || action.dispatch((&state).into()); + let first_generation_done: FirstGenerationDone = use_context().unwrap(); + let submit = move || { + if !first_generation_done.0.get() { + return; + } + action.dispatch((&state).into()); + }; + + let submit_mobile = move |_| action.dispatch((&state).into()); view! { <CoursesSelector state=state submit/> <span class="grow"></span> <div class="row-container input-item auto-bottom"><p>"Nombre de conflits maximum"</p><NumberInput value=state.max_nb_conflicts max=127/></div> <SelectOptimizations state=state submit/> - <button on:click=move |_| {submit()} class="select-none rounded-lg bg-amber-500 py-2 text-xl px-4 w-64 self-center text-center align-middle text-black shadow-md shadow-amber-500/20 transition-all hover:shadow-lg hover:shadow-amber-500/40 focus:opacity-[0.85] focus:shadow-none active:opacity-[0.85] active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none">"Générer les horaires"</button> + <button on:click=submit_mobile class="lg:hidden select-none rounded-lg bg-amber-500 py-2 text-xl px-4 w-64 self-center text-center align-middle text-black shadow-md shadow-amber-500/20 transition-all hover:shadow-lg hover:shadow-amber-500/40 focus:opacity-[0.85] focus:shadow-none active:opacity-[0.85] active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none">"Générer les horaires"</button> } } diff --git a/aep-schedule-website/src/frontend/components/options/search.rs b/aep-schedule-website/src/frontend/components/options/search.rs index 3a6c12a55ad251107259f112458d133743f79ede..ea932b6d7b20d506c7ae8417a6a058128d276cb7 100644 --- a/aep-schedule-website/src/frontend/components/options/search.rs +++ b/aep-schedule-website/src/frontend/components/options/search.rs @@ -7,11 +7,15 @@ use aep_schedule_generator::data::course::CourseName; use leptos::*; #[component] -pub fn SearchCourse( +pub fn SearchCourse<F>( courses: Result<Vec<CourseName>, ServerFnError>, set_selections: WriteSignal<Vec<ReactiveCourse>>, set_active_tab: WriteSignal<String>, -) -> impl IntoView { + submit: F, +) -> impl IntoView +where + F: Fn() + Copy + 'static, +{ let Ok(courses) = courses else { return None; }; @@ -21,7 +25,7 @@ pub fn SearchCourse( .collect(); let add_course = create_action( - |(sigle, set): &(String, WriteSignal<Vec<ReactiveCourse>>)| { + move |(sigle, set): &(String, WriteSignal<Vec<ReactiveCourse>>)| { let sigle = sigle.clone(); let set = *set; async move { @@ -31,6 +35,7 @@ pub fn SearchCourse( s.push(c.into()) } }); + submit(); } } }, @@ -38,7 +43,7 @@ pub fn SearchCourse( let on_submit = move |sigle: String| { set_active_tab(sigle.clone()); - add_course.dispatch((sigle, set_selections)) + add_course.dispatch((sigle, set_selections)); }; Some(view! { diff --git a/aep-schedule-website/src/frontend/components/options/todo.rs b/aep-schedule-website/src/frontend/components/options/todo.rs index d623481cebf04b91176a6aa00372185839be61c3..40e1e7f1ea956c9046c8f29fec5828bb9f0d1d3c 100644 --- a/aep-schedule-website/src/frontend/components/options/todo.rs +++ b/aep-schedule-website/src/frontend/components/options/todo.rs @@ -1,5 +1,10 @@ +use aep_schedule_generator::algorithm::{generation::SchedulesOptions, schedule::Schedule}; use leptos::*; +use crate::frontend::{ + components::options::state::OptionState, pages::generator::FirstGenerationDone, +}; + #[component] pub fn Step(n: u8, title: &'static str, description: &'static str) -> impl IntoView { view! { @@ -26,7 +31,15 @@ pub fn Step(n: u8, title: &'static str, description: &'static str) -> impl IntoV } #[component] -pub fn Todo() -> impl IntoView { +pub fn Todo(action: Action<SchedulesOptions, Vec<Schedule>>) -> impl IntoView { + let state: OptionState = use_context().unwrap(); + let first_generation_done: FirstGenerationDone = use_context().unwrap(); + + let submit = move |_| { + first_generation_done.0.set(true); + action.dispatch((&state).into()) + }; + view! { <div class="px-4 py-4 mx-auto"> <div class="grid gap-6 row-gap-10"> @@ -46,7 +59,7 @@ pub fn Todo() -> impl IntoView { </div> </div> <div class="pt-1"> - <p class="mb-2 text-lg font-bold">"Générer un horaire"</p> + <button on:click=submit class="select-none rounded-lg bg-amber-500 py-2 text-xl px-4 w-64 self-center text-center align-middle text-black shadow-md shadow-amber-500/20 transition-all hover:shadow-lg hover:shadow-amber-500/40 focus:opacity-[0.85] focus:shadow-none active:opacity-[0.85] active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none">"Générer les horaires"</button> <p class="text-gray-700"></p> </div> </div> diff --git a/aep-schedule-website/src/frontend/components/schedules.rs b/aep-schedule-website/src/frontend/components/schedules.rs index eaddf11e4af0ed504c3b4efdf4a8f81fbe4db134..d7ac67e761db96412e5d28e1e8832c33af622a1c 100644 --- a/aep-schedule-website/src/frontend/components/schedules.rs +++ b/aep-schedule-website/src/frontend/components/schedules.rs @@ -1,10 +1,14 @@ use crate::frontend::components::options::todo::Todo; use crate::{backend::routes::get_calendar, frontend::components::schedule::ScheduleComponent}; +use aep_schedule_generator::algorithm::generation::SchedulesOptions; use aep_schedule_generator::algorithm::schedule::Schedule; use leptos::*; #[component] -pub fn SchedulesComponent(read_signal: RwSignal<Option<Vec<Schedule>>>) -> impl IntoView { +pub fn SchedulesComponent( + read_signal: RwSignal<Option<Vec<Schedule>>>, + action: Action<SchedulesOptions, Vec<Schedule>>, +) -> impl IntoView { view! { <Await future=get_calendar @@ -18,7 +22,7 @@ pub fn SchedulesComponent(read_signal: RwSignal<Option<Vec<Schedule>>>) -> impl } }).collect_view(), None => view ! { - <Todo/> + <Todo action/> } } } diff --git a/aep-schedule-website/src/frontend/pages/apropos.rs b/aep-schedule-website/src/frontend/pages/apropos.rs index 913f4087913637b0c56fecef69e0a2ae19261fc4..04e5edcaf37867c99cef36a8ded1b71f53a748a1 100644 --- a/aep-schedule-website/src/frontend/pages/apropos.rs +++ b/aep-schedule-website/src/frontend/pages/apropos.rs @@ -16,7 +16,7 @@ pub fn HomePage() -> impl IntoView { </div> <div> <h3 class="font-semibold">"Qui sont les auteurs de la refonte du générateur d'horaire?"</h3> - <p class="mt-1 dark:text-gray-600">"Marc-Antoine Manningham - Création du frontend et du backend entièrement en Rust. Raphael Salvas, Achille Saint-Hillier, Sunnee Chevalier et Gabriel Billard - Inspiration de leur TP3 de LOG2420 pour l'interface la refonte du générateur"</p> + <p class="mt-1 dark:text-gray-600">"Marc-Antoine Manningham - Création du frontend et du backend entièrement en Rust. Raphael Salvas, Achille Saint-Hillier, Sunnee Chevalier et Gabriel Billard - Inspiration de leur TP3 de LOG2420 pour l'interface de la refonte du générateur"</p> </div> <div> <h3 class="font-semibold">"Comment le générateur génère aussi vite?"</h3> diff --git a/aep-schedule-website/src/frontend/pages/generator.rs b/aep-schedule-website/src/frontend/pages/generator.rs index 9a549e584dcdda99c7f23d83deefadc05f5edcde..8d3c6a774dfef08c58c5a615e950f04c5383d03c 100644 --- a/aep-schedule-website/src/frontend/pages/generator.rs +++ b/aep-schedule-website/src/frontend/pages/generator.rs @@ -1,5 +1,6 @@ use crate::frontend::components::icons::{caret_double_right::CaretDoubleRight, IconWeight}; use crate::frontend::components::notifications::Notifications; +use crate::frontend::components::options::state::OptionState; use crate::frontend::components::{options::form::OptionsForms, schedules::SchedulesComponent}; use aep_schedule_generator::algorithm::generation::SchedulesOptions; use aep_schedule_generator::data::group_sigle::SigleGroup; @@ -8,9 +9,14 @@ use leptos::*; #[derive(Clone, Copy)] pub struct SetModal(pub WriteSignal<Option<SigleGroup>>); +#[derive(Clone, Copy)] +pub struct FirstGenerationDone(pub RwSignal<bool>); + #[component] pub fn GeneratorPage() -> impl IntoView { let (hide, set_hide) = create_signal(false); + let first_generation_done = create_rw_signal(false); + // Creates a reactive value to update the button let action = create_action(move |s: &SchedulesOptions| { let s = s.clone(); @@ -20,14 +26,18 @@ pub fn GeneratorPage() -> impl IntoView { let (modal, set_modal) = create_signal(None); + let state = OptionState::default(); + + provide_context(state); provide_context(SetModal(set_modal)); + provide_context(FirstGenerationDone(first_generation_done)); view! { <aside class="left-panel" class=("hide-left-panel", hide)> <OptionsForms action=action/> </aside> <section class="right-panel"> - <SchedulesComponent read_signal=action.value()/> + <SchedulesComponent action=action read_signal=action.value()/> </section> <Notifications modal set_modal/> <button on:click=move |_| {set_hide(false)} id="go-back"><CaretDoubleRight weight=IconWeight::Regular size="3vh"/></button>