Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
123 commits
Select commit Hold shift + click to select a range
51796c7
solution
May 4, 2026
6e04ffc
solution
May 5, 2026
46d8b8c
Update Node.js version to 20.x in setup
mykhalenych May 13, 2026
7f882f1
Merge pull request #993 from mykhalenych/patch-7
marina-tilniak May 13, 2026
8844bbc
solution
May 17, 2026
349c439
solution
May 17, 2026
fb3007e
solution
May 17, 2026
06a7c02
solution
May 17, 2026
dab0c87
solution
May 17, 2026
1921a82
solution
May 17, 2026
7a7aa7c
solution
May 17, 2026
f62b537
fix: use command parameter for base path in vite config
May 18, 2026
196f68a
solution
May 18, 2026
f20d850
solution
May 18, 2026
9ca6292
solution
May 18, 2026
d14b190
solution
May 18, 2026
d538885
solution
May 18, 2026
17d3d66
solution
May 18, 2026
eedee98
solution
May 18, 2026
d9e8c65
solution
May 18, 2026
0a76045
solution
May 18, 2026
9c1b66b
slution
May 18, 2026
cf64f15
solution
May 18, 2026
f4ab9f7
solution
May 18, 2026
1caaf7a
solution
May 18, 2026
787d5e1
solution
May 18, 2026
240dbef
solution
May 18, 2026
ab13a55
solution
May 18, 2026
7bbd9d1
solution
May 19, 2026
1ba3c86
solution
May 19, 2026
4da4db9
solution
May 19, 2026
1b763eb
solution
May 19, 2026
cba0ee1
solution
May 19, 2026
a1d7abd
solution
May 19, 2026
5938c84
solution
May 19, 2026
af70604
solution
May 19, 2026
44c2508
solution
May 19, 2026
e9ab3c2
solution
May 19, 2026
3fe4aef
solution
May 19, 2026
fe0c908
solution
May 19, 2026
c37d9d5
solution
May 19, 2026
72d3cd5
solution
May 19, 2026
772171e
solution
May 19, 2026
1269cd9
solution
May 19, 2026
38b314d
solution
May 19, 2026
d8bbe00
solution
May 19, 2026
b8e8456
solution
May 19, 2026
e2236cd
solution
May 19, 2026
b818c00
solution
May 19, 2026
f41479f
solution
May 19, 2026
4de6dea
solution
May 19, 2026
9239233
solution
May 19, 2026
251a1d7
solution
May 19, 2026
9c24a9f
solution
May 19, 2026
6f5c2e3
solution
May 19, 2026
f46a6a3
solution
May 19, 2026
ec067bb
solution
May 19, 2026
94c9c27
solution
May 19, 2026
5065e31
solution
May 19, 2026
71cfd72
solution
May 19, 2026
bccc914
solution
May 19, 2026
5402e31
solution
May 19, 2026
3720c8a
solution
May 19, 2026
ba59907
solution
May 19, 2026
b6ce7b6
solution
May 19, 2026
e872996
solution
May 19, 2026
11ea7b4
solution
May 19, 2026
f642ef5
solution
May 19, 2026
e97b807
solution
May 19, 2026
433c2ba
solution
May 19, 2026
6b74f85
solution
May 19, 2026
d614c9e
solution
May 19, 2026
9a133bb
solution
May 19, 2026
cab5cc0
solution
May 19, 2026
0a54e9c
solution
May 19, 2026
57ee233
solution
May 19, 2026
0364dc7
solution
May 19, 2026
4066dbf
solution
May 19, 2026
4bfd1ab
solution
May 19, 2026
d33b9dc
solution
May 19, 2026
0311b0c
solution
May 19, 2026
71727c3
solution
May 19, 2026
fa18057
solution
May 20, 2026
d1f907e
solution
May 20, 2026
75f0701
solution
May 20, 2026
99d8c13
solution
May 20, 2026
881cbe0
solution
May 20, 2026
3d7930c
solution
May 20, 2026
37863d8
solution
May 20, 2026
38cd61c
solution
May 20, 2026
d69bf9b
solution
May 20, 2026
068e61f
solution
May 20, 2026
ca58528
solution
May 20, 2026
a7b686f
solution
May 20, 2026
ad303dc
solution
May 20, 2026
1ea4464
solution
May 20, 2026
979c52d
solution
May 20, 2026
40b7a01
solution
May 20, 2026
146a23e
solution
May 20, 2026
10d31b1
solution
May 20, 2026
0e7bda1
solution
May 20, 2026
6c166b6
solution
May 20, 2026
393177d
solution
May 20, 2026
7fcf1f7
solution
May 20, 2026
5057c2c
solution
May 20, 2026
31d3e38
solution
May 20, 2026
8fb6334
solution
May 20, 2026
199a95e
solution
May 20, 2026
e42fd63
solution
May 20, 2026
a88934c
solution
May 20, 2026
066a443
solution
May 20, 2026
30f23f1
solution
May 20, 2026
f903772
solution
May 20, 2026
a6b11c7
solution
May 20, 2026
deadda9
solution
May 20, 2026
1c5a0c8
solution
May 20, 2026
6844a91
solution
May 20, 2026
e0fc0d4
solution
May 20, 2026
579cdbf
solution
May 20, 2026
c4d8ccd
solution
May 20, 2026
da0dff1
solution
May 20, 2026
9b3e1e1
solution
May 20, 2026
d12cbc4
solution
May 20, 2026
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
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
"@mate-academy/scripts": "^1.8.5",
"@mate-academy/scripts": "^2.1.3",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
Expand Down
28 changes: 27 additions & 1 deletion src/App.scss
Original file line number Diff line number Diff line change
@@ -1 +1,27 @@
// not empty
@use './Components/Home/Home.scss' as *;
@use './Components/TopBar/top-bar.scss' as *;
@use './Components/TopBar/nav.scss' as *;
@use './utils/vars.scss' as *;
@use './utils/mixins.scss' as *;
@use './fonts/fonts.scss' as *;
@use '../src/Components/Header/header.scss' as *;
@use './styles/section.scss' as *;
@use './styles/WelcomeBlock.scss' as *;
@use './styles/icon.scss' as *;
@use './styles/categories.scss' as *;
@use './styles/Page.scss' as *;
@use '../src/Components/Footer/Footer.scss' as *;
@use './Components/ErrorNotification/ErrorNotification.scss' as *;
@use './Components/Aside/Aside.scss' as *;


