Skip to content

Commit

Permalink
feat(products): add logic for filtering and pagination to product list
Browse files Browse the repository at this point in the history
  • Loading branch information
tyronejosee committed Sep 25, 2024
1 parent e2ef1e4 commit 24e0000
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 61 deletions.
55 changes: 33 additions & 22 deletions frontend/src/app/(routes)/products/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Metadata } from "next";
import { getCategories, getProducts } from "@/lib/api";
import { getBrands, getCategories, getProducts } from "@/lib/api";
import {
HeaderPage,
PaginationItem,
Expand All @@ -20,38 +20,49 @@ export const metadata: Metadata = {
};

export default async function ProductsPage({ searchParams }: Props) {
const { sort_by, search, category, price } = searchParams;
const { sort_by, search, category, brand, page = 1 } = searchParams;

const params = {
...(sort_by && { sort_by }),
...(search && { search }),
...(category && { category }),
...(price && { price }),
...(brand && { brand }),
page,
};

const [products, categories] = await Promise.all([
const [productsData, brands, categories] = await Promise.all([
getProducts(params),
getBrands(),
getCategories(),
]);

const { results: products, count } = productsData;

const totalPages = Math.ceil(count / 10);

return (
<>
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 md:px-8 mt-16">
<HeaderPage title="Products" />
<>
<ProductToolbar categories={categories} />
{products.length === 0 ? (
<p className="text-center text-gray-500">
<main className="max-w-screen-xl mx-auto px-4 sm:px-6 md:px-8 mt-16 mb-10">
<HeaderPage title="Products" />
<>
<ProductToolbar brands={brands} categories={categories} />
{/* !TODO: Add component */}
{products.length === 0 ? (
<section className=" relative flex justify-center items-center w-full h-[400px] rounded-xl overflow-hidden">
<div className="z-10 text-center text-gray-500">
No products found for{" "}
<span className="font-semibold text-primary">{search}</span>
</p>
) : (
<>
<ProductList products={products} />
<PaginationItem />
</>
)}
</>
</div>
</>
<span className="ml-2 font-semibold text-primary">
{search?.toUpperCase()}
</span>
</div>
<div className="absolute z-5 w-full h-full bg-neutral-darkgrey animate-pulse"></div>
</section>
) : (
<>
<PaginationItem totalPages={totalPages} currentPage={page} />
<ProductList products={products} />
</>
)}
</>
</main>
);
}
3 changes: 1 addition & 2 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { NextUIProvider } from "@nextui-org/react";
import "../styles/globals.css";
import { mainFont } from "@/config/fonts";
import { getCompany } from "@/lib/api";
import { MenuBar, Footer, BackToTop, DataProvider } from "@/components";
import { MenuBar, Footer, DataProvider } from "@/components";

export const metadata: Metadata = {
title: "Atlanta Ink - Tattoo Studio",
Expand All @@ -25,7 +25,6 @@ export default async function RootLayout({
<MenuBar />
{children}
<Footer />
<BackToTop />
</DataProvider>
</NextUIProvider>
</body>
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
PromotionSection,
ServiceSection,
TattooSection,
BackToTop,
} from "@/components";
import {
getArtists,
Expand All @@ -29,7 +30,7 @@ export const metadata: Metadata = {
};

export default async function HomePage() {
const [services, tattoos, artists, prices, products, faqs] =
const [services, tattoos, artists, prices, productsData, faqs] =
await Promise.all([
getServices(),
getTattoos(),
Expand All @@ -39,6 +40,8 @@ export default async function HomePage() {
getFaqs(),
]);

const { results: products } = productsData;

return (
<main>
<HeroSection />
Expand All @@ -51,6 +54,7 @@ export default async function HomePage() {
<FAQSection faqs={faqs} />
<LocationSection />
<FinalCTASection />
<BackToTop />
</main>
);
}
29 changes: 15 additions & 14 deletions frontend/src/components/products/product-list/ProductList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect } from "react";
import { useEffect, useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { motion, useAnimation } from "framer-motion";
Expand All @@ -15,15 +15,17 @@ interface Props {
export const ProductList = ({ products }: Props) => {
const controls = useAnimation();
const { ref, inView } = useInView({
triggerOnce: true,
triggerOnce: false,
threshold: 0.1,
});

useEffect(() => {
if (inView) {
controls.start("visible");
} else {
controls.start("hidden");
}
}, [inView, controls]);
}, [inView, controls, products]);

const itemVariants = {
hidden: { opacity: 0, x: 0 },
Expand All @@ -38,35 +40,34 @@ export const ProductList = ({ products }: Props) => {
{products.map((product, index) => (
<motion.div
key={product.id}
className="bg-neutral-darkgrey rounded-xl p-2 group"
className="group"
variants={itemVariants}
initial="hidden"
animate={controls}
animate={inView ? "visible" : "hidden"}
transition={{
duration: 0.3,
delay: index * 0.1,
ease: "easeOut",
}}
>
<Link
key={product.id}
href={`/products/${product.slug}`}
className="bg-neutral-darkgrey shadow-lg rounded-xl overflow-hidden"
>
<Link href={`/products/${product.slug}`}>
<figure className="w-full h-60 relative rounded-lg overflow-hidden">
<Image
src={product.image || DEFAULT_IMAGE}
alt={product.name}
fill
style={{ objectFit: "cover" }}
className="object-cover rounded-b-xl"
className="object-cover transform transition-transform duration-300 group-hover:scale-110"
/>
</figure>
<div className="p-4">
<h3 className="text-lg font-semibold pb-2 mb-2 border-b border-b-neutral-gray group-hover:border-b-primary line-clamp-2">
<div className="pt-4">
<h3 className="line-clamp-2 group-hover:font-bold">
{product.name}
</h3>
<span>{product.price}</span>
<p className="text-neutral-gray line-clamp-1">{product.brand}</p>
<p className="text-xl font-extrabold text-primary">
${product.price}
</p>
</div>
</Link>
</motion.div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { useState, useEffect, useCallback } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Input, Select, SelectItem } from "@nextui-org/react";
import { SORT_CHOICES } from "@/config/constants";
import { ICategory } from "@/interfaces";
import { IBrand, ICategory } from "@/interfaces";

interface Props {
brands: IBrand[];
categories: ICategory[];
}

export const ProductToolbar = ({ categories }: Props) => {
export const ProductToolbar = ({ brands, categories }: Props) => {
const router = useRouter();
const searchParams = useSearchParams();
const [search, setSearch] = useState(searchParams.get("search") || "");
Expand Down Expand Up @@ -57,12 +58,20 @@ export const ProductToolbar = ({ categories }: Props) => {
[debounceTimeout, router],
);

const handleChange = (value: string) => {
const handleBrandChange = (value: string) => {
router.push(`/products?brand=${value}`);
};

const handleCategoryChange = (value: string) => {
router.push(`/products?category=${value}`);
};

const handleSortChange = (value: string) => {
router.push(`/products?sort_by=${value}`);
};

return (
<aside className="z-20 pb-8">
<aside className="pb-8">
<nav className="flex space-x-4">
<Input
label="Search"
Expand All @@ -71,13 +80,25 @@ export const ProductToolbar = ({ categories }: Props) => {
size="sm"
value={search}
onChange={handleSearchChange}
className="w-full"
className="w-1/4"
/>
<Select
size="sm"
label="Select brand"
className="w-1/4"
onChange={(e) => handleBrandChange(e.target.value)}
>
{brands.map((brand) => (
<SelectItem key={brand.id} value={brand.id}>
{brand.name}
</SelectItem>
))}
</Select>
<Select
size="sm"
label="Select category"
className="w-60"
onChange={(e) => handleChange(e.target.value)}
className="w-1/4"
onChange={(e) => handleCategoryChange(e.target.value)}
>
{categories.map((category) => (
<SelectItem key={category.id} value={category.id}>
Expand All @@ -88,8 +109,8 @@ export const ProductToolbar = ({ categories }: Props) => {
<Select
size="sm"
label="Sort by"
className="w-60"
onChange={(e) => handleChange(e.target.value)}
className="w-1/4"
onChange={(e) => handleSortChange(e.target.value)}
>
{SORT_CHOICES.map((choices) => (
<SelectItem key={choices.key} value={choices.key}>
Expand Down
8 changes: 1 addition & 7 deletions frontend/src/components/ui/footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
"use client";

import { usePathname } from "next/navigation";
import { HR } from "@/components";
import { useCompanyStore } from "@/store";
import { HR } from "@/components";

export const Footer = () => {
const { companyData } = useCompanyStore();
const pathname = usePathname();

if (pathname.startsWith("/products")) {
return null;
}

return (
<footer className="mt-auto bg-neutral-darkgrey">
Expand Down
24 changes: 20 additions & 4 deletions frontend/src/components/ui/pagination/PaginationItem.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
"use client";

import { useRouter } from "next/navigation";
import { Pagination } from "@nextui-org/react";

export const PaginationItem = () => {
interface PaginationProps {
totalPages: number;
currentPage: number;
}

export const PaginationItem = ({
totalPages,
currentPage,
}: PaginationProps) => {
const router = useRouter();

const handlePageChange = (page: number) => {
router.push(`?page=${page}`);
};

return (
<nav className="mt-auto flex justify-center items-center py-4">
<nav className="flex justify-end items-center pb-4">
<Pagination
loop
showControls
color={"primary"}
total={5}
initialPage={1}
total={totalPages}
initialPage={currentPage}
onChange={handlePageChange}
/>
</nav>
);
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/interfaces/product.interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export interface IBrand {
id: string;
name: string;
}

export interface ICategory {
id: string;
name: string;
Expand All @@ -8,6 +13,7 @@ export interface IProduct {
name: string;
slug: string;
sku: string;
brand: string;
description?: string;
price: number;
currency: string;
Expand All @@ -23,5 +29,7 @@ export interface IProductQueryParams {
sort_by?: string;
search?: string;
category?: string;
price?: string;
brand?: string;
page?: number;
page_size?: number;
}
6 changes: 5 additions & 1 deletion frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function getProducts(params?: {
sort_by?: string;
search?: string;
category?: string;
price?: string;
brand?: string;
}) {
return fetchData("products", params);
}
Expand All @@ -65,6 +65,10 @@ export async function getProduct(slug: string) {
return fetchData(`products/${slug}`);
}

export async function getBrands() {
return fetchData("brands");
}

export async function getCategories() {
return fetchData("categories");
}
Expand Down

0 comments on commit 24e0000

Please sign in to comment.