DEV Community

Cover image for Build a Mobile Search Engine using Next.js in 10 minutes.
Abhirup Kumar Bhowmick
Abhirup Kumar Bhowmick

Posted on

Build a Mobile Search Engine using Next.js in 10 minutes.

A quick guide to building your own mobile search engine using Next.js

Mobile Search Engine

Have you gone through all of NextJS 14’s features? It has evolved significantly and is now better than ever. If you’re having trouble building a website using Next.js 14, you’ve come to the right spot. In this article, I’m going to show you how to use Next.js 14 with Tailwind CSS to create a completely responsive mobile search engine.

Project: https://mobwiki.vercel.app

Github: https://github.com/abhirupkumar/mobwiki

Getting Started with Next.Js 14

The first step will be to create a new project. The new project should be created in a directory, so open a terminal and create or go to that directory. Run the following command in a terminal once you’re there to start the project.

    npx create-next-app@latest mobwiki
Enter fullscreen mode Exit fullscreen mode

Create the project in the following way:

Installing Next.js Guide

Your project will look like this.

Sample Code

Now as you can see, open the terminal and run

npm run dev
Enter fullscreen mode Exit fullscreen mode

Open your browser and search http://localhost:3000/

Sample Page

Run the follow command in the terminal.

    npm install @emotion/react @emotion/styled @mui/material react-alice-carousel react-icons
Enter fullscreen mode Exit fullscreen mode

Now that the setup part is complete, let’s start building the website.

Let the game begin

First, delete all the contents of global.css and paste this code there.

    @tailwind base;
    @tailwind components;
    @tailwind utilities;

    .prodimg-border{
      border-top-left-radius: 15px;
      border-top-right-radius: 15px;
    }

    .prod-sideimg{
      box-shadow: 0px 0px 2px;
      border-radius: 10px;
    }

    .prod-shadow {
      box-shadow: 0px 0px 2.5px;
      border-radius: 15px;
    }

    .lds-ripple {
      display: inline-block;
      position: relative;
      width: 80px;
      height: 80px;
    }
    .lds-ripple div {
      position: absolute;
      border: 4px solid #858585;
      opacity: 1;
      border-radius: 50%;
      animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
    }
    .lds-ripple div:nth-child(2) {
      animation-delay: -0.5s;
    }
    @keyframes lds-ripple {
      0% {
        top: 36px;
        left: 36px;
        width: 0;
        height: 0;
        opacity: 0;
      }
      4.9% {
        top: 36px;
        left: 36px;
        width: 0;
        height: 0;
        opacity: 0;
      }
      5% {
        top: 36px;
        left: 36px;
        width: 0;
        height: 0;
        opacity: 1;
      }
      100% {
        top: 0px;
        left: 0px;
        width: 72px;
        height: 72px;
        opacity: 0;
      }
    }
Enter fullscreen mode Exit fullscreen mode

Now create twofolders: utils, and components, just like mentioned in the below image.

Format of all the folders

And also create an assets folder inside the public folder. This folder will contain some images. And the utils folder will contain the data.json file, which contains all the data about different mobiles.

Download Link: https://github.com/abhirupkumar/mobwiki-starter-utils

Format of the folder

Now set serverActions to true in next.config.js

    /** @type {import('next').NextConfig} */
    const nextConfig = {
        experimental: {
            serverActions: true,
        }
    }

    module.exports = nextConfig
Enter fullscreen mode Exit fullscreen mode

