Merge pull request #3088 from Safelite/rlsmerge/2026.03.12-to-dev

Rlsmerge/2026.03.12 to dev
This commit is contained in:
CarlNation 2026-03-06 15:47:33 -05:00 committed by GitHub
commit 9731526eb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1077 additions and 96 deletions

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="21" viewBox="0 0 20 21">
<path fill="#E86421" fill-rule="nonzero" d="M10 .5c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10S4.477.5 10 .5zm.043 13.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm.77-9.2h-1.54l-.088.009a.502.502 0 0 0-.299.193.66.66 0 0 0-.125.47l.747 6.806.017.096c.065.249.263.425.495.426l.084-.008c.22-.042.396-.246.427-.51l.793-6.805.004-.102a.651.651 0 0 0-.127-.37.489.489 0 0 0-.388-.205z"/>
</svg>

Before

Width:  |  Height:  |  Size: 474 B

View file

@ -15,6 +15,8 @@ const GaCategories = {
APPOINTMENT: "Appointment",
SERVICE_LOCATION: "service-location",
CONFIRMATION_CLICKED: "confirmation clicked",
INSHOP_CONFIRMATION_CLICKED: "inshop confirmation clicked",
MOBILE_CONFIRMATION_CLICKED: "mobile confirmation clicked",
RECAL_DISCLAIMER: "Recalibration disclaimer",
};

View file

@ -5,6 +5,7 @@ const experimentUniverses = {
IGQ_SkipQuote: "NextGen_IGQSkipToInsurance",
AFTERPAY_BREAKOUT_DISPLAY: "AfterpayBreakoutDisplay",
MOBILE_FIRST_APPOINTMENT: "MobileFirstAppointment",
MULTI_LOCATION_POPUP: "MultiLocationPopup",
};
const experimentSettings = {
@ -40,6 +41,10 @@ const experimentSettings = {
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITHOUT_MSR: "Show_Mobile_First_Popup_Replace_Without_MSR",
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITH_MSR: "Show_Mobile_First_Popup_Replace_With_MSR",
OFFER_OEM: "OfferOem",
SHOW_MULTI_LOCATION_APPT: "ShowMultiLocationAppt",
SHOW_NO_AVAILABILE_DAYS_MULTI_LOCATION: "Show_NoAvailableDaysMultiLocation",
SHOW_NO_PM_DAYS_MULTI_LOCATION: "Show_NoPMDaysMultiLocation",
SHOW_PM_DAYS_MULTI_LOCATION: "Show_PMDaysMultiLocation",
USE_ADYEN_PAYMENT: "UseAdyenPayment",
SHOW_UNVERIFIED_DEDUCT_ENTRY: "ShowUnverifiedDeducEntry",
SERVICE_PACKAGE_ORDER: "ServicePackageOrder",

View file

@ -226,6 +226,7 @@ export default {
let classes;
switch (this.buttonTypeString) {
case "timeSlotModalListButton":
case "multiLocationRadioButton":
case "listButton":
classes = "w-100";
break;
@ -265,6 +266,8 @@ export default {
classes += " radio-button-container";
} else if (this.buttonTypeString == "servicePackageRadio") {
classes = "package-wrapper col";
} else if (this.buttonTypeString == "multiLocationRadioButton") {
classes = "multi-location-wrapper col";
}
if (this.buttonTypeString == "listCard" && this.buttonsInfo.length > 2) {

View file

@ -317,6 +317,10 @@ export default {
serviceLocationZipCode:
store.getters.order.serviceLocation.zipCode,
pricedLineItems: pricedLineItemsToTax,
serviceLocationAddressLine1:
store.getters.order.serviceLocation?.address,
serviceLocationAddressLine2:
store.getters.order.serviceLocation?.address2,
},
"payment-method",
false

View file

@ -343,14 +343,3 @@ export function splitCMSCopyOnParagraphTag(copy) {
export function splitCMSCopyOnBR(copy) {
return copy.split("<br>");
}
// This function takes in a number and returns it formatted as USD currency, with or without cents depending on if the number is an integer or not.
export function formatToUSDollar(amount) {
const isInteger = amount % 1 === 0;
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: isInteger ? 0 : 2,
maximumFractionDigits: 2,
}).format(amount);
}

View file

@ -0,0 +1,11 @@
import store from "@/store";
export function hasVehicleInfo() {
return (
store.getters.order?.vehicle.year &&
store.getters.order?.vehicle?.make &&
store.getters.order?.vehicle?.model &&
store.getters.order?.vehicle?.style &&
store.getters.order?.vehicle?.carId
);
}

View file

@ -322,6 +322,8 @@ export default {
serviceLocationState: store.getters.order.serviceLocation.state,
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
pricedLineItems: lineItemsForCart,
serviceLocationAddressLine1: store.getters.order.serviceLocation?.address,
serviceLocationAddressLine2: store.getters.order.serviceLocation?.address2,
},
"payment-method",
false
@ -526,6 +528,10 @@ export default {
serviceLocationState: this.$store.getters.order.serviceLocation.state,
serviceLocationZipCode: this.$store.getters.order.serviceLocation.zipCode,
pricedLineItems: this.lineItems,
serviceLocationAddressLine1:
this.$store.getters.order.serviceLocation?.address,
serviceLocationAddressLine2:
this.$store.getters.order.serviceLocation?.address2,
},
"payment-method",
false
@ -650,6 +656,8 @@ export default {
serviceLocationState: store.getters.order.serviceLocation.state,
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
pricedLineItems: this.lineItems,
serviceLocationAddressLine1: store.getters.order.serviceLocation?.address,
serviceLocationAddressLine2: store.getters.order.serviceLocation?.address2,
},
"payment-method",
false

View file

@ -0,0 +1,345 @@
<template>
<div id="multi-location-modal-container">
<modal
:ref="modalName"
:headerText="modalHeaderText"
suppressPageScroll
:onModalClosedCallback="onModalClosed"
:footerButtonText="modalFooterText"
:isFooterButtonPrimary="true"
@footer-button-event="confirmAppointment"
@isModalOpened="setIsModalOpen">
<buttonQuestion
ref="buttonQuestion"
buttonTypeString="multiLocationRadioButton"
:buttonTypeObject="multiLocationRadioButton"
class="mt-5"
:answers="availableAppointments"
groupName="multiLocationQuestion"
textPosition="text-center"
v-model="selectedValue"
isRequired
validationRules="time-slot-required" />
<template v-slot:modal-header-slot>
<span>{{ modalSubHeaderText }}</span>
</template>
<template v-slot:modal-footer-slot>
<button
type="button"
class="btn btn-link"
id="see-more-options"
:disabled="isLoading"
@click="closeModal">
{{ buttonText }}
</button>
</template>
</modal>
</div>
</template>
<script>
// Supporting files
import store from "@/store";
import modal from "@/digital-components/modal/modal";
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
import { get12HourTimeFormat } from "@/helpers/date-helper";
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
import buttonQuestion from "@/digital-components/button-question/button-question";
import multiLocationRadioButton from "./multi-location-radio/multi-location-radio";
// Validation
import { defineRule, useField } from "vee-validate";
import { errorMessages } from "@/constants/error-messages";
import { required } from "@/helpers/validation-rules";
const NULL_APPOINTMENT = {
estimatedServiceMinutes: {
minimum: null,
maximum: null,
},
timeSlot: null,
date: null,
};
// Validation for the modal button
defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
export default {
name: "multi-location-modal",
props: {
cmsWidgetName: String,
modalWidgetName: String,
buttonWidgetName: String,
},
data() {
return {
isModalOpen: false,
mobileAppointmentOption: NULL_APPOINTMENT,
inshopAppointmentOption: NULL_APPOINTMENT,
confirmedAppointment: false,
multiLocationRadioButton: multiLocationRadioButton,
selectedValue: null,
experimentSettings: "",
};
},
methods: {
splitCopyOnCMSPlaceHolder,
get12HourTimeFormat,
openModal() {
this.modal.openModal();
},
onModalClosed() {
this.pushEvent();
this.confirmedAppointment = false;
},
closeModal() {
this.modal.closeModal();
},
confirmAppointment() {
if (!this.selectedAppointment.timeSlot.id) return;
const selectedTimeSlot = {
timeSlot: this.selectedAppointment.timeSlot,
routeCode: this.selectedAppointment.timeSlot.id,
date: this.selectedAppointment.date,
};
this.confirmedAppointment = true;
this.$emit("confirm-appointment", {
selectedTimeSlot: selectedTimeSlot,
appointmentType: this.selectedValue,
});
this.modal.closeModal();
},
setPromotedMobileAppointment(appointment) {
this.mobileAppointmentOption = appointment;
},
setPromotedInshopAppointment(appointment) {
this.inshopAppointmentOption = appointment;
},
getIsModalOpen() {
return this.isModalOpen;
},
setIsModalOpen(isOpen) {
this.isModalOpen = isOpen;
},
captureExperimentSettings(settingsString) {
this.experimentSettings = settingsString;
},
pushEvent() {
let gaAction = this.confirmedAppointment ? this.GaActions.YES : this.GaActions.NO;
if (
this.selectedValue?.toLowerCase() === "mobile" &&
this.mobileAppointmentOption?.timeSlot
) {
this.pushEventToGA(
this.GaCategories.MOBILE_CONFIRMATION_CLICKED,
gaAction,
this.mobileAppointmentOption.timeSlot.startTime +
"-" +
this.mobileAppointmentOption.timeSlot.endTime +
" " +
this.mobileAppointmentOption.date,
true,
null,
this.experimentSettings
);
} else if (
this.selectedValue?.toLowerCase() === "inshop" &&
this.inshopAppointmentOption?.timeSlot
) {
this.pushEventToGA(
this.GaCategories.INSHOP_CONFIRMATION_CLICKED,
gaAction,
this.inshopAppointmentOption.timeSlot.startTime +
"-" +
this.inshopAppointmentOption.date,
true,
null,
this.experimentSettings
);
}
},
},
computed: {
selectedAppointment() {
if (this.selectedValue?.toLowerCase() === "mobile") {
return this.mobileAppointmentOption;
} else if (this.selectedValue?.toLowerCase() === "inshop") {
return this.inshopAppointmentOption;
} else {
return NULL_APPOINTMENT;
}
},
modalName() {
return this.modalWidgetName;
},
modal() {
return this.$refs[this.modalName];
},
modalSubHeaderText() {
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
},
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, "HeaderText");
},
modalFooterText() {
return this.getCmsContent(this.modalWidgetName, "FooterText");
},
buttonText() {
return this.getCmsContent(this.modalWidgetName, "FooterText2");
},
mobileButtonLabel() {
return this.getCmsContent(this.buttonWidgetName, "HeaderText");
},
inshopButtonLabel() {
return this.getCmsContent(this.buttonWidgetName, "SubheaderText");
},
mobileButtonDate() {
if (!this.mobileAppointmentOption.date) return "";
return new Date(this.mobileAppointmentOption.date).toLocaleDateString("en-US", {
timeZone: "UTC",
weekday: "short",
month: "long",
day: "numeric",
});
},
inshopButtonDate() {
if (!this.inshopAppointmentOption.date) return "";
return new Date(this.inshopAppointmentOption.date).toLocaleDateString("en-US", {
timeZone: "UTC",
weekday: "short",
month: "long",
day: "numeric",
});
},
mobileButtonTime() {
const timeslot =
this.get12HourTimeFormat(this.mobileAppointmentOption.timeSlot?.startTime) +
" - " +
this.get12HourTimeFormat(this.mobileAppointmentOption.timeSlot?.endTime);
return timeslot;
},
inshopButtonTime() {
const timeslot = this.get12HourTimeFormat(
this.inshopAppointmentOption.timeSlot?.startTime
);
return timeslot;
},
mobileLocationLabel() {
return this.getCmsContent(this.buttonWidgetName, "BodyText");
},
inshopLocationLabel() {
return this.inshopAppointmentOption?.addressCopy;
},
mobileServiceDuration() {
const durationTime = getDisplayTextForDurationLength(
this.mobileAppointmentOption.estimatedServiceMinutes.minimum,
this.mobileAppointmentOption.estimatedServiceMinutes.maximum
);
const footerText = this.getCmsContent(this.buttonWidgetName, "FooterText").replaceAll(
"{custom:serviceLength}",
durationTime
);
return footerText;
},
inshopServiceDuration() {
const durationTime = getDisplayTextForDurationLength(
this.inshopAppointmentOption.estimatedServiceMinutes.minimum,
this.inshopAppointmentOption.estimatedServiceMinutes.maximum
);
const footerText = this.getCmsContent(this.buttonWidgetName, "FooterText").replaceAll(
"{custom:serviceLength}",
durationTime
);
return footerText;
},
availableAppointments() {
return [
{
value: "mobile",
buttonLabel: this.mobileButtonDate,
buttonLabelSubCopy: this.mobileButtonTime,
buttonBodyCopy: this.mobileLocationLabel,
buttonAuxillaryCopy: this.mobileButtonLabel,
buttonFooterCopy: this.mobileServiceDuration,
},
{
value: "inshop",
buttonLabel: this.inshopButtonDate,
buttonLabelSubCopy: this.inshopButtonTime,
buttonBodyCopy: this.inshopLocationLabel,
buttonAuxillaryCopy: this.inshopButtonLabel,
buttonFooterCopy: this.inshopServiceDuration,
},
];
},
},
components: { modal, buttonQuestion },
};
</script>
<style lang="scss">
#multi-location-modal-container {
.modal-component {
@include media-breakpoint-up(md) {
.modal-dialog {
left: 0;
align-content: center;
flex-wrap: wrap;
width: 22.063rem;
transform: translate(0, 0);
.modal-content {
border-radius: $border-radius-lg;
}
}
}
.modal-dialog {
.modal-content {
.modal-header {
flex-direction: column;
text-align: center;
padding: 0 1rem;
& > span {
color: $red;
font-weight: $font-weight-600;
text-transform: uppercase;
font-family: $font-family-sans-serif-semibold;
}
.modal-title {
font-size: $font-size-20;
font-weight: $font-weight-normal;
}
}
.modal-body {
padding: 0.5rem 1rem;
.button-question {
margin-top: 0.75rem;
}
.modal-body-inner {
ul {
list-style: none;
padding: 1rem;
background-color: #f4f4f4;
font-size: $font-size-14;
}
}
}
.modal-footer {
flex-flow: wrap-reverse;
#see-more-options {
width: 100%;
text-decoration: none;
margin-top: 1.5rem;
font-weight: $font-weight-600;
}
}
}
}
}
}
</style>

