leptos_shadcn_date_picker/
default.rs1use 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 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 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 range_start.set(Some(date.clone()));
126 range_end.set(None);
127 selecting_end.set(true);
128 } else {
129 let start = range_start.get();
131 if let Some(ref start_date) = start {
132 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 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}