Merge pull request #3 from Safelite/feature/Digital/SSR-11

Initial set up.
This commit is contained in:
Kulbhushan Kaushik 2022-09-16 08:21:25 -07:00 committed by GitHub
commit ad5ff8e0a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 578 additions and 13474 deletions

View file

@ -1,3 +1,23 @@
module.exports = {
preset: '@vue/cli-plugin-unit-jest/presets/no-babel'
}
verbose: true,
coverageReporters: ["html", "text", "jest-junit", "cobertura"],
testResultsProcessor: "jest-junit",
preset: "@vue/cli-plugin-unit-jest",
transform: { "^.+\\.vue$": "vue-jest" },
moduleFileExtensions: ["js", "vue"],
modulePathIgnorePatterns: ["vin-lookup"],
collectCoverageFrom: [
"src/**/*.{js,vue}",
"!src/main.js",
"!src/constants/*.js",
"!src/router/**/*.js",
"!src/helpers/unit-test-helper.js"
// END
], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: {
// global: {
// statements: 80,
// },
},
};

13391
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,17 +5,21 @@
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit"
"test:unit": "vue-cli-service test:unit --coverage --ci",
"test:unit:lite": "vue-cli-service test:unit --ci"
},
"dependencies": {
"@popperjs/core": "^2.10.2",
"axios": "^0.27.2",
"bootstrap": "^5.1.1",
"pinia": "^2.0.22",
"pinia-plugin-persistedstate": "^2.2.0",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
"vue-router": "^4.0.3"
},
"devDependencies": {
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-unit-jest": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/test-utils": "^2.0.0-0",
"@vue/vue3-jest": "^27.0.0-alpha.1",

View file

@ -1,30 +1,13 @@
<template>
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view/>
<router-view v-slot="{ Component }">
<transition :duration="{ enter: 200, leave: 200 }" name="route-fade" mode="out-in">
<!-- The above durations should be kept in sync with the global css class "fade-on-route-transition" -->
<component :is="Component" />
</transition>
</router-view>
</template>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>
@import "./node_modules/bootstrap/scss/bootstrap";
@import "@/styles/common-animations.scss";
</style>

View file

@ -1,59 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest" target="_blank" rel="noopener">unit-jest</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View file

@ -0,0 +1,7 @@
const applicationConfig = {
CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT, // "Localhost", "Dev", "QA", and "Prod"
APPLICATION_NAME: "SelfService",
SITE_ENTRY_TRIGGER_VALUE: "SelfService"
};
export { applicationConfig };

View file

@ -0,0 +1,7 @@
const dynamicStrings = {
GLOBAL_STATE: "globalState",
CUSTOM: "custom",
ROUTER_LINK: "routerLink:"
};
export { dynamicStrings };

View file

@ -0,0 +1,77 @@
const endpoints = {
GetRouteInfo: {
url: "/content/api/v1/content/RouteInfo",
method: "POST",
},
GetHomepageInfo: {
url: "/content/api/v1/content/HomepageInfo",
method: "GET",
},
GetVehicleYears: {
url: "/vehicle/api/v1/vehicle/years",
method: "GET",
},
GetVehicleMakes: {
url: "/vehicle/api/v1/vehicle/Makes",
method: "GET",
},
GetVehicleModels: {
url: "/vehicle/api/v1/vehicle/Models",
method: "GET",
},
GetVehicleStyles: {
url: "/vehicle/api/v1/vehicle/Styles",
method: "GET",
},
GetVehicle: {
url: "/vehicle/api/v1/vehicle/lookup",
method: "GET",
},
GetDamageOptions: {
url: "/parts/api/v1/parts/damage-options",
method: "GET",
},
GetPageData: {
url: "/content/api/v1/content",
method: "GET",
},
LookupVehicleByYmms: {
url: "/vehicle/api/v1/vehicle/Lookup",
method: "GET",
},
LookupVehicleByVin: {
url: "/vehicle/api/v1/vehicle/Lookup",
method: "POST",
},
LookupVinByPlate: {
url: "/vehicle/api/v1/vehicle/lookup-vin-by-plate",
method: "POST",
},
LookupVinByAddress: {
url: "/vehicle/api/v1/vehicle/lookup-vin-by-address",
method: "POST",
},
GetPartsOrQuestions: {
url: "/parts/api/v1/parts/parts-or-questions",
method: "POST",
},
GetParts: {
url: "/parts/api/v1/parts/parts",
method: "POST",
},
GetCapabilityQuestions: {
url: "/parts/api/v1/parts/capability-questions",
method: "GET"
},
GetPartFromCapabilityAnswer: {
url: "/parts/api/v1/parts/part-from-capability-answer",
method: "POST"
},
ValidateZip: {
url: "/location/api/v1/location/zip",
method: "GET",
}
};
export { endpoints };

View file

@ -0,0 +1,29 @@
const errorMessages = {
DAMAGE_LOCATION_REQUIRED: "Please select damage location",
DAMAGE_SIDE_REQUIRED: "Please select vehicle side",
DRIVER_SIDE_OPTIONS_REQUIRED: "Please select window",
PASSENGER_SIDE_OPTIONS_REQUIRED: "Please select window",
WINDSHIELD_DAMAGE_TYPE_REQUIRED: "Please select windshield damage",
WINDSHIELD_CHIP_COUNT_REQUIRED: "Please select chip(s)",
WINSHIELD_REPLACE_OPTIONS_REQUIRED: "Please select windshield part",
REPLACE_OPTIONS_REQUIRED: "Please select rear window type",
STREET_ADDRESS_REQUIRED: "Please enter your street address",
CITY_REQUIRED: "Please enter your city",
STATE_REQUIRED: "Please enter your state",
ZIP_REQUIRED: "Please enter your ZIP",
ZIP_FORMAT: "Please enter a valid ZIP",
LICENSE_PLATE_REQUIRED: "Please enter your license plate number",
REGISTRATION_ZIP_REQUIRED: "Please enter your registration ZIP code",
FIRST_NAME_REQUIRED: "Please enter your first name",
LAST_NAME_REQUIRED: "Please enter your last name",
EMAIL_ADDRESS_REQUIRED: "Please enter your email address",
EMAIL_ADDRESS_FORMAT: "Please enter a valid email address",
SERVICE_ZIP_REQUIRED: "Please enter your service ZIP",
SERVICE_ZIP_FORMAT: "Please enter a valid service ZIP",
VIN_REQUIRED: "Please enter your VIN",
VIN_FORMAT: "Invalid VIN. Please make sure that you entered the correct 17-digit, alpha-numeric number. VINs do not contain the letters I, O, or Q",
OPTION_REQUIRED: "Please select an option",
VEHICLE_REQUIRED: "Please select a vehicle",
};
export { errorMessages };

View file

@ -0,0 +1,5 @@
const queryStrings = {
ISS_PAGE: 'issPage'
};
export { queryStrings };

View file

@ -0,0 +1,51 @@
const storeActions = {
// Content Actions
GET_ROUTE_INFO_ACTION: "getRouteInfo",
GET_HOMEPAGE_NAME: "getHomepageName",
GET_PAGE_DATA: "getPageData",
// Vehicle Actions
GET_VEHICLE_YEARS: "getVehicleYears",
GET_VEHICLE_MAKES: "getVehicleMakes",
GET_VEHICLE_MODELS: "getVehicleModels",
GET_VEHICLE_STYLES: "getVehicleStyles",
SET_VEHICLE: "setVehicle",
GET_DAMAGE_OPTIONS: "getDamageOptions",
GET_EVOX_IMAGE: "getEvoxImage",
// Lookup Actions
LOOKUP_VEHICLE_BY_YMMS: "lookupVehicleByYmms",
LOOKUP_VEHICLE_BY_VIN: "lookupVehicleByVin",
LOOKUP_VIN_BY_PLATE: "lookupVinByPlate",
LOOKUP_VIN_BY_ADDRESS: "lookupVinByAddress",
GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions",
GET_PARTS: "getParts",
GET_CAPABILITY_QUESTIONS: "getCapabilityQuestions",
GET_PART_FROM_CAPABILITY_QUESTION_ANSWER: "getPartFromCapabilityQuestionAnswer",
GET_MOLDING_QUESTIONS: "getMoldingQuestions",
VALIDATE_ZIP: "validateZip",
CLEAR_VIN: "clearVin",
// VEHICLE MUTATIONS
UPDATE_YEAR: "updateYear",
UPDATE_MAKE: "updateMake",
UPDATE_MODEL: "updateModel",
UPDATE_STYLE: "updateStyle",
UPDATE_CAR_ID: "updateCarId",
UPDATE_VEHICLE_CATEGORY: "updateVehicleCategory",
UPDATE_VEHICLE_IMAGE_URL: "updateVehicleImageUrl",
UPDATE_VEHICLE_IMAGE_VIF_NUMBER: "updateVehicleImageVifNumber",
UPDATE_VEHICLE_IMAGE_COLOR: "updateVehicleImageColor",
UPDATE_VEHICLE_VIN: "updateVehicleVin",
UPDATE_VEHICLE: "updateVehicle",
// DEPENDENCY MUTATIONS
// SAVE COMPONENT STATE
};
export { storeActions };

View file

@ -0,0 +1,11 @@
const vehicleCategories = {
CAR: "CAR",
TRUCK: "TRUCK",
VAN: "VAN",
COMMERCIALVAN: "COMMERCIAL VAN",
SUV: "SUV",
MOTORHOME: "MOTOR HOME",
SEMI: "SEMI",
};
export { vehicleCategories };

21
src/global-methods.js Normal file
View file

@ -0,0 +1,21 @@
import axios from "axios";
import { applicationConfig } from "@/constants/application-config.js";
export default {
callHttpClient({ method, endpoint, payload}) {
return new Promise((resolve, reject) => {
const cfDistroUrl = applicationConfig.CONSUMER_CF_DISTRO;
const payloadAndAnalyticsData = Object.assign({}, payload, { AppName: "SelfService" });
axios({ method: method, url: cfDistroUrl + endpoint, data: payloadAndAnalyticsData, crossDomain: true, responseType: {}})
.then((response) => {
return resolve(response);
},
error => {
console.error(error);
return reject(error.response);
}
);
});
}
};

View file

@ -1,6 +1,12 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import "../node_modules/bootstrap/dist/js/bootstrap.js";
const vueApp = createApp(App);
vueApp.use(router);
vueApp.mount("#app");
createApp(App).use(store).use(router).mount('#app')

50
src/mixins/base-mixin.js Normal file
View file

@ -0,0 +1,50 @@
import { storeActions } from "@/constants/store-actions.js";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { vehicleCategories } from "@/constants/vehicle-categories.js";
import { queryStrings } from "@/constants/query-strings";
import { dynamicStrings } from "@/constants/dynamic-strings";
export default {
data() {
return {
cmsContentByWidget: {}
};
},
methods: {
setCmsContent(cmsContent) {
this.$root.cmsContentByWidget = cmsContent;
},
getCmsContent(widgetName, fieldName) {
return this.$root.cmsContentByWidget?.[widgetName]?.[fieldName] ? this.$root.cmsContentByWidget[widgetName][fieldName] : '';
},
},
computed: {
storeActions() {
return storeActions;
},
navigationScenarios() {
return navigationScenarios;
},
vehicleCategories() {
return vehicleCategories;
},
queryStrings(){
return queryStrings;
},
dynamicStrings(){
return dynamicStrings;
},
cssClassNameForCmsWidget(){
return "widget-name-" + this.cmsWidgetName;
},
},
};
function encodeUriData(payload) {
if (payload && Object.keys(payload).length > 0) {
// Loop through the payload and encode the values
Object.keys(payload).forEach((key) => {
payload[key] = encodeURIComponent(payload[key]);
});
}
}

View file

@ -0,0 +1,3 @@
export function lazyLoadComponent(componentName) {
return () => import(`@/layouts/${componentName}/${componentName}.vue`); // /src/layouts/folder/component.vue
}

View file

@ -1,27 +1,103 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import { storeActions } from "@/constants/store-actions";
import { createWebHistory, createRouter } from "vue-router";
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: function () {
return import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
}
]
{
path: '/',
name: 'root',
async beforeEnter(to, from, next) {
try {
if (router.hasRoute(to.query.issPage)) {
return next({ name: to.query.issPage, query: to.query, params: to.params });
}
const routeData = await GetRouteInfoFromPageName(to.query.issPage);
// Add our dynamic route.
router.addRoute({
path: routeData[0].path, // Always the same path, because we control it with query strings.
name: routeData[0].name,
component: routeData[0].component,
});
// Assign current query string parameters, as well as our fmgPage one.
next({
name: routeData[0].name,
query: Object.assign(to.query, { issPage: routeData[0].name }),
params: to.params
});
} catch (error) {
console.log(error);
await GoToStartOn404(next);
}
}
}];
const router = createRouter({
history: createWebHistory("/"),
routes
})
history: createWebHistory("/"),
routes,
});
export default router
// Get route information by page name.
// This will reach out to the Cms and there is a 1:1 relationship between page names and route names.
async function GetRouteInfoFromPageName(pageName) {
const response = await store.dispatch(storeActions.GET_ROUTE_INFO_ACTION, {
pageName: pageName,
});
const jsonFromResponse = JSON.parse(response.data.Result);
let routeData = [];
// Add our route data and return our array.
Object.keys(jsonFromResponse).forEach((key) => {
routeData.push({
path: "/",
name: `${key}`,
component: lazyLoadComponent(jsonFromResponse[key].LayoutName),
});
});
return routeData;
}
// Navigate to the next route, depending on the scenario.
async function navigate(scenario, currentRoute, optionalQuery = {}, optionalParams = {}) {
if (!scenario) {
console.error("No scenario provided. Please review the routing table.");
return;
}
// Match our maps up and navigate if we have a destination.
const matchingScenarioMap = getNavigationMap(scenario, currentRoute);
const destinationIssPageValue = matchingScenarioMap.destinationIssPageValue;
if (destinationIssPageValue !== undefined) {
// We're always pushing the same path, just changing query strings. Make sure our optional query strings get combined with our issPage one.
router.push({
name: "root",
query: Object.assign(optionalQuery, {
fmgPage: destinationIssPageValue,
}),
params: optionalParams
});
} else if (matchingScenarioMap.destinationUrl !== undefined) {
navigateToUrl(matchingScenarioMap.destinationUrl, optionalQuery);
}
}
// Get navigation map depending on the scenario and the current 'page' you're on.
function getNavigationMap(scenario, currentRoute) {
const issPageValue = currentRoute.query.issPage;
const matchedQueryValue = routingTable(store)
.filter(
(item) =>
item.issPageValue === issPageValue &&
item.maps.filter((map) => map.scenario === scenario).length > 0
)
.map((m) => m.maps.filter((map) => map.scenario === scenario))[0]
.filter(x => x.filter === true || x.filter === undefined);
return matchedQueryValue[0];
}
export default router;