View file

@ -0,0 +1,121 @@
import { mount } from "@vue/test-utils";
import multiLocationRadio from "./multi-location-radio";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
describe("multi-location-radio.vue", () => {
it("Should include buttonLabel in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
});
// Act
const outputHtml = wrapper.html();
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabel"]));
});
it("Should include buttonLabelSubCopy in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
});
// Act
const outputHtml = wrapper.html();
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelSubCopy"]));
});
it("Should include buttonFooterCopy in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
});
// Act
const outputHtml = wrapper.html();
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonFooterCopy"]));
});
it("Should get 5 strings from getArrayOfListItemsFromRawCmsCopy when provided with a <ul><li>...</li>(x5)</ul> buttonBodyCopy", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
});
// Act
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy);
// Assert
expect(results.length).toBe(5);
});
it("Should get strings from getArrayOfListItemsFromRawCmsCopy without <ul> or <li> tags when provided with a <ul><li>...</li></ul> buttonBodyCopy", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
});
// Act
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy);
// Assert
const fileredResults = results.filter((result) => {
return (
result.includes("<ul>") ||
result.includes("</ul>") ||
result.includes("<li>") ||
result.includes("</li>")
);
});
expect(fileredResults.length).toBe(0);
});
it("Should get 5 strings from getArrayOfListItemsFromRawCmsCopy when buttonBodyCopy has 6 total <li>...</li>, but one is empty ", async () => {
// Arrange
const moddedProps = mockProps;
moddedProps["buttonBodyCopy"] =
"<ul><li>buttonBodyCopy test copy</li><li>2</li><li>3</li><li>4</li><li>5</li><li></li></ul>";
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: moddedProps,
},
});
// Act
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy);
// Assert
expect(results.length).toBe(5);
});
});
const mockProps = {
groupName: "group-name",
buttonLabel: "buttonLabel test copy",
buttonLabelAuxillaryCopy: "buttonLabelAuxillaryCopy test copy",
buttonLabelSubCopy: "buttonLabelSubCopy test copy",
buttonBodyCopy:
"<ul><li>buttonBodyCopy test copy</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>",
buttonFooterCopy: "buttonFooterCopy test copy",
buttonAuxillaryCopy: "buttonAuxillaryCopy test copy",
};
function setupMocks({ mountOptionsMockData = {} }) {
const wrapper = mount(multiLocationRadio, {
...mountOptionsMockData,
mixins: [inputButtonWrapperMixin],
});
return { wrapper };
}

View file

