Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: Custom date picker * chore: Calender footer * chore: Minor fix * chore: Reset date picker * chore: Minor fix * feat: Toggle button * chore: Clean up * chore: Use font inter * chore: Cleanup and fix bugs * fix: custom date range reset the calendar * chore: fix logic bug * feat: Add manual date range * fix: styles in rtl * chore: Helper specs * chore: Clean up * chore: Review fixes * chore: remove magic strings * chore: Add comments * chore: Review fixes * chore: Clean up * chore: remove magic strings * fix: Use outline instead of border * chore: Minor style fix * chore: disable pointer events for the disabled dates * chore: Fix code climate --------- Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
- Loading branch information
Showing
17 changed files
with
1,522 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
302 changes: 302 additions & 0 deletions
302
app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,302 @@ | ||
<script setup> | ||
import { ref, watch } from 'vue'; | ||
import { | ||
getActiveDateRange, | ||
moveCalendarDate, | ||
DATE_RANGE_TYPES, | ||
CALENDAR_TYPES, | ||
CALENDAR_PERIODS, | ||
} from './helpers/DatePickerHelper'; | ||
import { | ||
isValid, | ||
startOfMonth, | ||
subDays, | ||
startOfDay, | ||
endOfDay, | ||
isBefore, | ||
subMonths, | ||
addMonths, | ||
isSameMonth, | ||
differenceInCalendarMonths, | ||
setMonth, | ||
setYear, | ||
isAfter, | ||
} from 'date-fns'; | ||
import DatePickerButton from './components/DatePickerButton.vue'; | ||
import CalendarDateInput from './components/CalendarDateInput.vue'; | ||
import CalendarDateRange from './components/CalendarDateRange.vue'; | ||
import CalendarYear from './components/CalendarYear.vue'; | ||
import CalendarMonth from './components/CalendarMonth.vue'; | ||
import CalendarWeek from './components/CalendarWeek.vue'; | ||
import CalendarFooter from './components/CalendarFooter.vue'; | ||
const { LAST_7_DAYS, CUSTOM_RANGE } = DATE_RANGE_TYPES; | ||
const { START_CALENDAR, END_CALENDAR } = CALENDAR_TYPES; | ||
const { WEEK, MONTH, YEAR } = CALENDAR_PERIODS; | ||
const showDatePicker = ref(false); | ||
const calendarViews = ref({ start: WEEK, end: WEEK }); | ||
const currentDate = ref(new Date()); | ||
const selectedStartDate = ref(startOfDay(subDays(currentDate.value, 6))); // LAST_7_DAYS | ||
const selectedEndDate = ref(endOfDay(currentDate.value)); | ||
// Setting the start and end calendar | ||
const startCurrentDate = ref(startOfDay(selectedStartDate.value)); | ||
const endCurrentDate = ref( | ||
isSameMonth(selectedStartDate.value, selectedEndDate.value) | ||
? startOfMonth(addMonths(selectedEndDate.value, 1)) // Moves to the start of the next month if dates are in the same month (Mounted case LAST_7_DAYS) | ||
: startOfMonth(selectedEndDate.value) // Always shows the month of the end date starting from the first (Mounted case LAST_7_DAYS) | ||
); | ||
const selectingEndDate = ref(false); | ||
const selectedRange = ref(LAST_7_DAYS); | ||
const hoveredEndDate = ref(null); | ||
const manualStartDate = ref(selectedStartDate.value); | ||
const manualEndDate = ref(selectedEndDate.value); | ||
const emit = defineEmits(['change']); | ||
// Watcher will set the start and end dates based on the selected range | ||
watch(selectedRange, newRange => { | ||
if (newRange !== CUSTOM_RANGE) { | ||
// If selecting a range other than last 7 days, set the start and end dates to the selected start and end dates | ||
// If selecting last 7 days, set the start date to the selected start date | ||
// and the end date to one month ahead of the start date if the start date and end date are in the same month | ||
// Otherwise set the end date to the selected end date | ||
const isLast7days = newRange === LAST_7_DAYS; | ||
startCurrentDate.value = selectedStartDate.value; | ||
endCurrentDate.value = | ||
isLast7days && isSameMonth(selectedStartDate.value, selectedEndDate.value) | ||
? startOfMonth(addMonths(selectedStartDate.value, 1)) | ||
: selectedEndDate.value; | ||
selectingEndDate.value = false; | ||
} else if (!selectingEndDate.value) { | ||
// If selecting a custom range and not selecting an end date, set the start date to the selected start date | ||
startCurrentDate.value = startOfDay(currentDate.value); | ||
} | ||
}); | ||
// Watcher will set the input values based on the selected start and end dates | ||
watch( | ||
[selectedStartDate, selectedEndDate], | ||
([newStart, newEnd]) => { | ||
if (isValid(newStart)) { | ||
manualStartDate.value = newStart; | ||
} else { | ||
manualStartDate.value = selectedStartDate.value; | ||
} | ||
if (isValid(newEnd)) { | ||
manualEndDate.value = newEnd; | ||
} else { | ||
manualEndDate.value = selectedEndDate.value; | ||
} | ||
}, | ||
{ immediate: true } | ||
); | ||
// Watcher to ensure dates are always in logical order | ||
// This watch is will ensure that the start date is always before the end date | ||
watch( | ||
[startCurrentDate, endCurrentDate], | ||
([newStart, newEnd], [oldStart, oldEnd]) => { | ||
const monthDifference = differenceInCalendarMonths(newEnd, newStart); | ||
if (newStart !== oldStart) { | ||
if (isAfter(newStart, newEnd) || monthDifference === 0) { | ||
// Adjust the end date forward if the start date is adjusted and is after the end date or in the same month | ||
endCurrentDate.value = addMonths(newStart, 1); | ||
} | ||
} | ||
if (newEnd !== oldEnd) { | ||
if (isBefore(newEnd, newStart) || monthDifference === 0) { | ||
// Adjust the start date backward if the end date is adjusted and is before the start date or in the same month | ||
startCurrentDate.value = subMonths(newEnd, 1); | ||
} | ||
} | ||
}, | ||
{ immediate: true, deep: true } | ||
); | ||
const setDateRange = range => { | ||
selectedRange.value = range.value; | ||
const { start, end } = getActiveDateRange(range.value, currentDate.value); | ||
selectedStartDate.value = start; | ||
selectedEndDate.value = end; | ||
}; | ||
const moveCalendar = (calendar, direction, period = MONTH) => { | ||
const { start, end } = moveCalendarDate( | ||
calendar, | ||
startCurrentDate.value, | ||
endCurrentDate.value, | ||
direction, | ||
period | ||
); | ||
startCurrentDate.value = start; | ||
endCurrentDate.value = end; | ||
}; | ||
const selectDate = day => { | ||
selectedRange.value = CUSTOM_RANGE; | ||
if (!selectingEndDate.value || day < selectedStartDate.value) { | ||
selectedStartDate.value = day; | ||
selectedEndDate.value = null; | ||
selectingEndDate.value = true; | ||
} else { | ||
selectedEndDate.value = day; | ||
selectingEndDate.value = false; | ||
} | ||
}; | ||
const setViewMode = (calendar, mode) => { | ||
selectedRange.value = CUSTOM_RANGE; | ||
calendarViews.value[calendar] = mode; | ||
}; | ||
const openCalendar = (index, calendarType, period = MONTH) => { | ||
const current = | ||
calendarType === START_CALENDAR | ||
? startCurrentDate.value | ||
: endCurrentDate.value; | ||
const newDate = | ||
period === MONTH | ||
? setMonth(startOfMonth(current), index) | ||
: setYear(current, index); | ||
if (calendarType === START_CALENDAR) { | ||
startCurrentDate.value = newDate; | ||
} else { | ||
endCurrentDate.value = newDate; | ||
} | ||
setViewMode(calendarType, period === MONTH ? WEEK : MONTH); | ||
}; | ||
const updateManualInput = (newDate, calendarType) => { | ||
if (calendarType === START_CALENDAR) { | ||
selectedStartDate.value = newDate; | ||
startCurrentDate.value = newDate; | ||
} else { | ||
selectedEndDate.value = newDate; | ||
endCurrentDate.value = newDate; | ||
} | ||
selectingEndDate.value = false; | ||
}; | ||
const handleManualInputError = message => { | ||
bus.$emit('newToastMessage', message); | ||
}; | ||
const resetDatePicker = () => { | ||
startCurrentDate.value = startOfDay(currentDate.value); // Resets to today at start of the day | ||
endCurrentDate.value = addMonths(startOfDay(currentDate.value), 1); // Resets to one month ahead | ||
selectedStartDate.value = startOfDay(subDays(currentDate.value, 6)); | ||
selectedEndDate.value = endOfDay(currentDate.value); | ||
selectingEndDate.value = false; | ||
selectedRange.value = LAST_7_DAYS; | ||
// Reset view modes if they are being used to toggle between different calendar views | ||
calendarViews.value = { start: WEEK, end: WEEK }; | ||
}; | ||
const emitDateRange = () => { | ||
if (!isValid(selectedStartDate.value) || !isValid(selectedEndDate.value)) { | ||
bus.$emit('newToastMessage', 'Please select a valid time range'); | ||
} else { | ||
showDatePicker.value = false; | ||
emit('dateRangeChanged', [selectedStartDate.value, selectedEndDate.value]); | ||
} | ||
}; | ||
</script> | ||
|
||
<template> | ||
<div class="relative font-inter"> | ||
<DatePickerButton | ||
:selected-start-date="selectedStartDate" | ||
:selected-end-date="selectedEndDate" | ||
:selected-range="selectedRange" | ||
@open="showDatePicker = !showDatePicker" | ||
/> | ||
<div | ||
v-if="showDatePicker" | ||
class="flex absolute top-9 ltr:left-0 rtl:right-0 z-30 shadow-md select-none w-[880px] h-[490px] rounded-2xl border border-slate-50 dark:border-slate-800 bg-white dark:bg-slate-800" | ||
> | ||
<CalendarDateRange | ||
:selected-range="selectedRange" | ||
@set-range="setDateRange" | ||
/> | ||
<div | ||
class="flex flex-col w-[680px] ltr:border-l rtl:border-r border-slate-50 dark:border-slate-700/50" | ||
> | ||
<div class="flex justify-around h-fit"> | ||
<!-- Calendars for Start and End Dates --> | ||
<div | ||
v-for="calendar in [START_CALENDAR, END_CALENDAR]" | ||
:key="`${calendar}-calendar`" | ||
class="flex flex-col items-center" | ||
> | ||
<CalendarDateInput | ||
:calendar-type="calendar" | ||
:date-value=" | ||
calendar === START_CALENDAR ? manualStartDate : manualEndDate | ||
" | ||
:compare-date=" | ||
calendar === START_CALENDAR ? manualEndDate : manualStartDate | ||
" | ||
:is-disabled="selectedRange !== CUSTOM_RANGE" | ||
@update=" | ||
calendar === START_CALENDAR | ||
? (manualStartDate = $event) | ||
: (manualEndDate = $event) | ||
" | ||
@validate="updateManualInput($event, calendar)" | ||
@error="handleManualInputError($event)" | ||
/> | ||
<div class="py-5 border-b border-slate-50 dark:border-slate-700/50"> | ||
<div | ||
class="flex flex-col items-center gap-2 px-5 min-w-[340px] max-h-[352px]" | ||
:class=" | ||
calendar === START_CALENDAR && | ||
'ltr:border-r rtl:border-l border-slate-50 dark:border-slate-700/50' | ||
" | ||
> | ||
<CalendarYear | ||
v-if="calendarViews[calendar] === YEAR" | ||
:calendar-type="calendar" | ||
:start-current-date="startCurrentDate" | ||
:end-current-date="endCurrentDate" | ||
@select-year="openCalendar($event, calendar, YEAR)" | ||
/> | ||
<CalendarMonth | ||
v-else-if="calendarViews[calendar] === MONTH" | ||
:calendar-type="calendar" | ||
:start-current-date="startCurrentDate" | ||
:end-current-date="endCurrentDate" | ||
@select-month="openCalendar($event, calendar)" | ||
@set-view="setViewMode" | ||
@prev="moveCalendar(calendar, 'prev', YEAR)" | ||
@next="moveCalendar(calendar, 'next', YEAR)" | ||
/> | ||
<CalendarWeek | ||
v-else-if="calendarViews[calendar] === WEEK" | ||
:calendar-type="calendar" | ||
:current-date="currentDate" | ||
:start-current-date="startCurrentDate" | ||
:end-current-date="endCurrentDate" | ||
:selected-start-date="selectedStartDate" | ||
:selected-end-date="selectedEndDate" | ||
:selecting-end-date="selectingEndDate" | ||
:hovered-end-date="hoveredEndDate" | ||
@update-hovered-end-date="hoveredEndDate = $event" | ||
@select-date="selectDate" | ||
@set-view="setViewMode" | ||
@prev="moveCalendar(calendar, 'prev')" | ||
@next="moveCalendar(calendar, 'next')" | ||
/> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
<CalendarFooter @change="emitDateRange" @clear="resetDatePicker" /> | ||
</div> | ||
</div> | ||
</div> | ||
</template> |
79 changes: 79 additions & 0 deletions
79
app/javascript/dashboard/components/ui/DatePicker/components/CalendarAction.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
<script setup> | ||
import { CALENDAR_PERIODS } from '../helpers/DatePickerHelper'; | ||
defineProps({ | ||
calendarType: { | ||
type: String, | ||
default: 'start', | ||
}, | ||
firstButtonLabel: { | ||
type: String, | ||
default: '', | ||
}, | ||
buttonLabel: { | ||
type: String, | ||
default: '', | ||
}, | ||
viewMode: { | ||
type: String, | ||
default: '', | ||
}, | ||
}); | ||
const emit = defineEmits(['prev', 'next', 'set-view']); | ||
const { YEAR } = CALENDAR_PERIODS; | ||
const onClickPrev = type => { | ||
emit('prev', type); | ||
}; | ||
const onClickNext = type => { | ||
emit('next', type); | ||
}; | ||
const onClickSetView = (type, mode) => { | ||
emit('set-view', type, mode); | ||
}; | ||
</script> | ||
|
||
<template> | ||
<div class="flex items-start justify-between w-full h-9"> | ||
<button | ||
class="p-1 rounded-lg hover:bg-slate-75 dark:hover:bg-slate-700/50 rtl:rotate-180" | ||
@click="onClickPrev(calendarType)" | ||
> | ||
<fluent-icon | ||
icon="chevron-left" | ||
size="14" | ||
class="text-slate-900 dark:text-slate-50" | ||
/> | ||
</button> | ||
<div class="flex items-center gap-1"> | ||
<button | ||
v-if="firstButtonLabel" | ||
class="p-0 text-sm font-medium text-center text-slate-800 dark:text-slate-50 hover:text-woot-600 dark:hover:text-woot-600" | ||
@click="onClickSetView(calendarType, viewMode)" | ||
> | ||
{{ firstButtonLabel }} | ||
</button> | ||
<button | ||
v-if="buttonLabel" | ||
class="p-0 text-sm font-medium text-center text-slate-800 dark:text-slate-50" | ||
:class="{ 'hover:text-woot-600 dark:hover:text-woot-600': viewMode }" | ||
@click="onClickSetView(calendarType, YEAR)" | ||
> | ||
{{ buttonLabel }} | ||
</button> | ||
</div> | ||
<button | ||
class="p-1 rounded-lg hover:bg-slate-75 dark:hover:bg-slate-700/50 rtl:rotate-180" | ||
@click="onClickNext(calendarType)" | ||
> | ||
<fluent-icon | ||
icon="chevron-right" | ||
size="14" | ||
class="text-slate-900 dark:text-slate-50" | ||
/> | ||
</button> | ||
</div> | ||
</template> |
Oops, something went wrong.