DEV Community

Cover image for REST Countries API challenge solved with Chakra UI and React.
Femi Akinyemi
Femi Akinyemi

Posted on

REST Countries API challenge solved with Chakra UI and React.


Enter fullscreen mode Exit fullscreen mode

Introduction

In this article, I will explain how I solved the Frontend mentor "REST Countries API with a color theme switcher" challenge using Chakra UI and Create React App. At the end of this tutorial, we should be able to

  1. See all countries from the API on the homepage

  2. Search for a country using an input field

  3. Filter countries by region

  4. Click on a country to see more detailed information on a
    separate page

  5. Click through to the border countries on the detail page

  6. Toggle the color scheme between light and dark mode
    (optional)

Prerequisite

To follow this tutorial, you should have a basic understanding of the following.

  1. Basic knowledge of JavaScript ES6 syntax and features

  2. The basics of ReactJS terminology: JSX, State, Asynchronous
    JavaScript, etc.

  3. Basic understanding of Restful APIs.

  4. Basic knowledge of TypeScript

  5. Basic understanding of Chakra UI

  6. Basic knowledge of React Router

Demo and Github Links

Solution on Github
Live site

Component breakdown

  1. Header Component
  2. Home Component
  3. Singlepage Component

Setup

Before building each component, we start by creating a new create-react-app project from a template using Chakra UI automatic typescript template, as in the code below.

# TypeScript using npm
npx create-react-app my-app --template @chakra-ui/typescript
Enter fullscreen mode Exit fullscreen mode

This command will bootstrap the bare-bones react app that is ready to use.
After creating our app, our folder structure should look like what we have in the image below.

Image description

Now, we navigate into the my-app folder and start our app by running npm start. We should have something that looks like the image below.

Image description

Removing unwanted CSS

Now that we have generated a create-react-app project using a template, all we need to do is start creating our components and clean up the files.
Within the src folder, I have created two new folders: pages and components.

Image description

Packages

In addition to our pre-installed packages from create-react-app typescript templates, we will install additional two other packages that are

React-Router:

This will help us with routing within our app.

ChakraIcons:

This will provide a set of commonly used interface icons you can use in our project.

We can now navigate into our root folder and run the code below so you can install both packages together.

npm install react-router-dom@6 @chakra-ui/icons

Enter fullscreen mode Exit fullscreen mode

Navlink component

Here, we use a Chakra UI Navbar template with a user dropdown and a Dark theme switcher.

