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

Solution Rafał Fikus #8

Open
wants to merge 8 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
71 changes: 62 additions & 9 deletions app/api/products/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,62 @@
/**
* 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
*/

export async function GET() {
return Response.json([]);
}
import { mergeProductsLists } from "@/lib/products";
import { NextRequest, NextResponse } from "next/server";
import path from "path";

export async function GET(req: NextRequest) {
try {
// get page number from path params
const page = req.nextUrl.searchParams.get("page");
const pageNumber = page ? parseInt(page as string, 10) : 1;

// get path of json and csv products files
const jsonProductsPath = path.join(process.cwd(), "products.json");
const csvProductsPath = path.join(process.cwd(), "products.csv");

// merge json and csv products
const mergedProducts = await mergeProductsLists(
jsonProductsPath,
csvProductsPath
);

// sort merged products
mergedProducts.sort((product1, product2) =>
product1.id.localeCompare(product2.id)
);

// set items per page
const ITEMS_PER_PAGE = 10;

// count pagination pages amount
const pageCount = Math.ceil(mergedProducts.length / ITEMS_PER_PAGE);

// calculate the starting and ending indexes for elements displayed on one page
const startIndex = (pageNumber - 1) * ITEMS_PER_PAGE;
let endIndex = startIndex + ITEMS_PER_PAGE;

// correct the logic for the last page if is incomplete (receives less items than ITEMS_PER_PAGE)
if (endIndex > mergedProducts.length) {
endIndex = mergedProducts.length;
}

// slice list to get only items needed for visited website path
const paginatedProducts = mergedProducts.slice(startIndex, endIndex);

// count total products amount
const totalProductsAmount = mergedProducts.length;

const respondeData = {
paginatedProducts,
startIndex,
endIndex,
pageCount,
totalProductsAmount,
};

return NextResponse.json(respondeData);
} catch (err) {
return NextResponse.json(
{ message: "Error happened while trying to fetch products!" },
{ status: 500 }
);
}
}
19 changes: 19 additions & 0 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const CustomErrorPage = () => {
return (
<div className="flex items-center justify-center w-full h-screen px-16 bg-gray-200 md:px-0">
<div className="flex flex-col items-center justify-center px-4 py-8 bg-white border border-gray-200 rounded-lg shadow-2xl md:px-8 lg:px-24">
<p className="text-6xl font-bold tracking-wider text-gray-300 md:text-7xl lg:text-9xl">
404
</p>
<p className="mt-4 text-2xl font-bold tracking-wider text-gray-500 md:text-3xl lg:text-5xl">
Oops!
</p>
<p className="pb-4 mt-4 text-center text-gray-500 border-b-2">
Page not found...
</p>
</div>
</div>
);
};

export default CustomErrorPage;
10 changes: 8 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ export default function Home() {
<main
className={`flex min-h-screen flex-col place-content-center place-items-center p-24 ${inter.className}`}
>
<div className="flex place-items-center">
<div className="flex flex-col items-center justify-center p-8 shadow-2xl ">
<h1 className="text-4xl font-bold text-center">
Summer Camp 2024 Recruitment Task
</h1>
<h2 className="text-2xl mt-2">by Rafał Fikus</h2>
</div>
<div className="flex place-items-center mt-12">
<Link href="/products">Click here to see a list of products</Link>
<Link
href="/products"
className="p-6 text-lg text-white transition bg-blue-500 hover:bg-blue-600"
>
Click here to see a list of products
</Link>
</div>
</main>
);
Expand Down
131 changes: 32 additions & 99 deletions app/products/page.tsx
Original file line number Diff line number Diff line change
@@ -1,107 +1,40 @@
import { Product } from "@/lib/products";
import ProductsPagination from "@/components/ProductsPagination";
import ProductsTable from "@/components/ProductsTable";
import { getProducts } from "@/lib/products";
import { notFound } from "next/navigation";

