Merge pull request #28 from Safelite/feature/dynamic-routing
Routing changes
This commit is contained in:
commit
7faa638b51
20 changed files with 1200 additions and 243 deletions
971
package-lock.json
generated
971
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -14,6 +14,7 @@
|
|||
"bootstrap": "^5.1.1",
|
||||
"canvas-confetti": "^1.4.0",
|
||||
"core-js": "^3.6.5",
|
||||
"http-status-codes": "^2.1.4",
|
||||
"vue": "^3.0.0",
|
||||
"vue-router": "^4.0.11",
|
||||
"vuex": "^4.0.2",
|
||||
|
|
|
|||
65
src/App.vue
65
src/App.vue
|
|
@ -1,69 +1,4 @@
|
|||
<template>
|
||||
<router-view></router-view>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p class="text-center text-secondary fs-5 mb-4">
|
||||
Select a year to get started
|
||||
</p>
|
||||
<p class="text-center fs-6 fw-bold">What year is your vehichle?</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-5">
|
||||
<radioCard
|
||||
radioLabel="Windshield"
|
||||
radioImage="windshield-damage.svg"
|
||||
altText="Windshield"
|
||||
groupName="damageKey"
|
||||
radioID="windshield"
|
||||
/>
|
||||
<radioCard
|
||||
radioLabel="Side Window"
|
||||
radioImage="side-window-damage.svg"
|
||||
altText="Side Window"
|
||||
groupName="damageKey"
|
||||
radioID="sidewindow"
|
||||
/>
|
||||
<radioCard
|
||||
radioLabel="Back Glass"
|
||||
radioImage="back-glass-damage.svg"
|
||||
altText="Back Glass"
|
||||
groupName="damageKey"
|
||||
radioID="backglass"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col my-3 d-flex align-items-center">
|
||||
<buttonPrimary
|
||||
buttonText="Primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col my-3 d-flex align-items-center">
|
||||
<listButton
|
||||
buttonText="List Button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col my-3 d-flex align-items-center">
|
||||
<a href="#">Text Link</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import buttonPrimary from "@/uxComponents/buttonPrimary/buttonPrimary";
|
||||
import radioCard from "@/uxComponents/radioCard/radioCard";
|
||||
import listButton from "@/uxComponents/listButton/listButton";
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
buttonPrimary,
|
||||
radioCard,
|
||||
listButton
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
<template>
|
||||
<form class="list-view d-flex">
|
||||
<form class="list-view d-flex">
|
||||
<div class="car_list">
|
||||
<div v-for="attritbute in attritbutes" :key="attritbute" class="mb-2">
|
||||
<buttonPrimary
|
||||
:buttonText="attritbute"
|
||||
v-on:click="selectAttribute(attritbute)"
|
||||
buttonType="btn-funnel"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="attritbute in attritbutes" :key="attritbute" class="mb-2">
|
||||
<buttonPrimary :buttonText="attritbute" v-on:click="selectAttribute(attritbute)" buttonType="btn-funnel" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import buttonPrimary from "@/uxComponents/buttonPrimary/buttonPrimary";
|
||||
|
||||
export default {
|
||||
name: "CarAttributes",
|
||||
props: {
|
||||
attritbutes: Array,
|
||||
selectAttribute: Function,
|
||||
},
|
||||
components: {
|
||||
buttonPrimary,
|
||||
},
|
||||
name: "CarAttributes",
|
||||
props: {
|
||||
attritbutes: Array,
|
||||
selectAttribute: Function,
|
||||
},
|
||||
components: {
|
||||
buttonPrimary,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
5
src/constants/applicationConfig.js
Normal file
5
src/constants/applicationConfig.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const applicationConfig = {
|
||||
|
||||
}
|
||||
|
||||
export { applicationConfig }
|
||||
8
src/constants/endpoints.js
Normal file
8
src/constants/endpoints.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
const endpoints = {
|
||||
GetRouteInfoEndpoint: {
|
||||
url: '/content/api/v1/content/RouteInfo',
|
||||
method: 'POST'
|
||||
}
|
||||
}
|
||||
|
||||
export { endpoints }
|
||||
5
src/constants/storeActions.js
Normal file
5
src/constants/storeActions.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const storeActions = {
|
||||
GET_ROUTE_INFO_ACTION: "getRouteInfo",
|
||||
}
|
||||
|
||||
export { storeActions }
|
||||
31
src/global-methods.js
Normal file
31
src/global-methods.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
callHttpClient({ method, endpoint, payload, responseType = {} }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const apiGatewayUrl = "https://consumerapidev.safelite.com"; // TODO: Some environment variable
|
||||
const payloadAndAnalyticsData = Object.assign({}, payload, {
|
||||
AppName: "FixMyGlass",
|
||||
});
|
||||
|
||||
axios({
|
||||
method: method,
|
||||
url: `${apiGatewayUrl}${endpoint}`,
|
||||
data: payloadAndAnalyticsData,
|
||||
crossDomain: true,
|
||||
responseType: responseType,
|
||||
}).then(
|
||||
(response) => {
|
||||
if (response.data.isSuccess || response.status == 200) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(response);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
return reject(error.response);
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
60
src/layouts/componentTest/componentTest.vue
Normal file
60
src/layouts/componentTest/componentTest.vue
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div class="container-fluid">
|
||||
<div class="row mt-5">
|
||||
<radioCard
|
||||
radioLabel="Windshield"
|
||||
radioImage="windshield-damage.svg"
|
||||
altText="Windshield"
|
||||
groupName="damageKey"
|
||||
radioID="windshield"
|
||||
/>
|
||||
<radioCard
|
||||
radioLabel="Side Window"
|
||||
radioImage="side-window-damage.svg"
|
||||
altText="Side Window"
|
||||
groupName="damageKey"
|
||||
radioID="sidewindow"
|
||||
/>
|
||||
<radioCard
|
||||
radioLabel="Back Glass"
|
||||
radioImage="back-glass-damage.svg"
|
||||
altText="Back Glass"
|
||||
groupName="damageKey"
|
||||
radioID="backglass"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col my-3 d-flex align-items-center">
|
||||
<buttonPrimary
|
||||
buttonText="Primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col my-3 d-flex align-items-center">
|
||||
<listButton
|
||||
buttonText="List Button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col my-3 d-flex align-items-center">
|
||||
<a href="#">Text Link</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import buttonPrimary from "@/uxComponents/buttonPrimary/buttonPrimary";
|
||||
import radioCard from "@/uxComponents/radioCard/radioCard";
|
||||
import listButton from "@/uxComponents/listButton/listButton";
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
buttonPrimary,
|
||||
radioCard,
|
||||
listButton
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1 +0,0 @@
|
|||
test.todo('some test to be written in the future');
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<template>
|
||||
<div class="home">
|
||||
<h1>Home</h1>
|
||||
<router-link to="/select-car">SelectCar</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1 +0,0 @@
|
|||
test.todo('some test to be written in the future');
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
<template>
|
||||
<div>
|
||||
<p>Not found</p>
|
||||
</div>
|
||||
</template>
|
||||
<p> Not Found .... :( </p>
|
||||
</template>
|
||||
|
|
@ -1,37 +1,35 @@
|
|||
<template>
|
||||
<div>
|
||||
<baseTemplate
|
||||
:currentCarInfo="year"
|
||||
neededCarInfo="What make is your vehicle?"
|
||||
:backFunction="goBack"
|
||||
>
|
||||
<carAttributes :attritbutes="makes" :selectAttribute="selectMake" />
|
||||
<div>
|
||||
<baseTemplate :currentCarInfo="year" neededCarInfo="What make is your vehicle?" :backFunction="goBack">
|
||||
<carAttributes :attritbutes="makes" :selectAttribute="selectMake" />
|
||||
</baseTemplate>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import baseTemplate from "@/commonComponents/baseTemplate/baseTemplate";
|
||||
import carAttributes from "@/commonComponents/carAttributes/carAttributes";
|
||||
import { mapState } from "vuex";
|
||||
import {
|
||||
mapState
|
||||
} from "vuex";
|
||||
|
||||
export default {
|
||||
name: "SelectCar",
|
||||
computed: {
|
||||
...mapState(["year", "makes"]),
|
||||
},
|
||||
methods: {
|
||||
selectMake(make) {
|
||||
this.$store.dispatch("selectMake", make);
|
||||
name: "SelectCar",
|
||||
computed: {
|
||||
...mapState(["year", "makes"]),
|
||||
},
|
||||
goBack() {
|
||||
this.$store.state.slideTransition = "slide_reverse";
|
||||
this.$router.push("/select-year");
|
||||
methods: {
|
||||
selectMake(make) {
|
||||
this.$store.dispatch("selectMake", make);
|
||||
},
|
||||
goBack() {
|
||||
this.$store.state.slideTransition = "slide_reverse";
|
||||
this.$router.push("/select-year");
|
||||
},
|
||||
},
|
||||
components: {
|
||||
baseTemplate,
|
||||
carAttributes,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
baseTemplate,
|
||||
carAttributes,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
16
src/main.js
16
src/main.js
|
|
@ -1,12 +1,20 @@
|
|||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import store from "@/store";
|
||||
|
||||
import router from './router';
|
||||
import store from '@/store';
|
||||
import baseMixin from "@/mixins/baseMixin.js";
|
||||
//Bootstrap JavaScript
|
||||
import "../node_modules/bootstrap/dist/js/bootstrap.js";
|
||||
|
||||
//Get Bootstrap and Custom Styles
|
||||
import "./assets/scss/custom.scss";
|
||||
|
||||
createApp(App).use(router).use(store).mount("#app");
|
||||
// Vue App Setup
|
||||
const vueApp = createApp(App);
|
||||
|
||||
vueApp.use(router);
|
||||
vueApp.use(store);
|
||||
|
||||
vueApp.mixin(baseMixin);
|
||||
|
||||
vueApp.mount("#app");
|
||||
|
|
|
|||
22
src/mixins/baseMixin.js
Normal file
22
src/mixins/baseMixin.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { storeActions } from "@/constants/storeActions.js";
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
dispatchBlockingStoreAction(type, payload) {
|
||||
// globalMethods.showWaitingModal(true);
|
||||
|
||||
return this.dispatchNonBlockingStoreAction(type, payload)
|
||||
.finally(() => {
|
||||
// globalMethods.showWaitingModal(false);
|
||||
});;
|
||||
},
|
||||
dispatchNonBlockingStoreAction(type, payload) {
|
||||
return this.$store.dispatch(type, payload);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
storeActions() {
|
||||
return storeActions;
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
export function lazyLoadComponent(componentName) {
|
||||
return () => import(`@/layouts/${componentName}.vue`);
|
||||
return () => import(`@/layouts/${componentName}/${componentName}.vue`); // /src/layouts/folder/component.vue
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
import axios from "axios";
|
||||
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
|
||||
|
||||
export function getRouteData(relativeUrl) {
|
||||
return axios
|
||||
.post("/api/v1/content/RouteInfo", {
|
||||
relativeUrl: relativeUrl,
|
||||
})
|
||||
.then((response) => {
|
||||
// This is really only for Mockey. In a real case, this would just be a 404 and we would check status code.
|
||||
if (response.data.Result === undefined) {
|
||||
return Promise.reject("Could not find any details about given route.");
|
||||
}
|
||||
|
||||
// Get our route data
|
||||
let json = JSON.parse(response.data.Result);
|
||||
let routeData = [];
|
||||
|
||||
// Push the item onto the array
|
||||
Object.keys(json).forEach((key) => {
|
||||
routeData.push({
|
||||
path: `${json[key].RelativeUrl}`,
|
||||
name: `${key}`,
|
||||
component: lazyLoadComponent(json[key].LayoutName),
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.resolve(routeData);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,18 +1,63 @@
|
|||
import { createWebHistory, createRouter } from "vue-router";
|
||||
import { getRouteData } from "@/router/dynamic-routing/route-compile.js";
|
||||
import { storeActions } from "@/constants/storeActions.js";
|
||||
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
|
||||
import ComponentTest from "@/layouts/componentTest/componentTest.vue";
|
||||
import NotFound from "@/layouts/notFound/notFound.vue";
|
||||
import Home from "@/layouts/home/home.vue";
|
||||
import store from "@/store";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/not-found",
|
||||
name: "NotFound",
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: NotFound,
|
||||
name: "NotFound"
|
||||
},
|
||||
{
|
||||
path: "/component-test", // This is a temporary route for testing.
|
||||
name: "ComponentTest",
|
||||
component: ComponentTest,
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
Name: "Home",
|
||||
component: Home,
|
||||
beforeEnter(to, from, next) {
|
||||
|
||||
// If we have no query string, or we don't have the FmgPage query string.
|
||||
if (Object.keys(to.query).length === 0 ||to.query.fmgPage === undefined) {
|
||||
RetainStructureAndGoTo404(to, next);
|
||||
} else {
|
||||
|
||||
// If we already have our route, go to it.
|
||||
if(router.hasRoute(to.query.fmgPage))
|
||||
{
|
||||
return next({ name: to.query.fmgPage });
|
||||
}
|
||||
|
||||
// Get route info for the given url. Names will have a 1:1 relationship with names in the Cms.
|
||||
GetRouteInfoFromPageName(to.query.fmgPage).then((routeData) => {
|
||||
|
||||
// Add our dynamic route.
|
||||
router.addRoute({
|
||||
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, { fmgPage: routeData[0].name }),
|
||||
});
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
// If we can't find the route, go to the 404 page.
|
||||
RetainStructureAndGoTo404(to, next);
|
||||
|
||||
console.log("error:");
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -21,32 +66,42 @@ const router = createRouter({
|
|||
routes,
|
||||
});
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
await GoToDynamicRoute(to, next);
|
||||
});
|
||||
// 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.
|
||||
function GetRouteInfoFromPageName(pageName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
store
|
||||
.dispatch(storeActions.GET_ROUTE_INFO_ACTION, { pageName: pageName })
|
||||
.then((response) => {
|
||||
// Add our route data and return our array.
|
||||
let jsonFromResponse = JSON.parse(response.data.Result);
|
||||
let routeData = [];
|
||||
|
||||
async function GoToDynamicRoute(to, next) {
|
||||
let doesRouteResolve = router.resolve(to.path).matched.length > 0;
|
||||
Object.keys(jsonFromResponse).forEach((key) => {
|
||||
routeData.push({
|
||||
path: `${jsonFromResponse[key].RelativeUrl}`,
|
||||
name: `${key}`,
|
||||
component: lazyLoadComponent(jsonFromResponse[key].LayoutName),
|
||||
});
|
||||
});
|
||||
|
||||
console.log("Does route resolve?" + " " + (doesRouteResolve ? "Yes" : "No"));
|
||||
resolve(routeData);
|
||||
|
||||
if (!doesRouteResolve) {
|
||||
let routeData = await getRouteData(to.path).catch((exp) => {
|
||||
console.log(exp);
|
||||
next({ name: "NotFound" }); // Go to 404.
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add our dynamic route.
|
||||
router.addRoute({
|
||||
path: routeData[0].path,
|
||||
name: routeData[0].name,
|
||||
component: routeData[0].component,
|
||||
});
|
||||
|
||||
next(to.fullPath);
|
||||
} else {
|
||||
next(); // Home (/) and NotFound (/not-found) will always resolve.
|
||||
}
|
||||
// Go to our 404 page but retain our structure when we go there (path, queryString, hash).
|
||||
function RetainStructureAndGoTo404(to, next){
|
||||
next({
|
||||
name: 'NotFound',
|
||||
params: { pathMatch: to.path.split('/').slice(1) },
|
||||
query: to.query,
|
||||
hash: to.hash
|
||||
});
|
||||
}
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { createStore } from "vuex";
|
||||
import { endpoints } from "@/constants/endpoints.js";
|
||||
import createPersistedState from "vuex-persistedstate";
|
||||
import axios from "axios";
|
||||
import globalMethods from "@/global-methods";
|
||||
import router from "@/router";
|
||||
|
||||
export default createStore({
|
||||
|
|
@ -25,7 +27,7 @@ export default createStore({
|
|||
mutations: {
|
||||
updateYear(state, data) {
|
||||
state.year = data.year;
|
||||
state.makes = data.makes.data.Result;
|
||||
state.makes = data.makes;
|
||||
state.slideTransition = "slide";
|
||||
},
|
||||
updateMake(state, data) {
|
||||
|
|
@ -54,9 +56,16 @@ export default createStore({
|
|||
},
|
||||
actions: {
|
||||
async selectYear(context, year) {
|
||||
const makes = await axios.get(
|
||||
`https://mockey.qa.sagaws.net/service/makes/year=${year}`
|
||||
);
|
||||
let makes = [];
|
||||
await globalMethods
|
||||
.callHttpClient({
|
||||
method: "GET",
|
||||
endpoint: `https://mockey.qa.sagaws.net/service/makes/year=${year}`,
|
||||
payload: {},
|
||||
})
|
||||
.then(function (response) {
|
||||
makes = response.data.Result;
|
||||
});
|
||||
context.commit("updateYear", { year, makes });
|
||||
router.push(`/select-make?year=${year}`);
|
||||
},
|
||||
|
|
@ -82,5 +91,14 @@ export default createStore({
|
|||
`/damage?year=${state.year}&make=${state.make}&model=${state.model}&style=${style}`
|
||||
);
|
||||
},
|
||||
getRouteInfo(context, { pageName }) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetRouteInfoEndpoint.method,
|
||||
endpoint: endpoints.GetRouteInfoEndpoint.url,
|
||||
payload: {
|
||||
pageName: pageName,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue