CASH-2820 | Load more shops

New textLink component for "View more shops" button
Add in method which adds in new shops
New data member which tracks all providers
New data member which tracks if new shops are being loaded
This commit is contained in:
scottkiener-at-safelite 2026-07-15 12:54:54 -04:00
parent 3bd146796c
commit 1a36c38071

View file

@ -1,6 +1,6 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<interceptOverlay v-if="isLoadingDates" /> <interceptOverlay v-if="isLoadingDates || isLoadingMoreShops" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" /> <funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="container page-container-grouped-styles"> <div class="container page-container-grouped-styles">
<div class="row"> <div class="row">
@ -34,20 +34,40 @@
:isLoading="isLoadingDates" :isLoading="isLoadingDates"
@zip-code-clicked="onMobileZipCodeClicked" /> @zip-code-clicked="onMobileZipCodeClicked" />
<inshopSchedulingCard <inshopSchedulingCard
v-for="{ provider } in inShopProvidersAndTimeslots" v-for="entry in inShopProvidersAndTimeslots"
v-show="showInshopSchedulingCards" v-show="showInshopSchedulingCards"
:key="provider.providerNumber" :key="entry.provider.providerNumber"
class="mt-4" class="mt-4"
v-model="selectedScheduling" v-model="selectedScheduling"
:provider="provider" :provider="entry.provider"
:timeSlots=" :timeSlots="
getInshopTimeSlotsForSelectedDate(provider.providerNumber) getInshopTimeSlotsForSelectedDate(entry.provider.providerNumber)
" "
:radioGroupName="schedulingRadioGroupName" :radioGroupName="schedulingRadioGroupName"
:isLoading="isLoadingDates" :isLoading="isLoadingDates || Boolean(entry.isLoadingTimeSlots)"
@address-clicked="onInshopAddressClicked(provider)" /> @address-clicked="onInshopAddressClicked(entry.provider)" />
</div> </div>
</Transition> </Transition>
<div class="d-flex justify-content-center mt-4">
<textLink
v-if="hasMoreShopsAvailable"
id="viewMoreShopsLinkId"
linkType="text"
:text="viewMoreShopsText"
href="javascript:void(0)"
@click-event="onViewMoreShopsClick"
>
<template #after-text>
<span class="spacing-gap"></span>
<img
class="chevron-icon"
src="@/assets/img/icons/chevron-no-background.svg"
alt=""
aria-hidden="true" />
</template>
</textLink>
</div>
<waitlistQuestion <waitlistQuestion
class="mt-5" class="mt-5"
v-model="isWaitlistRequested" v-model="isWaitlistRequested"
@ -73,6 +93,7 @@ import mobileSchedulingCard from "@/layouts/scheduling/mobile-scheduling-card/mo
import inshopSchedulingCard from "@/layouts/scheduling/inshop-scheduling-card/inshop-scheduling-card"; import inshopSchedulingCard from "@/layouts/scheduling/inshop-scheduling-card/inshop-scheduling-card";
import interceptOverlay from "@/ux-components/intercept-overlay/intercept-overlay"; import interceptOverlay from "@/ux-components/intercept-overlay/intercept-overlay";
import waitlistQuestion from "@/layouts/scheduling/waitlist-question/waitlist-question"; import waitlistQuestion from "@/layouts/scheduling/waitlist-question/waitlist-question";
import textLink from "@/ux-components/text-link/text-link";
import store from "@/store"; import store from "@/store";
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants"; import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -100,6 +121,9 @@ function toDateString(offsetDays, base = new Date()) {
const SCHEDULE_FETCH_DAYS = 15; const SCHEDULE_FETCH_DAYS = 15;
const SCHEDULING_RADIO_GROUP_NAME = "schedulingTimeSlot"; const SCHEDULING_RADIO_GROUP_NAME = "schedulingTimeSlot";
const INITIAL_INSHOP_PROVIDER_COUNT = 3;
const MAX_INSHOP_PROVIDERS = 9;
const VIEW_MORE_SHOPS_BATCH_SIZE = 3;
/** /**
* Appends days from newSlots into entry.timeSlots, initializing it if absent. * Appends days from newSlots into entry.timeSlots, initializing it if absent.
@ -200,11 +224,12 @@ export default {
}, },
]; ];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
// Get the first 3 providers from the shopProviderData const allShopProviders = resultMap.providers?.shopProviders ?? [];
const providers = resultMap.providers?.shopProviders?.slice(0, 3) ?? []; const providers = allShopProviders.slice(0, INITIAL_INSHOP_PROVIDER_COUNT);
const mobileProviderNumber = resultMap.providers?.mobileProviderNumber ?? null; const mobileProviderNumber = resultMap.providers?.mobileProviderNumber ?? null;
next(async (vm) => { next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
vm.allShopProviders = allShopProviders;
vm.inShopProvidersAndTimeslots = providers.map((provider) => ({ vm.inShopProvidersAndTimeslots = providers.map((provider) => ({
provider, provider,
timeSlots: null, timeSlots: null,
@ -269,6 +294,9 @@ export default {
serviceLocationText() { serviceLocationText() {
return this.getCmsContent("ServiceLocationText", "Text"); return this.getCmsContent("ServiceLocationText", "Text");
}, },
viewMoreShopsText() {
return this.getCmsContent("ViewMoreShopsWidget", "Text");
},
serviceZipCode() { serviceZipCode() {
return store.getters.order.serviceLocation.zipCode; return store.getters.order.serviceLocation.zipCode;
}, },
@ -308,6 +336,10 @@ export default {
); );
return [...new Set([...inshopDates, ...mobileDates])].sort(); return [...new Set([...inshopDates, ...mobileDates])].sort();
}, },
hasMoreShopsAvailable() {
const maxVisible = Math.min(MAX_INSHOP_PROVIDERS, this.allShopProviders.length);
return this.inShopProvidersAndTimeslots.length < maxVisible;
},
}, },
data() { data() {
return { return {
@ -317,10 +349,12 @@ export default {
datePickerStartDate: toDateString(0), datePickerStartDate: toDateString(0),
datePickerEndDate: toDateString(SCHEDULE_FETCH_DAYS - 1), datePickerEndDate: toDateString(SCHEDULE_FETCH_DAYS - 1),
inShopProvidersAndTimeslots: [], inShopProvidersAndTimeslots: [],
allShopProviders: [],
mobileProviderAndTimeSlot: null, mobileProviderAndTimeSlot: null,
mobilePremiumAppointmentFee: null, mobilePremiumAppointmentFee: null,
selectedScheduling: null, selectedScheduling: null,
isWaitlistRequested: false, isWaitlistRequested: false,
isLoadingMoreShops: false,
}; };
}, },
methods: { methods: {
@ -341,6 +375,50 @@ export default {
onMobileZipCodeClicked() { onMobileZipCodeClicked() {
// TODO: open service zip modal when zip edit is implemented for scheduling page // TODO: open service zip modal when zip edit is implemented for scheduling page
}, },
async onViewMoreShopsClick() {
if (this.isLoadingDates || this.isLoadingMoreShops) return;
const maxVisible = Math.min(MAX_INSHOP_PROVIDERS, this.allShopProviders.length);
const remainingCapacity = maxVisible - this.inShopProvidersAndTimeslots.length;
if (remainingCapacity <= 0) return;
const displayedProviderNumbers = new Set(
this.inShopProvidersAndTimeslots.map(({ provider }) => provider.providerNumber)
);
const newProviders = this.allShopProviders
.filter((provider) => !displayedProviderNumbers.has(provider.providerNumber))
.slice(0, Math.min(VIEW_MORE_SHOPS_BATCH_SIZE, remainingCapacity));
if (!newProviders.length) return;
const newEntries = newProviders.map((provider) => ({
provider,
timeSlots: null,
isLoadingTimeSlots: true,
}));
this.inShopProvidersAndTimeslots = [...this.inShopProvidersAndTimeslots, ...newEntries];
this.isLoadingMoreShops = true;
try {
const resultMap = await settleAllPromises([
{
resultKey: "inshopTimeSlots",
promise: fetchInshopTimeSlots({
startDate: this.datePickerStartDate,
endDate: this.datePickerEndDate,
providerNumbers: newProviders.map((provider) => provider.providerNumber),
pageNameToLog: this.pageName,
}),
},
]);
assignInshopTimeSlotsFromV2Response(newEntries, resultMap.inshopTimeSlots);
} finally {
newEntries.forEach((entry) => {
entry.isLoadingTimeSlots = false;
});
this.isLoadingMoreShops = false;
}
},
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
const order = store.getters.order; const order = store.getters.order;
const logQueue = []; const logQueue = [];
@ -449,6 +527,7 @@ export default {
inshopSchedulingCard, inshopSchedulingCard,
interceptOverlay, interceptOverlay,
waitlistQuestion, waitlistQuestion,
textLink,
}, },
}; };
</script> </script>
@ -476,4 +555,19 @@ h5 {
.card-slide-leave-to { .card-slide-leave-to {
opacity: 0; opacity: 0;
} }
.chevron-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
transform: rotate(180deg);
transition: transform 150ms linear;
vertical-align: baseline;
}
.spacing-gap {
margin-right: 6.5px;
}
:deep(.text-link) {
font-weight:600;
}
</style> </style>