Now create an action.js file in the app folder. It will contain all the server functions that we will be using, such as handleSearch and SortDataByTag. The function handleSearch will be used to redirect to the mentioned query, and SortDataByTag will be used to sort the data based on the tag given.

    "use server";

    import { redirect } from "next/navigation";

    export async function handleSearch(formData) {
        const value = formData.get("search");
        redirect(`/search?query=${value}`);
    }

    export async function handleData(dataJson, val, pageNos, order, tag) {
        const search_value = val.toLowerCase().split(" ");
        var newData = [];
        for (let i = 0; i < dataJson.length; i++) {
            let chk = true;
            for (let j = 0; j < search_value.length; j++) {
                if (!dataJson[i].name.toLowerCase().includes(search_value[j])) {
                    chk = false;
                    break;
                }
            }
            if (chk) {
                newData.push(dataJson[i]);
            }
        }

        if (order != null) {
            newData = await SortDataByTag(order, tag, newData);
        }

        let page = 1;
        if (pageNos != null) page = parseInt(pageNos);

        const productsPerPage = 15;
        const startIndex = (page - 1) * productsPerPage;
        const endIndex = startIndex + productsPerPage;
        const totalPages = Math.ceil(newData.length / productsPerPage);

        const data = newData.slice(startIndex, endIndex);

        return { data, page, totalPages }
    }

    export async function SortDataByTag(order, tag, data) {
        if (order === "asc") {
            data.sort((a, b) => {
                if (tag === "price") {
                    let pa = a.price.replace('.', "").replace(',', "").replace(' ', "")
                    let intpa = parseInt(pa);
                    let pb = b.price.replace('.', "").replace(',', "").replace(' ', "")
                    let intpb = parseInt(pb);
                    return intpa - intpb;
                }
                else {
                    let sa = parseFloat(a.stars)
                    let sb = parseFloat(b.stars)
                    return sa - sb;
                }
            })
        }
        else {
            data.sort((a, b) => {
                if (tag === "price") {
                    let pa = a.price.replace('.', "").replace(',', "").replace(' ', "")
                    let intpa = parseInt(pa);
                    let pb = b.price.replace('.', "").replace(',', "").replace(' ', "")
                    let intpb = parseInt(pb);
                    return intpb - intpa;
                }
                else {
                    let sa = parseFloat(a.stars)
                    let sb = parseFloat(b.stars)
                    return sb - sa;
                }
            })
        }

        return data;
    }
Enter fullscreen mode Exit fullscreen mode

Now, create Header.js file in the components folder and paste the following code.

    "use client";
    import Link from "next/link";
    import React, { useState } from "react";

    export default function Header() {
        const [navbarOpen, setNavbarOpen] = useState(false);

        return (
            <div className="fixed top-0 w-full z-30 clearNav bg-opacity-90 transition duration-300 ease-in-out bg-white">
                <div className="flex flex-col max-w-6xl px-4 mx-auto md:items-center md:justify-between md:flex-row md:px-6 lg:px-8">
                    <div className="flex flex-row items-center justify-between p-4">
                        <Link
                            href="/"
                            className="text-lg font-semibold rounded-lg tracking-widest focus:outline-none focus:shadow-outline"
                        >
                            <h1 className="text-4xl Avenir tracking-tighter text-gray-900 md:text-4x1 lg:text-3xl">
                                MOBWiKi
                            </h1>
                        </Link>
                        <button
                            className="text-white cursor-pointer leading-none px-3 py-1 md:hidden outline-none focus:outline-none "
                            type="button"
                            aria-label="button"
                            onClick={() => setNavbarOpen(!navbarOpen)}
                        >
                            <svg
                                xmlns="http://www.w3.org/2000/svg"
                                width="24"
                                height="24"
                                viewBox="0 0 24 24"
                                fill="none"
                                stroke="#191919"
                                strokeWidth="2"
                                strokeLinecap="round"
                                strokeLinejoin="round"
                                className="feather feather-menu"
                            >
                                <line x1="3" y1="12" x2="21" y2="12"></line>
                                <line x1="3" y1="6" x2="21" y2="6"></line>
                                <line x1="3" y1="18" x2="21" y2="18"></line>
                            </svg>
                        </button>
                    </div>
                    <div
                        className={
                            "md:flex flex-grow items-center" +
                            (navbarOpen ? " flex" : " hidden")
                        }
                    >
                        <nav className="flex-col flex-grow ">
                            <ul className="flex flex-grow justify-end flex-wrap items-center">
                                <li>
                                    <Link
                                        href="/"
                                        className="font-medium text-gray-600 hover:text-gray-900 px-5 py-3 flex items-center transition duration-150 ease-in-out"
                                    >
                                        Mobiles
                                    </Link>
                                </li>
                            </ul>
                        </nav>
                    </div>
                </div>
            </div>
        );
    }
Enter fullscreen mode Exit fullscreen mode