import {
  Box,
  Flex,
  Button,
  useColorModeValue,
  Stack,
  useColorMode,
} from '@chakra-ui/react';
import { MoonIcon, SunIcon } from '@chakra-ui/icons';
import { useNavigate } from 'react-router-dom';
export default function Nav() {
  const { colorMode, toggleColorMode } = useColorMode();
  let navigate = useNavigate();
  return (
    <>
      <Box bg={useColorModeValue('gray.100', 'gray.900')} px={4}>
        <Flex h={16} alignItems={'center'} justifyContent={'space-between'}>
          <Box onClick={()=> navigate('/')}  >Where in the world?</Box>
          <Flex alignItems={'center'}>
            <Stack direction={'row'} spacing={7}>
              <Button onClick={toggleColorMode}>
                {colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
              </Button>
            </Stack>
          </Flex>
        </Flex>
      </Box>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

For this component, we are using ReactRouter for easy routing and chakra-UI/icons to access Chakra UI Icons.

Routing:

Here, we link our components to the appropriate pages.

import React from 'react'
import {Route, Routes } from "react-router-dom";
import Home from '../Pages/Home';
import SingleCountry from '../Pages/SingleCountry';


function Routing() {
  return (
    <div>
        <Routes>
        <Route path="/" element={<Home/>} />
        <Route path="/singlecountry/:countryname" element={<SingleCountry/>} />
      </Routes> 
    </div>
  )
}

export default Routing
Enter fullscreen mode Exit fullscreen mode

Now that we have our navbar and routing components, we move to create our Homepage file

Home:

// Importing 
import React from "react";
import { useState, useEffect } from "react";
import {
  Flex,
  GridItem,
  Image,
  Input,
  InputGroup,
  InputLeftElement,
  Select,
  SimpleGrid,
  Spacer,
} from "@chakra-ui/react";
import { Box } from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import { Progress } from "@chakra-ui/react";
import { SearchIcon } from "@chakra-ui/icons";
import Nav from "../Components/Navlink";

function Home() {

  //States
  const [data, setData] = useState([]);
  const [data2, setData2] = useState([]);
  const [searchInput, setSearchInput] = useState("");
  const [selectInput, setSelectInput] = useState("all");
  let navigate = useNavigate();


//Calling Apis
  useEffect(() => {
    if (selectInput === "all") {
      fetch(`https://restcountries.com/v3.1/all`)
        .then((res) => res.json())
        .then((data) => {
          return (
            setData(data),
            setData2(data))
        })
        .catch((err) => console.log("Error:", err.message));
    } else {
      fetch(`https://restcountries.com/v3.1/region/${selectInput}`)
        .then((res) => res.json()).then((data)=>{
          return (
            setData(data), 
            setData2(data)
          )
        })
        .catch((err) => console.log("Error:", err.message));
    }
  }, [selectInput]);


  //Handle Region select
  const handleChangeSelect = (e) => {
    setSelectInput(e.target.value);
  };

  //Handle Country Search
  const handleChangeInput = (e) => {
    e.preventDefault();
    setSearchInput(e.target.value);
    setData(
      data2.filter((x) =>
        x?.name?.common
          ?.toLowerCase()
          ?.includes(e?.target?.value?.toLowerCase())
      )
    );
  };

  return (
    <div>
      {/* Navbar */}
      <Nav/>
{/* 
    Country Search and Region Select form */}
      <form>
        <Flex pr="50" pl="50" flexWrap={"wrap"}>
          <Box p="4">
            <InputGroup>
              <InputLeftElement
                pointerEvents="none"
                children={<SearchIcon color="gray.300" />}
              />
              <Input
                value={searchInput}
                onChange={handleChangeInput}
                type="text"
                placeholder="Search for a country "
              />
            </InputGroup>
          </Box>
          <Spacer />
          <Box p="4">
            <Select onChange={handleChangeSelect} placeholder="Select option">
              <option value="all">All</option>
              <option value="africa">Africa</option>
              <option value="americas">Americas</option>
              <option value="asia">Asia</option>
              <option value="europe">Europe</option>
              <option value="oceania">Oceania</option>
            </Select>
          </Box>
        </Flex>
      </form>

      {/* Data Rendering */}

      {data2?.length === 0 ? (
        <Progress colorScheme="pink" size="xs" isIndeterminate />
      ) : (
        <Box w="100%">
          <SimpleGrid
            columns={[1, null, 4]}
            spacing={10}
            pt="100"
            pr="50"
            pl="50"
          >
            {data?.map((x) => (
              <GridItem
                key={x?.name?.common}
                onClick={() =>
                  navigate(`/singlecountry/${x?.cca2?.toLowerCase()}`, {})
                }
              >
                <Box
                  maxW="sm"
                  borderWidth="1px"
                  borderRadius="lg"
                  overflow="hidden"
                >
                  <Image
                    src={x?.flags?.svg}
                    alt={x?.name?.common}
                    height="200px"
                    width="100%"
                  />
                  <Box p="6">
                    <Box
                      mt="1"
                      fontWeight="semibold"
                      as="h4"
                      lineHeight="tight"
                      noOfLines={1}
                    >
                      {x?.name?.common}
                    </Box>

                    <Box
                      mt="1"
                      fontWeight="semibold"
                      as="h4"
                      lineHeight="tight"
                      noOfLines={1}
                    >
                      Population: {x?.population}
                    </Box>

                    <Box
                      mt="1"
                      fontWeight="semibold"
                      as="h4"
                      lineHeight="tight"
                      noOfLines={1}
                    >
                      Region: {x?.region}
                    </Box>

                    <Box
                      mt="1"
                      fontWeight="semibold"
                      as="h4"
                      lineHeight="tight"
                      noOfLines={1}
                    >
                      Capital: {x?.capital}
                    </Box>
                  </Box>
                </Box>
              </GridItem>
            ))}
          </SimpleGrid>
        </Box>
      )}
    </div>
  );
}

export default Home;

Enter fullscreen mode Exit fullscreen mode

As you can see, there are a lot of lines of code to comprehend at once, so let's take it one step at a time.

Importing all the necessary components

Here, We import Flex, grid, item, Image, Input, InputGroup, InputLeftElement,Select,SimpleGrid,Spacer,Box, Progress from Chakra UI, use navigate from react-router, SearchIcon from ChakraIcons, and Nav from our Navlinks component

// Importing 
import React from "react";
import { useState, useEffect } from "react";
import {
  Flex,
  GridItem,
  Image,
  Input,
  InputGroup,
  InputLeftElement,
  Select,
  SimpleGrid,
  Spacer,
  Box,
  Progress
} from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import { SearchIcon } from "@chakra-ui/icons";
import Nav from "../Components/Navlink";
Enter fullscreen mode Exit fullscreen mode

States:

We declared our states here

  //States
  const [data, setData] = useState([]);
  const [data2, setData2] = useState([]);
  const [searchInput, setSearchInput] = useState("");
  const [selectInput, setSelectInput] = useState("all");
  let navigate = useNavigate();
Enter fullscreen mode Exit fullscreen mode

Api:

Calling Api


//Calling Apis
  useEffect(() => {
    if (selectInput === "all") {
      fetch(`https://restcountries.com/v3.1/all`)
        .then((res) => res.json())
        .then((data) => {
          return (
            setData(data),
            setData2(data))
        })
        .catch((err) => console.log("Error:", err.message));
    } else {
      fetch(`https://restcountries.com/v3.1/region/${selectInput}`)
        .then((res) => res.json()).then((data)=>{
          return (
            setData(data), 
            setData2(data)
          )
        })
        .catch((err) => console.log("Error:", err.message));
    }
  }, [selectInput]);

Enter fullscreen mode Exit fullscreen mode

Region selection and Country Search functions

  //Handle Region select
  const handleChangeSelect = (e) => {
    setSelectInput(e.target.value);
  };

  //Handle Country Search
  const handleChangeInput = (e) => {
    e.preventDefault();
    setSearchInput(e.target.value);
    setData(
      data2.filter((x) =>
        x?.name?.common
          ?.toLowerCase()
          ?.includes(e?.target?.value?.toLowerCase())
      )
    );
  };

Enter fullscreen mode Exit fullscreen mode

Import Navbar


      <Nav/>

    Country Search and Region Select form 
      <form>
        <Flex pr="50" pl="50" flexWrap={"wrap"}>
          <Box p="4">
            <InputGroup>
              <InputLeftElement
                pointerEvents="none"
                children={<SearchIcon color="gray.300" />}
              />
              <Input
                value={searchInput}
                onChange={handleChangeInput}
                type="text"
                placeholder="Search for a country "
              />
            </InputGroup>
          </Box>
          <Spacer />
          <Box p="4">
            <Select onChange={handleChangeSelect} placeholder="Select option">
              <option value="all">All</option>
              <option value="africa">Africa</option>
              <option value="americas">Americas</option>
              <option value="asia">Asia</option>
              <option value="europe">Europe</option>
              <option value="oceania">Oceania</option>
            </Select>
          </Box>
        </Flex>
      </form>
Enter fullscreen mode Exit fullscreen mode

Data Rendering


 {data2?.length === 0 ? (
        <Progress colorScheme="pink" size="xs" isIndeterminate />
      ) : (
        <Box w="100%">
          <SimpleGrid
            columns={[1, null, 4]}
            spacing={10}
            pt="100"
            pr="50"
            pl="50"
          >
            {data?.map((x) => (
              <GridItem
                key={x?.name?.common}
                onClick={() =>
                  navigate(`/singlecountry/${x?.cca2?.toLowerCase()}`, {})
                }
              >
                <Box
                  maxW="sm"
                  borderWidth="1px"
                  borderRadius="lg"
                  overflow="hidden"
                >
                  <Image
                    src={x?.flags?.svg}
                    alt={x?.name?.common}
                    height="200px"
                    width="100%"
                  />
                  <Box p="6">
                    <Box
                      mt="1"
                      fontWeight="semibold"
                      as="h4"
                      lineHeight="tight"
                      noOfLines={1}
                    >
                      {x?.name?.common}
                    </Box>

                    <Box
                      mt="1"
                      fontWeight="semibold"
                      as="h4"
                      lineHeight="tight"
                      noOfLines={1}
                    >
                      Population: {x?.population}
                    </Box>

                    <Box
                      mt="1"
                      fontWeight="semibold"
                      as="h4"
                      lineHeight="tight"
                      noOfLines={1}
                    >
                      Region: {x?.region}
                    </Box>

                    <Box
                      mt="1"
                      fontWeight="semibold"
                      as="h4"
                      lineHeight="tight"
                      noOfLines={1}
                    >
                      Capital: {x?.capital}
                    </Box>
                  </Box>
                </Box>
              </GridItem>
            ))}
          </SimpleGrid>
        </Box>
      )}

Enter fullscreen mode Exit fullscreen mode

Single Country Component

Now, we create the single country component.

When a user clicks on a country, this is the single detailed information page that opens on a separate page. All we are doing here is setting our states and then updating the state with the response gotten from the API inside use effect. We then map our country state inside Chakra components.

import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import {
  Button,
  Center,
  GridItem,
  Image,
  Progress,
  SimpleGrid,
} from "@chakra-ui/react";
import { Box } from "@chakra-ui/react";
import Nav from "../Components/Navlink";


function SingleCountry() {
  let { countryname } = useParams();
  const [data, setData] = useState();
  let navigate = useNavigate();
  useEffect(() => {
    fetch(`https://restcountries.com/v3.1/alpha/${countryname}`)
      .then((res) => res.json())
      .then((data) => setData(data))
      .catch((err) => console.log("Error:", err.message));
  }, [countryname]);
  return (
    <div>
      <Nav />

      <Box onClick={() => navigate(-1)} p={'10'}  >
        <Button size="lg" variant="solid" mr="3">
          Back
        </Button>
      </Box>

      {data === undefined || data === null ? (
        <Progress colorScheme="pink" size="xs" isIndeterminate />
      ) : (
        data?.map((x) => {
          return (
            <Center key={x?.name?.common} >
              <SimpleGrid
                columns={[1, null, 2]}
                spacing={100}
                pt="100"
                pr="50"
                pl="50"
              >
                <GridItem w="100%">
                  <Image src={x?.flags?.svg} alt={x?.Region} height="350" />
                </GridItem>
                <GridItem w="100%">
                  <Box
                    mt="1"
                    fontWeight="semibold"
                    as="h4"
                    lineHeight="tight"
                    noOfLines={1}
                  >
                    {x?.name?.common}
                  </Box>

                  <SimpleGrid columns={2} spacing={10}>
                    <Box>Native Name: {x?.name?.common}</Box>
                    <Box>Top Level Domain: {x?.tld[0]}</Box>
                  </SimpleGrid>

                  <SimpleGrid columns={2} spacing={10}>
                    <Box>Population: {x?.population}</Box>
                    <Box>
                      Currencies:{" "}
                      {x?.currencies[Object?.keys(x?.currencies)[0]]?.name}
                    </Box>
                  </SimpleGrid>

                  <SimpleGrid columns={2} spacing={10}>
                    <Box>Region: {x?.region}</Box>
                    <Box>
                      Language(s): {x?.languages[Object.keys(x?.languages)[0]]}
                    </Box>
                  </SimpleGrid>

                  <SimpleGrid columns={2} spacing={10}>
                    <Box>Subregion: {x?.subregion}</Box>
                  </SimpleGrid>

                  <SimpleGrid columns={2} spacing={10}>
                    <Box>Capital: {x?.capital}</Box>
                  </SimpleGrid>

                  <SimpleGrid mt="50" columns={2} spacing={10}>
                    <Box>Border Countries:</Box>

                    <Box>
                      {x?.borders?.map((x) => (
                        <Button
                          onClick={() => navigate(`/singlecountry/${x}`)}
                          size="lg"
                          key={x}
                          variant="solid"
                          mr="3"
                        >
                          {x}
                        </Button>
                      ))}
                    </Box>
                  </SimpleGrid>
                </GridItem>
              </SimpleGrid>
            </Center>
          );
        })
      )}
    </div>
  );
}

export default SingleCountry;
Enter fullscreen mode Exit fullscreen mode

Let’s create the heart of this project.

App.tsx

import * as React from "react"
import { ChakraProvider, theme } from '@chakra-ui/react'

import { BrowserRouter } from "react-router-dom";
import Routing from "./Components/Routing";

export const App = () => (
 <div>
<ChakraProvider theme={theme}>
<BrowserRouter>
<Routing/>
</BrowserRouter>
</ChakraProvider>

 </div>
)
Enter fullscreen mode Exit fullscreen mode

Having created all our components, let's enter our app's directory and run npm start to start the app. At this point, we should see something like the image below.

Image description

At this point, if we click on any of the countries, it should route us to a different page where we can see more details about the country, like the image below.

Image description

Well, congrats on that great hustle! You have the solution to REST Countries API with a color theme switcher ready at your disposal.

Thanks for Reading🌟🎉

It's great to see that you have enjoyed the article. Please, let me know what you think in the comment section.

On to another blog, some other day, till then Femi👋.

Top comments (0)