impulse_thaw/calendar/
mod.rs

1use 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    /// selected date.
11    #[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}