This commit is contained in:
Milana Ievlewa 2023-12-23 18:22:10 +03:00
parent c6284273f2
commit ea59a679a7
50 changed files with 1603 additions and 148 deletions

File diff suppressed because one or more lines are too long

82
data2.json Normal file

File diff suppressed because one or more lines are too long

112
db-1703331402501.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -5,7 +5,8 @@
"target": "ES2020",
"jsx": "react",
"strictNullChecks": true,
"strictFunctionTypes": true
"strictFunctionTypes": true,
"sourceMap": true
},
"exclude": [
"node_modules",

5
json-server.json Normal file
View File

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

64
node_modules/.package-lock.json generated vendored
View File

@ -1404,6 +1404,21 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -7138,6 +7153,23 @@
"react": ">=16.8.6"
}
},
"node_modules/react-datepicker": {
"version": "4.25.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.25.0.tgz",
"integrity": "sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==",
"dependencies": {
"@popperjs/core": "^2.11.8",
"classnames": "^2.2.6",
"date-fns": "^2.30.0",
"prop-types": "^15.7.2",
"react-onclickoutside": "^6.13.0",
"react-popper": "^2.3.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17 || ^18",
"react-dom": "^16.9.0 || ^17 || ^18"
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@ -7150,6 +7182,11 @@
"react": "^18.2.0"
}
},
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
},
"node_modules/react-hot-toast": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
@ -7175,6 +7212,33 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"node_modules/react-onclickoutside": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz",
"integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==",
"funding": {
"type": "individual",
"url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md"
},
"peerDependencies": {
"react": "^15.5.x || ^16.x || ^17.x || ^18.x",
"react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x"
}
},
"node_modules/react-popper": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
"integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
"dependencies": {
"react-fast-compare": "^3.0.1",
"warning": "^4.0.2"
},
"peerDependencies": {
"@popperjs/core": "^2.0.0",
"react": "^16.8.0 || ^17 || ^18",
"react-dom": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/react-refresh": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",

View File

@ -1,65 +1,98 @@
{
"hash": "b7af8b2e",
"browserHash": "decf99ed",
"hash": "6801aba0",
"browserHash": "e4e3c25e",
"optimized": {
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "16f1495f",
"fileHash": "8e331b40",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "29f3be26",
"fileHash": "2b9a0b78",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "97be66e9",
"fileHash": "a5cda4c3",
"needsInterop": true
},
"axios": {
"src": "../../axios/index.js",
"file": "axios.js",
"fileHash": "580f79f2",
"needsInterop": false
},
"prop-types": {
"src": "../../prop-types/index.js",
"file": "prop-types.js",
"fileHash": "88078dcf",
"fileHash": "b7bd6296",
"needsInterop": true
},
"react-bootstrap": {
"src": "../../react-bootstrap/esm/index.js",
"file": "react-bootstrap.js",
"fileHash": "a9ed3cdf",
"fileHash": "ceb7c5a2",
"needsInterop": false
},
"react-bootstrap-icons": {
"src": "../../react-bootstrap-icons/dist/index.js",
"file": "react-bootstrap-icons.js",
"fileHash": "fd7a753f",
"needsInterop": false
},
"react-datepicker": {
"src": "../../react-datepicker/dist/es/index.js",
"file": "react-datepicker.js",
"fileHash": "27e8d453",
"needsInterop": false
},
"react-dom": {
"src": "../../react-dom/index.js",
"file": "react-dom.js",
"fileHash": "a1f51e08",
"needsInterop": true
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "892a6605",
"fileHash": "807f9a4d",
"needsInterop": true
},
"react-hot-toast": {
"src": "../../react-hot-toast/dist/index.mjs",
"file": "react-hot-toast.js",
"fileHash": "1332f4e7",
"needsInterop": false
},
"react-router-dom": {
"src": "../../react-router-dom/dist/index.js",
"file": "react-router-dom.js",
"fileHash": "6ee5c347",
"fileHash": "c70a2c6c",
"needsInterop": false
}
},
"chunks": {
"chunk-5SWDWV4L": {
"file": "chunk-5SWDWV4L.js"
"chunk-UFCMWVQY": {
"file": "chunk-UFCMWVQY.js"
},
"chunk-A2ELGX47": {
"file": "chunk-A2ELGX47.js"
"chunk-7FSKZ4FL": {
"file": "chunk-7FSKZ4FL.js"
},
"chunk-OCHDJWGW": {
"file": "chunk-OCHDJWGW.js"
"chunk-ZT4DMYUS": {
"file": "chunk-ZT4DMYUS.js"
},
"chunk-ZVMIEU5R": {
"file": "chunk-ZVMIEU5R.js"
"chunk-2VGUONL3": {
"file": "chunk-2VGUONL3.js"
},
"chunk-UXIASGQL": {
"file": "chunk-UXIASGQL.js"
"chunk-RDZYK52F": {
"file": "chunk-RDZYK52F.js"
},
"chunk-5WWUZCGV": {
"file": "chunk-5WWUZCGV.js"
}
}
}

65
package-lock.json generated
View File

@ -17,6 +17,7 @@
"react": "^18.2.0",
"react-bootstrap": "^2.9.1",
"react-bootstrap-icons": "^1.10.3",
"react-datepicker": "^4.25.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-router-dom": "^6.21.0"
@ -1772,6 +1773,21 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -7520,6 +7536,23 @@
"react": ">=16.8.6"
}
},
"node_modules/react-datepicker": {
"version": "4.25.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.25.0.tgz",
"integrity": "sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==",
"dependencies": {
"@popperjs/core": "^2.11.8",
"classnames": "^2.2.6",
"date-fns": "^2.30.0",
"prop-types": "^15.7.2",
"react-onclickoutside": "^6.13.0",
"react-popper": "^2.3.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17 || ^18",
"react-dom": "^16.9.0 || ^17 || ^18"
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@ -7532,6 +7565,11 @@
"react": "^18.2.0"
}
},
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
},
"node_modules/react-hot-toast": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
@ -7557,6 +7595,33 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"node_modules/react-onclickoutside": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz",
"integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==",
"funding": {
"type": "individual",
"url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md"
},
"peerDependencies": {
"react": "^15.5.x || ^16.x || ^17.x || ^18.x",
"react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x"
}
},
"node_modules/react-popper": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
"integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
"dependencies": {
"react-fast-compare": "^3.0.1",
"warning": "^4.0.2"
},
"peerDependencies": {
"@popperjs/core": "^2.0.0",
"react": "^16.8.0 || ^17 || ^18",
"react-dom": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/react-refresh": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",

View File

@ -11,6 +11,7 @@
"prod": "npm-run-all lint 'vite build' --parallel rest 'vite preview'"
},
"dependencies": {
"axios": "^1.6.1",
"bootstrap": "^5.3.2",
"install": "^0.13.0",
"lorem-ipsum": "^2.0.8",
@ -19,9 +20,9 @@
"react": "^18.2.0",
"react-bootstrap": "^2.9.1",
"react-bootstrap-icons": "^1.10.3",
"react-datepicker": "^4.25.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"axios": "^1.6.1",
"react-router-dom": "^6.21.0"
},
"devDependencies": {

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,8 @@
.cart-image {
width: 3.1rem;
padding: .25rem;
}
.cart-item {
height: auto;
}

View File

@ -0,0 +1,72 @@
import { Button, ButtonGroup, Card } from 'react-bootstrap';
import { DashLg, PlusLg, XLg } from 'react-bootstrap-icons';
import './Cart.css';
import useCart from './CartHook';
import Select from '../../input/Select.jsx';
import useTypeFilter from '../hooks/LinesFilterHook';
const Cart = () => {
const { types, currentFilter, handleFilterChange } = useTypeFilter();
const {
cart,
getCartSum,
addToCart,
removeFromCart,
clearCart,
} = useCart();
return (
<main className="container-fluid ml-2 mr-2">
<div className="col-lg-4 mt-0 text-white">
<Select
className={'mt-2'}
values={types}
label="Фильтр по товарам"
value={currentFilter}
onChange={handleFilterChange}
/>
</div>
<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} 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>
</main>
);
};
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

@ -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,48 @@
import PropTypes from 'prop-types';
import { Button, Form } from 'react-bootstrap';
import { useNavigate } from 'react-router-dom';
import useLinesItemForm from '../hooks/LinesItemFormHook';
import LinesItemForm from './LinesItemForm.jsx';
const LinesForm = ({ id }) => {
const navigate = useNavigate();
const {
item,
validated,
handleSubmit,
handleChange,
} = useLinesItemForm(id);
const onBack = () => {
navigate(-1);
};
const onSubmit = async (event) => {
if (await handleSubmit(event)) {
onBack();
}
};
return (
<>
<Form className='m-0 p-2' noValidate validated={validated} onSubmit={onSubmit}>
<LinesItemForm item={item} handleChange={handleChange} />
<Form.Group className='row justify-content-center m-0 mt-3'>
<Button className='col-5 col-lg-2 m-0 me-2' variant='secondary' onClick={() => onBack()}>
Назад
</Button>
<Button className='col-5 col-lg-2 m-0 ms-2' type='submit' variant='primary'>
Сохранить
</Button>
</Form.Group>
</Form>
</>
);
};
LinesForm.propTypes = {
id: PropTypes.string,
};
export default LinesForm;

View File

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

View File

@ -0,0 +1,46 @@
import PropTypes from 'prop-types';
import Input from '../../input/Input.jsx';
import Select from '../../input/Select.jsx';
import useTypes from '../../types/hooks/TypesHook';
import usePlaces from '../../types/hooks/PlacesHook.js';
import './LinesItemForm.css';
const LinesItemForm = ({ item, handleChange }) => {
const { types } = useTypes();
const { places } = usePlaces();
return (
<>
<div className='text-center'>
<img id='image-preview' className='rounded' alt='placeholder'
src={item.image} />
</div>
<Input name='name' label='Название' value={item.name} onChange={handleChange} required />
<Select values={types} name='typeId' label='Товары' value={item.typeId} onChange={handleChange}
required />
<div>
<label htmlFor='date' style={{ display: 'block' }}>Дата:</label>
<input type='date' id='date' name='date' value={item.date} onChange={handleChange} required style={{ display: 'block' }} />
</div>
<Input name='price' label='Цена' value={item.price} onChange={handleChange}
type='number' min='1000.0' step='100' required />
<Select values={places} name='placeId' label='Место' value={item.placeId} onChange={handleChange}
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 handleFilterChange = (event) => {
const type = event.target.value;
if (type) {
searchParams.set(filterName, event.target.value);
} else {
searchParams.delete(filterName);
}
setSearchParams(searchParams);
};
return {
types,
currentFilter: searchParams.get(filterName) || '',
handleFilterChange,
};
};
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();
}
};
console.log("лйанс форм модал итем"+item);
return {
isFormModalShow: isModalShow,
isFormValidated: validated,
showFormModal: showModalDialog,
currentItem: item,
handleItemChange: handleChange,
handleFormSubmit: onSubmit,
handleFormClose: onClose,
};
};
export default useLinesFormModal;

