1use chrono::{Datelike, Duration, Month, Months, NaiveDate, Utc};
2use chrono::{Locale, Weekday};
3use chronoutil::shift_months;
4
5use std::convert::TryFrom;
6use std::mem;
7
8use std::str::FromStr;
9use stylist::ast::Sheet;
10use stylist::Style;
11use yew::MouseEvent;
12use yew::{html, Callback, Component, Context, Html, Properties};
13use yew_template::template_html;
14
15pub struct Datepicker {
16    current_month: NaiveDate,
17    locale: Locale,
18    selected_date: Option<NaiveDate>,
19}
20
21#[derive(Properties, PartialEq)]
22pub struct DatepickerProperties {
23    pub on_select: Callback<NaiveDate>,
24    #[prop_or_default]
25    pub locale: Option<Locale>,
26}
27
28impl Default for DatepickerProperties {
29    fn default() -> Self {
30        DatepickerProperties {
31            on_select: Default::default(),
32            locale: Some(Locale::en_US),
33        }
34    }
35}
36
37pub enum DatepickerMessage {
38    CurrentMonthChange(NaiveDate),
39    Select(NaiveDate),
40}
41
42impl Component for Datepicker {
43    type Message = DatepickerMessage;
44    type Properties = DatepickerProperties;
45
46    fn create(ctx: &Context<Self>) -> Self {
47        let current_date = chrono::offset::Local::now()
48            .date_naive()
49            .with_day0(0)
50            .unwrap();
51        let locale = ctx.props().locale.unwrap_or_else(|| Locale::en_US);
52        Datepicker {
53            current_month: current_date,
54            locale,
55            selected_date: None,
56        }
57    }
58
59    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
60        match msg {
61            DatepickerMessage::CurrentMonthChange(date) => self.current_month = date,
62            DatepickerMessage::Select(selected) => {
63                self.selected_date = Some(selected);
64                let _ = ctx.props().on_select.emit(selected);
65                let in_current_mont_selected = selected.year() == self.current_month.year()
66                    && selected.month() == self.current_month.month();
67                if !in_current_mont_selected {
68                    ctx.link()
69                        .send_message(DatepickerMessage::CurrentMonthChange(
70                            selected.with_day0(0).unwrap(),
71                        ))
72                }
73            }
74        }
75        true
76    }
77
78    fn view(&self, ctx: &Context<Self>) -> Html {
79        const STYLE_FILE: &str = include_str!("styles.css");
80        let sheet = Sheet::from_str(STYLE_FILE).unwrap();
81        let style = Style::new(sheet).unwrap();
82        let days_names = self
83            .current_week()
84            .into_iter()
85            .map(|n: NaiveDate| {
86                html! {
87                   <div class="day">{n.format_localized("%a", self.locale).to_string()}</div>
88                }
89            })
90            .collect::<Html>();
91        let columns = html! {
92            <>
93            <div class="day"></div>
94            {days_names}
95            </>
96        };
97
98        let date = self.current_month.clone();
99        let context = ctx.link().clone();
100        let onclick = Callback::from(move |_| {
101            context.send_message(DatepickerMessage::CurrentMonthChange(shift_months(
102                date, -1,
103            )));
104        });
105        let prev = html! {
106            <button class="btn" {onclick} type="button">{"<"}</button>
107        };
108
109        let context = ctx.link().clone();
110        let onclick_next = Callback::from(move |_| {
111            context.send_message(DatepickerMessage::CurrentMonthChange(shift_months(date, 1)));
112        });
113        let next = html! {
114            <button class="btn" onclick={onclick_next} type="button">{">"}</button>
115        };
116
117        let calendarize = calendarize::calendarize_with_offset(self.current_month, 1);
118        let selected_day = self.selected_date;
119        let current_month = self.current_month;
120
121        let previous_month = self
122            .current_month
123            .checked_sub_months(Months::new(1))
124            .unwrap();
125
126        let prev_month = calendarize::calendarize_with_offset(previous_month, 1);
127        let prev_month_last_week = prev_month.last().unwrap();
128
129        let next_month = self
130            .current_month
131            .checked_add_months(Months::new(1))
132            .unwrap();
133        let next_month_calendarize = calendarize::calendarize_with_offset(next_month, 1);
134        let next_month_first_week = next_month_calendarize.first().unwrap();
135
136        let weeks_number = calendarize.len();
137
138        let rows = calendarize
139            .iter()
140            .cloned()
141            .enumerate()
142            .map(|(week_index, n)| {
143                let week_number = week_index + 1;
144                let is_first_week = week_index == 0;
145                let is_last_week = weeks_number == week_number;
146                let cells = n
147                    .iter()
148                    .cloned()
149                    .enumerate()
150                    .map(|(day_of_month_index, cl)| {
151                        let mut day_class = String::from("day");
152                        let context = ctx.link().clone();
153                        let current_iter_date: Option<NaiveDate>;
154
155                        let number: String;
156                        if cl > 0 {
157                            number = cl.to_string();
158                            current_iter_date = current_month.with_day(cl);
159                        } else {
160                            if is_first_week {
161                                let prev_month_last_week_day =
162                                    prev_month_last_week.get(day_of_month_index).unwrap();
163                                number = prev_month_last_week_day.to_string();
164                                day_class.push_str(" day--nc-month");
165                                current_iter_date =
166                                    previous_month.with_day(*prev_month_last_week_day);
167                            } else {
168                                if is_last_week {
169                                    let next_month_day =
170                                        next_month_first_week.get(day_of_month_index).unwrap();
171                                    number = next_month_day.to_string();
172                                    day_class.push_str(" day--nc-month");
173                                    current_iter_date = next_month.with_day(*next_month_day);
174                                } else {
175                                    number = String::new();
176                                    current_iter_date = None;
177                                }
178                            }
179                        }
180                        match selected_day {
181                            None => {}
182                            Some(s) => match current_iter_date {
183                                None => {}
184                                Some(sl) => {
185                                    if s == sl {
186                                        day_class.push_str(" day--selected");
187                                    }
188                                }
189                            },
190                        }
191                        let onclick: Callback<MouseEvent> =
192                            Callback::from(move |event: MouseEvent| {
193                                event.prevent_default();
194                                match current_iter_date {
195                                    None => {}
196                                    Some(s) => {
197                                        context.send_message(DatepickerMessage::Select(s));
198                                    }
199                                }
200                            });
201                        html! {
202                            <a class={day_class} {onclick} href="#">{number}</a>
203                        }
204                    })
205                    .collect::<Html>();
206                html! {
207                    <>
208                    <div>{week_number}</div>
209                    {cells}
210                    </>
211                }
212            })
213            .collect::<Html>();
214        let class = style.get_class_name().to_string();
215        let header = format!(
216            "{} {}",
217            self.current_month_name(),
218            self.current_month.year()
219        );
220        template_html!(
221            "src/templates/default.html",
222            prev = { prev },
223            next = { next },
224            columns = { columns },
225            rows = { rows },
226            class = { class },
227            header = { header }
228        )
229    }
230}
231
232impl Datepicker {
233    fn current_week(&self) -> Vec<NaiveDate> {
234        let current = Utc::now().date_naive();
235        let week = current.week(Weekday::Mon);
236        let first_day = week.first_day();
237        let last_day = week.last_day();
238        let mut result = Vec::new();
239        for day in NaiveDateRange(first_day, last_day) {
240            result.push(day);
241        }
242        result
243    }
244    fn current_month_name(&self) -> String {
245        match self.locale {
246            Locale::ru_RU => {
247                let month = Month::try_from(self.current_month.month() as u8).unwrap();
248                self.russian_month_name(month).to_string()
249            }
250            _ => self
251                .current_month
252                .format_localized("%B", self.locale)
253                .to_string(),
254        }
255    }
256    fn russian_month_name(&self, month: Month) -> &'static str {
257        match month {
258            Month::January => "Январь",
259            Month::February => "Февраль",
260            Month::March => "Март",
261            Month::April => "Апрель",
262            Month::May => "Май",
263            Month::June => "Июнь",
264            Month::July => "Июль",
265            Month::August => "Август",
266            Month::September => "Сентябрь",
267            Month::October => "Октябрь",
268            Month::November => "Ноябрь",
269            Month::December => "Декабрь",
270        }
271    }
272}
273
274struct NaiveDateRange(NaiveDate, NaiveDate);
275
276impl Iterator for NaiveDateRange {
277    type Item = NaiveDate;
278    fn next(&mut self) -> Option<Self::Item> {
279        if self.0 <= self.1 {
280            let next = self.0 + Duration::days(1);
281            Some(mem::replace(&mut self.0, next))
282        } else {
283            None
284        }
285    }
286}