setColor(color)}
18 | style={{ backgroundColor: color }}
19 | role="presentation"
20 | />
21 | ))}
22 |
23 | );
24 | };
25 |
26 | ColorChooser.propTypes = {
27 | availableColors: PropType.arrayOf(PropType.string).isRequired,
28 | onSelectedColorChange: PropType.func.isRequired
29 | };
30 |
31 | export default ColorChooser;
32 |
--------------------------------------------------------------------------------
/src/components/common/FiltersToggle.jsx:
--------------------------------------------------------------------------------
1 | import { useModal } from '@/hooks';
2 | import PropType from 'prop-types';
3 | import React from 'react';
4 | import Filters from './Filters';
5 | import Modal from './Modal';
6 |
7 | const FiltersToggle = ({ children }) => {
8 | const { isOpenModal, onOpenModal, onCloseModal } = useModal();
9 |
10 | return (
11 | <>
12 |
34 | >
35 | );
36 | };
37 |
38 | FiltersToggle.propTypes = {
39 | children: PropType.oneOfType([
40 | PropType.arrayOf(PropType.node),
41 | PropType.node
42 | ]).isRequired
43 | };
44 |
45 | export default FiltersToggle;
46 |
--------------------------------------------------------------------------------
/src/components/common/Footer.jsx:
--------------------------------------------------------------------------------
1 | import * as Route from '@/constants/routes';
2 | import logo from '@/images/logo-full.png';
3 | import React from 'react';
4 | import { useLocation } from 'react-router-dom';
5 |
6 | const Footer = () => {
7 | const { pathname } = useLocation();
8 |
9 | const visibleOnlyPath = [
10 | Route.HOME,
11 | Route.SHOP
12 | ];
13 |
14 | return !visibleOnlyPath.includes(pathname) ? null : (
15 |
41 | );
42 | };
43 |
44 | export default Footer;
45 |
--------------------------------------------------------------------------------
/src/components/common/ImageLoader.jsx:
--------------------------------------------------------------------------------
1 | import { LoadingOutlined } from '@ant-design/icons';
2 | import PropType from 'prop-types';
3 | import React, { useState } from 'react';
4 |
5 | const ImageLoader = ({ src, alt, className }) => {
6 | const loadedImages = {};
7 | const [loaded, setLoaded] = useState(loadedImages[src]);
8 |
9 | const onLoad = () => {
10 | loadedImages[src] = true;
11 | setLoaded(true);
12 | };
13 |
14 | return (
15 | <>
16 | {!loaded && (
17 |
28 | >
29 | );
30 | };
31 |
32 | ImageLoader.defaultProps = {
33 | className: 'image-loader'
34 | };
35 |
36 | ImageLoader.propTypes = {
37 | src: PropType.string.isRequired,
38 | alt: PropType.string,
39 | className: PropType.string
40 | };
41 |
42 | export default ImageLoader;
43 |
--------------------------------------------------------------------------------
/src/components/common/MessageDisplay.jsx:
--------------------------------------------------------------------------------
1 | import PropType from 'prop-types';
2 | import React from 'react';
3 |
4 | const MessageDisplay = ({
5 | message, description, buttonLabel, action
6 | }) => (
7 |
21 | );
22 |
23 | MessageDisplay.defaultProps = {
24 | description: undefined,
25 | buttonLabel: 'Okay',
26 | action: undefined
27 | };
28 |
29 | MessageDisplay.propTypes = {
30 | message: PropType.string.isRequired,
31 | description: PropType.string,
32 | buttonLabel: PropType.string,
33 | action: PropType.func
34 | };
35 |
36 | export default MessageDisplay;
37 |
--------------------------------------------------------------------------------
/src/components/common/MobileNavigation.jsx:
--------------------------------------------------------------------------------
1 | import { BasketToggle } from '@/components/basket';
2 | import { HOME, SIGNIN } from '@/constants/routes';
3 | import PropType from 'prop-types';
4 | import React from 'react';
5 | import { Link, useLocation } from 'react-router-dom';
6 | import UserNav from '@/views/account/components/UserAvatar';
7 | import Badge from './Badge';
8 | import FiltersToggle from './FiltersToggle';
9 | import SearchBar from './SearchBar';
10 |
11 | const Navigation = (props) => {
12 | const {
13 | isAuthenticating, basketLength, disabledPaths, user
14 | } = props;
15 | const { pathname } = useLocation();
16 |
17 | const onClickLink = (e) => {
18 | if (isAuthenticating) e.preventDefault();
19 | };
20 |
21 | return (
22 |
76 | );
77 | };
78 |
79 | Navigation.propTypes = {
80 | isAuthenticating: PropType.bool.isRequired,
81 | basketLength: PropType.number.isRequired,
82 | disabledPaths: PropType.arrayOf(PropType.string).isRequired,
83 | user: PropType.oneOfType([
84 | PropType.bool,
85 | PropType.object
86 | ]).isRequired
87 | };
88 |
89 | export default Navigation;
90 |
--------------------------------------------------------------------------------
/src/components/common/Modal.jsx:
--------------------------------------------------------------------------------
1 | import PropType from 'prop-types';
2 | import React from 'react';
3 | import AppModal from 'react-modal';
4 |
5 | const Modal = ({
6 | isOpen,
7 | onRequestClose,
8 | afterOpenModal,
9 | overrideStyle,
10 | children
11 | }) => {
12 | const defaultStyle = {
13 | content: {
14 | top: '50%',
15 | left: '50%',
16 | right: 'auto',
17 | bottom: 'auto',
18 | position: 'fixed',
19 | padding: '50px 20px',
20 | transition: 'all .5s ease',
21 | zIndex: 9999,
22 | marginRight: '-50%',
23 | transform: 'translate(-50%, -50%)',
24 | boxShadow: '0 5px 10px rgba(0, 0, 0, .1)',
25 | animation: 'scale .3s ease',
26 | ...overrideStyle
27 | }
28 | };
29 |
30 | AppModal.setAppElement('#app');
31 |
32 | return (
33 |
44 | );
45 | };
46 |
47 | Modal.defaultProps = {
48 | overrideStyle: {},
49 | afterOpenModal: () => { }
50 | };
51 |
52 | Modal.propTypes = {
53 | isOpen: PropType.bool.isRequired,
54 | onRequestClose: PropType.func.isRequired,
55 | afterOpenModal: PropType.func,
56 | // eslint-disable-next-line react/forbid-prop-types
57 | overrideStyle: PropType.object,
58 | children: PropType.oneOfType([
59 | PropType.arrayOf(PropType.node),
60 | PropType.node
61 | ]).isRequired
62 | };
63 |
64 | export default Modal;
65 |
--------------------------------------------------------------------------------
/src/components/common/Preloader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import logoWordmark from '../../../static/logo-wordmark.png';
3 |
4 | const Preloader = () => (
5 |
12 | );
13 |
14 | export default Preloader;
15 |
--------------------------------------------------------------------------------
/src/components/common/PriceRange/SliderRail.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-props-no-spreading */
2 | import PropType from 'prop-types';
3 | import React from 'react';
4 |
5 | const railOuterStyle = {
6 | position: 'absolute',
7 | transform: 'translate(0%, -50%)',
8 | width: '100%',
9 | height: 42,
10 | borderRadius: 7,
11 | cursor: 'pointer'
12 | // border: '1px solid grey',
13 | };
14 |
15 | const railInnerStyle = {
16 | position: 'absolute',
17 | width: '100%',
18 | height: 14,
19 | transform: 'translate(0%, -50%)',
20 | borderRadius: 7,
21 | pointerEvents: 'none',
22 | backgroundColor: '#d0d0d0'
23 | };
24 |
25 | const SliderRail = ({ getRailProps }) => (
26 |
30 | );
31 |
32 | SliderRail.propTypes = {
33 | getRailProps: PropType.func.isRequired
34 | };
35 |
36 | export default SliderRail;
37 |
--------------------------------------------------------------------------------
/src/components/common/PriceRange/Tick.jsx:
--------------------------------------------------------------------------------
1 | import PropType from 'prop-types';
2 | import React from 'react';
3 |
4 | const Tick = ({ tick, count, format }) => (
5 |
30 | );
31 |
32 | Tick.propTypes = {
33 | tick: PropType.shape({
34 | id: PropType.string.isRequired,
35 | value: PropType.number.isRequired,
36 | percent: PropType.number.isRequired
37 | }).isRequired,
38 | count: PropType.number.isRequired,
39 | format: PropType.func
40 | };
41 |
42 | Tick.defaultProps = {
43 | format: (d) => d
44 | };
45 |
46 | export default Tick;
47 |
--------------------------------------------------------------------------------
/src/components/common/PriceRange/TooltipRail.jsx:
--------------------------------------------------------------------------------
1 | import PropType from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | const railStyle = {
5 | position: 'absolute',
6 | width: '100%',
7 | transform: 'translate(0%, -50%)',
8 | height: 20,
9 | cursor: 'pointer',
10 | zIndex: 300
11 | // border: '1px solid grey',
12 | };
13 |
14 | const railCenterStyle = {
15 | position: 'absolute',
16 | width: '100%',
17 | transform: 'translate(0%, -50%)',
18 | height: 14,
19 | borderRadius: 7,
20 | cursor: 'pointer',
21 | pointerEvents: 'none',
22 | backgroundColor: '#d0d0d0'
23 | };
24 |
25 | class TooltipRail extends Component {
26 | constructor(props) {
27 | super(props);
28 |
29 | this.state = {
30 | value: null,
31 | percent: null
32 | };
33 | }
34 |
35 | onMouseEnter() {
36 | document.addEventListener('mousemove', this.onMouseMove);
37 | }
38 |
39 | onMouseLeave() {
40 | this.setState({ value: null, percent: null });
41 | document.removeEventListener('mousemove', this.onMouseMove);
42 | }
43 |
44 | onMouseMove(e) {
45 | const { activeHandleID, getEventData } = this.props;
46 |
47 | if (activeHandleID) {
48 | this.setState({ value: null, percent: null });
49 | } else {
50 | this.setState(getEventData(e));
51 | }
52 | }
53 |
54 | render() {
55 | const { value, percent } = this.state;
56 | const { activeHandleID, getRailProps } = this.props;
57 |
58 | return (
59 | <>
60 | {!activeHandleID && value ? (
61 |
86 | >
87 | );
88 | }
89 | }
90 |
91 | TooltipRail.defaultProps = {
92 | getEventData: undefined,
93 | activeHandleID: undefined,
94 | disabled: false
95 | };
96 |
97 | TooltipRail.propTypes = {
98 | getEventData: PropType.func,
99 | activeHandleID: PropType.string,
100 | getRailProps: PropType.func.isRequired,
101 | disabled: PropType.bool
102 | };
103 |
104 | export default TooltipRail;
105 |
--------------------------------------------------------------------------------
/src/components/common/PriceRange/Track.jsx:
--------------------------------------------------------------------------------
1 | import PropType from 'prop-types';
2 | import React from 'react';
3 |
4 | const Track = ({
5 | source, target, getTrackProps, disabled
6 | }) => (
7 |
22 | );
23 |
24 | Track.propTypes = {
25 | source: PropType.shape({
26 | id: PropType.string.isRequired,
27 | value: PropType.number.isRequired,
28 | percent: PropType.number.isRequired
29 | }).isRequired,
30 | target: PropType.shape({
31 | id: PropType.string.isRequired,
32 | value: PropType.number.isRequired,
33 | percent: PropType.number.isRequired
34 | }).isRequired,
35 | getTrackProps: PropType.func.isRequired,
36 | disabled: PropType.bool
37 | };
38 |
39 | Track.defaultProps = {
40 | disabled: false
41 | };
42 |
43 |
44 | export default Track;
45 |
--------------------------------------------------------------------------------
/src/components/common/SocialLogin.jsx:
--------------------------------------------------------------------------------
1 | import { FacebookOutlined, GithubFilled, GoogleOutlined } from '@ant-design/icons';
2 | import PropType from 'prop-types';
3 | import React from 'react';
4 | import { useDispatch } from 'react-redux';
5 | import { signInWithFacebook, signInWithGithub, signInWithGoogle } from '@/redux/actions/authActions';
6 |
7 | const SocialLogin = ({ isLoading }) => {
8 | const dispatch = useDispatch();
9 |
10 | const onSignInWithGoogle = () => {
11 | dispatch(signInWithGoogle());
12 | };
13 |
14 | const onSignInWithFacebook = () => {
15 | dispatch(signInWithFacebook());
16 | };
17 |
18 | const onSignInWithGithub = () => {
19 | dispatch(signInWithGithub());
20 | };
21 |
22 | return (
23 |
53 | );
54 | };
55 |
56 | SocialLogin.propTypes = {
57 | isLoading: PropType.bool.isRequired
58 | };
59 |
60 | export default SocialLogin;
61 |
--------------------------------------------------------------------------------
/src/components/common/index.js:
--------------------------------------------------------------------------------
1 | export { default as AdminNavigation } from './AdminNavigation';
2 | export { default as AdminSideBar } from './AdminSidePanel';
3 | export { default as Badge } from './Badge';
4 | export { default as Boundary } from './Boundary';
5 | export { default as ColorChooser } from './ColorChooser';
6 | export { default as Filters } from './Filters';
7 | export { default as FiltersToggle } from './FiltersToggle';
8 | export { default as Footer } from './Footer';
9 | export { default as ImageLoader } from './ImageLoader';
10 | export { default as MessageDisplay } from './MessageDisplay';
11 | export { default as MobileNavigation } from './MobileNavigation';
12 | export { default as Modal } from './Modal';
13 | export { default as Navigation } from './Navigation';
14 | export { default as Preloader } from './Preloader';
15 | export { default as PriceRange } from './PriceRange';
16 | export { default as SearchBar } from './SearchBar';
17 | export { default as SocialLogin } from './SocialLogin';
18 |
19 |
--------------------------------------------------------------------------------
/src/components/formik/CustomColorInput.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/label-has-associated-control */
2 | /* eslint-disable react/forbid-prop-types */
3 | import PropType from 'prop-types';
4 | import React from 'react';
5 |
6 | const InputColor = (props) => {
7 | const {
8 | name, form, push, remove
9 | } = props;
10 | const [selectedColor, setSelectedColor] = React.useState('');
11 |
12 | const handleColorChange = (e) => {
13 | const val = e.target.value;
14 | setSelectedColor(val);
15 | };
16 |
17 | const handleAddSelectedColor = () => {
18 | if (!form.values[name].includes(selectedColor)) {
19 | push(selectedColor);
20 | setSelectedColor('');
21 | }
22 | };
23 |
24 | return (
25 |
26 |
27 |
28 | {form.touched[name] && form.errors[name] ? (
29 |
{form.errors[name]}
30 | ) : (
31 |
34 | )}
35 | {selectedColor && (
36 | <>
37 |
38 |
44 |
45 | Add Selected Color
46 |
47 | >
48 | )}
49 |
50 |
56 |
57 |
58 |
Selected Color(s)
59 |
60 | {form.values[name]?.map((color, index) => (
61 |
remove(index)}
64 | className="color-item color-item-deletable"
65 | title={`Remove ${color}`}
66 | style={{ backgroundColor: color }}
67 | role="presentation"
68 | />
69 | ))}
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | InputColor.propTypes = {
77 | name: PropType.string.isRequired,
78 | form: PropType.shape({
79 | values: PropType.object,
80 | touched: PropType.object,
81 | errors: PropType.object
82 | }).isRequired,
83 | push: PropType.func.isRequired,
84 | remove: PropType.func.isRequired
85 | };
86 |
87 | export default InputColor;
88 |
--------------------------------------------------------------------------------
/src/components/formik/CustomCreatableSelect.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | import { useField } from 'formik';
3 | import PropType from 'prop-types';
4 | import React from 'react';
5 | import CreatableSelect from 'react-select/creatable';
6 |
7 | const CustomCreatableSelect = (props) => {
8 | const [field, meta, helpers] = useField(props);
9 | const {
10 | options, defaultValue, label, placeholder, isMulti, type, iid
11 | } = props;
12 | const { touched, error } = meta;
13 | const { setValue } = helpers;
14 |
15 | const handleChange = (newValue) => {
16 | if (Array.isArray(newValue)) {
17 | const arr = newValue.map((fieldKey) => fieldKey.value);
18 | setValue(arr);
19 | } else {
20 | setValue(newValue.value);
21 | }
22 | };
23 |
24 | const handleKeyDown = (e) => {
25 | if (type === 'number') {
26 | const { key } = e.nativeEvent;
27 | if (/\D/.test(key) && key !== 'Backspace') {
28 | e.preventDefault();
29 | }
30 | }
31 | };
32 |
33 | return (
34 |
35 | {touched && error ? (
36 | {error}
37 | ) : (
38 |
39 | )}
40 | ({
51 | ...provided,
52 | zIndex: 10
53 | }),
54 | container: (provided) => ({
55 | ...provided, marginBottom: '1.2rem'
56 | }),
57 | control: (provided) => ({
58 | ...provided,
59 | border: touched && error ? '1px solid red' : '1px solid #cacaca'
60 | })
61 | }}
62 | />
63 |
64 | );
65 | };
66 |
67 | CustomCreatableSelect.defaultProps = {
68 | isMulti: false,
69 | placeholder: '',
70 | iid: '',
71 | options: [],
72 | type: 'string'
73 | };
74 |
75 | CustomCreatableSelect.propTypes = {
76 | options: PropType.arrayOf(PropType.object),
77 | defaultValue: PropType.oneOfType([
78 | PropType.object,
79 | PropType.array
80 | ]).isRequired,
81 | label: PropType.string.isRequired,
82 | placeholder: PropType.string,
83 | isMulti: PropType.bool,
84 | type: PropType.string,
85 | iid: PropType.string
86 | };
87 |
88 | export default CustomCreatableSelect;
89 |
--------------------------------------------------------------------------------
/src/components/formik/CustomInput.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-props-no-spreading */
2 | /* eslint-disable react/forbid-prop-types */
3 | import PropType from 'prop-types';
4 | import React from 'react';
5 |
6 | const CustomInput = ({
7 | field, form: { touched, errors }, label, inputRef, ...props
8 | }) => (
9 |
10 | {touched[field.name] && errors[field.name] ? (
11 | {errors[field.name]}
12 | ) : (
13 |
14 | )}
15 |
23 |
24 | );
25 |
26 | CustomInput.defaultProps = {
27 | inputRef: undefined
28 | };
29 |
30 | CustomInput.propTypes = {
31 | label: PropType.string.isRequired,
32 | field: PropType.object.isRequired,
33 | form: PropType.object.isRequired,
34 | inputRef: PropType.oneOfType([
35 | PropType.func,
36 | PropType.shape({ current: PropType.instanceOf(Element) })
37 | ])
38 | };
39 |
40 | export default CustomInput;
41 |
--------------------------------------------------------------------------------
/src/components/formik/CustomMobileInput.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | import { useField } from 'formik';
3 | import PropType from 'prop-types';
4 | import React from 'react';
5 | import PhoneInput from 'react-phone-input-2';
6 |
7 | const CustomMobileInput = (props) => {
8 | const [field, meta, helpers] = useField(props);
9 | const { label, placeholder, defaultValue } = props;
10 | const { touched, error } = meta;
11 | const { setValue } = helpers;
12 |
13 | const handleChange = (value, data) => {
14 | const mob = {
15 | dialCode: data.dialCode,
16 | countryCode: data.countryCode,
17 | country: data.name,
18 | value
19 | };
20 |
21 | setValue(mob);
22 | };
23 |
24 | return (
25 |
26 | {touched && error ? (
27 |
{error?.value || error?.dialCode}
28 | ) : (
29 |
30 | )}
31 |
43 |
44 | );
45 | };
46 |
47 | CustomMobileInput.defaultProps = {
48 | label: 'Mobile Number',
49 | placeholder: '09254461351'
50 | };
51 |
52 | CustomMobileInput.propTypes = {
53 | label: PropType.string,
54 | placeholder: PropType.string,
55 | defaultValue: PropType.object.isRequired
56 | };
57 |
58 | export default CustomMobileInput;
59 |
--------------------------------------------------------------------------------
/src/components/formik/CustomTextarea.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-props-no-spreading */
2 | /* eslint-disable react/forbid-prop-types */
3 | import PropType from 'prop-types';
4 | import React from 'react';
5 |
6 | const CustomTextarea = ({
7 | field, form: { touched, errors }, label, ...props
8 | }) => (
9 |
10 | {touched[field.name] && errors[field.name] ? (
11 | {errors[field.name]}
12 | ) : (
13 |
14 | )}
15 |
24 |
25 | );
26 |
27 | CustomTextarea.propTypes = {
28 | label: PropType.string.isRequired,
29 | field: PropType.object.isRequired,
30 | form: PropType.object.isRequired
31 | };
32 |
33 | export default CustomTextarea;
34 |
--------------------------------------------------------------------------------
/src/components/formik/index.js:
--------------------------------------------------------------------------------
1 | export { default as CustomColorInput } from './CustomColorInput';
2 | export { default as CustomCreatableSelect } from './CustomCreatableSelect';
3 | export { default as CustomInput } from './CustomInput';
4 | export { default as CustomMobileInput } from './CustomMobileInput';
5 | export { default as CustomTextarea } from './CustomTextarea';
6 |
7 |
--------------------------------------------------------------------------------
/src/components/product/ProductFeatured.jsx:
--------------------------------------------------------------------------------
1 | import { ImageLoader } from '@/components/common';
2 | import PropType from 'prop-types';
3 | import React from 'react';
4 | import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
5 | import { useHistory } from 'react-router-dom';
6 |
7 | const ProductFeatured = ({ product }) => {
8 | const history = useHistory();
9 | const onClickItem = () => {
10 | if (!product) return;
11 |
12 | history.push(`/product/${product.id}`);
13 | };
14 |
15 | return (
16 |
17 |
18 |
19 | {product.image ? (
20 |
24 | ) : }
25 |
26 |
27 |
{product.name || }
28 |
29 | {product.brand || }
30 |
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | ProductFeatured.propTypes = {
38 | product: PropType.shape({
39 | image: PropType.string,
40 | name: PropType.string,
41 | id: PropType.string,
42 | brand: PropType.string
43 | }).isRequired
44 | };
45 |
46 | export default ProductFeatured;
47 |
--------------------------------------------------------------------------------
/src/components/product/ProductGrid.jsx:
--------------------------------------------------------------------------------
1 | import { useBasket } from '@/hooks';
2 | import PropType from 'prop-types';
3 | import React from 'react';
4 | import ProductItem from './ProductItem';
5 |
6 | const ProductGrid = ({ products }) => {
7 | const { addToBasket, isItemOnBasket } = useBasket();
8 |
9 | return (
10 |
11 | {products.length === 0 ? new Array(12).fill({}).map((product, index) => (
12 |
17 | )) : products.map((product) => (
18 |
24 | ))}
25 |
26 | );
27 | };
28 |
29 | ProductGrid.propTypes = {
30 | // eslint-disable-next-line react/forbid-prop-types
31 | products: PropType.array.isRequired
32 | };
33 |
34 | export default ProductGrid;
35 |
--------------------------------------------------------------------------------
/src/components/product/ProductList.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | import { Boundary, MessageDisplay } from '@/components/common';
3 | import PropType from 'prop-types';
4 | import React, { useEffect, useState } from 'react';
5 | import { useDispatch } from 'react-redux';
6 | import { setLoading } from '@/redux/actions/miscActions';
7 | import { getProducts } from '@/redux/actions/productActions';
8 |
9 | const ProductList = (props) => {
10 | const {
11 | products, filteredProducts, isLoading, requestStatus, children
12 | } = props;
13 | const [isFetching, setFetching] = useState(false);
14 | const dispatch = useDispatch();
15 |
16 | const fetchProducts = () => {
17 | setFetching(true);
18 | dispatch(getProducts(products.lastRefKey));
19 | };
20 |
21 | useEffect(() => {
22 | if (products.items.length === 0 || !products.lastRefKey) {
23 | fetchProducts();
24 | }
25 |
26 | window.scrollTo(0, 0);
27 | return () => dispatch(setLoading(false));
28 | }, []);
29 |
30 | useEffect(() => {
31 | setFetching(false);
32 | }, [products.lastRefKey]);
33 |
34 | if (filteredProducts.length === 0 && !isLoading) {
35 | return (
36 |
37 | );
38 | } if (filteredProducts.length === 0 && requestStatus) {
39 | return (
40 |
45 | );
46 | }
47 | return (
48 |
49 | {children}
50 | {/* Show 'Show More' button if products length is less than total products */}
51 | {products.items.length < products.total && (
52 |
53 |
61 |
62 | )}
63 |
64 | );
65 | };
66 |
67 | ProductList.defaultProps = {
68 | requestStatus: null
69 | };
70 |
71 | ProductList.propTypes = {
72 | products: PropType.object.isRequired,
73 | filteredProducts: PropType.array.isRequired,
74 | isLoading: PropType.bool.isRequired,
75 | requestStatus: PropType.string,
76 | children: PropType.oneOfType([
77 | PropType.arrayOf(PropType.node),
78 | PropType.node
79 | ]).isRequired
80 | };
81 |
82 | export default ProductList;
83 |
--------------------------------------------------------------------------------
/src/components/product/ProductShowcaseGrid.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | import { FeaturedProduct } from '@/components/product';
3 | import PropType from 'prop-types';
4 | import React from 'react';
5 |
6 | const ProductShowcase = ({ products, skeletonCount }) => (
7 |
8 | {(products.length === 0) ? new Array(skeletonCount).fill({}).map((product, index) => (
9 |
14 | )) : products.map((product) => (
15 |
19 | ))}
20 |
21 | );
22 |
23 | ProductShowcase.defaultProps = {
24 | skeletonCount: 4
25 | };
26 |
27 | ProductShowcase.propTypes = {
28 | products: PropType.array.isRequired,
29 | skeletonCount: PropType.number
30 | };
31 |
32 | export default ProductShowcase;
33 |
--------------------------------------------------------------------------------
/src/components/product/index.js:
--------------------------------------------------------------------------------
1 | export { default as AppliedFilters } from './ProductAppliedFilters';
2 | export { default as FeaturedProduct } from './ProductFeatured';
3 | export { default as ProductGrid } from './ProductGrid';
4 | export { default as ProductItem } from './ProductItem';
5 | export { default as ProductList } from './ProductList';
6 | export { default as ProductSearch } from './ProductSearch';
7 | export { default as ProductShowcaseGrid } from './ProductShowcaseGrid';
8 |
9 |
--------------------------------------------------------------------------------
/src/constants/index.js:
--------------------------------------------------------------------------------
1 | export * as actionType from './constants';
2 | export * as route from './routes';
3 |
4 |
--------------------------------------------------------------------------------
/src/constants/routes.js:
--------------------------------------------------------------------------------
1 | export const HOME = '/';
2 | export const SHOP = '/shop';
3 | export const FEATURED_PRODUCTS = '/featured';
4 | export const RECOMMENDED_PRODUCTS = '/recommended';
5 | export const ACCOUNT = '/account';
6 | export const ACCOUNT_EDIT = '/account/edit';
7 | export const ADMIN_DASHBOARD = '/admin/dashboard';
8 | export const ADMIN_PRODUCTS = '/admin/products';
9 | export const ADMIN_USERS = '/admin/users';
10 | export const ADD_PRODUCT = '/admin/add';
11 | export const EDIT_PRODUCT = '/admin/edit';
12 | export const SEARCH = '/search/:searchKey';
13 | export const SIGNIN = '/signin';
14 | export const SIGNOUT = '/signout';
15 | export const SIGNUP = '/signup';
16 | export const FORGOT_PASSWORD = '/forgot_password';
17 | export const CHECKOUT_STEP_1 = '/checkout/step1';
18 | export const CHECKOUT_STEP_2 = '/checkout/step2';
19 | export const CHECKOUT_STEP_3 = '/checkout/step3';
20 | export const VIEW_PRODUCT = '/product/:id';
21 |
--------------------------------------------------------------------------------
/src/helpers/utils.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-nested-ternary */
2 | export const displayDate = (timestamp) => {
3 | const date = new Date(timestamp);
4 |
5 | const monthNames = [
6 | 'January', 'February', 'March', 'April', 'May', 'June', 'July',
7 | 'August', 'September', 'October', 'November', 'December'
8 | ];
9 |
10 | const day = date.getDate();
11 | const monthIndex = date.getMonth();
12 | const year = date.getFullYear();
13 |
14 | // return day + ' ' + monthNames[monthIndex] + ' ' + year;
15 | return `${monthNames[monthIndex]} ${day}, ${year}`;
16 | };
17 |
18 | export const displayMoney = (n) => {
19 | const format = new Intl.NumberFormat('en-US', {
20 | style: 'currency',
21 | currency: 'USD'
22 | });
23 |
24 | // or use toLocaleString()
25 | return format.format(n);
26 | };
27 |
28 | export const calculateTotal = (arr) => {
29 | if (!arr || arr?.length === 0) return 0;
30 |
31 | const total = arr.reduce((acc, val) => acc + val, 0);
32 |
33 | return total.toFixed(2);
34 | };
35 |
36 | export const displayActionMessage = (msg, status = 'info') => {
37 | const div = document.createElement('div');
38 | const span = document.createElement('span');
39 |
40 | div.className = `toast ${status === 'info'
41 | ? 'toast-info'
42 | : status === 'success'
43 | ? 'toast-success'
44 | : 'toast-error'
45 | // eslint-disable-next-line indent
46 | }`;
47 | span.className = 'toast-msg';
48 | span.textContent = msg;
49 | div.appendChild(span);
50 |
51 |
52 | if (document.querySelector('.toast')) {
53 | document.body.removeChild(document.querySelector('.toast'));
54 | document.body.appendChild(div);
55 | } else {
56 | document.body.appendChild(div);
57 | }
58 |
59 | setTimeout(() => {
60 | try {
61 | document.body.removeChild(div);
62 | } catch (e) {
63 | console.log(e);
64 | }
65 | }, 3000);
66 | };
67 |
--------------------------------------------------------------------------------
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | export { default as useBasket } from './useBasket';
2 | export { default as useDidMount } from './useDidMount';
3 | export { default as useDocumentTitle } from './useDocumentTitle';
4 | export { default as useFeaturedProducts } from './useFeaturedProducts';
5 | export { default as useFileHandler } from './useFileHandler';
6 | export { default as useModal } from './useModal';
7 | export { default as useProduct } from './useProduct';
8 | export { default as useRecommendedProducts } from './useRecommendedProducts';
9 | export { default as useScrollTop } from './useScrollTop';
10 |
11 |
--------------------------------------------------------------------------------
/src/hooks/useBasket.js:
--------------------------------------------------------------------------------
1 | import { displayActionMessage } from '@/helpers/utils';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { addToBasket as dispatchAddToBasket, removeFromBasket } from '@/redux/actions/basketActions';
4 |
5 | const useBasket = () => {
6 | const { basket } = useSelector((state) => ({ basket: state.basket }));
7 | const dispatch = useDispatch();
8 |
9 | const isItemOnBasket = (id) => !!basket.find((item) => item.id === id);
10 |
11 | const addToBasket = (product) => {
12 | if (isItemOnBasket(product.id)) {
13 | dispatch(removeFromBasket(product.id));
14 | displayActionMessage('Item removed from basket', 'info');
15 | } else {
16 | dispatch(dispatchAddToBasket(product));
17 | displayActionMessage('Item added to basket', 'success');
18 | }
19 | };
20 |
21 | return { basket, isItemOnBasket, addToBasket };
22 | };
23 |
24 | export default useBasket;
25 |
--------------------------------------------------------------------------------
/src/hooks/useDidMount.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | const useDidMount = (initState = false) => {
4 | const [didMount, setDidMount] = useState(initState);
5 |
6 | useEffect(() => {
7 | setDidMount(true);
8 |
9 | return () => {
10 | setDidMount(false);
11 | };
12 | }, []);
13 |
14 | return didMount;
15 | };
16 |
17 | export default useDidMount;
18 |
--------------------------------------------------------------------------------
/src/hooks/useDocumentTitle.js:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect } from 'react';
2 |
3 | const useDocumentTitle = (title) => {
4 | useLayoutEffect(() => {
5 | if (title) {
6 | document.title = title;
7 | } else {
8 | document.title = 'Salinaka - eCommerce React App';
9 | }
10 | }, [title]);
11 | };
12 |
13 | export default useDocumentTitle;
14 |
--------------------------------------------------------------------------------
/src/hooks/useFeaturedProducts.js:
--------------------------------------------------------------------------------
1 | import { useDidMount } from '@/hooks';
2 | import { useEffect, useState } from 'react';
3 | import firebase from '@/services/firebase';
4 |
5 | const useFeaturedProducts = (itemsCount) => {
6 | const [featuredProducts, setFeaturedProducts] = useState([]);
7 | const [isLoading, setLoading] = useState(false);
8 | const [error, setError] = useState('');
9 | const didMount = useDidMount(true);
10 |
11 | const fetchFeaturedProducts = async () => {
12 | try {
13 | setLoading(true);
14 | setError('');
15 |
16 | const docs = await firebase.getFeaturedProducts(itemsCount);
17 |
18 | if (docs.empty) {
19 | if (didMount) {
20 | setError('No featured products found.');
21 | setLoading(false);
22 | }
23 | } else {
24 | const items = [];
25 |
26 | docs.forEach((snap) => {
27 | const data = snap.data();
28 | items.push({ id: snap.ref.id, ...data });
29 | });
30 |
31 | if (didMount) {
32 | setFeaturedProducts(items);
33 | setLoading(false);
34 | }
35 | }
36 | } catch (e) {
37 | if (didMount) {
38 | setError('Failed to fetch featured products');
39 | setLoading(false);
40 | }
41 | }
42 | };
43 |
44 | useEffect(() => {
45 | if (featuredProducts.length === 0 && didMount) {
46 | fetchFeaturedProducts();
47 | }
48 | }, []);
49 |
50 | return {
51 | featuredProducts, fetchFeaturedProducts, isLoading, error
52 | };
53 | };
54 |
55 | export default useFeaturedProducts;
56 |
--------------------------------------------------------------------------------
/src/hooks/useFileHandler.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-alert */
2 | import { useState } from 'react';
3 | import { v4 as uuidv4 } from 'uuid';
4 |
5 | const useFileHandler = (initState) => {
6 | const [imageFile, setImageFile] = useState(initState);
7 | const [isFileLoading, setFileLoading] = useState(false);
8 |
9 | const removeImage = ({ id, name }) => {
10 | const items = imageFile[name].filter((item) => item.id !== id);
11 |
12 | setImageFile({
13 | ...imageFile,
14 | [name]: items
15 | });
16 | };
17 |
18 | const onFileChange = (event, { name, type }) => {
19 | const val = event.target.value;
20 | const img = event.target.files[0];
21 | const size = img.size / 1024 / 1024;
22 | const regex = /(\.jpg|\.jpeg|\.png)$/i;
23 |
24 | setFileLoading(true);
25 | if (!regex.exec(val)) {
26 | alert('File type must be JPEG or PNG', 'error');
27 | setFileLoading(false);
28 | } else if (size > 0.5) {
29 | alert('File size exceeded 500kb, consider optimizing your image', 'error');
30 | setFileLoading(false);
31 | } else if (type === 'multiple') {
32 | Array.from(event.target.files).forEach((file) => {
33 | const reader = new FileReader();
34 | reader.addEventListener('load', (e) => {
35 | setImageFile((oldFiles) => ({
36 | ...oldFiles,
37 | [name]: [...oldFiles[name], { file, url: e.target.result, id: uuidv4() }]
38 | }));
39 | });
40 | reader.readAsDataURL(file);
41 | });
42 |
43 | setFileLoading(false);
44 | } else { // type is single
45 | const reader = new FileReader();
46 |
47 | reader.addEventListener('load', (e) => {
48 | setImageFile({
49 | ...imageFile,
50 | [name]: { file: img, url: e.target.result }
51 | });
52 | setFileLoading(false);
53 | });
54 | reader.readAsDataURL(img);
55 | }
56 | };
57 |
58 | return {
59 | imageFile,
60 | setImageFile,
61 | isFileLoading,
62 | onFileChange,
63 | removeImage
64 | };
65 | };
66 |
67 | export default useFileHandler;
68 |
--------------------------------------------------------------------------------
/src/hooks/useModal.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const useModal = () => {
4 | const [isOpenModal, setModalOpen] = useState(false);
5 |
6 | const onOpenModal = () => {
7 | setModalOpen(true);
8 | };
9 |
10 | const onCloseModal = () => {
11 | setModalOpen(false);
12 | };
13 |
14 | return { isOpenModal, onOpenModal, onCloseModal };
15 | };
16 |
17 | export default useModal;
18 |
--------------------------------------------------------------------------------
/src/hooks/useProduct.js:
--------------------------------------------------------------------------------
1 | import { useDidMount } from '@/hooks';
2 | import { useEffect, useState } from 'react';
3 | import { useSelector } from 'react-redux';
4 | import firebase from '@/services/firebase';
5 |
6 | const useProduct = (id) => {
7 | // get and check if product exists in store
8 | const storeProduct = useSelector((state) => state.products.items.find((item) => item.id === id));
9 |
10 | const [product, setProduct] = useState(storeProduct);
11 | const [isLoading, setLoading] = useState(false);
12 | const [error, setError] = useState(null);
13 | const didMount = useDidMount(true);
14 |
15 | useEffect(() => {
16 | (async () => {
17 | try {
18 | if (!product || product.id !== id) {
19 | setLoading(true);
20 | const doc = await firebase.getSingleProduct(id);
21 |
22 | if (doc.exists) {
23 | const data = { ...doc.data(), id: doc.ref.id };
24 |
25 | if (didMount) {
26 | setProduct(data);
27 | setLoading(false);
28 | }
29 | } else {
30 | setError('Product not found.');
31 | }
32 | }
33 | } catch (err) {
34 | if (didMount) {
35 | setLoading(false);
36 | setError(err?.message || 'Something went wrong.');
37 | }
38 | }
39 | })();
40 | }, [id]);
41 |
42 | return { product, isLoading, error };
43 | };
44 |
45 | export default useProduct;
46 |
--------------------------------------------------------------------------------
/src/hooks/useRecommendedProducts.js:
--------------------------------------------------------------------------------
1 | import { useDidMount } from '@/hooks';
2 | import { useEffect, useState } from 'react';
3 | import firebase from '@/services/firebase';
4 |
5 | const useRecommendedProducts = (itemsCount) => {
6 | const [recommendedProducts, setRecommendedProducts] = useState([]);
7 | const [isLoading, setLoading] = useState(false);
8 | const [error, setError] = useState('');
9 | const didMount = useDidMount(true);
10 |
11 | const fetchRecommendedProducts = async () => {
12 | try {
13 | setLoading(true);
14 | setError('');
15 |
16 | const docs = await firebase.getRecommendedProducts(itemsCount);
17 |
18 | if (docs.empty) {
19 | if (didMount) {
20 | setError('No recommended products found.');
21 | setLoading(false);
22 | }
23 | } else {
24 | const items = [];
25 |
26 | docs.forEach((snap) => {
27 | const data = snap.data();
28 | items.push({ id: snap.ref.id, ...data });
29 | });
30 |
31 | if (didMount) {
32 | setRecommendedProducts(items);
33 | setLoading(false);
34 | }
35 | }
36 | } catch (e) {
37 | if (didMount) {
38 | setError('Failed to fetch recommended products');
39 | setLoading(false);
40 | }
41 | }
42 | };
43 |
44 | useEffect(() => {
45 | if (recommendedProducts.length === 0 && didMount) {
46 | fetchRecommendedProducts();
47 | }
48 | }, []);
49 |
50 |
51 | return {
52 | recommendedProducts, fetchRecommendedProducts, isLoading, error
53 | };
54 | };
55 |
56 | export default useRecommendedProducts;
57 |
--------------------------------------------------------------------------------
/src/hooks/useScrollTop.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | const useScrollTop = () => {
4 | useEffect(() => {
5 | window.scrollTo(0, 0);
6 | }, []);
7 | };
8 |
9 | export default useScrollTop;
10 |
--------------------------------------------------------------------------------
/src/images/banner-girl-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgudo/ecommerce-react/8d37028200dc1475d78f27d3b352a4d0384a4ab1/src/images/banner-girl-1.png
--------------------------------------------------------------------------------
/src/images/banner-girl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgudo/ecommerce-react/8d37028200dc1475d78f27d3b352a4d0384a4ab1/src/images/banner-girl.png
--------------------------------------------------------------------------------
/src/images/banner-guy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgudo/ecommerce-react/8d37028200dc1475d78f27d3b352a4d0384a4ab1/src/images/banner-guy.png
--------------------------------------------------------------------------------
/src/images/creditcards.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgudo/ecommerce-react/8d37028200dc1475d78f27d3b352a4d0384a4ab1/src/images/creditcards.png
--------------------------------------------------------------------------------
/src/images/defaultAvatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgudo/ecommerce-react/8d37028200dc1475d78f27d3b352a4d0384a4ab1/src/images/defaultAvatar.jpg
--------------------------------------------------------------------------------
/src/images/defaultBanner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgudo/ecommerce-react/8d37028200dc1475d78f27d3b352a4d0384a4ab1/src/images/defaultBanner.jpg
--------------------------------------------------------------------------------
/src/images/logo-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgudo/ecommerce-react/8d37028200dc1475d78f27d3b352a4d0384a4ab1/src/images/logo-full.png
--------------------------------------------------------------------------------
/src/images/logo-wordmark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgudo/ecommerce-react/8d37028200dc1475d78f27d3b352a4d0384a4ab1/src/images/logo-wordmark.png
--------------------------------------------------------------------------------
/src/images/square.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgudo/ecommerce-react/8d37028200dc1475d78f27d3b352a4d0384a4ab1/src/images/square.jpg
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import { Preloader } from '@/components/common';
2 | import 'normalize.css/normalize.css';
3 | import React from 'react';
4 | import { render } from 'react-dom';
5 | import 'react-phone-input-2/lib/style.css';
6 | import { onAuthStateFail, onAuthStateSuccess } from '@/redux/actions/authActions';
7 | import configureStore from '@/redux/store/store';
8 | import '@/styles/style.scss';
9 | import WebFont from 'webfontloader';
10 | import App from './App';
11 | import firebase from '@/services/firebase';
12 |
13 | WebFont.load({
14 | google: {
15 | families: ['Tajawal']
16 | }
17 | });
18 |
19 | const { store, persistor } = configureStore();
20 | const root = document.getElementById('app');
21 |
22 | // Render the preloader on initial load
23 | render(
, root);
24 |
25 | firebase.auth.onAuthStateChanged((user) => {
26 | if (user) {
27 | store.dispatch(onAuthStateSuccess(user));
28 | } else {
29 | store.dispatch(onAuthStateFail('Failed to authenticate'));
30 | }
31 | // then render the app after checking the auth state
32 | render(
, root);
33 | });
34 |
35 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
36 | window.addEventListener('load', () => {
37 | navigator.serviceWorker.register('/sw.js').then((registration) => {
38 | console.log('SW registered: ', registration);
39 | }).catch((registrationError) => {
40 | console.log('SW registration failed: ', registrationError);
41 | });
42 | });
43 | }
44 |
--------------------------------------------------------------------------------
/src/redux/actions/authActions.js:
--------------------------------------------------------------------------------
1 | import * as type from '@/constants/constants';
2 |
3 | export const signIn = (email, password) => ({
4 | type: type.SIGNIN,
5 | payload: {
6 | email,
7 | password
8 | }
9 | });
10 |
11 | export const signInWithGoogle = () => ({
12 | type: type.SIGNIN_WITH_GOOGLE
13 | });
14 |
15 | export const signInWithFacebook = () => ({
16 | type: type.SIGNIN_WITH_FACEBOOK
17 | });
18 |
19 | export const signInWithGithub = () => ({
20 | type: type.SIGNIN_WITH_GITHUB
21 | });
22 |
23 | export const signUp = (user) => ({
24 | type: type.SIGNUP,
25 | payload: user
26 | });
27 |
28 | export const signInSuccess = (auth) => ({
29 | type: type.SIGNIN_SUCCESS,
30 | payload: auth
31 | });
32 |
33 | export const setAuthPersistence = () => ({
34 | type: type.SET_AUTH_PERSISTENCE
35 | });
36 |
37 | export const signOut = () => ({
38 | type: type.SIGNOUT
39 | });
40 |
41 | export const signOutSuccess = () => ({
42 | type: type.SIGNOUT_SUCCESS
43 | });
44 |
45 | export const onAuthStateChanged = () => ({
46 | type: type.ON_AUTHSTATE_CHANGED
47 | });
48 |
49 | export const onAuthStateSuccess = (user) => ({
50 | type: type.ON_AUTHSTATE_SUCCESS,
51 | payload: user
52 | });
53 |
54 | export const onAuthStateFail = (error) => ({
55 | type: type.ON_AUTHSTATE_FAIL,
56 | payload: error
57 | });
58 |
59 | export const resetPassword = (email) => ({
60 | type: type.RESET_PASSWORD,
61 | payload: email
62 | });
63 |
64 |
--------------------------------------------------------------------------------
/src/redux/actions/basketActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_QTY_ITEM, ADD_TO_BASKET,
3 | CLEAR_BASKET,
4 | MINUS_QTY_ITEM, REMOVE_FROM_BASKET,
5 | SET_BASKET_ITEMS
6 | } from '@/constants/constants';
7 |
8 | export const setBasketItems = (items = []) => ({
9 | type: SET_BASKET_ITEMS,
10 | payload: items
11 | });
12 |
13 | export const addToBasket = (product) => ({
14 | type: ADD_TO_BASKET,
15 | payload: product
16 | });
17 |
18 | export const removeFromBasket = (id) => ({
19 | type: REMOVE_FROM_BASKET,
20 | payload: id
21 | });
22 |
23 | export const clearBasket = () => ({
24 | type: CLEAR_BASKET
25 | });
26 |
27 | export const addQtyItem = (id) => ({
28 | type: ADD_QTY_ITEM,
29 | payload: id
30 | });
31 |
32 | export const minusQtyItem = (id) => ({
33 | type: MINUS_QTY_ITEM,
34 | payload: id
35 | });
36 |
--------------------------------------------------------------------------------
/src/redux/actions/checkoutActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | RESET_CHECKOUT, SET_CHECKOUT_PAYMENT_DETAILS, SET_CHECKOUT_SHIPPING_DETAILS
3 | } from '@/constants/constants';
4 |
5 | export const setShippingDetails = (details) => ({
6 | type: SET_CHECKOUT_SHIPPING_DETAILS,
7 | payload: details
8 | });
9 |
10 | export const setPaymentDetails = (details) => ({
11 | type: SET_CHECKOUT_PAYMENT_DETAILS,
12 | payload: details
13 | });
14 |
15 | export const resetCheckout = () => ({
16 | type: RESET_CHECKOUT
17 | });
18 |
--------------------------------------------------------------------------------
/src/redux/actions/filterActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | APPLY_FILTER,
3 | CLEAR_RECENT_SEARCH,
4 | REMOVE_SELECTED_RECENT, RESET_FILTER, SET_BRAND_FILTER,
5 | SET_MAX_PRICE_FILTER,
6 | SET_MIN_PRICE_FILTER, SET_TEXT_FILTER
7 | } from '@/constants/constants';
8 |
9 | export const setTextFilter = (keyword) => ({
10 | type: SET_TEXT_FILTER,
11 | payload: keyword
12 | });
13 |
14 | export const setBrandFilter = (brand) => ({
15 | type: SET_BRAND_FILTER,
16 | payload: brand
17 | });
18 |
19 | export const setMinPriceFilter = (min) => ({
20 | type: SET_MIN_PRICE_FILTER,
21 | payload: min
22 | });
23 |
24 | export const setMaxPriceFilter = (max) => ({
25 | type: SET_MAX_PRICE_FILTER,
26 | payload: max
27 | });
28 |
29 | export const resetFilter = () => ({
30 | type: RESET_FILTER
31 | });
32 |
33 | export const clearRecentSearch = () => ({
34 | type: CLEAR_RECENT_SEARCH
35 | });
36 |
37 | export const removeSelectedRecent = (keyword) => ({
38 | type: REMOVE_SELECTED_RECENT,
39 | payload: keyword
40 | });
41 |
42 | export const applyFilter = (filters) => ({
43 | type: APPLY_FILTER,
44 | payload: filters
45 | });
46 |
--------------------------------------------------------------------------------
/src/redux/actions/index.js:
--------------------------------------------------------------------------------
1 | export * as authActions from './authActions';
2 | export * as basketActions from './basketActions';
3 | export * as checkoutActions from './checkoutActions';
4 | export * as filterActions from './filterActions';
5 | export * as miscActions from './miscActions';
6 | export * as productActions from './productActions';
7 | export * as profileActions from './profileActions';
8 | export * as userActions from './userActions';
9 |
10 |
--------------------------------------------------------------------------------
/src/redux/actions/miscActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | IS_AUTHENTICATING, LOADING, SET_AUTH_STATUS, SET_REQUEST_STATUS
3 | } from '@/constants/constants';
4 |
5 | export const setLoading = (bool = true) => ({
6 | type: LOADING,
7 | payload: bool
8 | });
9 |
10 | export const setAuthenticating = (bool = true) => ({
11 | type: IS_AUTHENTICATING,
12 | payload: bool
13 | });
14 |
15 | export const setRequestStatus = (status) => ({
16 | type: SET_REQUEST_STATUS,
17 | payload: status
18 | });
19 |
20 |
21 | export const setAuthStatus = (status = null) => ({
22 | type: SET_AUTH_STATUS,
23 | payload: status
24 | });
25 |
--------------------------------------------------------------------------------
/src/redux/actions/productActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_PRODUCT,
3 | ADD_PRODUCT_SUCCESS,
4 | CANCEL_GET_PRODUCTS,
5 | CLEAR_SEARCH_STATE,
6 | EDIT_PRODUCT,
7 | EDIT_PRODUCT_SUCCESS,
8 | GET_PRODUCTS,
9 | GET_PRODUCTS_SUCCESS,
10 | REMOVE_PRODUCT,
11 | REMOVE_PRODUCT_SUCCESS,
12 | SEARCH_PRODUCT,
13 | SEARCH_PRODUCT_SUCCESS
14 | } from '@/constants/constants';
15 |
16 | export const getProducts = (lastRef) => ({
17 | type: GET_PRODUCTS,
18 | payload: lastRef
19 | });
20 |
21 | export const getProductsSuccess = (products) => ({
22 | type: GET_PRODUCTS_SUCCESS,
23 | payload: products
24 | });
25 |
26 | export const cancelGetProducts = () => ({
27 | type: CANCEL_GET_PRODUCTS
28 | });
29 |
30 | export const addProduct = (product) => ({
31 | type: ADD_PRODUCT,
32 | payload: product
33 | });
34 |
35 | export const searchProduct = (searchKey) => ({
36 | type: SEARCH_PRODUCT,
37 | payload: {
38 | searchKey
39 | }
40 | });
41 |
42 | export const searchProductSuccess = (products) => ({
43 | type: SEARCH_PRODUCT_SUCCESS,
44 | payload: products
45 | });
46 |
47 | export const clearSearchState = () => ({
48 | type: CLEAR_SEARCH_STATE
49 | });
50 |
51 | export const addProductSuccess = (product) => ({
52 | type: ADD_PRODUCT_SUCCESS,
53 | payload: product
54 | });
55 |
56 | export const removeProduct = (id) => ({
57 | type: REMOVE_PRODUCT,
58 | payload: id
59 | });
60 |
61 | export const removeProductSuccess = (id) => ({
62 | type: REMOVE_PRODUCT_SUCCESS,
63 | payload: id
64 | });
65 |
66 | export const editProduct = (id, updates) => ({
67 | type: EDIT_PRODUCT,
68 | payload: {
69 | id,
70 | updates
71 | }
72 | });
73 |
74 | export const editProductSuccess = (updates) => ({
75 | type: EDIT_PRODUCT_SUCCESS,
76 | payload: updates
77 | });
78 |
--------------------------------------------------------------------------------
/src/redux/actions/profileActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | CLEAR_PROFILE,
3 | SET_PROFILE,
4 | UPDATE_EMAIL,
5 | UPDATE_PROFILE,
6 | UPDATE_PROFILE_SUCCESS
7 | } from '@/constants/constants';
8 |
9 | export const clearProfile = () => ({
10 | type: CLEAR_PROFILE
11 | });
12 |
13 | export const setProfile = (user) => ({
14 | type: SET_PROFILE,
15 | payload: user
16 | });
17 |
18 | export const updateEmail = (password, newEmail) => ({
19 | type: UPDATE_EMAIL,
20 | payload: {
21 | password,
22 | newEmail
23 | }
24 | });
25 |
26 | export const updateProfile = (newProfile) => ({
27 | type: UPDATE_PROFILE,
28 | payload: {
29 | updates: newProfile.updates,
30 | files: newProfile.files,
31 | credentials: newProfile.credentials
32 | }
33 | });
34 |
35 | export const updateProfileSuccess = (updates) => ({
36 | type: UPDATE_PROFILE_SUCCESS,
37 | payload: updates
38 | });
39 |
--------------------------------------------------------------------------------
/src/redux/actions/userActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_USER,
3 |
4 | DELETE_USER, EDIT_USER, GET_USER, REGISTER_USER
5 | } from '@/constants/constants';
6 |
7 | // insert in profile array
8 | export const registerUser = (user) => ({
9 | type: REGISTER_USER,
10 | payload: user
11 | });
12 |
13 | export const getUser = (uid) => ({
14 | type: GET_USER,
15 | payload: uid
16 | });
17 |
18 | // different from registerUser -- only inserted in admins' users array not in profile array
19 | export const addUser = (user) => ({
20 | type: ADD_USER,
21 | payload: user
22 | });
23 |
24 | export const editUser = (updates) => ({
25 | type: EDIT_USER,
26 | payload: updates
27 | });
28 |
29 | export const deleteUser = (id) => ({
30 | type: DELETE_USER,
31 | payload: id
32 | });
33 |
--------------------------------------------------------------------------------
/src/redux/reducers/authReducer.js:
--------------------------------------------------------------------------------
1 | import { SIGNIN_SUCCESS, SIGNOUT_SUCCESS } from '@/constants/constants';
2 |
3 | const initState = null;
4 | // {
5 | // id: 'test-123',
6 | // role: 'ADMIN',
7 | // provider: 'password'
8 | // };
9 |
10 | export default (state = initState, action) => {
11 | switch (action.type) {
12 | case SIGNIN_SUCCESS:
13 | return {
14 | id: action.payload.id,
15 | role: action.payload.role,
16 | provider: action.payload.provider
17 | };
18 | case SIGNOUT_SUCCESS:
19 | return null;
20 | default:
21 | return state;
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/src/redux/reducers/basketReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_QTY_ITEM, ADD_TO_BASKET,
3 | CLEAR_BASKET,
4 | MINUS_QTY_ITEM, REMOVE_FROM_BASKET,
5 | SET_BASKET_ITEMS
6 | } from '@/constants/constants';
7 |
8 | export default (state = [], action) => {
9 | switch (action.type) {
10 | case SET_BASKET_ITEMS:
11 | return action.payload;
12 | case ADD_TO_BASKET:
13 | return state.some((product) => product.id === action.payload.id)
14 | ? state
15 | : [action.payload, ...state];
16 | case REMOVE_FROM_BASKET:
17 | return state.filter((product) => product.id !== action.payload);
18 | case CLEAR_BASKET:
19 | return [];
20 | case ADD_QTY_ITEM:
21 | return state.map((product) => {
22 | if (product.id === action.payload) {
23 | return {
24 | ...product,
25 | quantity: product.quantity + 1
26 | };
27 | }
28 | return product;
29 | });
30 | case MINUS_QTY_ITEM:
31 | return state.map((product) => {
32 | if (product.id === action.payload) {
33 | return {
34 | ...product,
35 | quantity: product.quantity - 1
36 | };
37 | }
38 | return product;
39 | });
40 | default:
41 | return state;
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/src/redux/reducers/checkoutReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RESET_CHECKOUT, SET_CHECKOUT_PAYMENT_DETAILS, SET_CHECKOUT_SHIPPING_DETAILS
3 | } from '@/constants/constants';
4 |
5 | const defaultState = {
6 | shipping: {},
7 | payment: {
8 | type: 'paypal',
9 | name: '',
10 | cardnumber: '',
11 | expiry: '',
12 | ccv: ''
13 | }
14 | };
15 |
16 | export default (state = defaultState, action) => {
17 | switch (action.type) {
18 | case SET_CHECKOUT_SHIPPING_DETAILS:
19 | return {
20 | ...state,
21 | shipping: action.payload
22 | };
23 | case SET_CHECKOUT_PAYMENT_DETAILS:
24 | return {
25 | ...state,
26 | payment: action.payload
27 | };
28 | case RESET_CHECKOUT:
29 | return defaultState;
30 | default:
31 | return state;
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/src/redux/reducers/filterReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | APPLY_FILTER,
3 | CLEAR_RECENT_SEARCH,
4 | REMOVE_SELECTED_RECENT, RESET_FILTER, SET_BRAND_FILTER,
5 | SET_MAX_PRICE_FILTER,
6 | SET_MIN_PRICE_FILTER, SET_TEXT_FILTER
7 | } from '@/constants/constants';
8 |
9 | const initState = {
10 | recent: [],
11 | keyword: '',
12 | brand: '',
13 | minPrice: 0,
14 | maxPrice: 0,
15 | sortBy: ''
16 | };
17 |
18 | export default (state = initState, action) => {
19 | switch (action.type) {
20 | case SET_TEXT_FILTER:
21 | return {
22 | ...state,
23 | recent: (!!state.recent.find((n) => n === action.payload) || action.payload === '') ? state.recent : [action.payload, ...state.recent],
24 | keyword: action.payload
25 | };
26 | case SET_BRAND_FILTER:
27 | return {
28 | ...state,
29 | brand: action.payload
30 | };
31 | case SET_MAX_PRICE_FILTER:
32 | return {
33 | ...state,
34 | maxPrice: action.payload
35 | };
36 | case SET_MIN_PRICE_FILTER:
37 | return {
38 | ...state,
39 | minPrice: action.payload
40 | };
41 | case RESET_FILTER:
42 | return initState;
43 | case CLEAR_RECENT_SEARCH:
44 | return {
45 | ...state,
46 | recent: []
47 | };
48 | case REMOVE_SELECTED_RECENT:
49 | return {
50 | ...state,
51 | recent: state.recent.filter((item) => item !== action.payload)
52 | };
53 | case APPLY_FILTER:
54 | return {
55 | ...state,
56 | ...action.payload
57 | };
58 | default:
59 | return state;
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/src/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | import authReducer from './authReducer';
2 | import basketReducer from './basketReducer';
3 | import checkoutReducer from './checkoutReducer';
4 | import filterReducer from './filterReducer';
5 | import miscReducer from './miscReducer';
6 | import productReducer from './productReducer';
7 | import profileReducer from './profileReducer';
8 | import userReducer from './userReducer';
9 |
10 | const rootReducer = {
11 | products: productReducer,
12 | basket: basketReducer,
13 | auth: authReducer,
14 | profile: profileReducer,
15 | filter: filterReducer,
16 | users: userReducer,
17 | checkout: checkoutReducer,
18 | app: miscReducer
19 | };
20 |
21 | export default rootReducer;
22 |
--------------------------------------------------------------------------------
/src/redux/reducers/miscReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | IS_AUTHENTICATING, LOADING,
3 | SET_AUTH_STATUS,
4 | SET_REQUEST_STATUS
5 | } from '@/constants/constants';
6 |
7 | const initState = {
8 | loading: false,
9 | isAuthenticating: false,
10 | authStatus: null,
11 | requestStatus: null,
12 | theme: 'light'
13 | };
14 |
15 | export default (state = initState, action) => {
16 | switch (action.type) {
17 | case LOADING:
18 | return {
19 | ...state,
20 | loading: action.payload
21 | };
22 | case IS_AUTHENTICATING:
23 | return {
24 | ...state,
25 | isAuthenticating: action.payload
26 | };
27 | case SET_REQUEST_STATUS:
28 | return {
29 | ...state,
30 | requestStatus: action.payload
31 | };
32 | case SET_AUTH_STATUS:
33 | return {
34 | ...state,
35 | authStatus: action.payload
36 | };
37 | default:
38 | return state;
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/src/redux/reducers/productReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_PRODUCT_SUCCESS,
3 | CLEAR_SEARCH_STATE, EDIT_PRODUCT_SUCCESS,
4 | GET_PRODUCTS_SUCCESS, REMOVE_PRODUCT_SUCCESS,
5 | SEARCH_PRODUCT_SUCCESS
6 | } from '@/constants/constants';
7 |
8 | const initState = {
9 | lastRefKey: null,
10 | total: 0,
11 | items: []
12 | };
13 |
14 | export default (state = {
15 | lastRefKey: null,
16 | total: 0,
17 | items: [],
18 | searchedProducts: initState
19 | }, action) => {
20 | switch (action.type) {
21 | case GET_PRODUCTS_SUCCESS:
22 | return {
23 | ...state,
24 | lastRefKey: action.payload.lastKey,
25 | total: action.payload.total,
26 | items: [...state.items, ...action.payload.products]
27 | };
28 | case ADD_PRODUCT_SUCCESS:
29 | return {
30 | ...state,
31 | items: [...state.items, action.payload]
32 | };
33 | case SEARCH_PRODUCT_SUCCESS:
34 | return {
35 | ...state,
36 | searchedProducts: {
37 | lastRefKey: action.payload.lastKey,
38 | total: action.payload.total,
39 | items: [...state.searchedProducts.items, ...action.payload.products]
40 | }
41 | };
42 | case CLEAR_SEARCH_STATE:
43 | return {
44 | ...state,
45 | searchedProducts: initState
46 | };
47 | case REMOVE_PRODUCT_SUCCESS:
48 | return {
49 | ...state,
50 | items: state.items.filter((product) => product.id !== action.payload)
51 | };
52 | case EDIT_PRODUCT_SUCCESS:
53 | return {
54 | ...state,
55 | items: state.items.map((product) => {
56 | if (product.id === action.payload.id) {
57 | return {
58 | ...product,
59 | ...action.payload.updates
60 | };
61 | }
62 | return product;
63 | })
64 | };
65 | default:
66 | return state;
67 | }
68 | };
69 |
--------------------------------------------------------------------------------
/src/redux/reducers/profileReducer.js:
--------------------------------------------------------------------------------
1 | import { CLEAR_PROFILE, SET_PROFILE, UPDATE_PROFILE_SUCCESS } from '@/constants/constants';
2 | // import profile from 'static/profile.jpg';
3 | // import banner from 'static/banner.jpg';
4 |
5 | // const initState = {
6 | // fullname: 'Pedro Juan',
7 | // email: 'juanpedro@gmail.com',
8 | // address: '',
9 | // mobile: {},
10 | // avatar: profile,
11 | // banner,
12 | // dateJoined: 1954234787348
13 | // };
14 |
15 | export default (state = {}, action) => {
16 | switch (action.type) {
17 | case SET_PROFILE:
18 | return action.payload;
19 | case UPDATE_PROFILE_SUCCESS:
20 | return {
21 | ...state,
22 | ...action.payload
23 | };
24 | case CLEAR_PROFILE:
25 | return {};
26 | default:
27 | return state;
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/src/redux/reducers/userReducer.js:
--------------------------------------------------------------------------------
1 | import { ADD_USER, DELETE_USER, EDIT_USER } from '@/constants/constants';
2 |
3 | // const initState = [
4 | // {
5 | // firstname: 'Gago',
6 | // lastname: 'Ka',
7 | // email: 'gagoka@mail.com',
8 | // password: 'gagooo',
9 | // avatar: '',
10 | // banner: '',
11 | // dateJoined: 0
12 | // }
13 | // ];
14 |
15 | export default (state = {}, action) => {
16 | switch (action.type) {
17 | case ADD_USER:
18 | return [...state, action.payload];
19 | case EDIT_USER:
20 | return state.map((user) => {
21 | if (user.id === action.payload.id) {
22 | return {
23 | ...user,
24 | ...action.payload
25 | };
26 | }
27 | return user;
28 | });
29 | case DELETE_USER:
30 | return state.filter((user) => user.id !== action.payload);
31 | default:
32 | return state;
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/src/redux/sagas/profileSaga.js:
--------------------------------------------------------------------------------
1 | import { UPDATE_EMAIL, UPDATE_PROFILE } from '@/constants/constants';
2 | import { ACCOUNT } from '@/constants/routes';
3 | import { displayActionMessage } from '@/helpers/utils';
4 | import { call, put, select } from 'redux-saga/effects';
5 | import { history } from '@/routers/AppRouter';
6 | import firebase from '@/services/firebase';
7 | import { setLoading } from '../actions/miscActions';
8 | import { updateProfileSuccess } from '../actions/profileActions';
9 |
10 | function* profileSaga({ type, payload }) {
11 | switch (type) {
12 | case UPDATE_EMAIL: {
13 | try {
14 | yield put(setLoading(false));
15 | yield call(firebase.updateEmail, payload.password, payload.newEmail);
16 |
17 | yield put(setLoading(false));
18 | yield call(history.push, '/profile');
19 | yield call(displayActionMessage, 'Email Updated Successfully!', 'success');
20 | } catch (e) {
21 | console.log(e.message);
22 | }
23 | break;
24 | }
25 | case UPDATE_PROFILE: {
26 | try {
27 | const state = yield select();
28 | const { email, password } = payload.credentials;
29 | const { avatarFile, bannerFile } = payload.files;
30 |
31 | yield put(setLoading(true));
32 |
33 | // if email & password exist && the email has been edited
34 | // update the email
35 | if (email && password && email !== state.profile.email) {
36 | yield call(firebase.updateEmail, password, email);
37 | }
38 |
39 | if (avatarFile || bannerFile) {
40 | const bannerURL = bannerFile ? yield call(firebase.storeImage, state.auth.id, 'banner', bannerFile) : payload.updates.banner;
41 | const avatarURL = avatarFile ? yield call(firebase.storeImage, state.auth.id, 'avatar', avatarFile) : payload.updates.avatar;
42 | const updates = { ...payload.updates, avatar: avatarURL, banner: bannerURL };
43 |
44 | yield call(firebase.updateProfile, state.auth.id, updates);
45 | yield put(updateProfileSuccess(updates));
46 | } else {
47 | yield call(firebase.updateProfile, state.auth.id, payload.updates);
48 | yield put(updateProfileSuccess(payload.updates));
49 | }
50 |
51 | yield put(setLoading(false));
52 | yield call(history.push, ACCOUNT);
53 | yield call(displayActionMessage, 'Profile Updated Successfully!', 'success');
54 | } catch (e) {
55 | console.log(e);
56 | yield put(setLoading(false));
57 | if (e.code === 'auth/wrong-password') {
58 | yield call(displayActionMessage, 'Wrong password, profile update failed :(', 'error');
59 | } else {
60 | yield call(displayActionMessage, `:( Failed to update profile. ${e.message ? e.message : ''}`, 'error');
61 | }
62 | }
63 | break;
64 | }
65 | default: {
66 | throw new Error('Unexpected action type.');
67 | }
68 | }
69 | }
70 |
71 | export default profileSaga;
72 |
--------------------------------------------------------------------------------
/src/redux/sagas/rootSaga.js:
--------------------------------------------------------------------------------
1 | import * as ACTION from '@/constants/constants';
2 | import { takeLatest } from 'redux-saga/effects';
3 | import authSaga from './authSaga';
4 | import productSaga from './productSaga';
5 | import profileSaga from './profileSaga';
6 |
7 | function* rootSaga() {
8 | yield takeLatest([
9 | ACTION.SIGNIN,
10 | ACTION.SIGNUP,
11 | ACTION.SIGNOUT,
12 | ACTION.SIGNIN_WITH_GOOGLE,
13 | ACTION.SIGNIN_WITH_FACEBOOK,
14 | ACTION.SIGNIN_WITH_GITHUB,
15 | ACTION.ON_AUTHSTATE_CHANGED,
16 | ACTION.ON_AUTHSTATE_SUCCESS,
17 | ACTION.ON_AUTHSTATE_FAIL,
18 | ACTION.SET_AUTH_PERSISTENCE,
19 | ACTION.RESET_PASSWORD
20 | ], authSaga);
21 | yield takeLatest([
22 | ACTION.ADD_PRODUCT,
23 | ACTION.SEARCH_PRODUCT,
24 | ACTION.REMOVE_PRODUCT,
25 | ACTION.EDIT_PRODUCT,
26 | ACTION.GET_PRODUCTS
27 | ], productSaga);
28 | yield takeLatest([
29 | ACTION.UPDATE_EMAIL,
30 | ACTION.UPDATE_PROFILE
31 | ], profileSaga);
32 | }
33 |
34 | export default rootSaga;
35 |
--------------------------------------------------------------------------------
/src/redux/store/store.js:
--------------------------------------------------------------------------------
1 | import {
2 | applyMiddleware,
3 | compose, createStore
4 | } from 'redux';
5 | import { persistCombineReducers, persistStore } from 'redux-persist';
6 | import storage from 'redux-persist/lib/storage';
7 | import createSagaMiddleware from 'redux-saga';
8 | import rootReducer from '../reducers';
9 | import rootSaga from '../sagas/rootSaga';
10 |
11 | const sagaMiddleware = createSagaMiddleware();
12 | const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
13 |
14 | const authPersistConfig = {
15 | key: 'root',
16 | storage,
17 | whitelist: ['auth', 'profile', 'basket', 'checkout']
18 | };
19 |
20 | export default () => {
21 | const store = createStore(
22 | persistCombineReducers(authPersistConfig, rootReducer),
23 | composeEnhancer(applyMiddleware(sagaMiddleware))
24 | );
25 | const persistor = persistStore(store);
26 | sagaMiddleware.run(rootSaga);
27 | return { store, persistor };
28 | };
29 |
--------------------------------------------------------------------------------
/src/routers/AdminRoute.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | /* eslint-disable react/jsx-props-no-spreading */
3 | import { AdminNavigation, AdminSideBar } from '@/components/common';
4 | import PropType from 'prop-types';
5 | import React from 'react';
6 | import { connect } from 'react-redux';
7 | import { Redirect, Route } from 'react-router-dom';
8 |
9 | const AdminRoute = ({
10 | isAuth, role, component: Component, ...rest
11 | }) => (
12 |
(
15 | isAuth && role === 'ADMIN' ? (
16 | <>
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | >
25 | ) :
26 | )}
27 | />
28 | );
29 |
30 | const mapStateToProps = ({ auth }) => ({
31 | isAuth: !!auth,
32 | role: auth?.role || ''
33 | });
34 |
35 | AdminRoute.defaultProps = {
36 | isAuth: false,
37 | role: 'USER'
38 | };
39 |
40 | AdminRoute.propTypes = {
41 | isAuth: PropType.bool,
42 | role: PropType.string,
43 | component: PropType.func.isRequired,
44 | // eslint-disable-next-line react/require-default-props
45 | rest: PropType.any
46 | };
47 |
48 | export default connect(mapStateToProps)(AdminRoute);
49 |
--------------------------------------------------------------------------------
/src/routers/ClientRoute.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | /* eslint-disable react/jsx-props-no-spreading */
3 | /* eslint-disable no-nested-ternary */
4 | import { ADMIN_DASHBOARD, SIGNIN } from '@/constants/routes';
5 | import PropType from 'prop-types';
6 | import React from 'react';
7 | import { connect } from 'react-redux';
8 | import { Redirect, Route } from 'react-router-dom';
9 |
10 | const PrivateRoute = ({
11 | isAuth, role, component: Component, ...rest
12 | }) => (
13 | {
16 | if (isAuth && role === 'USER') {
17 | return (
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | if (isAuth && role === 'ADMIN') {
25 | return ;
26 | }
27 |
28 | return (
29 |
35 | );
36 | }}
37 | />
38 | );
39 |
40 | PrivateRoute.defaultProps = {
41 | isAuth: false,
42 | role: 'USER'
43 | };
44 |
45 | PrivateRoute.propTypes = {
46 | isAuth: PropType.bool,
47 | role: PropType.string,
48 | component: PropType.func.isRequired,
49 | // eslint-disable-next-line react/require-default-props
50 | rest: PropType.any
51 | };
52 |
53 | const mapStateToProps = ({ auth }) => ({
54 | isAuth: !!auth,
55 | role: auth?.role || ''
56 | });
57 |
58 | export default connect(mapStateToProps)(PrivateRoute);
59 |
--------------------------------------------------------------------------------
/src/routers/PublicRoute.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | /* eslint-disable react/jsx-props-no-spreading */
3 | import { ADMIN_DASHBOARD, SIGNIN, SIGNUP } from '@/constants/routes';
4 | import PropType from 'prop-types';
5 | import React from 'react';
6 | import { connect } from 'react-redux';
7 | import { Redirect, Route } from 'react-router-dom';
8 |
9 | const PublicRoute = ({
10 | isAuth, role, component: Component, path, ...rest
11 | }) => (
12 | {
16 | // eslint-disable-next-line react/prop-types
17 | const { from } = props.location.state || { from: { pathname: '/' } };
18 |
19 | if (isAuth && role === 'ADMIN') {
20 | return ;
21 | }
22 |
23 | if ((isAuth && role === 'USER') && (path === SIGNIN || path === SIGNUP)) {
24 | return ;
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 | );
32 | }}
33 | />
34 | );
35 |
36 | PublicRoute.defaultProps = {
37 | isAuth: false,
38 | role: 'USER',
39 | path: '/'
40 | };
41 |
42 | PublicRoute.propTypes = {
43 | isAuth: PropType.bool,
44 | role: PropType.string,
45 | component: PropType.func.isRequired,
46 | path: PropType.string,
47 | // eslint-disable-next-line react/require-default-props
48 | rest: PropType.any
49 | };
50 |
51 | const mapStateToProps = ({ auth }) => ({
52 | isAuth: !!auth,
53 | role: auth?.role || ''
54 | });
55 |
56 | export default connect(mapStateToProps)(PublicRoute);
57 |
--------------------------------------------------------------------------------
/src/selectors/selector.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-plusplus */
2 | /* eslint-disable no-else-return */
3 | export const selectFilter = (products, filter) => {
4 | if (!products || products.length === 0) return [];
5 |
6 | const keyword = filter.keyword.toLowerCase();
7 |
8 | return products.filter((product) => {
9 | const isInRange = filter.maxPrice
10 | ? (product.price >= filter.minPrice && product.price <= filter.maxPrice)
11 | : true;
12 | const matchKeyword = product.keywords ? product.keywords.includes(keyword) : true;
13 | // const matchName = product.name ? product.name.toLowerCase().includes(keyword) : true;
14 | const matchDescription = product.description
15 | ? product.description.toLowerCase().includes(keyword)
16 | : true;
17 | const matchBrand = product.brand ? product.brand.toLowerCase().includes(filter.brand) : true;
18 |
19 | return ((matchKeyword || matchDescription) && matchBrand && isInRange);
20 | }).sort((a, b) => {
21 | if (filter.sortBy === 'name-desc') {
22 | return a.name < b.name ? 1 : -1;
23 | } else if (filter.sortBy === 'name-asc') {
24 | return a.name > b.name ? 1 : -1;
25 | } else if (filter.sortBy === 'price-desc') {
26 | return a.price < b.price ? 1 : -1;
27 | }
28 |
29 | return a.price > b.price ? 1 : -1;
30 | });
31 | };
32 |
33 | // Select product with highest price
34 | export const selectMax = (products) => {
35 | if (!products || products.length === 0) return 0;
36 |
37 | let high = products[0];
38 |
39 | for (let i = 0; i < products.length; i++) {
40 | if (products[i].price > high.price) {
41 | high = products[i];
42 | }
43 | }
44 |
45 | return Math.floor(high.price);
46 | };
47 |
48 | // Select product with lowest price
49 | export const selectMin = (products) => {
50 | if (!products || products.length === 0) return 0;
51 | let low = products[0];
52 |
53 | for (let i = 0; i < products.length; i++) {
54 | if (products[i].price < low.price) {
55 | low = products[i];
56 | }
57 | }
58 |
59 | return Math.floor(low.price);
60 | };
61 |
--------------------------------------------------------------------------------
/src/services/config.js:
--------------------------------------------------------------------------------
1 | const firebaseConfig = {
2 | apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
3 | authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
4 | databaseURL: import.meta.env.VITE_FIREBASE_DB_URL,
5 | projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
6 | storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
7 | messagingSenderId: import.meta.env.VITE_FIREBASE_MSG_SENDER_ID,
8 | appId: import.meta.env.VITE_FIREBASE_APP_ID
9 | };
10 |
11 | export default firebaseConfig;
12 |
--------------------------------------------------------------------------------
/src/styles/1 - settings/_breakpoints.scss:
--------------------------------------------------------------------------------
1 | //
2 | // BREAKPOINTS ---------
3 | //
4 | :root {
5 | --mobile: 43rem;
6 | }
7 |
8 |
9 | // Breakpoints
10 | $mobile: 30rem;
11 | $tablet: 50rem;
12 | $laptop: 64rem;
13 | $desktop: 95rem;
14 | $l-desktop: 102rem;
--------------------------------------------------------------------------------
/src/styles/1 - settings/_colors.scss:
--------------------------------------------------------------------------------
1 | //
2 | // COLORS ---------
3 | //
4 |
5 | :root {
6 | --nav-bg: #f8f8f8;
7 | --nav-bg-scrolled: #fff;
8 | --nav-bg-shadow: 0 5px 10px rgba(0, 0, 0, .02);
9 | }
10 |
11 | $nav-bg: #f8f8f8;
12 | $nav-bg-scrolled: #fff;
13 |
14 | $background-color: #f9f9f9;
15 | $background-color-01: #f2f2f2;
16 |
17 | // Paragraphs
18 | $paragraph-color: #4a4a4a; // base
19 |
20 | // Heading
21 | $heading-color: #1a1a1a;
22 |
23 | // Border
24 | $border-color: #e1e1e1;
25 | $border-color-focus: #c5c5c5;
26 |
27 | // General
28 | $white: #fff;
29 | $black: #000;
30 | $off-black: #303030;
31 | $off-white: #f0f0f0;
32 | $red: rgba(247, 45, 45, 0.986);
33 | $green: rgb(59, 150, 32);
34 | $yellow: rgb(228, 165, 31);
35 | $gray-01: #3a3a3a;
36 | $gray-10: #818181;
37 | $gray-20: #b6b6b6;
38 |
39 | // Buttons
40 | $button-color: #101010;
41 | $button-hover: lighten($button-color, 10%);
42 |
43 | // Social
44 | $color-facebook: #0078ff;
45 | $color-facebook-hover: darken($color-facebook, 5%);
46 | $color-github: #24292e;
47 | $color-github-hover: lighten($color-github, 5%);
48 |
49 | $color-success: #000;
50 |
--------------------------------------------------------------------------------
/src/styles/1 - settings/_sizes.scss:
--------------------------------------------------------------------------------
1 | //
2 | // SIZES ---------
3 | //
4 |
5 | $nav-height: 6rem;
6 |
7 | $xs-size: 1rem;
8 | $s-size: 1.2rem;
9 | $m-size: 1.6rem;
10 | $l-size: 3.2rem;
11 | $xl-size: 4.8rem;
12 | $xxl-size: 5.6rem;
13 |
14 | $top: 10rem;
15 | $top-mobile: 8.5rem;
16 | $bottom: 15rem;
17 | $line-height: 2.4rem;
18 |
19 | $pad-desktop: 10rem;
--------------------------------------------------------------------------------
/src/styles/1 - settings/_typography.scss:
--------------------------------------------------------------------------------
1 | //
2 | // TYPOGRAPHY ---------
3 | //
4 |
5 | $baseFontSize : 1.6rem;
6 | $font-small: 1.2rem;
7 | $font-medium: 1.5rem;
8 | $font-large: 2rem;
9 | $font-xlarge: 4rem;
10 |
--------------------------------------------------------------------------------
/src/styles/1 - settings/_zindex.scss:
--------------------------------------------------------------------------------
1 | $z-index: (
2 | toast: 100,
3 | modal: 80,
4 | basket: 60,
5 | navigation: 55,
6 | filter: 40,
7 | search: 30,
8 | content: 10,
9 | );
--------------------------------------------------------------------------------
/src/styles/2 - tools/_functions.scss:
--------------------------------------------------------------------------------
1 | //
2 | // FUNCTIONS ---------
3 | //
4 |
5 | @function rem($pixels, $context: $baseFontSize) {
6 | @if (unitless($pixels)) {
7 | $pixels: $pixels * 1px;
8 | }
9 |
10 | @if (unitless($context)) {
11 | $context: $context * 1px;
12 | }
13 |
14 | @return $pixels / $context * 1rem;
15 | }
16 |
--------------------------------------------------------------------------------
/src/styles/2 - tools/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin bezier-transition($prop: all, $speed: .5s, $func: cubic-bezier(.77,0,.175,1) ) {
2 | // -webkit-transition: all $speed cubic-bezier(.4, 0, .2, 1);
3 | -webkit-transition: $prop $speed $func;
4 | -moz-transition: $prop $speed $func;
5 | -o-transition: $prop $speed $func;
6 | -ms-transition: $prop $speed $func;
7 | transition: $prop $speed $func;
8 | }
9 |
10 |
11 | @mixin l-desktop {
12 | @media (min-width: $l-desktop) {
13 | @content;
14 | }
15 | }
16 |
17 | @mixin desktop {
18 | @media (min-width: $desktop) and (max-width: $l-desktop) {
19 | @content;
20 | }
21 | }
22 |
23 |
24 | @mixin tablet {
25 | @media (max-width: $tablet) {
26 | @content;
27 | }
28 | }
29 |
30 | @mixin mobile {
31 | @media (max-width: $mobile) {
32 | @content;
33 | }
34 | }
--------------------------------------------------------------------------------
/src/styles/4 - elements/_base.scss:
--------------------------------------------------------------------------------
1 | //
2 | // BASE STYLING ---------
3 | //
4 | * {
5 | box-sizing: border-box;
6 | }
7 |
8 | html {
9 | font-size: 62.5%;
10 | }
11 |
12 | body {
13 | min-height: 100vh;
14 | font-family: 'Tajawal', Helvetica, Arial, sans-serif;
15 | font-size: $baseFontSize;
16 | background: $background-color;
17 | font-weight: bold;
18 | overflow-x: hidden;
19 | }
20 |
21 | button:hover {
22 | cursor: pointer;
23 | }
24 |
25 | button:focus {
26 | outline: none;
27 | }
28 |
29 | p {
30 | color: $paragraph-color;
31 | // font-weight: 300;
32 | line-height: $line-height;
33 | }
34 |
35 | strong {
36 | font-weight: bold;
37 | }
38 |
39 | span {
40 | color: $paragraph-color;
41 | font-size: $font-small;
42 | position: relative;
43 | }
44 |
45 | h1,
46 | h2,
47 | h3,
48 | h4,
49 | h5,
50 | h6 {
51 | font-family: 'Tajawal', Helvetica, Arial, sans-serif;
52 | color: $heading-color;
53 | }
54 |
55 | // body::-webkit-scrollbar {
56 | // width: 8px;
57 | // }
58 |
59 | // body::-webkit-scrollbar-track {
60 | // -webkit-box-shadow: inset 0 0 4px rgba(0,0,0,0.2);
61 | // }
62 |
63 | // body::-webkit-scrollbar-thumb {
64 | // background-color: $heading-color;
65 | // outline: 1px solid lighten($heading-color, 10%);
66 | // }
67 |
68 | #app {
69 | width: 100%;
70 | }
71 |
72 | .content {
73 | width: 100%;
74 | min-height: 100vh;
75 | position: relative;
76 | padding: $top $pad-desktop;
77 | display: flex;
78 | animation: fadeIn .5s ease;
79 |
80 | @media (max-width: $mobile) {
81 | padding: 5rem $m-size;
82 | flex-direction: column;
83 | }
84 | }
85 |
86 | .content-admin {
87 | width: 100%;
88 | height: 100vh;
89 | position: fixed;
90 | top: 0;
91 | left: 0;
92 | position: relative;
93 | padding-top: $xxl-size;
94 | overflow: hidden;
95 | display: flex;
96 | }
97 |
98 | .content-admin-wrapper {
99 | overflow-y: scroll;
100 | flex-grow: 1;
101 | padding: $m-size 0;
102 | padding-top: 0;
103 | animation: fadeIn .5s ease;
104 | position: relative;
105 | }
106 |
--------------------------------------------------------------------------------
/src/styles/4 - elements/_button.scss:
--------------------------------------------------------------------------------
1 | .button {
2 | background: $button-color;
3 | padding: $m-size;
4 | border: 1px solid $button-color;
5 | color: $white;
6 | font-weight: bold;
7 | position: relative;
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | @include bezier-transition(all, .3s, ease);
12 |
13 | &:hover {
14 | cursor: pointer;
15 | background: $button-hover;
16 | border: 1px solid $button-hover;
17 | text-decoration: none;
18 | }
19 | }
20 |
21 | .button:disabled {
22 | opacity: .5;
23 |
24 | &:hover {
25 | cursor: not-allowed;
26 | // background: none;
27 | }
28 | }
29 |
30 | .button-link {
31 | @extend .button;
32 | background: none;
33 | color: $black;
34 | border: none;
35 |
36 | &:hover {
37 | background: none;
38 | border: none;
39 | }
40 | }
41 |
42 | .button-muted {
43 | @extend .button;
44 | background: $background-color-01;
45 | color: lighten($paragraph-color, 20%);
46 | border: 1px solid $border-color;
47 |
48 | &:hover {
49 | background: $background-color;
50 | border: 1px solid $border-color-focus;
51 | }
52 | }
53 |
54 | .button-block {
55 | display: block;
56 | width: 100%;
57 | }
58 |
59 | .button-border {
60 | background: transparent;
61 | border: 1px solid $button-color;
62 | color: $button-color;
63 |
64 | &:hover {
65 | background: $border-color;
66 | border: 1px solid $button-color;
67 | }
68 | }
69 |
70 | .button-danger {
71 | background: red;
72 | color: #fff;
73 |
74 | &:hover {
75 | background: darken(red, 10%);
76 | }
77 | }
78 |
79 | .button-border-gray {
80 | border: 1px solid $border-color;
81 | color: $paragraph-color;
82 |
83 | &:hover {
84 | border: 1px solid $border-color;
85 | background: $border-color;
86 | }
87 | }
88 |
89 | .button-small {
90 | font-size: $font-small;
91 | padding: $s-size $m-size;
92 | }
93 | .button-icon {
94 | display: flex;
95 | text-align: center;
96 | align-items: center;
97 | text-align: center;
98 |
99 | & * {
100 | font-size: inherit;
101 | }
102 | }
--------------------------------------------------------------------------------
/src/styles/4 - elements/_label.scss:
--------------------------------------------------------------------------------
1 | label {
2 | font-size: $font-small;
3 | font-weight: bold;
4 | background: $border-color;
5 | border: 1px solid $border-color-focus;
6 | padding: $s-size $m-size;
7 | display: inline-block;
8 | position: relative;
9 |
10 | &:hover {
11 | cursor: pointer;
12 | background: $border-color-focus;
13 | }
14 | }
15 |
16 | .label-input {
17 | border: none;
18 | background: none;
19 | padding: 1rem 1.2rem;
20 | color: #696868;
21 | border: none;
22 |
23 | &:hover {
24 | background: none;
25 | }
26 | }
27 |
28 | .label-error {
29 | color: rgb(235, 43, 43) !important;
30 | }
--------------------------------------------------------------------------------
/src/styles/4 - elements/_link.scss:
--------------------------------------------------------------------------------
1 | a {
2 | text-decoration: none;
3 | color: $button-color;
4 |
5 | // &:hover {
6 | // text-decoration: underline;
7 | // }
8 | }
--------------------------------------------------------------------------------
/src/styles/4 - elements/_select.scss:
--------------------------------------------------------------------------------
1 | select {
2 | font-size: $font-small;
3 | color: $paragraph-color;
4 | padding: 7px $m-size;
5 | background: transparent;
6 | font-weight: bold;
7 | border: 1px solid $border-color;
8 | -webkit-appearance: none;
9 | -moz-appearance: none;
10 | background-image:
11 | linear-gradient(45deg, transparent 50%, $border-color 50%),
12 | linear-gradient(135deg, $border-color 50%, transparent 50%),
13 | linear-gradient(to right, $border-color, $border-color);
14 | background-position:
15 | calc(100% - 20px) calc(1em + 2px),
16 | calc(100% - 15px) calc(1em + 2px),
17 | calc(100% - 3.2em) 0.2em;
18 | background-size:
19 | 5px 5px,
20 | 5px 5px,
21 | 1px 1.8em;
22 | background-repeat: no-repeat;
23 |
24 | &:focus {
25 | background: $background-color;
26 | border: 1px solid $border-color-focus;
27 | background-image:
28 | linear-gradient(45deg, $border-color-focus 50%, transparent 50%),
29 | linear-gradient(135deg, transparent 50%, $border-color-focus 50%),
30 | linear-gradient(to right, $border-color-focus, $border-color-focus);
31 | background-position:
32 | calc(100% - 15px) 1em,
33 | calc(100% - 20px) 1em,
34 | calc(100% - 3.2em) 0.2em;
35 | background-size:
36 | 5px 5px,
37 | 5px 5px,
38 | 1px 1.5em;
39 | background-repeat: no-repeat;
40 | outline: none;
41 | }
42 | }
43 |
44 | option {
45 | font-weight: bold;
46 | }
--------------------------------------------------------------------------------
/src/styles/4 - elements/_textarea.scss:
--------------------------------------------------------------------------------
1 | textarea {
2 | @extend input;
3 | line-height: $line-height;
4 | resize: none;
5 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/404/_page-not-found.scss:
--------------------------------------------------------------------------------
1 | .page-not-found {
2 | width: 100%;
3 | height: 100%;
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | flex-direction: column;
8 | padding: $xxl-size;
9 | margin: auto;
10 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/_badge.scss:
--------------------------------------------------------------------------------
1 | .badge {
2 | position: relative;
3 | }
4 |
5 | .badge-count {
6 | width: 20px;
7 | height: 20px;
8 | border-radius: 50%;
9 | background: $red;
10 | position: absolute;
11 | top: -12px;
12 | right: -15px;
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 | color: $white;
17 | font-size: 11px;
18 | font-weight: bold;
19 | }
20 |
--------------------------------------------------------------------------------
/src/styles/5 - components/_banner.scss:
--------------------------------------------------------------------------------
1 | .banner {
2 | width: 100%;
3 | height: 40rem;
4 | margin-top: 2rem;
5 |
6 | background: #f3f3f3;
7 | display: flex;
8 |
9 | @include mobile {
10 | height: auto;
11 | flex-direction: column;
12 | }
13 | }
14 |
15 | .banner-desc {
16 | display: flex;
17 | align-items: flex-start;
18 | justify-content: center;
19 | flex-direction: column;
20 | padding: 5rem;
21 | flex-basis: 50%;
22 |
23 | @include mobile {
24 | padding: 5rem 0;
25 | }
26 |
27 | h1 {
28 | font-size: 4.8rem;
29 | margin-bottom: 1rem;
30 | width: 80%;
31 |
32 | @include mobile {
33 | width: 100%;
34 | }
35 | }
36 | }
37 |
38 | .banner-img {
39 | position: relative;
40 | width: 100%;
41 | height: 100%;
42 | flex-basis: 50%;
43 | overflow: hidden;
44 |
45 | img {
46 | width: 100%;
47 | height: 100%;
48 | object-fit: cover;
49 | transform: translateX(5rem)
50 | }
51 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/_circular-progress.scss:
--------------------------------------------------------------------------------
1 | .circular-progress-light {
2 | width: 15px;
3 | height: 15px;
4 | margin-left: $s-size;
5 | margin-right: $s-size;
6 | border-radius: 50%;
7 | border-top: 2px solid $white;
8 | border-left: 2px solid $white;
9 | animation: spin 2s linear infinite;
10 | }
11 |
12 | .circular-progress-dark {
13 | @extend .circular-progress-light;
14 | border-top: 2px solid $black;
15 | border-left: 2px solid $black;
16 | }
17 |
18 | .progress-loading {
19 | width: 100%;
20 | height: inherit;
21 | display: flex;
22 | justify-content: center;
23 | align-items: center;
24 | flex-direction: column;
25 | }
26 |
27 | .loading-wrapper {
28 | width: 100%;
29 | height: 100%;
30 | display: flex;
31 | justify-content: center;
32 | align-items: center;
33 | position: absolute;
34 | top: 0;
35 | left: 0;
36 | right: 0;
37 | bottom: 0;
38 | margin: auto;
39 | background: rgba(0, 0, 0, .5);
40 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/_color-chooser.scss:
--------------------------------------------------------------------------------
1 | .color-chooser {
2 | width: 100%;
3 | display: flex;
4 |
5 | }
6 |
7 | .color-item {
8 | width: 30px;
9 | height: 30px;
10 | border-radius: 50%;
11 | margin: 0 10px;
12 | position: relative;
13 | z-index: 1;
14 | transition: transform .2s ease;
15 | flex-shrink: 0;
16 |
17 | &:hover {
18 | cursor: pointer;
19 | border: 2px solid #f1f1f1;
20 | }
21 | }
22 |
23 | .color-item-selected,
24 | .color-item-deletable {
25 | border: 2px solid #f1f1f1;
26 | transform: scale(1.2);
27 |
28 | &:after {
29 | content: '✓';
30 | position: absolute;
31 | top: 50%;
32 | left: 50%;
33 | transform: translate(-50%, -50%);
34 | margin: auto;
35 | color: #fff;
36 | }
37 | }
38 |
39 | .color-item-deletable {
40 | &:after {
41 | content: '✖';
42 | display: none;
43 | }
44 |
45 | &:hover:after {
46 | display: block;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/styles/5 - components/_filter.scss:
--------------------------------------------------------------------------------
1 | .filters {
2 | display: flex;
3 | flex-wrap: wrap;
4 | position: relative;
5 | z-index: map-get($z-index, 'filter');
6 | }
7 |
8 | .filters-field {
9 | width: 100%;
10 | margin-bottom: $m-size;
11 | padding-bottom: $m-size;
12 | border-bottom: 1px solid $border-color;
13 |
14 | &:nth-child(1),
15 | &:nth-child(2) {
16 | flex-basis: 50%;
17 | }
18 |
19 | @include mobile {
20 | flex-basis: 100% !important;
21 | }
22 | }
23 |
24 | .filters-brand {
25 | width: 100%;
26 | }
27 |
28 | .filters-action {
29 | display: flex;
30 | width: 100%;
31 | }
32 |
33 | .filters-button {
34 | flex-grow: 1;
35 | }
36 |
37 | .filters-toggle {
38 | position: relative;
39 | }
40 |
41 | .filters-toggle-sub {
42 | width: 400px;
43 | height: 360px;
44 | background: $white;
45 | position: relative;
46 | padding: $m-size;
47 |
48 | @include mobile {
49 | width: 100%;
50 | }
51 | }
52 |
53 | .is-open-filters .filters-toggle-sub {
54 | display: block;
55 | }
56 |
57 | .filters-toggle-caret {
58 | transform: rotate(-90deg);
59 | }
60 |
61 | .filters-wrapper {
62 | display: flex;
63 | align-items: center;
64 | }
65 |
66 | // .is-open-filters .filters-toggle-caret {
67 | // transform: rotate(180deg);
68 | // }
69 |
--------------------------------------------------------------------------------
/src/styles/5 - components/_footer.scss:
--------------------------------------------------------------------------------
1 | .footer {
2 | position: relative;
3 | padding: 0 $xxl-size;
4 | margin-top: $xl-size;
5 | background: $off-white;
6 | display: flex;
7 | align-items: center;
8 | justify-content: space-between;
9 |
10 | @include mobile {
11 | padding: 0;
12 | padding-top: 2.5rem;
13 | flex-direction: column;
14 | text-align: center;
15 | }
16 |
17 | a {
18 | text-decoration: underline;
19 | }
20 | }
21 |
22 | .footer-logo {
23 | width: 15rem;
24 | height: 6rem;
25 | object-fit: contain;
26 | }
27 |
28 | .footer-col-1 {
29 | flex-basis: 40%;
30 |
31 | @include mobile {
32 | flex-basis: none;
33 | order: 1;
34 | text-align: center;
35 | }
36 | }
37 |
38 | .footer-col-2 {
39 | padding: 3rem 0;
40 | text-align: center;
41 | flex-basis: 20%;
42 |
43 | @include mobile {
44 | flex-basis: none;
45 | order: 3;
46 | padding-bottom: 0;
47 | margin-bottom: 0;
48 | }
49 | }
50 |
51 | .footer-col-3 {
52 | flex-basis: 40%;
53 | text-align: right;
54 |
55 | @include mobile {
56 | flex-basis: none;
57 | order: 2;
58 | text-align: center;
59 | }
60 | }
61 |
62 | @include mobile {
63 | .footer-col-1,
64 | .footer-col-2,
65 | .footer-col-3 {
66 | width: 100%;
67 | margin: $s-size 0;
68 | }
69 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/_home.scss:
--------------------------------------------------------------------------------
1 | .home {
2 | width: 100%;
3 | }
4 |
5 | .home-featured {
6 | margin: 5rem;
7 | margin-top: 10rem;
8 | }
9 |
10 | .featured {
11 | width: 100%;
12 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/_icons.scss:
--------------------------------------------------------------------------------
1 | .anticon {
2 | font-size: 1.5rem;
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/_modal.scss:
--------------------------------------------------------------------------------
1 | .ReactModal__Overlay {
2 | z-index: map-get($z-index, 'modal');
3 | }
4 |
5 | .ReactModal__Body--open {
6 | overflow: hidden;
7 | }
8 |
9 | .ReactModal__Content--before-close {
10 | transform: translate(-50%, -50%) scale(0) !important;
11 | }
12 |
13 | .modal-close-button {
14 | position: absolute;
15 | top: 1rem;
16 | right: 1rem;
17 | font-size: 2rem;
18 | padding: 0;
19 | border: none;
20 | background: none;
21 |
22 | i {
23 | color: lighten($off-black, 10%);
24 | }
25 |
26 | &:hover {
27 | i {
28 | color: $off-black;
29 | }
30 | }
31 | }
32 |
33 | .ReactModal__Content {
34 | @include mobile {
35 | width: 90%;
36 | }
37 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/_navigation.scss:
--------------------------------------------------------------------------------
1 | .navigation {
2 | width: 100%;
3 | height: 10rem;
4 | background: $background-color;
5 | display: flex;
6 | align-items: center;
7 | padding: .5rem 5rem;
8 | padding-top: 3rem;
9 | position: absolute;
10 | top: 0;
11 | transform: translateY(0);
12 | z-index: map-get($z-index, navigation);
13 | box-shadow: none;
14 | @include bezier-transition(transform, .3s, ease);
15 |
16 | @include tablet {
17 | padding: .5rem 2rem;
18 | }
19 |
20 | @include mobile {
21 | padding: 0 $s-size;
22 | position: fixed;
23 | }
24 |
25 | .logo {
26 | height: inherit;
27 | margin-right: 2rem;
28 | }
29 |
30 | .logo img {
31 | width: 15rem;
32 | height: inherit;
33 | object-fit: contain;
34 | }
35 |
36 | .logo a {
37 | display: block;
38 | height: 100%;
39 | }
40 |
41 | .searchbar {
42 | width: 300px;
43 | }
44 | }
45 |
46 | .navigation-admin {
47 | height: 6rem;
48 | background: $white;
49 | box-shadow: 0 5px 10px rgba(0, 0, 0, .05);
50 | padding: .5rem $l-size;
51 | display: flex;
52 | justify-content: space-between;
53 | }
54 |
55 | .navigation-menu {
56 | display: flex;
57 | align-items: center;
58 | justify-content: flex-end;
59 | padding: 0;
60 | margin: 0;
61 | text-align: right;
62 | }
63 |
64 | .navigation-menu-main {
65 | padding-left: 0;
66 | margin-right: 2rem;
67 | flex-grow: 1;
68 |
69 | li {
70 | display: inline-block;
71 |
72 |
73 | a {
74 | padding: 10px 15px;
75 | font-size: 1.4rem;
76 | opacity: .5;
77 |
78 | &:hover {
79 | background: $background-color-01;
80 | }
81 | }
82 | }
83 | }
84 |
85 | .navigation-menu-item {
86 | display: inline-block;
87 | list-style-type: none;
88 | padding: 0;
89 | margin: 0;
90 | }
91 |
92 | .navigation-action {
93 | display: flex;
94 | align-items: center;
95 | margin-left: $xxl-size;
96 | }
97 |
98 | .navigation-menu-link {
99 | color: $heading-color;
100 | padding: $xs-size $m-size;
101 | text-decoration: none;
102 | font-size: $font-small;
103 | text-transform: uppercase;
104 | font-weight: bold;
105 | position: relative;
106 |
107 | &:hover {
108 | text-decoration: none;
109 | background: $background-color-01;
110 | }
111 | }
112 |
113 | .navigation-menu-active {
114 | font-weight: bold;
115 | opacity: 1 !important;
116 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/_pill.scss:
--------------------------------------------------------------------------------
1 | .pill {
2 | background: $border-color;
3 | padding: 0.8rem $m-size;
4 | border-radius: $m-size;
5 | display: inline-block;
6 | margin: 10px 5px;
7 | position: relative;
8 | text-align: center;
9 |
10 | &:hover {
11 | background: $border-color-focus;
12 | }
13 | }
14 |
15 | .pill-title {
16 | margin: 0;
17 | }
18 |
19 | .pill-content {
20 | max-width: 25rem;
21 | text-overflow: ellipsis;
22 | overflow: hidden;
23 | }
24 |
25 | .pill-remove {
26 | position: absolute;
27 | right: $s-size;
28 | top: 0;
29 | bottom: 0;
30 | margin: auto;
31 | display: flex;
32 | align-items: center;
33 | justify-content: center;
34 |
35 | &:hover {
36 | cursor: pointer;
37 | }
38 | }
39 |
40 | .pill-wrapper {
41 | text-align: center;
42 |
43 | @include mobile {
44 | display: inline-block;
45 | }
46 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/_preloader.scss:
--------------------------------------------------------------------------------
1 | .preloader {
2 | width: 100%;
3 | height: 100vh;
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | flex-direction: column;
8 | background: $background-color;
9 | animation: fadeIn .5s ease;
10 |
11 | img {
12 | width: 200px;
13 | height: 120px;
14 | object-fit: contain;
15 | }
16 | }
17 |
18 | .loader {
19 | width: 100%;
20 | height: inherit;
21 | background: $background-color-01;
22 | position: relative;
23 | padding: $m-size;
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 | flex-direction: column;
28 | animation: fadeIn .3s ease;
29 |
30 | @include mobile {
31 | min-height: 80vh;
32 | }
33 | }
34 |
35 | .logo-symbol {
36 | width: 70px;
37 | height: 70px;
38 | animation: rotate 1s ease infinite;
39 |
40 | .fill-white {
41 | fill: #fff;
42 | }
43 | }
44 |
45 | @keyframes rotate {
46 | 90% {
47 | transform: rotate(360deg);
48 | }
49 | 100% {
50 | transform: rotate(360deg);
51 | }
52 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/_pricerange.scss:
--------------------------------------------------------------------------------
1 | .price-range {
2 | width: 100%;
3 | position: relative;
4 | // box-shadow: inset 0 2px 5px rgba(0, 0, 0, .2);
5 | }
6 |
7 | .price-range-control {
8 | position: relative;
9 | width: 100%;
10 | height: 30px;
11 | display: flex;
12 | margin-bottom: 3rem;
13 | }
14 |
15 | .price-range-slider {
16 | position: absolute;
17 | left: 0;
18 | bottom: 0;
19 | z-index: 1;
20 |
21 | &:nth-child(1):hover {
22 | background: red;
23 | }
24 | }
25 |
26 | .price-range-input {
27 | width: 50%;
28 | border: none !important;
29 | font-size: 4rem !important;
30 | padding: 0 !important;
31 | flex-grow: 1;
32 | text-align: center;
33 | }
34 |
35 | .price-input-error {
36 | color: red !important;
37 | background: transparentize(red, .9);
38 | }
39 |
40 | .price-range-scale {
41 | display: flex;
42 | padding: $m-size 0;
43 | }
44 |
45 | .price-range-price {
46 | font-size: 12px;
47 | font-weight: bold;
48 | }
49 |
50 | .price-range-price:nth-child(1) {
51 | flex-grow: 1;
52 | }
53 |
54 | // .range-slider svg,
55 | // .range-slider input[type=range] {
56 | // position: absolute;
57 | // left: 0;
58 | // bottom: 0;
59 | // }
--------------------------------------------------------------------------------
/src/styles/5 - components/_searchbar.scss:
--------------------------------------------------------------------------------
1 | .searchbar {
2 | width: 400px;
3 | display: flex;
4 | position: relative;
5 | }
6 |
7 | .search-input {
8 | padding-left: $xl-size !important;
9 | }
10 |
11 | .searchbar-input {
12 | width: 100%;
13 | background: $white;
14 | }
15 |
16 | .searchbar-recent {
17 | position: absolute;
18 | top: 100%;
19 | left: 0;
20 | width: 100%;
21 | background: $white;
22 | box-shadow: 0 5px 10px rgba(0, 0, 0, .1);
23 | display: none;
24 | }
25 |
26 | .searchbar-recent-header {
27 | display: flex;
28 | justify-content: space-between;
29 | align-items: center;
30 | padding: 0 $m-size;
31 | position: relative;
32 |
33 | &:after {
34 | content: '';
35 | position: absolute;
36 | bottom: 0;
37 | left: 0;
38 | right: 0;
39 | margin: auto;
40 | width: 94%;
41 | height: 1px;
42 | background: $border-color;
43 | }
44 | }
45 |
46 | .searchbar-recent-clear:hover {
47 | cursor: pointer;
48 | }
49 |
50 | .searchbar-recent-wrapper {
51 | display: flex;
52 | justify-content: space-between;
53 | align-items: center;
54 | padding: 0 $m-size;
55 | padding-right: 0;
56 |
57 | &:hover {
58 | background: $border-color;
59 | cursor: pointer;
60 | }
61 | }
62 |
63 | .searchbar-recent-keyword {
64 | flex-grow: 1;
65 | padding: $s-size 0;
66 | }
67 |
68 | .searchbar-recent-button {
69 | font-weight: bold;
70 | padding: $s-size;
71 | }
72 |
73 | .searchbar-icon {
74 | position: absolute;
75 | left: $m-size;
76 | top: 0;
77 | bottom: 0;
78 | margin: auto;
79 | color: #7a7a7a;
80 | font-size: 1.6rem;
81 | }
82 |
--------------------------------------------------------------------------------
/src/styles/5 - components/_sidebar.scss:
--------------------------------------------------------------------------------
1 |
2 | .sidebar {
3 | width: 250px;
4 | // height: 100vh;
5 | position: relative;
6 | left: 0;
7 | top: 0;
8 | padding-top: 5px;
9 | // background: $white;
10 | // border: 1px solid $border-color;
11 | margin-right: $s-size;
12 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/_sidenavigation.scss:
--------------------------------------------------------------------------------
1 | .sidenavigation {
2 | width: 250px;
3 | height: 100%;
4 | background: $black;
5 | position: relative;
6 | padding-top: 1rem;
7 | border-right: 1px solid $border-color;
8 | }
9 |
10 | .sidenavigation-wrapper {
11 | width: 100%;
12 | height: 100%;
13 | display: flex;
14 | flex-direction: column;
15 | }
16 |
17 | .sidenavigation-menu {
18 | padding: $m-size 0 $m-size $l-size;
19 | display: block;
20 | color: $white;
21 | font-weight: bold;
22 | opacity: .8;
23 |
24 | &:hover {
25 | background: lighten($black, 5%);
26 | text-decoration: none;
27 | opacity: 1;
28 | }
29 | }
30 |
31 | .sidenavigation-menu-active {
32 | background: lighten($black, 7%);
33 | opacity: 1;
34 | border-right: 4px solid $yellow;
35 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/_toast.scss:
--------------------------------------------------------------------------------
1 | .toast {
2 | position: fixed;
3 | top: 100px;
4 | right: $l-size;
5 | background: $white;
6 | animation: slideInToast 3s ease;
7 | padding: $s-size $m-size;
8 | border: 1px solid $border-color-focus;
9 | box-shadow: 0 5px 10px rgba(0, 0, 0, .1);
10 | z-index: map-get($z-index, 'toast');
11 |
12 | @include mobile {
13 | width: 80%;
14 | right: 0;
15 | left: 0;
16 | margin: auto;
17 | display: flex;
18 | justify-content: center;
19 | }
20 | }
21 |
22 | .toast-msg {
23 | font-weight: bold;
24 | }
25 |
26 | .toast-error {
27 | color: $red;
28 | border: 1px solid $red;
29 | padding: $m-size;
30 | background: lighten($red, 40%);
31 |
32 | .toast-msg { color: $red; }
33 | }
34 |
35 | .toast-info {
36 | color: $yellow;
37 | border: 1px solid $yellow;
38 | padding: $m-size;
39 | background: lighten($yellow, 45%);
40 |
41 | .toast-msg { color: $yellow; }
42 | }
43 |
44 | .toast-success {
45 | color: $green;
46 | border: 1px solid $green;
47 | padding: $m-size;
48 | background: lighten($green, 60%);
49 |
50 | .toast-msg { color: $green; }
51 | }
52 |
53 | @keyframes slideInToast {
54 | 0% {
55 | opacity: 0;
56 | transform: translateY(-120%);
57 | }
58 | 20% {
59 | opacity: 1;
60 | transform: translateY(0);
61 | }
62 | 90% {
63 | opacity: 1;
64 | transform: translateY(0);
65 | }
66 | 100% {
67 | opacity: 0;
68 | transform: translateY(-120%);
69 | }
70 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/_tooltip.scss:
--------------------------------------------------------------------------------
1 | .tooltip {
2 | position: relative;
3 | display: inline-block;
4 | border-bottom: 1px dotted #222;
5 | margin-left: 22px;
6 | }
7 |
8 | .tooltip .tooltiptext {
9 | width: 100px;
10 | background-color: #222;
11 | color: #fff;
12 | opacity: 0.8;
13 | text-align: center;
14 | border-radius: 6px;
15 | padding: 5px 0;
16 | position: absolute;
17 | z-index: 1;
18 | bottom: 150%;
19 | left: 50%;
20 | margin-left: -60px;
21 | }
22 |
23 | .tooltip .tooltiptext::after {
24 | content: "";
25 | position: absolute;
26 | top: 100%;
27 | left: 50%;
28 | margin-left: -5px;
29 | border-width: 5px;
30 | border-style: solid;
31 | border-color: #222 transparent transparent transparent;
32 | }
33 |
--------------------------------------------------------------------------------
/src/styles/5 - components/admin/_grid.scss:
--------------------------------------------------------------------------------
1 | .grid {
2 | align-items: center;
3 | position: relative;
4 | // border: 1px solid transparent;
5 | }
6 |
7 | .grid-col {
8 | position: relative;
9 | }
10 |
11 | .product-admin-header {
12 | display: flex;
13 | align-items: center;
14 | padding: 0 $l-size;
15 | border-bottom: 1px solid $border-color;
16 | background: $white;
17 | position: sticky;
18 | top: 0;
19 | left: 0;
20 | z-index: 2;
21 | }
22 |
23 | .product-admin-header-title {
24 | flex-grow: 1;
25 | }
26 |
27 | .product-admin-filter {
28 | position: relative;
29 | z-index: map-get($z-index, 'filter');
30 | margin-right: $s-size;
31 | }
32 |
33 | .product-admin-items {
34 | padding: 0 $l-size;
35 | }
36 |
37 | .product-admin-filter-wrapper {
38 | width: 250px;
39 | padding: $m-size;
40 | position: absolute;
41 | top: 0;
42 | right: 100%;
43 | background: $white;
44 | box-shadow: 0 5px 15px rgba(0, 0, 0, .1);
45 | display: none;
46 | }
47 |
48 | .product-admin-filter:hover > .product-admin-filter-wrapper {
49 | display: block;
50 | }
51 |
52 | .product-form-container {
53 | padding: $l-size;
54 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/admin/_product.scss:
--------------------------------------------------------------------------------
1 | .item {
2 | position: relative;
3 | padding: $s-size;
4 | border: 1px solid transparent;
5 |
6 | &:nth-child(even) {
7 | background: $white;
8 | }
9 |
10 | &:hover {
11 | border: 1px solid $border-color-focus;
12 | box-shadow: 2px 5px 10px rgba(0, 0, 0, .1);
13 | z-index: 1;
14 | }
15 | }
16 |
17 | .item-products {
18 | padding: 0;
19 | }
20 |
21 | .item:hover > .item-action {
22 | display: flex;
23 | }
24 |
25 | // reset styles for loading item
26 | .item-loading {
27 | &:nth-child(even) {
28 | background: none;
29 | }
30 | }
31 |
32 | .item-loading:hover {
33 | border: none;
34 | box-shadow: none;
35 | }
36 |
37 | // ----
38 |
39 | .item-action {
40 | position: absolute;
41 | right: 0;
42 | top: 0;
43 | bottom: 0;
44 | padding: $s-size;
45 | background: $white;
46 | grid-column: span 2;
47 | display: flex;
48 | justify-content: flex-end;
49 | align-items: center;
50 | display: none;
51 | }
52 |
53 | .item-action-confirm {
54 | width: 350px;
55 | height: 100%;
56 | display: flex;
57 | position: absolute;
58 | top: 0;
59 | right: 0;
60 | padding: $s-size;
61 | display: flex;
62 | align-items: center;
63 | z-index: 1;
64 | display: none;
65 |
66 | h5 {
67 | flex-grow: 1;
68 | }
69 | }
70 |
71 | .item-active {
72 | border: 1px solid $border-color;
73 | }
74 |
75 | .item-active:after {
76 | content: '';
77 | position: absolute;
78 | left: 0;
79 | top: 0;
80 | width: 100%;
81 | height: 100%;
82 | background: linear-gradient(45deg, rgba(255, 255, 255, .3), $white 90%);
83 | }
84 |
85 | .item-active .item-action-confirm,
86 | .item-active .item-action {
87 | display: flex;
88 | }
89 |
90 | .item-img-wrapper {
91 | width: 80px;
92 | height: 50px;
93 | margin-left: $s-size;
94 | }
95 |
96 | .item-img {
97 | width: 100%;
98 | height: 100%;
99 | object-fit: contain;
100 | }
101 |
--------------------------------------------------------------------------------
/src/styles/5 - components/mobile/_bottom-navigation.scss:
--------------------------------------------------------------------------------
1 | .bottom-navigation {
2 | width: 100%;
3 | position: fixed;
4 | bottom: 0;
5 | background: $black;
6 | height: 80px;
7 | }
8 |
9 | .bottom-navigation-menu {
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | padding: 0 $m-size;
14 | margin: 0;
15 | }
16 |
17 | .bottom-navigation-item {
18 | list-style: none;
19 | padding: $s-size;
20 | height: 100%;
21 | color: $white;
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | flex-direction: column;
26 | flex-grow: 1;
27 |
28 | &:hover {
29 | background: $button-color;
30 | }
31 | }
32 |
33 | .bottom-navigation-link {
34 | color: $white;
35 | text-decoration: none;
36 | }
37 |
38 | .bottom-navigation-icon {
39 | width: 35px;
40 | height: 35px;
41 | overflow: hidden;
42 | margin-bottom: 5px;
43 | }
44 |
45 | .bottom-navigation-profile {
46 | border-radius: 50%;
47 | border: 1px solid $white;
48 | }
49 |
50 | .bottom-navigation-img {
51 | width: 100%;
52 | height: 100%;
53 | object-fit: cover;
54 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/mobile/_mobile-navigation.scss:
--------------------------------------------------------------------------------
1 | .mobile-navigation {
2 | width: 100%;
3 | position: fixed;
4 | top: 0;
5 | left: 0;
6 | // background: $background-color;
7 | background: #fff;
8 | z-index: map-get($z-index, 'navigation');
9 | box-shadow: 0 5px 10px rgba(0, 0, 0, .1);
10 | }
11 |
12 | .mobile-navigation-main {
13 | width: 100%;
14 | height: 50px;
15 | padding: 0 $s-size;
16 | display: flex;
17 | align-items: center;
18 | position: relative;
19 | z-index: 1;
20 | }
21 |
22 | .mobile-navigation-sec {
23 | display: flex;
24 | align-items: center;
25 | padding: 5px $s-size;
26 | }
27 |
28 | .mobile-navigation-menu {
29 | height: 100%;
30 | display: flex;
31 | align-items: center;
32 | margin: 0;
33 | padding: 0;
34 |
35 | .user-nav {
36 | height: 100%;
37 | margin: 0;
38 | }
39 |
40 | .user-nav h5 {
41 | display: none;
42 | }
43 | }
44 |
45 | .mobile-navigation-item {
46 | // width: 50%;
47 | height: 100%;
48 | list-style: none;
49 | position: relative;
50 | display: flex;
51 | align-items: center;
52 | justify-content: center;
53 | // border: 1px solid $border-color;
54 | }
55 |
56 | .mobile-navigation-logo {
57 | flex-grow: 1;
58 | padding-left: .5rem;
59 | margin-right: $xl-size;
60 | }
61 |
62 | .mobile-navigation-search {
63 | width: 50%;
64 | height: 80%;
65 | display: flex;
66 | justify-content: center;
67 | align-items: center;
68 | border-bottom: 1px solid $paragraph-color;
69 | }
70 |
71 | .mobile-navigation-search-title {
72 | margin: .6rem;
73 | font-weight: normal;
74 | }
75 |
--------------------------------------------------------------------------------
/src/styles/5 - components/profile/_editprofile.scss:
--------------------------------------------------------------------------------
1 | .edit-user {
2 | width: 600px;
3 | height: auto;
4 | padding: $m-size;
5 | margin: auto;
6 | display: flex;
7 | flex-direction: column;
8 |
9 | @include mobile {
10 | width: 100%;
11 | padding: 0;
12 | }
13 |
14 | .user-profile-banner-wrapper,
15 | .user-profile-avatar-wrapper {
16 | overflow: visible;
17 | }
18 | }
19 |
20 | .edit-user-details {
21 | width: 60%;
22 | }
23 |
24 | .edit-user-images {
25 | width: 40%;
26 | }
27 |
28 | // .edit-wrapper {
29 | // position: absolute;
30 | // left: 0;
31 | // right: 0;
32 | // top: 0;
33 | // bottom: 0;
34 | // width: 3rem;
35 | // height: 3rem;
36 | // border-radius: 50%;
37 | // background: $black;
38 | // color: $white;
39 | // }
40 |
41 | .edit-button {
42 | padding: $s-size;
43 | position: absolute;
44 | bottom: -10px;
45 | width: 3rem;
46 | height: 3rem;
47 | border-radius: 50%;
48 | padding: 0;
49 | display: flex;
50 | justify-content: center;
51 | align-items: center;
52 | border: none !important;
53 | background: $black !important;
54 | color: $white;
55 | }
56 |
57 | .edit-avatar-button {
58 | right: 0;
59 | }
60 |
61 | .edit-banner-button {
62 | right: 2rem;
63 | }
64 |
65 | .edit-user-action {
66 | display: flex;
67 | align-items: center;
68 | justify-content: space-between;
69 |
70 | @include mobile {
71 | button {
72 | width: 50%;
73 | }
74 | }
75 | }
76 |
77 |
--------------------------------------------------------------------------------
/src/styles/5 - components/profile/_user-nav.scss:
--------------------------------------------------------------------------------
1 | .user-nav {
2 | margin-left: $m-size;
3 | display: flex;
4 | align-items: center;
5 | position: relative;
6 | padding: 0 $s-size;
7 | border-bottom: 2px solid transparent;
8 | display: flex;
9 | justify-content: center;
10 |
11 | @include mobile {
12 | width: 100%;
13 | }
14 |
15 | &:hover {
16 | cursor: pointer;
17 | // border-bottom: 2px solid $black;
18 | }
19 | }
20 |
21 | .user-nav-img-wrapper {
22 | width: 30px;
23 | height: 30px;
24 | border-radius: 50%;
25 | overflow: hidden;
26 | margin-left: 10px;
27 | position: relative;
28 | background: $border-color-focus;
29 |
30 | &:after {
31 | content: attr('data-alt');
32 | position: absolute;
33 | left: 0;
34 | right: 0;
35 | top: 0;
36 | bottom: 0;
37 | margin: auto;
38 | color: $white;
39 | }
40 | }
41 |
42 | .user-nav-img {
43 | width: 100%;
44 | height: 100%;
45 | object-fit: cover;
46 | }
47 |
48 | .user-nav-sub {
49 | width: 150px;
50 | height: auto;
51 | background: $white;
52 | position: absolute;
53 | top: 100%;
54 | right: 0;
55 | box-shadow: 0 5px 12px rgba(0, 0, 0, .1);
56 | display: none;
57 | }
58 |
59 | .user-nav-sub-link {
60 | font-size: $font-small;
61 | font-weight: bold;
62 | display: flex;
63 | justify-content: space-between;
64 | padding: $s-size $m-size;
65 | text-align: left;
66 |
67 | &:hover {
68 | background: $background-color;
69 | text-decoration: none;
70 | }
71 |
72 | &:first-child {
73 | border-bottom: .5px solid $background-color;
74 | }
75 | }
76 |
77 | .user-sub-open {
78 | border-bottom: 2px solid $black;
79 | }
80 |
81 | .user-sub-open .user-caret {
82 | transform: rotate(180deg);
83 | }
84 |
85 | .user-sub-open > .user-nav-sub {
86 | display: block;
87 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/profile/_user-profile.scss:
--------------------------------------------------------------------------------
1 | .user-profile {
2 | display: flex;
3 | margin: auto;
4 |
5 | @include mobile {
6 | width: 100%;
7 | }
8 | }
9 |
10 | .user-profile-block {
11 | // width: 600px;
12 | width: 100%;
13 | height: auto;
14 | // padding: $m-size;
15 |
16 | // @include mobile {
17 | // padding: 0;
18 | // }
19 | }
20 |
21 | .user-profile-banner-wrapper {
22 | width: 100%;
23 | height: 100%;
24 | position: relative;
25 | background: lighten($border-color, 3%);
26 | overflow: hidden;
27 | // display: flex;
28 | // justify-content: center;
29 | // align-items: center;
30 | }
31 |
32 | .user-profile-banner {
33 | width: 100%;
34 | height: 150px;
35 | position: relative;
36 |
37 | @include mobile {
38 | height: 100px;
39 | }
40 | }
41 |
42 | .user-profile-banner-img {
43 | width: 100%;
44 | height: 100%;
45 | object-fit: cover;
46 | position: absolute;
47 | left: 0;
48 | top: 0;
49 | }
50 |
51 | .user-profile-avatar-wrapper {
52 | width: 100px;
53 | height: 100px;
54 | border-radius: 50%;
55 | border: 3px solid $white;
56 | position: absolute;
57 | left: $m-size;
58 | bottom: 30%;
59 | position: relative;
60 | background: $border-color;
61 | overflow: hidden;
62 | // display: flex;
63 | // justify-content: center;
64 | // align-items: center;
65 | }
66 |
67 | .user-profile-img {
68 | width: 100%;
69 | height: 100%;
70 | border-radius: 50%;
71 | object-fit: cover;
72 | }
73 |
74 | .user-profile-edit {
75 | position: absolute;
76 | right: $s-size;
77 | bottom: -12%;
78 | }
79 |
80 | .user-profile-details {
81 | padding: 9.5rem $m-size $l-size;
82 |
83 | & > div {
84 | margin-bottom: $s-size;
85 | }
86 | }
87 |
88 | .user-profile-name {
89 | text-transform: capitalize;
90 | }
--------------------------------------------------------------------------------
/src/styles/5 - components/profile/_user-tab.scss:
--------------------------------------------------------------------------------
1 | .user-tab {
2 | width: 700px;
3 | height: 100%;
4 | margin: 0 auto;
5 | margin-top: 3rem;
6 | background: $white;
7 | border: 1px solid $border-color;
8 |
9 | @media (max-width: $mobile) {
10 | width: 100%;
11 | margin-top: 6rem;
12 | }
13 | }
14 |
15 | .user-tab-content {
16 | padding: $m-size;
17 | height: 100%;
18 |
19 | @include mobile {
20 | padding: 0;
21 | }
22 | }
23 |
24 | .user-tab-nav {
25 | background: $background-color-01;
26 | border-bottom: 1px solid $border-color;
27 | padding: 0 $l-size;
28 |
29 | @media (max-width: $mobile) {
30 | padding: 0;
31 | }
32 | }
33 |
34 | .user-tab-menu {
35 | padding: 0;
36 | margin: 0;
37 | position: relative;
38 | bottom: -1px;
39 | }
40 |
41 | .user-tab-item {
42 | list-style-type: none;
43 | color: $gray-10;
44 | padding: $m-size;
45 | font-size: $font-medium;
46 | border-bottom: 1px solid transparent;
47 | display: inline-block;
48 | transition: all .3s ease;
49 |
50 | &:hover {
51 | cursor: pointer;
52 | background: $background-color;
53 | }
54 | }
55 |
56 | .user-tab-active {
57 | color: $paragraph-color;
58 | font-weight: bold;
59 | border-bottom: 1px solid $white;
60 | background: $white;
61 |
62 | &:hover {
63 | cursor: default;
64 | background: $white;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/styles/6 - utils/_animation.scss:
--------------------------------------------------------------------------------
1 | @keyframes fadeIn {
2 | 0% {
3 | opacity: 0;
4 | }
5 | 100% {
6 | opacity: 1;
7 | }
8 | }
9 |
10 | @keyframes spin {
11 | 100% {
12 | transform: rotate(360deg);
13 | }
14 | }
15 |
16 | @keyframes slide-up {
17 | 0% {
18 | opacity: 0;
19 | transform: translateY(50px);
20 | }
21 |
22 | 100% {
23 | opacity: 1;
24 | transform: translateY(0);
25 | }
26 | }
27 |
28 | @keyframes scale {
29 | 0% {
30 | transform: translate(-50%, -50%) scale(0);
31 | }
32 | 100% {
33 | transform: translate(-50%, -50%) scale(1);
34 | }
35 | }
36 |
37 | @keyframes fullWidth {
38 | 100% { width: 100%; }
39 | }
40 |
41 | @keyframes slide-down {
42 | 0% {
43 | transform: translateY(-100%);
44 | }
45 |
46 | 100% {
47 | transform: translateY(0);
48 | }
49 | }
--------------------------------------------------------------------------------
/src/styles/6 - utils/_state.scss:
--------------------------------------------------------------------------------
1 | //
2 | // STATE ---------
3 | //
4 |
5 | .is-active-link {
6 | font-weight: bold;
7 | color: green;
8 | }
9 |
10 | .is-basket-open {
11 | overflow-y: hidden;
12 | }
13 |
14 | .is-selected-payment {
15 | opacity: 1;
16 | }
17 |
18 | .is-basket-open .basket {
19 | transform: translateX(0);
20 | }
21 |
22 | .is-img-loading {
23 | opacity: 0;
24 | }
25 |
26 | .is-img-loaded {
27 | animation: fadeIn .3s ease;
28 | opacity: 1;
29 | }
30 |
31 | .is-open-recent-search .searchbar-recent {
32 | display: flex;
33 | flex-direction: column;
34 | }
35 |
36 | .is-nav-scrolled {
37 | position: fixed;
38 | animation: slide-down .3s ease 1;
39 | animation-fill-mode: forwards;
40 | top: 0;
41 | height: 6rem;
42 | padding-top: .5rem;
43 | background: $nav-bg-scrolled;
44 | box-shadow: 0 5px 10px rgba(0, 0, 0, .02);
45 | }
--------------------------------------------------------------------------------
/src/styles/style.scss:
--------------------------------------------------------------------------------
1 | // ---------------------------
2 | // 00 - VENDORS
3 |
4 | // ---------------------------
5 | // 01 - SETTINGS
6 |
7 | @import './1 - settings/breakpoints';
8 | @import './1 - settings/colors';
9 | @import './1 - settings/sizes';
10 | @import './1 - settings/typography';
11 | @import './1 - settings/zindex';
12 |
13 | // ---------------------------
14 | // 02 - TOOLS
15 |
16 | @import './2 - tools/functions';
17 | @import './2 - tools/mixins';
18 |
19 | // ---------------------------
20 | // 03 - GENERIC
21 |
22 | // ---------------------------
23 | // 04 - ELEMENTS
24 |
25 | @import './4 - elements/base';
26 | @import './4 - elements/button';
27 | @import './4 - elements/input';
28 | @import './4 - elements/link';
29 | @import './4 - elements/select';
30 | @import './4 - elements/textarea';
31 | @import './4 - elements/label';
32 |
33 |
34 | // ---------------------------
35 | // 05 - COMPONENTS
36 |
37 | @import './5 - components/icons';
38 | @import './5 - components/navigation';
39 | @import './5 - components/product';
40 | @import './5 - components/basket';
41 | @import './5 - components/sidebar';
42 | @import './5 - components/searchbar';
43 | @import './5 - components/badge';
44 | @import './5 - components/footer';
45 | @import './5 - components/filter';
46 | @import './5 - components/pricerange';
47 | @import './5 - components/modal';
48 | @import './5 - components/auth';
49 | @import './5 - components/banner';
50 | @import './5 - components/toast';
51 | @import './5 - components/home';
52 | @import './5 - components/circular-progress';
53 | @import './5 - components/preloader';
54 | @import './5 - components/pill';
55 | @import './5 - components/tooltip';
56 | @import './5 - components/color-chooser';
57 |
58 | // -- Admin components
59 | @import './5 - components/admin/grid';
60 | @import './5 - components/admin/product';
61 | @import './5 - components/sidenavigation';
62 | // @import './5 - components/admin/user';
63 |
64 | // -- Mobile components
65 | // @import './5 - components/mobile/bottom-navigation';
66 | @import './5 - components/mobile/mobile-navigation';
67 |
68 | // -- Profile components
69 | @import './5 - components/profile/user-nav';
70 | @import './5 - components/profile/user-profile';
71 | @import './5 - components/profile/user-tab';
72 | @import './5 - components/profile/editprofile';
73 |
74 | // -- Checkout components
75 | @import './5 - components/checkout/checkout';
76 |
77 | // -- 404
78 | @import './5 - components/404/page-not-found';
79 |
80 |
81 |
82 | // ---------------------------
83 | // 06 - UTILS
84 |
85 | @import './6 - utils/state';
86 | @import './6 - utils/animation';
87 | @import './6 - utils/utils';
88 |
--------------------------------------------------------------------------------
/src/sw-src.js:
--------------------------------------------------------------------------------
1 | import { precacheAndRoute } from 'workbox-precaching';
2 | import { registerRoute } from 'workbox-routing';
3 | import { CacheFirst } from 'workbox-strategies';
4 | import { ExpirationPlugin } from 'workbox-expiration';
5 | import { cacheNames } from 'workbox-core';
6 |
7 | precacheAndRoute(self.__WB_MANIFEST);
8 | let currentCacheNames = Object.assign({ precacheTemp: cacheNames.precache + "-temp" }, cacheNames);
9 |
10 | currentCacheNames.fonts = "googlefonts";
11 | registerRoute(
12 | /https:\/\/fonts.(?:googleapis|gstatic).com\/(.*)/,
13 | new CacheFirst({
14 | cacheName: currentCacheNames.fonts,
15 | plugins: [new ExpirationPlugin({ maxEntries: 30 })]
16 | }),
17 | "GET"
18 | );
19 |
20 | // clean up old SW caches
21 | self.addEventListener("activate", function (event) {
22 | event.waitUntil(
23 | caches.keys().then(function (cacheNames) {
24 | let validCacheSet = new Set(Object.values(currentCacheNames));
25 | return Promise.all(
26 | cacheNames
27 | .filter(function (cacheName) {
28 | return !validCacheSet.has(cacheName);
29 | })
30 | .map(function (cacheName) {
31 | console.log("deleting cache", cacheName);
32 | return caches.delete(cacheName);
33 | })
34 | );
35 | })
36 | );
37 | });
--------------------------------------------------------------------------------
/src/views/account/components/UserAccountTab.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable indent */
2 | import { ImageLoader } from '@/components/common';
3 | import { ACCOUNT_EDIT } from '@/constants/routes';
4 | import { displayDate } from '@/helpers/utils';
5 | import PropType from 'prop-types';
6 | import React from 'react';
7 | import { useSelector } from 'react-redux';
8 | import { withRouter } from 'react-router-dom';
9 |
10 | const UserProfile = (props) => {
11 | const profile = useSelector((state) => state.profile);
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 |
30 |
31 |
38 |
39 |
40 |
{profile.fullname}
41 | Email
42 |
43 | {profile.email}
44 | Address
45 |
46 | {profile.address ? (
47 | {profile.address}
48 | ) : (
49 | Address not set
50 | )}
51 | Mobile
52 |
53 | {profile.mobile ? (
54 | {profile.mobile.value}
55 | ) : (
56 | Mobile not set
57 | )}
58 | Date Joined
59 |
60 | {profile.dateJoined ? (
61 | {displayDate(profile.dateJoined)}
62 | ) : (
63 | Not available
64 | )}
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | UserProfile.propTypes = {
72 | history: PropType.shape({
73 | push: PropType.func
74 | }).isRequired
75 | };
76 |
77 | export default withRouter(UserProfile);
78 |
--------------------------------------------------------------------------------
/src/views/account/components/UserAvatar.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable indent */
2 | import {
3 | DownOutlined, LoadingOutlined, LogoutOutlined, UserOutlined
4 | } from '@ant-design/icons';
5 | import { ACCOUNT } from '@/constants/routes';
6 | import PropTypes from 'prop-types';
7 | import React, { useEffect, useRef } from 'react';
8 | import { useDispatch, useSelector } from 'react-redux';
9 | import { Link, withRouter } from 'react-router-dom';
10 | import { signOut } from '@/redux/actions/authActions';
11 |
12 | const UserNav = () => {
13 | const { profile, isAuthenticating } = useSelector((state) => ({
14 | profile: state.profile,
15 | isAuthenticating: state.app.isAuthenticating
16 | }));
17 | const userNav = useRef(null);
18 | const dispatch = useDispatch();
19 |
20 | const toggleDropdown = (e) => {
21 | const closest = e.target.closest('div.user-nav');
22 |
23 | try {
24 | if (!closest && userNav.current.classList.contains('user-sub-open')) {
25 | userNav.current.classList.remove('user-sub-open');
26 | }
27 | } catch (err) {
28 | console.log(err);
29 | }
30 | };
31 |
32 | useEffect(() => {
33 | document.addEventListener('click', toggleDropdown);
34 |
35 | return () => document.removeEventListener('click', toggleDropdown);
36 | }, []);
37 |
38 | const onClickNav = () => {
39 | userNav.current.classList.toggle('user-sub-open');
40 | };
41 |
42 | return isAuthenticating ? (
43 |
44 | Signing Out
45 |
46 |
47 |
48 | ) : (
49 | { }}
53 | ref={userNav}
54 | role="button"
55 | tabIndex={0}
56 | >
57 |
{profile.fullname && profile.fullname.split(' ')[0]}
58 |
59 |

64 |
65 |
66 |
67 | {profile.role !== 'ADMIN' && (
68 |
72 | View Account
73 |
74 |
75 | )}
76 |
dispatch(signOut())}
79 | role="presentation"
80 | >
81 | Sign Out
82 |
83 |
84 |
85 |
86 | );
87 | };
88 |
89 | UserNav.propType = {
90 | profile: PropTypes.object.isRequired
91 | };
92 |
93 | export default withRouter(UserNav);
94 |
--------------------------------------------------------------------------------
/src/views/account/components/UserOrdersTab.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // Just add this feature if you want :P
4 |
5 | const UserOrdersTab = () => (
6 |
7 |
My Orders
8 | You don't have any orders
9 |
10 | );
11 |
12 | export default UserOrdersTab;
13 |
--------------------------------------------------------------------------------
/src/views/account/components/UserTab.jsx:
--------------------------------------------------------------------------------
1 | import PropType from 'prop-types';
2 | import React, { useState } from 'react';
3 |
4 | const UserTab = (props) => {
5 | const { children } = props;
6 | const [activeTab, setActiveTab] = useState(children[0].props.index || 0);
7 | const onClickTabItem = (index) => setActiveTab(index);
8 |
9 | return (
10 |
11 |
12 |
13 | {children.map((child) => (
14 | - onClickTabItem(child.props.index)}
19 | >
20 | {child.props.label}
21 |
22 | ))}
23 |
24 |
25 |
26 | {children.map((child) => {
27 | if (child.props.index !== activeTab) return null;
28 |
29 | return child.props.children;
30 | })}
31 |
32 |
33 | );
34 | };
35 |
36 | UserTab.propTypes = {
37 | children: PropType.oneOfType([
38 | PropType.arrayOf(PropType.node),
39 | PropType.node
40 | ]).isRequired
41 | };
42 |
43 | export default UserTab;
44 |
--------------------------------------------------------------------------------
/src/views/account/components/UserWishListTab.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // Just add this feature if you want :P
4 |
5 | const UserWishListTab = () => (
6 |
7 |
My Wish List
8 | You don't have a wish list
9 |
10 | );
11 |
12 | export default UserWishListTab;
13 |
--------------------------------------------------------------------------------
/src/views/account/edit_account/ConfirmModal.jsx:
--------------------------------------------------------------------------------
1 | import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
2 | import { Modal } from '@/components/common';
3 | import { useFormikContext } from 'formik';
4 | import PropType from 'prop-types';
5 | import React, { useState } from 'react';
6 |
7 | const ConfirmModal = ({ onConfirmUpdate, modal }) => {
8 | const [password, setPassword] = useState('');
9 | const { values } = useFormikContext();
10 |
11 | return (
12 |
16 |
17 |
Confirm Update
18 |
19 | To continue updating profile including your
20 | email
21 | ,
22 |
23 | please confirm by entering your password
24 |
25 |
setPassword(e.target.value)}
28 | placeholder="Enter your password"
29 | required
30 | type="password"
31 | value={password}
32 | />
33 |
34 |
35 |
36 |
48 |
49 |
56 |
57 | );
58 | };
59 |
60 | ConfirmModal.propTypes = {
61 | onConfirmUpdate: PropType.func.isRequired,
62 | modal: PropType.shape({
63 | onCloseModal: PropType.func,
64 | isOpenModal: PropType.bool
65 | }).isRequired
66 | };
67 |
68 | export default ConfirmModal;
69 |
--------------------------------------------------------------------------------
/src/views/account/edit_account/EditForm.jsx:
--------------------------------------------------------------------------------
1 | import { ArrowLeftOutlined, CheckOutlined, LoadingOutlined } from '@ant-design/icons';
2 | import { CustomInput, CustomMobileInput } from '@/components/formik';
3 | import { ACCOUNT } from '@/constants/routes';
4 | import { Field, useFormikContext } from 'formik';
5 | import PropType from 'prop-types';
6 | import React from 'react';
7 | import { useHistory } from 'react-router-dom';
8 |
9 | const EditForm = ({ isLoading, authProvider }) => {
10 | const history = useHistory();
11 | const { values, submitForm } = useFormikContext();
12 |
13 | return (
14 |
15 |
24 |
32 |
41 |
47 |
48 |
49 |
59 |
69 |
70 |
71 | );
72 | };
73 |
74 | EditForm.propTypes = {
75 | isLoading: PropType.bool.isRequired,
76 | authProvider: PropType.string.isRequired
77 | };
78 |
79 | export default EditForm;
80 |
--------------------------------------------------------------------------------
/src/views/account/user_account/index.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-multi-comp */
2 | import { LoadingOutlined } from '@ant-design/icons';
3 | import { useDocumentTitle, useScrollTop } from '@/hooks';
4 | import React, { lazy, Suspense } from 'react';
5 | import UserTab from '../components/UserTab';
6 |
7 | const UserAccountTab = lazy(() => import('../components/UserAccountTab'));
8 | const UserWishListTab = lazy(() => import('../components/UserWishListTab'));
9 | const UserOrdersTab = lazy(() => import('../components/UserOrdersTab'));
10 |
11 | const Loader = () => (
12 |
13 |
14 |
Loading ...
15 |
16 | );
17 |
18 | const UserAccount = () => {
19 | useScrollTop();
20 | useDocumentTitle('My Account | Salinaka');
21 |
22 | return (
23 |
24 |
25 | }>
26 |
27 |
28 |
29 |
30 | }>
31 |
32 |
33 |
34 |
35 | }>
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export default UserAccount;
44 |
--------------------------------------------------------------------------------
/src/views/admin/add_product/index.jsx:
--------------------------------------------------------------------------------
1 | import { LoadingOutlined } from '@ant-design/icons';
2 | import { useDocumentTitle, useScrollTop } from '@/hooks';
3 | import React, { lazy, Suspense } from 'react';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { withRouter } from 'react-router-dom';
6 | import { addProduct } from '@/redux/actions/productActions';
7 |
8 | const ProductForm = lazy(() => import('../components/ProductForm'));
9 |
10 | const AddProduct = () => {
11 | useScrollTop();
12 | useDocumentTitle('Add New Product | Salinaka');
13 | const isLoading = useSelector((state) => state.app.loading);
14 | const dispatch = useDispatch();
15 |
16 | const onSubmit = (product) => {
17 | dispatch(addProduct(product));
18 | };
19 |
20 | return (
21 |
22 |
Add New Product
23 |
25 | Loading ...
26 |
27 |
28 |
29 | )}
30 | >
31 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default withRouter(AddProduct);
55 |
--------------------------------------------------------------------------------
/src/views/admin/components/ProductsNavbar.jsx:
--------------------------------------------------------------------------------
1 | import { FilterOutlined, PlusOutlined } from '@ant-design/icons';
2 | import { FiltersToggle, SearchBar } from '@/components/common';
3 | import { ADD_PRODUCT } from '@/constants/routes';
4 | import PropType from 'prop-types';
5 | import React from 'react';
6 | import { useHistory } from 'react-router-dom';
7 |
8 | const ProductsNavbar = (props) => {
9 | const { productsCount, totalProductsCount } = props;
10 | const history = useHistory();
11 |
12 | return (
13 |
37 | );
38 | };
39 |
40 | ProductsNavbar.propTypes = {
41 | productsCount: PropType.number.isRequired,
42 | totalProductsCount: PropType.number.isRequired
43 | };
44 |
45 | export default ProductsNavbar;
46 |
--------------------------------------------------------------------------------
/src/views/admin/components/ProductsTable.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | import PropType from 'prop-types';
3 | import React from 'react';
4 | import { ProductItem } from '.';
5 |
6 | const ProductsTable = ({ filteredProducts }) => (
7 |
41 | );
42 |
43 | ProductsTable.propTypes = {
44 | filteredProducts: PropType.array.isRequired
45 | };
46 |
47 | export default ProductsTable;
48 |
--------------------------------------------------------------------------------
/src/views/admin/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as ProductForm } from './ProductForm';
2 | export { default as ProductItem } from './ProductItem';
3 | export { default as ProductsNavbar } from './ProductsNavbar';
4 |
5 |
--------------------------------------------------------------------------------
/src/views/admin/dashboard/index.jsx:
--------------------------------------------------------------------------------
1 | import { useDocumentTitle, useScrollTop } from '@/hooks';
2 | import React from 'react';
3 |
4 | const Dashboard = () => {
5 | useDocumentTitle('Welcome | Admin Dashboard');
6 | useScrollTop();
7 |
8 | return (
9 |
12 | );
13 | };
14 |
15 | export default Dashboard;
16 |
--------------------------------------------------------------------------------
/src/views/admin/edit_product/index.jsx:
--------------------------------------------------------------------------------
1 | import { LoadingOutlined } from '@ant-design/icons';
2 | import { useDocumentTitle, useProduct, useScrollTop } from '@/hooks';
3 | import PropType from 'prop-types';
4 | import React, { lazy, Suspense } from 'react';
5 | import { useDispatch } from 'react-redux';
6 | import { Redirect, withRouter } from 'react-router-dom';
7 | import { editProduct } from '@/redux/actions/productActions';
8 |
9 | const ProductForm = lazy(() => import('../components/ProductForm'));
10 |
11 | const EditProduct = ({ match }) => {
12 | useDocumentTitle('Edit Product | Salinaka');
13 | useScrollTop();
14 | const { product, error, isLoading } = useProduct(match.params.id);
15 | const dispatch = useDispatch();
16 |
17 | const onSubmitForm = (updates) => {
18 | dispatch(editProduct(product.id, updates));
19 | };
20 |
21 | return (
22 |