Skip to content

Commit 676e7fd

Browse files
author
Jhih-Lin Jhou
committed
refine time range filter
1 parent f92b99b commit 676e7fd

2 files changed

Lines changed: 263 additions & 36 deletions

File tree

src/lib/common/shared/TimeRangePicker.svelte

Lines changed: 236 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
<script>
2-
import { createEventDispatcher } from 'svelte';
2+
import { onMount, createEventDispatcher } from 'svelte';
33
import { Button, Input } from '@sveltestrap/sveltestrap';
44
import Flatpickr from 'svelte-flatpickr';
55
import 'flatpickr/dist/flatpickr.css';
66
import { TIME_RANGE_OPTIONS, CUSTOM_DATE_RANGE } from '$lib/helpers/constants';
77
import { clickoutsideDirective } from '$lib/helpers/directives';
88
9+
// Constants
10+
const RECENT_TIME_RANGES_KEY = 'botsharp_recent_time_ranges';
11+
const MAX_RECENT_ITEMS = 10;
12+
const TAB_RELATIVE = 'relative';
13+
const TAB_RECENT = 'recent';
14+
const TAB_CUSTOM = 'custom';
15+
916
/** @type {string} */
1017
export let timeRange = '';
1118
@@ -38,6 +45,9 @@
3845
/** @type {any} */
3946
let flatpickrInstance = null;
4047
48+
/** @type {Array<{ startDate: string, endDate: string, label: string, timeRange?: string }>} */
49+
let recentTimeRanges = [];
50+
4151
// Format date for flatpickr (Date object to YYYY-MM-DD string)
4252
/** @param {Date} date */
4353
function formatDateForFlatpickr(/** @type {Date} */ date) {
@@ -68,6 +78,18 @@
6878
}
6979
};
7080
81+
// Format time range for Recent tab display
82+
const formatRecentTimeRangeLabel = (/** @type {string} */ sDate, /** @type {string} */ eDate) => {
83+
const start = formatDateForDisplay(sDate);
84+
const end = formatDateForDisplay(eDate);
85+
86+
if (start === end) {
87+
return `${start}`;
88+
} else {
89+
return `${start} - ${end}`;
90+
}
91+
};
92+
7193
// Handle manual input changes
7294
function handleStartDateChange() {
7395
if (tempStartDate && flatpickrInstance) {
@@ -113,6 +135,10 @@
113135
value: x.value
114136
}));
115137
138+
onMount(() => {
139+
loadRecentTimeRanges();
140+
});;
141+
116142
// Get today's date in YYYY-MM-DD format
117143
const getTodayStr = () => {
118144
const d = new Date();
@@ -151,24 +177,110 @@
151177
}
152178
}
153179
154-
const dispatch = createEventDispatcher();
180+
const dispatch = createEventDispatcher()
155181
156-
/** @param {string} optionValue */
157-
function handleRelativeOptionClick(/** @type {string} */ optionValue) {
158-
timeRange = optionValue;
182+
function loadRecentTimeRanges() {
183+
try {
184+
const stored = localStorage.getItem(RECENT_TIME_RANGES_KEY);
185+
if (stored) {
186+
recentTimeRanges = JSON.parse(stored);
187+
}
188+
} catch (e) {
189+
recentTimeRanges = [];
190+
}
191+
}
192+
193+
function clearSelection() {
194+
timeRange = '';
159195
startDate = '';
160196
endDate = '';
161197
showDatePicker = false;
162198
dispatch('change', { timeRange, startDate, endDate });
163199
}
164200
201+
/** @param {any} range */
202+
function handleRecentOptionClick(range) {
203+
if (range.timeRange) {
204+
timeRange = range.timeRange;
205+
} else {
206+
timeRange = CUSTOM_DATE_RANGE;
207+
startDate = range.startDate;
208+
endDate = range.endDate;
209+
}
210+
showDatePicker = false;
211+
dispatch('change', { timeRange, startDate, endDate });
212+
}
213+
214+
/**
215+
* @param {{ startDate: string, endDate: string, timeRange?: string, label?: string }} range
216+
*/
217+
function saveRecentTimeRange(range) {
218+
try {
219+
// Use provided label or format from dates
220+
const label = range.label || formatRecentTimeRangeLabel(range.startDate, range.endDate);
221+
const newRange = { ...range, label };
222+
223+
// Remove duplicate if exists (check by timeRange if it's a relative range, otherwise by dates)
224+
if (range.timeRange) {
225+
recentTimeRanges = recentTimeRanges.filter(r => r.timeRange !== range.timeRange);
226+
} else {
227+
recentTimeRanges = recentTimeRanges.filter(r =>
228+
r.startDate !== range.startDate ||
229+
r.endDate !== range.endDate
230+
);
231+
}
232+
233+
// Add to beginning and limit to MAX_RECENT_ITEMS
234+
recentTimeRanges = [newRange, ...recentTimeRanges].slice(0, MAX_RECENT_ITEMS);
235+
236+
localStorage.setItem(RECENT_TIME_RANGES_KEY, JSON.stringify(recentTimeRanges));
237+
} catch (e) {
238+
}
239+
}
240+
241+
/** @param {number} index */
242+
function removeRecentTimeRange(index) {
243+
recentTimeRanges = recentTimeRanges.filter((_, idx) => idx !== index);
244+
try {
245+
localStorage.setItem(RECENT_TIME_RANGES_KEY, JSON.stringify(recentTimeRanges));
246+
} catch (e) {
247+
}
248+
}
249+
250+
/** @param {string} optionValue */
251+
function handleRelativeOptionClick(/** @type {string} */ optionValue) {
252+
// If clicking the same option, unselect it
253+
if (timeRange === optionValue) {
254+
clearSelection();
255+
} else {
256+
timeRange = optionValue;
257+
258+
const option = TIME_RANGE_OPTIONS.find(x => x.value === optionValue);
259+
260+
if (option) {
261+
saveRecentTimeRange({
262+
startDate: '',
263+
endDate: '',
264+
timeRange: optionValue,
265+
label: option.label
266+
});
267+
}
268+
269+
startDate = '';
270+
endDate = '';
271+
showDatePicker = false;
272+
dispatch('change', { timeRange, startDate, endDate });
273+
}
274+
}
275+
165276
function handleApply() {
166277
if (tempStartDate) {
167278
const finalEndDate = tempEndDate || tempStartDate;
168279
// Update props through binding (will trigger reactivity)
169280
startDate = tempStartDate;
170281
endDate = finalEndDate;
171282
timeRange = CUSTOM_DATE_RANGE;
283+
saveRecentTimeRange({ startDate, endDate });
172284
// Dispatch change event with updated values
173285
dispatch('change', {
174286
timeRange: CUSTOM_DATE_RANGE,
@@ -184,16 +296,25 @@
184296
}
185297
</script>
186298

187-
<div class="position-relative">
299+
<div class="multiselect-container"
300+
bind:this={datePickerRef}
301+
use:clickoutsideDirective
302+
on:clickoutside={(/** @type {any} */ e) => {
303+
if (e.detail && e.detail.targetNode && datePickerRef) {
304+
if (!datePickerRef.contains(e.detail.targetNode)) {
305+
showDatePicker = false;
306+
}
307+
}
308+
}}>
188309
<button
189310
type="button"
190311
class="form-control text-start d-flex align-items-center justify-content-between"
191312
on:click={() => {
192313
showDatePicker = !showDatePicker;
193314
if (showDatePicker) {
194315
// If custom date is selected, switch to custom tab; otherwise use relative tab
195-
datePickerTab = timeRange === CUSTOM_DATE_RANGE ? 'custom' : 'relative';
196-
if (datePickerTab === 'custom') {
316+
datePickerTab = timeRange === CUSTOM_DATE_RANGE ? TAB_CUSTOM : TAB_RELATIVE;
317+
if (datePickerTab === TAB_CUSTOM) {
197318
// Delay init to ensure flatpickr is mounted
198319
setTimeout(() => {
199320
initCustomDates();
@@ -207,39 +328,39 @@
207328
<i class="bx bx-chevron-down"></i>
208329
</button>
209330
{#if showDatePicker}
210-
<div
211-
bind:this={datePickerRef}
212-
use:clickoutsideDirective
213-
on:clickoutside={(/** @type {any} */ e) => {
214-
if (e.detail && e.detail.targetNode && datePickerRef) {
215-
if (!datePickerRef.contains(e.detail.targetNode)) {
216-
showDatePicker = false;
217-
}
218-
}
219-
}}
220-
class="position-absolute top-100 start-0 mt-1 bg-white border rounded shadow-lg"
221-
style="z-index: 1050; min-width: 320px; max-width: 350px;"
222-
>
331+
<ul class="position-absolute top-100 start-0 mt-1 bg-white border rounded shadow-lg"
332+
style="z-index: 1050; min-width: 325px; max-width: 350px;">
223333
<ul class="nav nav-tabs border-bottom mb-0 px-2 pt-2" role="tablist">
224-
<li class="nav-item flex-fill" role="presentation">
334+
<li class="nav-item flex-fill d-flex justify-content-center" role="presentation">
225335
<button
226-
class="nav-link fw-semibold {datePickerTab === 'relative' ? 'active text-primary' : 'text-muted'}"
336+
class="nav-link fw-semibold {datePickerTab === TAB_RELATIVE ? 'active text-primary' : 'text-muted'}"
227337
type="button"
228338
role="tab"
229-
style="padding: 0.5rem 0.75rem; {datePickerTab === 'relative' ? 'border-bottom: 2px solid var(--bs-primary) !important; margin-bottom: -1px;' : ''}"
230-
on:click={() => datePickerTab = 'relative'}
339+
style="padding: 0.5rem 0.75rem; {datePickerTab === TAB_RELATIVE ? 'border-bottom: 2px solid var(--bs-primary) !important; margin-bottom: -1px;' : ''}"
340+
on:click={() => datePickerTab = TAB_RELATIVE}
231341
>
232342
Relative
233343
</button>
234344
</li>
235-
<li class="nav-item flex-fill" role="presentation">
345+
<li class="nav-item flex-fill d-flex justify-content-center" role="presentation">
346+
<button
347+
class="nav-link fw-semibold {datePickerTab === TAB_RECENT ? 'active text-primary' : 'text-muted'}"
348+
type="button"
349+
role="tab"
350+
style="padding: 0.5rem 0.75rem; {datePickerTab === TAB_RECENT ? 'border-bottom: 2px solid var(--bs-primary) !important; margin-bottom: -1px;' : ''}"
351+
on:click={() => datePickerTab = TAB_RECENT}
352+
>
353+
Recent
354+
</button>
355+
</li>
356+
<li class="nav-item flex-fill d-flex justify-content-center" role="presentation">
236357
<button
237-
class="nav-link fw-semibold {datePickerTab === 'custom' ? 'active text-primary' : 'text-muted'}"
358+
class="nav-link fw-semibold {datePickerTab === TAB_CUSTOM ? 'active text-primary' : 'text-muted'}"
238359
type="button"
239360
role="tab"
240-
style="padding: 0.5rem 0.75rem; {datePickerTab === 'custom' ? 'border-bottom: 2px solid var(--bs-primary) !important; margin-bottom: -1px;' : ''}"
361+
style="padding: 0.5rem 0.75rem; {datePickerTab === TAB_CUSTOM ? 'border-bottom: 2px solid var(--bs-primary) !important; margin-bottom: -1px;' : ''}"
241362
on:click={() => {
242-
datePickerTab = 'custom';
363+
datePickerTab = TAB_CUSTOM;
243364
// Delay init to ensure flatpickr is mounted
244365
setTimeout(() => {
245366
initCustomDates();
@@ -251,24 +372,72 @@
251372
</li>
252373
</ul>
253374

254-
<div class="p-4">
255-
{#if datePickerTab === 'relative'}
256-
<div class="d-flex flex-column gap-2" style="max-height: 300px; overflow-y: auto;">
375+
<div class="p-2">
376+
{#if datePickerTab === TAB_RELATIVE}
377+
<button
378+
type="button"
379+
class="clear-btn text-center d-flex align-items-center justify-content-center w-100"
380+
on:click|preventDefault|stopPropagation={() => {
381+
clearSelection();
382+
}}
383+
>
384+
<span class="text-secondary">
385+
{`Clear selection`}
386+
</span>
387+
</button>
388+
<div class="d-flex flex-column"
389+
style="max-height: 300px; overflow-y: auto;"
390+
>
257391
{#each presetTimeRangeOptions as option}
258392
<button
259393
type="button"
260-
class="btn btn-sm btn-outline-secondary text-start {timeRange === option.value ? 'active' : ''}"
394+
class="btn relative-option text-start d-flex align-items-center {timeRange === option.value ? 'active' : ''}"
261395
on:click={(e) => {
262396
e.preventDefault();
263397
e.stopPropagation();
264398
handleRelativeOptionClick(option.value);
265399
}}
266400
>
267-
{option.label}
401+
<i class="bx bx-check me-2" style="visibility: {timeRange === option.value ? 'visible' : 'hidden'}; font-size: 1.2rem;"></i>
402+
<span>{option.label}</span>
268403
</button>
269404
{/each}
270405
</div>
271-
{:else if datePickerTab === 'custom'}
406+
{:else if datePickerTab === TAB_RECENT}
407+
{#if recentTimeRanges.length === 0}
408+
<div class="text-muted text-center py-3 w-100">
409+
<i class="bx bx-time-five mb-2" style="font-size: 1.5rem;"></i>
410+
<p class="mb-0 small">{'No recent time ranges'}</p>
411+
</div>
412+
{:else}
413+
{#each recentTimeRanges as range, index}
414+
<!-- svelte-ignore a11y-click-events-have-key-events -->
415+
<!-- svelte-ignore a11y-no-static-element-interactions -->
416+
<div
417+
class="option-item clickable d-flex justify-content-between"
418+
role="button"
419+
tabindex="0"
420+
on:click|preventDefault|stopPropagation={() => {
421+
handleRecentOptionClick(range);
422+
}}
423+
>
424+
<div class="line-align-center">
425+
{range.label}
426+
</div>
427+
<button
428+
type="button"
429+
class="btn btn-sm btn-link text-muted p-0"
430+
title="Remove from recent"
431+
on:click|preventDefault|stopPropagation={() => {
432+
removeRecentTimeRange(index);
433+
}}
434+
>
435+
<i class="bx bx-x"></i>
436+
</button>
437+
</div>
438+
{/each}
439+
{/if}
440+
{:else if datePickerTab === TAB_CUSTOM}
272441
<!-- Calendar Grid -->
273442
<div class="mb-3">
274443
<Flatpickr
@@ -322,7 +491,7 @@
322491
</div>
323492
{/if}
324493
</div>
325-
</div>
494+
</ul>
326495
{/if}
327496
</div>
328497

@@ -331,4 +500,35 @@
331500
:global(.flatpickr-input) {
332501
display: none !important;
333502
}
503+
504+
.clear-btn {
505+
background-color: transparent;
506+
border: none;
507+
padding: 0.5rem 0.75rem;
508+
transition: background-color 0.15s ease-in-out;
509+
}
510+
511+
.clear-btn:hover {
512+
background-color: aliceblue;
513+
}
514+
515+
/* Hover and active styles for relative dropdown items */
516+
.relative-option {
517+
background-color: transparent;
518+
border: none;
519+
transition: background-color 0.15s ease-in-out;
520+
}
521+
522+
.relative-option:hover {
523+
background-color: aliceblue;
524+
}
525+
526+
.relative-option.active {
527+
background-color: transparent;
528+
color: inherit;
529+
}
530+
531+
.relative-option.active:hover {
532+
background-color: aliceblue;
533+
}
334534
</style>

0 commit comments

Comments
 (0)