leptos_use/
use_calendar.rs

1use crate::core::MaybeRwSignal;
2use chrono::*;
3use default_struct_builder::DefaultBuilder;
4use leptos::prelude::*;
5use std::ops::Deref;
6
7/// Create bare-bone calendar data to use in your component.
8/// See [`UseCalendarOptions`] for options and [`UseCalendarReturn`] for return values.
9///
10/// ## Demo
11///
12/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_calendar)
13///
14/// ## Usage
15///
16/// ```
17/// # use leptos::prelude::*;
18/// # use leptos_use::{use_calendar, UseCalendarReturn};
19/// #
20/// # #[component]
21/// # fn Demo() -> impl IntoView {
22/// let UseCalendarReturn {
23///     dates,
24///     weekdays,
25///     previous_month,
26///     today,
27///     month_by_date,
28///     next_month
29/// } = use_calendar();
30/// #
31/// # view! {
32/// # }
33/// # }
34/// ```
35///
36/// Use [`use_calendar_with_options`] to change the initial date and first day of the week.
37///
38/// ```
39/// # use leptos::prelude::*;
40/// # use chrono::NaiveDate;
41/// # use leptos_use::{use_calendar_with_options, UseCalendarReturn, UseCalendarOptions};
42/// #
43/// # #[component]
44/// # fn Demo() -> impl IntoView {
45/// let initial_date = RwSignal::new(
46///     Some(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap())
47/// );
48///
49/// let options = UseCalendarOptions::default()
50///     .first_day_of_the_week(6)
51///     .initial_date(initial_date);
52///
53/// let UseCalendarReturn {
54///     dates,
55///     weekdays,
56///     previous_month,
57///     today,
58///     month_by_date,
59///     next_month
60/// } = use_calendar_with_options(options);
61/// #
62/// # view! {
63/// # }
64/// # }
65/// ```
66///
67///
68/// ## Server-Side Rendering
69///
70/// Not tested yet.
71// #[doc(cfg(feature = "use_calendar"))]
72pub fn use_calendar() -> UseCalendarReturn<
73    impl Fn() + Clone + Send + Sync,
74    impl Fn() + Clone + Send + Sync,
75    impl Fn(&NaiveDate) + Clone + Send + Sync,
76    impl Fn() + Clone + Send + Sync,
77> {
78    use_calendar_with_options(UseCalendarOptions::default())
79}
80
81/// Version of [`use_calendar`] that takes a [`UseCalendarOptions`]. See [`use_calendar`] for how to use.
82// #[doc(cfg(feature = "use_calendar"))]
83pub fn use_calendar_with_options(
84    options: UseCalendarOptions,
85) -> UseCalendarReturn<
86    impl Fn() + Clone + Send + Sync,
87    impl Fn() + Clone + Send + Sync,
88    impl Fn(&NaiveDate) + Clone + Send + Sync,
89    impl Fn() + Clone + Send + Sync,
90> {
91    let UseCalendarOptions {
92        initial_date: date,
93        first_day_of_the_week,
94    } = options;
95    let (date, _set_date) = date.into_signal();
96
97    let show_date = RwSignal::new(date.get_untracked().unwrap_or(Local::now().date_naive()));
98    Effect::new(move |_| {
99        if let Some(selected_date) = date.get() {
100            let show_date_data = show_date.get_untracked();
101            if selected_date.year() != show_date_data.year()
102                || selected_date.month() != show_date_data.month()
103            {
104                show_date.set(selected_date);
105            }
106        }
107    });
108
109    let dates = Memo::new(move |_| {
110        let show_date = show_date.get();
111        let show_date_month = show_date.month();
112        let mut dates = vec![];
113
114        let mut current_date = show_date;
115        let mut current_weekday_number = None::<u32>;
116        loop {
117            let date = current_date - Days::new(1);
118            if date.month() != show_date_month {
119                if current_weekday_number.is_none() {
120                    current_weekday_number = Some(
121                        current_date.weekday().days_since(
122                            Weekday::try_from(first_day_of_the_week.get() as u8)
123                                .unwrap_or(Weekday::Mon),
124                        ),
125                    );
126                }
127                let weekday_number = current_weekday_number.unwrap();
128                if weekday_number == 0 {
129                    break;
130                }
131                current_weekday_number = Some(weekday_number - 1);
132
133                dates.push(CalendarDate::Previous(date));
134            } else {
135                dates.push(CalendarDate::Current(date));
136            }
137            current_date = date;
138        }
139        dates.reverse();
140        dates.push(CalendarDate::Current(show_date));
141        current_date = show_date;
142        current_weekday_number = None;
143        loop {
144            let date = current_date + Days::new(1);
145            if date.month() != show_date_month {
146                if current_weekday_number.is_none() {
147                    current_weekday_number = Some(
148                        current_date.weekday().days_since(
149                            Weekday::try_from(first_day_of_the_week.get() as u8)
150                                .unwrap_or(Weekday::Mon),
151                        ),
152                    );
153                }
154                let weekday_number = current_weekday_number.unwrap();
155                if weekday_number == 6 {
156                    break;
157                }
158                current_weekday_number = Some(weekday_number + 1);
159                dates.push(CalendarDate::Next(date));
160            } else {
161                dates.push(CalendarDate::Current(date));
162            }
163            current_date = date;
164        }
165        dates
166    });
167
168    let weekdays = Memo::<Vec<usize>>::new(move |_| {
169        if Weekday::try_from(first_day_of_the_week.get() as u8).is_ok() {
170            let first_weekdays = first_day_of_the_week.get()..7;
171            let last_weekdays = 0..first_day_of_the_week.get();
172            first_weekdays.chain(last_weekdays).collect()
173        } else {
174            (0..7).collect()
175        }
176    });
177
178    UseCalendarReturn {
179        previous_month: move || {
180            show_date.update(|date| {
181                *date = *date - Months::new(1);
182            });
183        },
184        today: move || {
185            show_date.set(Local::now().date_naive());
186        },
187        month_by_date: move |new_date: &NaiveDate| {
188            show_date.set(*new_date);
189        },
190
191        next_month: move || {
192            show_date.update(|date| {
193                *date = *date + Months::new(1);
194            });
195        },
196        weekdays: weekdays.into(),
197        dates: dates.into(),
198    }
199}
200
201/// Options for [`use_calendar_with_options`].
202// #[doc(cfg(feature = "use_calendar"))]
203#[derive(DefaultBuilder)]
204pub struct UseCalendarOptions {
205    /// Date being used to initialize the calendar month to be displayed.
206    /// Optional [`chrono::NaiveDate`](https://docs.rs/chrono/latest/chrono/struct.NaiveDate.html). Defaults to [`chrono::Local::now()`](https://docs.rs/chrono/latest/chrono/struct.Local.html#method.now).
207    #[builder(into)]
208    pub initial_date: MaybeRwSignal<Option<NaiveDate>>,
209    /// First day of the week as a number from 0 to 6. Defaults to 0 (Monday).
210    #[builder(into)]
211    pub first_day_of_the_week: Signal<usize>,
212}
213
214impl Default for UseCalendarOptions {
215    fn default() -> Self {
216        Self {
217            initial_date: Some(Local::now().date_naive()).into(),
218            first_day_of_the_week: 0.into(),
219        }
220    }
221}
222
223/// Return type of [`use_calendar`].
224// #[doc(cfg(feature = "use_calendar"))]
225pub struct UseCalendarReturn<PreviousMonthFn, TodayFn, MonthByDateFn, NextMonthFn>
226where
227    PreviousMonthFn: Fn() + Clone + Send + Sync,
228    TodayFn: Fn() + Clone + Send + Sync,
229    MonthByDateFn: Fn(&NaiveDate) + Clone + Send + Sync,
230    NextMonthFn: Fn() + Clone + Send + Sync,
231{
232    /// A function to go to the previous month.
233    pub previous_month: PreviousMonthFn,
234    /// A function to go to the current month.
235    pub today: TodayFn,
236    /// A function to go to the month by date.
237    pub month_by_date: MonthByDateFn,
238    /// A function to go to the next month.
239    pub next_month: NextMonthFn,
240    /// The first day of the week as a number from 0 to 6.
241    pub weekdays: Signal<Vec<usize>>,
242    /// A `Vec` of [`CalendarDate`]s representing the dates in the current month.
243    pub dates: Signal<Vec<CalendarDate>>,
244}
245
246/// Utility enum to represent a calendar date. Implements [`Deref`] to [`chrono::NaiveDate`](https://docs.rs/chrono/latest/chrono/struct.NaiveDate.html).
247#[derive(Clone, Copy, PartialEq)]
248pub enum CalendarDate {
249    Previous(NaiveDate),
250    Current(NaiveDate),
251    Next(NaiveDate),
252}
253
254impl CalendarDate {
255    pub fn is_other_month(&self) -> bool {
256        match self {
257            CalendarDate::Previous(_) | CalendarDate::Next(_) => true,
258            CalendarDate::Current(_) => false,
259        }
260    }
261    pub fn is_today(&self) -> bool {
262        let date = self.deref();
263        let now_date = Local::now().date_naive();
264        &now_date == date
265    }
266
267    pub fn is_selected(&self, selected_date: &NaiveDate) -> bool {
268        self.deref() == selected_date
269    }
270
271    pub fn is_before(&self, date: &NaiveDate) -> bool {
272        self.deref() < date
273    }
274
275    pub fn is_between(&self, start_date: &NaiveDate, end_date: &NaiveDate) -> bool {
276        let date = self.deref();
277        date >= start_date && date <= end_date
278    }
279
280    pub fn is_between_current_month(&self, start_date: &NaiveDate, end_date: &NaiveDate) -> bool {
281        match self {
282            CalendarDate::Current(date) => date >= start_date && date <= end_date,
283            CalendarDate::Next(date) => date > start_date && date <= end_date,
284            CalendarDate::Previous(date) => date >= start_date && date < end_date,
285        }
286    }
287
288    pub fn is_after(&self, date: &NaiveDate) -> bool {
289        self.deref() > date
290    }
291
292    pub fn is_first_day_of_month(&self) -> bool {
293        let date = self.deref();
294        if let Some(prev_date) = date.pred_opt() {
295            date.month() != prev_date.month()
296        } else {
297            true
298        }
299    }
300
301    pub fn is_last_day_of_month(&self) -> bool {
302        let date = self.deref();
303        if let Some(next_date) = date.succ_opt() {
304            date.month() != next_date.month()
305        } else {
306            true
307        }
308    }
309}
310
311impl Deref for CalendarDate {
312    type Target = NaiveDate;
313
314    fn deref(&self) -> &Self::Target {
315        match self {
316            CalendarDate::Previous(date)
317            | CalendarDate::Current(date)
318            | CalendarDate::Next(date) => date,
319        }
320    }
321}