@ -0,0 +1,311 @@
<template>
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange" v-model="selectedValue">
<div class="radio-label" for="testradio">
<div class="location-specs">
<div class="row">
<div class="col md-6">
<p class="m-0">
<span v-html="this.buttonLabel"></span>
</p>
<span class="service-tag" v-html="this.buttonAuxillaryCopy"></span>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
v-html="this.buttonLabelSubCopy"></p>
</div>
<div class="col md-6 location-info">
<div
class="location-info-label"
v-if="this.buttonBodyCopy && this.value === 'mobile'"
v-html="this.buttonBodyCopy"></div>
<div
class="location-info-label"
v-if="this.buttonBodyCopy && this.value === 'inshop'">
<textLink
linkType="newWindowLink"
:text="this.buttonBodyCopy"
:href="inshopHref"
target="_blank" />
</div>
</div>
<div class="col md-6 time-info" v-html="this.buttonFooterCopy"></div>
</div>
</div>
</div>
</baseInputButton>
</template>
<script>
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
import textLink from "@/ux-components/text-link/text-link";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
import {
getRouterLinkDisplayTextFromCopy,
getRouterLinkRouteFromCopy,
splitCopyOnCMSPlaceHolder,
doesCopyContainTextLink,
} from "@/helpers/cms-content-helper";
import baseMixin from "@/mixins/base-mixin.js";
export default {
name: "multiLocationRadioButton",
mixins: [inputButtonWrapperMixin],
components: {
baseInputButton,
textLink,
},
computed: {
arrayOfListItemsFromBodyText() {
return this.getArrayOfListItemsFromRawCmsCopy(this.buttonBodyCopy);
},
shouldDisplayStrikeThroughPrice() {
const displayPrice = this.buttonAuxillaryCopy?.substring(
this.buttonAuxillaryCopy.indexOf("$")
);
return displayPrice != this.additionalButtonData?.strikeThroughPrice;
},
isAppleBrowser() {
return baseMixin.methods.isAppleBrowser();
},
inshopHref() {
if (this.isAppleBrowser) {
return "https://maps.apple.com/?q=" + encodeURIComponent(this.buttonBodyCopy) + '"';
}
return (
"https://www.google.com/maps/search/?api=1&query=" +
encodeURIComponent(this.buttonBodyCopy) +
'"'
);
},
},
methods: {
getRouterLinkDisplayTextFromCopy,
getRouterLinkRouteFromCopy,
splitCopyOnCMSPlaceHolder,
doesCopyContainTextLink,
stripUlTagFromCopy(copy) {
const regex = /(?:<ul(?:.*?)>)|(?:<\/ul>)/g;
return copy?.replace(regex, "");
},
getArrayOfListItemsFromRawCmsCopy(copy) {
const withoutUlTags = this.stripUlTagFromCopy(copy);
return withoutUlTags
?.split(/(?:<li(?:.*?)>)|(?:<\/li>)/g)
?.filter((lineItem) => lineItem);
},
},
};
</script>
<style lang="scss">
.multi-location-wrapper {
margin: 1rem 0;
&:first-child {
margin: 1rem 0 0 0;
}
&:last-child {
margin: 1rem 0 0 0;
}
label {
display: block;
@include media-breakpoint-up(md) {
display: flex;
height: 100%;
}
}
input[type="radio"] {
opacity: 0;
position: absolute;
left: -9999px;
+ .radio-label {
display: flex;
align-items: flex-start;
position: relative;
cursor: pointer;
width: 100%;
padding: 0.75rem;
border: 1px solid $gray-300;
box-shadow:
0px 4px 8px -4px rgba(0, 0, 0, 0.15),
0px 4px 24px -8px rgba(0, 0, 0, 0.2);
border-radius: 0.5rem;
overflow: hidden;
min-height: 52px;
max-height: 126px;
@include media-breakpoint-up(md) {
padding-bottom: 3rem;
}
transition: max-height 0.25s ease-in;
&.has-subheader {
min-height: 72px;
}
&:before {
content: "";
position: relative;
top: 5px;
margin-right: 0.5rem;
border-radius: 50%;
border: 1px solid $gray-500;
width: 16px;
height: 16px;
min-width: 16px;
}
&:after {
content: "";
position: absolute;
left: 15px;
top: 20px;
border-radius: 50%;
width: 10px;
height: 10px;
min-width: 10px;
}
}
&:hover {
+ .radio-label {
&:before {
border: 1px solid #8e9292;
box-shadow:
0px 0px 0px 4px #9fcee6,
0px 1px 4px rgba(0, 0, 0, 0.2);
}
}
}
&:checked {
+ .radio-label {
.location-specs {
.hide-when-closed {
display: block;
}
.service-tag {
background: $blue;
color: $blue-150;
}
}
}
+ .radio-label {
border: 1px solid $blue;
background-color: $blue-100;
}
+ .radio-label {
&:before {
box-shadow: 0px 0px 0px 1px $blue;
}
}
+ .radio-label {
&:after {
background: $blue;
}
}
}
&:focus {
+ .radio-label {
&:before {
border: 2px solid $blue;
}
}
}
}
.location-specs {
display: flex;
flex-direction: column;
width: 100%;
transition: all 0.5s ease;
.row {
align-items: center;
.col {
flex: 1 0 100%;
}
.service-tag {
font-size: $font-size-12;
font-family: $font-family-sans-serif-semibold;
position: absolute;
top: 0;
right: 0;
background: $blue-150;
color: $blue;
padding: 0.25rem 0.5rem;
border-radius: 1rem;
margin: 0.5rem;
}
p {
font-size: $font-size-14;
& > span {
color: $black;
font-family: $font-family-sans-serif-semibold;
font-weight: $font-weight-bold;
font-size: $font-size-base;
}
}
.sub-label {
margin-bottom: 0.5rem !important;
border-bottom: 1px solid $gray-1000;
}
}
.location-info-label,
.time-info {
font-family: $font-family-sans-serif;
font-size: $font-size-12;
}
.location-info-label {
display: flex;
align-items: center;
&:before {
content: "";
display: inline-block;
position: relative;
top: 0;
margin-right: 0.375rem;
width: 0.75rem;
height: 1rem;
background-image: url(~@/assets/img/icons/location-pin.svg);
background-repeat: no-repeat;
background-position: 0 50%;
background-size: 0.75rem auto;
padding: 0 0.75rem 0 0;
@include blue-filter();
}
& > a.new-window-link {
margin-bottom: 0;
font-family: $font-family-sans-serif;
font-size: $font-size-12;
padding-bottom: 0;
}
}
@keyframes slideaway {
from {
display: block;
}
to {
transform: translateY(40px);
opacity: 0;
}
}
.hide-when-closed {
display: none;
@include media-breakpoint-up(md) {
display: flex;
flex-direction: column;
}
}
}
}
</style>