Create the MobileSlider.js file in the components folder. This will be our mobile slider. We will be using Alice Caraousel to build this slider.

    "use client";

    import React, { useEffect, useState } from 'react';
    import AliceCarousel from 'react-alice-carousel';
    import "react-alice-carousel/lib/alice-carousel.css";
    import dataJson from '../utils/data.json';
    import Link from 'next/link';
    import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri'
    import { AiFillStar } from 'react-icons/ai'

    const responsive = {
        2000: {
            items: 5,
        },
        1316: {
            items: 4,
        },
        880: {
            items: 3,
        },
        300: {
            items: 2,
        },
        0: {
            items: 1,
        }
    };

    const MobileSlider = () => {

        const [data, setData] = useState([])
        useEffect(() => {
            setData(dataJson)
        }, [])

        const renderNextButton = ({ isDisabled }) => {
            return <RiArrowRightSLine className="cursor-pointer h-10 w-10 absolute right-0 top-[45%]" />
        };

        const renderPrevButton = ({ isDisabled }) => {
            return <RiArrowLeftSLine className="cursor-pointer h-10 w-10 absolute left-0 top-[45%]" />
        };

        return (
            <div className="flex flex-wrap w-full my-20 flex-col items-center text-center">
                {data.length > 0 ? <AliceCarousel
                    responsive={responsive}
                    mouseTracking
                    infinite
                    controlsStrategy={"default"}
                    autoPlayStrategy='all'
                    autoPlayInterval={1000}
                    disableDotsControls
                    keyboardNavigation
                    style={{
                        width: "100%",
                        justifyContent: "center",
                    }}
                    renderPrevButton={renderPrevButton}
                    renderNextButton={renderNextButton}>
                    {data.length > 0 && data.map((index, item) => {
                        const rawstars = data[item]?.stars
                        const stars = rawstars.substring(0, rawstars.indexOf(" "));
                        return <div key={index} className="lg:w-[310px] md:w[250px] prod-shadow  lg:h-auto cursor-pointer m-2">
                            <Link href={data[item]?.url} rel="noopener noreferrer" target="_blank">
                                <div className="flex justify-center md:h-[380px] h-[200px] relative overflow-hidden">
                                    <img alt="ecommerce" className="m-auto md:m-0 md:h-[380px] h-[200px] prodimg-border block" src={data[item]?.image_src} loading='lazy' />
                                </div>
                                <div className="text-center mx-[10px] md:text-justify flex flex-col lg:h-[195px] h-[162px] justify-evenly">
                                    <h3 className="text-gray-500 mx-auto text-xs tracking-widest title-font">AMAZON</h3>
                                    <h2 className="text-gray-900 mx-auto text-left title-font lg:text-[15px] text-[0.63rem] font-medium">{data[item]?.name}</h2>
                                    <div className="mt-1 flex space-x-8">
                                        <p className="text-left text-black font-semibold md:text-base text-xs">₹ {data[item]?.price}</p>
                                        <div className="flex items-center space-x-2">
                                            <p className="text-black items-center flex font-semibold md:text-base text-xs">{stars} <AiFillStar className="text-yellow-400" /></p>
                                            <div className="text-sm font-medium text-gray-900 md:block hidden underline hover:no-underline dark:text-white">{data[item]?.rating}</div>
                                        </div>
                                    </div>
                                </div>
                            </Link>
                        </div>
                    })}
                </AliceCarousel>
                    :
                    <div className="lds-ripple">
                        <div></div>
                        <div></div>
                    </div>}
            </div>
        )
    }

    export default MobileSlider
Enter fullscreen mode Exit fullscreen mode

Create the SliderPage.js file in the components folder.

    "use client";

    import React from 'react';
    import AliceCarousel from 'react-alice-carousel';
    import "react-alice-carousel/lib/alice-carousel.css";
    import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';

    const SliderPage = () => {

        const renderNextButton = ({ isDisabled }) => {
            return <RiArrowRightSLine className="cursor-pointer h-12 w-12 absolute right-0 top-[45%]" />
        };

        const renderPrevButton = ({ isDisabled }) => {
            return <RiArrowLeftSLine className="cursor-pointer h-12 w-12 absolute left-0 top-[45%]" />
        };

        return (
            <div className='max-w-[1300px] w-full mx-auto'>
                <AliceCarousel
                    autoPlay={true}
                    playButtonEnabled={true}
                    infinite={true}
                    autoPlayInterval={4000}
                    renderPrevButton={renderPrevButton}
                    renderNextButton={renderNextButton}>
                    <img alt="banner1" src="./assets/img1.jpg" className='rounded-xl' loading="lazy" />
                    <img alt="banner2" src="./assets/img2.jpg" className='rounded-xl' loading="lazy" />
                    <img alt="banner3" src="./assets/img3.jpg" className='rounded-xl' loading="lazy" />
                    <img alt="banner4" src="./assets/img4.jpg" className='rounded-xl' loading="lazy" />
                </AliceCarousel>

            </div>
        )
    }

    export default SliderPage
