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
12 changes: 11 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import {Route, Routes} from 'react-router'
import React from 'react';
import Home from './pages/Home';
import ProductPage from './pages/ProductPage';
import NotFound from './pages/NotFound';

const App: React.FC = () => {
return (
<Home />
<>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products/:id" element={<ProductPage />} />
<Route path="*" element={<NotFound />} />
<Route path ="/category/:categoryName" element={<Home />} />
</Routes>
</>
);
};

Expand Down
72 changes: 72 additions & 0 deletions src/assets/css/product-page.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@

.product-page {
padding: 2rem 0;
}

.product-page__grid {
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 2rem;
align-items: flex-start;
}

/* Image column */
.product-page__image-wrapper {
display: flex;
justify-content: center;
align-items: center;
}

.product-page__image {
max-width: 100%;
height: auto;
object-fit: contain;
}

/* Info column */
.product-page__info {
display: flex;
flex-direction: column;
gap: 1rem;
}

.product-page__title {
font-size: 2rem;
margin: 0;
}

.product-page__category span {
font-weight: 600;
}

.product-page__price-row {
display: flex;
align-items: center;
gap: 1rem;
}

.product-page__price {
font-size: 1.5rem;
font-weight: 700;
}

.product-page__rating {
font-size: 0.9rem;
color: #777;
}

.product-page__description {
line-height: 1.5;
}

.product-page__actions {
display: flex;
gap: 1rem;
}

/* Responsive: stack on small screens */
@media (max-width: 768px) {
.product-page__grid {
grid-template-columns: 1fr;
}
}
21 changes: 10 additions & 11 deletions src/components/product-components/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import React from 'react';
import { Product } from '../../types';
import {Product} from '../../types';
import { Navigate, useNavigate } from 'react-router-dom';

interface CardProps {
}

