DEV Community

Cover image for Frontend Mentor - REST Countries API with color theme switcher
Nabilla Trisnani
Nabilla Trisnani

Posted on • Edited on

Frontend Mentor - REST Countries API with color theme switcher

This is my solution to Frontend Mentor REST Countries API with color theme switcher challenge.


Table of contents

Overview

The challenge is to integrate with the REST Countries API to pull country data and display it like in the designs.

The challenge

User 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 (optional)

Links

My process

Built with:

What I learned

  • GET data from API with React Hooks.
const [isLoaded, setIsLoaded] = useState(false);
const [items, setItems] = useState([]);

useEffect(() => {
  getData();
}, [])

const getData = () => {
  fetch("https://restcountries.com/v3.1/all")
    .then(res => res.json())
    .then(
    (results) => {
      setIsLoaded(true);
      setItems(results);
      // console.log(results)
    }
  )
}
Enter fullscreen mode Exit fullscreen mode
  • GET parameter with useParams().
import { useParams } from 'react-router-dom';

const [isLoaded, setIsLoaded] = useState(false);
const [items, setItems] = useState([]);

let { name } = useParams();

useEffect(() => {
  fetch(`https://restcountries.com/v3.1/alpha/${name}`)
    .then(res => res.json())
    .then(
    (results) => {
      setIsLoaded(true);
      setItems(results);
      console.log(results);
    }
  )
}, [name])
Enter fullscreen mode Exit fullscreen mode
  • Filtering data.
const [items, setItems] = useState([]);
const [filteredItems, setFilteredItems] = useState([]);
const [searchInput, setSearchInput] = useState("");
const [selectInput, setSelectInput] = useState("");
const regionList = ["Africa", "Americas", "Asia", "Europe", "Oceania"];

const handleChangeInput = (e) => {
  setSearchInput(e.target.value);
};
const handleChangeSelect = (e) => {
  setSelectInput(e.target.value);
};

useEffect(() => {
  const result = items.filter(
    (item) =>
      (!searchInput ||
        item.name.common.toLowerCase().includes(searchInput.toLowerCase())) &&
      (!selectInput || item.region === selectInput)
  );
  setFilteredItems(result);
  console.log(result);
}, [searchInput, items, selectInput]);

Enter fullscreen mode Exit fullscreen mode
<input type="text" placeholder="Search for a country" disabled={isLoaded ? false : true} value={searchInput} onChange={handleChangeInput} />

<select className="form__select" onChange={handleChangeSelect}>
  <option value="">All</option>
  {regionList.map((region) => (
    <option value={region} key={region}>
      {region}
    </option>
  ))}
</select>

Enter fullscreen mode Exit fullscreen mode
  • Handle theme with localStorage.
const [isActive, setActive] = useState("false");
localStorage.getItem('theme');

const handleToggle = () => {
  setActive(!isActive);
  if (isActive === true) {
    localStorage.setItem('theme', 'dark');
  } else {
    localStorage.setItem('theme', 'light');
  }
}
Enter fullscreen mode Exit fullscreen mode
<div className={localStorage.getItem("theme") === "light" ? "App light-theme" : "App dark-theme"}>
  <Navbar onClick={handleToggle} />
  <Routes />
</div>
Enter fullscreen mode Exit fullscreen mode

For theme style, I use SCSS nesting. If App have class light-theme then, children item such as card, input, and skeleton loading will have light background color and dark font, and if App have class dark-theme then, children item will have dark background and light font.

  • Make skeleton loading. For example I'm going to use card component.
const [isLoaded, setIsLoaded] = useState(false);

useEffect(() => {
  getData();
}, [])

const getData = () => {
  fetch("https://restcountries.com/v3.1/all")
    .then(res => res.json())
    .then(
    (results) => {
      // When results is loaded, we set isLoaded to true
      setIsLoaded(true);
      setItems(results);
      // console.log(results)
    }
  )
}
Enter fullscreen mode Exit fullscreen mode

For the HTML, if isLoaded is false then add class skeleton to form__search and disabled input.

<div className={isLoaded ? 'form__search' : 'form__search skeleton' }>
  <ion-icon name="search"></ion-icon>
  <input type="text" placeholder="Search for a country" disabled={isLoaded ? false : true} value={searchInput} onChange={handleChangeInput} />
</div>
Enter fullscreen mode Exit fullscreen mode

For styling I use SCSS nesting.

// variable
//color for dark theme
$dark-background: #202c37;
$dark-element: #2b3945;
$dark-text: #ffffff;

//color for light theme
$light-background: #fafafa;
$light-element: #ffffff;
$light-text: #111517;

//mixin
@mixin skeleton {
  content: "" !important;
  width: 50px;
  position: absolute;
  height: 100%;
  animation-name: gradient-animation;
  animation-duration: 2s;
  animation-iteration-count: infinite;
  filter: blur(5px);
}

@mixin skeleton-dark {
  background: $dark-element !important;

  &::after {
    background: linear-gradient(
      to right,
      $dark-element 25%,
      $dark-background 50%,
      $dark-element 100%
    );
  }
}

@mixin skeleton-light {
  background-color: #e2e2e2;

  &::after {
    background: linear-gradient(
      to right,
      #e2e2e2 25%,
      #d5d5d5 50%,
      #e2e2e2 100%
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
// input base

.form {
  &__search {
    display: flex;
    align-items: center;
    width: 480px;
    padding: 0 32px;
    border-radius: 8px;

    ion-icon {
      font-size: 18px;
      margin-right: 24px;
    }
    input {
      border: none;
      outline: none;
      padding: 18px 0;
      width: 100%;
      font-weight: 600;
      font-size: 14px;
      letter-spacing: -0.01em;
      background-color: transparent;
    }
  }
  &__search.skeleton {
    color: transparent;
    position: relative;
    overflow: hidden;
    border-radius: 0;

    ion-icon {
      color: transparent !important;
    }
    input {
      color: transparent !important;

      &::placeholder {
        color: transparent !important;
      }
    }
    &::after {
      @include skeleton;
      margin: 0 -32px;
    }
  }
}

@keyframes gradient-animation {
  from {
    left: 0%;
  }
  to {
    left: 100%;
  }
}
Enter fullscreen mode Exit fullscreen mode
// dark theme

.dark-theme {
  .form {
    &__search {
      background: $dark-element;

      ion-icon {
        color: $dark-text;
      }
      input {
        color: $dark-text;

        &::placeholder {
          color: $dark-text;
        }
      }
    }
    &__search.skeleton {
      @include skeleton-dark;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
// light theme

.light-theme {
  .form {
    &__search {
      background: $light-element;

      ion-icon {
        color: $light-text;
      }
      input {
        color: $light-text;

        &::placeholder {
          color: hsl(0, 0%, 52%);
        }
      }
    }
    &__search.skeleton {
      @include skeleton-light;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Author

Top comments (0)