DEV Community

Cover image for Build Mobile and Web Apps in a Single Codebase with the Ionic Framework and React JS
Busayo Samuel
Busayo Samuel

Posted on

Build Mobile and Web Apps in a Single Codebase with the Ionic Framework and React JS

I. Introduction

II. Getting Started with Ionic Framework 6

III. Building a movie app

IV. Deploying the application

V. Testing and simulating on devices

VI. Conclusion

VII. Additional Resources and Recommendations for Further Learning

Introduction

A developer alone in the dark
Why do developers prefer cross-platform frameworks? So they don't have to pull their hair out building and maintaining separate codebases for web and mobile apps. It's hard enough to build one app, let alone two!

Thankfully, the Ionic framework enables cross-platform development using React, Angular, and Vue from a single codebase. Pretty cool, right? This provides a seamless experience for both the developer and the user, as the user doesn't have to learn how to interact with a whole new interface, and the developer only has to maintain one codebase instead of two.

In this article, we will go over how to get started with Ionic, create a simple movie app, deploy and test it. So, if you are a front-end developer looking to expand your skills and build cross-platform apps, read on!

Getting Started with Ionic Framework 6

Before you get started with working with Ionic, make sure you have Node JS and npm installed on your computer. If you are using a windows device, install Android studio; if you are using a Macbook, install Xcode.

Install the Ionic CLI globally on your machine by running the following command in your terminal:

npm install -g @ionic/cli

Enter fullscreen mode Exit fullscreen mode

To confirm that the installation was successful, check the Ionic version on your machine with the command below:

ionic --version

Enter fullscreen mode Exit fullscreen mode

Once you have installed Ionic, you are ready to build your first application. In the next section, we will create a starter template for the Ionic application with React.

Building Web Apps with Ionic Framework 6 and React.js

Follow the steps below to get started:

  • Create a folder for your application and navigate to the directory using the code below:
mkdir movie
Enter fullscreen mode Exit fullscreen mode
cd movie
Enter fullscreen mode Exit fullscreen mode
  • Run the following command to create a starter template for your ionic app:
ionic start myapp

Enter fullscreen mode Exit fullscreen mode

When prompted to select a framework for your template, choose React. Then, in the following prompt, select the option for a blank template to create a new template.

Your folder structure should look like the image below.

Ionic application folder structure

  • Navigate to the myapp directory with the command below:
cd myapp

Enter fullscreen mode Exit fullscreen mode
  • Run the command below to spin up your application:
ionic serve
Enter fullscreen mode Exit fullscreen mode

This command starts a local development server and opens the app in your default browser at http://localhost:8100.
Your browser page should look like this

Ionic home page template

The Ionic framework provides a wide range of UI components that you can use to create a high-quality user interface for your app. These components are designed to look and feel like native components on iOS and Android, providing a seamless user experience. In the next section, we will explore the starter template and the UI components.

Exploring the starter template

The starter template's entry point is main.tsx. This file renders App.tsx, just like in a basic React JS application.
In the App.tsx file, a bunch of pre-installed packages are being imported to handle the routing and styling of the application. The application uses both react-router-dom and @ionic/react-router for routing.

Components

  • IonApp: IonApp is a component that encapsulates the entire application by providing a foundation for rendering other components.

  • IonRouterOutlet: To define routes, developers can utilize the <IonRouterOutlet> component, where each <Route> component represents a unique route that the application can handle.

  • IonPage: The Home.tsx file located in the pages folder is enclosed within the IonPage component. IonPage is a framework-provided component used to establish the structure of a page and contain all its constituent components. The starter template above wraps the Home component with the IonPage component, signifying that the Home component represents the entire page.

  • IonHeader: This component is used to define the header section of a page. It usually contains a title and other navigation elements.

  • IonToolbar: The toolbar component is used to create the navigation elements on the page. The component can be customized with various properties such as color, mode, and slot.

Read more on Ionic framework components.

Now that you have gone through the template and have a basic understanding of how Ionic applications are structured, let's start building the movie application in the next section.

Building a Movie App

In this section, you will learn how to build a simple movie app. The app will allow users to browse the latest movie releases and save desired movies to a bookmark list. By the end of this section, you will have a strong understanding of Ionic application development and be on your way to building more complex applications.

