From ae7d88165736ffe246640c12328e605f18ace956 Mon Sep 17 00:00:00 2001 From: luoxiaozero <48741584+luoxiaozero@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:24:20 +0800 Subject: [PATCH] Feat/calendar (#38) * feat: add calendar component * feat: calendar component add style * feat: calendar component add click * fix(calendar): select other months to display * fix(calendar): view date operation --- Cargo.toml | 2 + demo/src/app.rs | 1 + demo/src/pages/calendar/mod.rs | 50 +++++++ demo/src/pages/components.rs | 4 + demo/src/pages/mod.rs | 2 + src/button/button-group.css | 4 + src/calendar/calendar.css | 71 ++++++++++ src/calendar/mod.rs | 252 +++++++++++++++++++++++++++++++++ src/calendar/theme.rs | 26 ++++ src/lib.rs | 2 + src/theme/mod.rs | 9 +- 11 files changed, 420 insertions(+), 3 deletions(-) create mode 100644 demo/src/pages/calendar/mod.rs create mode 100644 src/calendar/calendar.css create mode 100644 src/calendar/mod.rs create mode 100644 src/calendar/theme.rs diff --git a/Cargo.toml b/Cargo.toml index 88bf70f..e7c951a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,10 +33,12 @@ icondata = { version = "0.1.0", features = [ "AiPlusOutlined", "AiPlusOutlined", "AiMinusOutlined", + "AiRightOutlined", ] } icondata_core = "0.0.2" uuid = { version = "1.5.0", features = ["v4"] } cfg-if = "1.0.0" +time = { version = "0.3.30", features = ["wasm-bindgen"] } [features] default = ["csr"] diff --git a/demo/src/app.rs b/demo/src/app.rs index 09d3e96..3f8427f 100644 --- a/demo/src/app.rs +++ b/demo/src/app.rs @@ -76,6 +76,7 @@ fn TheRouter(is_routing: RwSignal) -> impl IntoView { + diff --git a/demo/src/pages/calendar/mod.rs b/demo/src/pages/calendar/mod.rs new file mode 100644 index 0000000..01e2d2a --- /dev/null +++ b/demo/src/pages/calendar/mod.rs @@ -0,0 +1,50 @@ +use crate::components::{Demo, DemoCode}; +use leptos::*; +use prisms::highlight_str; +use thaw::*; + +#[component] +pub fn CalendarPage() -> impl IntoView { + let value = create_rw_signal(OffsetDateTime::now_utc().date()); + view! { +
+

"Calendar"

+ + + + + {highlight_str!( + r#" + let value = create_rw_singal(OffsetDateTime::now_utc().date()); + + view! { + + } + "#, + "rust" + )} + + + +

"Calendar Props"

+ + + + + + + + + + + + + + + + + +
"Name""Type""Default""Description"
"value""RwSignal"
+
+ } +} diff --git a/demo/src/pages/components.rs b/demo/src/pages/components.rs index f369e58..d6d788d 100644 --- a/demo/src/pages/components.rs +++ b/demo/src/pages/components.rs @@ -159,6 +159,10 @@ pub(crate) fn gen_menu_data() -> Vec { MenuGroupOption { label: "Data Display Components".into(), children: vec![ + MenuItemOption { + value: "calendar".into(), + label: "Calendar".into(), + }, MenuItemOption { value: "image".into(), label: "Image".into(), diff --git a/demo/src/pages/mod.rs b/demo/src/pages/mod.rs index 0ff65fc..9d3c425 100644 --- a/demo/src/pages/mod.rs +++ b/demo/src/pages/mod.rs @@ -4,6 +4,7 @@ mod avatar; mod badge; mod breadcrumb; mod button; +mod calendar; mod card; mod checkbox; mod color_picker; @@ -45,6 +46,7 @@ pub use avatar::*; pub use badge::*; pub use breadcrumb::*; pub use button::*; +pub use calendar::*; pub use card::*; pub use checkbox::*; pub use color_picker::*; diff --git a/src/button/button-group.css b/src/button/button-group.css index db78b56..317003e 100644 --- a/src/button/button-group.css +++ b/src/button/button-group.css @@ -1,3 +1,7 @@ +.thaw-button-group { + display: inline-flex; +} + .thaw-button-group--vertical { display: inline-flex; flex-direction: column; diff --git a/src/calendar/calendar.css b/src/calendar/calendar.css new file mode 100644 index 0000000..62519dd --- /dev/null +++ b/src/calendar/calendar.css @@ -0,0 +1,71 @@ +.thaw-calendar { + display: flex; + flex-direction: column; + height: 720px; +} + +.thaw-calendar__header { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: 16px; +} + +.thaw-calendar__header-title { + font-size: 22px; + font-weight: 500; +} + +.thaw-calendar__dates { + flex: 1; + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + grid-auto-rows: 1fr; + border-top: 1px solid; + border-left: 1px solid; + border-color: var(--thaw-border-color); +} + +.thaw-calendar-item { + position: relative; + padding: 8px 12px; + border-right: 1px solid; + border-bottom: 1px solid; + border-color: var(--thaw-border-color); + cursor: pointer; +} + +.thaw-calendar-item:hover { + background-color: var(--thaw-background-color-hover); +} + +.thaw-calendar-item--other-month { + color: var(--thaw-font-color-other-month); +} + +.thaw-calendar-item__header { + display: flex; + justify-content: space-between; +} + +.thaw-calendar-item--today .thaw-calendar-item__header-day { + display: flex; + justify-content: center; + align-items: center; + color: white; + background-color: var(--thaw-background-color-today); + border-radius: 50%; + margin-left: -0.4em; + margin-top: -0.3em; + width: 1.8em; + height: 1.8em; +} + +.thaw-calendar-item--selected .thaw-calendar-item__bar { + position: absolute; + left: 0; + right: 0; + bottom: 0; + background-color: var(--thaw-background-color-today); + height: 3px; +} diff --git a/src/calendar/mod.rs b/src/calendar/mod.rs new file mode 100644 index 0000000..59da414 --- /dev/null +++ b/src/calendar/mod.rs @@ -0,0 +1,252 @@ +mod theme; + +use crate::{use_theme, utils::mount_style, Button, ButtonGroup, ButtonVariant, Theme}; +use icondata::AiIcon; +use leptos::*; +use std::ops::Deref; +pub use theme::CalendarTheme; +pub use time::Date; +use time::Month; +pub use time::OffsetDateTime; + +#[component] +pub fn Calendar(#[prop(into)] value: RwSignal) -> impl IntoView { + mount_style("calendar", include_str!("./calendar.css")); + let theme = use_theme(Theme::light); + let css_vars = create_memo(move |_| { + let mut css_vars = String::new(); + theme.with(|theme| { + css_vars.push_str(&format!( + "--thaw-background-color-today: {};", + theme.common.color_primary + )); + css_vars.push_str(&format!( + "--thaw-font-color-other-month: {};", + theme.calendar.other_month_font_color, + )); + css_vars.push_str(&format!( + "--thaw-border-color: {};", + theme.calendar.border_color + )); + css_vars.push_str(&format!( + "--thaw-background-color-hover: {};", + theme.calendar.background_color_hover + )); + }); + css_vars + }); + let show_date = create_rw_signal(value.get_untracked()); + create_effect(move |_| { + let selected_date = value.get(); + let show_date_data = show_date.get_untracked(); + if selected_date.year() != show_date_data.year() + || selected_date.month() != show_date_data.month() + { + show_date.set(selected_date); + } + }); + + let dates = create_memo(move |_| { + let show_date = show_date.get(); + let show_date_month = show_date.month(); + let mut dates = vec![]; + + let mut current_date = show_date; + let mut current_weekday_number = None::; + loop { + let Some(date) = current_date.previous_day() else { + break; + }; + if date.month() != show_date_month { + if current_weekday_number.is_none() { + current_weekday_number = Some(current_date.weekday().number_days_from_sunday()); + } + let weekday_number = current_weekday_number.unwrap(); + if weekday_number == 0 { + break; + } + current_weekday_number = Some(weekday_number - 1); + + dates.push(CalendarItemDate::Previous(date)); + } else { + dates.push(CalendarItemDate::Current(date)); + } + current_date = date; + } + dates.reverse(); + dates.push(CalendarItemDate::Current(show_date)); + current_date = show_date; + current_weekday_number = None; + loop { + let Some(date) = current_date.next_day() else { + break; + }; + if date.month() != show_date_month { + if current_weekday_number.is_none() { + current_weekday_number = Some(current_date.weekday().number_days_from_sunday()); + } + let weekday_number = current_weekday_number.unwrap(); + if weekday_number == 6 { + break; + } + current_weekday_number = Some(weekday_number + 1); + dates.push(CalendarItemDate::Next(date)); + } else { + dates.push(CalendarItemDate::Current(date)); + } + current_date = date; + } + dates + }); + + let previous_month = move |_| { + show_date.update(|date| { + *date = date.previous_month(); + }); + }; + let today = move |_| { + show_date.set(OffsetDateTime::now_utc().date()); + }; + let next_month = move |_| { + show_date.update(|date| { + *date = date.next_month(); + }); + }; + view! { +
+
+ + + {move || { + show_date.with(|date| { format!("{} {}", date.month(), date.year()) }) + }} + + + + + + + + + +
+
+ + {move || { + dates + .get() + .into_iter() + .enumerate() + .map(|(index, date)| { + view! { } + }) + .collect_view() + }} + +
+
+ } +} + +#[component] +fn CalendarItem(value: RwSignal, index: usize, date: CalendarItemDate) -> impl IntoView { + let is_selected = create_memo({ + let date = date.clone(); + move |_| { + let value_date = value.get(); + value_date.to_calendar_date() == date.to_calendar_date() + } + }); + let weekday_str = vec!["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + let on_click = { + let date = date.clone(); + move |_| { + value.set(date.deref().clone()); + } + }; + view! { +
+
+ {date.day()} + + {if index < 7 { + view! { + {weekday_str[index]} + } + .into() + } else { + None + }} + +
+
+
+ } +} + +#[derive(Clone, PartialEq)] +enum CalendarItemDate { + Previous(Date), + Current(Date), + Next(Date), +} + +impl CalendarItemDate { + fn is_other_month(&self) -> bool { + match self { + CalendarItemDate::Previous(_) | CalendarItemDate::Next(_) => true, + CalendarItemDate::Current(_) => false, + } + } + + fn is_today(&self) -> bool { + let date = self.deref(); + let now_date = OffsetDateTime::now_utc().date(); + now_date.to_calendar_date() == date.to_calendar_date() + } +} + +impl Deref for CalendarItemDate { + type Target = Date; + + fn deref(&self) -> &Self::Target { + match self { + CalendarItemDate::Previous(date) + | CalendarItemDate::Current(date) + | CalendarItemDate::Next(date) => date, + } + } +} + +trait DateMonth { + fn previous_month(&self) -> Self; + fn next_month(&self) -> Self; +} + +impl DateMonth for Date { + fn previous_month(&self) -> Self { + let mut year = self.year(); + if self.month() == Month::January { + year -= 1 + } + Date::from_calendar_date(year, self.month().previous(), 1).unwrap() + } + + fn next_month(&self) -> Self { + let mut year = self.year(); + if self.month() == Month::December { + year += 1 + } + Date::from_calendar_date(year, self.month().next(), 1).unwrap() + } +} diff --git a/src/calendar/theme.rs b/src/calendar/theme.rs new file mode 100644 index 0000000..c5143ae --- /dev/null +++ b/src/calendar/theme.rs @@ -0,0 +1,26 @@ +use crate::theme::ThemeMethod; + +#[derive(Clone)] +pub struct CalendarTheme { + pub border_color: String, + pub other_month_font_color: String, + pub background_color_hover: String, +} + +impl ThemeMethod for CalendarTheme { + fn light() -> Self { + Self { + border_color: "#efeff5".into(), + other_month_font_color: "#c2c2c2".into(), + background_color_hover: "#f3f3f5".into(), + } + } + + fn dark() -> Self { + Self { + border_color: "#2d2d30".into(), + other_month_font_color: "#ffffff61".into(), + background_color_hover: "#2d2d30".into(), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index e74a413..b6b28e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ mod avatar; mod badge; mod breadcrumb; mod button; +mod calendar; mod card; mod checkbox; mod code; @@ -43,6 +44,7 @@ pub use avatar::*; pub use badge::*; pub use breadcrumb::*; pub use button::*; +pub use calendar::*; pub use card::*; pub use checkbox::*; pub use code::*; diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 19dc7ef..8e107d7 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -3,9 +3,9 @@ mod common; use self::common::CommonTheme; use crate::{ mobile::{NavBarTheme, TabbarTheme}, - AlertTheme, AutoCompleteTheme, AvatarTheme, BreadcrumbTheme, ButtonTheme, ColorPickerTheme, - InputTheme, MenuTheme, MessageTheme, ProgressTheme, SelectTheme, SkeletionTheme, SliderTheme, - SwitchTheme, TableTheme, TagTheme, TypographyTheme, UploadTheme, + AlertTheme, AutoCompleteTheme, AvatarTheme, BreadcrumbTheme, ButtonTheme, CalendarTheme, + ColorPickerTheme, InputTheme, MenuTheme, MessageTheme, ProgressTheme, SelectTheme, + SkeletionTheme, SliderTheme, SwitchTheme, TableTheme, TagTheme, TypographyTheme, UploadTheme, }; use leptos::*; @@ -38,6 +38,7 @@ pub struct Theme { pub breadcrumb: BreadcrumbTheme, pub progress: ProgressTheme, pub typograph: TypographyTheme, + pub calendar: CalendarTheme, } impl Theme { @@ -65,6 +66,7 @@ impl Theme { breadcrumb: BreadcrumbTheme::light(), progress: ProgressTheme::light(), typograph: TypographyTheme::light(), + calendar: CalendarTheme::light(), } } pub fn dark() -> Self { @@ -91,6 +93,7 @@ impl Theme { breadcrumb: BreadcrumbTheme::dark(), progress: ProgressTheme::dark(), typograph: TypographyTheme::dark(), + calendar: CalendarTheme::dark(), } } }