impulse_thaw/calendar/
mod.rs1use crate::{Button, ButtonGroup};
2use chrono::{Datelike, Days, Local, Month, Months, NaiveDate};
3use leptos::{prelude::*, tachys::view::any_view::AnyView};
4use std::{ops::Deref, sync::Arc};
5use thaw_utils::{class_list, mount_style, OptionModel, OptionModelWithValue};
6
7#[component]
8pub fn Calendar(
9 #[prop(optional, into)] class: MaybeProp<String>,
10 #[prop(optional, into)]
12 value: OptionModel<NaiveDate>,
13 #[prop(optional, into)] children: Option<CalendarChildrenFn>,
14) -> impl IntoView {
15 mount_style("calendar", include_str!("./calendar.css"));
16 let show_date = RwSignal::new(value.get_untracked().unwrap_or(now_date()));
17 Effect::new(move |_| {
18 if let Some(selected_date) = value.get() {
19 let show_date_data = show_date.get_untracked();
20 if selected_date.year() != show_date_data.year()
21 || selected_date.month() != show_date_data.month()
22 {
23 show_date.set(selected_date);
24 }
25 }
26 });
27
28 let dates = Memo::new(move |_| {
29 let show_date = show_date.get();
30 let show_date_month = show_date.month();
31 let mut dates = vec![];
32
33 let mut current_date = show_date;
34 let mut current_weekday_number = None::<u32>;
35 loop {
36 let date = current_date - Days::new(1);
37 if date.month() != show_date_month {
38 if current_weekday_number.is_none() {
39 current_weekday_number = Some(current_date.weekday().num_days_from_sunday());
40 }
41 let weekday_number = current_weekday_number.unwrap();
42 if weekday_number == 0 {
43 break;
44 }
45 current_weekday_number = Some(weekday_number - 1);
46
47 dates.push(CalendarItemDate::Previous(date));
48 } else {
49 dates.push(CalendarItemDate::Current(date));
50 }
51 current_date = date;
52 }
53 dates.reverse();
54 dates.push(CalendarItemDate::Current(show_date));
55 current_date = show_date;
56 current_weekday_number = None;
57 loop {
58 let date = current_date + Days::new(1);
59 if date.month() != show_date_month {
60 if current_weekday_number.is_none() {
61 current_weekday_number = Some(current_date.weekday().num_days_from_sunday());
62 }
63 let weekday_number = current_weekday_number.unwrap();
64 if weekday_number == 6 {
65 break;
66 }
67 current_weekday_number = Some(weekday_number + 1);
68 dates.push(CalendarItemDate::Next(date));
69 } else {
70 dates.push(CalendarItemDate::Current(date));
71 }
72 current_date = date;
73 }
74 dates
75 });
76
77 let previous_month = move |_| {
78 show_date.update(|date| {
79 *date = *date - Months::new(1);
80 });
81 };
82 let today = move |_| {
83 show_date.set(Local::now().date_naive());
84 };
85 let next_month = move |_| {
86 show_date.update(|date| {
87 *date = *date + Months::new(1);
88 });
89 };
90
91 view! {
92 <div class=class_list!["thaw-calendar", class]>
93 <div class="thaw-calendar__header">
94 <span class="thaw-calendar__header-title">
95
96 {move || {
97 show_date
98 .with(|date| {
99 format!(
100 "{} {}",
101 Month::try_from(date.month() as u8).unwrap().name(),
102 date.year(),
103 )
104 })
105 }}
106
107 </span>
108 <ButtonGroup>
109 <Button icon=icondata_ai::AiLeftOutlined on_click=previous_month />
110 <Button on_click=today>"Today"</Button>
111 <Button icon=icondata_ai::AiRightOutlined on_click=next_month />
112 </ButtonGroup>
113 </div>
114 <div class="thaw-calendar__dates">
115
116 {move || {
117 dates
118 .get()
119 .into_iter()
120 .enumerate()
121 .map(|(index, date)| {
122 view! {
123 <CalendarItem
124 value
125 index=index
126 date=date
127 children=children.clone()
128 />
129 }
130 })
131 .collect_view()
132 }}
133
134 </div>
135 </div>
136 }
137}
138
139#[component]
140fn CalendarItem(
141 value: OptionModel<NaiveDate>,
142 index: usize,
143 date: CalendarItemDate,
144 children: Option<CalendarChildrenFn>,
145) -> impl IntoView {
146 let is_selected = Memo::new({
147 let date = date.clone();
148 move |_| {
149 value.with(|value_date| match value_date {
150 OptionModelWithValue::T(v) => v == date.deref(),
151 OptionModelWithValue::Option(v) => v.as_ref() == Some(date.deref()),
152 })
153 }
154 });
155 let weekday_str = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
156 let on_click = {
157 let date = date.clone();
158 move |_| {
159 value.set(Some(*date.deref()));
160 }
161 };
162 view! {
163 <div
164 class="thaw-calendar-item"
165 class=("thaw-calendar-item--other-month", date.is_other_month())
166 class=("thaw-calendar-item--today", date.is_today())
167 class=("thaw-calendar-item--selected", move || is_selected.get())
168 on:click=on_click
169 >
170 <div class="thaw-calendar-item__header">
171 <span class="thaw-calendar-item__header-day">{date.day()}</span>
172
173 {if index < 7 {
174 view! {
175 <span class="thaw-calendar-item__header-title">{weekday_str[index]}</span>
176 }
177 .into()
178 } else {
179 None
180 }}
181
182 </div>
183 {children.map(|c| c(date.deref()))}
184 <div class="thaw-calendar-item__bar"></div>
185 </div>
186 }
187}
188
189#[derive(Clone, PartialEq)]
190pub(crate) enum CalendarItemDate {
191 Previous(NaiveDate),
192 Current(NaiveDate),
193 Next(NaiveDate),
194}
195
196impl CalendarItemDate {
197 pub fn is_other_month(&self) -> bool {
198 match self {
199 CalendarItemDate::Previous(_) | CalendarItemDate::Next(_) => true,
200 CalendarItemDate::Current(_) => false,
201 }
202 }
203
204 pub fn is_today(&self) -> bool {
205 let date = self.deref();
206 let now_date = now_date();
207 &now_date == date
208 }
209}
210
211impl Deref for CalendarItemDate {
212 type Target = NaiveDate;
213
214 fn deref(&self) -> &Self::Target {
215 match self {
216 CalendarItemDate::Previous(date)
217 | CalendarItemDate::Current(date)
218 | CalendarItemDate::Next(date) => date,
219 }
220 }
221}
222
223pub(crate) fn now_date() -> NaiveDate {
224 Local::now().date_naive()
225}
226
227#[derive(Clone)]
228pub struct CalendarChildrenFn(Arc<dyn Fn(&NaiveDate) -> AnyView + Send + Sync>);
229
230impl Deref for CalendarChildrenFn {
231 type Target = Arc<dyn Fn(&NaiveDate) -> AnyView + Send + Sync>;
232
233 fn deref(&self) -> &Self::Target {
234 &self.0
235 }
236}
237
238impl<F, C> From<F> for CalendarChildrenFn
239where
240 F: Fn(&NaiveDate) -> C + Send + Sync + 'static,
241 C: RenderHtml + Send + 'static,
242{
243 fn from(f: F) -> Self {
244 Self(Arc::new(move |date| f(date).into_any()))
245 }
246}