Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

solved task #11

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"cSpell.words": ["Gorrion"]
}
27 changes: 19 additions & 8 deletions app/api/products/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
/**
* TODO: Prepare an endpoint to return a list of products
* The endpoint should return a pagination of 10 products per page
* The endpoint should accept a query parameter "page" to return the corresponding page
*/
import { NextRequest, NextResponse } from "next/server";
import { fetchProducts } from "@/lib/products";
import { productsPerPage } from "@/app/constants/main";

export async function GET() {
return Response.json([]);
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page") || "1");
const searchQuery = String(searchParams.get("query") || "");
const numberOfProductsPerPage = Number(
searchParams.get("perPage") || productsPerPage
);

const { products, totalNumberOfProducts } = await fetchProducts(
page,
numberOfProductsPerPage,
searchQuery
);

return NextResponse.json({ products, totalNumberOfProducts });
}
37 changes: 37 additions & 0 deletions app/components/number-of-products.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { productsPerPageRevalidate } from "./products-revalidate";

const NOProductsSelect = () => {
const searchParams = useSearchParams();
const { replace } = useRouter();
const pathname = usePathname();
const params = new URLSearchParams(searchParams);

function handleSelect(e: string) {
params.set("perPage", e);
params.set("page", "1");
productsPerPageRevalidate();
replace(`${pathname}?${params.toString()}`);
}

return (
<div className="flex max-w-sm mx-auto">
<select
name="productsNumber"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
defaultValue={params.get("perPage") || 10}
onChange={(e) => handleSelect(e.target.value)}
>
<option value="5">5</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
);
};

export default NOProductsSelect;
76 changes: 76 additions & 0 deletions app/components/page-change-handler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { productsPerPage } from "../constants/main";
import { useEffect, useState } from "react";

interface ButtonProps {
buttonText: string;
buttonAction: string;
totalNumberOfProducts: number;
}

const ChangePageButton = ({
buttonText,
buttonAction,
totalNumberOfProducts,
}: ButtonProps) => {
const searchParams = useSearchParams();
const { replace } = useRouter();
const pathname = usePathname();
const params = new URLSearchParams(searchParams);
const numberOfProductsPerPage = Number(
params.get("perPage") || productsPerPage
);

const [isButtonDisabled, setIsButtonDisabled] = useState(false);

const currentPage = Number(params.get("page"));
const maxPage = Math.ceil(totalNumberOfProducts / numberOfProductsPerPage);

const pageHandler = () => {
if (buttonAction === "next") {
if (currentPage === 0) {
params.set("page", "2");
} else if (currentPage >= maxPage) {
params.set("page", String(currentPage));
} else {
params.set("page", String(currentPage + 1));
}
} else if (buttonAction === "prev") {
if (currentPage === 1 || currentPage === 0) {
params.set("page", "1");
} else {
params.set("page", String(currentPage - 1));
}
}
replace(`${pathname}?${params.toString()}`);
};

useEffect(() => {
if (
(buttonAction === "next" && currentPage >= maxPage) ||
(buttonAction === "prev" && (currentPage === 1 || currentPage === 0))
) {
setIsButtonDisabled(true);
} else {
setIsButtonDisabled(false);
}
}, [buttonAction, currentPage, maxPage]);

return (
<>
<button
aria-disabled={isButtonDisabled && true}
tabIndex={isButtonDisabled ? -1 : undefined}
onClick={pageHandler}
className={`${
isButtonDisabled ? "pointer-events-none" : ""
} relative ml-3 inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold ring-1 ring-inset ring-gray-300 hover:bg-gray-50 hover:text-black focus-visible:outline-offset-0`}
>
{buttonText}
</button>
</>
);
};

export default ChangePageButton;
6 changes: 6 additions & 0 deletions app/components/products-revalidate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"use server";
import { revalidatePath } from "next/cache";

export async function productsPerPageRevalidate() {
revalidatePath("/products");
}
66 changes: 66 additions & 0 deletions app/components/search-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client";

import { usePathname, useSearchParams, useRouter } from "next/navigation";
import { useDebouncedCallback } from "use-debounce";

const SearchBar = () => {
const searchParams = useSearchParams();
const { replace } = useRouter();
const pathname = usePathname();
const params = new URLSearchParams(searchParams);

// function handleSearch(term: string) {
// if (term) {
// params.set("query", term);
// params.set("page", "1");
// } else {
// params.delete("query");
// }
// replace(`${pathname}?${params.toString()}`);
// }

const handleSearch = useDebouncedCallback((term: string) => {
if (term) {
params.set("query", term);
params.set("page", "1");
} else {
params.delete("query");
}
replace(`${pathname}?${params.toString()}`);
}, 200);

return (
<div className="max-w-md mx-auto">
<div className="relative">
<div className="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg
className="w-4 h-4 text-gray-500 dark:text-gray-400"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 20 20"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
/>
</svg>
</div>
<input
type="search"
className="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Search by name"
onChange={(e) => {
handleSearch(e.target.value);
}}
defaultValue={searchParams.get("query")?.toString()}
/>
</div>
</div>
);
};

export default SearchBar;
1 change: 1 addition & 0 deletions app/constants/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const productsPerPage = 10;
14 changes: 14 additions & 0 deletions app/products/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Link from "next/link";
import React from "react";

const notFoundPage = () => {
return (
<div>
{`Sorry, we couldn't find what are you looking for. `}
{/* normal anchor tag instead of <Link> bcs next couldn't handle that and we need to rerender page for some reason*/}
<Link href={"/products"}>Go back to product page</Link>
</div>
);
};

export default notFoundPage;
Loading