View file

@ -0,0 +1,5 @@
const issPageValues = {
VEHICLE_YEAR: "vehicle-year",
};
export { issPageValues };

View file

@ -0,0 +1,10 @@
const navigationScenarios = {
// General
CLICKED_BACK: "CLICKED_BACK",
CLICKED_FORWARD: "CLICKED_FORWARD",
// YMMS
SELECTED_YEAR: "SELECTED_YEAR",
};
export { navigationScenarios };

View file

@ -0,0 +1,19 @@
import { issPageValues } from "@/router/router-constants/issPage-values";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
// Get store from router/index.js instead of importing it here to get updated values
const routingTable = function(store) {
return [
{
issPageValue: issPageValues.VEHICLE_YEAR,
maps: [
{
scenario: navigationScenarios.SELECTED_YEAR,
issPageValue: "PageOne"
},
],
},
];
};
export { routingTable };

View file

@ -1,14 +0,0 @@
import { createStore } from 'vuex'
export default createStore({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})

View file

@ -0,0 +1,30 @@
.fade-enter-active {
transition: opacity 0.8s ease;
}
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.route-fade-enter-active .fade-on-route-transition {
transition: opacity 0.2s ease-out; //This duration should be kept in sync with the app.vue <transition> element attribute
}
.route-fade-leave-active .fade-on-route-transition {
transition: opacity 0.2s ease-in; //This duration should be kept in sync with the app.vue <transition> element attribute
}
.route-fade-enter-from .fade-on-route-transition,
.route-fade-leave-to .fade-on-route-transition {
/*
Hack around a Chrome 96 bug in handling nested opacity transitions.
This is not needed in other browsers or Chrome 99+ where the bug
has been fixed.
*/
opacity: 0.001;
}

