This commit is contained in:
GokaPek 2023-12-08 00:33:38 +04:00
parent c4aa7e1f92
commit 39a552e5fe
18 changed files with 471 additions and 187 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,8 @@
.cart-image {
width: 3.1rem;
padding: .25rem;
}
.cart-item {
height: auto;
}

View File

@ -0,0 +1,59 @@
import { Button, ButtonGroup, Card } from 'react-bootstrap';
import { DashLg, PlusLg, XLg } from 'react-bootstrap-icons';
import imgPlaceholder from '../../assets/200.png';
import './Cart.css';
import useCart from './CartHook';
const Cart = () => {
const {
cart,
getCartSum,
addToCart,
removeFromCart,
clearCart,
} = useCart();
return (
<div className='d-flex flex-column align-items-center'>
<div className='mb-2 col-12 col-md-8 col-lg-6 d-flex align-items-center'>
<strong className='flex-fill'>Корзина</strong>
<Button variant='danger' onClick={() => clearCart()}>
<XLg /> Очистить
</Button>
</div>
{
cart.map((cartItem) =>
<Card key={cartItem.id} className='mb-2 col-12 col-md-8 col-lg-6'>
<Card.Body className='p-2 d-flex flex-column flex-sm-row align-items-center'>
<div className='cart-item flex-fill'>
<img className='cart-image' src={cartItem.image || imgPlaceholder} alt="Cart Image" />
{cartItem.type.name}
</div>
<div className='cart-item mt-2 mt-sm-0 d-flex flex-column align-items-center align-items-sm-end'>
<div>
{cartItem.price}
{' * '}
{cartItem.count}
{' = '}
{parseFloat(cartItem.price * cartItem.count).toFixed(2)}
</div>
<ButtonGroup className='mt-2 mt-sm-1' aria-label="Cart counter">
<Button variant="primary" onClick={() => addToCart(cartItem)}>
<PlusLg />
</Button>
<Button variant="danger" onClick={() => removeFromCart(cartItem)}>
<DashLg />
</Button>
</ButtonGroup>
</div>
</Card.Body>
</Card>)
}
<div className='mb-2 col-12 col-md-8 col-lg-6 d-flex justify-content-end'>
<strong>Итого: {getCartSum()} &#8381;</strong>
</div>
</div>
);
};
export default Cart;

View File

@ -0,0 +1,29 @@
import PropTypes from 'prop-types';
import {
createContext,
useEffect,
useReducer,
} from 'react';
import { cartReducer, loadCart, saveCart } from './CartReducer';
const CartContext = createContext(null);
export const CartProvider = ({ children }) => {
const [cart, dispatch] = useReducer(cartReducer, [], loadCart);
useEffect(() => {
saveCart(cart || []);
}, [cart]);
return (
<CartContext.Provider value={{ cart, dispatch }}>
{children}
</CartContext.Provider>
);
};
CartProvider.propTypes = {
children: PropTypes.node,
};
export default CartContext;

View File

@ -0,0 +1,26 @@
import { useContext } from 'react';
import CartContext from './CartContext.jsx';
import { cartAdd, cartClear, cartRemove } from './CartReducer';
const useCart = () => {
const { cart, dispatch } = useContext(CartContext);
const cartSum = () => {
return parseFloat(
cart?.reduce((sum, cartItem) => {
return sum + (cartItem.price * cartItem.count);
}, 0)
?? 0,
).toFixed(2);
};
return {
cart,
getCartSum: () => cartSum(),
addToCart: (item) => dispatch(cartAdd(item)),
removeFromCart: (item) => dispatch(cartRemove(item)),
clearCart: () => dispatch(cartClear()),
};
};
export default useCart;

View File

