This commit is contained in:
russell 2024-01-10 21:02:06 +04:00
parent 4d40ed3ebe
commit f53cd3f4d9
74 changed files with 3813 additions and 246 deletions

150
data.json Normal file

File diff suppressed because one or more lines are too long

15
jsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node",
"target": "ES2020",
"jsx": "react",
"strictNullChecks": true,
"strictFunctionTypes": true,
"sourceMap": true
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}

5
json-server.json Normal file
View File

@ -0,0 +1,5 @@
{
"static": "./node_modules/json-server/public",
"port": 8081,
"watch": "true"
}

1628
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,23 @@
{
"name": "lab4",
"name": "lab5",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
"rest": "json-server data.json",
"vite": "vite",
"build": "vite build",
"preview": "vite preview",
"dev": "npm-run-all --parallel rest vite",
"prod": "npm-run-all lint build --parallel rest preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.18.0",
"react-hot-toast": "^2.4.1",
"axios": "^1.6.1",
"bootstrap": "^5.3.2",
"react-bootstrap": "^2.9.1",
"react-bootstrap-icons": "^1.10.3",
@ -24,9 +29,12 @@
"@vitejs/plugin-react": "^4.2.0",
"eslint": "^8.53.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"json-server": "^0.17.4",
"npm-run-all": "^4.1.5",
"vite": "^5.0.0"
}
}

View File

@ -2,17 +2,21 @@ import { Container } from 'react-bootstrap';
import { Outlet } from 'react-router-dom';
import './App.css';
import Footer from './components/footer/Footer.jsx';
import { CartProvider } from './components/cart/CartContext.jsx';
import Navigation from './components/navigation/Navigation.jsx';
import { LoginProvider } from './components/login/LoginContext.jsx';
const App = () => {
return (
<>
<Navigation></Navigation>
<Container className='p-2' as="main" fluid>
<Outlet />
</Container>
<Footer />
</>
<LoginProvider>
<CartProvider>
<Navigation></Navigation>
<Container className='p-2' as="main" fluid>
<Outlet />
</Container>
<Footer />
</CartProvider>
</LoginProvider>
);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

BIN
src/assets/coat-hanger.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,83 @@
import { Button } from 'react-bootstrap';
import useOrders from '../order/hooks/OrdersHook';
import { useContext } from 'react';
import LoginContext from '../login/LoginContext';
import useLoginForm from '../login/LoginHook';
import imgPlaceholder from '../../assets/coat-hanger.png'
const Account = () => {
const { login } = useContext(LoginContext);
const { orders } = useOrders(login.id);
const { logOut } = useLoginForm();
return (
<>
<h2 className="text-center display-6 my-4">Личный кабинет</h2>
<div className="container-fluid mb-5 d-lg-flex">
<div className='col-lg-5 col-12 p-5'>
<h4 className='fs-3'>Ваши данные</h4>
<div className='d-flex my-4'>
<div className='col-3'>
<p className='fw-bold'>Фамилия</p>
<p className='fs-5'>{login.surname}</p>
<p className='fw-bold'>Имя</p>
<p className='fs-5'>{login.name}</p>
</div>
<div className='col-3'>
<p className='fw-bold'>Email</p>
<p className='fs-5'>{login.email}</p>
<p className='fw-bold'>Пароль</p>
<p className='fs-5'>{login.password}</p>
</div>
</div>
<Button className='fw-bold w-50' variant="primary" onClick={logOut}>Выйти</Button>
</div>
<div className="col-lg-7 col-12">
<h4 className='fs-3 text-center'>История заказов</h4>
{
orders.map((order) =>
<div key={order.id}>
<p className='fs-5'>Заказ от {order.date}</p>
<div className="row fs-4 p-2 text-center cart-labels align-items-center d-md-flex d-none">
<div className="col-6 text-start">
<span>товар</span>
</div>
<div className="col-2">
<span>цена, руб</span>
</div>
<div className="col-2">
<span>количество</span>
</div>
<div className="col-2">
<span>всего</span>
</div>
</div>
{
order.items.map((item, index) =>
<div key={index} className="row fs-5 p-2 text-center align-items-center justify-content-between">
<div className="cart-item col-md-6 text-start d-flex align-items-center">
<img className="w-25 me-4" src={item.line.image || imgPlaceholder} alt="" />
<span>{item.line.name}, {item.size}</span>
</div>
<div className="col-2 d-md-block d-none">
<span>{item.line.finalPrice} </span>
</div>
<div className="col-md-2 col-4">
<span>{item.count}</span>
</div>
<div className="col-md-2 col-4">
<span>{parseFloat(item.line.finalPrice * item.count).toFixed(2)} </span>
</div>
</div>
)
}
</div>
)
}
</div>
</div>
</>
);
};
export default Account;

View File

@ -0,0 +1,40 @@
import axios from 'axios';
import toast from 'react-hot-toast';
export class HttpError extends Error {
constructor(message = '') {
super(message);
this.name = 'HttpError';
Object.setPrototypeOf(this, new.target.prototype);
toast.error(message, { id: 'HttpError' });
}
}
function responseHandler(response) {
if (response.status === 200 || response.status === 201) {
const data = response?.data;
if (!data) {
throw new HttpError('API Error. No data!');
}
return data;
}
throw new HttpError(`API Error! Invalid status code ${response.status}!`);
}
function responseErrorHandler(error) {
if (error === null) {
throw new Error('Unrecoverable error!! Error is null!');
}
toast.error(error.message, { id: 'AxiosError' });
return Promise.reject(error.message);
}
export const ApiClient = axios.create({
baseURL: 'http://localhost:8081/',
timeout: '3000',
headers: {
Accept: 'application/json',
},
});
ApiClient.interceptors.response.use(responseHandler, responseErrorHandler);

View File

@ -0,0 +1,29 @@
import { ApiClient } from './ApiClient';
class ApiService {
constructor(url) {
this.url = url;
}
async getAll(expand) {
return ApiClient.get(`${this.url}${expand || ''}`);
}
async get(id, expand) {
return ApiClient.get(`${this.url}/${id}${expand || ''}`);
}
async create(body) {
return ApiClient.post(this.url, body);
}
async update(id, body) {
return ApiClient.put(`${this.url}/${id}`, body);
}
async delete(id) {
return ApiClient.delete(`${this.url}/${id}`);
}
}
export default ApiService;

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,35 @@
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 parseInt(
cart?.reduce((sum, cartItem) => {
return sum + (cartItem.price * cartItem.count);
}, 0)
?? 0,
).toFixed(2);
};
const cartCount = () => {
return parseFloat(
cart?.reduce((sum, cartItem) => {
return sum + cartItem.count;
}, 0)
?? 0, 10
);
};
return {
cart,
getCartSum: () => cartSum(),
addToCart: (item) => dispatch(cartAdd(item)),
removeFromCart: (item) => dispatch(cartRemove(item)),
clearCart: () => dispatch(cartClear()),
getCartCount: () => cartCount(),
};
};
export default useCart;

View File

@ -0,0 +1,71 @@
const setCartCount = (cart, item, value) => {
return cart.map((cartItem) => {
if (cartItem.id === item.id && cartItem.size === item.size) {
return { ...cartItem, count: cartItem.count + value };
}
return cartItem;
});
};
const addToCart = (cart, item) => {
const existsItem = cart.find((cartItem) => cartItem.id === item.id && cartItem.size === item.size);
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 && cartItem.size === item.size);
if (existsItem !== undefined && existsItem.count > 1) {
return setCartCount(cart, item, -1);
}
return cart.filter((cartItem) => !(cartItem.id === item.id && cartItem.size === item.size));
};
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

@ -0,0 +1,20 @@
import PropTypes from 'prop-types';
import Input from '../../input/Input.jsx';
const DiscountsForm = ({ discount, handleChange }) => {
return (
<>
<Input name='name' label='Название' value={discount.name} onChange={handleChange}
type='text' required />
<Input name='percent' label='Процент скидки' value={discount.percent} onChange={handleChange}
type='number' min='0' step='1' required />
</>
);
};
DiscountsForm.propTypes = {
discount: PropTypes.object,
handleChange: PropTypes.func,
};
export default DiscountsForm;

View File