body {
margin: 0;
background-color: #0F1121;
color: #F1F2F9;
font-family: Mont, sans-serif;
}

.container {
@include content-padding-inline;
}
14 changes: 9 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Outlet } from 'react-router-dom';
import './App.scss';
import React from 'react';

export const App = () => (
<div className="App">
<h1>Product Catalog</h1>
</div>
);
export const App = () => {
return (
<div className="App">
<Outlet />
</div>
);
};
21 changes: 21 additions & 0 deletions src/Components/Accessories/Accessories.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.accessories {
&__path {
display: flex;

gap: 8px;

align-items: center;

margin-top: 25px;


&-phones {
font-family: "Mont SemiBold", sans-serif;

color: #75767F;
font-weight: 700;
font-size: 12px;
line-height: 100%;
}
}
}
169 changes: 169 additions & 0 deletions src/Components/Accessories/Accessories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/* eslint-disable max-len */
import { Link } from 'react-router-dom';
import { Filter } from '../Filter/Filter';
import { Header } from '../Header/header';
import ArrowGray from '../../images/icons/ChevronGray.svg';
import Home from '../../images/icons/Home.svg';
import './Accessories.scss';
import { getAccessories } from '../../api/api';
import { useEffect, useState } from 'react';
import { Accessorie } from '../../types/Accessories';
import fav from '../../images/fav/Icons/Favourites (Heart Like).svg';
import activeFav from '../../images/icons/ActiveFav.svg';
import { Products } from '../../types/Products';
import { useCart } from '../../Context/Context';
import { useFav } from '../../Context/FavouritesContext';

