CSR-918 Fix merge conflicts

This commit is contained in:
Katie 2022-11-03 14:54:04 -04:00
commit d44fa3fee1
28 changed files with 852 additions and 384 deletions

View file

@ -28,7 +28,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: {
global: {
statements: 83,
statements: 82,
// Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90
},
},

View file

@ -228,7 +228,7 @@ describe("baseInputButton.vue", () => {
expect(wrapper.emitted()).not.toHaveProperty("update:modelValue");
});
test("focus and click enter on a checkbox => update:modelValue is emitted with correct value", async () => {
test("focus and click enter on a checkbox => nothing should happen", async () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
@ -243,9 +243,10 @@ describe("baseInputButton.vue", () => {
// Act
await input.trigger("keypress", { key: "enter" });
console.log(wrapper.emitted()["update:modelValue"]);
// Assert
expect(wrapper.emitted()["update:modelValue"][0][0]).toEqual(["Hi"]);
expect(wrapper.emitted()["update:modelValue"]).toBe(undefined);
});
});
@ -319,7 +320,7 @@ describe("baseInputButton.vue", () => {
expect(wrapper.vm.handlePushClickEventToGACheck).not.toHaveBeenCalled();
});
test("eventType === eventTypes.ENTER => call handleClick and handlePushClickEventToGACheck", () => {
test("eventType === eventTypes.ENTER => do nothing", () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
@ -336,11 +337,9 @@ describe("baseInputButton.vue", () => {
wrapper.vm.handleEventAction("enter", { myEvent: "test" });
// Assert
expect(wrapper.vm.handleClick).toHaveBeenCalledWith({
myEvent: "test",
});
expect(wrapper.vm.handlePushClickEventToGACheck).toHaveBeenCalledWith("click");
expect(wrapper.vm.handleClick).not.toHaveBeenCalled();
expect(wrapper.vm.handleSelectionChange).not.toHaveBeenCalled();
expect(wrapper.vm.handlePushClickEventToGACheck).not.toHaveBeenCalled();
});
test("eventType === eventTypes.CHANGE => call handleClick and handlePushClickEventToGACheck", () => {

View file

@ -53,7 +53,6 @@ export default {
handleEventAction(eventType, e) {
if (this.isMultiSelect) {
switch (eventType) {
case this.eventTypes.ENTER:
case this.eventTypes.CHANGE:
this.handleClick(e);
this.handlePushClickEventToGACheck(this.eventTypes.CLICK);

View file

@ -31,7 +31,7 @@ describe("buttonQuestion.vue", () => {
// Act
const wrapper = shallowMount(buttonQuestion, {
propsData: {
buttonType: "listCard",
buttonTypeString: "listCard",
groupName: "group-name",
},
});
@ -46,7 +46,7 @@ describe("buttonQuestion.vue", () => {
// Act
const wrapper = shallowMount(buttonQuestion, {
propsData: {
buttonType: "listButtonHorizontal",
buttonTypeString: "listButtonHorizontal",
groupName: "group-name",
},
});
@ -61,7 +61,7 @@ describe("buttonQuestion.vue", () => {
// Act
const wrapper = shallowMount(buttonQuestion, {
propsData: {
buttonType: "radio",
buttonTypeString: "radio",
groupName: "group-name",
},
});

View file

@ -33,9 +33,12 @@
v-for="answer in buttonsInfo"
:key="answer.value ? answer.value : answer">
<component
:is="buttonType"
:is="buttonTypeString"
:buttonLabel="answer.buttonLabel"
:buttonLabelSubCopy="answer.buttonLabelSubCopy"
:buttonBodyCopy="answer.buttonBodyCopy"
:buttonAuxillaryCopy="answer.buttonAuxillaryCopy"
:buttonFooterCopy="answer.buttonFooterCopy"
:buttonImage="answer.buttonImage"
:buttonImageId="answer.buttonImageId"
:groupName="answer.groupName"
@ -45,6 +48,7 @@
:isWide="isWide"
:validationRules="validationRules"
:textPosition="textPosition"
:additionalButtonStyling="additionalButtonStyling"
:lastValuePushedToGa="lastValuePushedToGa"
:setLastValuePushedToGa="setLastValuePushedToGa"
v-model="selectedValues" />
@ -78,10 +82,14 @@ import { ErrorMessage } from "vee-validate";
export default {
name: "buttonQuestion",
props: {
buttonType: {
buttonTypeString: {
type: String,
default: "listButton",
},
buttonTypeObject: {
type: Object,
default: null,
},
isMultiSelect: Boolean,
groupName: String,
questionText: String,
@ -109,6 +117,12 @@ export default {
suppressError: Boolean,
useTextForValue: Boolean,
valueToLogType: String,
additionalButtonStyling: String,
},
beforeMount() {
if (this.buttonTypeObject) {
this.$options.components[this.buttonTypeString] = this.buttonTypeObject;
}
},
data() {
return {
@ -119,7 +133,7 @@ export default {
getFieldSetClasses() {
if (this.isOverflowScrollable) {
return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0";
} else if (this.buttonType == "listCard") {
} else if (this.buttonTypeString == "listCard") {
return "w-100";
} else {
return "";
@ -127,7 +141,7 @@ export default {
},
getComponentLoopWrapperClasses() {
let classes;
switch (this.buttonType) {
switch (this.buttonTypeString) {
case "listButton":
classes = "w-100";
break;
@ -143,6 +157,9 @@ export default {
case "radio":
classes = "ui-radio d-flex";
break;
case "servicePackageRadio":
classes = "package-main";
break;
}
return classes;
},
@ -151,8 +168,10 @@ export default {
classes += this.isWide ? "col-12" : "col";
if (this.buttonType == "radio") {
if (this.buttonTypeString == "radio") {
classes += " radio-button-container";
} else if (this.buttonTypeString == "servicePackageRadio") {
classes = "package-wrapper";
}
return classes;
@ -162,6 +181,9 @@ export default {
buttonLabel: answer.buttonLabel ?? answer.Text ?? answer,
altText: answer.altText ?? (answer.Name ? answer.Name : answer),
buttonLabelSubCopy: answer.buttonLabelSubCopy ?? answer.SubText,
buttonBodyCopy: answer.buttonBodyCopy ?? answer.buttonBodyCopy,
buttonAuxillaryCopy: answer.buttonAuxillaryCopy ?? answer.buttonAuxillaryCopy,
buttonFooterCopy: answer.buttonFooterCopy ?? answer.buttonFooterCopy,
buttonImage: answer.buttonImage ?? answer.AnswerImageUrl,
buttonImageId: answer.buttonImageId ?? answer.ImageId,
groupName: this.formatString(this.groupName),

View file

@ -2,6 +2,7 @@ const dynamicStrings = {
GLOBAL_STATE: "globalState",
CUSTOM: "custom",
ROUTER_LINK: "routerLink:",
TEXT_LINK: "textLink:",
};
export { dynamicStrings };

View file

@ -0,0 +1,8 @@
const partTypeStrings = {
FRONT_WIPER: "FRONT WIPER",
REAR_WIPER: "REAR WIPER",
RAIN_DEFENSE: "RAIN DEFENSE",
RECALIBRATION: "RECALIBRATION",
};
export { partTypeStrings };

View file

@ -31,7 +31,7 @@ export function fetchCmsContentForPage(fmgPage) {
// Function to convert a string, into a matching global state item.
function mapStringToState(str) {
// Pull all matches out of the string.
const regexExp = new RegExp("{(.*?):(.*?)}", "g");
const regexExp = new RegExp("{([^{}]*?):([^{}]*?)}", "g");
const regexMatches = [...str.matchAll(regexExp)];
const globalStateMatches = regexMatches.filter((match) => {
return match[1] === dynamicStrings.GLOBAL_STATE;
@ -39,20 +39,13 @@ function mapStringToState(str) {
// Our final string value that will be built from the matches.
let stringBuilder = "";
for (const match of globalStateMatches) {
// Reset store state for each match.
let storeState = store.state;
for (const s of match[2].split(".")) {
if (storeState[s] != undefined) {
storeState = storeState[s];
} else {
return ""; // if we can't map our string to state data, return an empty string.
}
const valueFromStore = getStoreValueFromString(match[2]);
if (!valueFromStore) {
return ""; // if we can't map our string to state data, return an empty string.
}
const stringWithReplacement = str.replace(match[0], storeState);
const stringWithReplacement = str.replace(match[0], valueFromStore);
// If we still have values we need to substitute, call this function again.
if (stringWithReplacement.includes(dynamicStrings.GLOBAL_STATE)) {
@ -62,7 +55,6 @@ function mapStringToState(str) {
// Concatenate the string.
stringBuilder = `${stringBuilder} ${stringWithReplacement}`;
}
return stringBuilder.trimStart();
}
@ -88,6 +80,11 @@ function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
function processWidgetItemForReplacement(widgetModel, key) {
// If we have a string, and it needs to be replaced.
if (typeof widgetModel[key] === "string") {
widgetModel[key] = processIfStatements(
widgetModel[key],
dynamicStrings.GLOBAL_STATE,
getStoreValueFromString
);
if (widgetModel[key].includes(dynamicStrings.GLOBAL_STATE)) {
widgetModel[key] = mapStringToState(widgetModel[key]);
}
@ -107,23 +104,217 @@ function processWidgetItemForReplacement(widgetModel, key) {
return widgetModel[key];
}
function getStoreValueFromString(str) {
let storeOrStateObject = str.includes("getters") ? store : store.state;
for (const s of str.split(".")) {
if (storeOrStateObject[s] != undefined) {
storeOrStateObject = storeOrStateObject[s];
} else {
return ""; // if we can't map our string to state data, return an empty string.
}
}
return storeOrStateObject;
}
///////////////////////////////////
// If Statement Processing Logic //
///////////////////////////////////
/**
* Recursive function - replaces all instances of if statements from the CMS that utilize the specified ifConditionKeyword
* @param {*} str string - Input string to be processed
* @param {*} ifConditionKeyword string - Defines which if statements to process ex: 'globalState'
* @param {*} replacePlaceholderCallback function - Callback to replace CMS placeholder values
* @returns The processed string
*/
export function processIfStatements(str, ifConditionKeyword, replacePlaceholderCallback) {
const containsRelevantIfStatement = new RegExp("{if:" + ifConditionKeyword + ":.+?}", "g").test(
str
);
if (!containsRelevantIfStatement) {
return str;
} else {
const ifStatementRegexExpression = getIfStatementRegexExpression();
const ifStatementRegexMatches = [...str.matchAll(ifStatementRegexExpression)];
const completeIfStatementArray = getAndFlagFirstNonNestedIfStatementWithKeyword(
ifStatementRegexMatches,
ifConditionKeyword
);
executeIfStatementAndSetProcessedStrings(
completeIfStatementArray,
replacePlaceholderCallback
);
const reconstructedPostProcessedString = joinProcessedRegexArray(ifStatementRegexMatches);
return processIfStatements(
reconstructedPostProcessedString,
ifConditionKeyword,
replacePlaceholderCallback
);
}
}
function getAndFlagFirstNonNestedIfStatementWithKeyword(matches, ifConditionKeyword) {
let index = 0;
for (const match of matches) {
if (match.groups.isIfStatement && match.groups.ifConditionType === ifConditionKeyword) {
let interiorIndex = 0;
let nestedLevel = 0;
let elseStatementIndex = null;
for (const interiorMatch of matches.slice(index + 1)) {
if (interiorMatch.groups.isIfStatement) {
if (interiorMatch.groups.ifConditionType === ifConditionKeyword) {
break;
} else {
nestedLevel++;
}
} else if (interiorMatch.groups.isElseStatement) {
if (!nestedLevel) {
elseStatementIndex = interiorIndex + 1;
}
} else if (interiorMatch.groups.isEndStatement) {
if (nestedLevel) {
nestedLevel--;
} else {
const ifStatementArray = matches.slice(index, index + interiorIndex + 2);
flagMatchesForProcessing(ifStatementArray, elseStatementIndex);
return ifStatementArray;
}
}
interiorIndex++;
}
}
index++;
}
}
function flagMatchesForProcessing(matches, elseStatementIndex) {
matches[0].isFlaggedForProcessing = true;
matches[matches.length - 1].isFlaggedForProcessing = true;
if (elseStatementIndex) {
matches[elseStatementIndex].isFlaggedForProcessing = true;
}
}
function joinProcessedRegexArray(regexMatches) {
let processedString = "";
regexMatches.forEach((match) => {
const rawString = match[0];
processedString += match.groups.processedString ?? rawString;
});
return processedString;
}
function executeIfStatementAndSetProcessedStrings(ifStatementArray, replacePlaceholderCallback) {
const ifCondition = replacePlaceholderCallback(ifStatementArray[0].groups.ifCondition);
let isInsideDesiredBlock = ifCondition;
ifStatementArray.forEach((entry) => {
if (entry.groups.isElseStatement && entry.isFlaggedForProcessing) {
isInsideDesiredBlock = !ifCondition;
} else if (entry.groups.isEndStatement && entry.isFlaggedForProcessing) {
isInsideDesiredBlock = true;
}
setProcessedStringOnEntry(entry, isInsideDesiredBlock);
});
}
function setProcessedStringOnEntry(entry, isInsideDesiredBlock) {
if (!isInsideDesiredBlock) {
entry.groups.processedString = "";
} else {
if (entry.groups.isIfStatement) {
entry.groups.processedString = entry.isFlaggedForProcessing
? entry.groups.ifTrailingString
: entry[0];
} else if (entry.groups.isElseStatement) {
entry.groups.processedString = entry.isFlaggedForProcessing
? entry.groups.elseTrailingString
: entry[0];
} else {
entry.groups.processedString = entry.isFlaggedForProcessing
? entry.groups.endTrailingString
: entry[0];
}
}
}
function getIfStatementRegexExpression() {
// Matches but does not capture:
// {if:...} or {else} or {end}
const anyLogicOperatorNonCapture = "(?:{(?:end|else|if:.*?)})";
// NOTE: ?<variableName> syntax stores the captured match like so: match.groups.variableName
const matchStartOfString =
"(?<processedString>^.+?)" + // Match and Capture all characters (lazy), cannot be empty
"(?=(?:{if))"; // Looks ahead but does not capture {if
const matchIfOperator =
"(?<isIfStatement>{if:)" + // Match & Capture {if:
"(?<ifConditionType>.*?):" + // Match all chars up to and including next ":" - Capture all chars up to ":"
"(?<ifCondition>.*?)}" + // Match all chars up to and including next "}" - Capture all chars up to "}"
"(?<ifTrailingString>.*?)" + // Match and Capture all characters (lazy), can be empty
"(?=" +
anyLogicOperatorNonCapture +
")"; // Looks ahead but does not capture the next logic operator
const matchElseOperator =
"(?<isElseStatement>{else})" + // Match & Capture {else}
"(?<elseTrailingString>.*?)" + // Match & Capture all characters (lazy), can be empty
"(?=" +
anyLogicOperatorNonCapture +
")"; // Looks ahead but does not capture the next logic operator
const matchEndOperator =
"(?<isEndStatement>{end})" + // Match & Capture {end}
"(?<endTrailingString>.*?)" + // Match & Capture all chracters (lazy), can be empty
"(?=" +
anyLogicOperatorNonCapture +
"|$)"; // Looks ahead but does not capture the next logic operator
// Combine all matching patterns, separated by "or" pipes
return new RegExp(
matchStartOfString +
"|" +
matchIfOperator +
"|" +
matchElseOperator +
"|" +
matchEndOperator,
"g"
);
}
//////////////////////////////////////////
// End of If Statement Processing Logic //
//////////////////////////////////////////
export function doesCopyContainRouterLink(copy) {
return copy.includes(this.dynamicStrings.ROUTER_LINK);
}
export function doesCopyContainTextLink(copy) {
return copy.includes(dynamicStrings.TEXT_LINK);
}
/**
* splits copy on { ... } such as {routerlink: ...}
* @returns array of strings
*/
export function splitCopyOnCMSPlaceHolder(copy) {
// splits copy on { ... } such as {routerlink: ...}
return copy.split(/{(.*?)}/g);
}
export function getRouterLinkRouteFromCopy(copy) {
/**
* Returns string2 of input following this pattern: {string1:string2,string3}
* @returns string
*/
export function getLinkTargetFromCopy(copy) {
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'estimate'
return copy.split(":")[1].split(",")[0];
}
export function getRouterLinkDisplayTextFromCopy(copy) {
/**
* Returns string3 of input following this pattern: {string1:string2,string3}
* @returns string
*/
export function getLinkDisplayTextFromCopy(copy) {
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'provide your VIN'

View file

@ -1,7 +1,7 @@
<template>
<buttonQuestion
class="address-vehicles-question"
buttonType="listButton"
buttonTypeString="listButton"
groupName="ChooseAddressVehicle"
:questionText="questionText"
:answers="vehicles"

View file

@ -26,10 +26,10 @@
<span v-if="doesCopyContainRouterLink(copy)" class="text-body">
<router-link
:to="{
query: { fmgPage: `${getRouterLinkRouteFromCopy(copy)}` },
query: { fmgPage: `${getLinkTargetFromCopy(copy)}` },
name: 'root',
}"
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
>{{ getLinkDisplayTextFromCopy(copy) }}</router-link
>
</span>
<span v-else class="m-0 text-body" v-html="copy"></span>
@ -68,8 +68,8 @@ import { isGlassAvailableForCarId } from "@/helpers/damage-helper";
import {
doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getLinkTargetFromCopy,
getLinkDisplayTextFromCopy,
} from "@/helpers/cms-content-helper";
import { routerParams } from "@/router/router-constants/router-params";
import vinPagesMixin from "@/mixins/vin-pages-mixin";
@ -153,8 +153,8 @@ export default {
methods: {
doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getLinkTargetFromCopy,
getLinkDisplayTextFromCopy,
arePagePrerequisitesValid() {
if (
store.getters.order.vehicle.carId &&

View file

@ -15,7 +15,7 @@
cmsWidgetName="VinLookupMethod"
:answers="answersFromCms"
groupName="vinLookupMethodOption"
buttonType="listButton"
buttonTypeString="listButton"
v-model="selectedVinLookupMethod"
isRequired
validationRules="option-required" />
@ -174,6 +174,7 @@ export default {
async forwardButtonAction() {
if (this.isRepair) {
const zipCodeData = await this.getZipCodeData(this.serviceZipCode);
//todo: validation
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false);
await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_LOCATION,

View file

@ -3,9 +3,9 @@
<buttonQuestion
:answers="answersFromCms"
:groupName="groupName"
buttonType="listButtonHorizontal"
buttonTypeString="listButtonHorizontal"
v-model="selectedValues"
isCashOrInsurance
additionalButtonStyling="listButtonHorizontalStrong"
isRequired />
</div>
</template>

View file

@ -17,72 +17,23 @@
cmsWidgetName="CashOrInsuranceQuestionWidget"
v-model="isInsuranceSelected"
groupName="CashOrInsuranceQuestion" />
<radioServicePackage />
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="left"
typeStyle="caption"
class="mt-4" />
<h3 style="m-0">h1</h3>
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="left"
typeStyle="h1"
class="mt-4" />
<h3 style="m-0">h2</h3>
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="left"
typeStyle="h2"
class="mt-4" />
<h3 style="m-0">h3</h3>
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="left"
typeStyle="h3"
class="mt-4" />
<h3 style="m-0">h4</h3>
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="left"
typeStyle="h4"
class="mt-4" />
<h3 style="m-0">h5</h3>
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="left"
typeStyle="h5"
class="mt-4" />
<h3 style="m-0">h6</h3>
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="left"
typeStyle="h6"
class="mt-4" />
<h3 style="m-0">Body</h3>
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="left"
typeStyle="body"
class="mt-4" />
<h3 style="m-0">Label</h3>
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="left"
typeStyle="label"
class="mt-4" />
<h3 style="m-0">Small</h3>
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="left"
typeStyle="small"
class="mt-4" />
<h3 style="m-0">Caption</h3>
<servicePackageQuestion
ref="servicePackage"
cashCmsWidgetName="CashServicePackageQuestionWidget"
insuranceCmsWidgetName="InsuranceServicePackageQuestionWidget"
v-model="selectedPackage"
groupName="ServicePackageQuestion"
:availableLineItems="availableLineItems"
:isInsuranceSelected="isInsuranceSelected" />
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="left"
typeStyle="caption"
class="mt-4" />
<modal cmsWidgetName="EstimateRainDefenseModal" />
<modal cmsWidgetName="EstimateFrontWiperModal" />
<modal cmsWidgetName="EstimateRearWiperModal" />
<modal cmsWidgetName="EstimateRecalModal" />
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
@ -100,10 +51,11 @@ import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import radioServicePackage from "@/ux-components/radio-service-package/radio-service-package";
import cashOrInsuranceQuestion from "./cash-or-insurance-question/cash-or-insurance-question";
import servicePackageQuestion from "./service-package-question/service-package-question";
import textBlock from "@/common-components/text-block/text-block";
import store from "@/store";
import modal from "@/common-components/modal/modal";
import baseMixin from "@/mixins/base-mixin.js";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { Form } from "vee-validate";
@ -116,9 +68,9 @@ export default {
// TODOS - modify as needed, just a rough sketch to place initializations
// get supporting lineitems
// get supporting availableLineItems
// get wiper and rain defense lineitems
// get wiper and rain defense availableLineItems
// Settle promises and get results
@ -129,13 +81,63 @@ export default {
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(cmsContent);
vm.isInsuranceSelected = vm.getDefaultisInsuranceSelectedValue();
vm.availableLineItems = [
{
partNumber: "001",
Description: "Windshield with Recal",
partType: "Windshield",
Quantity: "1",
BasePartNumber: "001",
Color: "Green",
CanSafeliteRecalibrate: true,
price: 350.22,
},
{
partNumber: "SBB16",
description: "SAFELITE BEAM BLADE 16",
partType: "FRONT WIPER",
price: 32.64,
},
{
partNumber: "A DISPOSAL FEE",
partType: "DISPOSAL FEE",
price: 15.0,
},
{
partNumber: "SBB26",
description: "SAFELITE BEAM BLADE 26",
partType: "FRONT WIPER",
price: 53.04,
},
{
partNumber: "SBBR12A",
description: "SAFELITE REAR BLADE 12A",
partType: "REAR WIPER",
price: 24.48,
},
{
partNumber: "RAIN DEFENSE",
description: null,
partType: "RAIN DEFENSE",
price: 35.5,
},
{
partNumber: "RECAL STATIC",
Description: "Recalibration",
partType: "recalibration",
Quantity: "1",
price: 150.0,
},
];
vm.isInsuranceSelected = vm.getDefaultIsInsuranceSelectedValue();
});
},
data() {
return {
pricingRequestResult: null,
isInsurance: null,
isInsuranceSelected: null,
selectedPackage: null,
availableLineItems: null,
};
},
methods: {
@ -143,38 +145,27 @@ export default {
return true;
//return store.getters.order.damage.isRepair || (store.getters.order.lineItems?.glassParts != null && store.getters.order.lineItems.glassParts.length > 0);
},
getDefaultisInsuranceSelectedValue() {
var defaultisInsuranceSelectedValue = this.$store.getters.order.payment.isInsurance;
if (defaultisInsuranceSelectedValue != null) {
return defaultisInsuranceSelectedValue;
getDefaultIsInsuranceSelectedValue() {
const defaultIsInsuranceSelectedValue = this.$store.getters.order.payment.isInsurance;
if (defaultIsInsuranceSelectedValue != null) {
return defaultIsInsuranceSelectedValue;
} else {
return this.economyPackagePrice > 600;
return this.availableLineItems
? baseMixin.methods.getTierOnePackagePrice(this.availableLineItems) > 500
: null;
}
},
},
computed: {
economyPackagePrice() {
//TODO build out the pricing logic CSR-504
return 0;
},
selectedChipCountValues: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
},
components: {
funnelHeader,
funnelFooter,
vehicleBanner,
funnelSubHeader,
Form,
radioServicePackage,
textBlock,
cashOrInsuranceQuestion,
servicePackageQuestion,
modal,
},
};
</script>

View file

@ -0,0 +1,244 @@
<template>
<buttonQuestion
:answers="servicePackageAnswers"
:groupName="groupName"
buttonTypeString="servicePackageRadio"
:buttonTypeObject="servicePackageRadio"
v-model="selectedValues"
isRequired />
</template>
<script>
import buttonQuestion from "@/common-components/button-question/button-question";
import baseMixin from "@/mixins/base-mixin.js";
import { processIfStatements } from "@/helpers/cms-content-helper";
import { damageLocationsSelected as glassLocations } from "@/constants/damage-locations-selected";
import servicePackageRadio from "./service-package-radio/service-package-radio";
import { partTypeStrings } from "@/constants/part-type-strings";
const packageNames = {
TIER_ONE: "TierOne",
TIER_TWO: "TierTwo",
TIER_THREE: "TierThree",
};
export default {
name: "servicePackageQuestion",
props: {
modelValue: String,
groupName: String,
cashCmsWidgetName: String,
insuranceCmsWidgetName: String,
isInsuranceSelected: Boolean,
availableLineItems: null,
},
data() {
return {
servicePackageRadio: servicePackageRadio,
};
},
computed: {
selectedValues: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
nullSafeAvailableLineItems() {
return this.availableLineItems ?? [];
},
servicePackageAnswers() {
const cmsWidgetName = this.isInsuranceSelected
? this.insuranceCmsWidgetName
: this.cashCmsWidgetName;
let cmsAnswersContent = this.getCmsContent(cmsWidgetName, "Answers");
if (!cmsAnswersContent) {
return null;
}
if (!this.shouldDisplayTierTwoPackage) {
cmsAnswersContent = cmsAnswersContent.filter(
(answer) => answer.Name != packageNames.TIER_TWO
);
}
const modifiedAnswers = cmsAnswersContent.map((answer) => ({
value: answer.Name,
buttonLabel: this.getHeaderTextFromCms(answer.SubWidgetName),
buttonLabelSubCopy: this.getSubheaderTextFromCms(answer.SubWidgetName),
buttonBodyCopy: this.getBodyTextFromCms(answer.SubWidgetName),
buttonAuxillaryCopy: this.getPackagePriceString(answer.Name),
buttonFooterCopy: this.getFooterTextFromCms(answer.SubWidgetName),
}));
return modifiedAnswers;
},
isRecalibrationOnOrder() {
return this.lineItemsContainsPartType(partTypeStrings.RECALIBRATION);
},
frontWipersApplicableForTierTwo() {
const frontWipersAreAvailable = this.lineItemsContainsPartType(
partTypeStrings.FRONT_WIPER
);
const isRepair = this.$store.getters.order.damage.isRepair;
const glassToReplaceContainsWindshield = this.glassToReplaceContainsGlassLocation(
glassLocations.WINDSHIELD
);
if (frontWipersAreAvailable) {
if (isRepair) {
return true;
} else {
if (glassToReplaceContainsWindshield) {
return true;
} else {
return false;
}
}
} else {
return false;
}
},
rearWiperApplicableForTierTwo() {
const rearWiperIsAvailable = this.lineItemsContainsPartType(partTypeStrings.REAR_WIPER);
return (
this.glassToReplaceContainsGlassLocation(glassLocations.REAR) &&
rearWiperIsAvailable
);
},
frontWipersApplicableForTierThree() {
const frontWipersAreAvailable = this.lineItemsContainsPartType(
partTypeStrings.FRONT_WIPER
);
return frontWipersAreAvailable;
},
rearWiperApplicableForTierThree() {
const rearWiperIsAvailable = this.lineItemsContainsPartType(partTypeStrings.REAR_WIPER);
return (
this.glassToReplaceContainsGlassLocation(glassLocations.REAR) &&
rearWiperIsAvailable
);
},
rainDefenseApplicableForTierThree() {
const frontWipersAreAvailable = this.lineItemsContainsPartType(
partTypeStrings.FRONT_WIPER
);
const glassToReplaceContainsWindshield = this.glassToReplaceContainsGlassLocation(
glassLocations.WINDSHIELD
);
const glassToReplaceContainsRearGlass = this.glassToReplaceContainsGlassLocation(
glassLocations.REAR
);
if (this.frontWipersApplicableForTierTwo) {
return true;
} else if (!frontWipersAreAvailable) {
return true;
} else if (!glassToReplaceContainsWindshield && !glassToReplaceContainsRearGlass) {
return true;
} else {
return false;
}
},
shouldDisplayTierTwoPackage() {
return this.frontWipersApplicableForTierTwo || this.rearWiperApplicableForTierTwo;
},
},
methods: {
processIfStatements,
getHeaderTextFromCms(cmsWidgetName) {
return this.getCmsContent(cmsWidgetName, "HeaderText");
},
getSubheaderTextFromCms(cmsWidgetName) {
return this.getCmsContent(cmsWidgetName, "SubheaderText");
},
getBodyTextFromCms(cmsWidgetName) {
const bodyText = this.getCmsContent(cmsWidgetName, "BodyText");
return this.processIfStatements(bodyText, "custom", this.getCustomValueFromString);
},
getFooterTextFromCms(cmsWidgetName) {
const footerText = this.getCmsContent(cmsWidgetName, "FooterText");
return this.processIfStatements(footerText, "custom", this.getCustomValueFromString);
},
getPackagePriceString(packageName) {
const formattedPriceFloat = parseFloat(this.getPackagePrice(packageName)).toFixed(2);
return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat;
},
getPackagePrice(packageName) {
let priceFloat = this.isInsuranceSelected
? 0
: baseMixin.methods.getTierOnePackagePrice(this.nullSafeAvailableLineItems);
if (packageName === packageNames.TIER_TWO) {
priceFloat += this.getTierTwoPackageVapsPrice();
} else if (packageName === packageNames.TIER_THREE) {
priceFloat += this.getTierThreePackageVapsPrice();
}
return priceFloat;
},
getTierTwoPackageVapsPrice() {
let vapsPrice = 0;
const priceFrontWipers = this.frontWipersApplicableForTierTwo;
const priceRearWipers = this.rearWiperApplicableForTierTwo;
this.nullSafeAvailableLineItems.forEach((item) => {
if (
(priceFrontWipers &&
item.partType.toUpperCase() === partTypeStrings.FRONT_WIPER) ||
(priceRearWipers && item.partType.toUpperCase() === partTypeStrings.REAR_WIPER)
) {
vapsPrice += item.price;
}
});
return vapsPrice;
},
getTierThreePackageVapsPrice() {
let vapsPrice = 0;
const priceFrontWipers = this.frontWipersApplicableForTierThree;
const priceRearWipers = this.rearWiperApplicableForTierThree;
const priceRainDefense = this.rainDefenseApplicableForTierThree;
this.nullSafeAvailableLineItems.forEach((item) => {
if (
(priceFrontWipers &&
item.partType.toUpperCase() === partTypeStrings.FRONT_WIPER) ||
(priceRearWipers &&
item.partType.toUpperCase() === partTypeStrings.REAR_WIPER) ||
(priceRainDefense &&
item.partType.toUpperCase() === partTypeStrings.RAIN_DEFENSE)
) {
vapsPrice += item.price;
}
});
return vapsPrice;
},
getCustomValueFromString(str) {
switch (str) {
case "isRecalibrationOnOrder":
return this.isRecalibrationOnOrder;
case "frontWipersApplicableForTierTwo":
return this.frontWipersApplicableForTierTwo;
case "rearWiperApplicableForTierTwo":
return this.rearWiperApplicableForTierTwo;
case "frontWipersApplicableForTierThree":
return this.frontWipersApplicableForTierThree;
case "rearWiperApplicableForTierThree":
return this.rearWiperApplicableForTierThree;
case "rainDefenseApplicableForTierThree":
return this.rainDefenseApplicableForTierThree;
default:
return null;
}
},
lineItemsContainsPartType(partType) {
const partTypeMatches = this.nullSafeAvailableLineItems.filter(
(lineItem) => lineItem.partType.toUpperCase() === partType
);
return !!partTypeMatches.length;
},
glassToReplaceContainsGlassLocation(glassLocation) {
const glassLocationMatches = this.$store.getters.order.damage.glassToReplace.filter(
(glassToReplace) => glassToReplace.glassLocation === glassLocation
);
return !!glassLocationMatches.length;
},
},
components: {
buttonQuestion,
},
};
</script>

View file

@ -0,0 +1,217 @@
<template>
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange">
<div class="package-label" for="testradio">
<div class="package-specs">
<p :class="[this.buttonLabelSubCopy ? 'mb-2' : 'm-0']">
<span v-html="this.buttonLabel"></span>
<span class="pricing-info" v-html="this.buttonAuxillaryCopy"></span>
</p>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
v-html="this.buttonLabelSubCopy"></p>
<!-- Parse the body text containing <ul> with logic -->
<ul>
<li v-for="listItem in this.arrayOfListItemsFromBodyText" :key="listItem">
<!-- no textLink -->
<span v-if="!doesCopyContainTextLink(listItem)" v-html="listItem"></span>
<!-- with textLink -->
<template
v-else
v-for="copy in splitCopyOnCMSPlaceHolder(listItem)"
:key="copy">
<span v-if="!doesCopyContainTextLink(copy)" v-html="copy"></span>
<span v-else>
<textLink
linkType="text"
:text="getLinkDisplayTextFromCopy(copy)"
href="#!"
data-bs-toggle="modal"
:data-bs-target="'#' + getLinkTargetFromCopy(copy)"
aria-label="Modal window" />
</span>
</template>
</li>
</ul>
<!-- End of body text parsing -->
<div
class="package-footer"
v-if="this.buttonFooterCopy"
v-html="this.buttonFooterCopy"></div>
</div>
</div>
</baseInputButton>
</template>
<script>
import baseInputButton from "@/common-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 {
getLinkDisplayTextFromCopy,
getLinkTargetFromCopy,
splitCopyOnCMSPlaceHolder,
doesCopyContainTextLink,
} from "@/helpers/cms-content-helper";
export default {
name: "servicePackageRadio",
mixins: [inputButtonWrapperMixin],
components: {
baseInputButton,
textLink,
},
computed: {
arrayOfListItemsFromBodyText() {
return this.getArrayOfListItemsFromRawCmsCopy(this.buttonBodyCopy);
},
},
methods: {
getLinkDisplayTextFromCopy,
getLinkTargetFromCopy,
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">
.package-main {
.package-wrapper {
margin: 0.5rem 0;
label {
display: block;
}
input[type="radio"] {
opacity: 0;
position: absolute;
left: -9999px;
+ .package-label {
display: flex;
align-items: flex-start;
position: relative;
cursor: pointer;
width: 100%;
padding: 1rem;
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: 58px;
&:before {
content: "";
position: relative;
top: 5px;
margin-right: 1rem;
border-radius: 50%;
border: 1px solid $gray-500;
width: 16px;
height: 16px;
min-width: 16px;
}
&:after {
content: "";
position: absolute;
left: 19px;
top: 24px;
border-radius: 50%;
width: 10px;
height: 10px;
min-width: 10px;
}
}
&:hover {
+ .package-label {
&:before {
border: 1px solid #8e9292;
box-shadow: 0px 0px 0px 4px #9fcee6, 0px 1px 4px rgba(0, 0, 0, 0.2);
}
}
}
&:checked {
+ .package-label {
.package-specs {
max-height: 1000px;
}
}
+ .package-label {
background-color: $blue-100;
border: 1px solid $blue;
max-height: 500px;
}
+ .package-label {
&:before {
box-shadow: 0px 0px 0px 1px $blue;
}
}
+ .package-label {
&:after {
background: $blue;
}
}
}
&:focus {
+ .package-label {
&:before {
border: 2px solid $blue;
}
}
}
}
.package-footer {
font-size: 12px;
font-weight: 500;
color: #d4281c;
margin-left: -32px;
}
.package-specs {
display: flex;
flex-direction: column;
width: 100%;
max-height: 0;
transition: all 0.75s ease;
p {
font-weight: 500;
display: flex;
justify-content: space-between;
span.pricing-info {
color: $green;
}
&.sub-label {
color: $green;
text-transform: uppercase;
}
}
ul {
margin: 1rem 0 0 -15px;
padding: 0;
li {
margin-bottom: 0.5rem;
}
}
}
}
}
</style>

View file

@ -5,7 +5,7 @@
isMultiSelect
:answers="answersToDisplay"
:groupName="groupName"
buttonType="listCard"
buttonTypeString="listCard"
isRequired
v-model="selectedValues"
validationRules="damage-location-required" />

View file

@ -11,7 +11,7 @@
:isMultiSelect="isMultiSelect"
:answers="answersToDisplay"
:groupName="groupName"
buttonType="listCard"
buttonTypeString="listCard"
v-model="selectedValues"
:validationRules="validationRules"
:suppressError="suppressError"

View file

@ -10,7 +10,7 @@
isMultiSelect
:answers="answersToDisplay"
:groupName="groupName"
buttonType="listCard"
buttonTypeString="listCard"
v-model="selectedDoorSidesValues"
validationRules="damage-side-required"
isRequired />

View file

@ -5,7 +5,7 @@
:questionText="questionText"
:answers="answersFromCms"
:groupName="groupName"
buttonType="listButtonHorizontal"
buttonTypeString="listButtonHorizontal"
useTextForValue
v-model="selectedValue"
:validationRules="validationRules"

View file

@ -5,7 +5,7 @@
:questionText="questionText"
:answers="answersFromCms"
:groupName="groupName"
buttonType="listCard"
buttonTypeString="listCard"
v-model="selectedValues"
:suppressError="suppressError"
:validationRules="validationRules"

View file

@ -8,7 +8,7 @@
<buttonQuestion
v-model="selectedTint"
:answers="tintSelectionOptions"
buttonType="listCard"
buttonTypeString="listCard"
:isWide="true"
altText=""
isRequired
@ -18,7 +18,7 @@
<div class="col">
<buttonQuestion
v-model="selectedPartNumber"
buttonType="radio"
buttonTypeString="radio"
class="radioQuestion"
:questionText="glassFeatureQuestion"
:answers="featureListData[selectedTint]"

View file

@ -6,6 +6,7 @@ import { vehicleCategories } from "@/constants/vehicle-categories.js";
import { routerParams } from "@/router/router-constants/router-params";
import { queryStrings } from "@/constants/query-strings";
import { dynamicStrings } from "@/constants/dynamic-strings";
import { partTypeStrings } from "../constants/part-type-strings";
export default {
data() {
@ -61,6 +62,20 @@ export default {
state: serviceZipValidationResponse.data.state,
};
},
getTierOnePackagePrice(lineItems) {
let totalPrice = 0;
lineItems.forEach((lineItem) => {
const partType = lineItem.partType.toUpperCase();
if (
partType != partTypeStrings.FRONT_WIPER &&
partType != partTypeStrings.REAR_WIPER &&
partType != partTypeStrings.RAIN_DEFENSE
) {
totalPrice += lineItem.price;
}
});
return totalPrice;
},
},
computed: {
storeActions() {

View file

@ -9,6 +9,9 @@ export default {
...inputButtonProps,
buttonLabel: [Number, String],
buttonLabelSubCopy: String,
buttonBodyCopy: String,
buttonAuxillaryCopy: String,
buttonFooterCopy: String,
buttonImage: String,
buttonImageId: String,
altText: {
@ -18,6 +21,7 @@ export default {
textPosition: String,
screenReaderOnlyText: String,
isWide: Boolean,
additionalButtonStyling: String,
},
computed: {
selectedValue: {

View file

@ -392,6 +392,12 @@ export const getters = {
},
eventBus: (state) => state.applicationUser.eventBus,
damage: (state) => state.order.damage,
hasAnyNonWindshieldGlassParts: (state) => {
const nonWindshieldItems = state.order.damage.glassToReplace.filter(
(glassToReplace) => glassToReplace.glassLocation != "Windshield"
);
return !!nonWindshieldItems.length;
},
lineItems: (state) => state.order.lineItems,
pageData: (state) => (page) => {
return state.applicationUser.pageData[page];

View file

@ -19,10 +19,10 @@
<span v-else>
<router-link
:to="{
query: { fmgPage: `${getRouterLinkRouteFromCopy(copy)}` },
query: { fmgPage: `${getLinkTargetFromCopy(copy)}` },
name: 'root',
}"
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
>{{ getLinkDisplayTextFromCopy(copy) }}</router-link
>
</span>
</template>
@ -41,8 +41,8 @@
import {
doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getLinkTargetFromCopy,
getLinkDisplayTextFromCopy,
splitCMSCopyOnParagraphTag,
} from "@/helpers/cms-content-helper";
@ -92,8 +92,8 @@ export default {
methods: {
doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getLinkTargetFromCopy,
getLinkDisplayTextFromCopy,
ensureAlertIsInViewPort() {
if (this.shouldScrollToOnMount && this.$el.style.display != "none") {
var footerHeight = this.getFooterInfoBoxHeight();

View file

@ -52,12 +52,12 @@ describe("list-button-horizontal.vue", () => {
expect(input.attributes()["aria-required"]).toEqual("true");
});
it("is cash or insurance button => has 'radio-fancy' class", () => {
it("is cash or insurance button => has 'strong' class", () => {
// Arrange/Act
const { wrapper } = setupMocks({
mockData: {
propsData: {
isCashOrInsurance: true,
additionalButtonStyling: "listButtonHorizontalStrong",
},
},
});
@ -65,7 +65,7 @@ describe("list-button-horizontal.vue", () => {
// Assert
const label = wrapper.find("label");
expect(label.classes()).toContain("radio-fancy");
expect(label.classes()).toContain("strong");
});
test("has buttonLabel => displays buttonLabel", () => {

View file

@ -3,7 +3,7 @@
v-bind="$props"
:buttonWrapperClasses="[
'list-group list-button-horizontal d-flex flex-column w-100 base-input-button',
{ 'radio-fancy': isCashOrInsurance },
{ strong: isStrongStyling },
]"
v-model="selectedValue">
<div
@ -28,8 +28,10 @@ import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
export default {
name: "listButtonHorizontal",
mixins: [inputButtonWrapperMixin],
props: {
isCashOrInsurance: Boolean,
computed: {
isStrongStyling() {
return this.additionalButtonStyling === "listButtonHorizontalStrong";
},
},
components: {
baseInputButton,
@ -92,7 +94,7 @@ export default {
}
// Cash/Insurance option radio button styling
&.radio-fancy {
&.strong {
.list-button-horizontal-content {
border: 1px solid $blue-700;
z-index: 2;
@ -182,7 +184,7 @@ export default {
//Cash/insurance styling
&:first-of-type {
.list-button-horizontal.radio-fancy {
.list-button-horizontal.strong {
input[type="radio"] {
&:checked + .list-button-horizontal-content {
border-bottom-right-radius: 0;
@ -198,7 +200,7 @@ export default {
}
&:last-of-type {
.list-button-horizontal.radio-fancy {
.list-button-horizontal.strong {
input[type="radio"] {
&:checked + .list-button-horizontal-content {
border-bottom-left-radius: 0;

View file

@ -1,232 +0,0 @@
<template>
<div class="package-main">
<div class="package-wrapper">
<input type="radio" name="test-radio" id="testradio" />
<label for="testradio">
<div class="package-specs">
<p class="m-0">Economy service <span>As little as $0.00</span></p>
<ul>
<li>New replacement windshield</li>
<li>Expert installation</li>
<li>Nationwide lifetime warranty</li>
</ul>
</div>
</label>
</div>
<div class="package-wrapper">
<input type="radio" name="test-radio" id="testradio-2" />
<label for="testradio-2">
<div class="package-specs">
<p class="mb-2">Standard service <span>As little as 59.97</span></p>
<p class="sub-label m-0">most popular</p>
<ul>
<li>New replacement windshield</li>
<li>
<textLink
linkType="text"
text="Rain Defense"
href="#!"
data-bs-toggle="modal"
data-bs-target="#EstimateRainDefenseModal"
aria-label="Modal window" />
treatment
</li>
<li>
New
<textLink
linkType="text"
text="front wiper blades"
href="#!"
data-bs-toggle="modal"
data-bs-target="#EstimateFrontWiperModal"
aria-label="Modal window" />
</li>
<li>
New
<textLink
linkType="text"
text="rear wiper blade"
href="#!"
data-bs-toggle="modal"
data-bs-target="#EstimateRearWiperModal"
aria-label="Modal window" />
</li>
<li>
Expert
<textLink
linkType="text"
text="recalibration"
href="#!"
data-bs-toggle="modal"
data-bs-target="#EstimateRecalModal"
aria-label="Modal window" />
</li>
<li>Nationwide lifetime warranty</li>
<li>New wiper blades</li>
</ul>
</div>
</label>
</div>
<div class="package-wrapper">
<input type="radio" name="test-radio" id="testradio-3" />
<label for="testradio-3">
<div class="package-specs">
<p class="mb-2">Premium service <span>As little as $94.97</span></p>
<ul>
<li>New replacement windshield</li>
<li>Expert installation</li>
<li>Nationwide lifetime warranty</li>
<li>New wiper blades</li>
<li>Rain Defense</li>
</ul>
</div>
</label>
</div>
<modal cmsWidgetName="EstimateRainDefenseModal" />
<modal cmsWidgetName="EstimateFrontWiperModal" />
<modal cmsWidgetName="EstimateRearWiperModal" />
<modal cmsWidgetName="EstimateRecalModal" />
</div>
</template>
<script>
import textLink from "@/ux-components/text-link/text-link";
import modal from "@/common-components/modal/modal";
export default {
name: "radioServicePackage",
data() {
return {
isActive: false,
hasArrow: true,
};
},
methods: {
toggleClass: function (event) {
this.isActive = !this.isActive;
},
},
components: {
textLink,
modal,
},
};
</script>
<style lang="scss">
.package-main {
.package-wrapper {
margin: 0.5rem 0;
input[type="radio"] {
opacity: 0;
position: absolute;
left: -9999px;
+ label {
display: flex;
align-items: flex-start;
position: relative;
cursor: pointer;
width: 100%;
padding: 1rem;
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: 58px;
&:before {
content: "";
position: relative;
top: 5px;
margin-right: 1rem;
border-radius: 50%;
border: 1px solid $gray-500;
width: 16px;
height: 16px;
min-width: 16px;
}
&:after {
content: "";
position: absolute;
left: 19px;
top: 24px;
border-radius: 50%;
width: 10px;
height: 10px;
min-width: 10px;
}
}
&:hover {
+ label {
&:before {
border: 1px solid #8e9292;
box-shadow: 0px 0px 0px 4px #9fcee6, 0px 1px 4px rgba(0, 0, 0, 0.2);
}
}
}
&:checked {
+ label {
.package-specs {
max-height: 1000px;
}
}
+ label {
background-color: $blue-100;
border: 1px solid $blue;
max-height: 500px;
}
+ label {
&:before {
box-shadow: 0px 0px 0px 1px $blue;
}
}
+ label {
&:after {
background: $blue;
}
}
}
&:focus {
+ label {
&:before {
border: 2px solid $blue;
}
}
}
}
.package-specs {
display: flex;
flex-direction: column;
width: 100%;
max-height: 0;
transition: all 0.75s ease;
p {
font-weight: 500;
display: flex;
justify-content: space-between;
span {
color: $green;
}
&.sub-label {
color: $green;
text-transform: uppercase;
}
}
ul {
margin: 1rem 0 0 -15px;
padding: 0;
li {
margin-bottom: 0.5rem;
}
}
}
}
}
</style>