Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12515f925d | |||
| b3662b759f |
@@ -1,70 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,23 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -5,18 +5,13 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"server": "json-server --watch db.json --port 8000",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"json-server": "^1.0.0-beta.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.7.1",
|
||||
"sass-embedded": "^1.89.2"
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
|
||||
@@ -1,10 +1,108 @@
|
||||
// удаляем все, оставляя лишь пустую функцию App ниже
|
||||
import React from 'react'
|
||||
|
||||
class MyComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { data: null };
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.data ? (
|
||||
<div>Данные: {this.state.data}</div>
|
||||
) : (
|
||||
<div>Загрузка...</div>
|
||||
)}
|
||||
<button onClick={this.handleClick}>Обновить</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetch('https://api.example.com/data')
|
||||
.then(responce => responce.json())
|
||||
.then(json => this.setState({data: json}));
|
||||
|
||||
this.timerId = setInterval(()=> console.log('Tick'), 1000);
|
||||
}
|
||||
|
||||
componentWillUnmout() {
|
||||
clearInterval(this.timerId);
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
this.setState({data: 'Данные'})
|
||||
}
|
||||
}
|
||||
|
||||
class MyComponent2 extends React.Component {
|
||||
controller = new AbortController()
|
||||
|
||||
componentDidMount() {
|
||||
fetch('https://api.example.com/data',{ singnal: this.controller.signal })
|
||||
.then(responce => responce.json())
|
||||
.then(json => this.setState({data: json}))
|
||||
.catch(error => {
|
||||
if(error.name == 'AbortError') {
|
||||
console.log('Aborted');
|
||||
} else {
|
||||
console.log('Other error',error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.controller.abort()
|
||||
}
|
||||
}
|
||||
class MyComponent3 extends React.Component {
|
||||
componentDidMount() {
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
try {
|
||||
const response = await fetch('https://api.example.com/data');
|
||||
const data = await response.json();
|
||||
this.setSate({ data });
|
||||
} catch {
|
||||
console.log('error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Timer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { seconds: 0 }
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
console.log('Timer mounted');
|
||||
this.interval = setInterval(()=>{
|
||||
this.setState(prevState => ({seconds: prevState.seconds + 1}))
|
||||
},1000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
console.log('Timer unmount');
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
render() {
|
||||
return <h2>Прошло секунд: {this.state.seconds}</h2>
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [show,setShow] = React.useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Добавьте тестовый контент для проверки */}
|
||||
<h1>Hello, React!</h1>
|
||||
<h1>Таймер с классом</h1>
|
||||
<button onClick={()=>setShow(prev => !prev)}>{show ? 'Скрыть' : 'Показать'} таймер</button>
|
||||
{show && <Timer />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 27C11.5523 27 12 26.5523 12 26C12 25.4477 11.5523 25 11 25C10.4477 25 10 25.4477 10 26C10 26.5523 10.4477 27 11 27Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25 27C25.5523 27 26 26.5523 26 26C26 25.4477 25.5523 25 25 25C24.4477 25 24 25.4477 24 26C24 26.5523 24.4477 27 25 27Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 5H7L10 22H26" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 16.6667H25.59C25.7056 16.6667 25.8177 16.6267 25.9072 16.5535C25.9966 16.4802 26.0579 16.3782 26.0806 16.2648L27.8806 7.26479C27.8951 7.19222 27.8934 7.11733 27.8755 7.04552C27.8575 6.97371 27.8239 6.90678 27.7769 6.84956C27.73 6.79234 27.6709 6.74625 27.604 6.71462C27.5371 6.68299 27.464 6.66661 27.39 6.66666H8" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 20L16.2223 16.2156M18.3158 11.1579C18.3158 13.0563 17.5617 14.8769 16.2193 16.2193C14.8769 17.5617 13.0563 18.3158 11.1579 18.3158C9.2595 18.3158 7.43886 17.5617 6.0965 16.2193C4.75413 14.8769 4 13.0563 4 11.1579C4 9.2595 4.75413 7.43886 6.0965 6.0965C7.43886 4.75413 9.2595 4 11.1579 4C13.0563 4 14.8769 4.75413 16.2193 6.0965C17.5617 7.43886 18.3158 9.2595 18.3158 11.1579V11.1579Z" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 560 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 3.57143H2.33333L3.66667 19H14.3333L15.6667 3.57143H1M9 7.42857V15.1429M12.3333 7.42857L11.6667 15.1429M5.66667 7.42857L6.33333 15.1429M6.33333 3.57143L7 1H11L11.6667 3.57143" stroke="black" stroke-width="1.56" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 375 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 27V24.3333C24 22.9188 23.5224 21.5623 22.6722 20.5621C21.8221 19.5619 20.669 19 19.4667 19H11.5333C10.331 19 9.17795 19.5619 8.32778 20.5621C7.47762 21.5623 7 22.9188 7 24.3333V27" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16.5 14C18.9853 14 21 11.9853 21 9.5C21 7.01472 18.9853 5 16.5 5C14.0147 5 12 7.01472 12 9.5C12 11.9853 14.0147 14 16.5 14Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 598 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 1C2.7912 1 1 2.73964 1 4.88594C1 6.61852 1.7 10.7305 8.5904 14.8873C8.71383 14.961 8.85552 15 9 15C9.14448 15 9.28617 14.961 9.4096 14.8873C16.3 10.7305 17 6.61852 17 4.88594C17 2.73964 15.2088 1 13 1C10.7912 1 9 3.35511 9 3.35511C9 3.35511 7.2088 1 5 1Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 454 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 7C8.239 7 6 9.23669 6 11.9962C6 14.2238 6.875 19.5107 15.488 24.8551C15.6423 24.9499 15.8194 25 16 25C16.1806 25 16.3577 24.9499 16.512 24.8551C25.125 19.5107 26 14.2238 26 11.9962C26 9.23669 23.761 7 21 7C18.239 7 16 10.028 16 10.028C16 10.028 13.761 7 11 7Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 460 B |
|
Before Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 628 KiB |
|
Before Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 406 KiB |
@@ -1,4 +0,0 @@
|
||||
$white-color: white;
|
||||
$black-color: black;
|
||||
|
||||
$primary-color: #db4444;
|
||||
@@ -1,41 +0,0 @@
|
||||
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 (
|
||||
<div className={styles.detail}>
|
||||
<h2>Order Summary</h2>
|
||||
<div className={styles.stats}>
|
||||
<div>
|
||||
<p>Subtotal</p>
|
||||
<span>${subtotal}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>Discount (-20%)</p>
|
||||
<span>-${discount}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>Delivery Fee</p>
|
||||
<span>${delivery}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.total}>
|
||||
<p>Total</p>
|
||||
<span>${total}</span>
|
||||
</div>
|
||||
<button>Go to Checkout</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrderDetail
|
||||
@@ -1,67 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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 (
|
||||
<div className={styles.item}>
|
||||
<button className={styles.trash} onClick={() => removeFromCart(item.id)}>
|
||||
<img src={TrashIcon} alt='Удалить' />
|
||||
</button>
|
||||
<div className={styles.image}>
|
||||
<img src={item.imageUrl} alt={item.name} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<h2>{item.name}</h2>
|
||||
<div className={styles.wrapper}>
|
||||
<span className={styles.price}>${item.price}</span>
|
||||
<div className={styles.buttons}>
|
||||
<button onClick={handleDecrement}>-</button>
|
||||
<span>{item.quantity}</span>
|
||||
<button onClick={handleIncrement}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CartItem
|
||||
@@ -1,62 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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 (
|
||||
<div className={styles.product}>
|
||||
<div className={styles.image}>
|
||||
<img src={imageUrl} alt={name} />
|
||||
<div className={styles.buttons}>
|
||||
<button onClick={handleAdd}>В корзину</button>
|
||||
<button>
|
||||
<img src={WishIcon} alt='Избранное' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<h4>{name}</h4>
|
||||
<div className={styles.stats}>
|
||||
<span>${price}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductCard
|
||||
@@ -1,48 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
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 (
|
||||
<header className={styles.header}>
|
||||
<h2>Exclusive</h2>
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<Link to={{ pathname: '/' }}>Home</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to={{ pathname: '/contact' }}>Contact</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to={{ pathname: '/about' }}>About</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href='#'>Sign Up</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className={styles.search}>
|
||||
<input type='text' placeholder='What are you looking for?' />
|
||||
<img src={LoopIcon} alt='' />
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<button>
|
||||
<Link to={{ pathname: '/wishlist' }}>
|
||||
<img src={WishlistIcon} alt='' />
|
||||
</Link>
|
||||
</button>
|
||||
<button>
|
||||
<Link to={{ pathname: '/cart' }}>
|
||||
<div className={styles.cart}>
|
||||
<img src={CartIcon} alt='' className={styles.img} />
|
||||
{cart && cart.length > 0 ? <div className={styles.sticker}>{cart.length}</div> : ''}
|
||||
</div>
|
||||
</Link>
|
||||
</button>
|
||||
<button>
|
||||
<img src={UserIcon} alt='' />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -1,85 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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 <p>Загрузка корзины...</p>
|
||||
if (error) return <p>Ошибка: {error}</p>
|
||||
|
||||
return (
|
||||
<ul className={styles.list}>
|
||||
{cart.map(item => (
|
||||
<li key={item.id}>
|
||||
<CartItem item={item} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default CartList
|
||||
@@ -1,13 +0,0 @@
|
||||
.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%;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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 <div className={styles.loading}>Загрузка продуктов...</div>
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
<p>Ошибка при загрузке продуктов: {error}</p>
|
||||
<button onClick={() => window.location.reload()}>
|
||||
Попробовать снова
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={styles.list}>
|
||||
{products.map(product => (
|
||||
<li key={product.id}>
|
||||
<ProductCard {...product} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductList
|
||||
@@ -1,7 +0,0 @@
|
||||
.list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
padding-top: 60px;
|
||||
gap: 30px;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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 }
|
||||
}
|
||||
@@ -1,20 +1,8 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Routes,
|
||||
Route,
|
||||
Navigate,
|
||||
} from 'react-router-dom'
|
||||
|
||||
import Catalog from './pages/catalog'
|
||||
import Cart from './pages/cart'
|
||||
// удаляем import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path='/' element={<Navigate to='/catalog' replace />} />
|
||||
<Route path='/catalog' element={<Catalog />} />
|
||||
<Route path='/cart' element={<Cart />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
<App />
|
||||
)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
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 (
|
||||
<div className={styles.container}>
|
||||
<Header />
|
||||
<h1 style={{ marginTop: 24 }}>Your Cart</h1>
|
||||
<div style={{ display: 'flex', gap: 20 }}>
|
||||
<CartList />
|
||||
<OrderDetail />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cart
|
||||
@@ -1,4 +0,0 @@
|
||||
.container {
|
||||
max-width: 1170px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import styles from './index.module.scss'
|
||||
|
||||
import Header from '../../components/header/index'
|
||||
import ProductList from '../../components/list/product/index'
|
||||
|
||||
const Catalog = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Header />
|
||||
<ProductList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Catalog
|
||||
@@ -1,4 +0,0 @@
|
||||
.container {
|
||||
max-width: 1170px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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',
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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 || 'Ошибка при загрузке продуктов')
|
||||
}
|
||||
}
|
||||