Enter fullscreen mode Exit fullscreen mode

Create the PagenationPart.js file in the components folder. This is only the pagination part.

    "use client";

    import { Pagination } from '@mui/material'
    import { useRouter } from 'next/navigation';
    import React from 'react'

    const PagenationPart = ({ query, page, totalPages }) => {

        const router = useRouter()

        const handlePageChange = (event, value) => {
            router.push(`/search?query=${query}&page=${value}`)
        }

        return (
            <div className='my-10'>
                <Pagination
                    count={totalPages}
                    page={page}
                    onChange={handlePageChange}
                    variant="outlined" color="primary"
                />
            </div>
        )
    }

    export default PagenationPart
Enter fullscreen mode Exit fullscreen mode

Create the Table.js file in the components folder.

    "use client"

    import Link from 'next/link';
    import { useRouter } from 'next/navigation';
    import React, { useEffect, useState } from 'react'
    import { AiFillStar } from 'react-icons/ai';
    import { RiArrowDownSFill, RiArrowUpSFill } from 'react-icons/ri';

    const Table = ({ query, page, data }) => {

        const router = useRouter();

        const handleSort = (order, tag) => {
            var newlink = `/search?query=${query}`
            if (page != null)
                newlink += `&page=${page}`
            newlink += `&order=${order}&tag=${tag}`
            router.push(newlink);
        }

        return (
            <div className="overflow-x-auto md:px-12 px-2">
                <table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
                    <thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
                        <tr>
                            <th scope="col" className="px-6 py-3 w-[10rem]">
                                Image
                            </th>
                            <th scope="col" className="px-6 py-3 w-[22rem]">
                                Product Name
                            </th>
                            <th scope="col" className="px-6 py-3">
                                Site
                            </th>
                            <th scope="col" className="px-6 py-3">
                                <div className="flex justify-center items-center">
                                    Star
                                    <div className='flex flex-col items-center'>
                                        <button className='text-[16px] -mb-[10px]' onClick={() => handleSort("asc", "star")}>
                                            <RiArrowUpSFill value="asc-price" />
                                        </button>
                                        <button className='text-[16px]' onClick={() => handleSort("desc", "star")} >
                                            <RiArrowDownSFill value="desc-price" />
                                        </button>
                                    </div>
                                </div>
                            </th>
                            <th scope="col" className="px-6 py-3">
                                <div className="flex justify-center items-center">
                                    Price
                                    <div className='flex flex-col items-center'>
                                        <button className='text-[16px] -mb-[10px]' onClick={() => handleSort("asc", "price")}>
                                            <RiArrowUpSFill value="asc-price" />
                                        </button>
                                        <button className='text-[16px]' onClick={() => handleSort("desc", "price")} >
                                            <RiArrowDownSFill value="desc-price" />
                                        </button>
                                    </div>
                                </div>
                            </th>
                        </tr>
                    </thead>
                    <tbody>
                        {data.length > 0 && data.map((index, item) => {
                            const rawstars = data[item]?.stars
                            const stars = rawstars.substring(0, rawstars.indexOf(" "));
                            return <tr key={data[item]?.url} className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
                                <td className="pr-6 py-2">
                                    <a href={data[item]?.url} rel="noopener noreferrer" target="_blank">
                                        <img alt={`image-${index + 1}`} className="" src={data[item]?.image_src} loading='lazy' />
                                    </a>
                                </td>
                                <td className="px-6 py-2 text-black font-semibold">
                                    <a href={data[item]?.url} rel="noopener noreferrer" target="_blank">
                                        {data[item]?.name}
                                    </a>
                                </td>
                                <td className="px-6 py-2 text-black font-semibold">
                                    AMAZON
                                </td>
                                <td className="px-6 py-2">
                                    <div className='flex items-center space-x-1 text-black font-semibold'>
                                        <p className='font-medium'>{stars}</p>
                                        <AiFillStar className="text-yellow-400" />
                                    </div>
                                </td>
                                <td className="px-6 py-2 text-black font-semibold">
                                    ₹ {data[item].price}
                                </td>
                            </tr>
                        })}
                    </tbody>
                </table>
            </div>
        )
    }

    export default Table
Enter fullscreen mode Exit fullscreen mode

