Skip to content

TheDMSGroup/protect.com

Repository files navigation

Protect.com - Nuxt Application

A comprehensive insurance comparison and information platform built with Nuxt 3, Vue 3, and Bootstrap Vue Next.

Quick Links

Tech Stack

  • 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

Prerequisites

  • Node.js (v20 or higher recommended)
  • npm, pnpm, yarn, or bun package manager

Getting Started

1. Clone and Install

# Clone the repository
git clone https://github.com/TheDMSGroup/protect.com.git

# Install dependencies
npm install
# or
pnpm install
# or
yarn install

2. Development Server

Start the development server - defaults to http://localhost:3000:

npm run dev
# or
pnpm dev
# or
yarn dev

Project Structure

Nuxt 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.

1. /pages

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.

2. /components

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.

3. /server

Place api routes and server middleware here. For client side middleware, see /middleware

Api Route structure

/server/api/{context}/{filename}.js - Prefer to use index.js as filename when possible

Middleware Structure

/server/api/middleware/{filename}.js

4. /composables

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.

  1. Each composable should be prefixed with use. A composable that returns some formatted text could be called useTextFormatter.
  2. Prefer inline composables where possible. This can help organize your composition api code. See an example here (useArticleFromCacheOrApi). Explainer video here.
  3. Any composables that can be re-used throughout the app should be placed in /composables

5. /mixins

⚠️ Deprecated - use /composables instead

Styling

The project uses Bootstrap 5 with custom SCSS:

Prefer to use scoped attribute on styles in individual .vue files.

API Integration

You can create and interface with API endpoints defined in /server/api. Some endpoints used in this project are:

New endpoints should be placed in /server/api/{context}/index.js

Using an endpoint

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.

Data Flow

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:

pages/article/[slug].vue

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>

Navigation Tabs Usage (Parent)

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>

Navigation Tab Code (Child)

/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>

Useful composables

There are a few composables that can be relevant for any new component/page.

buildImageUrl (images.js)

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>

iconLoader (icons.js)

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>

redirectWithParams (utils.js)

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>

Code Styling

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 Commands

# Development server
npm run dev

# Build for production
npm run build

# Preview production build
npm run preview

# Generate static site
npm run generate

Contributing

We 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

Support

For development questions or issues, refer to: