DigitalConsumer.FixMyGlass/src/ux-components/date-picker/date-picker.vue
scottkiener-at-safelite fec5db0c9f CASH-2572 | Date-Picker for new schedule page
Full functionality
Haven't checked styling
Includes mocking data inside scheduling.vue
2026-04-30 12:08:23 -04:00

270 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="date-picker d-flex align-items-center">
<button
class="date-picker__nav-btn"
:disabled="!canGoBack"
aria-label="Previous dates"
@click="goBack">
</button>
<div class="date-picker__track d-flex flex-grow-1">
<button
v-for="date in visibleDates"
:key="date.value"
class="date-picker__day-card d-flex flex-column align-items-center justify-content-center"
:class="{
'date-picker__day-card--selected': isSelected(date.value),
'date-picker__day-card--disabled': !date.isAvailable,
}"
:disabled="!date.isAvailable"
:aria-pressed="isSelected(date.value)"
:aria-label="`${date.dayAbbr} ${date.monthAbbr} ${date.day}`"
@click="selectDate(date)">
<span class="date-picker__day-abbr">{{ date.dayAbbr }}</span>
<span class="date-picker__day-date">{{ date.monthAbbr }} {{ date.day }}</span>
</button>
</div>
<button
class="date-picker__nav-btn"
:disabled="!canGoForward"
aria-label="Next dates"
@click="goForward">
</button>
</div>
</template>
<script>
const DAY_ABBRS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
const MONTH_ABBRS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const MOBILE_WINDOW_SIZE = 3;
const DESKTOP_WINDOW_SIZE = 5;
const DESKTOP_BREAKPOINT_PX = 768;
function toLocalDateString(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
function parseLocalDate(str) {
const [year, month, day] = str.split("-").map(Number);
return new Date(year, month - 1, day);
}
export default {
name: "datePicker",
props: {
availableDates: {
type: Array,
default: () => [],
},
startDate: {
type: String,
default: null,
},
endDate: {
type: String,
default: null,
},
modelValue: {
type: String,
default: null,
},
isLoadingDates: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue", "requestMoreDates"],
data() {
return {
windowStart: 0,
windowSize: DESKTOP_WINDOW_SIZE,
};
},
computed: {
allDates() {
const availableSet = new Set(this.availableDates);
const hasBounds = this.startDate && this.endDate;
const hasAvailable = this.availableDates.length > 0;
if (!hasBounds && !hasAvailable) return [];
let first, last;
if (hasBounds) {
first = parseLocalDate(this.startDate);
last = parseLocalDate(this.endDate);
} else {
const sorted = [...this.availableDates].sort();
first = parseLocalDate(sorted[0]);
last = parseLocalDate(sorted[sorted.length - 1]);
}
const dates = [];
const cursor = new Date(first);
while (cursor <= last) {
const value = toLocalDateString(cursor);
dates.push({
value,
isAvailable: availableSet.has(value),
dayAbbr: DAY_ABBRS[cursor.getDay()],
monthAbbr: MONTH_ABBRS[cursor.getMonth()],
day: cursor.getDate(),
});
cursor.setDate(cursor.getDate() + 1);
}
return dates;
},
visibleDates() {
return this.allDates.slice(this.windowStart, this.windowStart + this.windowSize);
},
canGoBack() {
return this.windowStart > 0;
},
canGoForward() {
return !this.isLoadingDates && this.allDates.length > 0;
},
},
watch: {
// Only reset position when startDate changes — that signals a fresh range.
// Extending endDate/availableDates (loading more) keeps the current window.
startDate() {
this.windowStart = 0;
},
},
mounted() {
this.updateWindowSize();
window.addEventListener("resize", this.updateWindowSize);
if (this.modelValue && this.allDates.length) {
const idx = this.allDates.findIndex((d) => d.value === this.modelValue);
if (idx !== -1) {
this.windowStart = Math.floor(idx / this.windowSize) * this.windowSize;
}
}
},
beforeUnmount() {
window.removeEventListener("resize", this.updateWindowSize);
},
methods: {
updateWindowSize() {
this.windowSize =
window.innerWidth >= DESKTOP_BREAKPOINT_PX
? DESKTOP_WINDOW_SIZE
: MOBILE_WINDOW_SIZE;
},
goBack() {
this.windowStart = Math.max(0, this.windowStart - this.windowSize);
},
goForward() {
if (this.isLoadingDates) return;
const isAtEnd = this.windowStart + this.windowSize >= this.allDates.length;
if (isAtEnd) {
this.$emit("requestMoreDates");
} else {
const maxStart = Math.max(0, this.allDates.length - this.windowSize);
this.windowStart = Math.min(maxStart, this.windowStart + this.windowSize);
}
},
selectDate(date) {
if (!date.isAvailable) return;
this.$emit("update:modelValue", date.value);
},
isSelected(value) {
return this.modelValue === value;
},
},
};
</script>
<style lang="scss">
.date-picker {
gap: 0.25rem;
&__nav-btn {
flex-shrink: 0;
width: 2rem;
height: 2rem;
border: none;
background: transparent;
font-size: 2rem;
line-height: 1;
color: $blue;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border-radius: 50%;
transition: background 150ms ease;
&:hover:not(:disabled) {
background: $blue-100;
}
&:disabled {
color: $gray-300;
cursor: default;
}
}
&__track {
gap: 0.5rem;
}
&__day-card {
flex: 1;
min-width: 0;
border: 1px solid $gray-200;
border-radius: $border-radius-lg;
background: $white;
padding: 0.625rem 0.25rem;
cursor: pointer;
transition:
background 150ms ease,
border-color 150ms ease;
&:hover:not(:disabled):not(.date-picker__day-card--selected) {
border-color: $blue;
}
&--selected {
background: $blue;
border-color: $blue;
.date-picker__day-abbr,
.date-picker__day-date {
color: $white;
}
}
&--disabled {
background: $gray-100;
border-color: $gray-200;
cursor: default;
.date-picker__day-abbr,
.date-picker__day-date {
color: $gray-300;
}
}
}
&__day-abbr {
font-family: $font-family-sans-serif-semibold;
font-size: $font-size-12;
color: $gray-600;
text-transform: uppercase;
line-height: 1.2;
}
&__day-date {
font-family: $font-family-sans-serif-bold;
font-size: $font-size-14;
color: $gray-700;
line-height: 1.4;
white-space: nowrap;
}
}
</style>