const Card: React.FC<CardProps> = () => {
const Card: React.FC<Product> = ({category,description,id,image,price,title,rating:{rate,count}}) => {
const navigate = useNavigate();

return (
<div className='product scale-effect'>
<div className='product scale-effect' onClick={() => navigate(`/products/${id}`)} key={id}>
<div className='product-image'>
<img />
<img src={image} alt={title} />
</div>
<div className='product-info'>
<h2 className='product-title'></h2>
<h2 className='product-title'>{title}</h2>
<div className='product-brief'>
<p><strong>Price: </strong></p>
<p><strong>Rating: </strong></p>
<p><strong>Category: </strong></p>
<p><strong>Price: </strong>{price}</p>
<p><strong>Rating: </strong>{rate}</p>
<p><strong>Category: </strong>{category}</p>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui-elements/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const Navbar: React.FC = () => {
<div className="container flex flex-jc-sb flex-ai-c">
<div className="logo">
<a href="/">
<h1>Products</h1>
<h1>Products-axios</h1>
</a>
</div>
<Button
Expand Down
6 changes: 5 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import './index.css'
import App from './App'

const container = document.getElementById('root');
if (container) {
createRoot(container).render(

<StrictMode>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)
}
60 changes: 53 additions & 7 deletions src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,65 @@ import DropDownFilter from '../components/filters/DropDownFilter';
import Loading from '../components/ui-elements/Loading';
import Card from '../components/product-components/Card';
import { Product } from '../types';
import { useParams, useNavigate} from 'react-router-dom';

const Home: React.FC = () => {
const { categoryName } = useParams<{ categoryName?: string }>();
const navigate = useNavigate();

const [allProducts, setAllProducts] = useState<Product[]>([]);
const [products, setProducts] = useState<Product[]>([]);
const [isDropdownOpen, setIsDropdownOpen] = useState<boolean>(false);
const [selectedCategory, setSelectedCategory] = useState<string>('All');
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);



useEffect(() =>{
const fetchProducts = async () => {
try {
const response = await axios.get('https://fakestoreapi.com/products');
console.log(response.data);
setAllProducts(response.data);
setProducts(response.data);
setLoading(false);
}
catch(error:any) {
setError(error.message)
setLoading(false);
}
}

//TODO - useEffect + axios
fetchProducts();
}, []);

useEffect(() => {
if (!allProducts.length) return;

const urlCategory = categoryName ?? 'All';
setSelectedCategory(urlCategory);

if (urlCategory === 'All') {
setProducts(allProducts);
} else {
setProducts(
allProducts.filter((product) => product.category === urlCategory)
);
}
}, [allProducts, categoryName]);

const showFilterByCategory = () => {
setIsDropdownOpen(!isDropdownOpen);
};

// TODO - filter products by category - BONUS
const filterByCategory = (category: string) => {
setSelectedCategory(category);
//TODO - filter products by category

if (category === 'All') {
navigate('/');
} else {
navigate(`/category/${category}`);
}
};

if (loading) {
Expand All @@ -39,7 +79,6 @@ const Home: React.FC = () => {
</div>
);
}

return (
<>
<Navbar />
Expand All @@ -56,8 +95,15 @@ const Home: React.FC = () => {
<main className='main'>
<div className='container'>
<section>
<div className='products-grid' id='products-list'>
{/* TODO - map products */}
<div
className='products-grid'
id='products-list'>
{products.map((product: Product) => (
<Card key={product.id}
{...product}
/>
))}

</div>
</section>
</div>
Expand Down
16 changes: 16 additions & 0 deletions src/pages/NotFound.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Navbar from "../components/ui-elements/Navbar";

function NotFound() {
return (
<>
<Navbar />
<div>
<h1>404 - Page Not Found</h1>
<p>Sorry, the page you are looking for does not exist.</p>
</div>
</>
);
}

export default NotFound;

93 changes: 93 additions & 0 deletions src/pages/ProductPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, {useEffect, useState} from 'react';
import axios from 'axios';
import '../assets/css/common.css';
import '../assets/css/main.css';
import '../assets/css/product-page.css';
import Navbar from '../components/ui-elements/Navbar';
import Loading from '../components/ui-elements/Loading';
import { useParams } from 'react-router-dom';
import { Product } from '../types';


const ProductPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const fetchProduct = async () => {
try {
const response = await axios.get(`https://fakestoreapi.com/products/${id}`);
console.log(response.data);
setProduct(response.data);
setLoading(false);
}
catch(error:any) {
setError(error.message)
setLoading(false);
}
}
fetchProduct();
}, [id]);
if (loading) {
return <Loading />;
}
if (error || !product) {
return (
<div className='error-screen'>
<p>Error: {error}</p>
</div>
);
}
return (
<>
<Navbar />
<main className="main">
<div className="container">
<section className="product-page">
<div className="product-page__grid">

<div className="product-page__image-wrapper">
<img
src={product.image}
alt={product.title}
className="product-page__image"
/>
</div>


<div className="product-page__info">
<h1 className="product-page__title">{product.title}</h1>

<p className="product-page__category">
Category: <span>{product.category}</span>
</p>

<div className="product-page__price-row">
<span className="product-page__price">${product.price}</span>
{product.rating && (
<span className="product-page__rating">
⭐ {product.rating.rate} ({product.rating.count} reviews)
</span>
)}
</div>

<p className="product-page__description">
{product.description}
</p>

<div className="product-page__actions">
<button className="btn btn-primary">Add to Cart</button>
<button className="btn btn-secondary">Buy Now</button>
</div>
</div>
</div>
</section>
</div>
</main>
</>
);
};

export default ProductPage;
21 changes: 16 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
export interface Rating {
//TODO
}
// export interface Rating {
// count: number;
// rate: number;
// }

export interface Product {
//TODO
title: string;
category: string;
description: string;
id: string;
image: string;
price: number;
rating:{
rate: number;
count: number;
}
}

export interface Category {
//TODO
id: string;
name: string;
}