Skip to content

Commit b74ab30

Browse files
authored
Merge pull request #423 from yileicn/main
feat: Create reusable TimeRangePicker component with tabbed interface
2 parents ec4d358 + f150584 commit b74ab30

7 files changed

Lines changed: 276 additions & 81 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>

src/lib/helpers/constants.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export const IMAGE_DATA_PREFIX = 'data:image';
4949
export const INTEGER_REGEX = "[0-9]+";
5050
export const DECIMAL_REGEX = "[0-9.]+";
5151

52+
// Custom date range identifier (not in TimeRange enum)
53+
export const CUSTOM_DATE_RANGE = "Custom date";
54+
5255
export const TIME_RANGE_OPTIONS = [
5356
{ label: TimeRange.Last15Minutes, value: TimeRange.Last15Minutes, qty: 15, unit: 'minutes' },
5457
{ label: TimeRange.Last30Minutes, value: TimeRange.Last30Minutes, qty: 30, unit: 'minutes' },
@@ -57,7 +60,6 @@ export const TIME_RANGE_OPTIONS = [
5760
{ label: TimeRange.Last12Hours, value: TimeRange.Last12Hours, qty: 12, unit: 'hours' },
5861
{ label: TimeRange.Today, value: TimeRange.Today, qty: 1, unit: 'days' },
5962
{ label: TimeRange.Yesterday, value: TimeRange.Yesterday, qty: 1, unit: 'days' },
60-
{ label: TimeRange.SpecificDay, value: TimeRange.SpecificDay, isSpecificDay: true },
6163
{ label: TimeRange.Last3Days, value: TimeRange.Last3Days, qty: 3, unit: 'days' },
6264
{ label: TimeRange.Last7Days, value: TimeRange.Last7Days, qty: 7, unit: 'days' },
6365
{ label: TimeRange.Last30Days, value: TimeRange.Last30Days, qty: 30, unit: 'days' },

src/lib/helpers/enums.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,6 @@ const timeRange = {
257257
Last12Hours: "Last 12 hours",
258258
Today: "Today",
259259
Yesterday: "Yesterday",
260-
SpecificDay: "Specific day",
261260
Last3Days: "Last 3 days",
262261
Last7Days: "Last 7 days",
263262
Last30Days: "Last 30 days",

src/lib/helpers/types/conversationTypes.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,8 @@ IRichContent.prototype.language;
324324
* @property {UserStateDetailModel[]} states
325325
* @property {string[]} tags
326326
* @property {string?} [timeRange]
327-
* @property {string} [specificDate] - When timeRange is SpecificDay, date in YYYY-MM-DD (e.g. 2026-01-25)
327+
* @property {string} [startDate] - When timeRange is "Custom date", start date in YYYY-MM-DD format (e.g. 2026-01-25)
328+
* @property {string} [endDate] - When timeRange is "Custom date", end date in YYYY-MM-DD format (e.g. 2026-01-30). Defaults to startDate if not provided
328329
*/
329330

330331
/**

src/lib/helpers/utils/common.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { goto } from '$app/navigation';
22
import moment from 'moment';
3-
import { TIME_RANGE_OPTIONS } from '../constants';
3+
import { TIME_RANGE_OPTIONS, CUSTOM_DATE_RANGE } from '../constants';
44
import { TimeRange } from '../enums';
55

66
export function range(size = 3, startAt = 0) {
@@ -192,10 +192,11 @@ export function getCleanUrl(url) {
192192

193193
/**
194194
* @param {string} timeRange
195-
* @param {string} [specificDate] - When timeRange is SpecificDay, date in YYYY-MM-DD format (e.g. 2026-01-25)
195+
* @param {string} [startDate] - When timeRange is "Custom date", start date in YYYY-MM-DD format (e.g. 2026-01-25)
196+
* @param {string} [endDate] - When timeRange is "Custom date", end date in YYYY-MM-DD format (e.g. 2026-01-30). If not provided, uses startDate
196197
* @returns {{ startTime: string | null, endTime: string | null }}
197198
*/
198-
export function convertTimeRange(timeRange, specificDate) {
199+
export function convertTimeRange(timeRange, startDate, endDate) {
199200
let ret = { startTime: null, endTime: null };
200201

201202
if (!timeRange) {
@@ -241,14 +242,15 @@ export function convertTimeRange(timeRange, specificDate) {
241242
endTime: moment().subtract(1, 'days').endOf('day').utc().format()
242243
};
243244
break;
244-
case TimeRange.SpecificDay:
245-
if (specificDate && moment(specificDate).isValid()) {
245+
case CUSTOM_DATE_RANGE:
246+
if (startDate && moment(startDate).isValid()) {
247+
const endDateToUse = endDate && moment(endDate).isValid() ? endDate : startDate;
246248
ret = {
247249
...ret,
248250
// @ts-ignore
249-
startTime: moment(specificDate).startOf('day').utc().format(),
251+
startTime: moment(startDate).startOf('day').utc().format(),
250252
// @ts-ignore
251-
endTime: moment(specificDate).endOf('day').utc().format()
253+
endTime: moment(endDateToUse).endOf('day').utc().format()
252254
};
253255
}
254256
break;

0 commit comments

Comments
 (0)