CASH-2880 | Make the loader show upon page load & Add loader to datepick

Reduce time that the spinner shows upon page load
Add a loading animation to date-picker
This commit is contained in:
scottkiener-at-safelite 2026-06-24 15:52:11 -04:00
parent 5d8c5b44f5
commit d9b8c0906e
2 changed files with 91 additions and 59 deletions

View file

@ -14,23 +14,39 @@
class="date-picker__track d-flex flex-grow-1" class="date-picker__track d-flex flex-grow-1"
:class="{ 'date-picker__track--loading': isLoadingDates }" :class="{ 'date-picker__track--loading': isLoadingDates }"
:style="isLoadingDates ? { '--card-count': windowSize } : null"> :style="isLoadingDates ? { '--card-count': windowSize } : null">
<button <template v-if="showLoadingPlaceholders">
v-for="(date, index) in visibleDates" <div
:key="date.value" v-for="index in windowSize"
class="date-picker__day-card d-flex flex-column align-items-center justify-content-center" :key="index"
:class="{ class="date-picker__day-card date-picker__day-card--loading d-flex flex-column align-items-center justify-content-center"
'date-picker__day-card--selected': isSelected(date.value), :style="{ '--card-index': index - 1 }"
'date-picker__day-card--disabled': !date.isAvailable, aria-hidden="true">
'date-picker__day-card--loading': isLoadingDates, <!-- Placeholder text is intentionally non-empty so the spans occupy the
}" same height as real content. The --loading CSS hides them via
:style="isLoadingDates ? { '--card-index': index } : null" visibility:hidden, so they are never visible to users. -->
:disabled="!date.isAvailable || isLoadingDates" <span class="date-picker__day-abbr">MON</span>
:aria-pressed="isSelected(date.value)" <span class="date-picker__day-date">Jan 00</span>
:aria-label="`${date.dayAbbr} ${date.monthAbbr} ${date.day}`" </div>
@click="selectDate(date)"> </template>
<span class="date-picker__day-abbr">{{ date.dayAbbr }}</span> <template v-else>
<span class="date-picker__day-date">{{ date.monthAbbr }} {{ date.day }}</span> <button
</button> v-for="(date, index) 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,
'date-picker__day-card--loading': isLoadingDates,
}"
:style="isLoadingDates ? { '--card-index': index } : null"
:disabled="!date.isAvailable || isLoadingDates"
: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>
</template>
</div> </div>
<button <button
class="date-picker__nav-btn" class="date-picker__nav-btn"
@ -115,6 +131,9 @@ export default {
}; };
}, },
computed: { computed: {
showLoadingPlaceholders() {
return this.isLoadingDates && !this.allDates.length;
},
allDates() { allDates() {
if (this.availableDates === null) return []; if (this.availableDates === null) return [];

View file

@ -34,7 +34,7 @@
@zip-code-clicked="onMobileZipCodeClicked" /> @zip-code-clicked="onMobileZipCodeClicked" />
<inshopSchedulingCard <inshopSchedulingCard
v-for="{ provider } in inShopProvidersAndTimeslots" v-for="{ provider } in inShopProvidersAndTimeslots"
v-show="selectedDate" v-show="showInshopSchedulingCards"
:key="provider.providerNumber" :key="provider.providerNumber"
class="mt-4" class="mt-4"
v-model="selectedScheduling" v-model="selectedScheduling"
@ -138,17 +138,6 @@ export default {
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
const serviceZipCode = store.getters.order.serviceLocation.zipCode; const serviceZipCode = store.getters.order.serviceLocation.zipCode;
const shopProviderData = await store.dispatch("getProviders", {
payload: { serviceZipCode },
pageNameToLog: to.name,
});
// Get the first 3 providers from the shopProviderData
const providers = shopProviderData?.data?.shopProviders?.slice(0, 3) ?? [];
const mobileProviderNumber = shopProviderData?.data?.mobileProviderNumber ?? null;
const startDate = toDateString(0);
const endDate = toDateString(SCHEDULE_FETCH_DAYS - 1);
const promiseResultMap = [ const promiseResultMap = [
{ {
resultKey: "cmsContent", resultKey: "cmsContent",
@ -160,45 +149,64 @@ export default {
pageNameToLog: to.name, pageNameToLog: to.name,
}), }),
}, },
...providers.map((provider, i) => ({ {
resultKey: `inshopTimeSlots_${i}`, resultKey: "providers",
promise: fetchInshopTimeSlots({ promise: store.dispatch("getProviders", {
startDate, payload: { serviceZipCode },
endDate,
providerNumber: provider.providerNumber,
pageNameToLog: to.name, pageNameToLog: to.name,
}), }),
})), },
// Get the mobile time slots if a mobile provider number is available
...(mobileProviderNumber
? [
{
resultKey: "mobileTimeSlots",
promise: fetchMobileTimeSlots({
startDate,
endDate,
zipCode: serviceZipCode,
pageNameToLog: to.name,
}),
},
]
: []),
]; ];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
next((vm) => { // Get the first 3 providers from the shopProviderData
const providers = resultMap.providers?.shopProviders?.slice(0, 3) ?? [];
const mobileProviderNumber = resultMap.providers?.mobileProviderNumber ?? null;
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
vm.inShopProvidersAndTimeslots = providers.map((provider, i) => ({ vm.inShopProvidersAndTimeslots = providers.map((provider) => ({
provider, provider,
timeSlots: resultMap[`inshopTimeSlots_${i}`] ?? null, timeSlots: null,
})); }));
vm.mobileProviderAndTimeSlot = mobileProviderNumber vm.mobileProviderAndTimeSlot = mobileProviderNumber
? { ? { providerNumber: mobileProviderNumber, timeSlots: null }
providerNumber: mobileProviderNumber,
timeSlots: resultMap.mobileTimeSlots ?? null,
}
: null; : null;
const startDate = toDateString(0);
const endDate = toDateString(SCHEDULE_FETCH_DAYS - 1);
const timeSlotsPromiseResultMap = [
...providers.map((provider, i) => ({
resultKey: `inshopTimeSlots_${i}`,
promise: fetchInshopTimeSlots({
startDate,
endDate,
providerNumber: provider.providerNumber,
pageNameToLog: to.name,
}),
})),
// Get the mobile time slots if a mobile provider number is available
...(mobileProviderNumber
? [
{
resultKey: "mobileTimeSlots",
promise: fetchMobileTimeSlots({
startDate,
endDate,
zipCode: serviceZipCode,
pageNameToLog: to.name,
}),
},
]
: []),
];
const timeSlotsResultMap = await settleAllPromises(timeSlotsPromiseResultMap);
vm.inShopProvidersAndTimeslots.forEach((entry, i) => {
entry.timeSlots = timeSlotsResultMap[`inshopTimeSlots_${i}`] ?? null;
});
if (vm.mobileProviderAndTimeSlot) {
vm.mobileProviderAndTimeSlot.timeSlots = timeSlotsResultMap.mobileTimeSlots ?? null;
}
vm.datesLoaded = true; vm.datesLoaded = true;
vm.mobilePremiumAppointmentFee = resultMap.mobilePremiumFee ?? null; vm.mobilePremiumAppointmentFee = resultMap.mobilePremiumFee ?? null;
vm.isLoadingDates = false;
}); });
}, },
watch: { watch: {
@ -217,7 +225,12 @@ export default {
return store.getters.order.serviceLocation.zipCode; return store.getters.order.serviceLocation.zipCode;
}, },
showMobileSchedulingCard() { showMobileSchedulingCard() {
return Boolean(this.mobileProviderAndTimeSlot && this.selectedDate); return Boolean(
this.mobileProviderAndTimeSlot && (this.selectedDate || this.isLoadingDates)
);
},
showInshopSchedulingCards() {
return this.selectedDate || this.isLoadingDates;
}, },
mobileTimeSlotsForSelectedDate() { mobileTimeSlotsForSelectedDate() {
if (!this.selectedDate) { if (!this.selectedDate) {
@ -251,7 +264,7 @@ export default {
data() { data() {
return { return {
selectedDate: null, selectedDate: null,
isLoadingDates: false, isLoadingDates: true,
datesLoaded: false, datesLoaded: false,
datePickerStartDate: toDateString(0), datePickerStartDate: toDateString(0),
datePickerEndDate: toDateString(SCHEDULE_FETCH_DAYS - 1), datePickerEndDate: toDateString(SCHEDULE_FETCH_DAYS - 1),