Setting Up the App Layout

To get started;

  • Open the App.tsx file and replace the default code with the following:
import { Redirect, Route } from "react-router-dom";
import { IonApp, IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs, setupIonicReact } from "@ionic/react";
import { list, person, videocam } from "ionicons/icons";
import { IonReactRouter } from "@ionic/react-router";
import MovieList from "./pages/MovieList";
import MovieDetails from "./pages/MovieDetails";
import WatchList from "./pages/WatchList";

/* Core CSS required for Ionic components to work properly */
import "@ionic/react/css/core.css";

/* Basic CSS for apps built with Ionic */
import "@ionic/react/css/normalize.css";
import "@ionic/react/css/structure.css";
import "@ionic/react/css/typography.css";

/* Theme variables */
import "./theme/variables.css";

setupIonicReact();

const App: React.FC = () => (
  <IonApp>
      <IonReactRouter>
        <IonTabs>
          <IonRouterOutlet>
            <Route path="/movies" component={MovieList} exact={true} />

            <Redirect exact from="/" to="/movies" />
          </IonRouterOutlet>
          <IonTabBar slot="bottom">
            <IonTabButton tab="movies" href="/movies">
              <IonIcon icon={videocam} />
              <IonLabel>Movies</IonLabel>
            </IonTabButton>
            <IonTabButton tab="watchlist" href="/watchlist">
              <IonIcon icon={list} />
              <IonLabel>Watch List</IonLabel>
            </IonTabButton>
            <IonTabButton tab="account" href="/account">
              <IonIcon icon={person} />
              <IonLabel>Account</IonLabel>
            </IonTabButton>
          </IonTabBar>
        </IonTabs>
      </IonReactRouter>
    </IonApp>
);

export default App;

Enter fullscreen mode Exit fullscreen mode

In an Ionic application, the hierarchy of the JSX components is critical. The root of the application is wrapped in the <IonApp> component, which is imported from the ionic/react package. This component indicates that the project is an Ionic application. The <IonReactRouter> and <IonReactOutlet> components are also fundamental components of the Ionic routing. On the other hand, the <IonTabs> component is not a fundamental part of the hierarchy, but it allows the application to have a bottom tab bar for navigation.

  • In the src folder, create a pages folder and add a MovieList.jsx file in the folder. Add the code below to the file:
import React, { useState, useEffect } from "react";
import {
  IonApp,
  IonContent,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonSearchbar,
  SearchbarChangeEventDetail,
  IonPage,
  IonLoading,
} from "@ionic/react";
import axios from "axios";
import { MovieProps } from "../type";
import MovieCard from "../components/MovieCard";

const App = () => {
  const [movies, setMovies] = useState<MovieProps[]>([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      const response = await axios.get(
        `https://api.themoviedb.org/3/movie/popular?api_key=${process.env.REACT_APP_TMDB_KEY}&language=en-US&page=1`
      );
      setMovies(response.data.results);
    };

    fetchData();
  }, []);

  const searchMovies = async (searchText: string) => {
    setLoading(true);
    const response = await fetch(
      `https://api.themoviedb.org/3/search/movie?api_key=${process.env.REACT_APP_TMDB_KEY}&query=${searchText}`
    );
    const data = await response.json();
    setLoading(false);
    return data.results;
  };

  const handleSearch = async (
    event: CustomEvent<SearchbarChangeEventDetail>
  ) => {
    const text = event.detail.value || "";
    if (text.length === 0) {
      return;
    }
    try {
      const results = await searchMovies(text);
      setMovies(results);
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <IonPage>
      <IonLoading isOpen={loading} message={"Searching..."} duration={2000} />
      <IonApp>
        <IonHeader>
          <IonToolbar>
            <div className="toolbar-container">
              <IonTitle>Latest Movies</IonTitle>
              <IonSearchbar
                placeholder="Search movies..."
                onIonChange={handleSearch}
                className="search-input"
              ></IonSearchbar>
            </div>
          </IonToolbar>
        </IonHeader>
        <IonContent>
          <div className="movie-container">
            {movies.map((movie) => (
              <MovieCard key={movie.id} movie={movie} />
            ))}
          </div>
        </IonContent>
      </IonApp>
    </IonPage>
  );
};

