diff --git a/db.json b/db.json
new file mode 100644
index 0000000..9e8ff29
--- /dev/null
+++ b/db.json
@@ -0,0 +1,70 @@
+{
+ "products": [
+ {
+ "id": "1",
+ "name": "Breed Dry Dog Food",
+ "price": 100,
+ "imageUrl": "./src/assets/images/products/dog-food.svg"
+ },
+ {
+ "id": "2",
+ "name": "CANON EOS DSLR Camera",
+ "price": 360,
+ "imageUrl": "./src/assets/images/products/camera.svg"
+ },
+ {
+ "id": "3",
+ "name": "ASUS FHD Gaming Laptop",
+ "price": 700,
+ "imageUrl": "./src/assets/images/products/laptop.svg"
+ },
+ {
+ "id": "4",
+ "name": "Curology Product Set ",
+ "price": 500,
+ "imageUrl": "./src/assets/images/products/curoset.svg"
+ },
+ {
+ "id": "5",
+ "name": "Kids Electric Car",
+ "price": 960,
+ "imageUrl": "./src/assets/images/products/car.svg"
+ },
+ {
+ "id": "6",
+ "name": "Jr. Zoom Soccer Cleats",
+ "price": 1160,
+ "imageUrl": "./src/assets/images/products/soccer.svg"
+ },
+ {
+ "id": "7",
+ "name": "GP11 Shooter USB Gamepad",
+ "price": 660,
+ "imageUrl": "./src/assets/images/products/gamepad.svg"
+ },
+ {
+ "id": "8",
+ "name": "Quilted Satin Jacket",
+ "price": 660,
+ "imageUrl": "./src/assets/images/products/jacket.svg"
+ }
+ ],
+ "cart": [
+ {
+ "id": "f7d5",
+ "productId": "1",
+ "name": "Breed Dry Dog Food",
+ "price": 100,
+ "imageUrl": "./src/assets/images/products/dog-food.svg",
+ "quantity": 3
+ },
+ {
+ "id": "bb06",
+ "productId": "2",
+ "name": "CANON EOS DSLR Camera",
+ "price": 360,
+ "imageUrl": "./src/assets/images/products/camera.svg",
+ "quantity": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/assets/icons/cart.svg b/src/assets/icons/cart.svg
new file mode 100644
index 0000000..cb051ca
--- /dev/null
+++ b/src/assets/icons/cart.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/icons/loop.svg b/src/assets/icons/loop.svg
new file mode 100644
index 0000000..77862ef
--- /dev/null
+++ b/src/assets/icons/loop.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/trash.svg b/src/assets/icons/trash.svg
new file mode 100644
index 0000000..2c6f001
--- /dev/null
+++ b/src/assets/icons/trash.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/user.svg b/src/assets/icons/user.svg
new file mode 100644
index 0000000..5b9803a
--- /dev/null
+++ b/src/assets/icons/user.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/icons/wish.svg b/src/assets/icons/wish.svg
new file mode 100644
index 0000000..001ccbf
--- /dev/null
+++ b/src/assets/icons/wish.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/wishlist.svg b/src/assets/icons/wishlist.svg
new file mode 100644
index 0000000..8eab1b8
--- /dev/null
+++ b/src/assets/icons/wishlist.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/products/camera.svg b/src/assets/images/products/camera.svg
new file mode 100644
index 0000000..729188c
--- /dev/null
+++ b/src/assets/images/products/camera.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/products/car.svg b/src/assets/images/products/car.svg
new file mode 100644
index 0000000..a12ad22
--- /dev/null
+++ b/src/assets/images/products/car.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/products/curoset.svg b/src/assets/images/products/curoset.svg
new file mode 100644
index 0000000..bfacd10
--- /dev/null
+++ b/src/assets/images/products/curoset.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/products/dog-food.svg b/src/assets/images/products/dog-food.svg
new file mode 100644
index 0000000..81aaf78
--- /dev/null
+++ b/src/assets/images/products/dog-food.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/products/gamepad.svg b/src/assets/images/products/gamepad.svg
new file mode 100644
index 0000000..54d90ed
--- /dev/null
+++ b/src/assets/images/products/gamepad.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/products/jacket.svg b/src/assets/images/products/jacket.svg
new file mode 100644
index 0000000..e1b9d52
--- /dev/null
+++ b/src/assets/images/products/jacket.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/products/laptop.svg b/src/assets/images/products/laptop.svg
new file mode 100644
index 0000000..4401a4d
--- /dev/null
+++ b/src/assets/images/products/laptop.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/products/soccer.svg b/src/assets/images/products/soccer.svg
new file mode 100644
index 0000000..a565a9a
--- /dev/null
+++ b/src/assets/images/products/soccer.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/styles/_variables.scss b/src/assets/styles/_variables.scss
new file mode 100644
index 0000000..95ae2a3
--- /dev/null
+++ b/src/assets/styles/_variables.scss
@@ -0,0 +1,4 @@
+$white-color: white;
+$black-color: black;
+
+$primary-color: #db4444;
diff --git a/src/components/OrderDetail.jsx b/src/components/OrderDetail.jsx
new file mode 100644
index 0000000..022edcb
--- /dev/null
+++ b/src/components/OrderDetail.jsx
@@ -0,0 +1,41 @@
+import styles from './OrderDetail.module.scss'
+import { useCart } from '../hooks/useCart'
+
+const OrderDetail = () => {
+ const { cart } = useCart()
+
+ const subtotal = cart.reduce(
+ (acc, item) => acc + item.price * item.quantity,
+ 0
+ )
+ const discount = subtotal * 0.2
+ const delivery = 15
+ const total = subtotal - discount + delivery
+
+ return (
+
+
Order Summary
+
+
+
Subtotal
+
${subtotal}
+
+
+
Discount (-20%)
+
-${discount}
+
+
+
Delivery Fee
+
${delivery}
+
+
+
+
+
+ )
+}
+
+export default OrderDetail
diff --git a/src/components/OrderDetail.module.scss b/src/components/OrderDetail.module.scss
new file mode 100644
index 0000000..47b0691
--- /dev/null
+++ b/src/components/OrderDetail.module.scss
@@ -0,0 +1,67 @@
+@use '../assets/styles/variables' as *;
+
+.detail {
+ border: 1px solid gainsboro;
+ border-radius: 20px;
+
+ flex-basis: 50%;
+ margin-top: 24px;
+ padding: 20px;
+
+ display: flex;
+ flex-direction: column;
+
+ .stats {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ margin-top: 24px;
+ padding-bottom: 20px;
+
+ border-bottom: 1px solid gainsboro;
+ div {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ p {
+ color: gray;
+ font-size: 20px;
+ }
+
+ span {
+ font-size: 20px;
+ font-weight: bold;
+ }
+ }
+ }
+
+ .total {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ p {
+ color: gray;
+ font-size: 20px;
+ }
+
+ span {
+ font-size: 20px;
+ font-weight: bold;
+ margin-top: 20px;
+ }
+ }
+
+ button {
+ outline: 0;
+ border: 0;
+ font-size: 16px;
+ padding: 20px;
+ border-radius: 62px;
+ background-color: $primary-color;
+ color: $white-color;
+ cursor: pointer;
+ margin-top: 24px;
+ }
+}
diff --git a/src/components/card/cart/index.jsx b/src/components/card/cart/index.jsx
new file mode 100644
index 0000000..87fb14d
--- /dev/null
+++ b/src/components/card/cart/index.jsx
@@ -0,0 +1,41 @@
+import styles from './index.module.scss'
+import TrashIcon from '../../../assets/icons/trash.svg'
+import { useCart } from '../../../hooks/useCart'
+
+const CartItem = ({ item }) => {
+ const { removeFromCart, updateQuantity } = useCart()
+
+ const handleIncrement = () => {
+ updateQuantity(item.id, item.quantity + 1)
+ }
+
+ const handleDecrement = () => {
+ if (item.quantity > 1) {
+ updateQuantity(item.id, item.quantity - 1)
+ }
+ }
+
+ return (
+
+
+
+

+
+
+
{item.name}
+
+
${item.price}
+
+
+ {item.quantity}
+
+
+
+
+
+ )
+}
+
+export default CartItem
diff --git a/src/components/card/cart/index.module.scss b/src/components/card/cart/index.module.scss
new file mode 100644
index 0000000..01362b0
--- /dev/null
+++ b/src/components/card/cart/index.module.scss
@@ -0,0 +1,62 @@
+.item {
+ border-bottom: 1px solid #dcdcdc;
+ padding: 24px 0;
+ position: relative;
+
+ .trash {
+ outline: 0;
+ border: 0;
+ background-color: white;
+ position: absolute;
+
+ top: 20px;
+ right: 20px;
+
+ cursor: pointer;
+ }
+
+ .image {
+ background-color: #f5f5f5;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 35px;
+ height: 124px;
+ width: 124px;
+
+ img {
+ width: 90px;
+ object-fit: contain;
+ }
+ }
+
+ .wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .price {
+ font-size: 24px;
+ }
+
+ .buttons {
+ display: flex;
+ background-color: #f0f0f0;
+ padding: 12px 20px;
+ gap: 20px;
+ border-radius: 62px;
+ align-items: center;
+
+ span {
+ font-size: 14px;
+ }
+
+ button {
+ outline: 0;
+ border: 0;
+ font-size: 16px;
+ cursor: pointer;
+ }
+ }
+ }
+}
diff --git a/src/components/card/product/index.jsx b/src/components/card/product/index.jsx
new file mode 100644
index 0000000..f7210cd
--- /dev/null
+++ b/src/components/card/product/index.jsx
@@ -0,0 +1,33 @@
+import styles from './index.module.scss'
+import WishIcon from '../../../assets/icons/wish.svg'
+import { useCart } from '../../../hooks/useCart'
+
+const ProductCard = ({ id, name, price, imageUrl }) => {
+ const { addToCart } = useCart()
+
+ const handleAdd = () => {
+ addToCart({ id, name, price, imageUrl })
+ }
+
+ return (
+
+
+

+
+
+
+
+
+
+
{name}
+
+ ${price}
+
+
+
+ )
+}
+
+export default ProductCard
diff --git a/src/components/card/product/index.module.scss b/src/components/card/product/index.module.scss
new file mode 100644
index 0000000..d4013a3
--- /dev/null
+++ b/src/components/card/product/index.module.scss
@@ -0,0 +1,48 @@
+@use '../../../assets/styles/variables' as *;
+
+.product {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ width: 270px;
+
+ .image {
+ background-color: #f5f5f5;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 35px;
+ height: 250px;
+ position: relative;
+
+ .buttons {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+
+ button {
+ display: flex;
+ outline: 0;
+ border: 0;
+ border-radius: 50%;
+ padding: 10px;
+ background-color: white;
+
+ cursor: pointer;
+ }
+ }
+ }
+
+ .content {
+ h4 {
+ font-size: 16px;
+ }
+
+ .stats {
+ span {
+ font-size: 16px;
+ color: $primary-color;
+ }
+ }
+ }
+}
diff --git a/src/components/header/index.jsx b/src/components/header/index.jsx
new file mode 100644
index 0000000..01f4c5a
--- /dev/null
+++ b/src/components/header/index.jsx
@@ -0,0 +1,57 @@
+import styles from './index.module.scss'
+
+import CartIcon from '../../assets/icons/cart.svg'
+import UserIcon from '../../assets/icons/user.svg'
+import WishlistIcon from '../../assets/icons/wishlist.svg'
+import LoopIcon from '../../assets/icons/loop.svg'
+import { Link } from 'react-router-dom'
+import { useCart } from '../../hooks/useCart'
+
+const Header = () => {
+ const { cart, loading, error } = useCart()
+ return (
+
+ Exclusive
+
+
+
+

+
+
+
+
+
+
+
+ )
+}
+
+export default Header
diff --git a/src/components/header/index.module.scss b/src/components/header/index.module.scss
new file mode 100644
index 0000000..3595d58
--- /dev/null
+++ b/src/components/header/index.module.scss
@@ -0,0 +1,85 @@
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid gainsboro;
+ padding: 30px 0;
+
+ h2 {
+ font-size: 24px;
+ }
+
+ ul {
+ list-style: none;
+ display: flex;
+ gap: 48px;
+
+ a {
+ color: black;
+ text-decoration: none;
+ font-size: 16px;
+ cursor: pointer;
+ }
+ }
+
+ .search {
+ display: flex;
+ align-items: center;
+ background-color: #f5f5f5;
+ padding: 7px 12px;
+ gap: 35px;
+
+ input {
+ outline: 0;
+ border: 0;
+ background-color: #f5f5f5;
+ font-size: 12px;
+ }
+
+ img {
+ cursor: pointer;
+ }
+ }
+
+ .buttons {
+ display: flex;
+ gap: 16px;
+
+ button {
+ background: none;
+ outline: 0;
+ border: 0;
+ cursor: pointer;
+ }
+ }
+
+ .cart {
+ position: relative;
+ display: inline-block;
+ border-radius: 12px;
+ /*overflow: hidden;*/
+ }
+
+ .sticker {
+ position: absolute;
+ top: 15px;
+ /* отступ сверху */
+ left: 15px;
+ /* отступ слева */
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
+ /* Градиент стикера */
+ color: #ffffff;
+ padding: 6px 10px 6px 10px;
+ border-radius: 20px;
+ /* Округление стикера */
+ box-shadow: 0 4px 12px rgba(255, 65, 108, 0.4);
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.5px;
+ z-index: 10;
+ }
+}
diff --git a/src/components/list/cart/index.jsx b/src/components/list/cart/index.jsx
new file mode 100644
index 0000000..026b093
--- /dev/null
+++ b/src/components/list/cart/index.jsx
@@ -0,0 +1,22 @@
+import CartItem from '../../card/cart'
+import styles from './index.module.scss'
+import { useCart } from '../../../hooks/useCart'
+
+const CartList = () => {
+ const { cart, loading, error } = useCart()
+
+ if (loading) return Загрузка корзины...
+ if (error) return Ошибка: {error}
+
+ return (
+
+ {cart.map(item => (
+ -
+
+
+ ))}
+
+ )
+}
+
+export default CartList
diff --git a/src/components/list/cart/index.module.scss b/src/components/list/cart/index.module.scss
new file mode 100644
index 0000000..1acd00e
--- /dev/null
+++ b/src/components/list/cart/index.module.scss
@@ -0,0 +1,13 @@
+.list {
+ display: flex;
+ border: 1px solid gainsboro;
+ border-radius: 20px;
+ padding: 20px;
+ margin-top: 24px;
+ list-style: none;
+
+ max-width: 715px;
+ flex-direction: column;
+
+ flex-basis: 80%;
+}
diff --git a/src/components/list/product/index.jsx b/src/components/list/product/index.jsx
new file mode 100644
index 0000000..78c44ae
--- /dev/null
+++ b/src/components/list/product/index.jsx
@@ -0,0 +1,34 @@
+import styles from './index.module.scss'
+import ProductCard from '../../card/product'
+import { useProducts } from '../../../hooks/useProducts'
+
+const ProductList = () => {
+ const { products, loading, error } = useProducts()
+
+ if (loading) {
+ return Загрузка продуктов...
+ }
+
+ if (error) {
+ return (
+
+
Ошибка при загрузке продуктов: {error}
+
+
+ )
+ }
+
+ return (
+
+ {products.map(product => (
+ -
+
+
+ ))}
+
+ )
+}
+
+export default ProductList
diff --git a/src/components/list/product/index.module.scss b/src/components/list/product/index.module.scss
new file mode 100644
index 0000000..19d3736
--- /dev/null
+++ b/src/components/list/product/index.module.scss
@@ -0,0 +1,7 @@
+.list {
+ display: flex;
+ flex-wrap: wrap;
+ list-style: none;
+ padding-top: 60px;
+ gap: 30px;
+}
diff --git a/src/hooks/useCart.js b/src/hooks/useCart.js
new file mode 100644
index 0000000..3dd21d9
--- /dev/null
+++ b/src/hooks/useCart.js
@@ -0,0 +1,48 @@
+import React from 'react'
+import { CartService } from '../services/CartService'
+
+export function useCart() {
+ const [cart, setCart] = React.useState([])
+ const [loading, setLoading] = React.useState(true)
+ const [error, setError] = React.useState(null)
+
+ const fetchCart = React.useCallback(async () => {
+ try {
+ setLoading(true)
+ const data = await CartService.getCart()
+ setCart(data)
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ const addToCart = async product => {
+ await CartService.addToCart(product)
+ await fetchCart()
+ }
+
+ const removeFromCart = async id => {
+ await CartService.removeFromCart(id)
+ await fetchCart()
+ }
+
+ const updateQuantity = async (id, quantity) => {
+ await CartService.updateQuantity(id, quantity)
+ await fetchCart()
+ }
+
+ React.useEffect(() => {
+ fetchCart()
+ }, [fetchCart])
+
+ return {
+ cart,
+ loading,
+ error,
+ addToCart,
+ removeFromCart,
+ updateQuantity,
+ }
+}
diff --git a/src/hooks/useProducts.js b/src/hooks/useProducts.js
new file mode 100644
index 0000000..44b460f
--- /dev/null
+++ b/src/hooks/useProducts.js
@@ -0,0 +1,42 @@
+import React from 'react'
+import { fetchProducts } from '../services/ProductService'
+
+export function useProducts() {
+ const [products, setProducts] = React.useState([])
+ const [loading, setLoading] = React.useState(true)
+ const [error, setError] = React.useState(null)
+
+ React.useEffect(() => {
+ let cancelled = false
+
+ const loadProducts = async () => {
+ try {
+ setLoading(true)
+ setError(null)
+
+ const data = await fetchProducts()
+
+ if (!cancelled) {
+ setProducts(data)
+ }
+ } catch (err) {
+ if (!cancelled) {
+ setError(err.message)
+ setProducts([])
+ }
+ } finally {
+ if (!cancelled) {
+ setLoading(false)
+ }
+ }
+ }
+
+ loadProducts()
+
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ return { products, loading, error }
+}
diff --git a/src/pages/cart/index.jsx b/src/pages/cart/index.jsx
new file mode 100644
index 0000000..1c6e816
--- /dev/null
+++ b/src/pages/cart/index.jsx
@@ -0,0 +1,20 @@
+import styles from './index.module.scss'
+
+import Header from '../../components/header'
+import CartList from '../../components/list/cart'
+import OrderDetail from '../../components/OrderDetail'
+
+const Cart = () => {
+ return (
+
+ )
+}
+
+export default Cart
diff --git a/src/pages/cart/index.module.scss b/src/pages/cart/index.module.scss
new file mode 100644
index 0000000..3d536a3
--- /dev/null
+++ b/src/pages/cart/index.module.scss
@@ -0,0 +1,4 @@
+.container {
+ max-width: 1170px;
+ margin: 0 auto;
+}
diff --git a/src/pages/catalog/index.jsx b/src/pages/catalog/index.jsx
new file mode 100644
index 0000000..d12a636
--- /dev/null
+++ b/src/pages/catalog/index.jsx
@@ -0,0 +1,15 @@
+import styles from './index.module.scss'
+
+import Header from '../../components/header/index'
+import ProductList from '../../components/list/product/index'
+
+const Catalog = () => {
+ return (
+
+ )
+}
+
+export default Catalog
diff --git a/src/pages/catalog/index.module.scss b/src/pages/catalog/index.module.scss
new file mode 100644
index 0000000..3d536a3
--- /dev/null
+++ b/src/pages/catalog/index.module.scss
@@ -0,0 +1,4 @@
+.container {
+ max-width: 1170px;
+ margin: 0 auto;
+}
diff --git a/src/services/CartService.js b/src/services/CartService.js
new file mode 100644
index 0000000..0effe16
--- /dev/null
+++ b/src/services/CartService.js
@@ -0,0 +1,53 @@
+const BASE_URL = 'http://localhost:8000/cart'
+
+export const CartService = {
+ async getCart() {
+ const response = await fetch(BASE_URL)
+ if (!response.ok) throw new Error('Ошибка при получении корзины')
+ return response.json()
+ },
+ async getCartCount() {
+ const response = await fetch(BASE_URL)
+ if (!response.ok) throw new Error('Ошибка при получении корзины')
+ return response.json().length
+ },
+ async addToCart(product) {
+ const existing = await fetch(`${BASE_URL}?productId=${product.id}`)
+ const data = await existing.json()
+
+ if (data.length > 0) {
+ const current = data[0]
+ return fetch(`${BASE_URL}/${current.id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ quantity: current.quantity + 1 }),
+ })
+ } else {
+ return fetch(BASE_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ productId: product.id,
+ name: product.name,
+ price: product.price,
+ imageUrl: product.imageUrl,
+ quantity: 1,
+ }),
+ })
+ }
+ },
+
+ async updateQuantity(id, quantity) {
+ return fetch(`${BASE_URL}/${id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ quantity }),
+ })
+ },
+
+ async removeFromCart(id) {
+ return fetch(`${BASE_URL}/${id}`, {
+ method: 'DELETE',
+ })
+ },
+}
diff --git a/src/services/ProductService.js b/src/services/ProductService.js
new file mode 100644
index 0000000..9439e27
--- /dev/null
+++ b/src/services/ProductService.js
@@ -0,0 +1,15 @@
+const API_URL = 'http://localhost:8000/products'
+
+export async function fetchProducts() {
+ try {
+ const response = await fetch(API_URL)
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ return await response.json()
+ } catch (error) {
+ throw new Error(error.message || 'Ошибка при загрузке продуктов')
+ }
+}