A comprehensive insurance comparison and information platform built with Nuxt 3, Vue 3, and Bootstrap Vue Next.
- Tech Stack
- Prerequisites
- Getting Started
- Project Structure
- Styling
- API Integration
- Data Flow
- Useful Composables
- Code Styling
- Development Commands
- Contributing
- Support
- Framework: Nuxt 3
- UI Library: Bootstrap Vue Next
- Styling: SCSS with Bootstrap integration
- Client-Side State Management: Pinia
- Images: Nuxt Image with optimization
- Content: GraphQL API integration via Hygraph CMS
- Node.js (v20 or higher recommended)
- npm, pnpm, yarn, or bun package manager
# Clone the repository
git clone https://github.com/TheDMSGroup/protect.com.git
# Install dependencies
npm install
# or
pnpm install
# or
yarn installStart the development server - defaults to http://localhost:3000:
npm run dev
# or
pnpm dev
# or
yarn devNuxt 3 uses a pages router by default, meaning the folder structure inside the
/pages directory defines our routing. Components and re-usable layouts belong
in their respective folders.
Defines routing and handles high-level page layouts. Keep re-usable
components in /components, re-usable layouts in /layouts, and unique layouts as an index.vue file in your directory.
Re-usable layouts and components should be placed here. Name each file with
first letter capitalized, and do not name any files that would conflict with
HTML5 standard elements. Example: a re-usable header component should be named
AppHeader.vue. ESLint rules will also warn about conflicting naming
conventions, as well as single-word component names.
Nuxt 3 has auto imports, so we do not need to manually define importsfor files
that are in the /components directory. We can simply reference the file like
<ActionBanner> or <action-banner> and Nuxt 3 will handle the import.
Nested components act similarly, we just need to prefix the component name with
the subdirectory name, for instance <ArticlesDynamicShell> will instruct Nuxt
to import /components/Articles/DynamicShell.vue.
Place api routes and server middleware here. For client side middleware, see /middleware
/server/api/{context}/{filename}.js - Prefer to use index.js as filename when possible
/server/api/middleware/{filename}.js
Vue3 version of mixins. Composables follow a few simple rules. We are working to conform to the recommended styling set forth by the Vue team.
- Each composable should be prefixed with
use. A composable that returns some formatted text could be calleduseTextFormatter. - Prefer inline composables where possible. This can help organize your composition api code. See an example here (useArticleFromCacheOrApi). Explainer video here.
- Any composables that can be re-used throughout the app should be placed in
/composables
/composables instead
The project uses Bootstrap 5 with custom SCSS:
- Variables:
scss/_variables.scss - Type:
scss/type.scss - Main styles:
scss/main.scss - Component-specific styles in individual
.vuefiles
Prefer to use scoped attribute on styles in individual .vue files.
You can create and interface with API endpoints defined in /server/api. Some endpoints used in this project are:
- Multiple Articles API:
server/api/articles/index.js - Single Article:
server/api/article/index.js - Sitemap generation:
server/api/sitemap/urls.js
New endpoints should be placed in /server/api/{context}/index.js
See here (useArticleFromCacheOrApi). for an example.
Ensure to follow the pattern defined in useArticleFromCacheOrApi as it leverages both our custom api endpoint and Nuxt's built in caching mechanisms.
Prefer to use API endpoints within your page, rather than a component. See pages/article/[slug].vue and Data Flow
Sticking point: Your cache key must be unique to each type of API call, or else Nuxt cannot effectively cache your requests. Example:
const cacheKey = `articles-${vertical}-${route.params.slug}`;
// create a cache key like "articles-auto-some-unque-article-slug"The above code will generate a cache key unique to the vertical and article slug, so the next time a user navgiates to some-unque-article-slug in the session, the cache can be used rather than another API request.
This project strives to adhere to the unidirectional data flow paradigm (one-way data flow), or "Props down, events up". This ensures we can track where our data comes from, and not mutate state or data from a child component lower in the tree.
Examples:
The API is called at the top of the component structure (inside our page), and the resulting data is passed down into the child component
<template>
<!--Props are passed down to UI component-->
<SingleArticle :article="article"/>
</template>
<script setup>
//extract article data from cache or API
const { articleResult, error } = await useArticleFromCacheOrApi();
//reactive computed properties for article data
const article = computed(() => articleResult.value?.response.article || {});
</script>pages/car-insurance/discounts/index.vue
Notice how we pass all data that controls the child component down as props. We don't pass callback functions, we will rely on emitted events propegating up from the child. We listen for those events using @update:active-tab="switchTab"
<template>
<div>
Page content...
<NavigationTabs
:tabs="[
{ label: 'Policy & Loyalty', target: 'policy-loyalty' },
{ label: 'Safe Driving & Habits', target: 'safe-driving' },
{ label: 'Driver Profile & Lifestyle', target: 'driver-profile' },
{ label: 'Vehicle Equipment & Technology', target: 'vehicle-equipment' },
]"
:active-tab="currentTab"
:previous-tab="'policy-loyalty'"
@update:active-tab="switchTab"
/>
<!-- Category 1: Policy & Loyalty Discounts -->
<div v-show="currentTab === 'policy-loyalty'" id="policy-loyalty" class="discount-category">
<h3>1. Policy & Loyalty Discounts</h3>
<p class="category-description">These savings are based on how you manage your account and how long you've been a customer.</p>
</div>
<!-- Category 2: Safe Driving & Habits Discounts -->
<div v-show="currentTab === 'safe-driving'" id="safe-driving" class="discount-category">
<h3>2. Safe Driving & Habits Discounts</h3>
<p class="category-description">Your behavior on the road is the biggest factor in your premium cost.</p>
</div>
<!-- Category 3: Driver Profile & Lifestyle Discounts -->
<div v-show="currentTab === 'driver-profile'" id="driver-profile" class="discount-category">
<h3>3. Driver Profile & Lifestyle Discounts</h3>
<p class="category-description">Who you are, where you work, and your life milestones can trigger lower rates.</p>
</div>
<!-- Category 4: Vehicle Equipment & Technology Discounts -->
<div v-show="currentTab === 'vehicle-equipment'" id="vehicle-equipment" class="discount-category">
<h3>4. Vehicle Equipment & Technology Discounts</h3>
<p class="category-description">The safety and security features of your car can work in your favor.</p>
</div>
</div>
</template>
<script setup>
const previousTab = ref(null);
const currentTab = ref('privacy-policy');
const switchTab = (tab) => {
previousTab.value = currentTab.value;
currentTab.value = tab;
};
</script>/components/Navigation/Tabs.vue
Notice how we accept all incoming params from our parent component as props. We make no logical decisions, and do not rely on any expected layout, names, ids, etc. We simply accept props as the single source of truth, and emit an event back up when something changes using @click="$emit('update:activeTab', tab.target)". This helps with the mental model of the component, avoiding accidental prop mutation, and creating more re-usable components that aren't bound to strict logic.
<template>
<ul ref="tabList">
<li v-for="(tab, index) in tabs" :key="tab.name" :ref="(el) => setTabRef(el, index)" :class="{ active: tab.target === activeTab }">
<button @click="$emit('update:activeTab', tab.target)">{{ tab.label }}</button>
</li>
<span class="indicator" :style="{ left: indicatorLeft, width: indicatorWidth }" />
</ul>
</template>
<script setup>
const props = defineProps({
tabs: {
type: Array,
required: true,
},
activeTab: {
type: String,
required: true,
},
previousTab: {
type: String,
required: true,
},
});
defineEmits(["update:activeTab"]);
</script>There are a few composables that can be relevant for any new component/page.
This composable provides a way to dynamically generate image urls. This is especially useful when image paths or directories frequently change. This gives the developer a way to agnostically include images or icons without needing to know the base path for the assets. Simply call
<template>
<div>
<NuxtImg :src='buildImageUrl('your-image-name.jpg')'>
</div>
</template>
<script setup>
import { buildImageUrl } from "~/composables/images";
</script>This composable provides a way to dynamically import icons into a file. For example, a form button can have 2 states, where an arrow is shown inside the button by default, and a loading spinner is then shown on form submit. You can leverafe iconLoader to achieve this so we are not loading all icons unless that state is specifically triggered.
<template>
<button>
SUBMIT
<div v-if="iconComponentName" class="button-icon">
<component :is="iconComponentName" class="choice-icon" :name="iconComponentName" />
</div>
</button>
</template>
<script setup>
import { iconLoader } from "~/composables/icons";
//use vue's reactivity to update the currentIcon based on state, which in turn causes the iconComponentName shallowRef to load the new component on the fly.
const currentIcon = computed(() => {
return isSubmitting.value ? 'LoadingIcon' : props.config.icon;
});
//use shallowRef here, as iconLoader returns a component and we don't want to wrap the component in reactive ref
const iconComponentName = shallowRef(iconLoader(currentIcon?.value || null));
</script>Provides a clean way to redirect to other routes within the app, or to an external source. This function gathers all availible url params, and also appends any data you have provided in a key/value format to the existing url params, then routes accordingly. Do not use for normal mnavigation, only when you find yourself reaching for a solution to passing params around before navigation
<template>
<!-- External redirect-->
<form>
<label for="first_name">First Name</label>
<input name="first_name" type="text" value="Matt" v-model="firstName"/>
<button @click.prevent="handleExternalRedirect">
SUBMIT
</button>
</form>
<!-- App redirect-->
<form>
<label for="first_name">First Name</label>
<input name="first_name" type="text" value="Matt" v-model="appfirstName"/>
<button @click.prevent="handleAppRedirect">
SUBMIT
</button>
</form>
</template>
<script setup>
import { redirectWithParams } from "@/composables/utils.js";
const firstName = ref("");
const appFirstName = ref("");
const handleExternalRedirect = () => {
redirectWithParams("insure.protect.com", {
first_name: firstName.value
});
//results in a new tab opening to https://insure.protect.com?first_name=Matt
}
const handleAppRedirect = () => {
const router = useRouter();
redirectWithParams("/some/route", {
first_name: firstName.value
}, router);
//by passing client-side router, we provide a flag to the function to make a client side redirect to /some/route?first_name=Matt
}
</script>Prefer the following layout for new vue files
<template>
...template code
</template>
<script setup>
// all setup here
</script>
<style>
/* scss code here */
</style> Prefer this layout for script setup blocks
<script setup>
//imports first
import { buildImageUrl } from "~/composables/images";
//props definitions next
const props = defineProps({
//props definitions here
});
//...other code below
console.log(props.someProp);
</script># Development server
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
# Generate static site
npm run generateWe strive to use semantic branch names as much as possible.
The basic prefixes for branch naming is defined below, the prefixes should be followed by a concise decsription or task id.
Example feature/bundle-page-DSN-1588
feature/ - indicates a new feature, page, component, etc.
bugfix/ - bugfix(es)
styling/ - updates to code styling
refactor/ - a refactoring of any scale
documentation/ - adding/updating docs or function comments
For development questions or issues, refer to: