From f5b8aeafa6a33446c85e2a8c2c987cab9e990b52 Mon Sep 17 00:00:00 2001 From: Phosphorus-M Date: Wed, 21 Oct 2026 00:24:07 -0300 Subject: [PATCH] feat: add calendar component --- Cargo.lock | 155 +++++++++++++++++++++++ crates/leptos/Cargo.toml | 1 + crates/leptos/src/calendar.rs | 80 ++++++++++++ crates/leptos/src/calendar/date.rs | 45 +++++++ crates/leptos/src/calendar/day_button.rs | 43 +++++++ crates/leptos/src/calendar/selection.rs | 62 +++++++++ crates/leptos/src/icons.rs | 4 + crates/leptos/src/icons/arrow_left.rs | 9 +- crates/leptos/src/icons/arrow_right.rs | 9 +- crates/leptos/src/icons/calendar.rs | 26 ++++ crates/leptos/src/lib.rs | 2 + 11 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 crates/leptos/src/calendar.rs create mode 100644 crates/leptos/src/calendar/date.rs create mode 100644 crates/leptos/src/calendar/day_button.rs create mode 100644 crates/leptos/src/calendar/selection.rs create mode 100644 crates/leptos/src/icons/calendar.rs diff --git a/Cargo.lock b/Cargo.lock index 415faa8..61bca22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "any_spawner" version = "0.3.0" @@ -86,6 +95,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "base16" version = "0.2.1" @@ -125,12 +140,35 @@ version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +[[package]] +name = "cc" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "codee" version = "0.3.2" @@ -235,6 +273,12 @@ dependencies = [ "convert_case 0.11.0", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -382,6 +426,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + [[package]] name = "fnv" version = "1.0.7" @@ -592,6 +642,30 @@ dependencies = [ "throw_error", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -790,6 +864,7 @@ dependencies = [ name = "leptos-components" version = "0.2.10" dependencies = [ + "chrono", "leptos", "rustlanges-components-core", "tailwind_fuse", @@ -896,6 +971,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + [[package]] name = "manyhow" version = "0.11.4" @@ -947,6 +1028,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.17.0" @@ -1429,6 +1519,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "slab" version = "0.4.10" @@ -1864,6 +1960,65 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/crates/leptos/Cargo.toml b/crates/leptos/Cargo.toml index 073d369..0838906 100644 --- a/crates/leptos/Cargo.toml +++ b/crates/leptos/Cargo.toml @@ -12,3 +12,4 @@ tailwind_fuse = { version = "0.3.2", features = [ "variant", "tailwind_fuse_macro", ] } +chrono = { version = "0.4", default-features = false, features = ["clock", "wasmbind"] } diff --git a/crates/leptos/src/calendar.rs b/crates/leptos/src/calendar.rs new file mode 100644 index 0000000..5342222 --- /dev/null +++ b/crates/leptos/src/calendar.rs @@ -0,0 +1,80 @@ +use crate::{ + button::{Button, Variant as ButtonVariant}, + icons::{ArrowLeft, ArrowRight}, +}; +pub use chrono::NaiveDate; +use chrono::{Local, Months}; +pub use date::{CalendarDay, MONTH_NAMES, calendar_days, month_label}; +use leptos::prelude::*; +pub use selection::{CalendarRange, Selection}; + +mod date; +mod day_button; +mod selection; + +use day_button::CalendarDayButton; + +pub const WEEK_DAYS: [&str; 7] = [ + "Lunes", + "Martes", + "Miercoles", + "Jueves", + "Viernes", + "Sabado", + "Domingo", +]; + +#[component] +pub fn Calendar( + selection: Selection, + #[prop(optional)] default_month: Option, + #[prop(into, optional)] class: String, +) -> impl IntoView { + let month = RwSignal::new(default_month.unwrap_or_else(|| Local::now().date_naive())); + + let class = crate::tw!( + "shadow-rb-black grid gap-4 rounded-[20px] px-3 pt-4 pb-10", + "bg-light border-2 border-black text-black", + "dark:bg-neutral-900 dark:border-neutral-950 dark:text-neutral-50", + class + ); + + view! { +
+
+
+
    + {WEEK_DAYS + .iter() + .map(|day| view! {
  • {&day[..2]}
  • }) + .collect_view()} + + + + } + } + /> +
+
+ } +} diff --git a/crates/leptos/src/calendar/date.rs b/crates/leptos/src/calendar/date.rs new file mode 100644 index 0000000..9d66c1c --- /dev/null +++ b/crates/leptos/src/calendar/date.rs @@ -0,0 +1,45 @@ +use chrono::{Datelike, Days, Months, NaiveDate, Weekday}; + +#[derive(Clone)] +pub struct CalendarDay { + pub date: NaiveDate, + pub current_month: bool, +} + +pub const MONTH_NAMES: [&str; 12] = [ + "Enero", + "Febrero", + "Marzo", + "Abril", + "Mayo", + "Junio", + "Julio", + "Agosto", + "Septiembre", + "Octubre", + "Noviembre", + "Diciembre", +]; + +pub fn month_label(date: NaiveDate) -> String { + let month = MONTH_NAMES[date.month0() as usize]; + let year = date.year(); + format!("{month} {year}") +} + +pub fn calendar_days(date: NaiveDate) -> Vec { + let first = date.with_day(1).expect("every month has a day 1"); + let last = first + Months::new(1) - Days::new(1); + let end = last.week(Weekday::Mon).last_day(); + + first + .week(Weekday::Mon) + .first_day() + .iter_days() + .take_while(|day| *day <= end) + .map(|day| CalendarDay { + date: day, + current_month: day.month() == date.month(), + }) + .collect() +} diff --git a/crates/leptos/src/calendar/day_button.rs b/crates/leptos/src/calendar/day_button.rs new file mode 100644 index 0000000..c30b029 --- /dev/null +++ b/crates/leptos/src/calendar/day_button.rs @@ -0,0 +1,43 @@ +use chrono::Datelike; +use leptos::prelude::*; + +use super::{CalendarDay, Selection}; + +const DAY_SELECTED: &str = "bg-primary-500 text-light dark:text-neutral-950"; +const DAY_DISABLED: &str = "opacity-50 cursor-not-allowed"; + +#[component] +pub fn CalendarDayButton(day: CalendarDay, selection: Selection) -> impl IntoView { + let CalendarDay { + date, + current_month, + } = day; + let disabled = !current_month; + let selected = move || selection.is_selected(date); + + let class = move || { + crate::tw!( + "mb-1 flex w-full min-h-9 min-w-9 cursor-pointer appearance-none items-center justify-center rounded-sm border-2 text-sm font-medium transition", + "border-black", + "dark:border-neutral-50", + disabled.then_some(DAY_DISABLED), + selected().then_some(DAY_SELECTED) + ) + }; + + view! { + + } +} diff --git a/crates/leptos/src/calendar/selection.rs b/crates/leptos/src/calendar/selection.rs new file mode 100644 index 0000000..4f8c955 --- /dev/null +++ b/crates/leptos/src/calendar/selection.rs @@ -0,0 +1,62 @@ +use super::NaiveDate; +use leptos::prelude::*; +use std::collections::BTreeSet; + +#[derive(Clone)] +pub struct CalendarRange { + pub start: Option, + pub end: Option, +} + +impl CalendarRange { + fn select(self, date: NaiveDate) -> Self { + match (self.start, self.end) { + (Some(start), None) => CalendarRange { + start: Some(start.min(date)), + end: Some(start.max(date)), + }, + _ => CalendarRange { + start: Some(date), + end: None, + }, + } + } +} + +// I don't know how to solve this, I just fix this shit adding Copy sorry 🙏😭 god of borrow +#[derive(Clone, Copy)] +pub enum Selection { + Single(RwSignal>), + Multiple(RwSignal>), + Range(RwSignal), +} + +impl Selection { + pub fn is_selected(&self, date: NaiveDate) -> bool { + match self { + Selection::Single(value) => value.get() == Some(date), + Selection::Multiple(value) => value.with(|dates| dates.contains(&date)), + Selection::Range(value) => match value.get() { + CalendarRange { + start: Some(start), + end: Some(end), + } => (start..=end).contains(&date), + CalendarRange { start, .. } => start == Some(date), + }, + } + } + + pub fn select(&self, date: NaiveDate) { + match self { + Selection::Single(value) => value.update(|current| { + *current = (*current != Some(date)).then_some(date); + }), + Selection::Multiple(value) => value.update(|dates| { + if !dates.remove(&date) { + dates.insert(date); + } + }), + Selection::Range(value) => value.update(|range| *range = range.clone().select(date)), + } + } +} diff --git a/crates/leptos/src/icons.rs b/crates/leptos/src/icons.rs index aa8428e..867202f 100644 --- a/crates/leptos/src/icons.rs +++ b/crates/leptos/src/icons.rs @@ -4,6 +4,7 @@ mod arrow_left; mod arrow_right; mod arrow_up; mod book; +mod calendar; mod close; mod discord; mod ferris; @@ -32,6 +33,7 @@ pub use arrow_left::ArrowLeft; pub use arrow_right::ArrowRight; pub use arrow_up::ArrowUp; pub use book::Book; +pub use calendar::Calendar; pub use close::Close; pub use discord::Discord; pub use ferris::Ferris; @@ -63,6 +65,7 @@ pub enum IconVariant { ArrowRight, ArrowUp, Book, + Calendar, Close, Discord, Ferris, @@ -95,6 +98,7 @@ impl IconVariant { Self::ArrowRight => view! {}.into_any(), Self::ArrowUp => view! {}.into_any(), Self::Book => view! {}.into_any(), + Self::Calendar => view! {}.into_any(), Self::Close => view! {}.into_any(), Self::Discord => view! {}.into_any(), Self::Ferris => view! {}.into_any(), diff --git a/crates/leptos/src/icons/arrow_left.rs b/crates/leptos/src/icons/arrow_left.rs index 34933e5..4055b00 100644 --- a/crates/leptos/src/icons/arrow_left.rs +++ b/crates/leptos/src/icons/arrow_left.rs @@ -10,10 +10,17 @@ pub fn ArrowLeft( width=size height=size viewBox="0 0 24 24" + fill="none" class=class xmlns="http://www.w3.org/2000/svg" > - + } } diff --git a/crates/leptos/src/icons/arrow_right.rs b/crates/leptos/src/icons/arrow_right.rs index 31a367f..a262a1d 100644 --- a/crates/leptos/src/icons/arrow_right.rs +++ b/crates/leptos/src/icons/arrow_right.rs @@ -10,10 +10,17 @@ pub fn ArrowRight( width=size height=size viewBox="0 0 24 24" + fill="none" class=class xmlns="http://www.w3.org/2000/svg" > - + } } diff --git a/crates/leptos/src/icons/calendar.rs b/crates/leptos/src/icons/calendar.rs new file mode 100644 index 0000000..e72286b --- /dev/null +++ b/crates/leptos/src/icons/calendar.rs @@ -0,0 +1,26 @@ +use leptos::prelude::*; + +#[component] +pub fn Calendar( + #[prop(into, optional, default = 24)] size: i32, + #[prop(into, default = "".to_string())] class: String, +) -> impl IntoView { + view! { + + + + } +} diff --git a/crates/leptos/src/lib.rs b/crates/leptos/src/lib.rs index a1a7fe6..0753990 100644 --- a/crates/leptos/src/lib.rs +++ b/crates/leptos/src/lib.rs @@ -3,6 +3,7 @@ pub(crate) use tailwind_fuse::tw_merge as tw; pub mod avatar; pub mod badge; pub mod button; +pub mod calendar; pub mod card; pub mod chip; pub mod flap; @@ -22,6 +23,7 @@ pub mod prelude { avatar::Avatar, badge::{Badge, Type as BadgeType, Variant as BadgeVariant}, button::{Button, Variant as ButtonVariant}, + calendar::{Calendar, CalendarRange, NaiveDate, Selection as CalendarSelection}, card::Card, chip::{Chip, Variant as ChipVariant}, flap::Flap,