Wow! We have built a lot of components. Now let’s get onto assembling them by adding them to the page file in the app directory. Change the code of the page.js file to this.

    import MobileSlider from '@/components/MobileSlider'
    import SliderPage from '@/components/SliderPage'
    import { AiOutlineSearch } from 'react-icons/ai'
    import { handleSearch } from './action';

    export default function Home() {

      return (
        <>
          <form className="flex flex-1 mt-10 justify-end items-center lg:mx-10 mx-2" action={handleSearch}>
            <input className='border-2 border-gray-300 bg-white h-12 px-5 lg:w-[50vw] w-[80vw] pr-16 flex rounded-xl text-sm focus:outline-none' type="search" name="search" placeholder="Search for any mobile..." />
            <button type="submit" className="absolute mx-2 text-xl bg-blue-800 p-2 rounded-xl text-white flex">
              <AiOutlineSearch />
            </button>
          </form>
          <MobileSlider />
          <SliderPage />
        </>
      )
    }
Enter fullscreen mode Exit fullscreen mode

And add this to your layout.js file.

    import MobileSlider from '@/components/MobileSlider'
    import SliderPage from '@/components/SliderPage'
    import { AiOutlineSearch } from 'react-icons/ai'
    import { handleSearch } from './action';

    export default function Home() {

      return (
        <>
          <form className="flex flex-1 mt-10 justify-end items-center lg:mx-10 mx-2" action={handleSearch}>
            <input className='border-2 border-gray-300 bg-white h-12 px-5 lg:w-[50vw] w-[80vw] pr-16 flex rounded-xl text-sm focus:outline-none' type="search" name="search" placeholder="Search for any mobile..." />
            <button type="submit" className="absolute mx-2 text-xl bg-blue-800 p-2 rounded-xl text-white flex">
              <AiOutlineSearch />
            </button>
          </form>
          <MobileSlider />
          <SliderPage />
        </>
      )
    }
Enter fullscreen mode Exit fullscreen mode

Create a search folder in app directory and add loading.js in that folder.

    import React from 'react'

    const loading = () => {
        return (
            <div className="lds-ripple">
                <div></div>
                <div></div>
            </div>
        )
    }

    export default loading
Enter fullscreen mode Exit fullscreen mode

Add page.js to the search file. When the user searches for something, this page is shown, where all the mobile phones as per his search are shown.

    import React from 'react'
    import { AiFillStar, AiOutlineSearch } from 'react-icons/ai';
    import { handleData, handleSearch } from '../action';
    import dataJson from '../../utils/data.json';
    import Table from '@/components/Table';
    import PagenationPart from '@/components/PagenationPart';

    const Search = async ({ searchParams }) => {
        const { data, page, totalPages } = await handleData(dataJson, searchParams.query ?? "", searchParams.page ?? null, searchParams.order ?? null, searchParams.tag ?? null)

        return (
            <div className='flex flex-col w-full justify-center items-center'>
                <form className="flex mt-10 justify-center items-center lg:mx-10 mx-2" action={handleSearch}>
                    <div className="flex justify-end items-center">
                        <input className='border-2 border-gray-300 bg-white h-12 md:w-[30rem] w-[20rem] px-5 flex-1 pr-16 flex rounded-xl text-sm focus:outline-none' type="search" name="search" placeholder="Search for any mobile..." />
                        <button type="submit" className="absolute mx-2 text-xl bg-blue-800 p-2 rounded-xl text-white flex">
                            <AiOutlineSearch />
                        </button>
                    </div>
                </form>
                {data.length > 0 && <PagenationPart query={searchParams.query ?? ""} page={page} totalPages={totalPages} />}
                <Table query={searchParams.query ?? ""} page={page} data={data} />
                {data.length > 0 && <PagenationPart page={page} totalPages={totalPages} />}

            </div>
        )
    }

    export default Search
Enter fullscreen mode Exit fullscreen mode

Search Page

Congratulations your project is complete now. You can host your website for free on Netlify or Vercel. You may even host it on Hostinger, Digital Ocean, or any other premium service if you desire.

Conclusion

You may, of course, modify the code to make more unique websites. Next includes an extensive list of features that make frontend and backend development simple. I hope you enjoyed my blog and learned something new.

Guys, if you enjoyed this, please give it a clap and share it with your friends who also want to learn and implement Next.js. And if you want more articles like this, follow me on dev.to . If you missed anything or want to check out the full code here. Thanks for reading!

Top comments (0)