your personal website on atproto - mirror
blento.app
1<script lang="ts">
2 // @ts-nocheck
3 import { DatePicker } from 'bits-ui';
4 import { CalendarDate, type DateValue } from '@internationalized/date';
5 import { untrack } from 'svelte';
6
7 let {
8 value = $bindable(''),
9 required = false,
10 minValue = '',
11 locale = 'en',
12 onSelect
13 }: {
14 value: string;
15 required?: boolean;
16 minValue?: string;
17 locale?: string;
18 onSelect?: () => void;
19 } = $props();
20
21 let isOpen = $state(false);
22
23 const currentYear = new Date().getFullYear();
24 const yearRange = Array.from({ length: 7 }, (_, i) => currentYear - 1 + i);
25 const today = new Date();
26 const todayDay = today.getDate();
27 const todayMonth = today.getMonth() + 1;
28 const todayYear = today.getFullYear();
29
30 let internalValue: CalendarDate | undefined = $state(undefined);
31
32 function parseDateStr(str: string): CalendarDate | undefined {
33 if (!str) return undefined;
34 const [yearStr, monthStr, dayStr] = str.split('-');
35 const year = parseInt(yearStr, 10);
36 const month = parseInt(monthStr, 10);
37 const day = parseInt(dayStr, 10);
38 if (isNaN(year) || isNaN(month) || isNaN(day)) return undefined;
39 return new CalendarDate(year, month, day);
40 }
41
42 function formatDateStr(dt: CalendarDate): string {
43 const y = String(dt.year).padStart(4, '0');
44 const m = String(dt.month).padStart(2, '0');
45 const d = String(dt.day).padStart(2, '0');
46 return `${y}-${m}-${d}`;
47 }
48
49 let internalMinValue: CalendarDate | undefined = $derived.by(() => {
50 return parseDateStr(minValue);
51 });
52
53 $effect(() => {
54 const parsed = parseDateStr(value);
55 untrack(() => {
56 if (parsed) {
57 if (
58 !internalValue ||
59 parsed.year !== internalValue.year ||
60 parsed.month !== internalValue.month ||
61 parsed.day !== internalValue.day
62 ) {
63 internalValue = parsed;
64 }
65 } else {
66 internalValue = undefined;
67 }
68 });
69 });
70
71 function handleValueChange(newVal: DateValue | undefined) {
72 if (newVal && newVal instanceof CalendarDate) {
73 internalValue = newVal;
74 value = formatDateStr(newVal);
75 }
76 }
77
78 function handleOpenChange(open: boolean) {
79 isOpen = open;
80 }
81
82 function handleOpenChangeComplete(open: boolean) {
83 if (!open && internalValue) {
84 onSelect?.();
85 }
86 }
87</script>
88
89<DatePicker.Root
90 bind:value={internalValue}
91 onValueChange={handleValueChange}
92 onOpenChange={handleOpenChange}
93 onOpenChangeComplete={handleOpenChangeComplete}
94 minValue={internalMinValue}
95 granularity="day"
96 fixedWeeks={true}
97 weekdayFormat="short"
98 {locale}
99 {required}
100>
101 <div
102 class="border-base-300 bg-base-100 text-base-900 focus-within:border-accent-500 dark:border-base-700 dark:bg-base-800 dark:text-base-100 dark:focus-within:border-accent-400 flex items-center rounded-xl border px-2.5 py-1.5 text-sm transition-colors"
103 >
104 <DatePicker.Input>
105 {#snippet children({ segments })}
106 {#each segments as segment, i (segment.part + i)}
107 {#if segment.part === 'literal'}
108 <span class="text-base-400 dark:text-base-500">{segment.value}</span>
109 {:else}
110 <DatePicker.Segment
111 part={segment.part}
112 class="hover:bg-base-200 focus:bg-base-200 dark:hover:bg-base-700 dark:focus:bg-base-700 rounded px-0.5 focus:outline-none"
113 >
114 {segment.value}
115 </DatePicker.Segment>
116 {/if}
117 {/each}
118 {/snippet}
119 </DatePicker.Input>
120
121 <DatePicker.Trigger
122 class="text-base-400 hover:text-base-600 dark:text-base-500 dark:hover:text-base-300 ml-auto cursor-pointer pl-1.5"
123 >
124 <svg
125 xmlns="http://www.w3.org/2000/svg"
126 fill="none"
127 viewBox="0 0 24 24"
128 stroke-width="1.5"
129 stroke="currentColor"
130 class="size-4"
131 >
132 <path
133 stroke-linecap="round"
134 stroke-linejoin="round"
135 d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5"
136 />
137 </svg>
138 </DatePicker.Trigger>
139 </div>
140
141 <DatePicker.Content
142 class="border-base-200 bg-base-50 dark:border-base-700 dark:bg-base-900 z-50 rounded-2xl border p-4 shadow-lg"
143 >
144 <DatePicker.Calendar>
145 {#snippet children({ months, weekdays })}
146 <DatePicker.Header class="flex items-center justify-between">
147 <DatePicker.PrevButton
148 class="text-base-500 hover:bg-base-200 dark:text-base-400 dark:hover:bg-base-700 inline-flex size-8 items-center justify-center rounded-lg"
149 >
150 <svg
151 xmlns="http://www.w3.org/2000/svg"
152 viewBox="0 0 20 20"
153 fill="currentColor"
154 class="size-5"
155 >
156 <path
157 fill-rule="evenodd"
158 d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
159 clip-rule="evenodd"
160 />
161 </svg>
162 </DatePicker.PrevButton>
163
164 <div class="flex items-center gap-1.5">
165 <DatePicker.MonthSelect
166 monthFormat="long"
167 class="text-base-900 dark:text-base-100 hover:text-accent-500 dark:hover:text-accent-400 cursor-pointer border-0 bg-transparent text-sm font-medium outline-none focus:ring-0 focus:outline-none"
168 />
169 <DatePicker.YearSelect
170 years={yearRange}
171 class="text-base-900 dark:text-base-100 hover:text-accent-500 dark:hover:text-accent-400 cursor-pointer border-0 bg-transparent text-sm font-medium outline-none focus:ring-0 focus:outline-none"
172 />
173 </div>
174
175 <DatePicker.NextButton
176 class="text-base-500 hover:bg-base-200 dark:text-base-400 dark:hover:bg-base-700 inline-flex size-8 items-center justify-center rounded-lg"
177 >
178 <svg
179 xmlns="http://www.w3.org/2000/svg"
180 viewBox="0 0 20 20"
181 fill="currentColor"
182 class="size-5"
183 >
184 <path
185 fill-rule="evenodd"
186 d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 1 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z"
187 clip-rule="evenodd"
188 />
189 </svg>
190 </DatePicker.NextButton>
191 </DatePicker.Header>
192
193 {#each months as month (month.value.month)}
194 <DatePicker.Grid class="mt-3 w-full">
195 <DatePicker.GridHead>
196 <DatePicker.GridRow class="flex w-full">
197 {#each weekdays as weekday, i (i)}
198 <DatePicker.HeadCell
199 class="text-base-400 dark:text-base-500 flex-1 text-center text-xs font-medium"
200 >
201 {weekday}
202 </DatePicker.HeadCell>
203 {/each}
204 </DatePicker.GridRow>
205 </DatePicker.GridHead>
206
207 <DatePicker.GridBody>
208 {#each month.weeks as week, weekIndex (weekIndex)}
209 <DatePicker.GridRow class="flex w-full">
210 {#each week as day (day.toString())}
211 <DatePicker.Cell date={day} month={month.value} class="flex-1 p-0.5">
212 <DatePicker.Day>
213 {#snippet children({ selected, disabled, day: dayText })}
214 <div
215 class="relative flex size-9 items-center justify-center rounded-lg text-sm
216 {selected
217 ? 'bg-accent-500 font-medium text-white'
218 : disabled
219 ? 'text-base-300 dark:text-base-600 pointer-events-none'
220 : day.month !== month.value.month
221 ? 'text-base-300 dark:text-base-600'
222 : 'text-base-700 hover:bg-base-200 dark:text-base-300 dark:hover:bg-base-700'}"
223 >
224 {dayText}
225 {#if day.day === todayDay && day.month === todayMonth && day.year === todayYear}
226 <span
227 class="bg-accent-500 absolute bottom-1 left-1/2 size-1 -translate-x-1/2 rounded-full"
228 class:bg-white={selected}
229 ></span>
230 {/if}
231 </div>
232 {/snippet}
233 </DatePicker.Day>
234 </DatePicker.Cell>
235 {/each}
236 </DatePicker.GridRow>
237 {/each}
238 </DatePicker.GridBody>
239 </DatePicker.Grid>
240 {/each}
241 {/snippet}
242 </DatePicker.Calendar>
243 </DatePicker.Content>
244</DatePicker.Root>