export const Accessories = () => {
const [accessories, setAccessories] = useState<Accessorie[]>([]);
const [, setLoading] = useState(true);
const [, setErrorMessage] = useState('');
const { totalQuantity, addToCart, removeFromCart, isInCart } = useCart();
const { addToFav, removeFromFav, isInFav } = useFav();

const mapAccessorieToProduct = (accessorie: Accessorie): Products => ({
id: String(accessorie.id),
itemId: String(accessorie.id),
name: accessorie.name,
category: accessorie.category,
fullPrice: Number(accessorie.priceRegular),
price: Number(accessorie.priceDiscount || accessorie.priceRegular),
screen: accessorie.screen,
capacity: accessorie.capacity,
color: accessorie.color || accessorie.colorsAvailable?.[0] || '—',
ram: accessorie.ram,
year: new Date().getFullYear(),
image:
typeof accessorie.images === 'string'
? accessorie.images
: (accessorie.images?.[0] ?? '/img/placeholder.png'),
});

useEffect(() => {
setLoading(true);
setErrorMessage('');

getAccessories()
.then(setAccessories)
.catch(() => setErrorMessage(`Couldn't load any tablets`))
.finally(() => setLoading(false));
}, []);

return (
<div className="tablets">
<Header cartItemsCount={totalQuantity} />
<div className="container">
<div className="accessories__path page__path">
<div className="accessories__path-icon">
<Link to="/" className="accessories__icon-link page__icon-link">
<img
className="accessories__icon-home page__icon icon-home icon"
src={Home}
alt="Home"
/>
<img
className="accessories__icon-home icon-home icon"
src={ArrowGray}
alt="Arrow"
/>
</Link>
</div>
<span className="accessories__path-phones page__paths">
Accessories
</span>
</div>
<div className="Accessories__text page__text">
<p className="Accessories__title page__title">Accessories</p>
<span className="accessories__subtitle page__subtitle">
100 models
</span>
</div>
<Filter />
<div className="tablets__content">
<section className="page__models section">
<div className="page__models-tablet tablet__articles page__grid">
{accessories.map(accessorie => {
const product = mapAccessorieToProduct(accessorie);
const added = isInCart(product.id);
const addedFav = isInFav(product.id);

return (
<article key={accessorie.id} className="page__models-phone">
<div className="page__models-container">
<div className="page__models-img">
<img
src={product.image}
alt=""
className="page__models-image"
/>
</div>
<p className="page__models-title">{accessorie.name}</p>
<span className="page__models-price">
{accessorie.priceRegular}$
</span>
<div className="page__models-string"></div>
<div className="page__models-info">
<p className="page_models-text page__models-text__first">
Screen{' '}
<span className="page__models-span">
{accessorie.screen}
</span>
</p>
<p className="page__models-text">
Capacity{' '}
<span className="page__models-span">
{accessorie.capacity}
</span>
</p>
<p className="page__models-text">
RAM{' '}
<span className="page__models-span">
{accessorie.ram}
</span>
</p>
</div>
<div className="page__models-buttons">
<button
className={`page__models-cart ${added ? 'page__models-cart-active' : ''}`}
type="button"
onClick={() =>
added
? removeFromCart(product.id)
: addToCart(product)
}
>
<p className="page__models-cart__text">
{added ? 'Added' : 'Add to cart'}
</p>
</button>
<button
className="page__models-fav"
type="button"
onClick={() =>
addedFav
? removeFromFav(product.id)
: addToFav(product)
}
>
<img
className="page__models-fav__img"
src={addedFav ? activeFav : fav}
alt="Favourites"
/>
</button>
</div>
</div>
</article>
);
})}
</div>
</section>
</div>
</div>

{/* <div className="page__footer">
<Footer />
</div> */}
</div>
);
};
69 changes: 69 additions & 0 deletions src/Components/Aside/Aside.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
@use '../../utils/mixins.scss' as *;

.favourites {
overflow: hidden;
box-sizing: border-box;
height: 100vh;
background-color: #0F1121;

&__top-bar {
padding-left: 20px;
box-shadow: 0 1px #323542;
}

&__container {
padding-left: 20px;
max-width: 1136px;
}

&__top {
position: sticky;
top: 0;
z-index: 1;
}

&__footer {
position: fixed;
bottom: 1.7px;
left: 0;
right: 0;
}

&__title {
margin: 0;

font-family: Mont, sans-serif;
font-weight: 800;
letter-spacing: -1%;
font-size: 32px;
line-height: 41px;

@include on-tablet {
font-size: 48px;
line-height: 56px;
}
}

&__back{

&-text {
margin: 0;
font-family: 'Mont regular', sans-serif;
font-weight: 700;
font-size: 12px;
line-height: 100%;

color: #F1F2F9;
}

&-button {
display: flex;
align-items: center;
gap: 4px;

margin-top: 40px;
margin-bottom: 15px;

text-decoration: none;
}}
}
28 changes: 28 additions & 0 deletions src/Components/Aside/Aside.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Link } from 'react-router-dom';
import { Footer } from '../Footer/Footer';
import { TopBar } from '../TopBar/TopBar';

const arrowLeft = new URL(
'../../images/icons/Chevron (Arrow Left).svg',
import.meta.url,
).href;

export const Aside = () => {
return (
<aside className="favourites page__favourites" id="favourites">
<div className="favourites__top-bar">
<TopBar cartItemsCount={0} />
</div>
<div className="favourites__container">
<Link to="/Home" className="favourites__back-button">
<img src={arrowLeft} alt="Arrow Left" />
<p className="favourites__back-text">Back</p>
</Link>
<p className="favourites__title">Cart</p>
</div>
<div className="favourites__footer">
<Footer />
</div>
</aside>
);
};
12 changes: 12 additions & 0 deletions src/Components/ErrorNotification/ErrorNotification.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@use '../../utils/mixins.scss' as *;

.error-notification {
@include content-padding-inline;

&__title {
font-family: Mont, sans-serif;
font-weight: 800;
font-size: 48px;
line-height: 56px;
}
}
10 changes: 10 additions & 0 deletions src/Components/ErrorNotification/ErrorNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import '../../../public/img/page-not-found.png';

export const ErrorNotification = () => {
return (
<div className="error-notification">
<p className="error-notification__title">Page Not Found</p>
<img src="../../../public/img/page-not-found.png" alt="" />
</div>
);
};
Loading
Loading