View file

@ -5,7 +5,12 @@
<mobileFirstModal
modalWidgetName="MobileFirstModalWidget"
ref="mobileFirstModal"
@confirm-appointment="updateTimeSlotandNavigateForward" />
@confirm-appointment="updateMobileFirstTimeSlotandNavigateForward" />
<multiLocationModal
modalWidgetName="MultiLocationModalWidget"
buttonWidgetName="MultiLocationRadioButtonWidget"
ref="multiLocationModal"
@confirm-appointment="updateMultiLocationTimeSlotandNavigateForward" />
<recalAckModal
modalWidgetName="RecalAckModalWidget"
agreementWidgetName="RecalAgreementQuestionWidget"
@ -14,7 +19,9 @@
ref="recalAckModal" />
<div
class="container page-container-grouped-styles"
:class="[isMobileFirstModalOpen ? 'hidden-background' : '']">
:class="[
isMobileFirstModalOpen || isMultiLocationModalOpen ? 'hidden-background' : '',
]">
<div class="row">
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
<mobileFeeWaiverAlert
@ -74,16 +81,6 @@
cmsWidgetName="AlertNoShopsWidget"
v-if="displayNoShopsAlert"
alertClass="alert-warning" />
<alert
ref="alertMSRNotCoveredByInsurance"
class="mt-5 mb-5"
v-if="isMSRFeeNotCoveredByInsurance"
:manualHeadline="MSRFeeAlertHeadlineText"
:manualCopy="MSRFeeAlertBodyText"
:showWarningIcon="true"
:hasBorder="true"
:showHorizontalRow="true"
alertClass="alert-warning" />
</div>
</div>
<div
@ -201,6 +198,7 @@ import shopLocation from "@/layouts/schedule/shop-location/shop-location";
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
import durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.vue";
import mobileFirstModal from "@/layouts/schedule/mobile-first-modal/mobile-first-modal.vue";
import multiLocationModal from "@/layouts/schedule/multi-location-modal/multi-location-modal.vue";
import recalAckModal from "@/layouts/schedule/recal-ack-modal/recal-ack-modal.vue";
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
@ -231,11 +229,7 @@ import {
RECAL_ACK_YES,
} from "@/constants/schedule-constants";
import {
fetchCmsContentForPage,
splitCopyOnCMSPlaceHolder,
formatToUSDollar,
} from "@/helpers/cms-content-helper";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import {
getPricedMobileFeePart,
@ -251,6 +245,7 @@ import { partNumberStrings } from "@/constants/part-number-strings";
import store from "@/store";
import { storeActions } from "@/constants/store-actions";
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
import {
calcDaysBetweenDates,
convertDateStringToDate,
@ -488,10 +483,7 @@ export default {
shopListButton: shopListButton,
lastSelectedInshopOrDropoffProvider: null,
selectedMobileFirstAppointment: false,
isShowMobileFirstAppt: experimentMixin.methods.hasSettingEqualTo(
experimentSettings.SHOW_MOBILE_FIRST_APPT,
"true"
),
selectedMultiLocationAppointment: false,
calendarLoadingStatus: "none",
};
},
@ -624,7 +616,8 @@ export default {
shopProviderData.data
);
vm.initializeDatePicker().then(() => {
vm.showMobileFirstModal();
// vm.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
vm.showMultiLocationModal();
vm.hideLoadingModal();
vm.calendarLoadingStatus = "none";
if (!vm.preSelectedDate) vm.setSelectedDateToFirstAvailable();
@ -690,7 +683,8 @@ export default {
isMobileStaticRecalibrationApplicable() {
return (
this.displayMSR &&
this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE
this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE &&
(this.isCashItacNoComp || this.mobileFeePart?.isInsurable)
);
},
displayMSR() {
@ -700,28 +694,6 @@ export default {
?.toLowerCase() === "true"
);
},
isMSRFeeNotCoveredByInsurance() {
return (
this.isMobileSelected &&
this.isMobileStaticRecalibrationApplicable &&
this.mobileFeeHasPrice &&
this.isInsurance &&
!this.mobileFeePart?.isInsurable
);
},
MSRFeeAlertHeadlineText() {
return this.getCmsContent("AlertMSRNotCoveredByInsuranceWidget", "HeadlineText");
},
MSRFeeAlertBodyText() {
const cmsContentText = this.getCmsContent(
"AlertMSRNotCoveredByInsuranceWidget",
"BodyText"
);
return cmsContentText.replaceAll(
"{custom:mobileFee}",
formatToUSDollar(this.mobileFee)
);
},
isCashItacNoComp() {
return !this.isInsurance || this.isITAC || this.isNoComp;
},
@ -934,6 +906,11 @@ export default {
? this.$refs.mobileFirstModal?.getIsModalOpen()
: false;
},
isMultiLocationModalOpen() {
return this.selectableDatesMobile.days && this.selectableDatesInshop.days
? this.$refs.multiLocationModal?.getIsModalOpen()
: false;
},
displayWaitList() {
if (!this.appointmentType) return false;
const firstMobileDateString =
@ -988,6 +965,18 @@ export default {
return this.selectableDatesInshop?.days?.length > 0;
}
},
isShowMobileFirstAppt() {
return experimentMixin.methods.hasSettingEqualTo(
experimentSettings.SHOW_MOBILE_FIRST_APPT,
"true"
);
},
isShowMultiLocationPopup() {
return experimentMixin.methods.hasSettingEqualTo(
experimentSettings.SHOW_MULTI_LOCATION_APPT,
"true"
);
},
},
methods: {
splitCopyOnCMSPlaceHolder,
@ -1273,7 +1262,8 @@ export default {
}
}
this.initializeDatePicker().then(() => {
this.showMobileFirstModal();
// this.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
this.showMultiLocationModal();
this.hideLoadingModal();
this.calendarLoadingStatus = "none";
this.setSelectedDateToFirstAvailable();
@ -1307,7 +1297,7 @@ export default {
},
async initializeDatePicker() {
this.isShowMobileFirstAppt && this.showLoadingModal();
this.isShowMultiLocationPopup && this.showLoadingModal();
const includeMobileTimeSlots = this.isServiceableMobile;
const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff;
@ -1819,7 +1809,8 @@ export default {
this.zipCodeCtu = newZipCode.zipCodeCtu;
this.selectedDate = null;
this.initializeDatePicker().then(() => {
this.showMobileFirstModal();
// this.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
this.showMultiLocationModal();
this.hideLoadingModal();
this.calendarLoadingStatus = "none";
this.setSelectedDateToFirstAvailable();
@ -1934,10 +1925,10 @@ export default {
const order = this.$store.getters.order;
const isRepair = order.damage.isRepair;
let preSelectedMobileAppointment = null;
let promotedMobileAppointment = null;
const todaysDate = getTodayDate();
let firstMobileAMAppt = await this.getFirstAvailableMobileApptByTOD("AM");
let firstMobilePMAppt = await this.getFirstAvailableMobileApptByTOD("PM");
let firstMobileAMAppt = await this.getFirstAvailableApptByTOD("AM", "mobile");
let firstMobilePMAppt = await this.getFirstAvailableApptByTOD("PM", "mobile");
if (!firstMobileAMAppt && !firstMobilePMAppt) {
return;
}
@ -2032,7 +2023,7 @@ export default {
if (this.isShowMobileFirstAppt) {
if (pmMobileDays == 0) {
// Selects the first available time slot
preSelectedMobileAppointment =
promotedMobileAppointment =
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
? firstMobileAMAppt
: firstMobilePMAppt;
@ -2041,20 +2032,20 @@ export default {
numberOfDaysToFirstMobilePMDate <= pmMobileDays
) {
// Selects the first available PM time slot
preSelectedMobileAppointment = firstMobilePMAppt;
promotedMobileAppointment = firstMobilePMAppt;
} else if (
firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate >= noPMMobileDays
) {
// Selects the first available AM time slot. If none, selects the first available PM time slot
preSelectedMobileAppointment = firstMobileAMAppt
promotedMobileAppointment = firstMobileAMAppt
? firstMobileAMAppt
: firstMobilePMAppt;
}
if (preSelectedMobileAppointment) {
if (promotedMobileAppointment) {
this.$refs.mobileFirstModal.setSelectedAppointment(
preSelectedMobileAppointment
promotedMobileAppointment
);
this.$refs.mobileFirstModal.openModal();
}
@ -2062,11 +2053,176 @@ export default {
}
}
},
async getFirstAvailableMobileApptByTOD(timeOfDay) {
if (!this.selectableDatesMobile?.days?.length) {
async showMultiLocationModal() {
if (!this.selectedRouteCodeData?.routeCode) {
let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION
);
let maxDayRangeToShowNoPmTimeslot = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_NO_PM_DAYS_MULTI_LOCATION
);
let maxDayRangeToShowAnyTimeslot = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_NO_AVAILABILE_DAYS_MULTI_LOCATION
);
let showMultiLocationAppointment = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_MULTI_LOCATION_APPT
);
let experimentSettingsString =
"ShowPm:" +
maxDayRangeToShowPmTimeslot +
"|" +
"NoPm:" +
maxDayRangeToShowNoPmTimeslot +
"|" +
"NoAvail:" +
maxDayRangeToShowAnyTimeslot;
let promotedMobileAppointment = null;
let promotedInshopAppointment = null;
const todaysDate = getTodayDate();
// Helper function to calculate days until appointment (handles null/undefined)
const calculateDaysUntilDate = (appointmentObj) => {
const dateString = appointmentObj?.date;
return dateString
? (new Date(dateString) - todaysDate) / (1000 * 60 * 60 * 24)
: null;
};
// Fetch all appointments
const [firstMobileAMAppt, firstMobilePMAppt, firstInshopAMAppt, firstInshopPMAppt] =
await Promise.all([
this.getFirstAvailableApptByTOD("AM", "mobile"),
this.getFirstAvailableApptByTOD("PM", "mobile"),
this.getFirstAvailableApptByTOD("AM", "inshop"),
this.getFirstAvailableApptByTOD("PM", "inshop"),
]);
// Extract dates
const firstMobileAMDate = firstMobileAMAppt?.date;
const firstMobilePMDate = firstMobilePMAppt?.date;
const firstInshopAMDate = firstInshopAMAppt?.date;
const firstInshopPMDate = firstInshopPMAppt?.date;
// Calculate days until appointments
const numberOfDaysToFirstMobileAMDate = calculateDaysUntilDate(firstMobileAMAppt);
const numberOfDaysToFirstMobilePMDate = calculateDaysUntilDate(firstMobilePMAppt);
const numberOfDaysToFirstInshopAMDate = calculateDaysUntilDate(firstInshopAMAppt);
const numberOfDaysToFirstInshopPMDate = calculateDaysUntilDate(firstInshopPMAppt);
const shouldExposeMultiLocationModal = () => {
const isMobileAppointmentInDateRange =
(firstMobileAMDate &&
numberOfDaysToFirstMobileAMDate <= maxDayRangeToShowAnyTimeslot) ||
(firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate <= maxDayRangeToShowAnyTimeslot);
const isInshopAppointmentInDateRange =
(firstInshopAMDate &&
numberOfDaysToFirstInshopAMDate <= maxDayRangeToShowAnyTimeslot) ||
(firstInshopPMDate &&
numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowAnyTimeslot);
if (showMultiLocationAppointment?.toLowerCase() === "true") {
return isMobileAppointmentInDateRange && isInshopAppointmentInDateRange;
}
return false;
};
if (shouldExposeMultiLocationModal()) {
const multiLocationPopupExperiment =
store.getters.applicationUser.experiments.find(
(e) => e.universeName === experimentUniverses.MULTI_LOCATION_POPUP
);
const hasExposedMultiLocationPopup = multiLocationPopupExperiment?.isExposed;
if (!hasExposedMultiLocationPopup && multiLocationPopupExperiment) {
baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.LOG_EXPERIMENT_EXPOSURE_AND_UPDATE_STORE,
{
userId: getUserIdValue(),
deviceId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(),
pageName: "schedule",
experiment: multiLocationPopupExperiment,
},
"schedule",
false
);
}
if (this.isShowMultiLocationPopup) {
if (maxDayRangeToShowPmTimeslot == 0) {
// Selects the first available mobile time slot
promotedMobileAppointment =
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
? firstMobileAMAppt
: firstMobilePMAppt;
// Selects the first available inshop time slot
promotedInshopAppointment =
numberOfDaysToFirstInshopAMDate <= numberOfDaysToFirstInshopPMDate
? firstInshopAMAppt
: firstInshopPMAppt;
} else {
if (
firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate <= maxDayRangeToShowPmTimeslot
) {
// Selects the first available PM time slot
promotedMobileAppointment = firstMobilePMAppt;
} else if (
firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate >= maxDayRangeToShowNoPmTimeslot
) {
// Selects the first available AM time slot. If none, selects the first available PM time slot
promotedMobileAppointment = firstMobileAMAppt
? firstMobileAMAppt
: firstMobilePMAppt;
}
if (
firstInshopPMDate &&
numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowPmTimeslot
) {
// Selects the first available PM time slot
promotedInshopAppointment = firstInshopPMAppt;
} else if (
firstInshopPMDate &&
numberOfDaysToFirstInshopPMDate >= maxDayRangeToShowNoPmTimeslot
) {
// Selects the first available AM time slot. If none, selects the first available PM time slot
promotedInshopAppointment = firstInshopAMAppt
? firstInshopAMAppt
: firstInshopPMAppt;
}
}
if (promotedMobileAppointment && promotedInshopAppointment) {
this.$refs.multiLocationModal.setPromotedMobileAppointment(
promotedMobileAppointment
);
this.$refs.multiLocationModal.setPromotedInshopAppointment(
promotedInshopAppointment
);
this.$refs.multiLocationModal.openModal();
this.$refs.multiLocationModal.captureExperimentSettings(
experimentSettingsString
);
}
}
}
}
},
async getFirstAvailableApptByTOD(timeOfDay, appointmentType) {
let selectableDays;
if (appointmentType?.toLowerCase() == "mobile") {
selectableDays = this.selectableDatesMobile?.days;
} else if (appointmentType?.toLowerCase() == "inshop") {
selectableDays = this.selectableDatesInshop?.days;
} else {
return;
}
if (!selectableDays?.length) {
return null;
} else {
for (const dateObj of this.selectableDatesMobile.days) {
for (const dateObj of selectableDays) {
let matchingTimeSlot = {
estimatedServiceMinutes: {
minimum: this.estimatedServiceMinutesMinimum,
@ -2074,6 +2230,7 @@ export default {
},
timeSlot: null,
date: null,
addressCopy: null,
};
const isMatchingApptDay = dateObj.timeSlots.some(
(slot) =>
@ -2081,6 +2238,16 @@ export default {
(matchingTimeSlot.timeSlot = slot) &&
(matchingTimeSlot.date = dateObj.date)
);
if (
appointmentType?.toLowerCase() == "inshop" &&
this.selectedShopAnswer?.address1 &&
this.selectedShopAnswer?.address2
) {
matchingTimeSlot.addressCopy =
this.selectedShopAnswer?.address1 +
", " +
this.selectedShopAnswer?.address2;
}
if (isMatchingApptDay && matchingTimeSlot) {
return matchingTimeSlot;
}
@ -2088,7 +2255,7 @@ export default {
return null;
}
},
updateTimeSlotandNavigateForward(timeSlotObj) {
updateMobileFirstTimeSlotandNavigateForward(timeSlotObj) {
this.selectedMobileFirstAppointment = true;
this.appointmentType = AppointmentTypeStrings.MOBILE;
this.selectedDate = timeSlotObj.date;
@ -2096,6 +2263,18 @@ export default {
this.updateTimeSlot(timeSlotObj);
this.forwardButtonAction();
},
updateMultiLocationTimeSlotandNavigateForward({ selectedTimeSlot, appointmentType }) {
if (appointmentType?.toLowerCase() == "mobile") {
this.appointmentType = AppointmentTypeStrings.MOBILE;
} else if (appointmentType?.toLowerCase() == "inshop") {
this.appointmentType = AppointmentTypeStrings.IN_SHOP;
}
this.selectedMultiLocationAppointment = true;
this.selectedDate = selectedTimeSlot.date;
this.updateSelectedProvider();
this.updateTimeSlot(selectedTimeSlot);
this.forwardButtonAction();
},
updateRecalAcknowledgedAndNavigateForward(isAcknowledged) {
this.isRecalAcknowledgedForScheduling = isAcknowledged;
this.forwardButtonAction();
@ -2119,6 +2298,7 @@ export default {
textBlock,
timeSlotQuestion,
mobileFirstModal,
multiLocationModal,
recalAckModal,
alert,
serviceZipModalQuestion,

View file

@ -209,6 +209,15 @@ export default {
userAgent.includes("webOS")
);
},
isAppleBrowser() {
const userAgent = navigator.userAgent;
return (
userAgent.includes("iPod") ||
userAgent.includes("iPad") ||
userAgent.includes("iPhone") ||
userAgent.includes("Mac")
);
},
getSubmittedOrder() {
return JSON.parse(
window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE)

View file

@ -3,6 +3,7 @@ import { getBoolFromString } from "@/helpers/boolean-helper";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { routeData } from "@/router/constants/routes";
import { isVerifiedInsurance } from "@/router/methods/helpers/requires-verified-redirect";
import { hasVehicleInfo } from "@/helpers/vehicle-helper";
import store from "@/store";
import { queryStrings } from "@/constants/query-strings";
import baseMixin from "@/mixins/base-mixin";
@ -33,7 +34,7 @@ export async function vehicleBeforeEnter(to, from) {
}
}
if (isVerifiedInsurance()) {
if (isVerifiedInsurance() && hasVehicleInfo()) {
return {
name: routeData.VEHICLE_DAMAGE.name,
replace: true,

View file

@ -3115,6 +3115,8 @@ export const actions = {
serviceLocationState,
serviceLocationZipCode,
pricedLineItems,
serviceLocationAddressLine1,
serviceLocationAddressLine2,
},
pageNameToLog,
}
@ -3148,6 +3150,8 @@ export const actions = {
appointmentType: appointmentType,
pricedLineItems: lineItemsWithOnlyPriceInfo,
serviceLocation: {
addressLine1: appointmentType == "Mobile" ? serviceLocationAddressLine1 : null,
addressLine2: appointmentType == "Mobile" ? serviceLocationAddressLine2 : null,
city: appointmentType == "Mobile" ? serviceLocationCity : null,
state: appointmentType == "Mobile" ? serviceLocationState : null,
zipCode: appointmentType == "Mobile" ? serviceLocationZipCode : null,

View file

@ -20,3 +20,9 @@
}
// Usage: @include responsive-font-size-md(mobile-font-size-in-rem, desktop-font-size-in-rem);
// Example: @include responsive-font-size-md(0.875rem, 1rem);
// Filter to use on SVG elements that are background
@mixin blue-filter {
filter: brightness(0) saturate(100%) invert(31%) sepia(68%) saturate(1676%) hue-rotate(186deg)
brightness(93%) contrast(110%);
}

View file

@ -127,6 +127,12 @@ $body-color: $gray-600;
$font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif;
$font-family-monospace:
UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
$font-family-sans-serif-semibold: UrbanistSemibold, Arial, Helvetica, sans-serif;
$font-family-sans-serif-bold: UrbanistBold, Arial, Helvetica, sans-serif;
$font-family-sans-serif-extrabold: UrbanistExtraBold, Arial, Helvetica, sans-serif;
$font-family-sans-serif-medium-italic: UrbanistMedium_Italic, Arial, Helvetica, sans-serif;
$font-family-sans-serif-bold-italic: UrbanistBold_Italic, Arial, Helvetica, sans-serif;
// stylelint-enable value-keyword-case
$font-family-base: $font-family-sans-serif;
$font-family-code: $font-family-monospace;

View file

@ -7,15 +7,9 @@
isDismissible ? 'alert-dismissible' : '',
this.alertClass,
this.cssClassNameForCmsWidget,
this.hasBorder ? 'bordered' : '',
]">
<p class="mx-6 my-0 fw-bold alert-heading">
<img v-if="showInfoIcon" :src="infoIcon" alt="Info Icon" class="info-icon" />
<img
v-if="showWarningIcon"
:src="warningIcon"
alt="Warning Icon"
class="warning-icon" />
{{ alertHeadline }}
</p>
<hr v-if="showHorizontalRow" />
@ -69,9 +63,7 @@ export default {
name: "alert",
props: {
showInfoIcon: Boolean,
showWarningIcon: Boolean,
showHorizontalRow: Boolean,
hasBorder: Boolean,
isDismissible: Boolean,
alertClass: String,
/*
@ -104,9 +96,6 @@ export default {
infoIcon() {
return require(`@/assets/img/icons/info-circle-blue.svg`);
},
warningIcon() {
return require(`@/assets/img/icons/alert-circle-yellow.svg`);
},
pageQueryString() {
return applicationConfig.PAGE_QUERYSTRING;
},
@ -222,19 +211,12 @@ export default {
background-color: $yellow-100;
.alert-heading {
color: $yellow-600;
.warning-icon {
height: 1rem;
}
}
svg {
fill: $yellow-600;
width: 1rem;
height: 1rem;
}
hr {
border-color: $yellow-800;
}
}
&.alert-success {
background-color: $green-100;
@ -247,9 +229,6 @@ export default {
height: 1rem;
}
}
&.bordered {
border: 1px solid;
}
& p {
font-size: 0.875rem;
}