@ -0,0 +1,33 @@
import { useEffect, useState } from 'react';
import DiscountsApiService from '../service/DiscountsApiService';
const useDiscount = (id) => {
const emptyDiscount = {
id: '',
name: '',
percent: '0'
};
const [discount, setDiscount] = useState({ ...emptyDiscount });
const getDiscountById = async (discountId = undefined) => {
if (discountId && discountId > 0) {
const data = await DiscountsApiService.get(discountId);
setDiscount(data);
} else {
setDiscount({ ...emptyDiscount });
}
};
useEffect(() => {
getDiscountById(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
return {
discount,
setDiscount
};
};
export default useDiscount;

View File

@ -0,0 +1,58 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import useModal from '../../modal/ModalHook';
import DiscountsApiService from '../service/DiscountsApiService';
import LinesApiService from '../../lines/service/LinesApiService';
import useLines from '../../lines/hooks/LinesHook';
const useDiscountsDeleteModal = (discountsChangeHandle, handleLinesChange) => {
const { isModalShow, showModal, hideModal } = useModal();
const [currentId, setCurrentId] = useState(0);
const showModalDialog = (id) => {
showModal();
setCurrentId(id);
};
const onClose = () => {
hideModal();
};
const { lines } = useLines();
const onDelete = async () => {
const updatePromises = lines.map(async (line, index) => {
console.log(`index = ${index}: ${currentId}, ${line.discountId}`)
if (currentId == line.discountId){
const price = parseFloat(line.price).toFixed(2);
await LinesApiService.update(line.id, {
typeId: line.typeId,
discountId: 1,
name: line.name,
price: price.toString(),
finalPrice: price.toString(),
image: line.image,
});
}
});
await Promise.all(updatePromises);
await DiscountsApiService.delete(currentId);
discountsChangeHandle();
toast.success('Элемент успешно удален', { id: 'DiscountsTable' });
onClose();
handleLinesChange();
};
return {
isDeleteModalShow: isModalShow,
showDeleteModal: showModalDialog,
handleDeleteConfirm: onDelete,
handleDeleteCancel: onClose,
};
};
export default useDiscountsDeleteModal;

View File

@ -0,0 +1,28 @@
import { useSearchParams } from 'react-router-dom';
import useDiscounts from './DiscountsHook';
const useDiscountFilter = () => {
const filterName = 'discount';
const [searchParams, setSearchParams] = useSearchParams();
const { discounts } = useDiscounts();
const handleDiscountFilterChange = (event) => {
const discount = event.target.value;
if (discount) {
searchParams.set(filterName, event.target.value);
} else {
searchParams.delete(filterName);
}
setSearchParams(searchParams);
};
return {
discounts,
currentDiscountFilter: searchParams.get(filterName) || '',
handleDiscountFilterChange,
};
};
export default useDiscountFilter;

View File

@ -0,0 +1,94 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import useDiscount from './DiscountByIdHook';
import DiscountsApiService from '../service/DiscountsApiService';
import useLines from '../../lines/hooks/LinesHook';
import LinesApiService from '../../lines/service/LinesApiService';
const useDiscountsForm = (id, discountsChangeHandle, handleLinesChange) => {
const { discount, setDiscount } = useDiscount(id);
const [validated, setValidated] = useState(false);
const resetValidity = () => {
setValidated(false);
};
const getDiscountObject = (formData) => {
const name = formData.name;
const percent = parseInt(formData.percent, 10);
return {
name: name,
percent: percent.toString()
};
};
const getLineObject = (formData, discount) => {
const typeId = parseInt(formData.typeId, 10);
const discountId = parseInt(formData.discountId, 10);
const name = formData.name;
const price = parseFloat(formData.price).toFixed(2);
const finalPrice = parseFloat(price - (price * discount.percent * 0.01)).toFixed(2);
const image = formData.image.startsWith('data:image') ? formData.image : '';
return {
typeId: typeId.toString(),
discountId: discountId.toString(),
name: name,
price: price.toString(),
finalPrice: finalPrice.toString(),
image,
};
};
const handleChange = (event) => {
const inputName = event.target.name;
const inputValue = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
setDiscount({
...discount,
[inputName]: inputValue,
});
};
const { lines } = useLines();
const handleSubmit = async (event) => {
const form = event.currentTarget;
event.preventDefault();
event.stopPropagation();
const body = getDiscountObject(discount);
if (form.checkValidity()) {
if (id === undefined) {
await DiscountsApiService.create(body);
} else {
await DiscountsApiService.update(id, body);
await Promise.all(lines.map(async (line) => {
if (discount.id == line.discountId){
let currentId = line.id;
const currentDiscount = await DiscountsApiService.get(line.discountId);
const currentLine = getLineObject(line, currentDiscount);
await LinesApiService.update(currentId, currentLine);
}
}));
handleLinesChange();
}
if (discountsChangeHandle) discountsChangeHandle();
toast.success('Элемент успешно сохранен', { id: 'DiscountsTable' });
return true;
}
setValidated(true);
return false;
};
return {
discount,
validated,
handleSubmit,
handleChange,
resetValidity,
};
};
export default useDiscountsForm;

View File

@ -0,0 +1,48 @@
import { useState } from 'react';
import useModal from '../../modal/ModalHook';
import useDiscountsForm from './DiscountsFormHook';
const useDiscountsFormModal = (discountsChangeHandle, handleLinesChange) => {
const { isModalShow, showModal, hideModal } = useModal();
const [currentId, setCurrentId] = useState(0);
const {
discount,
validated,
handleSubmit,
handleChange,
resetValidity,
} = useDiscountsForm(currentId, discountsChangeHandle, handleLinesChange);
const showModalDialog = (id) => {
setCurrentId(id);
resetValidity();
showModal();
};
const onClose = () => {
setCurrentId(-1);
hideModal();
};
const onSubmit = async (event) => {
if (await handleSubmit(event)) {
onClose();
}
};
return {
isFormModalShow: isModalShow,
isFormValidated: validated,
showFormModal: showModalDialog,
currentDiscount: discount,
handleDiscountChange: handleChange,
handleFormSubmit: onSubmit,
handleFormClose: onClose,
};
};
export default useDiscountsFormModal;

View File

@ -0,0 +1,24 @@
import { useEffect, useState } from 'react';
import DiscountsApiService from '../service/DiscountsApiService';
const useDiscounts = () => {
const [discounts, setDiscounts] = useState([]);
const [discountsRefresh, setDiscountsRefresh] = useState(false);
const handleDiscountsChange = () => setDiscountsRefresh(!discountsRefresh);
const getDiscounts = async () => {
const data = await DiscountsApiService.getAll();
setDiscounts(data ?? []);
};
useEffect(() => {
getDiscounts();
}, [discountsRefresh]);
return {
discounts,
handleDiscountsChange
};
};
export default useDiscounts;

View File

@ -0,0 +1,5 @@
import ApiService from '../../api/ApiService';
const DiscountsApiService = new ApiService('discounts');
export default DiscountsApiService;

View File

@ -0,0 +1,66 @@
import { Button } from 'react-bootstrap';
import ModalConfirm from '../../modal/ModalConfirm.jsx';
import ModalForm from '../../modal/ModalForm.jsx';
import useDiscountsDeleteModal from '../hooks/DiscountsDeleteModalHook.js';
import useDiscountsFormModal from '../hooks/DiscountsFormModalHook.js';
import useDiscounts from '../hooks/DiscountsHook';
import DiscountsTable from './DiscountsTable.jsx';
import DiscountsTableRow from './DiscountsTableRow.jsx';
import DiscountsForm from '../form/DiscountsForm.jsx';
import PropTypes from 'prop-types';
const Discounts = ({ handleLinesChange }) => {
const { discounts, handleDiscountsChange } = useDiscounts();
const {
isDeleteModalShow,
showDeleteModal,
handleDeleteConfirm,
handleDeleteCancel,
} = useDiscountsDeleteModal(handleDiscountsChange, handleLinesChange);
const {
isFormModalShow,
isFormValidated,
showFormModal,
currentDiscount,
handleDiscountChange,
handleFormSubmit,
handleFormClose,
} = useDiscountsFormModal(handleDiscountsChange, handleLinesChange);
return (
<>
<DiscountsTable>
{
discounts.map((discount, index) =>
<DiscountsTableRow key={discount.id}
index={index} discount={discount}
onDelete={() => showDeleteModal(discount.id)}
onEdit={() => showFormModal(discount.id)}
/>)
}
</DiscountsTable>
<div className="d-flex justify-content-center">
<Button variant='primary' className="fw-bold px-5 mb-5" onClick={() => showFormModal()}>
Добавить акцию
</Button>
</div>
<ModalConfirm show={isDeleteModalShow}
onConfirm={handleDeleteConfirm} onClose={handleDeleteCancel}
title='Удаление' message='Удалить элемент?' />
<ModalForm show={isFormModalShow} validated={isFormValidated}
onSubmit={handleFormSubmit} onClose={handleFormClose}
title='Редактирование'>
<DiscountsForm discount={currentDiscount} handleChange={handleDiscountChange} />
</ModalForm>
</>
);
};
Discounts.propTypes = {
handleLinesChange: PropTypes.func,
}
export default Discounts;

View File

@ -0,0 +1,27 @@
import PropTypes from 'prop-types';
import { Table } from 'react-bootstrap';
const DiscountsTable = ({ children }) => {
return (
<Table className='mt-2' striped responsive hover>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Название акции</th>
<th scope="col">Процент скидки</th>
<th scope="col" />
<th scope="col" />
</tr>
</thead>
<tbody>
{children}
</tbody >
</Table >
);
};
DiscountsTable.propTypes = {
children: PropTypes.node,
};
export default DiscountsTable;

View File

@ -0,0 +1,43 @@
import PropTypes from 'prop-types';
import { PencilFill, Trash3 } from 'react-bootstrap-icons';
const DiscountsTableRow = ({
index, discount, onDelete, onEdit,
}) => {
const handleAnchorClick = (event, action) => {
event.preventDefault();
action();
};
if (index === 0) {
return (
<tr>
<th scope="row">{index + 1}</th>
<td>{discount.name}</td>
<td>{discount.percent}</td>
<td></td>
<td></td>
</tr>
);
}
return (
<tr>
<th scope="row">{index + 1}</th>
<td>{discount.name}</td>
<td>{discount.percent}</td>
<td><a href="#" onClick={(event) => handleAnchorClick(event, onEdit)}><PencilFill /></a></td>
<td><a href="#" onClick={(event) => handleAnchorClick(event, onDelete)}><Trash3 /></a></td>
</tr>
);
};
DiscountsTableRow.propTypes = {
index: PropTypes.number,
discount: PropTypes.object,
onDelete: PropTypes.func,
onEdit: PropTypes.func,
onEditInPage: PropTypes.func,
};
export default DiscountsTableRow;

View File

@ -0,0 +1,23 @@
import PropTypes from 'prop-types';
import { Form } from 'react-bootstrap';
const Input = ({
name, label, value, onChange, className, ...rest
}) => {
return (
<Form.Group className={`mb-2 ${className || ''}`} controlId={name}>
<Form.Label>{label}</Form.Label>
<Form.Control name={name || ''} value={value || ''} onChange={onChange} {...rest} />
</Form.Group>
);
};
Input.propTypes = {
name: PropTypes.string,
label: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
className: PropTypes.string,
};
export default Input;

View File

@ -0,0 +1,29 @@
import PropTypes from 'prop-types';
import { Form } from 'react-bootstrap';
const Select = ({
values, name, label, value, onChange, className, ...rest
}) => {
return (
<Form.Group className={`mb-2 ${className || ''}`} controlId={name}>
<Form.Label className='form-label'>{label}</Form.Label>
<Form.Select name={name || ''} value={value || ''} onChange={onChange} {...rest}>
<option value=''>Выберите значение</option>
{
values.map((type) => <option key={type.id} value={type.id}>{type.name}</option>)
}
</Form.Select>
</Form.Group>
);
};
Select.propTypes = {
values: PropTypes.array,
name: PropTypes.string,
label: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
className: PropTypes.string,
};
export default Select;

View File

@ -0,0 +1,23 @@
import PropTypes from 'prop-types';
import imgPlaceholder from '../../assets/coat-hanger.png';
import { Link } from 'react-router-dom';
const Item = ({ line }) => {
return (
<>
<Link className="link-light" to={`/product/${line.id}`}>
<div className="d-flex align-items-center justify-content-center flex-column text-center">
<h4 className="fs-1 fw-bold pt-5">{line.name}</h4>
<p className="fs-2 p-0">{`${parseFloat(line.finalPrice).toFixed(2)}`}</p>
</div>
<img className="img-fluid" src={line.image || imgPlaceholder} alt="" />
</Link>
</>
);
};
Item.propTypes = {
line: PropTypes.object,
};
export default Item;

View File

@ -0,0 +1,41 @@
import useLines from '../../components/lines/hooks/LinesHook';
import Item from './Item';
import useTypeFilter from '../lines/hooks/LinesFilterHook';
import { useLocation } from 'react-router-dom';
import { useEffect } from 'react';
import useTypes from '../types/hooks/TypesHook';
const ItemsCollection = () => {
const { currentTypeFilter, handleTypeFilterChange } = useTypeFilter();
const { lines } = useLines(currentTypeFilter);
const location = useLocation();
const types = useTypes();
let typeName = types.types[parseInt(currentTypeFilter, 10) - 1]?.name;
useEffect(() => {
const searchParams = new URLSearchParams(location.search);
const filterValue = searchParams.get('type');
if (filterValue) handleTypeFilterChange({ target: { value: filterValue } });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.search]);
return (
<>
<h2 className="text-center display-6 my-4">{typeName || 'Весь ассортимент'}</h2>
<section className="product-grid-container">
<div className="product-grid">
{
lines.map((line) =>
<Item key={line.id} line={line} />)
}
</div>
</section>
</>
);
};
export default ItemsCollection;

View File

@ -0,0 +1,3 @@
#image-preview {
width: 200px;
}

View File

@ -0,0 +1,38 @@
import PropTypes from 'prop-types';
import imgPlaceholder from '../../../assets/coat-hanger.png';
import Input from '../../input/Input.jsx';
import Select from '../../input/Select.jsx';
import useTypes from '../../types/hooks/TypesHook';
import useDiscounts from '../../discounts/hooks/DiscountsHook';
import './LinesItemForm.css';
const LinesItemForm = ({ item, handleChange }) => {
const { types } = useTypes();
const { discounts } = useDiscounts();
return (
<>
<div className='text-center'>
<img id='image-preview' className='rounded' alt='placeholder'
src={item.image || imgPlaceholder} />
</div>
<Select values={types} name='typeId' label='Товары' value={item.typeId} onChange={handleChange}
required />
<Select values={discounts} name='discountId' label='Акция' value={item.discountId} onChange={handleChange}
required />
<Input name='name' label='Название' value={item.name} onChange={handleChange}
type='text' required />
<Input name='price' label='Цена' value={item.price} onChange={handleChange}
type='number' min='1000.0' step='10' required />
<Input name='image' label='Изображение' onChange={handleChange}
type='file' accept='image/*' />
</>
);
};
LinesItemForm.propTypes = {
item: PropTypes.object,
handleChange: PropTypes.func,
};
export default LinesItemForm;

View File

@ -0,0 +1,34 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import useModal from '../../modal/ModalHook';
import LinesApiService from '../service/LinesApiService';
const useLinesDeleteModal = (linesChangeHandle) => {
const { isModalShow, showModal, hideModal } = useModal();
const [currentId, setCurrentId] = useState(0);
const showModalDialog = (id) => {
showModal();
setCurrentId(id);
};
const onClose = () => {
hideModal();
};
const onDelete = async () => {
await LinesApiService.delete(currentId);
linesChangeHandle();
toast.success('Элемент успешно удален', { id: 'LinesTable' });
onClose();
};
return {
isDeleteModalShow: isModalShow,
showDeleteModal: showModalDialog,
handleDeleteConfirm: onDelete,
handleDeleteCancel: onClose,
};
};
export default useLinesDeleteModal;

View File

@ -0,0 +1,28 @@
import { useSearchParams } from 'react-router-dom';
import useTypes from '../../types/hooks/TypesHook';
const useTypeFilter = () => {
const filterName = 'type';
const [searchParams, setSearchParams] = useSearchParams();
const { types } = useTypes();
const handleTypeFilterChange = (event) => {
const type = event.target.value;
if (type) {
searchParams.set(filterName, event.target.value);
} else {
searchParams.delete(filterName);
}
setSearchParams(searchParams);
};
return {
types,
currentTypeFilter: searchParams.get(filterName) || '',
handleTypeFilterChange,
};
};
export default useTypeFilter;

View File

@ -0,0 +1,45 @@
import { useState } from 'react';
import useModal from '../../modal/ModalHook';
import useLinesItemForm from './LinesItemFormHook';
const useLinesFormModal = (linesChangeHandle) => {
const { isModalShow, showModal, hideModal } = useModal();
const [currentId, setCurrentId] = useState(0);
const {
item,
validated,
handleSubmit,
handleChange,
resetValidity,
} = useLinesItemForm(currentId, linesChangeHandle);
const showModalDialog = (id) => {
setCurrentId(id);
resetValidity();
showModal();
};
const onClose = () => {
setCurrentId(-1);
hideModal();
};
const onSubmit = async (event) => {
if (await handleSubmit(event)) {
onClose();
}
};
return {
isFormModalShow: isModalShow,
isFormValidated: validated,
showFormModal: showModalDialog,
currentItem: item,
handleItemChange: handleChange,
handleFormSubmit: onSubmit,
handleFormClose: onClose,
};
};
export default useLinesFormModal;

View File

@ -0,0 +1,34 @@
import { useEffect, useState } from 'react';
import LinesApiService from '../service/LinesApiService';
const useLines = (typeFilter, discountFilter) => {
const [linesRefresh, setLinesRefresh] = useState(false);
const [lines, setLines] = useState([]);
const handleLinesChange = () => setLinesRefresh(!linesRefresh);
const getLines = async () => {
let expand = '?_expand=type&_expand=discount';
if (typeFilter && discountFilter) {
expand = `${expand}&typeId=${typeFilter}&discountId=${discountFilter}`;
} else if (typeFilter) {
expand = `${expand}&typeId=${typeFilter}`;
} else if (discountFilter) {
expand = `${expand}&discountId=${discountFilter}`;
}
const data = await LinesApiService.getAll(expand);
setLines(data ?? []);
};
useEffect(() => {
getLines();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [linesRefresh, typeFilter, discountFilter]);
return {
lines,
handleLinesChange,
};
};
export default useLines;

View File

@ -0,0 +1,86 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import getBase64FromFile from '../../utils/Base64';
import LinesApiService from '../service/LinesApiService';
import useLinesItem from './LinesItemHook';
import DiscountsApiService from '../../discounts/service/DiscountsApiService';
const useLinesItemForm = (id, linesChangeHandle) => {
const { item, setItem } = useLinesItem(id);
const [validated, setValidated] = useState(false);
const resetValidity = () => {
setValidated(false);
};
const getLineObject = (formData, discount) => {
const typeId = parseInt(formData.typeId, 10);
const discountId = parseInt(formData.discountId, 10);
const name = formData.name;
const price = parseFloat(formData.price).toFixed(2);
const finalPrice = parseFloat(price - (price * discount.percent * 0.01)).toFixed(2);
const image = formData.image.startsWith('data:image') ? formData.image : '';
return {
typeId: typeId.toString(),
discountId: discountId.toString(),
name: name,
price: price.toString(),
finalPrice: finalPrice.toString(),
image,
};
};
const handleImageChange = async (event) => {
const { files } = event.target;
const file = await getBase64FromFile(files.item(0));
setItem({
...item,
image: file,
});
};
const handleChange = (event) => {
if (event.target.type === 'file') {
handleImageChange(event);
return;
}
const inputName = event.target.name;
const inputValue = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
setItem({
...item,
[inputName]: inputValue,
});
};
const handleSubmit = async (event) => {
const form = event.currentTarget;
event.preventDefault();
event.stopPropagation();
const discount = await DiscountsApiService.get(item.discountId);
const body = getLineObject(item, discount);
if (form.checkValidity()) {
if (id === undefined) {
await LinesApiService.create(body);
} else {
await LinesApiService.update(id, body);
}
if (linesChangeHandle) linesChangeHandle();
toast.success('Элемент успешно сохранен', { id: 'LinesTable' });
return true;
}
setValidated(true);
return false;
};
return {
item,
validated,
handleSubmit,
handleChange,
resetValidity,
};
};
export default useLinesItemForm;

View File

@ -0,0 +1,36 @@
import { useEffect, useState } from 'react';
import LinesApiService from '../service/LinesApiService';
const useLinesItem = (id) => {
const emptyItem = {
id: '',
typeId: '',
discountId: '1',
name: '',
price: '0',
finalPrice: '0',
image: '',
};
const [item, setItem] = useState({ ...emptyItem });
const getItem = async (itemId = undefined) => {
if (itemId && itemId > 0) {
const data = await LinesApiService.get(itemId);
setItem(data);
} else {
setItem({ ...emptyItem });
}
};
useEffect(() => {
getItem(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
return {
item,
setItem,
};
};
export default useLinesItem;

View File

@ -0,0 +1,5 @@
import ApiService from '../../api/ApiService';
const LinesApiService = new ApiService('lines');
export default LinesApiService;

View File

@ -0,0 +1,75 @@
import { Button } from 'react-bootstrap';
import Select from '../../input/Select.jsx';
import ModalConfirm from '../../modal/ModalConfirm.jsx';
import ModalForm from '../../modal/ModalForm.jsx';
import LinesItemForm from '../form/LinesItemForm.jsx';
import useLinesDeleteModal from '../hooks/LinesDeleteModalHook';
import useLinesFormModal from '../hooks/LinesFormModalHook';
import LinesTable from './LinesTable.jsx';
import LinesTableRow from './LinesTableRow.jsx';
import PropTypes from 'prop-types';
const Lines = ({ lines, handleLinesChange, types, currentTypeFilter, handleTypeFilterChange, discounts, currentDiscountFilter, handleDiscountFilterChange}) => {
const {
isDeleteModalShow,
showDeleteModal,
handleDeleteConfirm,
handleDeleteCancel,
} = useLinesDeleteModal(handleLinesChange);
const {
isFormModalShow,
isFormValidated,
showFormModal,
currentItem,
handleItemChange,
handleFormSubmit,
handleFormClose,
} = useLinesFormModal(handleLinesChange);
return (
<>
<Select className='mt-2' values={types} label='Фильтр по товарам'
value={currentTypeFilter} onChange={handleTypeFilterChange} />
<Select className='mt-2' values={discounts} label='Фильтр по акциям'
value={currentDiscountFilter} onChange={handleDiscountFilterChange} />
<LinesTable>
{
lines.map((line, index) =>
<LinesTableRow key={line.id}
index={index} line={line}
onDelete={() => showDeleteModal(line.id)}
onEdit={() => showFormModal(line.id)}
/>)
}
</LinesTable>
<div className="d-flex justify-content-center">
<Button variant='primary' className="fw-bold px-5 mb-5" onClick={() => showFormModal()}>
Добавить товар
</Button>
</div>
<ModalConfirm show={isDeleteModalShow}
onConfirm={handleDeleteConfirm} onClose={handleDeleteCancel}
title='Удаление' message='Удалить элемент?' />
<ModalForm show={isFormModalShow} validated={isFormValidated}
onSubmit={handleFormSubmit} onClose={handleFormClose}
title='Редактирование'>
<LinesItemForm item={currentItem} handleChange={handleItemChange} />
</ModalForm>
</>
);
};
Lines.propTypes = {
lines: PropTypes.array,
handleLinesChange: PropTypes.func,
types: PropTypes.array,
currentTypeFilter: PropTypes.string,
handleTypeFilterChange: PropTypes.func,
discounts: PropTypes.array,
currentDiscountFilter: PropTypes.string,
handleDiscountFilterChange: PropTypes.func
}
export default Lines;

View File

@ -0,0 +1,31 @@
import PropTypes from 'prop-types';
import { Table } from 'react-bootstrap';
const LinesTable = ({ children }) => {
return (
<Table className='mt-2' striped responsive hover>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Название товара</th>
<th scope="col">Категория</th>
<th scope="col">Цена</th>
<th scope="col">Акция</th>
<th scope="col">Скидка</th>
<th scope="col">Цена с учетом акции</th>
<th scope="col" />
<th scope="col" />
</tr>
</thead>
<tbody>
{children}
</tbody >
</Table >
);
};
LinesTable.propTypes = {
children: PropTypes.node,
};
export default LinesTable;

View File

@ -0,0 +1,35 @@
import PropTypes from 'prop-types';
import { PencilFill, Trash3 } from 'react-bootstrap-icons';
const LinesTableRow = ({
index, line, onDelete, onEdit,
}) => {
const handleAnchorClick = (event, action) => {
event.preventDefault();
action();
};
return (
<tr>
<th scope="row">{index + 1}</th>
<td>{line.name}</td>
<td>{line.type.name}</td>
<td>{`${parseFloat(line.price).toFixed(2)}`}</td>
<td>{line.discount.name}</td>
<td>{`${line.discount.percent}%`}</td>
<td>{`${parseFloat(line.finalPrice).toFixed(2)}`}</td>
<td><a href="#" onClick={(event) => handleAnchorClick(event, onEdit)}><PencilFill /></a></td>
<td><a href="#" onClick={(event) => handleAnchorClick(event, onDelete)}><Trash3 /></a></td>
</tr>
);
};
LinesTableRow.propTypes = {
index: PropTypes.number,
line: PropTypes.object,
onDelete: PropTypes.func,
onEdit: PropTypes.func,
onEditInPage: PropTypes.func,
};
export default LinesTableRow;

View File

@ -0,0 +1,23 @@
import PropTypes from 'prop-types';
import {
createContext,
useState,
} from 'react';
const LoginContext = createContext(null);
export const LoginProvider = ({ children }) => {
const [login, setLogin] = useState(JSON.parse(localStorage.getItem('localLogin')));
return (
<LoginContext.Provider value={{ login, setLogin }}>
{children}
</LoginContext.Provider>
);
};
LoginProvider.propTypes = {
children: PropTypes.node,
};
export default LoginContext;

View File

@ -0,0 +1,53 @@
import { Link } from 'react-router-dom';
import { Button, Form } from 'react-bootstrap';
import PropTypes from 'prop-types';
import useLoginForm from './LoginHook';
import { useState } from 'react';
const LoginForm = ({ setNeedsRegistration }) => {
const {
formData,
validated,
correctLoginInfo,
handleSubmit,
handleChange,
} = useLoginForm();
let wrongLoginInfoWarning;
if (!correctLoginInfo) wrongLoginInfoWarning = (<p className='text-danger text-center mt-3'>Неверные Email или пароль</p>);
const [showPassword, setShowPassword] = useState(false);
return (
<>
<Form className="p-5 rounded login-form" noValidate validated={validated} onSubmit={handleSubmit}>
<p className="text-center mb-4">
Только для зарегистрированных пользователей
<br />
Нет аккаунта?{" "}
<Link to='#' className="link-info" onClick={() => setNeedsRegistration(true)}>
Зарегистрироваться
</Link>
</p>
<Form.Group className='mb-2 fw-bold' controlId='email'>
<Form.Label>E-mail</Form.Label>
<Form.Control type='email' name='email' required
value={formData.email} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-2 position-relative" controlId='password'>
<Form.Label className="fw-bold">Пароль</Form.Label>
<Form.Control type={showPassword ? 'text' : 'password'} name='password' required
value={formData.password} onChange={handleChange} />
<input className='mt-2' type="checkbox" onClick={() => setShowPassword(!showPassword)} /> Показать пароль
</Form.Group>
<Button className="w-100 fw-bold mt-3" variant='primary' type='submit'>Войти</Button>
{wrongLoginInfoWarning}
</Form>
</>
);
};
LoginForm.propTypes = {
setNeedsRegistration: PropTypes.func
};
export default LoginForm;

View File

@ -0,0 +1,61 @@
import { useState, useContext } from 'react';
import LoginContext from './LoginContext'
import AccountsApiService from '../registration/AccountsApiService';
const useLoginForm = () => {
const [validated, setValidated] = useState(false);
const [correctLoginInfo, setCorrectLoginInfo] = useState(true);
const [formData, setFormData] = useState({
email: '',
password: ''
});
const { setLogin } = useContext(LoginContext);
const LOGIN_KEY = 'localLogin';
const handleSubmit = async (event) => {
const form = event.currentTarget;
event.preventDefault();
event.stopPropagation();
if (form.checkValidity() !== false) {
const loggedInUser = (await AccountsApiService.getAll(`?email=${formData.email}`))[0];
if (loggedInUser && loggedInUser.password === formData.password){
localStorage.setItem(LOGIN_KEY, JSON.stringify(loggedInUser));
setLogin(loggedInUser);
setCorrectLoginInfo(true);
}
else {
setCorrectLoginInfo(false);
}
}
setValidated(true);
};
const logOut = () => {
localStorage.removeItem(LOGIN_KEY)
setValidated(false);
setLogin(null);
};
const handleChange = (event) => {
const inputName = event.target.name;
const inputValue = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
setFormData({
...formData,
[inputName]: inputValue,
});
};
return {
formData,
validated,
correctLoginInfo,
handleSubmit,
handleChange,
logOut
};
};
export default useLoginForm;

View File

@ -0,0 +1,7 @@
.modal-title {
font-size: 1.2rem;
}
.modal-content, .modal-content form {
background-color: #FFF8E7;
}

View File

@ -0,0 +1,42 @@
import PropTypes from 'prop-types';
import { Button, Modal } from 'react-bootstrap';
import { createPortal } from 'react-dom';
import './Modal.css';
const ModalConfirm = ({
show, title, message, onConfirm, onClose,
}) => {
return createPortal(
<Modal show={show} backdrop='static' onHide={() => onClose()}>
<Modal.Header className='pt-2 pb-2 ps-3 pe-3' closeButton>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
<Modal.Body>
{message}
</Modal.Body>
<Modal.Footer className='m-0 pt-2 pb-2 ps-3 pe-3 row justify-content-center'>
<Button variant='secondary' className='col-5 m-0 me-2'
onClick={() => onClose()}>
Нет
</Button>
<Button variant='primary' className='col-5 m-0 ms-2'
onClick={() => onConfirm()}>
Да
</Button>
</Modal.Footer>
</Modal>,
document.body,
);
};
ModalConfirm.propTypes = {
show: PropTypes.bool,
title: PropTypes.string,
message: PropTypes.string,
onConfirm: PropTypes.func,
onClose: PropTypes.func,
};
export default ModalConfirm;

View File

@ -0,0 +1,43 @@
import PropTypes from 'prop-types';
import { Button, Form, Modal } from 'react-bootstrap';
import { createPortal } from 'react-dom';
import './Modal.css';
const ModalForm = ({
show, title, validated, onSubmit, onClose, children,
}) => {
return createPortal(
<Modal show={show} backdrop='static' onHide={() => onClose()}>
<Modal.Header className='pt-2 pb-2 ps-3 pe-3' closeButton>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
<Form className='m-0 rounded-bottom' noValidate validated={validated} onSubmit={onSubmit}>
<Modal.Body>
{children}
</Modal.Body>
<Modal.Footer className='m-0 pt-2 pb-2 ps-3 pe-3 row justify-content-center'>
<Button variant='secondary' className='col-5 m-0 me-2'
onClick={() => onClose()}>
Отмена
</Button>
<Button variant='primary' className='col-5 m-0 ms-2' type='submit'>
Сохранить
</Button>
</Modal.Footer>
</Form>
</Modal>,
document.body,
);
};
ModalForm.propTypes = {
show: PropTypes.bool,
title: PropTypes.string,
validated: PropTypes.bool,
onSubmit: PropTypes.func,
onClose: PropTypes.func,
children: PropTypes.node,
};
export default ModalForm;

View File

@ -0,0 +1,21 @@
import { useState } from 'react';
const useModal = () => {
const [showModal, setShowModal] = useState(false);
const showModalDialog = () => {
setShowModal(true);
};
const hideModalDialog = () => {
setShowModal(false);
};
return {
isModalShow: showModal,
showModal: showModalDialog,
hideModal: hideModalDialog,
};
};
export default useModal;

View File

@ -5,3 +5,11 @@
.dropdown button::after{
display: none;
}
.cart-counter {
background-color: #5fc2ff;
font-size: 14px;
padding: 0 30%;
border-radius: 100px;
top: -10px;
left: 15px;
}

View File

@ -3,8 +3,19 @@ import { Link } from 'react-router-dom';
import './Navigation.css';
import { Bag, Person } from 'react-bootstrap-icons';
import Dropdown from 'react-bootstrap/Dropdown';
import useTypes from '../types/hooks/TypesHook';
import useCart from '../cart/CartHook';
import { useContext } from 'react';
import LoginContext from '../login/LoginContext.jsx'
const Navigation = () => {
const { types } = useTypes();
const { getCartCount } = useCart();
let cartCounter;
if (getCartCount() !== 0) cartCounter = (<span className='cart-counter fw-bold position-absolute'>{getCartCount()}</span>);
const { login } = useContext(LoginContext);
let accountDropdown = "Вход";
if(login) accountDropdown = "Личный кабинет";
return (
<header className="text-white text-center pt-4">
@ -12,29 +23,26 @@ const Navigation = () => {
<Navbar expand='lg' data-bs-theme='dark' className='border-top border-2 border-white justify-content-end'>
<div className="login-cart d-flex align-items-center me-1 me-lg-3 me-xl-4 me-xxl-5">
<Dropdown>
<Dropdown.Toggle className="me-2" id="dropdown-basic">
<Person size={28}/>
<Dropdown.Toggle className="me-2 d-flex align-items-end fw-bold" id="dropdown-basic">
{login?.name}&nbsp;<Person size={28}/>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-end">
<Dropdown.Item as={Link} to="/login">Вход</Dropdown.Item>
<Dropdown.Item as={Link} to="/account">{accountDropdown}</Dropdown.Item>
<Dropdown.Item as={Link} to="/admin">Панель администратора</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Link to="/cart"><Bag size={24}/></Link>
<Link className='position-relative' to="/cart"><Bag size={24}/>{cartCounter}</Link>
</div>
<Navbar.Toggle aria-controls='main-navbar' />
<Container fluid>
<Navbar.Collapse className='justify-content-center' id='main-navbar'>
<Nav className='align-items-center fs-5'>
<div className="nav-item m-2"><Link to="/catalog">новинки<div></div></Link></div>
<div className="nav-item m-2"><Link to="/catalog">верхняя одежда<div></div></Link></div>
<div className="nav-item m-2"><Link to="/catalog">брюки<div></div></Link></div>
<div className="nav-item m-2"><Link to="/catalog">футболки<div></div></Link></div>
<div className="nav-item m-2"><Link to="/catalog">рубашки<div></div></Link></div>
<div className="nav-item m-2"><Link to="/catalog">обувь<div></div></Link></div>
<div className="nav-item m-2"><Link to="/catalog">головные уборы<div></div></Link></div>
<div className="nav-item m-2"><Link to="/catalog">весь ассортимент<div></div></Link></div>
{
types.map((type) =>
<div key={type.id} className="nav-item m-2"><Link to={`/catalog?type=${type.id}`}>{type.name.toLowerCase()}<div className="rotating-stripe"></div></Link></div>)
}
<div className="nav-item m-2"><Link to="/catalog">весь ассортимент<div className="rotating-stripe"></div></Link></div>
</Nav>
</Navbar.Collapse>
</Container>

View File

@ -0,0 +1,31 @@
import { useEffect, useState } from 'react';
import OrdersApiService from '../service/OrdersApiService';
import LinesApiService from '../../lines/service/LinesApiService';
const useOrders = (accountId) => {
const [orders, setOrders] = useState([]);
const getOrders = async () => {
const orders = await OrdersApiService.getAll(`?accountId=${accountId}`);
const fullOrders = await Promise.all(orders.map(async (order) => {
return {...order, items: await Promise.all(order.items.map(async (item) => {
const line = await LinesApiService.get(item.lineId);
return {line: line, size: item.size, count: item.count};
}))
};
}))
setOrders(fullOrders.reverse() ?? []);
};
useEffect(() => {
getOrders()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return {
orders
};
};
export default useOrders;

View File

@ -0,0 +1,25 @@
import { useContext } from 'react';
import LoginContext from '../../login/LoginContext';
import OrdersApiService from '../service/OrdersApiService';
import CartContext from '../../cart/CartContext';
const usePlaceOrder = () => {
const { login } = useContext(LoginContext);
const { cart } = useContext(CartContext);
const placeOrder = async () => {
let items =[];
cart.forEach(async (cartItem) => {
items.push({lineId: cartItem.id, size: cartItem.size, count: cartItem.count});
});
const data = {date: new Date().toJSON().slice(0, 10), accountId: login.id, items: items}
await OrdersApiService.create(data);
};
return {
placeOrder
};
};
export default usePlaceOrder;

View File

@ -0,0 +1,5 @@
import ApiService from '../../api/ApiService';
const ItemsInOrdersApiService = new ApiService('items-in-orders');
export default ItemsInOrdersApiService;

View File

@ -0,0 +1,5 @@
import ApiService from '../../api/ApiService';
const OrdersApiService = new ApiService('orders');
export default OrdersApiService;

View File

@ -0,0 +1,5 @@
import ApiService from '../api/ApiService';
const AccountsApiService = new ApiService('accounts');
export default AccountsApiService;

View File

@ -0,0 +1,60 @@
import { Link } from 'react-router-dom';
import { Button, Form } from 'react-bootstrap';
import PropTypes from 'prop-types';
import useRegistrationForm from './RegistrationHook';
import { useState } from 'react';
const RegistrationForm = ({ setNeedsRegistration }) => {
const {
formData,
validated,
accountAlreadyExists,
handleSubmit,
handleChange,
} = useRegistrationForm();
let accountAlreadyExistsWarning;
if (accountAlreadyExists) accountAlreadyExistsWarning = (<p className='text-danger text-center mt-3'>Данный Email уже зарегистрирован</p>);
const [showPassword, setShowPassword] = useState(false);
return (
<>
<Form className="p-5 rounded registration-form" noValidate validated={validated} onSubmit={handleSubmit}>
<p className="text-center">
Уже есть аккаунт?{" "}
<Link to='#' className="link-info" onClick={() => setNeedsRegistration(false)}>
Войти
</Link>
</p>
<Form.Group className="mb-2" controlId='surname'>
<Form.Label className="fw-bold">Фамилия</Form.Label>
<Form.Control type='text' name='surname' required
value={formData.login} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-2" controlId='name'>
<Form.Label className="fw-bold">Имя</Form.Label>
<Form.Control type='text' name='name' required
value={formData.login} onChange={handleChange} />
</Form.Group>
<Form.Group className='mb-2 fw-bold' controlId='email'>
<Form.Label>E-mail</Form.Label>
<Form.Control type='email' name='email' required
value={formData.email} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-2" controlId='password'>
<Form.Label className="fw-bold">Пароль</Form.Label>
<Form.Control type={showPassword ? 'text' : 'password'} name='password' required
value={formData.password} onChange={handleChange} />
<input className='mt-2' type="checkbox" onClick={() => setShowPassword(!showPassword)} /> Показать пароль
</Form.Group>
<Button className="w-100 fw-bold mt-3" variant='primary' type='submit'>Зарегистрироваться</Button>
{accountAlreadyExistsWarning}
</Form>
</>
);
};
RegistrationForm.propTypes = {
setNeedsRegistration: PropTypes.func
};
export default RegistrationForm;

View File

@ -0,0 +1,53 @@
import { useState, useContext } from 'react';
import AccountsApiService from './AccountsApiService';
import LoginContext from '../login/LoginContext'
const useRegistrationForm = () => {
const [validated, setValidated] = useState(false);
const [formData, setFormData] = useState({
surname: '',
name: '',
email: '',
password: ''
});
const { setLogin } = useContext(LoginContext);
const [accountAlreadyExists, setAccountAlreadyExists] = useState(false);
const handleSubmit = async (event) => {
const form = event.currentTarget;
event.preventDefault();
event.stopPropagation();
if (form.checkValidity() !== false) {
if(!(await AccountsApiService.getAll(`?email=${formData.email}`))[0]) {
const newUser = await AccountsApiService.create(formData);
localStorage.setItem('localLogin', JSON.stringify(newUser));
setLogin(newUser);
} else {
setAccountAlreadyExists(true);
}
}
setValidated(true);
};
const handleChange = (event) => {
const inputName = event.target.name;
const inputValue = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
setFormData({
...formData,
[inputName]: inputValue,
});
};
return {
formData,
validated,
accountAlreadyExists,
handleSubmit,
handleChange
};
};
export default useRegistrationForm;

View File

@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
import TypesApiService from '../service/TypesApiService';
const useTypes = () => {
const [types, setTypes] = useState([]);
const getTypes = async () => {
const data = await TypesApiService.getAll();
setTypes(data ?? []);
};
useEffect(() => {
getTypes();
}, []);
return {
types,
};
};
export default useTypes;

View File

@ -0,0 +1,5 @@
import ApiService from '../../api/ApiService';
const TypesApiService = new ApiService('types');
export default TypesApiService;

View File

@ -0,0 +1,15 @@
const getBase64FromFile = async (file) => {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onloadend = () => {
const fileContent = reader.result;
resolve(fileContent);
};
reader.onerror = () => {
reject(new Error('Oops, something went wrong with the file reader.'));
};
reader.readAsDataURL(file);
});
};
export default getBase64FromFile;

View File

@ -38,7 +38,7 @@ nav a, nav a:visited, nav a:hover {
justify-content: center;
}
nav a div {
.rotating-stripe {
position: relative;
border-bottom: 1px white solid;
transition-property: width, transform;
@ -49,7 +49,7 @@ nav a div {
animation-duration: .4s;
}
nav a:hover div {
nav a:hover .rotating-stripe {
transform: rotateY(0);
}
@ -84,21 +84,25 @@ footer{
}
.btn-primary{
background-color: #45b6fe !important;
border-color: #45b6fe !important;
background-color: #5fc2ff !important;
border-color: #5fc2ff !important;
}
.btn-primary:hover{
background-color: #389bd9 !important;
border-color: #389bd9 !important;
}
.login-form input {
.login-form input, .registration-form input {
border: none;
border-radius: 10px;
background-color: #FFF8E7;
padding: 10px;
}
.registration-form{
width: 450px;
}
.dropdown-menu {
background-color: #4B3621;
}
@ -111,3 +115,17 @@ footer{
.dropdown a:active {
background-color: #4B3621 !important;
}
table {
--bs-table-bg: none !important;
}
.size-select {
border: 2px solid #5fc2ff;
width: 210px !important;
}
.passwordVisibilityTogler {
border: none;
background: none;
}

View File

@ -9,9 +9,10 @@ import Homepage from './pages/Homepage.jsx';
import Catalog from './pages/Catalog/Catalog.jsx';
import Cart from './pages/Cart/Cart.jsx';
import Admin from './pages/Admin.jsx';
import Login from './pages/Login.jsx';
import AccountPage from './pages/AccountPage.jsx';
import AboutUs from './pages/AboutUs.jsx';
import PhysicalStore from './pages/PhysicalStore.jsx';
import Product from './pages/Product.jsx';
const routes = [
{
@ -32,8 +33,8 @@ const routes = [
element: <Admin />,
},
{
path: '/login',
element: <Login />,
path: '/account',
element: <AccountPage />,
},
{
path: '/about-us',
@ -43,6 +44,10 @@ const routes = [
path: '/physical-store',
element: <PhysicalStore />,
},
{
path: '/product/:id?',
element: <Product />,
},
];
const router = createBrowserRouter([

37
src/pages/AccountPage.jsx Normal file
View File

@ -0,0 +1,37 @@
import LoginForm from "../components/login/LoginForm";
import RegistrationForm from "../components/registration/RegistrationForm";
import LoginContext from '../components/login/LoginContext'
import { useContext, useState } from 'react';
import Account from '../components/account/Account';
const AccountPage = () => {
const { login } = useContext(LoginContext);
const [needsRegistration, setNeedsRegistration] = useState(false);
if (!login){
if (!needsRegistration) return (
<>
<h2 className="text-center display-6 my-4">Вход</h2>
<div className="container-fluid d-flex align-items-center justify-content-center mb-5">
<LoginForm setNeedsRegistration={setNeedsRegistration}/>
</div>
</>
);
else return (
<>
<h2 className="text-center display-6 my-4">Регистрация</h2>
<div className="container-fluid d-flex align-items-center justify-content-center mb-5">
<RegistrationForm setNeedsRegistration={setNeedsRegistration} />
</div>
</>
);
}
else return (
<>
<Account />
</>
);
};
export default AccountPage;

View File

@ -1,50 +1,32 @@
import Lines from '../components/lines/table/Lines.jsx';
import Discounts from '../components/discounts/table/Discounts.jsx';
import useTypeFilter from '../components/lines/hooks/LinesFilterHook.js';
import useDiscountFilter from '../components/discounts/hooks/DiscountsFilterHook.js';
import useLines from '../components/lines/hooks/LinesHook';
const Admin = () => {
const { types, currentTypeFilter, handleTypeFilterChange } = useTypeFilter();
const { discounts, currentDiscountFilter, handleDiscountFilterChange } = useDiscountFilter();
const { lines, handleLinesChange } = useLines(currentTypeFilter, currentDiscountFilter);
return (
<>
<h2 className="text-center display-6 my-4">Панель администратора</h2>
<div className="container-lg table-responsive">
<h3>Товары</h3>
<table id="items-table" className="table table-striped table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Название товара</th>
<th scope="col">Категория</th>
<th scope="col">Цена</th>
<th scope="col">Акция</th>
<th scope="col">Скидка</th>
<th scope="col">Цена с учетом акции</th>
<th scope="col" />
<th scope="col" />
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div id="items-add" className="d-flex justify-content-center">
<button className="btn btn-primary fw-bold px-5 mb-5">
Добавить товар
</button>
<Lines lines={lines} handleLinesChange={handleLinesChange}
types={types} currentTypeFilter={currentTypeFilter}
handleTypeFilterChange={handleTypeFilterChange}
discounts={discounts} currentDiscountFilter={currentDiscountFilter}
handleDiscountFilterChange={handleDiscountFilterChange} />
</div>
<div className="container-lg table-responsive">
<h3>Акции</h3>
<table id="discounts-table" className="table table-striped table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Название акции</th>
<th scope="col">Процент скидки</th>
<th scope="col" />
<th scope="col" />
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div id="discounts-add" className="d-flex justify-content-center">
<button className="btn btn-primary fw-bold px-5 mb-5">
Добавить акцию
</button>
<Discounts handleLinesChange={handleLinesChange} />
</div>
</>
);

View File

@ -2,15 +2,11 @@
font-variant: small-caps;
background-color: #EFDECD;
}
.item-counter input{
width: 33.33333%;
border: none;
background-color: #FFF8E7;
text-align: center;
}
.item-counter button{
border: none;
width: 33.33333%;
background-color: #FFF8E7;
}
.clear-cart button{
background-color: #FFF8E7;
}
.proceed-labels {

View File

@ -1,9 +1,21 @@
import "./Cart.css"
import shirt1 from '../../assets/57010633_30_B.png';
import { ArrowRight, ArrowLeft } from 'react-bootstrap-icons';
import { Link } from 'react-router-dom';
import useCart from '../../components/cart/CartHook';
import imgPlaceholder from '../../assets/coat-hanger.png';
import { XLg } from 'react-bootstrap-icons';
import usePlaceOrder from "../../components/order/hooks/PlaceOrderHook";
const Cart = () => {
const {
cart,
getCartSum,
addToCart,
removeFromCart,
clearCart,
} = useCart();
const { placeOrder } = usePlaceOrder();
if (cart.length !== 0)
return (
<>
<h2 className="text-center display-6 my-4">Ваша корзина</h2>
@ -22,46 +34,62 @@ const Cart = () => {
<span>всего</span>
</div>
</div>
<div className="row fs-5 p-2 text-center align-items-center justify-content-between mb-5">
<div className="cart-item col-md-6 text-start d-flex align-items-center">
<img className="w-25" src={shirt1} alt="" />
<span>Printed Bowling Shirt</span>
</div>
<div className="col-2 d-md-block d-none">
<span>1 990 </span>
</div>
<div className="col-md-2 col-4 d-flex justify-content-center">
<div className="item-counter w-75 d-flex border border-1 border-dark">
<button>-</button>
<input defaultValue={1} type="text" />
<button>+</button>
{
cart.map((cartItem, index) =>
<div key={index} className="row fs-5 p-2 text-center align-items-center justify-content-between">
<div className="cart-item col-md-6 text-start d-flex align-items-center">
<img className="w-25 me-4" src={cartItem.image || imgPlaceholder} alt="" />
<span>{cartItem.name}, {cartItem.size}</span>
</div>
<div className="col-2 d-md-block d-none">
<span>{cartItem.finalPrice} </span>
</div>
<div className="col-md-2 col-4 d-flex justify-content-center">
<div className="item-counter w-75 d-flex border border-1 border-dark justify-content-evenly">
<button className="px-2" onClick={() => removeFromCart(cartItem)}>-</button>
<span>{cartItem.count}</span>
<button className="px-2" onClick={() => addToCart(cartItem)}>+</button>
</div>
</div>
<div className="col-md-2 col-4">
<span>{parseFloat(cartItem.finalPrice * cartItem.count).toFixed(2)} </span>
</div>
</div>
</div>
<div className="col-md-2 col-4">
<span>1 990 </span>
</div>
</div>
<div className="d-flex flex-column align-items-md-center">
<div className="proceed">
<div className="proceed-labels p-4 d-flex justify-content-between">
<span>Заказ на сумму</span>
<span>1 990 </span>
</div>
<div className="p-4 d-flex justify-content-between">
<Link to="/catalog" className="link-secondary text-decoration-none fw-bold d-flex align-items-center">
<ArrowLeft className="me-2" size={28}/>
Вернуться в каталог
</Link>
<button className="checkout btn btn-primary fw-bold rounded-pill d-flex align-items-center px-4">
Оформить заказ
<ArrowRight className="ms-2" size={28}/>
)
}
<div className="d-flex justify-content-end clear-cart">
<button className="py-2 px-4 border border-1 border-dark" onClick={() => clearCart()}>
<XLg /> Очистить
</button>
</div>
</div>
<div className="d-flex flex-column align-items-md-center mt-5">
<div className="proceed">
<div className="proceed-labels p-4 d-flex justify-content-between">
<span>Заказ на сумму</span>
<span>{getCartSum()} </span>
</div>
<div className="p-4 d-flex justify-content-between">
<Link to="/catalog" className="link-secondary text-decoration-none fw-bold d-flex align-items-center">
<ArrowLeft className="me-2" size={28}/>
Вернуться в каталог
</Link>
<Link to="/account" onClick={() => {placeOrder(); clearCart()}} className="checkout btn btn-primary fw-bold rounded-pill d-flex align-items-center px-4">
Оформить заказ
<ArrowRight className="ms-2" size={28}/>
</Link>
</div>
</div>
</div>
</div>
</>
);
else
return (
<>
<p className="text-center display-6 my-4">В корзине пока пусто</p>
<p className="text-center fs-4">Воспользуйтесь каталогом, чтобы выбрать товары</p>
</>
)
};
export default Cart;

View File

@ -1,61 +1,10 @@
import shirt1 from '../../assets/57010633_30_B.png';
import shirt2 from '../../assets/57067703_30_B.png';
import shirt3 from '../../assets/57077709_30_B.png';
import shirt4 from '../../assets/57020618_01_B.png';
import shirt5 from '../../assets/47065903_45_B.png';
import shirt6 from '../../assets/57010584_54_B.png';
import './Catalog.css'
import ItemsCollection from '../../components/items/ItemsCollection'
const Catalog = () => {
return (
<>
<h2 className="text-center display-6 my-4">Рубашки</h2>
<section className="product-grid-container">
<div className="product-grid">
<a className="link-light" href="">
<div className="d-flex align-items-center justify-content-center flex-column text-center">
<h4 className="fs-1 fw-bold pt-5">Printed Bowling Shirt</h4>
<p className="fs-2 p-0">1 990 </p>
</div>
<img className="img-fluid" src={shirt1} alt="" />
</a>
<a className="link-light" href="">
<div className="d-flex align-items-center justify-content-center flex-column text-center">
<h4 className="fs-1 fw-bold pt-5">Cotton fil-a-fil shirt</h4>
<p className="fs-2 p-0">4 990 </p>
</div>
<img className="img-fluid" src={shirt2} alt="" />
</a>
<a className="link-light" href="">
<div className="d-flex align-items-center justify-content-center flex-column text-center">
<h4 className="fs-1 fw-bold pt-5">Corduroy pockets overshirt</h4>
<p className="fs-2 p-0">5 990 </p>
</div>
<img className="img-fluid" src={shirt3} alt="" />
</a>
<a className="link-light" href="">
<div className="d-flex align-items-center justify-content-center flex-column text-center">
<h4 className="fs-1 fw-bold pt-5">Shirt</h4>
<p className="fs-2 p-0">1 990 </p>
</div>
<img className="img-fluid" src={shirt4} alt="" />
</a>
<a className="link-light" href="">
<div className="d-flex align-items-center justify-content-center flex-column text-center">
<h4 className="fs-1 fw-bold pt-5">Shirt</h4>
<p className="fs-2 p-0">2 990 </p>
</div>
<img className="img-fluid" src={shirt5} alt="" />
</a>
<a className="link-light" href="">
<div className="d-flex align-items-center justify-content-center flex-column text-center">
<h4 className="fs-1 fw-bold pt-5">Shirt</h4>
<p className="fs-2 p-0">3 990 </p>
</div>
<img className="img-fluid" src={shirt6} alt="" />
</a>
</div>
</section>
<ItemsCollection />
</>
);
};

View File

@ -1,63 +0,0 @@
import { Button, Form } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { useState } from 'react';
const Login = () => {
const [validated, setValidated] = useState(false);
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [showPassword, setShowPassword] = useState(false);
const handleSubmit = (event) => {
const form = event.currentTarget;
event.preventDefault();
event.stopPropagation();
if (form.checkValidity() !== false) {
console.log(formData);
}
setValidated(true);
};
const handleChange = (event) => {
const inputName = event.target.name;
const inputValue = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
setFormData({
...formData,
[inputName]: inputValue,
});
};
return (
<>
<h2 className="text-center display-6 my-4">Вход</h2>
<div className="container-fluid d-flex align-items-center justify-content-center mb-5">
<Form className="p-5 rounded login-form" noValidate validated={validated} onSubmit={handleSubmit}>
<p className="text-center mb-4">
Только для зарегистрированных пользователей
<br />
Нет аккаунта?{" "}
<Link to='#' className="link-info">
Зарегистрироваться
</Link>
</p>
<Form.Group className='mb-2 fw-bold' controlId='email'>
<Form.Label>E-mail</Form.Label>
<Form.Control type='email' name='email' required
value={formData.email} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-2 position-relative" controlId='password'>
<Form.Label className="fw-bold">Пароль</Form.Label>
<Form.Control type={showPassword ? 'text' : 'password'} name='password' required
value={formData.password} onChange={handleChange} />
<input className='mt-2' type="checkbox" onClick={() => setShowPassword(!showPassword)} /> Показать пароль
</Form.Group>
<Button className="w-100 fw-bold mt-3" variant='primary' type='submit'>Войти</Button>
</Form>
</div>
</>
);
};
export default Login;

51
src/pages/Product.jsx Normal file
View File

@ -0,0 +1,51 @@
import { useParams } from 'react-router-dom';
import { Button } from 'react-bootstrap';
import useLinesItem from '../components/lines/hooks/LinesItemHook';
import useCart from '../components/cart/CartHook';
import imgPlaceholder from '../assets/coat-hanger.png';
import { Form } from 'react-bootstrap';
import { useState } from 'react';
const Product = () => {
const { id } = useParams();
const { item } = useLinesItem(id);
const { addToCart } = useCart();
const [selectedSize, setSelectedSize] = useState('XS');
const handleSizeChange = (event) => {
setSelectedSize(event.target.value);
};
const handleAddToCart = () => {
addToCart({...item, size: selectedSize});
};
const sizes = ['XS', 'S', 'M', 'L', 'XL', 'XXL'];
return (
<>
<section className="container-sm mb-5">
<div className="row flex-sm-row">
<div className="col-sm-6 d-flex align-items-center">
<img className="img-fluid" src={item.image || imgPlaceholder} alt="" />
</div>
<div className="col-sm-6 d-flex flex-column justify-content-center align-items-center">
<h4 className="text-center fs-1 fw-bold">{item.name}</h4>
<p className="text-center fs-3 fw-bold">{item.price} </p>
<Form.Select className="fw-bold rounded-pill mb-3 w-50 size-select" aria-label="Default select example" value={selectedSize} onChange={handleSizeChange}>
{sizes.map((size) => (
<option key={size} value={size}>{size}</option>
))}
</Form.Select>
<Button variant='primary' className="fw-bold rounded-pill d-flex align-items-center px-4" onClick={handleAddToCart}>
Добавить в корзину
</Button>
</div>
</div>
</section>
</>
);
};
export default Product;