leptos_shadcn_date_picker/
default.rs

1use leptos::prelude::*;
2use tailwind_fuse::tw_merge;
3use leptos_shadcn_calendar::{Calendar as CalendarComponent, CalendarDate};
4use leptos_shadcn_button::{Button, ButtonVariant};
5
6const DATE_PICKER_CLASS: &str = "w-full";
7const DATE_PICKER_TRIGGER_CLASS: &str = "w-full justify-start text-left font-normal";
8const DATE_PICKER_PLACEHOLDER_CLASS: &str = "text-muted-foreground";
9
10#[component]
11pub fn DatePicker(
12    #[prop(optional)] selected: MaybeProp<CalendarDate>,
13    #[prop(optional)] on_select: Option<Callback<CalendarDate>>,
14    #[prop(optional)] disabled: MaybeProp<Vec<CalendarDate>>,
15    #[prop(optional)] placeholder: MaybeProp<String>,
16    #[prop(optional)] class: MaybeProp<String>,
17) -> impl IntoView {
18    let is_open = RwSignal::new(false);
19    let selected_date = RwSignal::new(selected.get());
20    
21    // Update selected date when prop changes
22    Effect::new(move |_| {
23        if let Some(new_selected) = selected.get() {
24            selected_date.set(Some(new_selected));
25        }
26    });
27    
28    let handle_select = move |date: CalendarDate| {
29        selected_date.set(Some(date.clone()));
30        is_open.set(false);
31        if let Some(on_select) = on_select {
32            on_select.run(date);
33        }
34    };
35    
36    let format_date = |date: &CalendarDate| -> String {
37        let months = [
38            "January", "February", "March", "April", "May", "June",
39            "July", "August", "September", "October", "November", "December"
40        ];
41        format!("{} {}, {}", 
42            months[(date.month - 1) as usize], 
43            date.day, 
44            date.year
45        )
46    };
47    
48    let merged_class = tw_merge!(&format!("{} {}", 
49        DATE_PICKER_CLASS,
50        class.get().unwrap_or_default()
51    ));
52    
53    view! {
54        <div class={merged_class}>
55            <Button 
56                variant=ButtonVariant::Outline
57                class={tw_merge!(&DATE_PICKER_TRIGGER_CLASS)}
58                on:click=move |_| is_open.set(!is_open.get())
59            >
60                <svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
61                    <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
62                    <line x1="16" y1="2" x2="16" y2="6"></line>
63                    <line x1="8" y1="2" x2="8" y2="6"></line>
64                    <line x1="3" y1="10" x2="21" y2="10"></line>
65                </svg>
66                {move || {
67                    if let Some(date) = selected_date.get() {
68                        format_date(&date)
69                    } else {
70                        placeholder.get().unwrap_or_else(|| "Pick a date".to_string())
71                    }
72                }}
73            </Button>
74            {move || if is_open.get() {
75                view! {
76                    <div class="mt-2 w-auto p-0 border rounded-md bg-background">
77                        <CalendarComponent
78                            selected=selected_date
79                            on_select=Callback::new(move |date: CalendarDate| {
80                                selected_date.set(Some(date.clone()));
81                                is_open.set(false);
82                                if let Some(cb) = on_select.clone() {
83                                    cb.run(date);
84                                }
85                            })
86                            disabled=disabled.get().unwrap_or_default()
87                        />
88                    </div>
89                }.into_any()
90            } else { view! {}.into_any() }}
91        </div>
92    }
93}
94
95#[component]
96pub fn DatePickerWithRange(
97    #[prop(optional)] from: MaybeProp<CalendarDate>,
98    #[prop(optional)] to: MaybeProp<CalendarDate>,
99    #[prop(optional)] on_select: Option<Callback<(Option<CalendarDate>, Option<CalendarDate>)>>,
100    #[prop(optional)] disabled: MaybeProp<Vec<CalendarDate>>,
101    #[prop(optional)] placeholder: MaybeProp<String>,
102    #[prop(optional)] class: MaybeProp<String>,
103) -> impl IntoView {
104    let is_open = RwSignal::new(false);
105    let range_start = RwSignal::new(from.get());
106    let range_end = RwSignal::new(to.get());
107    let selecting_end = RwSignal::new(false);
108    
109    // Update range when props change
110    Effect::new(move |_| {
111        if let Some(new_from) = from.get() {
112            range_start.set(Some(new_from));
113        }
114    });
115    
116    Effect::new(move |_| {
117        if let Some(new_to) = to.get() {
118            range_end.set(Some(new_to));
119        }
120    });
121    
122    let handle_select = move |date: CalendarDate| {
123        if !selecting_end.get() {
124            // First selection - set start date
125            range_start.set(Some(date.clone()));
126            range_end.set(None);
127            selecting_end.set(true);
128        } else {
129            // Second selection - set end date
130            let start = range_start.get();
131            if let Some(ref start_date) = start {
132                // Ensure end is after start using tuple comparison
133                if (date.year, date.month, date.day) >= (start_date.year, start_date.month, start_date.day)
134                {
135                    range_end.set(Some(date.clone()));
136                } else {
137                    // If selected date is before start, make it the new start
138                    range_start.set(Some(date.clone()));
139                    range_end.set(start.clone());
140                }
141            }
142            selecting_end.set(false);
143            is_open.set(false);
144        }
145        
146        if let Some(on_select) = on_select {
147            on_select.run((range_start.get(), range_end.get()));
148        }
149    };
150    
151    let format_date = |date: &CalendarDate| -> String {
152        let months = [
153            "January", "February", "March", "April", "May", "June",
154            "July", "August", "September", "October", "November", "December"
155        ];
156        format!("{} {}, {}", months[(date.month - 1) as usize], date.day, date.year)
157    };
158
159    let format_date_range = move || -> String {
160        let start = range_start.get();
161        let end = range_end.get();
162        
163        match (start, end) {
164            (Some(start_date), Some(end_date)) => {
165                format!("{} - {}", format_date(&start_date), format_date(&end_date))
166            },
167            (Some(start_date), None) => {
168                format!("{} - ", format_date(&start_date))
169            },
170            _ => placeholder.get().unwrap_or_else(|| "Pick a date range".to_string())
171        }
172    };
173    
174    let merged_class = tw_merge!(&format!("{} {}", 
175        DATE_PICKER_CLASS,
176        class.get().unwrap_or_default()
177    ));
178    
179    view! {
180        <div class={merged_class}>
181            <Button 
182                variant=ButtonVariant::Outline
183                class={tw_merge!(&DATE_PICKER_TRIGGER_CLASS)}
184                on:click=move |_| is_open.set(!is_open.get())
185            >
186                <svg class="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
187                    <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
188                    <line x1="16" y1="2" x2="16" y2="6"></line>
189                    <line x1="8" y1="2" x2="8" y2="6"></line>
190                    <line x1="3" y1="10" x2="21" y2="10"></line>
191                </svg>
192                <span class={
193                    move || if range_start.get().is_none() { 
194                        DATE_PICKER_PLACEHOLDER_CLASS 
195                    } else { 
196                        "" 
197                    }
198                }>
199                    {format_date_range}
200                </span>
201            </Button>
202            {move || if is_open.get() {
203                view! {
204                    <div class="mt-2 w-auto p-0 border rounded-md bg-background">
205                        <CalendarComponent
206                            selected=range_start
207                            on_select=Callback::new(move |date: CalendarDate| {
208                                handle_select(date);
209                            })
210                            disabled=disabled.get().unwrap_or_default()
211                        />
212                    </div>
213                }.into_any()
214            } else { view! {}.into_any() }}
215        </div>
216    }
217}