Skip to content

Commit ff63066

Browse files
committed
refactor: extract time range picker into reusable componen
1 parent 611a6ef commit ff63066

3 files changed

Lines changed: 256 additions & 440 deletions

File tree

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
<script>
2+
import { createEventDispatcher } from 'svelte';
3+
import { Button, Input } from '@sveltestrap/sveltestrap';
4+
import { TIME_RANGE_OPTIONS, CUSTOM_DATE_RANGE } from '$lib/helpers/constants';
5+
import { clickoutsideDirective } from '$lib/helpers/directives';
6+
7+
/** @type {string} */
8+
export let timeRange = '';
9+
10+
/** @type {string} */
11+
export let startDate = '';
12+
13+
/** @type {string} */
14+
export let endDate = '';
15+
16+
/** @type {string} */
17+
let timeRangeDisplayText = '';
18+
19+
/** @type {boolean} */
20+
let showDatePicker = false;
21+
22+
/** @type {HTMLDivElement | null} */
23+
let datePickerRef = null;
24+
25+
/** @type {string} */
26+
let datePickerTab = 'relative';
27+
28+
// Preset time range options
29+
const presetTimeRangeOptions = TIME_RANGE_OPTIONS.map(x => ({
30+
label: x.label,
31+
value: x.value
32+
}));
33+
34+
// Get today's date in YYYY-MM-DD format
35+
const getTodayStr = () => {
36+
const d = new Date();
37+
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
38+
};
39+
40+
// Get yesterday's date in YYYY-MM-DD format
41+
const getYesterdayStr = () => {
42+
const d = new Date();
43+
d.setDate(d.getDate() - 1);
44+
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
45+
};
46+
47+
// Format date for display (YYYY-MM-DD -> MM/DD/YYYY)
48+
const formatDateForDisplay = (/** @type {string} */ dateStr) => {
49+
if (!dateStr) return '';
50+
const [year, month, day] = dateStr.split('-');
51+
return `${month}/${day}/${year}`;
52+
};
53+
54+
// Update time range display text reactively
55+
$: {
56+
if (timeRange === CUSTOM_DATE_RANGE && startDate && endDate) {
57+
const start = formatDateForDisplay(startDate);
58+
const end = formatDateForDisplay(endDate);
59+
if (start === end) {
60+
timeRangeDisplayText = start;
61+
} else {
62+
timeRangeDisplayText = `${start} - ${end}`;
63+
}
64+
} else if (timeRange === CUSTOM_DATE_RANGE) {
65+
timeRangeDisplayText = 'Custom';
66+
} else {
67+
const selected = presetTimeRangeOptions.find(x => x.value === timeRange);
68+
timeRangeDisplayText = selected ? selected.label : '';
69+
}
70+
}
71+
72+
const dispatch = createEventDispatcher();
73+
74+
/** @param {string} optionValue */
75+
function handleRelativeOptionClick(/** @type {string} */ optionValue) {
76+
timeRange = optionValue;
77+
startDate = '';
78+
endDate = '';
79+
showDatePicker = false;
80+
dispatch('change', { timeRange, startDate, endDate });
81+
}
82+
83+
function handleCustomConfirm() {
84+
if (startDate) {
85+
// If endDate is not provided, default to startDate
86+
if (!endDate) {
87+
endDate = startDate;
88+
}
89+
// Force reactivity by reassigning
90+
timeRange = CUSTOM_DATE_RANGE;
91+
}
92+
showDatePicker = false;
93+
dispatch('change', { timeRange, startDate, endDate });
94+
}
95+
96+
function handleCancel() {
97+
showDatePicker = false;
98+
}
99+
</script>
100+
101+
<div class="position-relative">
102+
<button
103+
type="button"
104+
class="form-control text-start d-flex align-items-center justify-content-between"
105+
on:click={() => {
106+
showDatePicker = !showDatePicker;
107+
if (showDatePicker) {
108+
// If custom date is selected, switch to custom tab; otherwise use relative tab
109+
datePickerTab = timeRange === CUSTOM_DATE_RANGE ? 'custom' : 'relative';
110+
}
111+
}}
112+
style="cursor: pointer;"
113+
>
114+
<span>{timeRangeDisplayText || 'Select time range'}</span>
115+
<i class="bx bx-chevron-down"></i>
116+
</button>
117+
{#if showDatePicker}
118+
<div
119+
bind:this={datePickerRef}
120+
use:clickoutsideDirective
121+
on:clickoutside={(/** @type {any} */ e) => {
122+
if (e.detail && e.detail.targetNode && datePickerRef) {
123+
if (!datePickerRef.contains(e.detail.targetNode)) {
124+
showDatePicker = false;
125+
}
126+
}
127+
}}
128+
class="position-absolute top-100 start-0 mt-1 bg-white border rounded shadow-lg"
129+
style="z-index: 1050; min-width: 320px; max-width: 350px;"
130+
>
131+
<ul class="nav nav-tabs border-bottom mb-0 px-2 pt-2" role="tablist">
132+
<li class="nav-item flex-fill" role="presentation">
133+
<button
134+
class="nav-link fw-semibold {datePickerTab === 'relative' ? 'active text-primary' : 'text-muted'}"
135+
type="button"
136+
role="tab"
137+
style="padding: 0.5rem 0.75rem; {datePickerTab === 'relative' ? 'border-bottom: 2px solid var(--bs-primary) !important; margin-bottom: -1px;' : ''}"
138+
on:click={() => datePickerTab = 'relative'}
139+
>
140+
Relative
141+
</button>
142+
</li>
143+
<li class="nav-item flex-fill" role="presentation">
144+
<button
145+
class="nav-link fw-semibold {datePickerTab === 'custom' ? 'active text-primary' : 'text-muted'}"
146+
type="button"
147+
role="tab"
148+
style="padding: 0.5rem 0.75rem; {datePickerTab === 'custom' ? 'border-bottom: 2px solid var(--bs-primary) !important; margin-bottom: -1px;' : ''}"
149+
on:click={() => {
150+
datePickerTab = 'custom';
151+
// Set default dates to yesterday and today if not already set
152+
if (!startDate && !endDate) {
153+
startDate = getYesterdayStr();
154+
endDate = getTodayStr();
155+
}
156+
}}
157+
>
158+
Custom
159+
</button>
160+
</li>
161+
</ul>
162+
163+
<div class="p-4">
164+
{#if datePickerTab === 'relative'}
165+
<div class="d-flex flex-column gap-2" style="max-height: 300px; overflow-y: auto;">
166+
{#each presetTimeRangeOptions as option}
167+
<button
168+
type="button"
169+
class="btn btn-sm btn-outline-secondary text-start {timeRange === option.value ? 'active' : ''}"
170+
on:click={(e) => {
171+
e.preventDefault();
172+
e.stopPropagation();
173+
handleRelativeOptionClick(option.value);
174+
}}
175+
>
176+
{option.label}
177+
</button>
178+
{/each}
179+
</div>
180+
{:else if datePickerTab === 'custom'}
181+
<div class="mb-3">
182+
<label for="start-date-picker" class="form-label small mb-2">From:</label>
183+
<Input
184+
id="start-date-picker"
185+
type="date"
186+
bind:value={startDate}
187+
class="form-control form-control-sm"
188+
/>
189+
</div>
190+
<div class="mb-4">
191+
<label for="end-date-picker" class="form-label small mb-2">To:</label>
192+
<Input
193+
id="end-date-picker"
194+
type="date"
195+
bind:value={endDate}
196+
class="form-control form-control-sm"
197+
/>
198+
</div>
199+
<div class="d-flex justify-content-end gap-2 mt-3">
200+
<Button
201+
color="secondary"
202+
size="sm"
203+
type="button"
204+
on:click={(e) => {
205+
e.preventDefault();
206+
e.stopPropagation();
207+
handleCancel();
208+
}}
209+
>
210+
Cancel
211+
</Button>
212+
<Button
213+
color="primary"
214+
size="sm"
215+
type="button"
216+
on:click={(e) => {
217+
e.preventDefault();
218+
e.stopPropagation();
219+
handleCustomConfirm();
220+
}}
221+
>
222+
Confirm
223+
</Button>
224+
</div>
225+
{/if}
226+
</div>
227+
</div>
228+
{/if}
229+
</div>

0 commit comments

Comments
 (0)