View file

@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page ... with new changes!</h1>
</div>
</template>

View file

@ -1,18 +0,0 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'HomeView',
components: {
HelloWorld
}
}
</script>

View file

@ -1,5 +1,22 @@
process.env.VUE_APP_CONSUMER_CF_DISTRO =
"https://consumerapidev.safelite.com";
process.env.VUE_APP_CURRENT_ENVIRONMENT = "Localhost";
module.exports = {
publicPath: "/",
css: {
loaderOptions: {
sass: {
// Load Order Matters!!!
// Note: only include functions, variables and mixins here
additionalData: `
@import "./node_modules/bootstrap/scss/functions";
@import "./node_modules/bootstrap/scss/variables";
@import "./node_modules/bootstrap/scss/mixins";
`,
},
},
},
configureWebpack: {
devtool: 'source-map'
},

View file

@ -1,5 +1,21 @@
process.env.VUE_APP_CONSUMER_CF_DISTRO = "__VUE_APP_CONSUMER_CF_DISTRO__";
process.env.VUE_APP_CURRENT_ENVIRONMENT = "__VUE_APP_CURRENT_ENVIRONMENT__";
module.exports = {
publicPath: "/",
css: {
loaderOptions: {
sass: {
// Load Order Matters!!!
// Note: only include functions, variables and mixins here
additionalData: `
@import "./node_modules/bootstrap/scss/functions";
@import "./node_modules/bootstrap/scss/variables";
@import "./node_modules/bootstrap/scss/mixins";
`,
},
},
},
configureWebpack: {
devtool: 'source-map'
},