Skip to content
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
5 changes: 4 additions & 1 deletion frontend/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
/* config options here */
images: {
remotePatterns: [{ protocol: "https", hostname: "fakestoreapi.com" }],
},
};

export default nextConfig;
30 changes: 17 additions & 13 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
@import "tailwindcss";

:root {
--background: #ffffff;
--foreground: #171717;
--background: #ffffff;
--foreground: #171717;
}

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

html {
padding: 0;
margin: 0;
}

body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;

font-family: Arial, Helvetica, sans-serif;
}
4 changes: 2 additions & 2 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Frontend 3-5years test",
description: "This is a codebility assessment test for frontend developers with 3-5 years of experience.",
};

export default function RootLayout({
Expand Down
210 changes: 111 additions & 99 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,103 +1,115 @@
import Image from "next/image";
"use client";
import ProductCard from "@/components/ProductCard";
import { useEffect, useState } from "react";

export type Product = {
id: number;
title: string;
price: number;
description: string;
category: string;
image: string;
};

export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
const [allProducts, setAllProducts] = useState<Product[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | any>(null);
const [searchTerm, setSearchTerm] = useState("");
const [category, setCategory] = useState("All Categories");

useEffect(() => {
const fetchProducts = async () => {
try {
setIsLoading(true);
const res = await fetch("https://fakestoreapi.com/products");
if (!res.ok) {
throw new Error("Failed to fetch products");
}
const data = await res.json();

setAllProducts(data);
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
fetchProducts();
}, []);

const filteredProducts = allProducts.filter(
(product) =>
product.title.toLowerCase().includes(searchTerm.toLowerCase()) &&
(category === "All Categories" || product.category === category),
);
const categories = [
"All Categories",
...new Set(allProducts.map((product) => product.category)),
];
const handleClearFilters = () => {
setSearchTerm("");
setCategory("All Categories");
};

return (
<div className="flex flex-col items-center justify-items-center min-h-screen pb-20 gap-8 font-[family-name:var(--font-geist-sans)] max-w-screen bg-gray-200">
<header className="bg-white text-black flex w-full h-20 items-center justify-center text-2xl font-bold shadow-lg">
<h1>Fake Store</h1>
</header>
<main className="max-w-[1200px] flex flex-col items-center justify-items-center w-full gap-10 px-8 ">
<div className="flex md:flex-row md:items-center justify-start w-full gap-2 flex-col items-start">
<div>
Search:
<input
type="text"
onChange={(e) => setSearchTerm(e.target.value)}
value={searchTerm}
className="bg-white border p-2 text-black rounded-lg text-sm ml-1"
placeholder="Search product name here..."
/>
</div>
<div className="flex flex-row items-center gap-2">
<p>Filter by:</p>
<select
name="category"
id="category"
value={category}
className="bg-white border p-2 text-black rounded-lg text-sm "
onChange={(e) => setCategory(e.target.value)}
>
{categories.map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
</select>
</div>

<button
className="bg-gray-800 hover:bg-gray-600 text-white py-2 px-4 rounded-xl cursor-pointer"
onClick={handleClearFilters}
>
Clear Search & Filters
</button>
</div>

<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div>
);
{isLoading ? (
<p className="text-2xl font-bold flex items-center justify-center">
Loading...
</p>
) : error ? (
<p className="text-2xl font-bold text-red-500">
Error: {error?.message ?? String(error)}
</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-10">
{filteredProducts.map((product) => (
<ProductCard product={product} key={product.id} />
))}
</div>
)}
</main>
</div>
);
}
30 changes: 30 additions & 0 deletions frontend/src/components/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Product } from "@/app/page";
import React, { useState } from "react";
import Image from "next/image";
import ProductModal from "./ProductModal";

export default function ProductCard({ product }: { product: Product }) {
const [showModal, setShowModal] = useState(false);
return (
<div>
<div
className="border border-gray-300 rounded-lg p-4 bg-white h-full cursor-pointer shadow-lg hover:scale-105 transition-transform duration-300 flex flex-col justify-between"
onClick={() => setShowModal(true)}
>
<div className="h-70 object-fill flex items-center justify-center ">
<Image
src={product.image}
alt={product.title}
width={150}
height={150}
/>
</div>
<h2 className="text-lg font-semibold text-gray-900">{product.title}</h2>
<p className="text-gray-600">${product.price}</p>
</div>
{showModal && (
<ProductModal product={product} setShowModal={setShowModal} />
)}
</div>
);
}
64 changes: 64 additions & 0 deletions frontend/src/components/ProductModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Product } from "@/app/page";
import React from "react";
import Image from "next/image";

export default function ProductModal({
product,
setShowModal,
}: {
product: Product;
setShowModal: React.Dispatch<React.SetStateAction<boolean>>;
}) {
return (
<div
className="fixed inset-0 bg-black/50 h-screen w-screen flex items-center justify-center text-white z-40"
onClick={() => setShowModal(false)}
role="dialog"
>
<div
className="border border-gray-300 rounded-lg bg-gray-200 lg:w-200 flex sm:flex-row gap-2 h-160 flex-col m-1 sm:w-[90%] w-[80%] max-h-[95vh] "
onClick={(e) => e.stopPropagation()}
>
<div className="bg-white p-2 h-full flex items-center justify-center rounded-lg flex-1 relative object-contain ">
<Image
src={product.image}
alt={product.title}

style={{ objectFit: 'contain',padding: '30px' }}
fill
/>
</div>
<div className="p-4 sm:flex-1 flex flex-col gap-3 flex-2 sm:h-150 h-110 max-h-[94vh] ">
<h2 className=" font-semibold text-gray-900 text-xl">
{product.title}
</h2>
<p className="text-gray-600">
{" "}
<span className="font-semibold"> Id: </span> {product.id}
</p>
<p className="text-gray-600 capitalize">
{" "}
<span className="font-semibold"> Category: </span>
{product.category}
</p>
<p className="text-gray-600">
{" "}
<span className="font-semibold"> Price: </span> ${product.price}
</p>
<p className="text-gray-600 h-full overflow-y-auto">
{" "}
<span className="font-semibold"> Description: </span>{" "}
{product.description}
</p>

<button
className="bg-gray-800 hover:bg-gray-500 text-white py-1 px-3 rounded text-sm font-medium w-fit self-end cursor-pointer"
onClick={() => setShowModal(false)}
>
Close
</button>
</div>
</div>
</div>
);
}
Loading