View File

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

View File

@ -0,0 +1,83 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import getBase64FromFile from '../../utils/Base64';
import LinesApiService from '../service/LinesApiService';
import useLinesItem from './LinesItemHook';
const useLinesItemForm = (id, linesChangeHandle) => {
const { item, setItem } = useLinesItem(id);
const [validated, setValidated] = useState(false);
const resetValidity = () => {
setValidated(false);
};
const getLineObject = (formData) => {
const ItName = formData.name;
const typeId = parseInt(formData.typeId, 10);
const date = formData.date;
const price = parseFloat(formData.price).toFixed(2);
const placeId = parseInt(formData.placeId, 10);
const image = formData.image.startsWith('data:image') ? formData.image : '';
return {
ItName: ItName.toString(),
typeId: typeId.toString(),
date: date.toString(),
price: price.toString(),
placeId: placeId.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 body = getLineObject(item);
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: '',
ItName: '',
typeId: '',
date: '',
price: '0',
placeId: '',
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

@ -1,8 +1,9 @@
body{
background-color: #231e1e;
}
.button-overlayAdd button{
background-color: #ff5500;
color: #fff;
font-size: 15px;
width: 16vh;
}
@ -29,13 +30,14 @@ body{
border-radius: 5px;
text-align: center;
color:#fafafa;
object-fit: cover;
}
.product img {
border: 4px solid #1b1818;
border-radius: 5px;
max-width: 100%;
height: auto;
height: 35vh;
margin-bottom: 2%;
}

View File

@ -0,0 +1,57 @@
import Select from '../../input/Select.jsx';
import { Link } from 'react-router-dom';
import useTypeFilter from '../hooks/LinesFilterHook';
import useLines from '../hooks/LinesHook';
import usePlaces from '../../types/hooks/PlacesHook.js';
import './Catalog.css';
const Catalog = () => {
const { places } = usePlaces();
const { types, currentFilter, handleFilterChange } = useTypeFilter();
const { lines } = useLines(currentFilter, places);
console.log(lines);
return (
<>
<main className="container-fluid ml-2 mr-2">
<div className="col-lg-4 mt-0 text-white">
<Select
className={'mt-2'}
values={types}
label="Фильтр по товарам"
value={currentFilter}
onChange={handleFilterChange}
/>
</div>
<div className="container">
<div className="row">
{lines.map((product, index) => (
<div className="col-md-3 product mb-3" key={index}>
<div className="product-img">
<img src={product.image} alt={product.ItName} className="img-fluid product-image"></img>
</div>
<div className="product-info">
<h2>
<Link to={`/detailpage/${product.ItName}`} style={{ color: '#fafafa', outline: 'none' }}>
{product.ItName}
</Link>
</h2>
<p>{product.date}</p>
<p>Price: {product.price}</p>
<p>Place: {product.place.name}</p>
<div className="button-overlayAdd">
<button className="btn btn-primary" >add to cart</button>
</div>
</div>
</div>
))}
</div>
</div>
</main>
</>
);
};
export default Catalog;

View File

@ -0,0 +1,33 @@
import PropTypes from 'prop-types';
import { Table } from 'react-bootstrap';
import React from 'react';
const LinesTable = ({ children }) => {
return (
<Table className='mt-2' striped responsive>
<thead>
<tr>
<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' className='w-25'>Место проведения</th>
<th scope='col'></th>
<th scope='col'></th>
</tr>
</thead>
<tbody>
{React.Children.map(children, (child, index) => {
return React.cloneElement(child, { key: index });
})}
</tbody >
</Table >
);
};
LinesTable.propTypes = {
children: PropTypes.node,
};
export default LinesTable;

View File

@ -0,0 +1,34 @@
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.ItName}</td>
<td>{line.type.name}</td>
<td>{line.date}</td>
<td>{parseFloat(line.price).toFixed(2)}</td>
<td>{line.place.name}</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,5 @@
import ApiService from '../../api/ApiService';
const LinesApiService = new ApiService('lines');
export default LinesApiService;

View File

@ -0,0 +1,12 @@
.buttonAdd {
background-color: #ff5500;
color: #fff;
border: none;
border-radius: 4px;
font-size: 15px;
}
.buttonAdd:hover {
background-color: #5233ff;
color: aqua;
}

View File

@ -0,0 +1,92 @@
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 useTypeFilter from '../hooks/LinesFilterHook';
import useLinesFormModal from '../hooks/LinesFormModalHook';
import useLines from '../hooks/LinesHook';
import LinesTable from './LinesTable.jsx';
import LinesTableRow from './LinesTableRow.jsx';
import usePlaces from '../../types/hooks/PlacesHook.js';
import './Lines.css';
const Lines = () => {
const { places } = usePlaces();
const { types, currentFilter, handleFilterChange } = useTypeFilter();
const { lines, handleLinesChange } = useLines(currentFilter, places);
console.log(lines);
const {
isDeleteModalShow,
showDeleteModal,
handleDeleteConfirm,
handleDeleteCancel,
} = useLinesDeleteModal(handleLinesChange);
const {
isFormModalShow,
isFormValidated,
showFormModal,
currentItem,
handleItemChange,
handleFormSubmit,
handleFormClose,
} = useLinesFormModal(handleLinesChange);
console.log("Лайнс куррент итем"+currentItem);
return (
<>
<main className="container-fluid ml-2 mr-2">
<div className="row">
<div className="col-lg-8">
<LinesTable>
{lines.map((line, index) => (
<LinesTableRow
key={line.id}
index={index}
line={line}
onDelete={() => showDeleteModal(line.id)}
onEdit={() => showFormModal(line.id)}
/>
))}
</LinesTable>
</div>
<div className="col-lg-4">
<Select
className="mt-2"
values={types}
label="Фильтр по товарам"
value={currentFilter}
onChange={handleFilterChange}
/>
<Button variant="primary" className="buttonAdd" onClick={() => showFormModal()}>
Добавить товар
</Button>
</div>
</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>
</main>
</>
);
};
export default Lines;

View File

@ -0,0 +1,33 @@
import PropTypes from 'prop-types';
import { Table } from 'react-bootstrap';
import React from 'react';
const LinesTable = ({ children }) => {
return (
<Table className='mt-2' striped responsive>
<thead>
<tr>
<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' className='w-25'>Место проведения</th>
<th scope='col'></th>
<th scope='col'></th>
</tr>
</thead>
<tbody>
{React.Children.map(children, (child, index) => {
return React.cloneElement(child, { key: index });
})}
</tbody >
</Table >
);
};
LinesTable.propTypes = {
children: PropTypes.node,
};
export default LinesTable;

View File

@ -0,0 +1,34 @@
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.ItName}</td>
<td>{line.type.name}</td>
<td>{line.date}</td>
<td>{parseFloat(line.price).toFixed(2)}</td>
<td>{line.place.name}</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,3 @@
.modal-title {
font-size: 1.2rem;
}

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' 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

@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
import PlacesApiService from '../service/PlacesApiService';
const usePlaces = () => {
const [places, setPlaces] = useState([]);
const getPlaces = async () => {
const data = await PlacesApiService.getAll();
setPlaces(data ?? []);
};
useEffect(() => {
getPlaces();
}, []);
return {
places,
};
};
export default usePlaces;

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 PlacesApiService = new ApiService('places');
export default PlacesApiService;

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

@ -5,7 +5,7 @@ import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { App } from './App.jsx';
import ErrorPage from './pages/ErrorPage.jsx';
import Index from './pages/Index.jsx';
import Catalog from './pages/Catalog.jsx';
import Catalog from './pages/CatalogPage.jsx';
import Into from './pages/Into.jsx';
import Login from './pages/Login.jsx';
import Admin from './pages/Admin.jsx';

View File

@ -1,29 +1,8 @@
import '../pagescss/Admin.css';
import Lines from '../components/lines/table/Lines.jsx';
const Admin = () => {
return (
<main className="container-fluid p-2">
<div>
<table id="items-table" className="table table-striped">
<thead>
<tr>
<th scope="col" style={{ backgroundColor: '#2F2727', color: 'white' }}></th>
<th scope="col" style={{ backgroundColor: '#2F2727', color: 'white' }}>Название</th>
<th scope="col" style={{ backgroundColor: '#2F2727', color: 'white' }}>Тип события</th>
<th scope="col" style={{ backgroundColor: '#2F2727', color: 'white' }}>Дата проведения</th>
<th scope="col" style={{ backgroundColor: '#2F2727', color: 'white' }}>Стоимость</th>
<th scope="col" style={{ backgroundColor: '#2F2727', color: 'white' }}>Место проведения</th>
<th scope="col" style={{ backgroundColor: '#2F2727', color: 'white' }}></th>
<th scope="col" style={{ backgroundColor: '#2F2727', color: 'white' }}></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div className="d-flex justify-content-center" role="group">
<button id="items-add" className="btn btn-primary" style={{ fontSize: '30px' }}>Добавить</button>
</div>
</main>
<Lines />
);
};

View File

@ -1,47 +0,0 @@
import '../pagescss/Catalog.css';
import {Link} from 'react-router-dom';
const Catalog = () => {
const products = [
{ name: 'AC_DC', date: '08.12.23', price: '$10', place: 'Russia, Moscow' },
{ name: 'Deftones', date: '08.12.23', price: '$10', place: 'Russia, Moscow' },
{ name: 'Placebo', date: '08.12.23', price: '$10', place: 'Russia, Moscow' },
{ name: 'Slipknot', date: '08.12.23', price: '$10', place: 'Russia, Moscow' },
{ name: 'The Cure', date: '08.12.23', price: '$10', place: 'Russia, Moscow' },
{ name: 'Weezer', date: '08.12.23', price: '$10', place: 'Russia, Moscow' },
];
return (
<>
<main className="container-fluid p-2">
<div className="container">
<div className="row">
{products.map((product, index) => (
<div className="col-md-3 product mb-3" key={index}>
<div className="product-img">
<img src={`src/assets/pictures/${product.name}.jpg`} alt={product.name} className="img-fluid"></img>
</div>
<div className="product-info">
<h2>
<Link to={`/detailpage/${product.name}`} style={{ color: '#fafafa', outline: 'none' }}>
{product.name}
</Link>
</h2>
<p>{product.date}</p>
<p>Price: {product.price}</p>
<p>Place: {product.place}</p>
<div className="button-overlayAdd">
<button className="btn btn-primary" >add to cart</button>
</div>
</div>
</div>
))}
</div>
</div>
</main>
</>
);
};
export default Catalog;

View File

@ -0,0 +1,9 @@
import Catalog from "../components/lines/serverCatalog/Catalog";
const CatalogPage = () => {
return (
<Catalog />
);
};
export default CatalogPage;

View File

@ -1,7 +1,14 @@
import Cart from "../components/cart/Cart";
const MyTickets = () => {
return (
<>
<main className="container-fluid p-2">
<Cart />
</>
);
};
export default MyTickets;
/**<main className="container-fluid p-2">
<div className="container">
<div className="row">
<div className="col-md">
@ -35,9 +42,4 @@ const MyTickets = () => {
</div>
</div>
</div>
</main>
</>
);
};
export default MyTickets;
</main> */