A quick guide to building your own mobile search engine using Next.js
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
Create the project in the following way:
Your project will look like this.
Now as you can see, open the terminal and run
npm run dev
Open your browser and search http://localhost:3000/
Run the follow command in the terminal.
npm install @emotion/react @emotion/styled @mui/material react-alice-carousel react-icons
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;
}
}
Now create twofolders: utils, and components, just like mentioned in the below image.
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
Now set serverActions to true in next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
}
}
module.exports = nextConfig
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;
}
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>
);
}
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
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
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
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
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 />
</>
)
}
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 />
</>
)
}
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
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
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)