Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Custom date picker #9247

Merged
merged 43 commits into from Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ca8140f
feat: Custom date picker
iamsivin Apr 15, 2024
619f961
chore: Calender footer
iamsivin Apr 15, 2024
07ec936
chore: Minor fix
iamsivin Apr 15, 2024
291acb0
chore: Reset date picker
iamsivin Apr 15, 2024
74b8a11
chore: Minor fix
iamsivin Apr 16, 2024
f4281c8
feat: Toggle button
iamsivin Apr 16, 2024
0d0e9e9
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 16, 2024
00c0e1b
chore: Clean up
iamsivin Apr 16, 2024
9b55bcd
chore: Use font inter
iamsivin Apr 16, 2024
f0cca4d
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 17, 2024
9ea2a68
chore: Cleanup and fix bugs
iamsivin Apr 17, 2024
e029b87
fix: custom date range reset the calendar
iamsivin Apr 17, 2024
3f56b16
chore: fix logic bug
iamsivin Apr 17, 2024
924d744
feat: Add manual date range
iamsivin Apr 17, 2024
b376319
fix: styles in rtl
iamsivin Apr 17, 2024
ba064bd
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 18, 2024
d84b49c
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 18, 2024
def2b64
chore: Helper specs
iamsivin Apr 18, 2024
79b8b45
chore: Clean up
iamsivin Apr 18, 2024
87fff08
chore: Review fixes
iamsivin Apr 19, 2024
db4fd64
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 20, 2024
8ca8f71
chore: remove magic strings
iamsivin Apr 22, 2024
430eca5
chore: Add comments
iamsivin Apr 22, 2024
dcf20df
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 22, 2024
c907702
chore: Review fixes
iamsivin Apr 23, 2024
f3e68dc
chore: Clean up
iamsivin Apr 23, 2024
9b0a9d8
chore: remove magic strings
iamsivin Apr 23, 2024
a0a5344
fix: Use outline instead of border
iamsivin Apr 23, 2024
c6f7933
chore: Minor style fix
iamsivin Apr 23, 2024
a623f3c
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 24, 2024
9cc723e
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 24, 2024
0f9f697
chore: disable pointer events for the disabled dates
iamsivin Apr 24, 2024
0fd2331
Merge branch 'develop' into feat/custom-date-picker
scmmishra Apr 24, 2024
6adce5d
chore: Fix code climate
iamsivin Apr 24, 2024
17adde8
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 24, 2024
82e6aff
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 25, 2024
b84f0bd
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 25, 2024
c5de7a1
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 25, 2024
383ea57
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 25, 2024
0b77b7f
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 26, 2024
a28f495
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 26, 2024
401af07
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 28, 2024
259e1da
Merge branch 'develop' into feat/custom-date-picker
iamsivin Apr 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/javascript/dashboard/components/index.js
Expand Up @@ -24,6 +24,7 @@ import SubmitButton from './buttons/FormSubmitButton';
import Tabs from './ui/Tabs/Tabs';
import TabsItem from './ui/Tabs/TabsItem';
import Thumbnail from './widgets/Thumbnail.vue';
import DatePicker from './ui/DatePicker/DatePicker.vue';

const WootUIKit = {
AvatarUploader,
Expand Down Expand Up @@ -51,6 +52,7 @@ const WootUIKit = {
Tabs,
TabsItem,
Thumbnail,
DatePicker,
install(Vue) {
const keys = Object.keys(this);
keys.pop(); // remove 'install' from keys
Expand Down
302 changes: 302 additions & 0 deletions app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue
@@ -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>
@@ -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>