@ -0,0 +1,71 @@
const setCartCount = (cart, item, value) => {
return cart.map((cartItem) => {
if (cartItem.id === item.id) {
return { ...cartItem, count: cartItem.count + value };
}
return cartItem;
});
};
const addToCart = (cart, item) => {
const existsItem = cart.find((cartItem) => cartItem.id === item.id);
if (existsItem !== undefined) {
return setCartCount(cart, item, 1);
}
return [...cart, { ...item, count: 1 }];
};
const removeFromCart = (cart, item) => {
const existsItem = cart.find((cartItem) => cartItem.id === item.id);
if (existsItem !== undefined && existsItem.count > 1) {
return setCartCount(cart, item, -1);
}
return cart.filter((cartItem) => cartItem.id !== item.id);
};
const CART_KEY = 'localCart';
const CART_ADD = 'cart/add';
const CART_REMOVE = 'cart/remove';
const CART_CLEAR = 'cart/clear';
export const saveCart = (cart) => {
localStorage.setItem(CART_KEY, JSON.stringify(cart));
};
export const loadCart = (initialValue = []) => {
const cartData = localStorage.getItem(CART_KEY);
if (cartData) {
return JSON.parse(cartData);
}
return initialValue;
};
export const cartReducer = (cart, action) => {
const { item } = action;
switch (action.type) {
case CART_ADD: {
return addToCart(cart, item);
}
case CART_REMOVE: {
return removeFromCart(cart, item);
}
case CART_CLEAR: {
return [];
}
default: {
throw Error(`Unknown action: ${action.type}`);
}
}
};
export const cartAdd = (item) => ({
type: CART_ADD, item,
});
export const cartRemove = (item) => ({
type: CART_REMOVE, item,
});
export const cartClear = () => ({
type: CART_CLEAR,
});

View File

@ -3,6 +3,7 @@
background-color: #c03000 !important;
height: 70px;
border-radius: 4% / 60%;
z-index: 1030;
}
.my-navbar .link a:hover {

View File

@ -21,7 +21,7 @@ const Navigation = ({ routes }) => {
<Navbar.Toggle aria-controls='navbarSupportedContent' />
<Navbar.Collapse id='navbarSupportedContent'>
<Nav className='me-auto'>
<Form className='d-flex mr-4 mt-4'>
<Form className='d-flex mr-4 mt-1' as={Link} to='/search'>
<FormControl
type='search'
placeholder='Search'
@ -40,7 +40,7 @@ const Navigation = ({ routes }) => {
</Nav.Link>)
}
</Nav>
<Nav.Link as={Link} to='/login.html'>
<Nav.Link as={Link} to='/login'>
<img id='profile' className='user-photo mt-1 ml-4 mr-3' />
</Nav.Link>
</Nav>

View File

@ -5,32 +5,4 @@
url('/src/fonts/ITCErasStd-Bold.woff') format('woff');
font-style: normal;
font-weight: 400;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.25em;
}
h3 {
font-size: 1.1em;
}
.btn-mw {
width: 100%;
}
@media (min-width: 768px) {
.btn-mw {
width: 30%;
}
}
@media (min-width: 1200px) {
.btn-mw {
width: 20%;
}
}

View File

