diff --git a/aep-schedule-generator/Cargo.toml b/aep-schedule-generator/Cargo.toml index e34fdea8ec68e95527e0a744a78cbb07a6222f4b..60d7e46ea5b6feac77be47a4be069808ca670a54 100644 --- a/aep-schedule-generator/Cargo.toml +++ b/aep-schedule-generator/Cargo.toml @@ -10,8 +10,7 @@ crate-type = ["rlib"] [profile.release] lto = true codegen-units = 1 -# strip = true -debug = true +strip = true [profile.dev] opt-level = 3 diff --git a/aep-schedule-generator/src/algorithm/conflicts.rs b/aep-schedule-generator/src/algorithm/conflicts.rs new file mode 100644 index 0000000000000000000000000000000000000000..cf7a7084c3dff7967170fe3313ce6c4a58d787cd --- /dev/null +++ b/aep-schedule-generator/src/algorithm/conflicts.rs @@ -0,0 +1,8 @@ +use compact_str::CompactString; + +pub struct Conflicts(Conflict, Conflict); + +pub enum Conflict { + Course { sigle: CompactString }, + Personal, +} diff --git a/aep-schedule-generator/src/algorithm/generation.rs b/aep-schedule-generator/src/algorithm/generation.rs index 1cc9ccca143cd4ddea56e9887cc07ba0ae9f6734..cf57da95b64a0af3c99be1da25197073b677ab29 100644 --- a/aep-schedule-generator/src/algorithm/generation.rs +++ b/aep-schedule-generator/src/algorithm/generation.rs @@ -1,4 +1,11 @@ -use super::{schedule::ScheduleBuilder, schedules::Schedules, scores::EvaluationOption}; +use compact_str::CompactString; + +use super::{ + //conflicts::Conflicts, + schedule::ScheduleBuilder, + schedules::Schedules, + scores::EvaluationOption, +}; use crate::data::{course::Course, time::week::Week}; #[derive(Default, Debug, Clone, PartialEq)] @@ -11,6 +18,27 @@ pub struct SchedulesOptions { } impl SchedulesOptions { + // pub fn get_simple_conflict<'a>(&'a self) -> Option<Conflicts> { + + // } + pub fn apply_personal_schedule<'a>(&'a mut self) { + self.courses_to_take + .iter_mut() + .for_each(|c| c.apply_week_mask(&self.user_conflicts)); + } + pub fn get_impossible_course<'a>(&'a self) -> Vec<CompactString> { + self.courses_to_take + .iter() + .filter(|c| c.is_impossible()) + .map(|c| c.sigle.clone()) + .collect() + } + pub fn get_nb_combinations<'a>(&'a self) -> usize { + self.courses_to_take + .iter() + .map(Course::nb_combinations) + .product() + } pub fn get_schedules<'a>(&'a self) -> Schedules<'a> { let mut schedules = Schedules::new(&self); if self.courses_to_take.len() == 0 { diff --git a/aep-schedule-generator/src/algorithm/mod.rs b/aep-schedule-generator/src/algorithm/mod.rs index 903073f074e77a2abc57a2391a95515ed4f3a198..efdaa18759b4c7861c00ca7c212c6537f7ff4695 100644 --- a/aep-schedule-generator/src/algorithm/mod.rs +++ b/aep-schedule-generator/src/algorithm/mod.rs @@ -1,3 +1,4 @@ +//pub mod conflicts; pub mod generation; pub mod schedule; pub mod schedules; diff --git a/aep-schedule-generator/src/algorithm/schedule.rs b/aep-schedule-generator/src/algorithm/schedule.rs index c5367f9258f61183520feb284207eac0cc0df33d..339516f96c728e297e5660a8da5f1cc35a6c7768 100644 --- a/aep-schedule-generator/src/algorithm/schedule.rs +++ b/aep-schedule-generator/src/algorithm/schedule.rs @@ -10,7 +10,6 @@ use crate::data::{ day::Day, hours::NO_HOUR, period::Period, - week::Week, weeks::Weeks, }, }; @@ -74,7 +73,6 @@ impl<'a> ScheduleBuilder<'a> { &self, n: u8, min: f64, - user_conflicts: &Week<5>, options: EvaluationOption, new_course: TakenCourseBuilder, ) -> Option<Self> { @@ -82,10 +80,6 @@ impl<'a> ScheduleBuilder<'a> { let mut is_cancelled = false; new_course.for_each_group(self.courses, |group| { for period in &group.periods { - if user_conflicts.user_conflict_in_day(period) { - is_cancelled = true; - return; - } if new_schedule.week.conflict_in_day(period) { new_schedule.conflicts += 1; if new_schedule.conflicts > n { diff --git a/aep-schedule-generator/src/algorithm/schedules.rs b/aep-schedule-generator/src/algorithm/schedules.rs index 80cdc6a62d14529aa195ce442648f5216079d4c5..3dbf24433491a49dba1cfdf766e3c0a25554fdce 100644 --- a/aep-schedule-generator/src/algorithm/schedules.rs +++ b/aep-schedule-generator/src/algorithm/schedules.rs @@ -46,12 +46,11 @@ impl<'a> Schedules<'a> { }; let min = self.get_min(); let e = self.options.evaluation; - let c = self.options.user_conflicts; match &course.course_type { CourseType::LabOnly { lab_groups } => { for lab_group in lab_groups.iter().filter(|g| g.open) { let course = TakenCourseBuilder::new(i, GroupIndex::none(), lab_group.into()); - if let Some(schedule) = schedule.add_check_conflicts(n, min, &c, e, course) { + if let Some(schedule) = schedule.add_check_conflicts(n, min, e, course) { self.get_schedules_rec(schedule, n, i + 1); } } @@ -59,7 +58,7 @@ impl<'a> Schedules<'a> { CourseType::TheoOnly { theo_groups } => { for theo_group in theo_groups.iter().filter(|g| g.open) { let course = TakenCourseBuilder::new(i, theo_group.into(), GroupIndex::none()); - if let Some(schedule) = schedule.add_check_conflicts(n, min, &c, e, course) { + if let Some(schedule) = schedule.add_check_conflicts(n, min, e, course) { self.get_schedules_rec(schedule, n, i + 1); } } @@ -72,8 +71,7 @@ impl<'a> Schedules<'a> { for lab_group in lab_groups.iter().filter(|g| g.open) { let course = TakenCourseBuilder::new(i, theo_group.into(), lab_group.into()); - if let Some(schedule) = schedule.add_check_conflicts(n, min, &c, e, course) - { + if let Some(schedule) = schedule.add_check_conflicts(n, min, e, course) { self.get_schedules_rec(schedule, n, i + 1); } } @@ -89,7 +87,7 @@ impl<'a> Schedules<'a> { .zip(lab_groups.iter().filter(|g| g.open)) { let course = TakenCourseBuilder::new(i, theo_group.into(), lab_group.into()); - if let Some(schedule) = schedule.add_check_conflicts(n, min, &c, e, course) { + if let Some(schedule) = schedule.add_check_conflicts(n, min, e, course) { self.get_schedules_rec(schedule, n, i + 1); } } diff --git a/aep-schedule-generator/src/data/course.rs b/aep-schedule-generator/src/data/course.rs index 6325c7ad76da4f75ba10c825b802bcfbf3b09b8d..070ab8f88221f4a3a3f4f83f00c7f6eb562ff7aa 100644 --- a/aep-schedule-generator/src/data/course.rs +++ b/aep-schedule-generator/src/data/course.rs @@ -3,6 +3,7 @@ use super::{ group::Group, group_index::GroupIndex, group_sigle::{GroupType, SigleGroup}, + time::week::Week, }; use compact_str::CompactString; use serde::{Deserialize, Serialize}; @@ -120,7 +121,17 @@ impl Course { } } } - + pub fn nb_combinations(&self) -> usize { + match &self.course_type { + CourseType::TheoOnly { theo_groups } => theo_groups.len(), + CourseType::LabOnly { lab_groups } => lab_groups.len(), + CourseType::Both { + theo_groups, + lab_groups, + } => theo_groups.len() * lab_groups.len(), + CourseType::Linked { theo_groups, .. } => theo_groups.len(), + } + } pub fn is_open(&self, sigle_group: &SigleGroup) -> bool { match &self.course_type { CourseType::TheoOnly { theo_groups } => theo_groups[sigle_group.group_index] @@ -145,4 +156,35 @@ impl Course { .is_some_and(|g| g.open), } } + pub fn is_impossible(&self) -> bool { + match &self.course_type { + CourseType::TheoOnly { theo_groups } => theo_groups.is_impossible(), + CourseType::LabOnly { lab_groups } => lab_groups.is_impossible(), + CourseType::Both { + theo_groups, + lab_groups, + } => theo_groups.is_impossible() | lab_groups.is_impossible(), + CourseType::Linked { theo_groups, .. } => theo_groups.is_impossible(), + } + } + pub fn apply_week_mask(&mut self, week: &Week<5>) { + match &mut self.course_type { + CourseType::TheoOnly { theo_groups } => theo_groups.apply_week_mask(week), + CourseType::LabOnly { lab_groups } => lab_groups.apply_week_mask(week), + CourseType::Both { + theo_groups, + lab_groups, + } => { + theo_groups.apply_week_mask(week); + lab_groups.apply_week_mask(week); + } + CourseType::Linked { + theo_groups, + lab_groups, + } => { + theo_groups.apply_week_mask(week); + lab_groups.apply_week_mask(week); + } + } + } } diff --git a/aep-schedule-generator/src/data/groups.rs b/aep-schedule-generator/src/data/groups.rs index 560d2adf34a35159c53772d6e0add3aad828607a..f6fdc31c8568f14257655b20c36394c5e909b96d 100644 --- a/aep-schedule-generator/src/data/groups.rs +++ b/aep-schedule-generator/src/data/groups.rs @@ -2,6 +2,7 @@ use super::{ group::Group, group_index::GroupIndex, group_sigle::{GroupType, SigleGroup}, + time::week::Week, }; use compact_str::CompactString; use serde::{Deserialize, Serialize}; @@ -37,6 +38,10 @@ impl Groups { self } + pub fn len(&self) -> usize { + self.iter().count() + } + pub fn into_iter(self) -> impl Iterator<Item = Group> { self.0.into_iter().filter_map(|g| g) } @@ -57,12 +62,22 @@ impl Groups { self[index].as_mut() } + pub fn is_impossible(&self) -> bool { + self.iter().all(|c| !c.open) + } + pub fn get_closed(&self, group_type: GroupType, sigle: &CompactString) -> Vec<SigleGroup> { self.iter() .filter(|g| !g.open) .map(|g| SigleGroup::new(sigle.clone(), group_type, g.number)) .collect() } + + pub fn apply_week_mask(&mut self, week: &Week<5>) { + self.iter_mut() + .filter(|g| g.periods.iter().any(|p| week.user_conflict_in_day(p))) + .for_each(|g| g.open = false); + } } impl Index<GroupIndex> for Groups { diff --git a/aep-schedule-generator/src/data/time/week.rs b/aep-schedule-generator/src/data/time/week.rs index ec95e751d1e52fdb76320acdc845f5a6259748e4..6e741d399192853840ffe6f7a8d0f5c8dd486e51 100644 --- a/aep-schedule-generator/src/data/time/week.rs +++ b/aep-schedule-generator/src/data/time/week.rs @@ -8,6 +8,7 @@ use std::ops::Deref; pub struct Week<const N: usize>([Hours; N]); impl<const N: usize> Default for Week<N> { + #[inline] fn default() -> Self { Self([NO_HOUR; N]) } @@ -15,21 +16,29 @@ impl<const N: usize> Default for Week<N> { impl<const N: usize> Deref for Week<N> { type Target = [Hours; N]; + #[inline] fn deref(&self) -> &Self::Target { &self.0 } } impl<const N: usize> Week<N> { + #[inline] pub fn new(week: [u64; N]) -> Self { Self(week.map(|d| Hours::from(d))) } + + #[inline] pub fn add_period(&mut self, period: &Period) { self.0[period.day as usize] |= period.hours; } + + #[inline] pub fn conflict_in_day(&self, period: &Period) -> bool { self.0[period.day as usize] & period.hours != NO_HOUR } + + #[inline] pub fn user_conflict_in_day(&self, period: &Period) -> bool { if period.day as usize >= N { return false; diff --git a/aep-schedule-generator/src/main.rs b/aep-schedule-generator/src/main.rs index 5b28fb8ca9e38a3269639a567a690b7e9f650108..bffaaf65a7966d5cdd80ae0bc65e2075c571c896 100644 --- a/aep-schedule-generator/src/main.rs +++ b/aep-schedule-generator/src/main.rs @@ -31,14 +31,16 @@ fn main() { max_size: 20, user_conflicts: Week::default(), }; + let nb_combination = options.get_nb_combinations(); let instant = Instant::now(); let schedules = options.get_schedules(); let number = schedules.number; let result = schedules.into_sorted_vec(); println!( - "{:?} computed in {:?} {} combinations evaluated", + "{:?} computed in {:?} {} combinations evaluated of {} total", result.len(), instant.elapsed().as_secs_f64(), number, + nb_combination, ); } diff --git a/aep-schedule-website/.cargo/config.toml b/aep-schedule-website/.cargo/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..1aa838e5390ab42c84455a8f630fda66c13b0c41 --- /dev/null +++ b/aep-schedule-website/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["-Z", "threads=8"] diff --git a/aep-schedule-website/Cargo.lock b/aep-schedule-website/Cargo.lock index 43a25ad611630e609c8df4eff6827281e9c96382..863ad386592587ff3f5909d53b371092ddfe0840 100644 --- a/aep-schedule-website/Cargo.lock +++ b/aep-schedule-website/Cargo.lock @@ -147,9 +147,9 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arrayref" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" [[package]] name = "arrayvec" @@ -159,9 +159,9 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-compression" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" +checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" dependencies = [ "flate2", "futures-core", @@ -178,7 +178,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -189,7 +189,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -209,7 +209,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -225,7 +225,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -300,7 +300,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -441,9 +441,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.4" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9711f33475c22aab363b05564a17d7b789bf3dfec5ebabb586adee56f0e271b5" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" [[package]] name = "cfg-if" @@ -783,7 +783,7 @@ checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -878,9 +878,9 @@ dependencies = [ [[package]] name = "email_address" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1019fa28f600f5b581b7a603d515c3f1635da041ca211b5055804788673abfe" +checksum = "46b7a0ac6570e31bfe2c6cf575a576a55af9893d1a6b30b4444e6e90b216bb84" [[package]] name = "encoding_rs" @@ -1019,7 +1019,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -1562,9 +1562,9 @@ dependencies = [ [[package]] name = "leptos" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5fae88b21265cbb847c891d7cf660e284a1282da15459691992be9358e906fb" +checksum = "57727cd8f6d1e78aa9721270002037d7f63b5a7a2b60a7830239f6938cbca9b7" dependencies = [ "cfg-if", "leptos_config", @@ -1582,9 +1582,9 @@ dependencies = [ [[package]] name = "leptos_axum" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac4250852926ccc78245dea46196217977342dda58cf7f19d35d4efcb0cd3ba" +checksum = "3923af454949eb7a5ea9a89d5fdc4d21e4850eb8d47d942d35355e5079444867" dependencies = [ "axum", "cfg-if", @@ -1606,9 +1606,9 @@ dependencies = [ [[package]] name = "leptos_config" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec33b6f994829469ba7c62bfd5fb572a639071d0de715a41c2aa0df86301a4fa" +checksum = "6e519478cf13b84e0169f14660fda6425a419d181903cf511cec416555c9ff51" dependencies = [ "config", "regex", @@ -1619,9 +1619,9 @@ dependencies = [ [[package]] name = "leptos_dom" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "867d2afc153cc0f1d6f775872d5dfc409385f4d8544831ee45f720d88f363e6b" +checksum = "46a32ccb530d95c82b522ff86827a9c14efb3d3a75c508c20605d4603beb1695" dependencies = [ "async-recursion", "cfg-if", @@ -1649,9 +1649,9 @@ dependencies = [ [[package]] name = "leptos_hot_reload" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5ce56051f2eff2c4736b7a2056177e67be19597b767ff72fbab20917a7422d" +checksum = "71eb2b309ff0e526d147e32afcbbbf39b43c1ed5b7264b7abfb6635c388c0e9e" dependencies = [ "anyhow", "camino", @@ -1661,15 +1661,15 @@ dependencies = [ "quote", "rstml", "serde", - "syn 2.0.71", + "syn 2.0.72", "walkdir", ] [[package]] name = "leptos_integration_utils" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ea6256cc3c42b516ee6a7f5885c6e4dcfc66ec84d1ae725dea17e3d22a3803d" +checksum = "313ea1dc9243d8803376c77fc191bf1c3b9c8e081e8c50428706bab009cb1e42" dependencies = [ "futures", "leptos", @@ -1681,9 +1681,9 @@ dependencies = [ [[package]] name = "leptos_macro" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "019016cc0193831660a7794aa046e4c01617d97ccb2f403a5c10b21744391a71" +checksum = "d38b41f4e38b6f0e26858ae40464e12702096d2219c48afd41b79693fd025a9d" dependencies = [ "attribute-derive", "cfg-if", @@ -1697,16 +1697,16 @@ dependencies = [ "quote", "rstml", "server_fn_macro", - "syn 2.0.71", + "syn 2.0.72", "tracing", "uuid", ] [[package]] name = "leptos_meta" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873ec368535d9971df4848ca21e91aa96fa00a6884258ab8f926db69ea97f547" +checksum = "206825db2cb802a9b06c1f33c08569086706a7fa4d8acb86e5ed6892a8dd2cec" dependencies = [ "cfg-if", "indexmap", @@ -1718,9 +1718,9 @@ dependencies = [ [[package]] name = "leptos_reactive" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076064e3c84e3aa12d4bad283e82ba5968675f5f9714d04a5c38f169cd4f26b5" +checksum = "bc38db7b14f2d48cb427dd4c197ee58354f5de9407fb3417f2ee7ff85097ffd3" dependencies = [ "base64", "cfg-if", @@ -1746,9 +1746,9 @@ dependencies = [ [[package]] name = "leptos_router" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66db10225f0caf60b0183935b288791ac3494ca6e1306d415ea97287b6c2db5" +checksum = "d397f1c3217368aaf6009ea0bf6bdfa47325ab9c8fb8c124eda5ebc527d11445" dependencies = [ "cached", "cfg-if", @@ -1778,9 +1778,9 @@ dependencies = [ [[package]] name = "leptos_server" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "603064a2d7ac46dba4b3ed5397a475076f9738918dd605670869dfe877d5966c" +checksum = "9c0ea6eed569e70fa4eed722ee3f042ed76c40d2b0a82b7240eb660893d6a5e9" dependencies = [ "inventory", "lazy_static", @@ -1882,7 +1882,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -1941,13 +1941,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ + "hermit-abi", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2047,16 +2048,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_threads" version = "0.1.7" @@ -2068,9 +2059,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.1" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" dependencies = [ "memchr", ] @@ -2099,9 +2090,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -2120,7 +2111,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -2131,9 +2122,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", @@ -2238,7 +2229,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -2311,7 +2302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -2374,7 +2365,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", "version_check", "yansi", ] @@ -2417,14 +2408,14 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] name = "quoted_printable" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" [[package]] name = "rand" @@ -2458,9 +2449,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags 2.6.0", ] @@ -2592,7 +2583,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.71", + "syn 2.0.72", "syn_derive", "thiserror", ] @@ -2624,9 +2615,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.11" +version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ "once_cell", "rustls-pki-types", @@ -2653,9 +2644,9 @@ checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.5" +version = "0.102.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" dependencies = [ "ring", "rustls-pki-types", @@ -2778,7 +2769,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -2826,9 +2817,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -2856,9 +2847,9 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06e6e5467a2cd93ce1accfdfd8b859404f0b3b2041131ffd774fabf666b8219" +checksum = "e9aaae169927ef701a4734d680adcb08f13269c9f0af822bf175d681fe56d65c" dependencies = [ "axum", "bytes", @@ -2891,26 +2882,26 @@ dependencies = [ [[package]] name = "server_fn_macro" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c216bb1c1ac890151397643c663c875a1836adf0b269be4e389cb1b48c173c" +checksum = "583085903fd5d091884eb52d99550e32777015af21f0f5dbec49bedcc320ce82" dependencies = [ "const_format", "convert_case", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", "xxhash-rust", ] [[package]] name = "server_fn_macro_default" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00783df297ec85ea605779f2fef9cbec98981dffe2e01e1a9845c102ee1f1ae6" +checksum = "b628649700e28cf8bc33e0df5de5c9d591a1828ba005a25b425477055f1e0e74" dependencies = [ "server_fn_macro", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3063,9 +3054,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.71" +version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote", @@ -3081,7 +3072,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3131,22 +3122,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3199,32 +3190,31 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3265,9 +3255,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c" dependencies = [ "serde", "serde_spanned", @@ -3277,18 +3267,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.15" +version = "0.22.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" +checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" dependencies = [ "indexmap", "serde", @@ -3372,7 +3362,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3407,7 +3397,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3517,9 +3507,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" @@ -3576,7 +3566,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", "wasm-bindgen-shared", ] @@ -3610,7 +3600,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3852,9 +3842,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c" dependencies = [ "memchr", ] @@ -3871,9 +3861,9 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63658493314859b4dfdf3fb8c1defd61587839def09582db50b8a4e93afca6bb" +checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" [[package]] name = "yansi" @@ -3898,7 +3888,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] diff --git a/aep-schedule-website/Cargo.toml b/aep-schedule-website/Cargo.toml index 186003719a9ab917be6284d44e3796fec38dac28..e5193b63afb31c5b702f50b545395b9644daaf33 100644 --- a/aep-schedule-website/Cargo.toml +++ b/aep-schedule-website/Cargo.toml @@ -66,10 +66,16 @@ ssr = [ "dep:tracing", ] +[profile.dev] +opt-level = 1 + +[profile.dev.package."*"] +opt-level = 3 + # Defines a size-optimized profile for the WASM bundle in release mode [profile.wasm-release] inherits = "release" -opt-level = 'z' +opt-level = 3 lto = true panic = "abort" codegen-units = 4 @@ -138,3 +144,5 @@ lib-default-features = false # # Optional. Defaults to "release". lib-profile-release = "wasm-release" + +tailwind-input-file = "style/tailwind.css" diff --git a/aep-schedule-website/public/favicon.ico b/aep-schedule-website/public/favicon.ico index 2ba8527cb12f5f28f331b8d361eef560492d4c77..d57cad78cbd6e3e7f6d886c752cca0134791a9a8 100644 Binary files a/aep-schedule-website/public/favicon.ico and b/aep-schedule-website/public/favicon.ico differ diff --git a/aep-schedule-website/src/backend/state.rs b/aep-schedule-website/src/backend/state.rs index a9b37e014192c7ef1f8d0b50cf59e1d959c80eca..391be8e2fde8e5545bc2f13b5bf4d468f8561f09 100644 --- a/aep-schedule-website/src/backend/state.rs +++ b/aep-schedule-website/src/backend/state.rs @@ -36,16 +36,20 @@ pub struct AppState { impl AppState { pub async fn new(leptos_options: LeptosOptions, routes: Vec<RouteListing>) -> Self { - let client = reqwest::Client::builder() - .user_agent("NCSA Mosaic/1.0 (X11;SunOS 4.1.4 sun4m)") - .build() - .unwrap(); - let horsage = client.get(HORSAGE_URL).send().await.unwrap(); - let horsage = horsage.text().await.unwrap(); - let fermes = client.get(FERME_URL).send().await.unwrap(); - let fermes = fermes.text().await.unwrap(); - fs::write("horsage.csv", horsage).expect("Unable to write file"); - fs::write("fermes.csv", fermes).expect("Unable to write file"); + #[cfg(not(debug_assertions))] + { + // Don't spam Poly when reloading the website in debug mode + let client = reqwest::Client::builder() + .user_agent("NCSA Mosaic/1.0 (X11;SunOS 4.1.4 sun4m)") + .build() + .unwrap(); + let horsage = client.get(HORSAGE_URL).send().await.unwrap(); + let horsage = horsage.text().await.unwrap(); + let fermes = client.get(FERME_URL).send().await.unwrap(); + let fermes = fermes.text().await.unwrap(); + fs::write("horsage.csv", horsage).expect("Unable to write file"); + fs::write("fermes.csv", fermes).expect("Unable to write file"); + } let horsage = BufReader::new(File::open("horsage.csv").unwrap()); let fermes = BufReader::new(File::open("fermes.csv").unwrap()); let courses = Arc::new(RwLock::new(Courses::from_csv(horsage, fermes))); diff --git a/aep-schedule-website/src/frontend/app.rs b/aep-schedule-website/src/frontend/app.rs index 9c86b325626e458e966ca54c0f196089e92125cd..eca83922131fe3449f2d9a55211cf88014262733 100644 --- a/aep-schedule-website/src/frontend/app.rs +++ b/aep-schedule-website/src/frontend/app.rs @@ -28,14 +28,19 @@ pub fn App() -> impl IntoView { <Router> <header> <nav class=is_active> - <A href="/">"Générateur d'horaire"</A> - <A href="/local">"Horaire d'un local"</A> - <A href="/apropos">"À propos"</A> + <span class="text-2xl font-semibold leading-none font-sans tracking-tight">"Générateur d'horaire de l'AEP" + <span class="text-amber-600">"v2"</span> + </span> + <A class="rounded-md font-medium text-gray-700 text-lg font-sans tracking-tight" href="/">"Générateur d'horaire"</A> + <A class="rounded-md font-medium text-gray-700 text-lg font-sans tracking-tight" href="/local">"Horaire d'un local"</A> + <A class="rounded-md font-medium text-gray-700 text-lg font-sans tracking-tight" href="/apropos">"À propos"</A> + + <a href="https://git.step.polymtl.ca/Lemark/aep-schedule-generator-rusty/-/issues/new" class="sources pad-left" target="_blank"> - <span>"Signaler un bug"</span> + <span class="rounded-md font-medium text-gray-700 text-lg font-sans tracking-tight">"Signaler un bug"</span> <Bug weight=IconWeight::Regular size="3vh"/> </a> - <a href="https://git.step.polymtl.ca/Lemark/aep-schedule-generator-rusty" class="sources" target="_blank" ><span>"Sources "</span><GitlabLogo weight=IconWeight::Regular size="3vh"/></a> + <a href="https://git.step.polymtl.ca/Lemark/aep-schedule-generator-rusty" class="sources" target="_blank" ><span class="rounded-md font-medium text-gray-700 text-lg font-sans tracking-tight">"Sources "</span><GitlabLogo weight=IconWeight::Regular size="3vh"/></a> </nav> <div class=move || is_active.get() + " hamburger" on:click=move |_| { set_active.update(|text| { @@ -51,7 +56,7 @@ pub fn App() -> impl IntoView { <span class="hamburger-bar"></span> </div> </header> - <main> + <main class="h-full"> <Routes> <Route path="/" view=GeneratorPage/> <Route path="/apropos" view=HomePage/> diff --git a/aep-schedule-website/src/frontend/components/common/autocomplete.rs b/aep-schedule-website/src/frontend/components/common/autocomplete.rs index 329b546b37772a79ebd7405cd135cf4e6fa2690b..c15deeae570f9181a075e66ad7dbaa845b118aae 100644 --- a/aep-schedule-website/src/frontend/components/common/autocomplete.rs +++ b/aep-schedule-website/src/frontend/components/common/autocomplete.rs @@ -16,7 +16,7 @@ impl AutoCompleteOption { fn get_suggestions( sorted_possibilities: Vec<AutoCompleteOption>, - is_ok: RwSignal<String>, + is_hidden: RwSignal<bool>, input_value: String, suggestion_range: WriteSignal<Range<usize>>, ) { @@ -28,11 +28,11 @@ fn get_suggestions( 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_ok.set(String::from("add-button")); + is_hidden.set(false); return; } - is_ok.set(String::from("hidden add-button")); + is_hidden.set(true); suggestion_range.set(bottom..top); } @@ -48,29 +48,37 @@ pub fn AutoComplete<F: FnMut(String) + Copy + Clone + 'static>( let input = create_rw_signal(String::new()); let (suggestion_range, set_suggestion_range) = create_signal(0..0); let suggestions = suggestion_list.clone(); - let is_ok = create_rw_signal(String::from("hidden add-button")); + let is_hidden = create_rw_signal(true); let on_input = move |ev| { let value = event_target_value(&ev); input.set(value.clone()); let suggestion_list = suggestion_list.clone(); - get_suggestions(suggestion_list, is_ok, value, set_suggestion_range); + get_suggestions(suggestion_list, is_hidden, value, set_suggestion_range); + }; + + let button_theme = move || { + let is_hidden = is_hidden.get(); + match is_hidden { + true => "absolute text-black top-0.5 right-0.5 hidden", + false => "absolute text-black top-0.5 right-0.5", + } }; view! { - <div class="search-container ".to_owned() + &class> - <input type="text" class="search-bar" on:input=on_input placeholder=placeholder prop:value=input id=id on:keyup=move |ev| { - if ev.key() == "Enter" && is_ok.get() == "add-button" { + <div class="relative search-container ".to_owned() + &class> + <input type="text" class="py-2 px-3 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none text-black" on:input=on_input placeholder=placeholder prop:value=input id=id on:keyup=move |ev| { + if ev.key() == "Enter" && !is_hidden.get() { let input = input.get().trim().to_uppercase(); submit(input); } } /> - <button class=is_ok on:click=move |_| { + <button class=button_theme on:click=move |_| { let input = input.get().trim().to_uppercase(); submit(input); }> - <PlusCircle size="2.5em"/> + <PlusCircle size="2em"/> </button> <div class="result-box"> {suggestions.into_iter().enumerate().map(|(i, autocomplete)| view!{ diff --git a/aep-schedule-website/src/frontend/components/common/checkbox.rs b/aep-schedule-website/src/frontend/components/common/checkbox.rs deleted file mode 100644 index f8b24c7ea5a7bed02af571886fc2fbf54302e285..0000000000000000000000000000000000000000 --- a/aep-schedule-website/src/frontend/components/common/checkbox.rs +++ /dev/null @@ -1,18 +0,0 @@ -use leptos::*; - -#[component] -pub fn CheckboxChip( - #[prop(optional, into)] value: RwSignal<bool>, - #[prop(optional, into)] class: &'static str, - children: Children, -) -> impl IntoView { - view! { - <div on:click=move |_| {value.update(|b| *b = !*b)} class="chips ".to_owned() + class> - <input - type="checkbox" - prop:checked=value - /> - {children()} - </div> - } -} diff --git a/aep-schedule-website/src/frontend/components/common/mod.rs b/aep-schedule-website/src/frontend/components/common/mod.rs index 6480e0174490976dbf0c5abb5ed088439e09337a..31ba3d955fa8cc7ab7bc736b367e2c3cd83efbc2 100644 --- a/aep-schedule-website/src/frontend/components/common/mod.rs +++ b/aep-schedule-website/src/frontend/components/common/mod.rs @@ -1,5 +1,4 @@ pub mod autocomplete; -pub mod checkbox; pub mod number_input; pub mod schedule; pub mod tab; diff --git a/aep-schedule-website/src/frontend/components/common/number_input.rs b/aep-schedule-website/src/frontend/components/common/number_input.rs index ad2ba1db6615565984461fee5359487906910c5a..5526b856a02650f11521b80db399c104588455e7 100644 --- a/aep-schedule-website/src/frontend/components/common/number_input.rs +++ b/aep-schedule-website/src/frontend/components/common/number_input.rs @@ -1,19 +1,62 @@ use leptos::*; #[component] -pub fn NumberInput(#[prop(optional, into)] value: RwSignal<u8>, max: u8) -> impl IntoView { +pub fn NumberInput<F>( + #[prop(optional, into)] value: RwSignal<u8>, + max: u8, + label: &'static str, + submit: F, +) -> impl IntoView +where + F: Fn() + Copy + 'static, +{ let on_input = move |ev| { value.set(event_target_value(&ev).parse::<u8>().unwrap()); + submit(); + }; + + let minus = move |_| { + let v = value.get(); + if v > 0 { + value.set(v - 1); + submit(); + } + }; + + let plus = move |_| { + let v = value.get(); + if v != u8::MAX { + value.set(v + 1); + submit(); + } + submit(); }; view! { - <input - on:input=on_input - type="number" - min="0" - max=max - prop:value=value - class="number-input" - /> + <div class="flex flex-row gap-8 items-center"> + <p class="text-white font-sans font-medium tracking-tight">{label}</p> + <div class="relative flex items-center max-w-20"> + <button on:click=minus type="button" class="bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded-s-lg p-1 h-7 focus:ring-gray-100 focus:ring-2 focus:outline-none"> + <svg class="w-3 h-3 text-gray-900" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 2"> + <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h16"/> + </svg> + </button> + <input + type="numeric" + class="bg-gray-50 border-x-0 border-gray-300 h-7 text-center text-gray-900 text-sm focus:ring-amber-500 focus:border-amber-500 block w-full py-2.5" + placeholder="0" + on:input=on_input + type="number" + min="0" + max=max + prop:value=value + required /> + <button on:click=plus type="button" class="bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded-e-lg p-1 h-7 focus:ring-gray-100 focus:ring-2 focus:outline-none"> + <svg class="w-3 h-3 text-gray-900" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 18"> + <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 1v16M1 9h16"/> + </svg> + </button> + </div> + </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 58602e7cb7110828303175038edd71a83fa070e7..f89f9567c1c932eb04851d4f7a71cdb3b3ae3e13 100644 --- a/aep-schedule-website/src/frontend/components/options/courses_selector.rs +++ b/aep-schedule-website/src/frontend/components/options/courses_selector.rs @@ -1,7 +1,6 @@ use super::state::OptionState; use super::state::ReactiveCourse; use crate::backend::routes::get_courses; -use crate::frontend::components::common::checkbox::CheckboxChip; use crate::frontend::components::common::tab::Tab; use crate::frontend::components::icons::bell_ringing::BellRinging; use crate::frontend::components::icons::calendar_x::CalendarX; @@ -19,22 +18,32 @@ 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> - <span>{group.number.to_string()}</span> - <div class="col-container group-text-col"> + <div on:click=move |_| { + open.update(|b| *b = !*b); + submit(); + } + class="chips gap-2" + class=("bg-green-500", move || {open.get()}) + class=("bg-red-500", move || {!open.get()}) + > + <span class="text-lg text-bold text-sans">{group.number.to_string()}</span> + <div class="flex flex-col justify-between w-full"> {group.periods.iter().map(|p| { view!{ - <div class="row-container group-text"> + <div class="flex group-text w-full justify-between"> <span>{p.day.to_string()}</span> <span class="period-group">{p.hours.to_string()}</span> </div> @@ -53,23 +62,26 @@ fn GroupsChips( }), true => None, }} - </CheckboxChip> + </div> } } #[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 group course_sigle=course_sigle.clone() group_type submit/> } }).collect_view() } @@ -77,29 +89,32 @@ 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()> <p>{course.name}</p> - <div class="row-container row-evenly"> + <div class="flex justify-around"> { match course.course_type { ReactiveCourseType::TheoOnly { theo_open, theo_groups } => { let groups = theo_groups; view!{ - <div> + <div class="flex gap-2 flex-col pb-2"> <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() }, ReactiveCourseType::LabOnly { lab_open, lab_groups } => { let groups = lab_groups; view!{ - <div> + <div class="flex gap-2 flex-col pb-2"> <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() }, @@ -107,23 +122,23 @@ fn CourseTab(course: ReactiveCourse, active_tab: ReadSignal<String>) -> impl Int let theo_groups = theo_groups; let lab_groups = lab_groups; view!{ - <div> + <div class="flex gap-2 flex-col pb-2"> <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> + <div class="flex gap-2 flex-col pb-2"> <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() }, ReactiveCourseType::Linked { both_open, theo_groups, lab_groups } => { let groups = theo_groups.merge(lab_groups); view!{ - <div> + <div class="flex gap-2 flex-col pb-2"> <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() }, @@ -139,16 +154,18 @@ pub fn CoursesSelector<F>(state: OptionState, submit: F) -> impl IntoView where F: Fn() + Copy + 'static, { - let (selections, set_selections) = state.selections; let (active_tab, set_active_tab) = create_signal("".to_string()); + + let action_courses = state.action_courses; + view! { <Await future=get_courses let:courses > - <SearchCourse courses=courses.clone() set_selections set_active_tab/> + <SearchCourse courses=courses.clone() action_courses set_active_tab/> </Await> - <div class="row-container tab-width"> + <div class="flex tab-width"> <button class="tab-button chips" class=("tab-selected", move || active_tab.get() == "") id="personal" on:click={ move |_| set_active_tab.set("".to_string()) }> @@ -156,7 +173,7 @@ where {"Horaire personnel"} </button> <For - each=selections + each=move || {action_courses.value().get().unwrap_or_default()} key=|c| c.sigle.clone() children=move |course| { let sigle = course.sigle.to_string(); @@ -171,7 +188,12 @@ where <button class="close" on:click={ let sigle = sigle.clone(); move |_| { - set_selections.update(|courses| courses.retain(|c| c.sigle.as_str() != sigle)) + action_courses.value().update(|courses| { + if let Some(courses) = courses { + courses.retain(|c| c.sigle.as_str() != sigle); + }} + ); + submit(); } }><X weight=IconWeight::Regular size="16px"/></button> </button> @@ -183,11 +205,11 @@ where <PersonalTimeSelector week=state.week submit></PersonalTimeSelector> </Tab> <For - each=selections + each=move || {action_courses.value().get().unwrap_or_default()} 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 b333f811ed9f6f92e5388f79039451e55a30a9dc..d7d6efaf4959f7aaaee5618a666a37250bea8c9a 100644 --- a/aep-schedule-website/src/frontend/components/options/form.rs +++ b/aep-schedule-website/src/frontend/components/options/form.rs @@ -1,28 +1,48 @@ -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( +pub fn OptionsForms<F>( action: Action<SchedulesOptions, Vec<Schedule>>, -) -> impl IntoView { - let state = OptionState::default(); + step: ReadSignal<u8>, + validate: F, +) -> impl IntoView +where + F: Fn(OptionState) + Copy + 'static, +{ + let state: OptionState = use_context().unwrap(); + let first_generation_done: FirstGenerationDone = use_context().unwrap(); let submit = move || { - action.dispatch((&state).into()) + validate(state); + if !first_generation_done.0.get() || step.get() != 5 { + return; + } + action.dispatch((&state).into()); }; + create_local_resource(state.action_courses.pending(), move |_| { + submit(); + async move {} + }); + + let submit_mobile = move |_| action.dispatch((&state).into()); + view! { - <h1 class="title">"Options de générations"</h1> <CoursesSelector state=state submit/> - <span class="spacer"></span> - <div class="row-container input-item auto-bottom"><p>"Nombre de conflits maximum"</p><NumberInput value=state.max_nb_conflicts max=127/></div> + <span class="grow"></span> + <NumberInput value=state.max_nb_conflicts max=127 label="Nombre de période de cours en conflits maximum: " submit/> <SelectOptimizations state=state submit/> - <button on:click=move |_| {submit()} class="submit">"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/mod.rs b/aep-schedule-website/src/frontend/components/options/mod.rs index caf828630eb18aa1a71314f9aa0fc4b6a9bf5137..588858cffdef257b5fcd2af607602ecfad5165cb 100644 --- a/aep-schedule-website/src/frontend/components/options/mod.rs +++ b/aep-schedule-website/src/frontend/components/options/mod.rs @@ -4,3 +4,4 @@ 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 52d500f1880889e59e07ef6767301b27946a22b1..ffffefa8fec163acbfe74fda7b1a85750ba91517 100644 --- a/aep-schedule-website/src/frontend/components/options/optimizations.rs +++ b/aep-schedule-website/src/frontend/components/options/optimizations.rs @@ -27,29 +27,29 @@ where view! { <div class="three-col"> - <div class="col-container"> + <div class="flex flex-col items-center"> <House weight=weight_house size="10vh"/> - <p>"Plus de congés"</p> - <input type="range" min="0" max="4" class="opti-slider" prop:value=state.day_off on:input=move |ev| { + <p class="font-sans font-medium tracking-tight">"Plus de congés"</p> + <input type="range" min="0" max="4" class="w-24 accent-amber-500" prop:value=state.day_off on:input=move |ev| { state.day_off.set(event_target_value(&ev).parse::<u8>().unwrap()); submit(); }/> </div> - <div class="col-container"> - <div class="row-container"> + <div class="flex flex-col items-center"> + <div class="flex"> <SunHorizon weight=weight_early size="10vh"/> <Sun weight=weight_morning size="10vh"/> </div> - <p>"Cours plus tôt ou plus tard"</p> - <input type="range" min="-4" max="4" class="opti-slider" prop:value=state.morning on:input=move |ev| { + <p class="font-sans font-medium tracking-tight">"Cours plus tôt ou plus tard"</p> + <input type="range" min="-4" max="4" class="w-48 accent-amber-500" prop:value=state.morning on:input=move |ev| { state.morning.set(event_target_value(&ev).parse::<i8>().unwrap()); submit(); }/> </div> - <div class="col-container"> + <div class="flex flex-col items-center"> <CalendarCheck weight=weight_finish size="10vh"/> - <p>"Finir plus tôt"</p> - <input type="range" min="0" max="4" class="opti-slider" prop:value=state.finish_early on:input=move |ev| { + <p class="font-sans font-medium tracking-tight">"Finir plus tôt"</p> + <input type="range" min="0" max="4" class="w-24 accent-amber-500" prop:value=state.finish_early on:input=move |ev| { state.finish_early.set(event_target_value(&ev).parse::<u8>().unwrap()); submit(); }/> diff --git a/aep-schedule-website/src/frontend/components/options/personal.rs b/aep-schedule-website/src/frontend/components/options/personal.rs index c11fe0083896a4070009c72cf423c37ea7d7cc7c..81cf017db580aa246b8df4198f675e5df0d7f132 100644 --- a/aep-schedule-website/src/frontend/components/options/personal.rs +++ b/aep-schedule-website/src/frontend/components/options/personal.rs @@ -40,9 +40,9 @@ where for day in initial_x..=destination_x { week[day].update(|v| { if is_positive { - *v |= 2u64.pow(destination_y as u32 + 1) - 2u64.pow(initial_y as u32); + *v |= 2u64.pow(destination_y as u32 + 2) - 2u64.pow(initial_y as u32); } else { - *v &= !(2u64.pow(destination_y as u32 + 1) - 2u64.pow(initial_y as u32)); + *v &= !(2u64.pow(destination_y as u32 + 2) - 2u64.pow(initial_y as u32)); } }); } @@ -73,22 +73,22 @@ where let day = week[i].get(); let hour = day & (1 << j); if hour != 0 { - "selected-hour" + "touch-none selected-hour" } else { - "" + "touch-none" } }; view! { - <div class="remove-touch" style=style class=class - on:pointerdown=move |e| { - set_initial.set(Some((i, j))); - set_positive.set((week[i].get() & (1 << j)) == 0); - let _ = e.target().unwrap().dyn_ref::<Element>().unwrap().release_pointer_capture(e.pointer_id()); - } - on:pointerover=move |_| { - set_destination.set((i, j)); - } - ></div> + <div style=style class=class + on:pointerdown=move |e| { + set_initial.set(Some((i, j))); + set_positive.set((week[i].get() & (1 << j)) == 0); + let _ = e.target().unwrap().dyn_ref::<Element>().unwrap().release_pointer_capture(e.pointer_id()); + } + on:pointerover=move |_| { + set_destination.set((i, j)); + }> + </div> } }).collect_view() }).collect_view()} diff --git a/aep-schedule-website/src/frontend/components/options/search.rs b/aep-schedule-website/src/frontend/components/options/search.rs index a69d236b840e5e7dea41528be753f7e70fd062a3..02773d9c2bde40c8482d23a8fceb25b6ba7b47c3 100644 --- a/aep-schedule-website/src/frontend/components/options/search.rs +++ b/aep-schedule-website/src/frontend/components/options/search.rs @@ -1,16 +1,13 @@ use super::state::ReactiveCourse; -use crate::{ - backend::routes::get_course, - frontend::components::common::autocomplete::{AutoComplete, AutoCompleteOption}, -}; +use crate::frontend::components::common::autocomplete::{AutoComplete, AutoCompleteOption}; use aep_schedule_generator::data::course::CourseName; use leptos::*; #[component] pub fn SearchCourse( courses: Result<Vec<CourseName>, ServerFnError>, - set_selections: WriteSignal<Vec<ReactiveCourse>>, set_active_tab: WriteSignal<String>, + action_courses: Action<String, Vec<ReactiveCourse>>, ) -> impl IntoView { let Ok(courses) = courses else { return None; @@ -20,28 +17,12 @@ pub fn SearchCourse( .map(|c| AutoCompleteOption::new(c.sigle.clone(), c.sigle + " - " + &c.name)) .collect(); - let add_course = create_action( - |(sigle, set): &(String, WriteSignal<Vec<ReactiveCourse>>)| { - let sigle = sigle.clone(); - let set = *set; - async move { - if let Ok(c) = get_course(sigle).await { - set.update(|s| { - if !s.iter().any(|react_c| react_c.sigle == c.sigle) { - s.push(c.into()) - } - }); - } - } - }, - ); - let on_submit = move |sigle: String| { set_active_tab(sigle.clone()); - add_course.dispatch((sigle, set_selections)) + action_courses.dispatch(sigle); }; Some(view! { - <AutoComplete suggestion_list=courses placeholder="Cours" class="input-item" submit=on_submit id="course-submitter"/> + <AutoComplete suggestion_list=courses placeholder="Cours" submit=on_submit id="course-submitter"/> }) } diff --git a/aep-schedule-website/src/frontend/components/options/state.rs b/aep-schedule-website/src/frontend/components/options/state.rs index 836bddd2e9b15ed5a6ce36748bac8ae8a5d156b5..8c9989b0eaa6f3224fad71946ffaa6052df54ece 100644 --- a/aep-schedule-website/src/frontend/components/options/state.rs +++ b/aep-schedule-website/src/frontend/components/options/state.rs @@ -5,12 +5,12 @@ use aep_schedule_generator::{ use compact_str::CompactString; use leptos::*; +use crate::backend::routes::get_course; + #[derive(Copy, Clone)] pub struct OptionState { - pub selections: ( - ReadSignal<Vec<ReactiveCourse>>, - WriteSignal<Vec<ReactiveCourse>>, - ), + 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>, @@ -51,12 +51,31 @@ pub struct ReactiveCourse { 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 { - selections: create_signal(vec![]), + 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(0), + morning: create_rw_signal(1), finish_early: create_rw_signal(1), } } @@ -179,9 +198,10 @@ impl From<Course> for ReactiveCourse { impl From<&OptionState> for SchedulesOptions { fn from(state: &OptionState) -> Self { let courses_to_take = state - .selections - .0 + .action_courses + .value() .get() + .unwrap_or_default() .into_iter() .map(|c| c.into()) .collect(); @@ -197,7 +217,7 @@ impl From<&OptionState> for SchedulesOptions { max_nb_conflicts, evaluation, user_conflicts, - max_size: 5, + max_size: 10, } } } diff --git a/aep-schedule-website/src/frontend/components/options/todo.rs b/aep-schedule-website/src/frontend/components/options/todo.rs new file mode 100644 index 0000000000000000000000000000000000000000..6ba9845852a8e2158739b7ec2263e8d6233213f6 --- /dev/null +++ b/aep-schedule-website/src/frontend/components/options/todo.rs @@ -0,0 +1,101 @@ +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, +}; + +#[component] +pub fn Step( + n: u8, + step: ReadSignal<u8>, + title: &'static str, + description: &'static str, + #[prop(optional)] children: Option<Children>, +) -> impl IntoView { + let bg_color = move || { + match n.cmp(&step.get()) { + Ordering::Less => "flex transition-all items-center justify-center w-10 h-10 border rounded-full bg-green-400", + Ordering::Greater => "flex transition-all items-center justify-center w-10 h-10 border rounded-full bg-gray-100", + Ordering::Equal => "flex transition-all items-center justify-center w-10 h-10 border rounded-full bg-amber-400", + } + }; + + view! { + <div class="flex"> + <div class="flex flex-col items-center mr-4"> + <div> + <div class=bg_color> + <svg class="w-4 text-gray-600" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"> + <line fill="none" stroke-miterlimit="10" x1="12" y1="2" x2="12" y2="22"></line> + <polyline fill="none" stroke-miterlimit="10" points="19,15 12,22 5,15"></polyline> + </svg> + </div> + </div> + <div class="w-px h-full bg-gray-300"></div> + </div> + <div class="pt-1 pb-8"> + <p class="mb-2 text-lg font-bold">{n}" - "{title}</p> + <p class="text-gray-700"> + {description} + </p> + {children.map(|c| c())} + </div> + </div> + } +} + +#[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(); + let first_generation_done: FirstGenerationDone = use_context().unwrap(); + + let submit = move |_| { + first_generation_done.0.set(true); + action.dispatch((&state).into()) + }; + + let disab = move || { + let step = step.get(); + match step { + 0..=4 => "disabled", + _ => "", + } + }; + + view! { + <div class="px-4 py-4 mx-auto"> + <div class="grid gap-6 row-gap-10"> + <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> + </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> + </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"> + <div class="flex flex-col items-center mr-4"> + <div> + <div class="flex transition-colors items-center justify-center w-10 h-10 border rounded-full" class=("bg-gray-100", move || step.get() != 5) class=("bg-green-400", move || step.get() == 5)> + <svg class="w-6 text-gray-600" stroke="currentColor" viewBox="0 0 24 24"> + <polyline fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="6,12 10,16 18,8"></polyline> + </svg> + </div> + </div> + </div> + <button on:click=submit class="select-none rounded-lg bg-amber-500 py-1 text-lg font-sans font-semibold px-2 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" prop:disabled=disab>"Générer les horaires"</button> + </div> + </div> + </div> + </div> + } +} diff --git a/aep-schedule-website/src/frontend/components/schedule.rs b/aep-schedule-website/src/frontend/components/schedule.rs index b2885c03ccc5b5f71a054821da44b6b539e29d81..e90f77c989e924426ee65265c6a1a78d751748b6 100644 --- a/aep-schedule-website/src/frontend/components/schedule.rs +++ b/aep-schedule-website/src/frontend/components/schedule.rs @@ -11,12 +11,26 @@ use aep_schedule_generator::{ use leptos::{html::A, *}; #[component] -pub fn Course<'a>(course: &'a TakenCourse) -> impl IntoView { +pub fn Course<'a>(i: usize, course: &'a TakenCourse) -> impl IntoView { let theo_group = course.theo_group().map(|g| format!("T: {}", g.number)); let lab_group = course.lab_group().map(|g| format!("L: {}", g.number)); + let color_box = match i % 8 { + 0 => "w-5 h-5 color1", + 1 => "w-5 h-5 color2", + 2 => "w-5 h-5 color3", + 3 => "w-5 h-5 color4", + 4 => "w-5 h-5 color5", + 5 => "w-5 h-5 color6", + 6 => "w-5 h-5 color7", + _ => "w-5 h-5 color8", + }; + view! { <tr> - <td>{course.sigle.to_string()}</td> + <td class="flex items-center gap-1"> + <div class=color_box></div> + <span>{course.sigle.to_string()}</span> + </td> <td>{course.name.to_string()}</td> <td>{theo_group}</td> <td>{lab_group}</td> @@ -33,11 +47,15 @@ fn PeriodEvent<'a>( ) -> impl IntoView { let location = period.hours.to_string() + " - " + period.room.as_str(); let sigle = course.sigle.to_string() + " - " + period_type; - let mut class = match i % 4 { + let mut class = match i % 8 { 0 => " color1".to_string(), 1 => " color2".to_string(), 2 => " color3".to_string(), - _ => " color4".to_string(), + 3 => " color4".to_string(), + 4 => " color5".to_string(), + 5 => " color6".to_string(), + 6 => " color7".to_string(), + _ => " color8".to_string(), }; match period.week_nb { WeekNumber::B1 => class.push_str(" b1"), @@ -100,21 +118,23 @@ pub fn ScheduleComponent(schedule: Schedule, calendar: Calendar) -> impl IntoVie let link: NodeRef<A> = create_node_ref(); view! { - <a class="hidden" download="cours.ics" href=move || download.get() node_ref=link></a> - <table class="cours"> - {schedule.taken_courses.iter().map(|c| view!{<Course course={c} />}).collect_view()} - </table> - <Schedule last_day=schedule.last_day> - {schedule.taken_courses.iter().enumerate().map(|(i, c)| view!{<CoursePeriods i course=c />}).collect_view()} - </Schedule> - <button class="button-download" on:click=move |_| { - let ics = schedule2.generate_ics(&calendar); - let url = url_escape::encode_fragment(&ics); - set_download("data:text/plain;charset=utf-8,".to_string() + &url); - link().unwrap().click(); - }> - <Download weight=IconWeight::Regular size="3vh"/> - <span>"Télécharger le calendrier de cet horaire"</span> - </button> + <div class="flex flex-col w-full items-center"> + <a class="hidden" download="cours.ics" href=move || download.get() node_ref=link></a> + <table class="cours"> + {schedule.taken_courses.iter().enumerate().map(|(i, c)| view!{<Course i course={c} />}).collect_view()} + </table> + <Schedule last_day=schedule.last_day> + {schedule.taken_courses.iter().enumerate().map(|(i, c)| view!{<CoursePeriods i course=c />}).collect_view()} + </Schedule> + <button class="button-download flex" on:click=move |_| { + let ics = schedule2.generate_ics(&calendar); + let url = url_escape::encode_fragment(&ics); + set_download("data:text/plain;charset=utf-8,".to_string() + &url); + link().unwrap().click(); + }> + <Download weight=IconWeight::Regular size="3vh"/> + <span>"Télécharger le calendrier de cet horaire"</span> + </button> + </div> } } diff --git a/aep-schedule-website/src/frontend/components/schedules.rs b/aep-schedule-website/src/frontend/components/schedules.rs index ee71459f19b0d5fab5927c0dbd39149b6a478c9c..a03e5aa77fe57436d3c836033207903e494abb64 100644 --- a/aep-schedule-website/src/frontend/components/schedules.rs +++ b/aep-schedule-website/src/frontend/components/schedules.rs @@ -1,20 +1,33 @@ +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>>, + section_error: RwSignal<String>, + personal_error: RwSignal<String>, + step: ReadSignal<u8>, +) -> impl IntoView { view! { <Await future=get_calendar children=move |calendar| { - read_signal.get().unwrap_or_default().into_iter().rev().map(|schedule| { - let calendar = calendar.clone().unwrap(); - let schedule = schedule.clone(); - view! { - <ScheduleComponent schedule 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(), + _ => view ! { + <Todo action step section_error personal_error/> } - }).collect_view() + } } /> } diff --git a/aep-schedule-website/src/frontend/pages/apropos.rs b/aep-schedule-website/src/frontend/pages/apropos.rs index cb634f4c23a268c48fd285697f2cd2135d17b0e3..04e5edcaf37867c99cef36a8ded1b71f53a748a1 100644 --- a/aep-schedule-website/src/frontend/pages/apropos.rs +++ b/aep-schedule-website/src/frontend/pages/apropos.rs @@ -3,16 +3,31 @@ use leptos::*; #[component] pub fn HomePage() -> impl IntoView { view! { - <section class="home"> - <h1>"Générateur d'horaire de l'AEP v2"</h1> - <p> - "Vous en avez assez d'avoir des horaires horribles fait pas le registrariat? Vous êtes au bon endroit. Le générateur d'horaire v2 est une réécriture plus performante, intuive et moderne du générateur d'horaire. Il vous aidera à trouver un horaire parfait." - </p> - <h2>"Auteurs"</h2> - <ul> - <li>"Marc-Antoine Manningham - Création du frontend et du backend entièrement en Rust"</li> - <li>"Raphael Salvas, Achille Saint-Hillier, Sunnee Chevalier et Gabriel Billard - Inspiration de leur TP3 de LOG2420 pour le design du générateur"</li> - </ul> + <section class="p-1 w-full h-full"> + <div class="container flex flex-col justify-center p-4 mx-auto md:p-8"> + <p class="p-2 text-sm font-medium tracking-wider text-center uppercase">"Comment ça marche?"</p> + <h2 class="mb-12 text-4xl font-bold leading-none text-center sm:text-5xl">"Foires aux questions"</h2> + <div class="grid gap-10 md:gap-8 sm:p-3 md:grid-cols-2 lg:px-12"> + <div> + <h3 class="font-semibold">"Pourquoi avoir refait l'ancien générateur?"</h3> + <p class="mt-1 dark:text-gray-600"> + "L'ancien générateur d'horaire était en mode maintenance depuis au moins 15 ans et changer l'architecture aurait été très difficile, en effet la vénérable base de code contenait littéralement 5 langages de programmation différents!"<br/> "- Marc-Antoine Manningham" + </p> + </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 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> + <p class="mt-1 dark:text-gray-600">"D'abord, les horaires sont généré directement dans le navigateur de l'utilisateur en WebAssembly plutôt que sur le serveur. Aussi, plutôt que de générer tout les horaires, juste le top des horaires sont générés. Plusieurs techniques pour couper l'espace de combinaisons sont utilisées pour éliminer les branches qui ne respectent pas les contraintes ou qui sont sous-optimales. (branch and bound)"</p> + </div> + <div> + <h3 class="font-semibold">"Si j'ai des suggestions où aller les mettre?"</h3> + <p class="mt-1 dark:text-gray-600">"Le bouton signaler un bug peut aussi être utilisé pour soumettre une fonctionnalité sur le GitLab. Sinon, les contributions sont aussi les bienvenues si vous êtes prêt à faire un peu de Rust!"</p> + </div> + </div> + </div> </section> } } diff --git a/aep-schedule-website/src/frontend/pages/classroom.rs b/aep-schedule-website/src/frontend/pages/classroom.rs index 3485aad63da23067d717c56d2b8eaf5c42cc5d91..4c4adbe6ae35fe863c98a3deb062c6682649113a 100644 --- a/aep-schedule-website/src/frontend/pages/classroom.rs +++ b/aep-schedule-website/src/frontend/pages/classroom.rs @@ -51,7 +51,7 @@ pub fn ClassRoomComponent() -> impl IntoView { let on_submit = move |sigle: String| change_classroom.dispatch((sigle, set_periods)); view! { - <div class="col-container classroom-page"> + <section class="flex flex-col w-full justify-between items-center p-4"> <div class="warning-box"> <WarningCircle size="5em"/> <span> @@ -66,13 +66,13 @@ pub fn ClassRoomComponent() -> impl IntoView { {classrooms.as_ref().map(|classrooms| { let classrooms = classrooms.iter().map(|c| AutoCompleteOption::new(c.to_string(), c.to_string())).collect(); view!{ - <AutoComplete suggestion_list=classrooms placeholder="Local" class="input-item" submit=on_submit id="input-classroom"/> + <AutoComplete suggestion_list=classrooms placeholder="Local" class="w-96" submit=on_submit id="input-classroom"/> } }).ok()} </Await> <Schedule last_day=5 col_height="0.6em"> {move || periods.get().iter().enumerate().map(|(i, p)| view!{<PeriodEvent i period_course=p/>}).collect_view()} </Schedule> - </div> + </section> } } diff --git a/aep-schedule-website/src/frontend/pages/generator.rs b/aep-schedule-website/src/frontend/pages/generator.rs index 9a549e584dcdda99c7f23d83deefadc05f5edcde..cfc00c422508ff7de438a33d3faa50b92732fedf 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,26 +9,77 @@ 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); + + let (step, set_step) = create_signal(1); + // Creates a reactive value to update the button let action = create_action(move |s: &SchedulesOptions| { - let s = s.clone(); + 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=action/> + <OptionsForms action validate step/> </aside> <section class="right-panel"> - <SchedulesComponent read_signal=action.value()/> + <SchedulesComponent section_error personal_error action=action read_signal=action.value() step/> </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/style/common.scss b/aep-schedule-website/style/common.scss index d8bb204c64272d425fe3530eb89288fea3b640d3..5d3bed4de9b3e64377080bd307f8698c775c243e 100644 --- a/aep-schedule-website/style/common.scss +++ b/aep-schedule-website/style/common.scss @@ -7,13 +7,7 @@ display: none; } -.search-bar { - width: 100%; - box-sizing: border-box; - padding: 4pt; -} - -input:focus ~ .result-box { +input:focus~.result-box { display: block; } @@ -35,22 +29,18 @@ input:focus ~ .result-box { z-index: 69; } -.result-box > div { +.result-box>div { z-index: 70; cursor: pointer; background-color: #fff; padding: 4pt; - border-bottom: 1px solid #d4d4d4; + border-bottom: 1px solid #d4d4d4; } -.result-box > div:hover { +.result-box>div:hover { background-color: #d4d4d4; } -.spacer { - flex-grow: 1; -} - .warning-box { padding: 1em; gap: 1em; diff --git a/aep-schedule-website/style/fonts.scss b/aep-schedule-website/style/fonts.scss deleted file mode 100644 index 957532c840975b0c833c2ffce103eafdda11a704..0000000000000000000000000000000000000000 --- a/aep-schedule-website/style/fonts.scss +++ /dev/null @@ -1,4 +0,0 @@ -@font-face { - font-family: lato; - src: url("/fonts/Lato-Regular.ttf") format("truetype"); -} \ No newline at end of file diff --git a/aep-schedule-website/style/main.scss b/aep-schedule-website/style/main.scss index 678018a6e4790326745357326870caad6d9af2d6..8ff3c651c30d2f5598f8e043f24e84753d42c3b3 100644 --- a/aep-schedule-website/style/main.scss +++ b/aep-schedule-website/style/main.scss @@ -2,12 +2,11 @@ @import 'options.scss'; @import 'constant.scss'; @import 'common.scss'; -@import 'fonts.scss'; body { font-family: lato, sans-serif; margin: 0; - min-height: calc(100vh - 3em); + height: auto; $background-color: $light-background; } @@ -17,11 +16,13 @@ nav { top: -100%; transition: 0.3s; } + @media screen and (min-width: 1000px) { flex-direction: row; height: 3em; left: 0; } + width: 100%; display: flex; box-sizing: border-box; @@ -36,47 +37,47 @@ nav { z-index: 42; } -nav > a { +nav>a { display: block; } @keyframes slideInFromLeft { 0% { - transform: translateX(-100%); + transform: translateX(-100%); } + 100% { - transform: translateX(0); + transform: translateX(0); } } @keyframes slideOutToLeft { 0% { - transform: translateX(0); + transform: translateX(0); } + 100% { - transform: translateX(-100%); - display: none; + transform: translateX(-100%); + display: none; } } main { display: flex; justify-content: flex-end; - min-height: calc(100vh - 3em); background-color: $light-background; + @media screen and (max-width: 1000px) { + min-height: calc(100vh); margin-top: 0; } + @media screen and (min-width: 1000px) { + min-height: calc(100vh - 3em); margin-top: 3em; } } -section { - width: 100%; - height: 100%; -} - .hide-left-panel.left-panel { @media screen and (max-width: 1000px) { animation: 0.5s ease-out 0s 1 slideOutToLeft; @@ -90,11 +91,13 @@ section { width: 100%; height: 100%; } + @media screen and (min-width: 1000px) { top: 3em; height: calc(100vh - 3em); width: 42%; } + box-sizing: border-box; z-index: 20; position: fixed; @@ -104,7 +107,7 @@ section { flex-direction: column; justify-items: center; background-color: $background-color; - padding: 0.2em 0.69em; + padding: 1.0rem; gap: 0.2em; animation: 0.5s ease-out 0s 1 slideInFromLeft; } @@ -113,6 +116,7 @@ section { @media screen and (max-width: 1000px) { width: 100%; } + @media screen and (min-width: 1000px) { width: 58%; } @@ -121,20 +125,22 @@ section { display: flex; flex-direction: column; align-items: center; - gap: 10px; - padding: 0.42em; + gap: 3rem; + padding: 1rem; background-color: $light-background; } #go-back { position: fixed; + @media screen and (max-width: 1000px) { top: 0.42em; } - + @media screen and (min-width: 1000px) { display: none; } + left: 0.42em; width: 3em; height: 3em; @@ -149,7 +155,7 @@ section { cursor: pointer; } -a:link { +a:link { text-decoration: none; } @@ -165,11 +171,7 @@ a:hover { text-decoration: underline; } -.home { - padding: 0em 1em; -} - -nav > a { +nav>a { color: black; } @@ -190,7 +192,7 @@ nav > a { @media screen and (min-width: 1000px) { display: none; } - + z-index: 100; position: fixed; top: 0.42em; @@ -226,7 +228,8 @@ nav.active { } } -.classroom-page { - justify-content: space-between; - padding: 2em; -} +[aria-current]:not([aria-current="false"]) { + color: black; + text-decoration: underline rgb(245 158 11); + text-decoration-thickness: 2px; +} \ No newline at end of file diff --git a/aep-schedule-website/style/options.scss b/aep-schedule-website/style/options.scss index 6d81169263dad6eec3b2cccd57c6af32586fa8bd..84bc1950a4bcefd632b737e81551faf95cb58d3f 100644 --- a/aep-schedule-website/style/options.scss +++ b/aep-schedule-website/style/options.scss @@ -1,18 +1,5 @@ @import 'constant.scss'; -.submit { - background-color: $highlight-color; - border: none; - color: $highlight-text; - padding: 0.2em 0.5em; - text-decoration: none; - border-radius: 10px; - cursor: pointer; - font-size: 20pt; - width: 12em; - align-self: center; -} - .relative { position: relative; } @@ -34,31 +21,10 @@ right: 2px; } -.row-container { - display: flex; - flex-direction: row; -} - -.col-container { - display: flex; - flex-direction: column; - width: 100%; - align-items: center; -} - -.row-center { - justify-content: center; - flex-wrap: wrap; -} - -.row-evenly { - justify-content: space-evenly; -} - .three-col { display: grid; grid-template-columns: 1fr 2fr 1fr; - width: 98%; + width: 100%; } .bottom { @@ -74,30 +40,10 @@ padding: 0px; } -.opti-slider { - width: 70%; -} - .auto-bottom { margin-top: auto; } -.group-chip { - background-color: $highlight-color; - color: $background-color; - margin: 4px; - gap: 7px; - font-weight: normal; - overflow: hidden; -} - -.group-text-col { - justify-content: space-between; - max-height: 22px; - gap: 8px; - flex-wrap: nowrap; -} - @keyframes translateOpen { 0% { max-height: 22px; @@ -108,11 +54,6 @@ } } -.group-text-col:hover { - max-height: fit-content; - animation: 0.5s ease-out 1 translateOpen; -} - .group-text { gap: 3px; width: 100%; @@ -132,13 +73,6 @@ background-color: $error-color; } -.input-item { - width: 100%; - display: flex; - align-items: center; - gap: 10px; -} - .vertical-bar { max-height: 100%; width: 2px; @@ -195,7 +129,7 @@ } .selected-hour { - background-color: $background-color; + background-color: rgb(87, 104, 175); } .unselected-hour { @@ -206,18 +140,6 @@ pointer-events: none; } -.add-button { - position: absolute; - right: 0; - height: 3em; - width: 3em; - background: transparent; - display: flex; - justify-content: center; - align-items: center; - border: none !important; -} - .notif-modal { position: absolute; left: 0; diff --git a/aep-schedule-website/style/schedule.scss b/aep-schedule-website/style/schedule.scss index 1aedaf2985d1633a8131681bdbe312e48cca70c1..701433779957dc84b1500c1c9d3ac350d6be7651 100644 --- a/aep-schedule-website/style/schedule.scss +++ b/aep-schedule-website/style/schedule.scss @@ -20,7 +20,6 @@ $days: $time-width 10px repeat(5, 1fr); width: 100%; display: grid; grid-template-rows: $days-height auto; - margin-bottom: 0.4em; } .days { @@ -49,7 +48,7 @@ $days: $time-width 10px repeat(5, 1fr); align-self: end; font-size: 80%; position: relative; - bottom: -1ex; + bottom: -1.4ex; color: black; padding-right: 2px; } @@ -116,10 +115,6 @@ $days: $time-width 10px repeat(5, 1fr); cursor: pointer; } -.remove-touch { - touch-action: none; -} - .color1 { background-color: #41AAE6; } @@ -134,4 +129,20 @@ $days: $time-width 10px repeat(5, 1fr); .color4 { background-color: #B91E32; +} + +.color5 { + background-color: #950de4; +} + +.color6 { + background-color: #007706; +} + +.color7 { + background-color: #ffd20b; +} + +.color8 { + background-color: #ff008c; } \ No newline at end of file diff --git a/aep-schedule-website/style/tailwind.css b/aep-schedule-website/style/tailwind.css new file mode 100644 index 0000000000000000000000000000000000000000..3add53971a1625bba8ee85d5f07fdb7246b63a29 --- /dev/null +++ b/aep-schedule-website/style/tailwind.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + @font-face { + font-family: "lato"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/Lato-Regular.ttf") format("truetype"); + } +} \ No newline at end of file diff --git a/aep-schedule-website/tailwind.config.js b/aep-schedule-website/tailwind.config.js new file mode 100644 index 0000000000000000000000000000000000000000..39efc393f4dc6745a20a08280f5f094bdc4731ef --- /dev/null +++ b/aep-schedule-website/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["*.html", "./src/**/*.rs",], + theme: { + extend: { + fontFamily: { + "lato": ["lato"], + } + }, + }, + plugins: [], +} \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index 738cd4e55901a799b624c466c82e1bf1e8dde800..e799fd37df7990cc5a00d37f529a7c6b753c1bd8 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,7 +1,33 @@ services: - web: - build: . + reverse-proxy: + image: traefik:v3.1 + command: + - "--providers.docker=true" + - "--entryPoints.web.address=:80" + - "--entryPoints.websecure.address=:443" + - "--certificatesresolvers.myresolver.acme.httpchallenge=true" + - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web" + - "--certificatesresolvers.myresolver.acme.email=EMAIL@PROVIDER.COM" # change this to your email + - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" ports: - - "80:6942" + - "80:80" + - "443:443" + volumes: + - ./letsencrypt:/letsencrypt + - /var/run/docker.sock:/var/run/docker.sock + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + horaire: + image: registry.git.step.polymtl.ca/lemark/aep-schedule-generator-rusty:main labels: - - "traefik.http.routers.web.rule=Host('sopti-rusty.step.polymtl.ca')" \ No newline at end of file + - "traefik.http.routers.horaire.rule=Host(`URL.COM`)" # Put the URL here + - "traefik.http.services.horaire.loadbalancer.server.port=6942" + - "traefik.http.routers.horaire.entrypoints=websecure" + - "traefik.http.routers.horaire.tls.certresolver=myresolver" + environment: + SMTP_USERNAME: "" # Put SMTP username here + SMTP_PASSWORD: "" # Put SMTP password here + SMTP_RELAY: "smtp.polymtl.ca" + SMTP_PORT: 587