async function getProducts(): Promise<Product[]> {
const res = await fetch("http://localhost:3000/api/products");
return res.json();
}
export default async function Products({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined };
}) {
const currentPage = Number(searchParams?.page ?? 1);
const {
paginatedProducts,
startIndex,
endIndex,
pageCount,
totalProductsAmount,
} = await getProducts(currentPage);

export default async function Products() {
/* TODO: Create an endpoint that returns a list of products, and use that here.
*/
const products = await getProducts();
// handling invalid page parameters (non-numeric or out of range)
if (isNaN(currentPage) || currentPage < 1 || currentPage > pageCount) {
notFound();
}

return (
<div className="mx-auto max-w-7xl">
<div className="mt-8 flow-root">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<table className="min-w-full divide-y divide-gray-700">
<thead>
<tr>
<th
scope="col"
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-white sm:pl-0"
>
ID
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-white"
>
Name
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-white"
>
Price
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-white"
>
Quantity
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-white"
>
Contains alcohol
</th>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-0">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{products.map((product) => (
<tr key={product.id}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-white sm:pl-0">
{product.id}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-300">
{product.name}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-300">
{product.price} {product.currency}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-300">
{product.quantity}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-300">
{product.isAlcohol ? "Yes" : "No"}
</td>
</tr>
))}
</tbody>
</table>
{/* TODO: Pagination */}
<nav
className="flex items-center justify-between py-3"
aria-label="Pagination"
>
<div className="hidden sm:block">
<p className="text-sm">
Showing <span className="font-medium">1</span> to{" "}
<span className="font-medium">1</span> of{" "}
<span className="font-medium">N</span> results
</p>
</div>
<div className="flex flex-1 justify-between sm:justify-end">
<a
href="#"
className="relative 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 focus-visible:outline-offset-0"
>
Previous
</a>
<a
href="#"
className="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 focus-visible:outline-offset-0"
>
Next
</a>
</div>
</nav>
<div className="mt-8 mx-4 lg:mx-0 flow-root">
<div className="-my-2 lg:-mx-8">
<div>
<ProductsTable paginatedProducts={paginatedProducts} />
<ProductsPagination
currentPage={currentPage}
startIndex={startIndex}
endIndex={endIndex}
pageCount={pageCount}
totalProductsAmount={totalProductsAmount}
/>
</div>
</div>
</div>
Expand Down
56 changes: 56 additions & 0 deletions components/ProductsPagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Link from "next/link";

interface ProductsPaginationProps {
currentPage: number;
startIndex: number;
endIndex: number;
pageCount: number;
totalProductsAmount: number;
}

const ProductsPagination = ({
currentPage,
startIndex,
endIndex,
pageCount,
totalProductsAmount,
}: ProductsPaginationProps) => {
return (
<nav
className="flex items-center justify-between py-3"
aria-label="Pagination"
>
<div className="hidden sm:block">
<p className="text-sm">
Showing <span className="font-medium">{startIndex + 1}</span> to{" "}
<span className="font-medium">{endIndex}</span> of{" "}
<span className="font-medium">{totalProductsAmount}</span> results
</p>
</div>
<div className="flex flex-1 justify-between sm:justify-end ">
<Link
href={`/products?page=${currentPage - 1}`}
aria-disabled={currentPage <= 1}
tabIndex={currentPage <= 1 ? -1 : undefined}
className={`relative 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 focus-visible:outline-offset-0 ${
currentPage <= 1 && "pointer-events-none opacity-50"
}`}
>
Previous
</Link>
<Link
href={`/products?page=${currentPage + 1}`}
aria-disabled={currentPage >= pageCount}
tabIndex={currentPage >= pageCount ? -1 : undefined}
className={`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 focus-visible:outline-offset-0 ${
currentPage >= pageCount && "pointer-events-none opacity-50"
}`}
>
Next
</Link>
</div>
</nav>
);
};

export default ProductsPagination;
74 changes: 74 additions & 0 deletions components/ProductsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Product } from "@/types";

interface ProductsTableProps {
paginatedProducts: Product[];
}

const ProductsTable = ({ paginatedProducts }: ProductsTableProps) => {
return (
<div className="overflow-x-auto">
<table className="w-full divide-y divide-gray-700">
<thead>
<tr>
<th
scope="col"
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-red-400 sm:pl-0"
>
ID
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-red-400"
>
Name
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-red-400"
>
Price
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-red-400"
>
Quantity
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-red-400"
>
Contains alcohol
</th>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-0">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{paginatedProducts?.map((product) => (
<tr key={product.id}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-black sm:pl-0">
{product.id}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-700">
{product.name}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-700">
{product.price} {product.currency}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-700">
{product.quantity}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-700">
{product.isAlcohol ? "Yes" : "No"}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};

export default ProductsTable;
Loading