diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce54b16ee9673453880c930b769c46e0b20a6da..07431aba1afaef012b8a31013cd120f21ec2d244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,4 +2,26 @@ All notable changes to this project will be documented in this file. +## [unreleased] + +### 🚀 Features + +- Improve schedule rerendering +- Implement infinite scroll + +### 🛠Bug Fixes + +- Localstorage not updated +- Autocomplete now take in charge the last character +- Improve accessibility by shrinking automatically the tab + +### 🚜 Refactor + +- Improve state management +- Improve schedule generation call + +### âš™ï¸ Miscellaneous Tasks + +- Cleanup the codebase and small bugfix + <!-- generated by git-cliff --> diff --git a/aep-schedule-generator/src/algorithm/schedule.rs b/aep-schedule-generator/src/algorithm/schedule.rs index 339516f96c728e297e5660a8da5f1cc35a6c7768..15847179e58f5f2dd97eaac4a40b7f3321eb0bc2 100644 --- a/aep-schedule-generator/src/algorithm/schedule.rs +++ b/aep-schedule-generator/src/algorithm/schedule.rs @@ -18,7 +18,10 @@ use ical::{ ical_property, property::Property, }; -use std::cmp::Ordering; +use std::{ + cmp::Ordering, + hash::{DefaultHasher, Hash, Hasher}, +}; use uuid::Uuid; #[derive(PartialEq, Debug, Clone)] @@ -121,14 +124,20 @@ impl<'a> ScheduleBuilder<'a> { } else { 7 }; - let taken_courses = self + let taken_courses: Vec<TakenCourse> = self .taken_courses .iter() .map(|c| c.build(self.courses)) .collect(); + + let mut hasher = DefaultHasher::new(); + taken_courses.hash(&mut hasher); + let id = hasher.finish(); + Schedule { taken_courses, last_day, + id, } } } @@ -137,6 +146,7 @@ impl<'a> ScheduleBuilder<'a> { pub struct Schedule { pub taken_courses: Vec<TakenCourse>, pub last_day: u8, + pub id: u64, } impl Schedule { diff --git a/aep-schedule-generator/src/algorithm/schedules.rs b/aep-schedule-generator/src/algorithm/schedules.rs index 3dbf24433491a49dba1cfdf766e3c0a25554fdce..5b42bf6458f86ea1b9162ae04e6cfa111988e96d 100644 --- a/aep-schedule-generator/src/algorithm/schedules.rs +++ b/aep-schedule-generator/src/algorithm/schedules.rs @@ -34,7 +34,6 @@ impl<'a> Schedules<'a> { .into_sorted_vec() .into_iter() .map(|r| r.0.build()) - .rev() .collect() } diff --git a/aep-schedule-generator/src/algorithm/taken_course.rs b/aep-schedule-generator/src/algorithm/taken_course.rs index 5a0e3b3b5eb5189b8d4de28f7d73b21a26c8fdf9..a7968e357a27366a601c160561a1981c47a35327 100644 --- a/aep-schedule-generator/src/algorithm/taken_course.rs +++ b/aep-schedule-generator/src/algorithm/taken_course.rs @@ -81,7 +81,7 @@ impl TakenCourseBuilder { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] pub enum TakenCourseType { TheoOnly { theo_group: Group }, LabOnly { lab_group: Group }, @@ -89,7 +89,7 @@ pub enum TakenCourseType { Linked { theo_group: Group, lab_group: Group }, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] pub struct TakenCourse { pub sigle: String, pub name: String, diff --git a/aep-schedule-generator/src/data/group.rs b/aep-schedule-generator/src/data/group.rs index 6e0129b963083f339113b65a3d32d48a890e3e21..3d6dbde613a637232c13e7d43494ca2f0b650811 100644 --- a/aep-schedule-generator/src/data/group.rs +++ b/aep-schedule-generator/src/data/group.rs @@ -4,7 +4,7 @@ use super::{ }; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Hash)] pub struct Group { pub number: GroupIndex, pub open: bool, diff --git a/aep-schedule-generator/src/data/time/day.rs b/aep-schedule-generator/src/data/time/day.rs index ad5bc6bf112eb38a4c5a17de67f30329c1e573c6..1771247056fbe8b05e7bb0a53d589a539a764bb8 100644 --- a/aep-schedule-generator/src/data/time/day.rs +++ b/aep-schedule-generator/src/data/time/day.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; // There is no course the saturday at Poly, but knowing them, it wouldn't be far // stretched to assume that, it could. #[repr(u8)] -#[derive(Deserialize, Serialize, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Deserialize, Serialize, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Day { Monday = 0, Tuesday = 1, diff --git a/aep-schedule-generator/src/data/time/hours.rs b/aep-schedule-generator/src/data/time/hours.rs index 41e2fdaa9a7225d25c7af0536ce5841ef38d4359..be21f741fc81c800fe3041a54b2f67de1942d08e 100644 --- a/aep-schedule-generator/src/data/time/hours.rs +++ b/aep-schedule-generator/src/data/time/hours.rs @@ -6,7 +6,7 @@ use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}; pub const NO_HOUR: Hours = Hours(0); -#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)] pub struct Hours(pub u64); impl Debug for Hours { diff --git a/aep-schedule-generator/src/data/time/period.rs b/aep-schedule-generator/src/data/time/period.rs index d0c46d49c02f0de2f962eb215d67463bf1019ceb..10e4465a355971ec944e0e17e040065aef492259 100644 --- a/aep-schedule-generator/src/data/time/period.rs +++ b/aep-schedule-generator/src/data/time/period.rs @@ -2,7 +2,7 @@ use super::{day::Day, hours::Hours, week_number::WeekNumber}; use compact_str::CompactString; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Period { pub day: Day, pub room: CompactString, diff --git a/aep-schedule-generator/src/data/time/week_number.rs b/aep-schedule-generator/src/data/time/week_number.rs index 56f871fd034fe2481e6ebbb38b3162cb93e8ab59..9b519b62b160c15dfce349b6fe6fc738f18971ca 100644 --- a/aep-schedule-generator/src/data/time/week_number.rs +++ b/aep-schedule-generator/src/data/time/week_number.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Display; #[repr(u8)] -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum WeekNumber { B1 = 0, B2 = 1, diff --git a/aep-schedule-website/src/frontend/components/common/autocomplete.rs b/aep-schedule-website/src/frontend/components/common/autocomplete.rs index c15deeae570f9181a075e66ad7dbaa845b118aae..731eb50335493ac76641637a564ace6802c59789 100644 --- a/aep-schedule-website/src/frontend/components/common/autocomplete.rs +++ b/aep-schedule-website/src/frontend/components/common/autocomplete.rs @@ -27,14 +27,11 @@ fn get_suggestions( .partition_point(|c| c.value[0..cmp::min(i, c.value.len())] < input_value[0..i]); let top = sorted_possibilities .partition_point(|c| c.value[0..cmp::min(i, c.value.len())] <= input_value[0..i]); - if bottom < sorted_possibilities.len() && sorted_possibilities[bottom].value == input_value { - is_hidden.set(false); - return; - } - - is_hidden.set(true); suggestion_range.set(bottom..top); + is_hidden.set( + !(bottom < sorted_possibilities.len() && sorted_possibilities[bottom].value == input_value), + ); } #[component] diff --git a/aep-schedule-website/src/frontend/components/common/tab.rs b/aep-schedule-website/src/frontend/components/common/tab.rs index 908181257377cbd73b890473293bfd68859c1b68..e330d3bcd7569118ad1f8c20d13a7ac18407decb 100644 --- a/aep-schedule-website/src/frontend/components/common/tab.rs +++ b/aep-schedule-website/src/frontend/components/common/tab.rs @@ -3,7 +3,7 @@ use leptos::*; #[component] pub fn Tab(active_tab: ReadSignal<String>, tab_id: String, children: Children) -> impl IntoView { view! { - <div class="relative card tab tab-width" class=("hidden", {move || tab_id != active_tab.get()})> + <div class="relative card tab shrink w-full overflow-y-auto" class=("hidden", {move || tab_id != active_tab.get()})> {children()} </div> } 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 5e60c6c0243b42039462d7485812dac943cd266c..cb2776cfd8def890dc4d9b882f51175e974c32c6 100644 --- a/aep-schedule-website/src/frontend/components/options/courses_selector.rs +++ b/aep-schedule-website/src/frontend/components/options/courses_selector.rs @@ -1,5 +1,3 @@ -use super::state::OptionState; -use super::state::ReactiveCourse; use crate::backend::routes::get_courses; use crate::frontend::components::common::tab::Tab; use crate::frontend::components::icons::bell_ringing::BellRinging; @@ -8,8 +6,10 @@ use crate::frontend::components::icons::x::X; use crate::frontend::components::icons::IconWeight; use crate::frontend::components::options::personal::PersonalTimeSelector; use crate::frontend::components::options::search::SearchCourse; -use crate::frontend::components::options::state::ReactiveCourseType; use crate::frontend::pages::generator::SetModal; +use crate::frontend::state::reactive_course::ReactiveCourse; +use crate::frontend::state::reactive_course::ReactiveCourseType; +use crate::frontend::state::OptionState; use aep_schedule_generator::data::group::Group; use aep_schedule_generator::data::group_sigle::GroupType; use aep_schedule_generator::data::group_sigle::SigleGroup; @@ -28,7 +28,7 @@ fn GroupsChips<F>( where F: Fn() + Copy + 'static, { - let set_modal = use_context::<SetModal>().unwrap().0; + let set_modal = SetModal::from_context(); view! { <div on:click=move |_| { @@ -112,7 +112,7 @@ where ReactiveCourseType::LabOnly { lab_open, lab_groups } => { let groups = lab_groups; view!{ - <div class="flex gap-2 flex-col pb-2 max-h-[26rem] overflow-y-auto"> + <div class="flex gap-2 flex-col pb-2 overflow-y-auto"> <h3>"Laboratoire"</h3> <GroupsSettings groups open=lab_open course_sigle=course_sigle.clone() group_type=GroupType::LabGroup submit/> </div> @@ -122,12 +122,12 @@ where let theo_groups = theo_groups; let lab_groups = lab_groups; view!{ - <div class="flex gap-2 flex-col pb-2 max-h-[26rem] overflow-y-auto"> + <div class="flex gap-2 flex-col pb-2 overflow-y-auto"> <h3>"Théorie"</h3> <GroupsSettings groups=theo_groups open=theo_open course_sigle=course_sigle.clone() group_type=GroupType::TheoGroup submit/> </div> <div class="vertical-bar"></div> - <div class="flex gap-2 flex-col pb-2 max-h-[26rem] overflow-y-auto"> + <div class="flex gap-2 flex-col pb-2 overflow-y-auto"> <h3>"Laboratoire"</h3> <GroupsSettings groups=lab_groups open=lab_open course_sigle=course_sigle.clone() group_type=GroupType::LabGroup submit/> </div> @@ -136,7 +136,7 @@ where ReactiveCourseType::Linked { both_open, theo_groups, lab_groups } => { let groups = theo_groups.merge(lab_groups); view!{ - <div class="flex gap-2 flex-col pb-2 max-h-[26rem] overflow-y-auto"> + <div class="flex gap-2 flex-col pb-2 overflow-y-auto"> <h3>"Théorie et laboratoire lié"</h3> <GroupsSettings groups open=both_open course_sigle=course_sigle group_type=GroupType::LabGroup submit/> </div> @@ -165,7 +165,7 @@ where > <SearchCourse courses=courses.clone() action_courses set_active_tab/> </Await> - <div class="flex tab-width"> + <div class="flex w-full flex-wrap"> <button class="tab-button chips" class=("tab-selected", move || active_tab.get() == "") id="personal" on:click={ move |_| set_active_tab.set("".to_string()) }> diff --git a/aep-schedule-website/src/frontend/components/options/form.rs b/aep-schedule-website/src/frontend/components/options/form.rs index d7d6efaf4959f7aaaee5618a666a37250bea8c9a..b8252a102db486e4bd5cbfa8fefe57bc7081ce9d 100644 --- a/aep-schedule-website/src/frontend/components/options/form.rs +++ b/aep-schedule-website/src/frontend/components/options/form.rs @@ -1,34 +1,24 @@ use crate::frontend::{ components::{ common::number_input::NumberInput, - options::{ - courses_selector::CoursesSelector, optimizations::SelectOptimizations, - state::OptionState, - }, + options::{courses_selector::CoursesSelector, optimizations::SelectOptimizations}, }, pages::generator::FirstGenerationDone, + state::OptionState, }; -use aep_schedule_generator::algorithm::{generation::SchedulesOptions, schedule::Schedule}; use leptos::*; #[component] -pub fn OptionsForms<F>( - action: Action<SchedulesOptions, Vec<Schedule>>, - step: ReadSignal<u8>, - validate: F, -) -> impl IntoView -where - F: Fn(OptionState) + Copy + 'static, -{ - let state: OptionState = use_context().unwrap(); +pub fn OptionsForms() -> impl IntoView { + let state = OptionState::from_context(); let first_generation_done: FirstGenerationDone = use_context().unwrap(); let submit = move || { - validate(state); - if !first_generation_done.0.get() || step.get() != 5 { + state.validate(); + if !first_generation_done.0.get() || state.step.get() != 5 { return; } - action.dispatch((&state).into()); + state.generate(); }; create_local_resource(state.action_courses.pending(), move |_| { @@ -36,7 +26,7 @@ where async move {} }); - let submit_mobile = move |_| action.dispatch((&state).into()); + let submit_mobile = move |_| state.generate(); view! { <CoursesSelector state=state submit/> diff --git a/aep-schedule-website/src/frontend/components/options/mod.rs b/aep-schedule-website/src/frontend/components/options/mod.rs index 588858cffdef257b5fcd2af607602ecfad5165cb..7a3dee47f147d46314c6b6f5d6d9443f4efe390a 100644 --- a/aep-schedule-website/src/frontend/components/options/mod.rs +++ b/aep-schedule-website/src/frontend/components/options/mod.rs @@ -3,5 +3,4 @@ pub mod form; pub mod optimizations; pub mod personal; pub mod search; -pub mod state; pub mod todo; diff --git a/aep-schedule-website/src/frontend/components/options/optimizations.rs b/aep-schedule-website/src/frontend/components/options/optimizations.rs index 7bb0d8f7d35e83538f318cfecbfeee1ef8e4a692..074d7668ec0d922ca363f982eed6a127977c633e 100644 --- a/aep-schedule-website/src/frontend/components/options/optimizations.rs +++ b/aep-schedule-website/src/frontend/components/options/optimizations.rs @@ -1,6 +1,8 @@ -use super::state::OptionState; -use crate::frontend::components::icons::{ - calendar_check::CalendarCheck, house::House, sun::Sun, sun_horizon::SunHorizon, IconWeight, +use crate::frontend::{ + components::icons::{ + calendar_check::CalendarCheck, house::House, sun::Sun, sun_horizon::SunHorizon, IconWeight, + }, + state::OptionState, }; use leptos::*; use std::cmp; @@ -52,7 +54,8 @@ where <input type="range" min="0" max="4" class="lg:w-24 w-16 accent-amber-500" prop:value=state.finish_early on:input=move |ev| { state.finish_early.set(event_target_value(&ev).parse::<u8>().unwrap()); submit(); - }/> + } + /> </div> </div> } diff --git a/aep-schedule-website/src/frontend/components/options/personal.rs b/aep-schedule-website/src/frontend/components/options/personal.rs index 81cf017db580aa246b8df4198f675e5df0d7f132..bcfebc147a438d6c0d9d7049f089458aa0735ef1 100644 --- a/aep-schedule-website/src/frontend/components/options/personal.rs +++ b/aep-schedule-website/src/frontend/components/options/personal.rs @@ -59,7 +59,7 @@ where } }; view! { - <Schedule col_height="0.35em"> + <Schedule col_height="0.4em"> {(0..5).into_iter().map(|i| { (0..26).into_iter().map(|j| { let j = 2 * j; diff --git a/aep-schedule-website/src/frontend/components/options/search.rs b/aep-schedule-website/src/frontend/components/options/search.rs index 02773d9c2bde40c8482d23a8fceb25b6ba7b47c3..12a223fc02b06214c21407a7fe82052901fc5239 100644 --- a/aep-schedule-website/src/frontend/components/options/search.rs +++ b/aep-schedule-website/src/frontend/components/options/search.rs @@ -1,5 +1,7 @@ -use super::state::ReactiveCourse; -use crate::frontend::components::common::autocomplete::{AutoComplete, AutoCompleteOption}; +use crate::frontend::{ + components::common::autocomplete::{AutoComplete, AutoCompleteOption}, + state::reactive_course::ReactiveCourse, +}; use aep_schedule_generator::data::course::CourseName; use leptos::*; diff --git a/aep-schedule-website/src/frontend/components/options/todo.rs b/aep-schedule-website/src/frontend/components/options/todo.rs index 6ba9845852a8e2158739b7ec2263e8d6233213f6..4d11b914e7721ebd45a5afbdab7c616c0b4084c8 100644 --- a/aep-schedule-website/src/frontend/components/options/todo.rs +++ b/aep-schedule-website/src/frontend/components/options/todo.rs @@ -1,16 +1,13 @@ use std::cmp::Ordering; -use aep_schedule_generator::algorithm::{generation::SchedulesOptions, schedule::Schedule}; use leptos::*; -use crate::frontend::{ - components::options::state::OptionState, pages::generator::FirstGenerationDone, -}; +use crate::frontend::{pages::generator::FirstGenerationDone, state::OptionState}; #[component] pub fn Step( n: u8, - step: ReadSignal<u8>, + step: RwSignal<u8>, title: &'static str, description: &'static str, #[prop(optional)] children: Option<Children>, @@ -48,20 +45,17 @@ pub fn Step( } #[component] -pub fn Todo( - action: Action<SchedulesOptions, Vec<Schedule>>, - step: ReadSignal<u8>, - section_error: RwSignal<String>, - personal_error: RwSignal<String>, -) -> impl IntoView { - let state: OptionState = use_context().unwrap(); +pub fn Todo() -> impl IntoView { + let state = OptionState::from_context(); let first_generation_done: FirstGenerationDone = use_context().unwrap(); let submit = move |_| { first_generation_done.0.set(true); - action.dispatch((&state).into()) + state.generate(); }; + let step = state.step; + let disab = move || { let step = step.get(); match step { @@ -76,10 +70,10 @@ pub fn Todo( <div class="lg:py-6 lg:pr-16"> <Step n=1 step title="Ajoutez vos cours" description="Utilisez la barre de recherche à gauche pour trouver et sélectionner vos cours. Une fois les cours sélectionnés, ils apparaîtront comme un onglet."/> <Step n=2 step title="Ouvrez des sections" description="Assurez d'avoir au moins une section d'ouverte pour la théorie et la pratique. En sélectionnant l'onglet du cours et en appuyant sur les sections."> - <span class="text-red-800">{section_error}</span> + <span class="text-red-800">{state.section_error}</span> </Step> <Step n=3 step title="Forcer des heures libres" description="Sélectionnez une plage de temps à avoir absolument libre en pressant et relâchant sur votre horaire personnel."> - <span class="text-red-800">{personal_error}</span> + <span class="text-red-800">{state.personal_error}</span> </Step> <Step n=4 step title="Ajustez les paramètres" description="Bougez les curseurs en bas pour ajuster vos préférences. Vous pouvez choisir d'avoir plus de congés, de commencer en moyenne les cours plus tôt ou plus tard, ou de finir en moyenne plus tôt."/> <div class="flex items-center"> diff --git a/aep-schedule-website/src/frontend/components/schedule.rs b/aep-schedule-website/src/frontend/components/schedule.rs index e90f77c989e924426ee65265c6a1a78d751748b6..9ac0044a14be11260bdbbe4a67fcc7aba98f6457 100644 --- a/aep-schedule-website/src/frontend/components/schedule.rs +++ b/aep-schedule-website/src/frontend/components/schedule.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use crate::frontend::components::common::schedule::{Schedule, ScheduleEvent}; use crate::frontend::components::icons::download::Download; use crate::frontend::components::icons::IconWeight; @@ -112,7 +114,7 @@ fn CoursePeriods<'a>(i: usize, course: &'a TakenCourse) -> impl IntoView { } #[component] -pub fn ScheduleComponent(schedule: Schedule, calendar: Calendar) -> impl IntoView { +pub fn ScheduleComponent(schedule: Schedule, calendar: Rc<Calendar>) -> impl IntoView { let schedule2 = schedule.clone(); let (download, set_download) = create_signal("".to_string()); let link: NodeRef<A> = create_node_ref(); diff --git a/aep-schedule-website/src/frontend/components/schedules.rs b/aep-schedule-website/src/frontend/components/schedules.rs index a03e5aa77fe57436d3c836033207903e494abb64..be07f722b4ab9e34b480f670ff9fc34975492b14 100644 --- a/aep-schedule-website/src/frontend/components/schedules.rs +++ b/aep-schedule-website/src/frontend/components/schedules.rs @@ -1,31 +1,38 @@ +use std::rc::Rc; + use crate::frontend::components::options::todo::Todo; +use crate::frontend::pages::generator::FirstGenerationDone; +use crate::frontend::state::OptionState; 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>>>, - action: Action<SchedulesOptions, Vec<Schedule>>, - section_error: RwSignal<String>, - personal_error: RwSignal<String>, - step: ReadSignal<u8>, -) -> impl IntoView { +pub fn SchedulesComponent() -> impl IntoView { + let state = OptionState::from_context(); + let first_generation_done: FirstGenerationDone = use_context().unwrap(); + view! { <Await future=get_calendar children=move |calendar| { - match (read_signal.get(), step.get() == 5) { - (Some(result), true) => result.into_iter().rev().map(|schedule| { - let calendar = calendar.clone().unwrap(); - let schedule = schedule.clone(); - view! { - <ScheduleComponent schedule calendar/> - } - }).collect_view(), + match state.step.get() == 5 && first_generation_done.0.get() { + true => { + let calendar = Rc::new(calendar.clone().unwrap()); + view !{ + <For + each=move || {state.schedule.get()} + key= |course| course.id + children= move |schedule| { + let calendar = Rc::clone(&calendar); + view !{ + <ScheduleComponent schedule calendar/> + } + } + /> + } + }, _ => view ! { - <Todo action step section_error personal_error/> + <Todo/> } } } diff --git a/aep-schedule-website/src/frontend/mod.rs b/aep-schedule-website/src/frontend/mod.rs index b84c897231a9cb3bc7791c3c7f679d2ceaab0184..db1870003e5a06d6e5e19f6b1937e5ea8389d47a 100644 --- a/aep-schedule-website/src/frontend/mod.rs +++ b/aep-schedule-website/src/frontend/mod.rs @@ -1,3 +1,4 @@ pub mod app; pub mod components; pub mod pages; +pub mod state; diff --git a/aep-schedule-website/src/frontend/pages/generator.rs b/aep-schedule-website/src/frontend/pages/generator.rs index cfc00c422508ff7de438a33d3faa50b92732fedf..9c5228ece8964a1ea280ed6099b896849ddfb783 100644 --- a/aep-schedule-website/src/frontend/pages/generator.rs +++ b/aep-schedule-website/src/frontend/pages/generator.rs @@ -1,13 +1,18 @@ 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 crate::frontend::state::OptionState; use aep_schedule_generator::data::group_sigle::SigleGroup; use leptos::*; #[derive(Clone, Copy)] -pub struct SetModal(pub WriteSignal<Option<SigleGroup>>); +pub struct SetModal(WriteSignal<Option<SigleGroup>>); + +impl SetModal { + pub fn from_context() -> WriteSignal<Option<SigleGroup>> { + use_context::<Self>().unwrap().0 + } +} #[derive(Clone, Copy)] pub struct FirstGenerationDone(pub RwSignal<bool>); @@ -16,70 +21,32 @@ pub struct FirstGenerationDone(pub RwSignal<bool>); pub fn GeneratorPage() -> impl IntoView { let (hide, set_hide) = create_signal(false); let first_generation_done = create_rw_signal(false); - - let (step, set_step) = create_signal(1); - - // Creates a reactive value to update the button - let action = create_action(move |s: &SchedulesOptions| { - let mut s = s.clone(); - set_hide(true); - s.apply_personal_schedule(); - async move { s.get_schedules().into_sorted_vec() } - }); - let (modal, set_modal) = create_signal(None); - let state = OptionState::default(); - let section_error = create_rw_signal("".to_string()); - let personal_error = create_rw_signal("".to_string()); - - let validate = move |state: OptionState| { - let mut options: SchedulesOptions = (&state).into(); - if options.courses_to_take.is_empty() { - set_step.set(1); - return; - } - let mut impossible_courses = options.get_impossible_course().into_iter(); - if let Some(first_impossible_course) = impossible_courses.next() { - let mut error = format!("Les sections des/du cours {}", first_impossible_course); - for impossible_course in impossible_courses { - error.push_str(", "); - error.push_str(&impossible_course); - } - error.push_str(" sont toutes fermées."); - section_error.set(error); - set_step.set(2); - return; - } - section_error.set("".to_string()); - options.apply_personal_schedule(); - let mut impossible_courses = options.get_impossible_course().into_iter(); - if let Some(first_impossible_course) = impossible_courses.next() { - let mut error = format!("Les sections des/du cours {}", first_impossible_course); - for impossible_course in impossible_courses { - error.push_str(", "); - error.push_str(&impossible_course); - } - error.push_str(" sont en conflits avec les heures libres sélectionnées."); - personal_error.set(error); - set_step.set(3); - return; - } - personal_error.set("".to_string()); - set_step.set(5); - }; - 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 validate step/> + <OptionsForms/> </aside> - <section class="right-panel"> - <SchedulesComponent section_error personal_error action=action read_signal=action.value() step/> + <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; + if (scroll_top + target.client_height() as f64 >= target.scroll_height() as f64 - 500.0) && state.step.get() == 5 { + state.regenerate(); + } + + }> + <SchedulesComponent/> </section> <Notifications modal set_modal/> <button on:click=move |_| {set_hide(false)} id="go-back"><CaretDoubleRight weight=IconWeight::Regular size="3vh"/></button> diff --git a/aep-schedule-website/src/frontend/state/mod.rs b/aep-schedule-website/src/frontend/state/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..ede69bf60010cad19e46573be99d2daef8f38ed6 --- /dev/null +++ b/aep-schedule-website/src/frontend/state/mod.rs @@ -0,0 +1,162 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +use aep_schedule_generator::{ + algorithm::{generation::SchedulesOptions, schedule::Schedule, scores::EvaluationOption}, + data::time::week::Week, +}; +use leptos::*; +use reactive_course::ReactiveCourse; + +use crate::backend::routes::get_course; + +pub mod reactive_course; + +#[derive(Copy, Clone)] +pub struct OptionState { + pub stored_courses: StoredValue<Vec<ReactiveCourse>>, + pub action_courses: Action<String, Vec<ReactiveCourse>>, + pub week: [RwSignal<u64>; 5], + pub max_nb_conflicts: RwSignal<u8>, + pub day_off: RwSignal<u8>, + pub morning: RwSignal<i8>, + pub finish_early: RwSignal<u8>, + pub section_error: RwSignal<String>, + pub personal_error: RwSignal<String>, + pub step: RwSignal<u8>, + pub hide: RwSignal<bool>, + pub schedule: RwSignal<Vec<Schedule>>, + pub max_size: StoredValue<AtomicUsize>, +} + +impl OptionState { + pub fn from_context() -> Self { + use_context().unwrap() + } + + pub fn validate(self) { + let mut options: SchedulesOptions = (&self).into(); + if options.courses_to_take.is_empty() { + self.step.set(1); + return; + } + let mut impossible_courses = options.get_impossible_course().into_iter(); + if let Some(first_impossible_course) = impossible_courses.next() { + let mut error = format!("Les sections des/du cours {}", first_impossible_course); + for impossible_course in impossible_courses { + error.push_str(", "); + error.push_str(&impossible_course); + } + error.push_str(" sont toutes fermées."); + self.section_error.set(error); + self.step.set(2); + return; + } + self.section_error.set("".to_string()); + options.apply_personal_schedule(); + let mut impossible_courses = options.get_impossible_course().into_iter(); + if let Some(first_impossible_course) = impossible_courses.next() { + let mut error = format!("Les sections des/du cours {}", first_impossible_course); + for impossible_course in impossible_courses { + error.push_str(", "); + error.push_str(&impossible_course); + } + error.push_str(" sont en conflits avec les heures libres sélectionnées."); + self.personal_error.set(error); + self.step.set(3); + return; + } + self.personal_error.set("".to_string()); + self.step.set(5); + } + + pub fn generate(&self) { + self.max_size + .update_value(|v| v.store(8, Ordering::Relaxed)); + self.gen(); + } + + pub fn regenerate(&self) { + self.max_size.update_value(|size| { + let _ = size.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| { + Some(std::cmp::min(v * 2, 2usize.pow(12))) + }); + }); + self.gen(); + } + + fn gen(&self) { + let mut schedule_option: SchedulesOptions = self.into(); + schedule_option.apply_personal_schedule(); + let schedules = schedule_option.get_schedules().into_sorted_vec(); + self.schedule.set(schedules); + } +} + +impl Default for OptionState { + fn default() -> Self { + let stored_courses: StoredValue<Vec<ReactiveCourse>> = store_value(vec![]); + + let action_courses = create_action(move |sigle: &String| { + let sigle = sigle.clone(); + async move { + if let Ok(c) = get_course(sigle).await { + if !stored_courses + .get_value() + .iter() + .any(|react_c| react_c.sigle == c.sigle) + { + stored_courses.update_value(|courses| courses.push(c.into())); + } + } + stored_courses.get_value() + } + }); + + Self { + stored_courses, + action_courses, + max_nb_conflicts: create_rw_signal(0), + week: std::array::from_fn(|_i| create_rw_signal(0)), + day_off: create_rw_signal(3), + morning: create_rw_signal(1), + finish_early: create_rw_signal(1), + section_error: create_rw_signal("".to_string()), + personal_error: create_rw_signal("".to_string()), + step: create_rw_signal(0), + schedule: create_rw_signal(vec![]), + hide: create_rw_signal(false), + max_size: store_value(AtomicUsize::from(8)), + } + } +} + +impl From<&OptionState> for SchedulesOptions { + fn from(state: &OptionState) -> Self { + let courses_to_take = state + .action_courses + .value() + .get() + .unwrap_or_default() + .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 evaluation = EvaluationOption { + day_off: state.day_off.get(), + morning: state.morning.get(), + finish_early: state.finish_early.get(), + }; + let user_conflicts = Week::new(state.week.map(|s| s.get() << 2)); + Self { + courses_to_take, + max_nb_conflicts, + evaluation, + user_conflicts, + max_size, + } + } +} diff --git a/aep-schedule-website/src/frontend/components/options/state.rs b/aep-schedule-website/src/frontend/state/reactive_course.rs similarity index 65% rename from aep-schedule-website/src/frontend/components/options/state.rs rename to aep-schedule-website/src/frontend/state/reactive_course.rs index 8c9989b0eaa6f3224fad71946ffaa6052df54ece..3e6b0648cc46624e267b2f23da6326722139bc16 100644 --- a/aep-schedule-website/src/frontend/components/options/state.rs +++ b/aep-schedule-website/src/frontend/state/reactive_course.rs @@ -1,23 +1,7 @@ -use aep_schedule_generator::{ - algorithm::{generation::SchedulesOptions, scores::EvaluationOption}, - data::{course::Course, course_type::CourseType, groups::Groups, time::week::Week}, -}; +use aep_schedule_generator::data::{course::Course, course_type::CourseType, groups::Groups}; use compact_str::CompactString; use leptos::*; -use crate::backend::routes::get_course; - -#[derive(Copy, Clone)] -pub struct OptionState { - pub stored_courses: StoredValue<Vec<ReactiveCourse>>, - pub action_courses: Action<String, Vec<ReactiveCourse>>, - pub week: [RwSignal<u64>; 5], - pub max_nb_conflicts: RwSignal<u8>, - pub day_off: RwSignal<u8>, - pub morning: RwSignal<i8>, - pub finish_early: RwSignal<u8>, -} - #[derive(Clone, Debug)] pub enum ReactiveCourseType { TheoOnly { @@ -49,38 +33,6 @@ pub struct ReactiveCourse { pub nb_credit: usize, } -impl Default for OptionState { - fn default() -> Self { - let stored_courses: StoredValue<Vec<ReactiveCourse>> = store_value(vec![]); - - let action_courses = create_action(move |sigle: &String| { - let sigle = sigle.clone(); - async move { - if let Ok(c) = get_course(sigle).await { - if !stored_courses - .get_value() - .iter() - .any(|react_c| react_c.sigle == c.sigle) - { - stored_courses.update_value(|courses| courses.push(c.into())); - } - } - stored_courses.get_value() - } - }); - - Self { - stored_courses, - action_courses, - max_nb_conflicts: create_rw_signal(0), - week: std::array::from_fn(|_i| create_rw_signal(0)), - day_off: create_rw_signal(3), - morning: create_rw_signal(1), - finish_early: create_rw_signal(1), - } - } -} - impl From<ReactiveCourse> for Course { fn from(value: ReactiveCourse) -> Self { let course_type = match value.course_type { @@ -194,30 +146,3 @@ impl From<Course> for ReactiveCourse { } } } - -impl From<&OptionState> for SchedulesOptions { - fn from(state: &OptionState) -> Self { - let courses_to_take = state - .action_courses - .value() - .get() - .unwrap_or_default() - .into_iter() - .map(|c| c.into()) - .collect(); - let max_nb_conflicts = state.max_nb_conflicts.get(); - let evaluation = EvaluationOption { - day_off: state.day_off.get(), - morning: state.morning.get(), - finish_early: state.finish_early.get(), - }; - let user_conflicts = Week::new(state.week.map(|s| s.get() << 2)); - Self { - courses_to_take, - max_nb_conflicts, - evaluation, - user_conflicts, - max_size: 10, - } - } -} diff --git a/aep-schedule-website/style/constant.scss b/aep-schedule-website/style/constant.scss index fe36f9f0a9dde60e6a72009998b9240ee54fcd1c..cf3a170768a42fa8dca052275553a74ff95d69b8 100644 --- a/aep-schedule-website/style/constant.scss +++ b/aep-schedule-website/style/constant.scss @@ -1,5 +1,5 @@ $background-color: #151420; -$highlight-color: #F28A00; +$highlight-color: rgb(245 158 11); $highlight-text: #0F1741; $tern-color: #daa662; $tern-text: #3b3b3b; @@ -11,9 +11,10 @@ $success: #0FCC3B; @keyframes fadeIn { 0% { - opacity: 0; + opacity: 0; } + 100% { - opacity: 1; + opacity: 1; } } \ No newline at end of file diff --git a/aep-schedule-website/style/main.scss b/aep-schedule-website/style/main.scss index 8ff3c651c30d2f5598f8e043f24e84753d42c3b3..9c2943d14b805a978fedef3ddbece9f8c7a3ccbf 100644 --- a/aep-schedule-website/style/main.scss +++ b/aep-schedule-website/style/main.scss @@ -125,6 +125,8 @@ main { display: flex; flex-direction: column; align-items: center; + overflow-y: auto; + max-height: calc(100vh - 3em); gap: 3rem; padding: 1rem; background-color: $light-background; diff --git a/aep-schedule-website/style/options.scss b/aep-schedule-website/style/options.scss index 84bc1950a4bcefd632b737e81551faf95cb58d3f..051f66c92eb624ec7526bc1dd2dd20592e743f1d 100644 --- a/aep-schedule-website/style/options.scss +++ b/aep-schedule-website/style/options.scss @@ -102,11 +102,6 @@ transition-duration: 150ms; } -.tab-width { - flex-wrap: wrap; - width: 100%; -} - .tab-selected { background-color: $highlight-color; color: $tern-text; @@ -119,15 +114,6 @@ align-items: center; } -.number-input { - height: 22pt; - margin-left: auto; -} - -.title { - line-height: 0.1; -} - .selected-hour { background-color: rgb(87, 104, 175); } @@ -136,10 +122,6 @@ background-color: $light-background; } -.selection { - pointer-events: none; -} - .notif-modal { position: absolute; left: 0;