export default App;

Enter fullscreen mode Exit fullscreen mode

In the code snippet above, the latest movie list is being fetched from the TMBD website using their API key. To obtain the key, you need to create an account on the TMBD site and request for an API key. Once you have the key, create an .env file in your src folder and add your API key to it, as shown in the code below. Replace "YOUR_API_KEY" with the API key gotten from the TMDB website:

REACT_APP_TMBD_KEY=YOUR_API_KEY
Enter fullscreen mode Exit fullscreen mode

In the useEffect hook in the MovieList page, the API result is fetched using axios and saved in the movies state. The searchMovies function handles searching the TMDB database for movies using the text input obtained from the IonSearchBar component, and the handleSearch function triggers the search when the submit button is clicked.

The IonLoading component is used to display a loading state while searching for movies. The result gotten after the API is then mapped to the MoviesCard component, which will be created later in the article.

  • Create a type.ts file in the src folder and add the snippet below:
export type MovieProps = {
  title: string;
  poster: string;
  plot: string;
  id: string;
  poster_path:string
  release_date:string
  backdrop_path:string
};

Enter fullscreen mode Exit fullscreen mode
  • Create a components folder in the src folder and add a MoviesCard.tsx file.

  • Add the following code to the MoviesCard component:

import { IonCard, IonIcon, IonImg, IonLabel } from "@ionic/react";
import { MovieProps } from "../type";
import { trashBin } from "ionicons/icons";
import { useState } from "react";

type props = {
  movie: MovieProps;
  watch?: boolean;
  handleDelete?: (
    event: {
      preventDefault: () => void;
      stopPropagation: () => void;
    },
    movie: MovieProps
  ) => void;
};
function MovieCard({ movie, watch, handleDelete }: props) {
  const [showSuccessToast, setshowSuccessToast] = useState<boolean>(false);

  return (
    <IonCard routerLink={`/movies/${movie.id}`}>
      <div className="movie-item">
        {watch && handleDelete && (
          <IonIcon
            icon={trashBin}
            slot="start"
            onClick={(e) => handleDelete(e, movie)}
            className="trash"
          />
        )}
        <IonImg
          src= {`https://image.tmdb.org/t/p/w500/${movie.backdrop_path}`}
          alt={movie.title}
        />
        <IonLabel>
          <h2 className="movie-title">{movie.title}</h2>
        </IonLabel>
      </div>
    </IonCard>
  );
}
export default MovieCard;

Enter fullscreen mode Exit fullscreen mode

This component is a simple card displaying an image and the movie title. When a user clicks on any part of the card, they will be redirected to the MovieDetails page. Since the Card component will also be utilized by the WatchList page, the trash bin is being conditionally rendered.
Before proceeding to create the next page in the application, consider adding a styles folder to the application to provide a different look and feel to the MovieList page.

  • In the src folder, create a global.css file and add the following code to the file:
/* global styles */
ion-content {
  --background: #141414;
  --color: #fff;
}
ion-card {
  z-index: 1 !important;
}
/* home page styles */
.home-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
}

.home-container h1 {
  font-size: 1.5rem;
  margin-top: 20px;
  margin-bottom: 30px;
}

/* movie list styles */
.movie-container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  grid-gap: 20px;
}

.movie-item {
  position: relative;
}

.movie-item ion-img {
  width: 100%;
  height: 300px;
  object-fit: cover;
  border-radius: 10px;
}

.movie-item .movie-title,
.bookmark,
.trash {
  position: absolute;
  background-color: rgba(0, 0, 0, 0.7);
  color: #fff;
  padding: 10px;
  text-align: start;
  font-weight: bold;
  font-size: 1rem;
}
.movie-item .movie-title {
  bottom: 0;
  left: 0;
  right: 0;
}
.movie-details .bookmark,
.movie-item .trash {
  top: 0;
  right: 0;
  font-size: 1.2rem;
}
/* movie details styles */
.movie-details {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
}
.movie-details .text-wrapper {
  margin: 0 auto;
}
.title {
  margin-left: 20px;
  color: #838181;
}
ion-button {
  --background: #5c5b5b;
  --color: white;
  margin-left: 10px;
}
.movie-details h2 {
  font-size: 1.5rem;
  margin-top: 20px;
  margin-bottom: 10px;
}

