rojaence / rest-countries-api

FrontendMentor challenge built with Vue 3, Vue-router, Pinia and scss

Home Page:https://rojaence.github.io/rest-countries-api/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Frontend Mentor - REST Countries API with color theme switcher solution

This is a solution to the REST Countries API with color theme switcher challenge on Frontend Mentor. Frontend Mentor challenges help you improve your coding skills by building realistic projects.

Table of contents

Overview

The challenge

Users should be able to:

  • See all countries from the API on the homepage
  • Search for a country using an input field
  • Filter countries by region
  • Click on a country to see more detailed information on a separate page
  • Click through to the border countries on the detail page
  • Toggle the color scheme between light and dark mode

Screenshots

Desktop
Home view

Home view - dark mode

Detail view

Detail view - dark mode

Mobile

Filter options

Links

My process

Built with

  • Semantic HTML5 markup
  • BEM naming convention
  • CSS custom properties
  • Flexbox
  • CSS Grid
  • SCSS
  • Mobile-first workflow
  • Vue 3 - JS framework
  • Pinia - Vue store
  • Vue Router - Route management

Recommended IDE Setup

This template should help get you started developing with Vue 3 in Vite.

VSCode + Volar (and disable Vetur) + TypeScript Vue Plugin (Volar).

Project Setup

npm install

Compile and Hot-Reload for Development

npm run dev

Compile and Minify for Production

npm run build

What I learned

Dark mode

The app theme is managed by a service that change the class in the root document with the theme setup, wich in turn that updates in the store.

Theme provider

export default class ThemeProvider {
  constructor () {
    this.DARK_MODE_KEY = "RestCountriesApiDarkMode";
  }

  selectDarkMode(value) {
    value ? 
    document.documentElement.classList.add("theme--dark")
    : 
    document.documentElement.classList.remove("theme--dark")
  };

  setDarkMode() {
    let value = localStorage.getItem(this.DARK_MODE_KEY); 
    let response = false;
    if (value === "true") {
      localStorage.setItem(this.DARK_MODE_KEY, false)
      this.selectDarkMode(false);
    } else if (value === "false") {
      localStorage.setItem(this.DARK_MODE_KEY, true)
      this.selectDarkMode(true);
      response = true;
    } else {
      localStorage.setItem(this.DARK_MODE_KEY, false)
      this.selectDarkMode(false);
    }
    return response;
  }

  setThemeOnInit() {
    document.documentElement.classList.add("theme");
    let response = false;
    let value = localStorage.getItem(this.DARK_MODE_KEY); 
    if (value === "true") {
      this.selectDarkMode(true);
      response = true;
    } else if (value === "false") {
      this.selectDarkMode(false);
    } else {
      this.setDarkMode();
    }
    return response;
  };
}

Use service

import ThemeProvider from '@/services/ThemeProvider'

const indexStore = useIndexStore();
const themeProvider = new ThemeProvider();

// On init app - App.vue

onBeforeMount(() => {
  if (themeProvider.setThemeOnInit() === false) {
    indexStore.setDarkMode(false);
  } else {
    indexStore.setDarkMode(true);
  }
});

// On change with dark mode button - AppHeader.vue

const setAppTheme = () => {
  if (themeProvider.setDarkMode() === false) {
    indexStore.setDarkMode(false);
  } else {
    indexStore.setDarkMode(true);
  }
}

DOM Optimization

The app has some transitions and animations, to optimize performance when loading or filtering data, I decided to program pagination that limits the number of DOM elements.

Showing all country cards (250) while leaking data or re-requesting from the API could slow down the browser; with paging, the DOM element limit is reduced from +4000 to just 547, in this particular case displaying 32 cards per page.

<transition-group 
  class="countries-gallery__list" 
  ref="countriesList" 
  tag="ul" 
  name="list-transition">
  <li 
    class="countries-gallery__item" 
    v-for="(item, index) in filteredCountries.slice(initialIndex, finalIndex)" 
    :key="index">      
    <country-card 
      @click="showItemDetails(item)" 
      :data="item" 
      :dense="true">
    </country-card>
  </li>
</transition-group>

Pagination control

const initialIndex = ref(0);
const maxPageItems = 32;
const finalIndex = ref(0);

const prevPage = () => {
  if (initialIndex.value === 0 || filteredCountries.value.length === 0) return;
  initialIndex.value -= maxPageItems;
  finalIndex.value -= maxPageItems;
}

const nextPage = () => {
  let dataLength = filteredCountries.value.length;
  if (dataLength === 0 || finalIndex.value === dataLength) return;
  if (initialIndex.value + maxPageItems < dataLength) {
    initialIndex.value += maxPageItems;
    finalIndex.value += maxPageItems;
  }
}

SVG Icon Component

Create a component to render SVG icons with editable props

<component
    :is="iconComponent"
    role="img"
    class="icon"
    :class="{ 'icon--outline': outline, 'icon--error': color === 'error' }"
    :height="size"
    :width="size"
    viewBox="0 0 24 24"
  >
  </component>

This component imports the SVG vue files with the icons and uses the props to render

import IconSearch from '@/components/icons/IconSearch.vue'
import IconAlert from '@/components/icons/IconAlert.vue'
import IconChevron from '@/components/icons/IconChevron.vue'
import IconRefresh from '@/components/icons/IconRefresh.vue'
import IconTheme from '@/components/icons/IconTheme.vue'
import IconArrow from '@/components/icons/IconArrow.vue'
import IconClose from '@/components/icons/IconClose.vue'

const icons = {
  search: IconSearch,
  alert: IconAlert,
  chevron: IconChevron,
  refresh: IconRefresh,
  theme: IconTheme,
  arrow: IconArrow,
  close: IconClose,
}

const props = defineProps({
  name: {
    type: String,
    required: true
  },
  size: {
    type: Number,
    default: 20,
  },
  outline: {
    type: Boolean,
    default: false
  },
  color: {
    type: String,
    required: false,
  }
});

const iconComponent = computed(() => {
  if (icons[props.name]) {
    return icons[props.name];
  } else {
    return IconSearch;
  }
})

Registration as a global component in main.js

import Icon from './components/common/Icon.vue'

const app = createApp(App)
app.component('icon', Icon)

When you need to use a specific icon, only use the tag with the props

<icon name="arrow"></icon>

<icon name="alert" :size="128"></icon>

<icon name="alert" color="error" :size="200"></icon>

Continued Development

Look for other ways to optimize performance and content loading, such as a recycler or list virtualizer, for example react-window or vue-virtual-scroller, in addition to lazy loading and infinite scrolling.

Useful resources

Author

About

FrontendMentor challenge built with Vue 3, Vue-router, Pinia and scss

https://rojaence.github.io/rest-countries-api/


Languages

Language:Vue 72.1%Language:SCSS 14.4%Language:JavaScript 12.4%Language:HTML 1.2%