@ -10,6 +10,9 @@ import Page2 from './pages/Page2.jsx';
import Page3 from './pages/Page3.jsx';
import Page4 from './pages/Page4.jsx';
import PageEdit from './pages/PageEdit.jsx';
import Admin from './pages/Admin.jsx';
import Login from './pages/Login.jsx';
import Search from './pages/Search.jsx';
const routes = [
{
@ -36,6 +39,18 @@ const routes = [
path: '/page-edit',
element: <PageEdit />,
},
{
path: '/admin',
element: <Admin />,
},
{
path: '/login',
element: <Login />,
},
{
path: '/search',
element: <Search />,
},
];
const router = createBrowserRouter([

View File

@ -0,0 +1,26 @@
import './pages.css';
import { Button, ButtonGroup, Table } from 'react-bootstrap';
const Admin = () => {
return (
<>
<ButtonGroup>
<Button id="items-add" className="btn-info" variant="info">Add book</Button>
</ButtonGroup>
<Table className="mt-2" striped>
<thead>
<th scope="col">#</th>
<th scope="col">Category</th>
<th scope="col">Name</th>
<th scope="col">Author</th>
<th scope="col">ISBN</th>
<th scope="col">Price</th>
<th scope="col">Image</th>
</thead>
<tbody></tbody>
</Table>
</>
);
};
export default Admin;

View File

@ -0,0 +1,29 @@
import './pages.css';
import { Form, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom';
const Login = () => {
return (
<Form className="col-md-6 mr-sm-10 mx-auto mt-3 pt-5" method="get">
<Form.Group controlId="firstname">
<Form.Control type="text" placeholder="name" className="mb-3" required/>
</Form.Group>
<Form.Group controlId="email">
<Form.Control type="email" placeholder="mail@example.ru" className="mb-3" required/>
</Form.Group>
<Form.Group controlId="password">
<Form.Control type="password" placeholder="password" className="mb-3" required/>
</Form.Group>
<div className="text-center">
<Button variant="primary" type="submit" className="w-50">
<>Submit</>
</Button>
<Button variant="primary" type="submit" as={Link} to='/admin' className="w-50">
<>Admin</>
</Button>
</div>
</Form>
);
};
export default Login;

View File

@ -1,35 +1,31 @@
import { Link } from 'react-router-dom';
import Banner from '../components/banner/Banner.jsx';
import './pages.css';
import { Carousel } from 'react-bootstrap';
const Page1 = () => {
return (
<>
<Banner />
<>
<h1>Пример web-страницы.</h1>
<h2>1. Структурные элементы</h2>
<p><b>Полужирное начертание <i>курсив</i></b></p>
<p>Абзац 2 <Link to="/page2">Ссылка</Link></p>
<h3>1.1. Списки</h3>
<p>
Список маркированный:
</p>
<ul>
<li><a href="/page2" target="_blank">
Элемент списка 1</a></li>
<li>Элемент списка 2</li>
<li>...</li>
</ul>
<p>
Список нумерованный:
</p>
<ol>
<li>Элемент списка 1</li>
<li>Элемент списка 2</li>
<li>...</li>
</ol>
</>
</>
<Carousel className="container-fluid p-0 mt-5">
<Carousel.Item className="p-5">
<img
className="img d-block w-100 pt-3 mx-auto"
src="src/assets/books/metro.jpg"
alt="First slide"
/>
</Carousel.Item>
<Carousel.Item className="p-5">
<img
className="img d-block w-100 pt-3 mx-auto"
src="src/assets/books/blev.jpg"
alt="Second slide"
/>
</Carousel.Item>
<Carousel.Item className="p-5">
<img
className="img d-block w-100 pt-3 mx-auto"
src="src/assets/books/capital.jpg"
alt="Third slide"
/>
</Carousel.Item>
</Carousel>
);
};

View File

@ -1,42 +1,57 @@
import { Table } from 'react-bootstrap';
import ulstuLogo from '../assets/logo.png';
import {
Form, InputGroup, FormControl, Button, Dropdown, Table,
} from 'react-bootstrap';
const Page2 = () => {
return (
<>
<p className="text-center">
Вторая страница содержит пример рисунка (рис. 1) и таблицы (таб. 1).
</p>
<div className="text-center">
<img src={ulstuLogo} alt="logo" width="128" />
<br />
Рис. 1. Пример рисунка.
</div>
<div className="mt-3 row justify-content-center">
<Table className="w-50" bordered>
<caption>Таблица 1. Пример таблицы.</caption>
<thead>
<tr>
<th className="w-25">Номер</th>
<th>ФИО</th>
<th className="w-25">Телефон</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Иванов</td>
<td>89999999999</td>
</tr>
<tr>
<td>2</td>
<td>Петров</td>
<td>89999999991</td>
</tr>
</tbody>
</Table>
</div>
</>
<Form inline className="form-inlinecustom-search mx-auto mt-3 pt-5" id="big-search">
<InputGroup>
<FormControl type="search" placeholder="Search" aria-label="Search" className="mb-3" />
<Dropdown >
<Dropdown.Toggle variant="success" id="dropdown-basic" className="mb-3">
Choose category...
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item>1</Dropdown.Item>
<Dropdown.Item>2</Dropdown.Item>
<Dropdown.Item>3</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Button variant="outline-success" className="mb-3">Search</Button>
</InputGroup>
<div className="scroll-panel-favour">
<Table striped bordered hover>
<thead>
<tr>
<th>#</th>
<th>Book</th>
<th>Author</th>
<th>Gener</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Capital</td>
<td>Marks K.</td>
<td>Philosofy</td>
</tr>
<tr>
<td>2</td>
<td>Capital</td>
<td>Marks K.</td>
<td>Philosofy</td>
</tr>
<tr>
<td>3</td>
<td>Capital</td>
<td>Marks K.</td>
<td>Philosofy</td>
</tr>
</tbody>
</Table>
</div>
</Form>
);
};

View File

@ -1,77 +1,31 @@
import { useState } from 'react';
import { Button, Form } from 'react-bootstrap';
import './pages.css';
import { Carousel } from 'react-bootstrap';
const Page3 = () => {
const [validated, setValidated] = useState(false);
const handleSubmit = (event) => {
const form = event.currentTarget;
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
setValidated(true);
};
return (
<div className="row justify-content-center">
<Form className="col-md-6 m-0" noValidate validated={validated} onSubmit={handleSubmit}>
<Form.Group className="mb-2" controlId="lastname">
<Form.Label>Фамилия</Form.Label>
<Form.Control type="text" name="lastname" required />
<Form.Control.Feedback>Фамилия заполнена</Form.Control.Feedback>
<Form.Control.Feedback type="invalid">Фамилия не заполнена</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-2" controlId="firstname">
<Form.Label>Имя</Form.Label>
<Form.Control type="text" name="firstname" required />
</Form.Group>
<Form.Group className="mb-2" controlId="email">
<Form.Label>E-mail</Form.Label>
<Form.Control type="email" name="email" placeholder="name@example.ru" required />
</Form.Group>
<Form.Group className="mb-2" controlId="password">
<Form.Label>Пароль</Form.Label>
<Form.Control type="password" name="password" required />
</Form.Group>
<Form.Group className="mb-2" controlId="date">
<Form.Label>Дата</Form.Label>
<Form.Control type="date" name="date" required />
</Form.Group>
<Form.Group className="mb-2" controlId="disabled">
<Form.Label>Выключенный компонент</Form.Label>
<Form.Control type="text" name="disabled" value="Некоторое значение" disabled />
</Form.Group>
<Form.Group className="mb-2" controlId="readonly">
<Form.Label>Компонент только для чтения</Form.Label>
<Form.Control type="text" name="readonly" value="Некоторое значение" readOnly />
</Form.Group>
<Form.Group className="mb-2" controlId="color">
<Form.Label>Выбор цвета</Form.Label>
<Form.Control type="color" name="color" defaultValue='#ff0055' />
</Form.Group>
<Form.Group className="mb-2 d-md-flex flex-md-row">
<Form.Check className="me-md-3" type='checkbox' name='checkbox1' label='Флажок 1' />
<Form.Check className="me-md-3" type='checkbox' name='checkbox2' label='Флажок 2' />
</Form.Group>
<Form.Group className="mb-2 d-md-flex flex-md-row">
<Form.Check className="me-md-3" type='radio' name='radio1' label='Переключатель 1' />
<Form.Check className="me-md-3" type='switch' name='radio2' label='Переключатель 2' />
</Form.Group>
<Form.Group className="mb-2">
<Form.Select name='selected' required>
<option value="" selected>Выберите значение</option>
<option value="1">Один</option>
<option value="2">Два</option>
<option value="3">Три</option>
</Form.Select>
</Form.Group>
<div className="text-center">
<Button className="w-50" variant="primary" type="submit">Отправить</Button>
</div>
</Form>
</div>
<Carousel className="container-fluid p-0 mt-5">
<Carousel.Item className="p-5">
<img
className="img d-block w-100 pt-3 mx-auto"
src="src/assets/books/capital.jpg"
alt="First slide"
/>
</Carousel.Item>
<Carousel.Item className="p-5">
<img
className="img d-block w-100 pt-3 mx-auto"
src="src/assets/books/capital.jpg"
alt="Second slide"
/>
</Carousel.Item>
<Carousel.Item className="p-5">
<img
className="img d-block w-100 pt-3 mx-auto"
src="src/assets/books/capital.jpg"
alt="Third slide"
/>
</Carousel.Item>
</Carousel>
);
};

View File

@ -1,27 +1,31 @@
import { Button, ButtonGroup, Table } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import './pages.css';
import { Carousel } from 'react-bootstrap';
const Page4 = () => {
return (
<>
<ButtonGroup>
<Button id="items-add" variant="info">Добавить товар (диалог)</Button>
<Button as={Link} to="/page-edit" variant="success">Добавить товар (страница)</Button>
</ButtonGroup>
<Table className="mt-2" striped>
<thead>
<th scope="col"></th>
<th scope="col" className="w-25">Товар</th>
<th scope="col" className="w-25">Цена</th>
<th scope="col" className="w-25">Количество</th>
<th scope="col" className="w-25">Сумма</th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</thead>
<tbody></tbody>
</Table>
</>
<Carousel className="container-fluid p-0 mt-5">
<Carousel.Item className="p-5">
<img
className="img d-block w-100 pt-3 mx-auto"
src="src/assets/books/blev.jpg"
alt="First slide"
/>
</Carousel.Item>
<Carousel.Item className="p-5">
<img
className="img d-block w-100 pt-3 mx-auto"
src="src/assets/books/blev.jpg"
alt="Second slide"
/>
</Carousel.Item>
<Carousel.Item className="p-5">
<img
className="img d-block w-100 pt-3 mx-auto"
src="src/assets/books/blev.jpg"
alt="Third slide"
/>
</Carousel.Item>
</Carousel>
);
};

View File

@ -0,0 +1,58 @@
import {
Form, InputGroup, FormControl, Button, Dropdown, Table,
} from 'react-bootstrap';
const Search = () => {
return (
<Form inline className="form-inlinecustom-search mx-auto mt-3 pt-5" id="big-search">
<InputGroup>
<FormControl type="search" placeholder="Search" aria-label="Search" className="mb-3" />
<Dropdown >
<Dropdown.Toggle variant="success" id="dropdown-basic" className="mb-3">
Choose category...
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item>1</Dropdown.Item>
<Dropdown.Item>2</Dropdown.Item>
<Dropdown.Item>3</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Button variant="outline-success" className="mb-3">Search</Button>
</InputGroup>
<div className="scroll-panel-favour">
<Table striped bordered hover>
<thead>
<tr>
<th>#</th>
<th>Book</th>
<th>Author</th>
<th>Gener</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Capital</td>
<td>Marks K.</td>
<td>Philosofy</td>
</tr>
<tr>
<td>2</td>
<td>Capital</td>
<td>Marks K.</td>
<td>Philosofy</td>
</tr>
<tr>
<td>3</td>
<td>Capital</td>
<td>Marks K.</td>
<td>Philosofy</td>
</tr>
</tbody>
</Table>
</div>
</Form>
);
};
export default Search;

View File

@ -0,0 +1,21 @@
.Carousel {
width: 80%;
background: #c03000;
height: 400px;
border-radius: 17% / 60%;
}
.img {
max-width: 200px;
overflow: hidden;
}
.btn-info{
background-color: #c03000;
color: white;
}
.btn{
background-color: #c03000;
color: white;
}