.movie-details p {
  font-size: 1rem;
  line-height: 1.5;
  margin-bottom: 20px;
  max-width: 40ch;
}
.img-wrapper {

  position: relative;
}
.movie-details img {
  border-radius: 20px;
  max-height: 600px;
  max-width: 400px;
  height: 100%;
  width: 100%;
  object-fit: cover;
}
.movie-details .rating {
  display: flex;
  align-items: center;
  font-size: 1.2rem;
  margin-bottom: 20px;
  gap: 5px;
  place-items: center;
}
.movie-details .release-date {
  color: #5c5b5b;
}

.movie-details .rating ion-icon {
  color: #fbc02d;
  margin-right: 10px;
}

/* search bar styles */
.search-container {
  padding: 20px;
}
.toolbar-container {
  display: flex;
  justify-content: start;
  padding: 10px 40px 10px 0px;
}
.search-input {
  width: 400px;
}
ion-toolbar {
  display: flex !important;
}
/* error message styles */
.error-message {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

.error-message h1 {
  font-size: 2rem;
  margin-right: 20px;
}

/* loading indicator styles */
.loading-indicator {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

.loading-indicator ion-spinner {
  --color: #fff;
}

Enter fullscreen mode Exit fullscreen mode
  • Import the global styles to your App.tsx file like so:
/* Global styles */
import "./global.css";
Enter fullscreen mode Exit fullscreen mode

The movie list should be displayed in a grid layout like the image below.

Movie list grid

  • Add a MovieDetails.tsx file in your pages folder and add the code below:
import React, { useEffect, useState } from "react";
import { RouteComponentProps, useHistory, useParams } from "react-router-dom";
import {
  IonContent,
  IonHeader,
  IonPage,
  IonTitle,
  IonToolbar,
  IonImg,
  IonList,
  IonItem,
  IonLabel,
  IonText,
  IonBadge,
  IonButton,
  IonIcon,
  IonLoading,
  IonToast,
} from "@ionic/react";
import {
  arrowBack,
  bookmarkOutline,
  star,
  starHalf,
  starOutline,
} from "ionicons/icons";
import axios from "axios";
import { MovieProps } from "../type";

interface MovieDetails {
  id: number;
  title: string;
  poster_path: string;
  backdrop_path: string;
  release_date: string;
  overview: string;
  vote_average: number;
  vote_count: number;
  genres: { id: number; name: string }[];
}

const MovieDetails: React.FC = () => {
  let watchlist: MovieProps[] = localStorage.getItem("watchlist")
    ? JSON.parse(localStorage.getItem("watchlist")!)
    : [];
  const { id } = useParams<{ id: string }>();
  const [movieDetails, setMovieDetails] = useState<MovieDetails | null>(null);
  const [showSuccessToast, setshowSuccessToast] = useState<boolean>(false);
  const [message, setMessage] = useState({ present: false, message: "" });
  const history = useHistory();

  useEffect(() => {
    const fetchMovieDetails = async () => {
      try {
        const response = await axios.get(
          `https://api.themoviedb.org/3/movie/${id}?api_key=${process.env.REACT_APP_TMDB_KEY}`
        );
        setMovieDetails(response.data);
      } catch (error) {
        console.log(error);
      }
    };

    fetchMovieDetails();
  }, [id]);
  const handleSaveToWatchList = () => {
    const movieId = movieDetails?.id;
    const isMovieInWatchlist = watchlist.find(
      (movie: any) => movie.id === movieId
    );
    setshowSuccessToast(true);
    if (isMovieInWatchlist) {
      setMessage({
        message: "This movie has already been bookmarked",
        present: true,
      });
    }
    if (!isMovieInWatchlist) {
      const newList = [...watchlist, movieDetails];
      localStorage.setItem("watchlist", JSON.stringify(newList));
      setMessage({
        message: "Movie has been successfully bookmarked",
        present: false,
      });
    }
  };

  if (!movieDetails) {
    return (
      <IonLoading
        isOpen={!movieDetails}
        message={"Please wait..."}
        duration={3000}
      />
    );
  }
  let rating = movieDetails.vote_average / 2;
  const starRating = [];

  for (let i = 0; i < 5; i++) {
    if (rating >= 1) {
      starRating.push(<IonIcon key={i} icon={star} />);
      rating--;
    } else if (rating >= 0.5) {
      starRating.push(<IonIcon key={i} icon={starHalf} />);
      rating -= 0.5;
    } else {
      starRating.push(<IonIcon key={i} icon={starOutline} />);
    }
  }
  return (
    <IonPage>
      <IonToast
        isOpen={showSuccessToast}
        message={message.message}
        onDidDismiss={() => setshowSuccessToast(false)}
        duration={5000}
        position="top"
        animated={true}
        color={message.present ? "medium" : "success"}
      ></IonToast>
      <IonHeader>
        <IonToolbar>
          <IonButton onClick={() => history.goBack()} slot="start">
            <IonIcon icon={arrowBack} />
          </IonButton>
          <h1 className="title">{movieDetails.title}</h1>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        {movieDetails && (
          <div className="movie-details">
            <div className="img-wrapper">
              {" "}
              <IonIcon
                icon={bookmarkOutline}
                slot="start"
                color={showSuccessToast ? "success" : "medium"}
                onClick={handleSaveToWatchList}
                className="bookmark"
              />
              <img
                src={`https://image.tmdb.org/t/p/w500/${movieDetails.poster_path}`}
                alt={`${movieDetails.title} poster`}
              />
            </div>
            <div className="text-wrapper">
              <h2>{movieDetails.title}</h2>
              <p className="release-date">
                Release date: {movieDetails.release_date}
              </p>
              <p className="overview">{movieDetails.overview}</p>
              <div className="rating">{starRating}</div>
            </div>
          </div>
        )}
      </IonContent>
    </IonPage>
  );
};

export default MovieDetails;

Enter fullscreen mode Exit fullscreen mode

On the MovieDetails page, the id obtained from useParams is used to fetch movie details and stored in the movieDetails state. Clicking on the bookmark icon calls the handleSaveToWatchList function. This function checks whether the movie has already been saved in the watch list stored in local storage, and adds the new movie to the list if it hasn't been saved yet. If the operation is successful, the IonToast component is triggered by setting showSuccessToast to true.

  • Add a WatchList page in your pages folder and add the code below:
import React, { useEffect, useState } from "react";
import {
  IonButton,
  IonContent,
  IonHeader,
  IonIcon,
  IonPage,
  IonTitle,
  IonToolbar,
} from "@ionic/react";
import MovieCard from "../components/MovieCard";
import { MovieProps } from "../type";
import { arrowBack } from "ionicons/icons";
import { useHistory } from "react-router";

const WatchList: React.FC = () => {
  const history = useHistory();
  const [list, setList] = useState<MovieProps[]>(() => {
    const watchlist: MovieProps[] = localStorage.getItem("watchlist")
      ? JSON.parse(localStorage.getItem("watchlist")!)
      : [];
    return watchlist;
  });

  useEffect(() => {
    const watchlist: MovieProps[] = localStorage.getItem("watchlist")
      ? JSON.parse(localStorage.getItem("watchlist")!)
      : [];
    setList(watchlist);
  }, [localStorage.getItem("watchlist")]);

  const handleClear = () => {
    localStorage.clear();
    setList([]);
  };

  const handleDelete = (
    event: {
      preventDefault: () => void;
      stopPropagation: () => void;
    },
    movie: MovieProps
  ) => {
    event.preventDefault();
    event.stopPropagation();
    const updatedWatchList = list && list.filter((i) => i.id !== movie.id);
    localStorage.setItem("watchlist", JSON.stringify(updatedWatchList));
    setList && setList(updatedWatchList);
  };

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonButton onClick={() => history.goBack()} slot="start">
            <IonIcon icon={arrowBack} />
          </IonButton>
          <IonTitle>Watch List</IonTitle>
        </IonToolbar>
        <IonButton onClick={handleClear}>Clear all</IonButton>
      </IonHeader>
      <IonContent fullscreen>
        <div className="movie-container">
          {list &&
            list.map((movie) => (
              <MovieCard
                key={movie.id}
                movie={movie}
                watch={true}
                handleDelete={handleDelete}
              />
            ))}
        </div>
      </IonContent>
    </IonPage>
  );
};

export default WatchList;

Enter fullscreen mode Exit fullscreen mode

The watch list is initially retrieved from local storage and stored in the watchList state. The list is then mapped and displayed using the movie card component.

  • Add the new pages to the routes inside the App.tsx file:
<Route path="/movies/:id" component={MovieDetails} exact={true} />
<Route path="/watchlist" component={WatchList} exact={true} />

Enter fullscreen mode Exit fullscreen mode

Great job! You have now created a simple movie web and mobile application using the Ionic framework. However, at the moment, the application can only be viewed on a web browser. In the next section, we will cover how to deploy this application on both iOS and Android platforms.

Deploying the Application

To deploy and use this project as a mobile application for android platforms. Follow the steps below;

  • Build the application by running the command below:
ionic build --prod

Enter fullscreen mode Exit fullscreen mode

This will create a production-ready version of the application in the www directory.

  • Deploy to android by running the command below:
ionic capacitor add android

Enter fullscreen mode Exit fullscreen mode

This will add the Android platform to the application.

  • Run the following command to build the application:
ionic capacitor build android --prod

Enter fullscreen mode Exit fullscreen mode

To deploy the application for iOS platforms, follow these steps;

  • Deploy the application by running the following command:
ionic capacitor add ios

Enter fullscreen mode Exit fullscreen mode

This will add the iOS platform to the application.

  • Build the application using the command below:
ionic capacitor build ios --prod

Enter fullscreen mode Exit fullscreen mode

Testing and Simulating on devices

To test the application on devices, you can use Ionic's tools for live-reload and debugging. Simply run the following command in the terminal for android mobiles:

ionic capacitor run android -l --external

Enter fullscreen mode Exit fullscreen mode

or run the command below for iOS devices:

ionic capacitor run ios -l --external

Enter fullscreen mode Exit fullscreen mode

This command will launch the application on your device and enable live-reload and debugging. You can then make changes to the application's code and see the changes immediately on the device. You can either choose to simulate the application on your physical device or use an emulator.

Here is how the application looks on a mobile device.

Mobile app view
Mobile app view

Best Practices

Here are some best practices to follow when deploying Ionic applications:

  • Optimize the application's performance for each platform by using platform-specific optimizations, such as lazy-loading and tree-shaking.
  • Test the application on a wide range of devices and platforms to ensure that it works well for all users.
  • Use Ionic's tools for debugging and profiling to identify and fix performance issues.
  • Use Ionic's built-in UI components and styling to ensure a consistent look and feel across all platforms.

By following these best practices, you can ensure that your application is performant, user-friendly, and accessible to a wide range of users.

Conclusion

Whew! You sure have come a long way. You started out from knowing little to nothing about Ionic to building a whole application using the framework.

Together we covered the basics of creating a new Ionic project, adding pages and components, integrating with APIs, and styling the application. We even discussed how to test and deploy the application on both Android and iOS devices.

Ionic Framework's flexibility and ease-of-use make it an ideal choice for building cross-platform applications quickly and efficiently. I hope you keep up the momentum and continue to build amazing things with Ionic. Who knows, your next project could be the next big thing.

Additional Resources and Recommendations for Further Learning

Here are some recommended resources for further learning:

  1. Ionic Documentation : The official Ionic documentation is a great place to start. It offers a comprehensive overview of the framework's features and provides detailed instructions on how to use them.

  2. Ionic Academy : This website is dedicated to teaching developers how to use Ionic. It offers a wide range of tutorials, courses, and articles on various topics related to the framework.

  3. GitHub : GitHub offers a vast collection of open-source projects using Ionic. You can search for Ionic projects on GitHub and see how other developers have implemented various features and functionalities.

Top comments (2)

Collapse
 
bambadiack profile image
bambadiack

merci beaucoup

Collapse
 
femi_akinyemi profile image
Femi Akinyemi

Thanks for sharing!