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>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<interceptOverlay v-if="isLoadingDates" />
<interceptOverlay v-if="isLoadingDates || isLoadingMoreShops" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="container page-container-grouped-styles">
<div class="row">
@ -34,20 +34,40 @@
:isLoading="isLoadingDates"
@zip-code-clicked="onMobileZipCodeClicked" />
<inshopSchedulingCard
v-for="{ provider } in inShopProvidersAndTimeslots"
v-for="entry in inShopProvidersAndTimeslots"
v-show="showInshopSchedulingCards"
:key="provider.providerNumber"
:key="entry.provider.providerNumber"
class="mt-4"
v-model="selectedScheduling"
:provider="provider"
:provider="entry.provider"
:timeSlots="
getInshopTimeSlotsForSelectedDate(provider.providerNumber)
getInshopTimeSlotsForSelectedDate(entry.provider.providerNumber)
"
:radioGroupName="schedulingRadioGroupName"
:isLoading="isLoadingDates"
@address-clicked="onInshopAddressClicked(provider)" />
:isLoading="isLoadingDates || Boolean(entry.isLoadingTimeSlots)"
@address-clicked="onInshopAddressClicked(entry.provider)" />
</div>
</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
class="mt-5"
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 interceptOverlay from "@/ux-components/intercept-overlay/intercept-overlay";
import waitlistQuestion from "@/layouts/scheduling/waitlist-question/waitlist-question";
import textLink from "@/ux-components/text-link/text-link";
import store from "@/store";
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -100,6 +121,9 @@ function toDateString(offsetDays, base = new Date()) {
const SCHEDULE_FETCH_DAYS = 15;
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.
@ -200,11 +224,12 @@ export default {
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Get the first 3 providers from the shopProviderData
const providers = resultMap.providers?.shopProviders?.slice(0, 3) ?? [];
const allShopProviders = resultMap.providers?.shopProviders ?? [];
const providers = allShopProviders.slice(0, INITIAL_INSHOP_PROVIDER_COUNT);
const mobileProviderNumber = resultMap.providers?.mobileProviderNumber ?? null;
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.allShopProviders = allShopProviders;
vm.inShopProvidersAndTimeslots = providers.map((provider) => ({
provider,
timeSlots: null,
@ -269,6 +294,9 @@ export default {
serviceLocationText() {
return this.getCmsContent("ServiceLocationText", "Text");
},
viewMoreShopsText() {
return this.getCmsContent("ViewMoreShopsWidget", "Text");
},
serviceZipCode() {
return store.getters.order.serviceLocation.zipCode;
},
@ -308,6 +336,10 @@ export default {
);
return [...new Set([...inshopDates, ...mobileDates])].sort();
},
hasMoreShopsAvailable() {
const maxVisible = Math.min(MAX_INSHOP_PROVIDERS, this.allShopProviders.length);
return this.inShopProvidersAndTimeslots.length < maxVisible;
},
},
data() {
return {
@ -317,10 +349,12 @@ export default {
datePickerStartDate: toDateString(0),
datePickerEndDate: toDateString(SCHEDULE_FETCH_DAYS - 1),
inShopProvidersAndTimeslots: [],
allShopProviders: [],
mobileProviderAndTimeSlot: null,
mobilePremiumAppointmentFee: null,
selectedScheduling: null,
isWaitlistRequested: false,
isLoadingMoreShops: false,
};
},
methods: {
@ -341,6 +375,50 @@ export default {
onMobileZipCodeClicked() {
// 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() {
const order = store.getters.order;
const logQueue = [];
@ -449,6 +527,7 @@ export default {
inshopSchedulingCard,
interceptOverlay,
waitlistQuestion,
textLink,
},
};
</script>
@ -476,4 +555,19 @@ h5 {
.card-slide-leave-to {
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>