├── README.md ├── src ├── components │ ├── index.js │ ├── layouts │ │ └── Layout.jsx │ ├── skeleton │ │ ├── ProductSkeleton.jsx │ │ └── CategorySkeleton.jsx │ ├── form │ │ ├── Select.jsx │ │ └── Input.jsx │ ├── admin │ │ ├── Drawer.jsx │ │ ├── TableList.jsx │ │ └── PageHeader.jsx │ ├── sidebar │ │ ├── SidebarItem.jsx │ │ └── index.jsx │ ├── main │ │ ├── MainCategory.jsx │ │ ├── product.jsx │ │ └── index.jsx │ ├── basket │ │ ├── Order.jsx │ │ ├── product.jsx │ │ └── index.jsx │ └── settings │ │ └── SettingsData.jsx ├── assets │ ├── images │ │ ├── imgs │ │ │ ├── logo 1.png │ │ │ ├── product1.jpeg │ │ │ ├── product1.png │ │ │ ├── product2.jpeg │ │ │ ├── product2.png │ │ │ ├── product3.jpeg │ │ │ ├── product3.png │ │ │ └── product4.jpeg │ │ └── icons │ │ │ ├── search.svg │ │ │ ├── Remove.jsx │ │ │ ├── ProductsIcon.jsx │ │ │ ├── PizzaCategory.jsx │ │ │ ├── CategoryIcon.jsx │ │ │ ├── DrinkCategory.jsx │ │ │ ├── DessertCategory.jsx │ │ │ ├── Heart.jsx │ │ │ ├── HomeIcon.jsx │ │ │ ├── Help.jsx │ │ │ ├── burger_menu.svg │ │ │ ├── burger_menu2.svg │ │ │ ├── WatermelonCategory.jsx │ │ │ ├── question_mark.svg │ │ │ ├── delete.svg │ │ │ ├── Settings.jsx │ │ │ └── sidebar__logo-text.svg │ └── scss │ │ ├── pages.scss │ │ ├── admin │ │ ├── product │ │ │ └── products.scss │ │ ├── table.scss │ │ ├── pageHeader.scss │ │ ├── login.scss │ │ └── drawer.scss │ │ ├── skeleton.scss │ │ ├── global │ │ ├── _normalize.scss │ │ ├── _settings.scss │ │ └── _variables.scss │ │ ├── mixins │ │ └── _mixins.scss │ │ ├── toggle_btn.scss │ │ ├── order.scss │ │ ├── settings.scss │ │ ├── sidebar.scss │ │ ├── basket.scss │ │ └── main.scss ├── pages │ ├── help │ │ └── index.jsx │ ├── favourite │ │ └── index.jsx │ ├── home │ │ └── index.jsx │ ├── login │ │ └── index.jsx │ ├── settings │ │ └── index.jsx │ ├── categories │ │ └── index.jsx │ └── products │ │ └── index.jsx ├── api │ └── index.js ├── redux │ ├── actions │ │ ├── cartAction.js │ │ ├── loginAction.js │ │ ├── productsActions.js │ │ └── categoriesAction.js │ ├── slices │ │ ├── colorSlice.js │ │ ├── userSlice.js │ │ ├── productSlice.js │ │ ├── categoriesSlice.js │ │ ├── authSlice.js │ │ └── cartSlice.js │ └── store.js ├── utils │ ├── urls.js │ └── routes.jsx ├── main.jsx ├── hooks │ └── useIsAuth.jsx ├── helpers │ ├── settings.js │ ├── sidebar_icons.js │ └── products.js └── App.jsx ├── vite.config.js ├── .gitignore ├── index.html ├── SECURITY.md ├── package.json ├── LICENSE └── public └── vite.svg /README.md: -------------------------------------------------------------------------------- 1 | # remolo-pizza 2 | 3 | Remolo-pizza is a food delivery service app for a restaurant. -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import { Layout } from "./layouts/Layout"; 2 | 3 | export { Layout }; 4 | -------------------------------------------------------------------------------- /src/assets/images/imgs/logo 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiryusupov/remolo-pizza/HEAD/src/assets/images/imgs/logo 1.png -------------------------------------------------------------------------------- /src/assets/images/imgs/product1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiryusupov/remolo-pizza/HEAD/src/assets/images/imgs/product1.jpeg -------------------------------------------------------------------------------- /src/assets/images/imgs/product1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiryusupov/remolo-pizza/HEAD/src/assets/images/imgs/product1.png -------------------------------------------------------------------------------- /src/assets/images/imgs/product2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiryusupov/remolo-pizza/HEAD/src/assets/images/imgs/product2.jpeg -------------------------------------------------------------------------------- /src/assets/images/imgs/product2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiryusupov/remolo-pizza/HEAD/src/assets/images/imgs/product2.png -------------------------------------------------------------------------------- /src/assets/images/imgs/product3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiryusupov/remolo-pizza/HEAD/src/assets/images/imgs/product3.jpeg -------------------------------------------------------------------------------- /src/assets/images/imgs/product3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiryusupov/remolo-pizza/HEAD/src/assets/images/imgs/product3.png -------------------------------------------------------------------------------- /src/assets/images/imgs/product4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiryusupov/remolo-pizza/HEAD/src/assets/images/imgs/product4.jpeg -------------------------------------------------------------------------------- /src/pages/help/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Help() { 4 | return ( 5 |
Help
6 | ) 7 | } 8 | 9 | export default Help; -------------------------------------------------------------------------------- /src/pages/favourite/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Favourites() { 4 | return ( 5 |
Favourites
6 | ) 7 | } 8 | 9 | export default Favourites; -------------------------------------------------------------------------------- /src/pages/home/index.jsx: -------------------------------------------------------------------------------- 1 | import { Layout } from "../../components" 2 | 3 | const HomePage = () => { 4 | return ( 5 | 6 | ) 7 | } 8 | 9 | export default HomePage -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { appMode, domain } from "../utils/urls"; 3 | 4 | const axiosUrl = axios.create({ 5 | baseURL: `${domain}${appMode}` 6 | }) 7 | 8 | export default axiosUrl; -------------------------------------------------------------------------------- /src/assets/scss/pages.scss: -------------------------------------------------------------------------------- 1 | @import "./admin/product/products.scss"; 2 | @import "./basket.scss"; 3 | @import "./settings.scss"; 4 | @import "./admin/login.scss"; 5 | @import "./admin/pageHeader.scss"; 6 | @import "./admin/table.scss"; 7 | @import "./admin/drawer.scss" -------------------------------------------------------------------------------- /src/assets/scss/admin/product/products.scss: -------------------------------------------------------------------------------- 1 | .products { 2 | width: 100%; 3 | @include flex(flex-start, center, unset); 4 | padding: 30px; 5 | &__container { 6 | width: 100%; 7 | max-width: 1260px; 8 | margin: 0 auto; 9 | padding: 0 20px; 10 | } 11 | } -------------------------------------------------------------------------------- /src/components/layouts/Layout.jsx: -------------------------------------------------------------------------------- 1 | import Basket from "../basket"; 2 | import Main from "../main"; 3 | 4 | export const Layout = () => { 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/redux/actions/cartAction.js: -------------------------------------------------------------------------------- 1 | import { DECREASE_QTY, DELETE_CART, INCREASE_QTY, SET_CART } from "../actionTypes"; 2 | 3 | export const setCartAc = (payload) => ({type: SET_CART, payload}) 4 | export const delCartAc = (payload) => ({type: DELETE_CART, payload}) 5 | export const increaseCartAc = (payload) => ({type: INCREASE_QTY, payload}) 6 | export const decreaseCartAc = (payload) => ({type: DECREASE_QTY, payload}) -------------------------------------------------------------------------------- /src/assets/images/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Remolo Pizza 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/utils/urls.js: -------------------------------------------------------------------------------- 1 | export const domain = "https://pizzaapi.pythonanywhere.com" 2 | export const appMode = "/api" 3 | export const login = "/token/" 4 | export const productsListUrl="/product/all" 5 | export const productAddUrl="/product/all/" 6 | export const productEditUrl=id=>`/product/${id}/` 7 | export const categoryAddUrl="/category/all/" 8 | export const categoryEditUrl=id=>`/category/${id}/` 9 | export const categoriesListUrl="/category/all" -------------------------------------------------------------------------------- /src/redux/slices/colorSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | color: "red" 5 | } 6 | 7 | const colorSlice = createSlice({ 8 | name: "color", 9 | initialState, 10 | reducers: { 11 | setColor: (state, action) => { 12 | state.color = action.payload 13 | } 14 | } 15 | }) 16 | export const {setColor} = colorSlice.actions 17 | export default colorSlice.reducer; -------------------------------------------------------------------------------- /src/assets/scss/skeleton.scss: -------------------------------------------------------------------------------- 1 | .skeleton__category { 2 | width: 100%; 3 | @include flex(center, space-between, row); 4 | height: 50px; 5 | &-item { 6 | @include flex(center, center, row); 7 | gap: 15px; 8 | margin-bottom: 15px; 9 | } 10 | } 11 | .skeleton__products { 12 | width: 100%; 13 | @include flex(center, flex-start, row); 14 | flex-wrap: wrap; 15 | margin-top: 35px; 16 | gap: 20px 20px; 17 | } -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import { BrowserRouter } from "react-router-dom"; 5 | import { Provider } from "react-redux"; 6 | import store from "./redux/store.js"; 7 | 8 | ReactDOM.createRoot(document.getElementById("root")).render( 9 | 10 | 11 | 12 | 13 | 14 | ); -------------------------------------------------------------------------------- /src/redux/actions/loginAction.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from "@reduxjs/toolkit"; 2 | import axiosUrl from "../../api"; 3 | import { login } from "../../utils/urls"; 4 | 5 | export const loginRequest = createAsyncThunk( 6 | 'user/login', 7 | async (data, thunkApi) => { 8 | try { 9 | const response = await axiosUrl.post(login, data) 10 | return response.data 11 | } 12 | catch (error) { 13 | return error 14 | } 15 | } 16 | ) -------------------------------------------------------------------------------- /src/components/skeleton/ProductSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Skeleton from 'react-loading-skeleton'; 3 | 4 | function ProductSkeleton({products}) { 5 | return ( 6 |
7 | { 8 | Array(products).fill(0).map((_, id) => { 9 | return ( 10 | 11 | ) 12 | } 13 | ) 14 | } 15 |
16 | ) 17 | } 18 | 19 | export default ProductSkeleton; -------------------------------------------------------------------------------- /src/components/form/Select.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function SelectForm({ options, defaultValue, labelValue, labelName, inputName }) { 4 | return ( 5 |
6 | 7 | 16 |
17 | ) 18 | } 19 | 20 | export default SelectForm -------------------------------------------------------------------------------- /src/components/form/Input.jsx: -------------------------------------------------------------------------------- 1 | export const Input = ({ 2 | value, 3 | placeholder, 4 | labelInput, 5 | labelName, 6 | inputName, 7 | type = "text", 8 | onBlur = () => {}, 9 | onChange 10 | }) => { 11 | return ( 12 |
13 | 14 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/assets/images/icons/Remove.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | const RemoveIcon = (props) => ( 3 | 10 | 16 | 17 | ) 18 | export default RemoveIcon 19 | -------------------------------------------------------------------------------- /src/assets/images/icons/ProductsIcon.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | const ProductIcon = (props) => ( 3 | 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | export default ProductIcon -------------------------------------------------------------------------------- /src/assets/images/icons/PizzaCategory.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | const PizzaCategoryIcon = (props) => ( 3 | 10 | 14 | 15 | ) 16 | export default PizzaCategoryIcon -------------------------------------------------------------------------------- /src/assets/images/icons/CategoryIcon.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | const CategoryIcon = (props) => ( 3 | 10 | 11 | 12 | ) 13 | export default CategoryIcon -------------------------------------------------------------------------------- /src/redux/slices/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const localeStorageUser = localStorage.getItem("user"); 4 | const initialState = JSON.parse(localeStorageUser) 5 | ? JSON.parse(localeStorageUser) 6 | : { 7 | name: "", 8 | address: "", 9 | phone: "", 10 | }; 11 | 12 | const userSlice = createSlice({ 13 | name: "user", 14 | initialState, 15 | reducers: { 16 | setUser: (state, action) => { 17 | state[action.payload.key] = action.payload.value; 18 | localStorage.setItem("user", JSON.stringify(state)); 19 | }, 20 | }, 21 | }); 22 | export const { setUser } = userSlice.actions; 23 | export default userSlice.reducer; 24 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux" 2 | import categoriesSlice from "./slices/categoriesSlice" 3 | import { configureStore } from "@reduxjs/toolkit" 4 | import productSlice from "./slices/productSlice" 5 | import cartSlice from "./slices/cartSlice" 6 | import userSlice from "./slices/userSlice" 7 | import colorSlice from "./slices/colorSlice" 8 | import authSlice from "./slices/authSlice" 9 | 10 | const reducer = combineReducers({ 11 | categories: categoriesSlice, 12 | products: productSlice, 13 | cart: cartSlice, 14 | user: userSlice, 15 | color: colorSlice, 16 | auth: authSlice 17 | }) 18 | 19 | const store = configureStore({ reducer }) 20 | export default store; -------------------------------------------------------------------------------- /src/assets/images/icons/DrinkCategory.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | const DrinkIcon = (props) => ( 3 | 10 | 14 | 15 | ) 16 | export default DrinkIcon -------------------------------------------------------------------------------- /src/assets/scss/global/_normalize.scss: -------------------------------------------------------------------------------- 1 | @import "../global/variables"; 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | box-sizing: border-box; 6 | -webkit-tap-highlight-color: transparent; 7 | } 8 | 9 | ul li { 10 | list-style: none; 11 | } 12 | 13 | a { 14 | text-decoration: none; 15 | color: inherit; 16 | } 17 | a, span, button { 18 | font-family: var(--main-font); 19 | } 20 | 21 | button { 22 | cursor: pointer; 23 | } 24 | 25 | input::-webkit-outer-spin-button, 26 | input::-webkit-inner-spin-button { 27 | -webkit-appearance: none; 28 | margin: 0; 29 | } 30 | 31 | /* Firefox */ 32 | input[type="number"] { 33 | -moz-appearance: textfield; 34 | } 35 | 36 | body, 37 | html { 38 | height: 100%; 39 | } 40 | -------------------------------------------------------------------------------- /src/redux/slices/productSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit" 2 | import { getProducts } from "../actions/productsActions" 3 | 4 | const initialState = { 5 | items: [], 6 | loading: false, 7 | error: "" 8 | } 9 | 10 | const productsSlice = createSlice({ 11 | name: "products", 12 | initialState, 13 | reducers: {}, 14 | extraReducers: { 15 | [getProducts.fulfilled.type]: (state, action) => { 16 | state.items = action.payload, 17 | state.loading = false, 18 | state.error = "" 19 | }, 20 | [getProducts.pending.type]: (state) => { 21 | state.loading = true 22 | } 23 | } 24 | }) 25 | 26 | export default productsSlice.reducer -------------------------------------------------------------------------------- /src/assets/scss/global/_settings.scss: -------------------------------------------------------------------------------- 1 | // body.open-modal, 2 | // body.modal-wrap-active { 3 | // height: 100vh; 4 | // overflow: hidden; 5 | 6 | // @include mediaMobileFirst(700) { 7 | // padding-right: 10px; 8 | // } 9 | // } 10 | 11 | body { 12 | background-color: #fafafd; 13 | } 14 | 15 | // body.show { 16 | // height: 100vh; 17 | // overflow: hidden; 18 | 19 | // @include mediaMobileFirst(700) { 20 | // padding-right: 10px; 21 | // } 22 | // } 23 | 24 | button span { 25 | pointer-events: none; 26 | } 27 | 28 | .wrapper { 29 | position: relative; 30 | overflow-x: hidden; 31 | width: 100%; 32 | 33 | &.show { 34 | height: 100vh; 35 | } 36 | } 37 | 38 | .container { 39 | width: 100%; 40 | max-width: 1180px; 41 | padding: 0 20px; 42 | margin: 0 auto; 43 | } 44 | -------------------------------------------------------------------------------- /src/components/admin/Drawer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | 3 | function Drawer({ title, open, close, children }) { 4 | useEffect(() => { 5 | const backdrop = document.querySelector(".drawer__backdrop") 6 | backdrop.addEventListener("click", close) 7 | 8 | return () => backdrop.removeEventListener("click", close) 9 | }) 10 | return ( 11 |
12 |
13 |
14 |
15 | {title} 16 | 17 |
18 |
{children}
19 |
20 |
21 | ) 22 | } 23 | 24 | export default Drawer; -------------------------------------------------------------------------------- /src/components/skeleton/CategorySkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Skeleton from "react-loading-skeleton"; 3 | import "react-loading-skeleton/dist/skeleton.css"; 4 | 5 | function CategorySkeleton({ categories }) { 6 | return ( 7 |
8 | { 9 | Array(categories).fill(0).map((_, id) => { 10 | return ( 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | ) 20 | }) 21 | } 22 |
23 | ); 24 | } 25 | 26 | export default CategorySkeleton; 27 | -------------------------------------------------------------------------------- /src/hooks/useIsAuth.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { setAuth } from "../redux/slices/authSlice"; 5 | 6 | function useIsAuth() { 7 | const { tokens, isAuth } = useSelector((state) => state.auth) 8 | const dispatch = useDispatch() 9 | const navigate = useNavigate() 10 | return useMemo(() => { 11 | if ( 12 | Boolean(tokens.accessToken?.trim().length) && 13 | Boolean(tokens.refreshToken?.trim().length) 14 | ) { 15 | console.log(true, "if trueeee"); 16 | dispatch(setAuth(true)) 17 | navigate("/products") 18 | } 19 | else { 20 | console.log(false, "if falseeee"); 21 | dispatch(setAuth(false)) 22 | } 23 | return isAuth 24 | }, [tokens]) 25 | } 26 | 27 | export default useIsAuth; -------------------------------------------------------------------------------- /src/redux/actions/productsActions.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from "@reduxjs/toolkit"; 2 | import axiosUrl from "../../api"; 3 | import { productAddUrl, productEditUrl, productsListUrl } from "../../utils/urls"; 4 | 5 | export const getProducts = createAsyncThunk( 6 | 'products/getAllProducts', 7 | async (_, thunkApi) => { 8 | const response = await axiosUrl.get(productsListUrl) 9 | return response.data; 10 | } 11 | ) 12 | 13 | export const addProduct = async (data) => { 14 | try { 15 | const response = await axiosUrl.put(productAddUrl, data) 16 | return response.data 17 | } catch (error) { 18 | return error 19 | } 20 | } 21 | export const editProduct = async (data, id) => { 22 | try { 23 | const response = await axiosUrl.put(productEditUrl(id), data) 24 | return response.data 25 | } catch (error) { 26 | return error 27 | } 28 | } -------------------------------------------------------------------------------- /src/redux/slices/categoriesSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { getCategories } from "../actions/categoriesAction"; 3 | 4 | const initialState = { 5 | activeCategory: 1, 6 | items: [], 7 | loading: true 8 | } 9 | 10 | const categoriesSlice = createSlice({ 11 | name: "categories", 12 | initialState, 13 | reducers: { 14 | setActiveCategory: (state, action) => { 15 | state.activeCategory = action.payload 16 | }, 17 | }, 18 | extraReducers: { 19 | [getCategories.fulfilled.type]: (state, action) => { 20 | state.items = action.payload, 21 | state.loading = false 22 | }, 23 | [getCategories.pending.type]: (state) => { 24 | state.loading = true 25 | }, 26 | 27 | } 28 | }) 29 | export const {setActiveCategory} = categoriesSlice.actions 30 | export default categoriesSlice.reducer; -------------------------------------------------------------------------------- /src/redux/actions/categoriesAction.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from "@reduxjs/toolkit"; 2 | import axiosUrl from "../../api"; 3 | import { categoriesListUrl, categoryAddUrl, categoryEditUrl } from "../../utils/urls"; 4 | 5 | export const getCategories = createAsyncThunk( 6 | 'categories/getAllCategories', 7 | async (_, thunkApi) => { 8 | const response = await axiosUrl.get(categoriesListUrl) 9 | return response.data; 10 | } 11 | ) 12 | export const addCategory = async (data) => { 13 | try { 14 | const response = await axiosUrl.put(categoryAddUrl, data) 15 | return response.data 16 | } catch (error) { 17 | return error 18 | } 19 | } 20 | export const editCategory = async (data, id) => { 21 | try { 22 | const response = await axiosUrl.put(categoryEditUrl(id), data) 23 | return response.data 24 | } catch (error) { 25 | return error 26 | } 27 | } -------------------------------------------------------------------------------- /src/components/admin/TableList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function TableList({ columns, data, loading }) { 4 | return ( 5 | 6 | 7 | 8 | {columns.map((item, key) => { 9 | return ( 10 | 11 | ) 12 | })} 13 | 14 | 15 | { 16 | loading ? "loading" : 17 | {data.map((dataItem, i) => { 18 | return ( 19 | 20 | {columns.map((columnItem, k) => { 21 | return ( 22 | 23 | ) 24 | })} 25 | 26 | ) 27 | })} 28 | 29 | } 30 |
{item.title}
{Boolean(columnItem.dataKey) ? dataItem[columnItem.dataKey] : columnItem.render(dataItem)}
31 | ) 32 | } 33 | 34 | export default TableList -------------------------------------------------------------------------------- /src/assets/images/icons/DessertCategory.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | const DessertIcon = (props) => ( 3 | 10 | 14 | 15 | ) 16 | export default DessertIcon 17 | -------------------------------------------------------------------------------- /src/assets/scss/global/_variables.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap'); 2 | :root { 3 | --main-font: 'Poppins', sans-serif; 4 | --black-color: #353535; 5 | --white-color: #FFFFFF; 6 | --sidebar-bg: #F6F6F6; 7 | --gray-color: #616161; 8 | --gray-btn-color: #8A8A8A; 9 | --gray-bg-color: #E8E8E8; 10 | --btn-bg-color: #EBEBEB; 11 | --dark-black-color: #464646; 12 | --order-bg-color: #5C5C5C; 13 | --light-gray: #A6A6A6; 14 | --gray-border-color: #D2D2D2; 15 | --gray-plus-color: #AEAEAE; 16 | --violet-color: #6c63ff; 17 | --violet-hover: #594fff; 18 | --main-color: #FF4E4E; 19 | [data-colors="red"] { 20 | --main-color: #FF4E4E; 21 | }; 22 | [data-colors="yellow"] { 23 | --main-color: #FFA73F; 24 | }; 25 | [data-colors="blue"] { 26 | --main-color: #50C2E7; 27 | }; 28 | [data-colors="pink"] { 29 | --main-color: #FF678C; 30 | }; 31 | } -------------------------------------------------------------------------------- /src/assets/images/icons/Heart.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | const HeartIcon = (props) => ( 3 | 11 | 15 | 16 | ) 17 | export default HeartIcon 18 | -------------------------------------------------------------------------------- /src/assets/scss/mixins/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin flex($alignItem: center, $justifyContent: center, $direction: row) { 2 | display: flex; 3 | align-items: $alignItem; 4 | justify-content: $justifyContent; 5 | flex-direction: $direction; 6 | } 7 | 8 | @mixin font($size, $min_size, $weight, $height) { 9 | @include adaptive-value("font-size", $size, $min_size, 1); 10 | font-weight: $weight; 11 | line-height: $height; 12 | } 13 | 14 | $maxWidth: 1920; 15 | $maxWidthContainer: 1440; 16 | @mixin adaptive-value($property, $startSize, $minSize, $type) { 17 | $addSize: $startSize - $minSize; 18 | #{$property}: $startSize + px; 19 | @media (max-width:#{$maxWidthContainer + px}) { 20 | #{$property}: calc( 21 | #{$minSize + px} + #{$addSize} * 22 | ((100vw - 320px) / #{$maxWidthContainer - 320}) 23 | ); 24 | } 25 | } 26 | 27 | @mixin flex-center { 28 | @include flex(); 29 | } 30 | 31 | @mixin media($width) { 32 | @media only screen and (max-width: #{$width + px}) { 33 | @content; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Link, useLocation } from "react-router-dom"; 3 | import { useSelector } from 'react-redux'; 4 | import { settings } from '../../helpers/settings'; 5 | 6 | function SidebarItem({sidebar__data}) { 7 | const {pathname} = useLocation() 8 | const [activePage, setActivePage] = useState(pathname) 9 | useEffect(() => { 10 | setActivePage(pathname) 11 | }, [pathname]) 12 | return ( 13 |
14 | { 15 | sidebar__data.map(el => { 16 | return ( 17 |
18 | 19 | {el.image} 20 | {el.text} 21 | 22 |
23 | ) 24 | }) 25 | } 26 |
27 | ) 28 | } 29 | 30 | export default SidebarItem -------------------------------------------------------------------------------- /src/assets/images/icons/HomeIcon.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | const HomeIcon = (props) => ( 3 | 11 | 15 | 16 | ) 17 | export default HomeIcon; -------------------------------------------------------------------------------- /src/components/main/MainCategory.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useDispatch } from 'react-redux'; 3 | import { setActiveCategory } from '../../redux/slices/categoriesSlice'; 4 | import parse from "html-react-parser" 5 | 6 | function MainCategory({categoriesData, activeCategory}) { 7 | const dispatch = useDispatch() 8 | const handleCategoryChange = (name) => { 9 | dispatch(setActiveCategory(name)) 10 | } 11 | return ( 12 |
13 | { 14 | categoriesData.map(item => { 15 | return ( 16 | 22 | ) 23 | }) 24 | } 25 |
26 | ) 27 | } 28 | 29 | export default MainCategory; -------------------------------------------------------------------------------- /src/utils/routes.jsx: -------------------------------------------------------------------------------- 1 | import CategoriesPage from "../pages/categories"; 2 | import Favourites from "../pages/favourite"; 3 | import Help from "../pages/help"; 4 | import HomePage from "../pages/home/index"; 5 | import LoginPage from "../pages/login"; 6 | import ProductsPage from "../pages/products"; 7 | import Settings from "../pages/settings/index"; 8 | 9 | export const routes = [ 10 | { 11 | id: 1, 12 | path: "/", 13 | component: , 14 | }, 15 | { 16 | id: 2, 17 | path: "/favourites", 18 | component: , 19 | }, 20 | { 21 | id: 3, 22 | path: "/settings", 23 | component: , 24 | }, 25 | { 26 | id: 4, 27 | path: "/help", 28 | component: , 29 | }, 30 | { 31 | id: 5, 32 | path: "/login", 33 | component: , 34 | }, 35 | ]; 36 | 37 | export const adminRoutes = [ 38 | { 39 | id: 1, 40 | path: "/products", 41 | component: , 42 | }, 43 | { 44 | id: 2, 45 | path: "/categories", 46 | component: , 47 | }, 48 | ]; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@reduxjs/toolkit": "^1.9.5", 14 | "axios": "^1.4.0", 15 | "d": "^1.0.1", 16 | "html-react-parser": "^4.0.0", 17 | "node-sass": "^9.0.0", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-loading-skeleton": "^3.3.1", 21 | "react-redux": "^8.1.1", 22 | "react-router-dom": "^6.4.1", 23 | "redux": "^4.2.1", 24 | "sass": "^1.63.3" 25 | }, 26 | "devDependencies": { 27 | "@types/react": "^18.0.37", 28 | "@types/react-dom": "^18.0.11", 29 | "@vitejs/plugin-react": "^4.0.0", 30 | "eslint": "^8.38.0", 31 | "eslint-plugin-react": "^7.32.2", 32 | "eslint-plugin-react-hooks": "^4.6.0", 33 | "eslint-plugin-react-refresh": "^0.3.4", 34 | "vite": "^4.3.9" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/helpers/settings.js: -------------------------------------------------------------------------------- 1 | export const settings = [ 2 | { 3 | id: 1, 4 | title: "Ism Familiya", 5 | description: "Bu yerga kiritgan ma'lumotlaringiz buyurtma berishda ishlatilinadi", 6 | text: "Ism Familiyangiz", 7 | key: "name" 8 | }, 9 | { 10 | id: 2, 11 | title: "Yashash manzil", 12 | description: "Bu yerga kiritgan ma'lumotlaringiz buyurtma berishda ishlatilinadi", 13 | text: "Yashash manzilingiz", 14 | key: "address" 15 | }, 16 | { 17 | id: 3, 18 | title: "Telefon raqam", 19 | description: "Bu yerga kiritgan ma'lumotlaringiz buyurtma berishda ishlatilinadi", 20 | text: "Telefon raqamingiz", 21 | key: "phone" 22 | } 23 | ] 24 | 25 | export const settings_color = [ 26 | { 27 | id: 1, 28 | color: "red" 29 | }, 30 | { 31 | id: 2, 32 | color: "yellow" 33 | }, 34 | { 35 | id: 3, 36 | color: "blue" 37 | }, 38 | { 39 | id: 4, 40 | color: "pink" 41 | }, 42 | ] -------------------------------------------------------------------------------- /src/assets/images/icons/Help.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | const HelpIcon = (props) => ( 3 | 10 | 14 | 15 | ) 16 | export default HelpIcon; 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Amir Yusupov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/admin/PageHeader.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { signOut } from "../../redux/slices/authSlice"; 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | function PageHeader({ title, children }) { 7 | const [close, setClose] = useState(0) 8 | const dispatch = useDispatch() 9 | const navigate = useNavigate() 10 | const handleSignOut = (e) => { 11 | dispatch(signOut(e)) 12 | navigate("/") 13 | return window.location.reload() 14 | } 15 | return ( 16 |
17 |
18 | 19 |
20 | Sign out 21 |
22 |
23 |
24 |
25 | {title} 26 |
{children}
27 |
28 |
29 |
30 | ); 31 | } 32 | 33 | export default PageHeader; 34 | -------------------------------------------------------------------------------- /src/helpers/sidebar_icons.js: -------------------------------------------------------------------------------- 1 | import HomeIcon from "../assets/images/icons/HomeIcon" 2 | import SettingsIcon from "../assets/images/icons/Settings" 3 | import HeartIcon from "../assets/images/icons/Heart" 4 | import HelpIcon from "../assets/images/icons/Help" 5 | import ProductIcon from "../assets/images/icons/ProductsIcon" 6 | import CategoryIcon from "../assets/images/icons/CategoryIcon" 7 | 8 | export const sidebar__icons = [ 9 | { 10 | id: 1, 11 | image: HomeIcon(), 12 | text: "Inicio", 13 | link: "/" 14 | }, 15 | { 16 | id: 2, 17 | image: HeartIcon(), 18 | text: "Favoritos", 19 | link: "/favourites" 20 | }, 21 | { 22 | id: 3, 23 | image: SettingsIcon(), 24 | text: "Preferencias", 25 | link: "/settings" 26 | }, 27 | { 28 | id: 4, 29 | image: HelpIcon(), 30 | text: "Resportar", 31 | link: "/help" 32 | }, 33 | ] 34 | 35 | export const sidebarAdminData = [ 36 | { 37 | id: 1, 38 | image: ProductIcon(), 39 | text: "Products", 40 | link: "/products", 41 | }, 42 | { 43 | id: 2, 44 | image: CategoryIcon(), 45 | text: "Category", 46 | link: "/categories", 47 | } 48 | ] -------------------------------------------------------------------------------- /src/assets/scss/admin/table.scss: -------------------------------------------------------------------------------- 1 | .admin-actions { 2 | @include flex(); 3 | gap: 10px; 4 | .admin-btn { 5 | @include font(16, 16, 500, normal); 6 | border: none; 7 | padding: 7px; 8 | border-radius: 5px; 9 | color: var(--white-color); 10 | background-color: var(--violet-color); 11 | transition: all 0.2s; 12 | &:hover { 13 | background-color: var(--violet-hover); 14 | } 15 | } 16 | } 17 | .table { 18 | width: 100%; 19 | @include flex(center, space-between, column); 20 | thead { 21 | width: 100%; 22 | padding: 15px; 23 | color: var(--white-color); 24 | background-color: var(--violet-color); 25 | tr { 26 | @include flex(center, space-around, row); 27 | flex: 1; 28 | th { 29 | @include font(16, 16, 600, normal); 30 | font-family: var(--main-font); 31 | } 32 | } 33 | } 34 | tbody { 35 | width: 100%; 36 | tr { 37 | width: 100%; 38 | padding: 17px; 39 | border-bottom: 1px solid var(--gray-plus-color); 40 | @include flex(center, space-around, row); 41 | &:nth-child(even) { 42 | background-color: #f2f2f2; 43 | } 44 | td { 45 | flex: 1; 46 | @include font(16, 16, 500, normal); 47 | font-family: var(--main-font); 48 | text-align: center; 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes, useLocation } from "react-router-dom"; 2 | import Sidebar from "../src/components/sidebar/index" 3 | import { adminRoutes, routes } from "./utils/routes"; 4 | import { SkeletonTheme } from "react-loading-skeleton"; 5 | import Basket from "./components/basket"; 6 | import { useSelector } from "react-redux"; 7 | import "./assets/scss/main.scss" 8 | import useIsAuth from "./hooks/useIsAuth"; 9 | 10 | function App() { 11 | const { color } = useSelector((state) => state.color) 12 | const { isAuth: auth } = useSelector((state) => state.auth) 13 | const { pathname } = useLocation() 14 | const isCart = ["/", "/favourites"] 15 | const isAuth = useIsAuth() 16 | return ( 17 |
18 | 19 | 20 | 21 | {auth 22 | ? adminRoutes.map((item) => ( 23 | 24 | )) : routes.map((item) => ( 25 | 26 | )) 27 | } 28 | 29 | {!auth && isCart.includes(pathname) ? : null} 30 | 31 |
32 | ) 33 | } 34 | 35 | export default App -------------------------------------------------------------------------------- /src/components/main/product.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useDispatch } from 'react-redux'; 3 | import { setCart } from '../../redux/slices/cartSlice'; 4 | import HeartIcon from "../../assets/images/icons/Heart" 5 | function Products({ product }) { 6 | const dispatch = useDispatch() 7 | const handleCartBtn = (product) => { 8 | dispatch(setCart({ product })) 9 | } 10 | const [like, setLike] = useState(0) 11 | return ( 12 |
13 | { 14 | product.map(item => { 15 | return ( 16 |
17 | 18 | product__img 19 |
20 |
21 | {item.name} 22 | $ {item.price}.00 23 |
24 | 25 |
26 |
27 | ) 28 | }) 29 | } 30 |
31 | ) 32 | } 33 | 34 | export default Products; -------------------------------------------------------------------------------- /src/assets/scss/toggle_btn.scss: -------------------------------------------------------------------------------- 1 | .switch { 2 | position: relative; 3 | display: inline-block; 4 | width: 60px; 5 | height: 34px; 6 | input { 7 | opacity: 0; 8 | width: 0; 9 | height: 0; 10 | &:checked + .slider { 11 | background-color: #2196F3; 12 | } 13 | &:focus + .slider { 14 | box-shadow: 0 0 1px #2196F3; 15 | } 16 | &:checked + .slider:before { 17 | -webkit-transform: translateX(26px); 18 | -ms-transform: translateX(26px); 19 | transform: translateX(26px); 20 | } 21 | } 22 | .slider { 23 | position: absolute; 24 | cursor: pointer; 25 | top: 0; 26 | left: 0; 27 | right: 0; 28 | bottom: 0; 29 | background-color: #ccc; 30 | -webkit-transition: .4s; 31 | transition: .4s; 32 | &.round { 33 | border-radius: 34px; 34 | &:before { 35 | border-radius: 50%; 36 | } 37 | } 38 | &:before { 39 | position: absolute; 40 | content: ""; 41 | height: 26px; 42 | width: 26px; 43 | left: 4px; 44 | bottom: 4px; 45 | background-color: white; 46 | -webkit-transition: .4s; 47 | transition: .4s; 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/components/basket/Order.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import RemoveIcon from '../../assets/images/icons/Remove'; 3 | import { settings } from '../../helpers/settings'; 4 | import { useSelector } from 'react-redux'; 5 | 6 | function Order() { 7 | const {user} = useSelector((state) => state) 8 | return ( 9 |
10 |
11 |
12 |
13 |
14 | Checkout 15 | Confirme su pedido 16 |
17 | 18 |
19 |
20 | { 21 | settings.map((item) => { 22 | return ( 23 |
24 | {item.title}* 25 | {user[item.key]} 26 |
27 | ) 28 | }) 29 | } 30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 | ) 38 | } 39 | 40 | export default Order; -------------------------------------------------------------------------------- /src/assets/images/icons/burger_menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/icons/burger_menu2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/icons/WatermelonCategory.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | const WatermelonIcon = (props) => ( 3 | 10 | 14 | 15 | ) 16 | export default WatermelonIcon -------------------------------------------------------------------------------- /src/assets/scss/admin/pageHeader.scss: -------------------------------------------------------------------------------- 1 | .pageHeader { 2 | width: 100%; 3 | @include flex(flex-end, center, column); 4 | .pageHeader-profile { 5 | position: relative; 6 | @include flex(center, center, column); 7 | &__text { 8 | @include font(20, 18, 500, normal); 9 | border: none; 10 | padding: 10px 18px; 11 | border-radius: 50px; 12 | color: var(--white-color); 13 | background-color: var(--violet-color); 14 | transition: all 0.2s; 15 | &:hover { 16 | background-color: var(--violet-hover); 17 | } 18 | } 19 | &__dropdown { 20 | display: none; 21 | &.active { 22 | position: absolute; 23 | @include flex(center, center, row); 24 | top: 100%; 25 | right: 0; 26 | cursor: pointer; 27 | padding: 10px 10px; 28 | border-radius: 5px; 29 | background-color: var(--gray-border-color); 30 | span { 31 | @include font(16, 16, 500, normal); 32 | color: var(--black-color); 33 | text-wrap: nowrap; 34 | } 35 | } 36 | } 37 | margin-bottom: 20px; 38 | } 39 | .pageHeader-wrapper { 40 | width: 100%; 41 | background-color: var(--white-color); 42 | border-radius: 12px; 43 | padding: 12px; 44 | margin-bottom: 20px; 45 | box-shadow: 0 3px 10px 3px rgba(0, 0, 0, 0.15); 46 | .pageHeader-row { 47 | @include flex(center, space-between, row); 48 | } 49 | &-title { 50 | @include font(18, 18, 500, normal); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/assets/images/icons/question_mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/scss/admin/login.scss: -------------------------------------------------------------------------------- 1 | .login-page { 2 | width: 100%; 3 | @include flex(center, center, column); 4 | height: 100vh; 5 | background-color: var(--sidebar-bg); 6 | 7 | .login-form { 8 | background-color: var(--white-color); 9 | padding: 40px; 10 | border-radius: 4px; 11 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 12 | width: 400px; 13 | 14 | &__title { 15 | @include font(24, 24, 600, normal); 16 | text-align: center; 17 | font-family: var(--main-font); 18 | margin-bottom: 30px; 19 | } 20 | 21 | &__form { 22 | margin-bottom: 20px; 23 | } 24 | 25 | &__group { 26 | margin-bottom: 20px; 27 | } 28 | 29 | &__label { 30 | @include font(16, 16, 500, normal); 31 | font-family: var(--main-font); 32 | display: block; 33 | margin-bottom: 8px; 34 | } 35 | 36 | &__input { 37 | width: 100%; 38 | padding: 10px; 39 | border: 1px solid #ccc; 40 | border-radius: 4px; 41 | transition: border-color 0.3s ease-in-out; 42 | } 43 | 44 | &__input:focus { 45 | border-color: var(--violet-color); 46 | } 47 | 48 | &__button { 49 | @include font(16, 16, 500, normal); 50 | color: white; 51 | padding: 12px 20px; 52 | border: none; 53 | border-radius: 4px; 54 | background-color: var(--violet-color); 55 | transition: background-color 0.3s ease-in-out; 56 | 57 | &:hover { 58 | background-color: var(--violet-hover); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/helpers/products.js: -------------------------------------------------------------------------------- 1 | import product1 from "../assets/images/imgs/product1.jpeg" 2 | import product2 from "../assets/images/imgs/product2.jpeg" 3 | import product3 from "../assets/images/imgs/product3.jpeg" 4 | import product4 from "../assets/images/imgs/product4.jpeg" 5 | 6 | export let products = [ 7 | { 8 | id: 1, 9 | name: "Pizza muzzarella", 10 | price: 1200, 11 | img: product1, 12 | category: "Pizzas" 13 | }, 14 | { 15 | id: 2, 16 | name: "Pizza pepperoni", 17 | price: 1100, 18 | img: product2, 19 | category: "Pizzas" 20 | }, 21 | { 22 | id: 3, 23 | name: "Pizza napolitana", 24 | price: 1050, 25 | img: product3, 26 | category: "Empanadas" 27 | }, 28 | { 29 | id: 4, 30 | name: "Pizza jamón y morrón", 31 | price: 9400, 32 | img: product4, 33 | category: "Empanadas" 34 | }, 35 | { 36 | id: 5, 37 | name: "Pizza jamón y morrón", 38 | price: 1900, 39 | img: product4, 40 | category: "Bebidas" 41 | }, 42 | { 43 | id: 6, 44 | name: "Pizza jamón y morrón", 45 | price: 9200, 46 | img: product4, 47 | category: "Bebidas" 48 | }, 49 | { 50 | id: 7, 51 | name: "Pizza jamón y morrón", 52 | price: 9300, 53 | img: product4, 54 | category: "Postres" 55 | }, 56 | { 57 | id: 8, 58 | name: "Pizza jamón y morrón", 59 | price: 9040, 60 | img: product4, 61 | category: "Postres" 62 | }, 63 | ] -------------------------------------------------------------------------------- /src/redux/slices/authSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { loginRequest } from "../actions/loginAction"; 3 | 4 | const accessToken = localStorage.getItem("accessToken"); 5 | const refreshToken = localStorage.getItem("refreshToken"); 6 | const isAuth = localStorage.getItem("auth"); 7 | const initialState = { 8 | tokens: { 9 | accessToken: accessToken ? accessToken : "", 10 | refreshToken: refreshToken ? refreshToken : "" 11 | }, 12 | loading: false, 13 | isAuth: isAuth ? isAuth : "" 14 | } 15 | const authSlice = createSlice({ 16 | name: "auth", 17 | initialState, 18 | reducers: { 19 | setAuth: (state, action) => { 20 | state[action.payload.key] = action.payload.value; 21 | }, 22 | setIsAuth: (state, action) => { 23 | state.isAuth = action.payload 24 | }, 25 | signOut: (state, action) => { 26 | state = action.payload; 27 | localStorage.clear(); 28 | } 29 | }, 30 | extraReducers: { 31 | [loginRequest.pending.type]: (state, action) => { 32 | state.loading = true 33 | }, 34 | [loginRequest.fulfilled.type]: (state, action) => { 35 | state.tokens = { 36 | accessToken: action.payload.access, 37 | refreshToken: action.payload.refresh 38 | } 39 | state.loading = false 40 | state.isAuth = true 41 | localStorage.setItem("accessToken", action.payload.access) 42 | localStorage.setItem("refreshToken", action.payload.refresh) 43 | if(action.payload.access !== undefined) { 44 | localStorage.setItem("auth", state.isAuth) 45 | } 46 | } 47 | } 48 | }); 49 | export const { setAuth, setIsAuth, signOut } = authSlice.actions; 50 | export default authSlice.reducer; 51 | -------------------------------------------------------------------------------- /src/assets/images/icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/sidebar/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import burger__menu from "../../assets/images/icons/burger_menu.svg" 3 | import burger__menu2 from "../../assets/images/icons/burger_menu2.svg" 4 | import sidebar_logo_icon from "../../assets/images/icons/sidebar__logo-icon.svg" 5 | import sidebar_logo_text from "../../assets/images/icons/sidebar__logo-text.svg" 6 | import SidebarItem from "./SidebarItem"; 7 | import { Link, useLocation } from "react-router-dom"; 8 | import { useSelector } from "react-redux"; 9 | import {sidebarAdminData, sidebar__icons} from "../../helpers/sidebar_icons" 10 | const Sidebar = () => { 11 | const [active, setActive] = useState(0) 12 | const {isAuth} = useSelector((state) => state.auth) 13 | const {pathname} = useLocation() 14 | if(pathname === "/login") { 15 | return null 16 | } 17 | return ( 18 |
19 |
20 | 23 |
24 | 25 | sidebar__logo 26 | sidebar__logo 27 | 28 | 31 |
32 | 33 |
34 |
35 | ) 36 | }; 37 | export default Sidebar; -------------------------------------------------------------------------------- /src/pages/login/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { loginRequest } from '../../redux/actions/loginAction' 4 | 5 | function LoginPage() { 6 | const initialState = { 7 | email: "", 8 | password: "" 9 | } 10 | const [formState, setFormState] = useState(initialState) 11 | const dispatch = useDispatch() 12 | const {loading} = useSelector((state) => state.auth) 13 | const onInputChange = ({target}) => { 14 | const {name, value} = target 15 | setFormState({...formState, [name]: value}) 16 | } 17 | const onSubmit = () => { 18 | dispatch(loginRequest(formState)) 19 | } 20 | return ( 21 |
22 |
23 |

Login

24 |
25 |
26 | 27 | 35 |
36 |
37 | 38 | 46 |
47 | 48 |
49 |
50 |
51 | ); 52 | } 53 | 54 | export default LoginPage; -------------------------------------------------------------------------------- /src/assets/scss/order.scss: -------------------------------------------------------------------------------- 1 | .order { 2 | width: 100%; 3 | height: 90vh; 4 | &__column { 5 | height: 100%; 6 | @include flex(flex-start, space-between, column); 7 | .order__detail { 8 | width: 100%; 9 | .order__checkout { 10 | width: 100%; 11 | @include flex(flex-start, space-between, row); 12 | margin-bottom: 20px; 13 | &__text { 14 | @include flex(flex-start, center, column); 15 | .order__checkout__title { 16 | @include font(20, 20, 600, normal); 17 | color: var(--black-color); 18 | } 19 | .order__checkout__text { 20 | @include font(16, 16, 500, normal); 21 | color: var(--order-bg-color); 22 | } 23 | } 24 | svg { 25 | cursor: pointer; 26 | } 27 | } 28 | .order__data { 29 | width: 100%; 30 | @include flex(flex-start, center, column); 31 | &__item { 32 | @include flex(flex-start, center, column); 33 | margin-bottom: 15px; 34 | &-title { 35 | @include font(16, 16, 500, normal); 36 | color: var(--gray-btn-color); 37 | } 38 | &-text { 39 | @include font(18, 18, 600, normal); 40 | color: var(--black-color); 41 | } 42 | } 43 | } 44 | } 45 | .order__confirm { 46 | width: 100%; 47 | @include flex(center, center, unset); 48 | &-btn { 49 | width: 100%; 50 | height: 60px; 51 | border-radius: 50px; 52 | margin-top: 20px; 53 | border: none; 54 | cursor: pointer; 55 | color: var(--white-color); 56 | background-color: var(--order-bg-color); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/components/basket/product.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RemoveIcon from "../../assets/images/icons/Remove.jsx"; 3 | import { useDispatch } from "react-redux"; 4 | import { decreaseQty, deleteCart, increaseQty } from "../../redux/slices/cartSlice.js"; 5 | 6 | function Product({ products }) { 7 | const dispatch = useDispatch() 8 | const handleCartItemDel = (id) => { 9 | dispatch(deleteCart({ id })); 10 | }; 11 | const handleIncrease = (id) => { 12 | dispatch(increaseQty({id})) 13 | } 14 | const handleDecrease = (id) => { 15 | dispatch(decreaseQty({id})) 16 | } 17 | 18 | return ( 19 |
20 | {products.items.map((item) => { 21 | return ( 22 |
23 | basket__product 28 |
29 |
30 | {item.name} 31 | 32 | $ {item.price * item.qty} 33 | 34 |
35 |
36 | 37 | {item.qty} 38 | 39 |
40 |
41 |
handleCartItemDel(item.id)}> 42 | 43 |
44 |
45 | ); 46 | })} 47 |
48 | ); 49 | } 50 | 51 | export default Product; 52 | -------------------------------------------------------------------------------- /src/pages/settings/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SettingsData from "../../components/settings/SettingsData"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { settings_color } from "../../helpers/settings"; 5 | import { setColor } from "../../redux/slices/colorSlice"; 6 | function Settings() { 7 | const dispatch = useDispatch(); 8 | const { color } = useSelector((state) => state); 9 | function colorHandler(color) { 10 | dispatch(setColor(color)) 11 | } 12 | return ( 13 |
14 |
15 |
16 | Preferencias 17 | 18 | Configura la página, tema oscuro/claro y automatiza el checkout 19 | 20 |
21 | 22 |
23 | Tema oscuro 24 | 25 | Alterna entre el tema claro y oscuro, así puedes cuidar tu vista 26 | 27 |
28 | 32 |
33 |
34 | { 35 | settings_color.map((item) => { 36 | return ( 37 | 38 | ) 39 | }) 40 | } 41 |
42 |
43 |
44 |
45 | ); 46 | } 47 | 48 | export default Settings; 49 | -------------------------------------------------------------------------------- /src/components/settings/SettingsData.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { settings } from "../../helpers/settings"; 3 | import { Input } from "../form/Input"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { setUser } from "../../redux/slices/userSlice"; 6 | 7 | const initialActiveInput = { 8 | name: false, 9 | address: false, 10 | phone: false, 11 | }; 12 | function SettingsData() { 13 | const [activeInput, setActiveInput] = useState(initialActiveInput); 14 | const { user } = useSelector((state) => state); 15 | const dispatch = useDispatch(); 16 | const handleActiveInput = (key) => { 17 | setActiveInput({ 18 | ...activeInput, 19 | [key]: !activeInput[key], 20 | }); 21 | }; 22 | const blurHandler = () => { 23 | setActiveInput(initialActiveInput); 24 | }; 25 | const handleChangeInput = (e, key) => { 26 | dispatch(setUser({ key, value: e.target.value })); 27 | }; 28 | return ( 29 |
30 | {settings.map((item) => { 31 | return ( 32 |
33 | {item.title} 34 | 35 | {item.description} 36 | 37 | {activeInput[item.key] ? ( 38 | handleChangeInput(e, item.key)} 43 | /> 44 | ) : ( 45 | handleActiveInput(item.key)} 48 | > 49 | {user[item.key] ? user[item.key] : item.text} 50 | 51 | )} 52 |
53 | ); 54 | })} 55 |
56 | ); 57 | } 58 | 59 | export default SettingsData; 60 | -------------------------------------------------------------------------------- /src/redux/slices/cartSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit" 2 | 3 | const localeStorage = JSON.parse(localStorage.getItem("cart")) 4 | const initialState = { 5 | items: localeStorage ? localeStorage : [] 6 | } 7 | 8 | const cartSlice = createSlice({ 9 | name: "cart", 10 | initialState, 11 | reducers: { 12 | setCart: (state, {payload}) => { 13 | let isCart = state.items.some((item) => item.id === payload.product.id) 14 | state.items = isCart ? state.items.map((item) => 15 | item.id === payload.product.id 16 | ? { ...item, qty: item.qty + 1 } 17 | : item 18 | ) 19 | : [...state.items, { ...payload.product, qty: 1 }]; 20 | localStorage.setItem("cart", JSON.stringify(state.items)) 21 | }, 22 | deleteCart: (state, {payload}) => { 23 | state.items = state.items.filter((item) => item.id !== payload.id); 24 | localStorage.setItem("cart", JSON.stringify(state.items)) 25 | }, 26 | deleteAllCarts: (state, {payload}) => { 27 | state.items = state.items.filter((item) => item === payload); 28 | localStorage.setItem("cart", JSON.stringify(state.items)) 29 | }, 30 | increaseQty: (state, {payload}) => { 31 | state.items = state.items.map((item) => 32 | item.id === payload.id ? { ...item, qty: item.qty + 1 } : item 33 | ); 34 | localStorage.setItem("cart", JSON.stringify(state.items)) 35 | }, 36 | decreaseQty: (state, {payload}) => { 37 | state.items = state.items.map((item) => 38 | item.id === payload.id 39 | ? {...item, qty: item.qty === 1 ? 1 : item.qty - 1} 40 | : item 41 | ); 42 | localStorage.setItem("cart", JSON.stringify(state.items)) 43 | } 44 | } 45 | }) 46 | 47 | export const {setCart, deleteCart, deleteAllCarts, increaseQty, decreaseQty} = cartSlice.actions 48 | export default cartSlice.reducer -------------------------------------------------------------------------------- /src/components/main/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import search from "../../assets/images/icons/search.svg"; 3 | import Products from "./product"; 4 | import MainCategory from "./MainCategory"; 5 | import burger__menu from "../../assets/images/icons/burger_menu.svg"; 6 | import { useDispatch, useSelector } from "react-redux"; 7 | import { getCategories } from "../../redux/actions/categoriesAction"; 8 | import CategorySkeleton from "../skeleton/CategorySkeleton"; 9 | import ProductSkeleton from "../skeleton/ProductSkeleton"; 10 | import { getProducts } from "../../redux/actions/productsActions"; 11 | 12 | function Main() { 13 | const { categories, products } = useSelector((state) => state); 14 | const dispatch = useDispatch() 15 | useEffect(() => { 16 | dispatch(getCategories()) 17 | dispatch(getProducts()) 18 | }, []) 19 | return ( 20 |
21 |
22 |
23 | search__img 24 | 25 |
26 |
27 | 28 | search__img 29 |
30 |
31 | Categorias 32 | 33 | Elige nuestras deliciosas pizzas 34 | 35 |
36 | {categories.loading === true && } 37 | 41 | {products.loading === true && } 42 | item.category === categories.activeCategory 45 | )} 46 | /> 47 |
48 |
49 | ); 50 | } 51 | 52 | export default Main; 53 | -------------------------------------------------------------------------------- /src/components/basket/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import del_img from "../../assets/images/icons/delete.svg"; 3 | import Product from "./product"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { deleteAllCarts } from "../../redux/slices/cartSlice"; 6 | import Order from "./Order"; 7 | 8 | function Basket() { 9 | const { cart } = useSelector((state) => state) 10 | const dispatch = useDispatch() 11 | const productPrice = cart.items.map(item => item.price * item.qty) 12 | const totalPrice = () => { 13 | const newProductPrices = [...productPrice]; 14 | const newTotal = newProductPrices.reduce((sum, price) => sum + price, 0); 15 | return newTotal 16 | }; 17 | const handleAllCartsDel = (items) => { 18 | dispatch(deleteAllCarts({items})) 19 | } 20 | const [isCheckout, setIsCheckout] = useState(false) 21 | return ( 22 |
23 |
24 | Mi orden 25 | { 26 | isCheckout ? : ( 27 |
28 |
29 |
30 | 31 | Listado del pedido 32 | 33 | {cart.items?.length} Items 34 |
35 | handleAllCartsDel(cart)} 38 | className="basket__products-desc-img" 39 | alt="delete__img" 40 | /> 41 |
42 | 43 |
44 | )} 45 |
46 |
47 |
48 | Envío 49 | $ 100.00 50 |
51 |
52 | Items totales 53 | $ {`${totalPrice()}`}.00 54 |
55 |
56 | 57 |
58 |
59 |
60 | ); 61 | } 62 | 63 | export default Basket; 64 | -------------------------------------------------------------------------------- /src/assets/scss/settings.scss: -------------------------------------------------------------------------------- 1 | .settings { 2 | width: 92%; 3 | height: 100vh; 4 | padding: 20px 0 0 45px; 5 | &__row { 6 | width: 100%; 7 | @include flex(flex-start, flex-start, column); 8 | .settings__title { 9 | @include flex(flex-start, center, column); 10 | &-text { 11 | @include font(26, 26, 700, normal); 12 | margin-bottom: 5px; 13 | color: var(--black-color); 14 | } 15 | &-desc { 16 | @include font(16, 16, 600, normal); 17 | color: var(--light-gray); 18 | } 19 | } 20 | .settings__data { 21 | @include flex(flex-start, center, column); 22 | margin-top: 40px; 23 | &__item { 24 | @include flex(flex-start, center, column); 25 | margin-bottom: 30px; 26 | .form-group { 27 | background-color: var(--sidebar-bg); 28 | color: var(--light-gray); 29 | input { 30 | font-size: 16px; 31 | border: none; 32 | padding: 10px 15px; 33 | background-color: var(--sidebar-bg); 34 | &:focus { 35 | outline: none 36 | } 37 | } 38 | } 39 | &-title, &-text { 40 | @include font(18, 18, 600, normal); 41 | color: var(--black-color) 42 | } 43 | &-desc { 44 | @include font(16, 16, 500, normal); 45 | color: var(--light-gray); 46 | margin: 5px 0 15px; 47 | } 48 | } 49 | } 50 | .settings__theme { 51 | @include flex(flex-start, center, column); 52 | &-title { 53 | @include font(18, 18, 600, normal); 54 | color: var(--black-color); 55 | } 56 | &-desc { 57 | @include font(16, 16, 500, normal); 58 | color: var(--light-gray); 59 | margin: 5px 0 15px 0; 60 | } 61 | &__switch { 62 | @import "./toggle_btn.scss"; 63 | } 64 | } 65 | .settings__colors { 66 | width: 100%; 67 | @include flex(center, flex-start, row); 68 | gap: 10px; 69 | margin-top: 15px; 70 | .settings__color { 71 | padding: 15px; 72 | border-radius: 15px; 73 | border: 1px solid transparent; 74 | &.red { 75 | background-color: #FF4E4E; 76 | } 77 | &.yellow { 78 | background-color: #FFA73F; 79 | } 80 | &.blue { 81 | background-color: #50C2E7; 82 | } 83 | &.pink { 84 | background-color: #FF678C; 85 | } 86 | &.active { 87 | border: 1px solid var(--black-color); 88 | } 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/assets/images/icons/Settings.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | const SettingsIcon = (props) => ( 3 | 11 | 15 | 16 | ) 17 | export default SettingsIcon -------------------------------------------------------------------------------- /src/pages/categories/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { addCategory, editCategory, getCategories } from "../../redux/actions/categoriesAction"; 4 | import PageHeader from "../../components/admin/PageHeader" 5 | import TableList from "../../components/admin/TableList" 6 | import Drawer from '../../components/admin/Drawer'; 7 | import { Input } from '../../components/form/Input'; 8 | function CategoriesPage() { 9 | const { items, loading } = useSelector((state) => state.categories) 10 | const [modalOpen, setModalOpen] = useState(false) 11 | const [isEdit, setIsEdit] = useState(null) 12 | const [formLoading, setFormLoading] = useState(false) 13 | 14 | const dispatch = useDispatch() 15 | useEffect(() => { 16 | dispatch(getCategories()) 17 | }, []) 18 | const form = useRef() 19 | const setValues = (data) => { 20 | for (let item in data) { 21 | const { current: { elements } } = form 22 | if (elements[item]) { 23 | elements[item].value = data[item] 24 | } 25 | } 26 | } 27 | const resetForm = () => { 28 | form.current.reset() 29 | } 30 | const handleSubmit = async (e) => { 31 | e.preventDefault() 32 | const { target } = e 33 | const obj = {} 34 | for (let i = 0; i < target.elements.length - 1; i++) { 35 | obj[target.elements[i].name] = target.elements[i].value 36 | console.log(obj); 37 | } 38 | setFormLoading(true) 39 | const response = isEdit ? await editCategory(obj, isEdit) : await addCategory(obj) 40 | console.log(response); 41 | if (response.id) { 42 | handleModalClose() 43 | dispatch(getCategories()) 44 | setFormLoading(false) 45 | } 46 | } 47 | const handleEditBtn = (el) => { 48 | setValues(el) 49 | handleModalOpen() 50 | setIsEdit(el.id) 51 | } 52 | const tableColumns = [ 53 | { 54 | title: "Name", 55 | dataKey: "name" 56 | }, 57 | { 58 | title: "Actions", 59 | render: (el) => { 60 | return ( 61 |
62 | 63 | 64 |
65 | ) 66 | } 67 | } 68 | ] 69 | const handleModalOpen = () => { 70 | setModalOpen(true) 71 | } 72 | const handleModalClose = () => { 73 | setModalOpen(false) 74 | resetForm() 75 | setIsEdit(null) 76 | } 77 | return ( 78 |
79 |
80 | 82 | 83 | 84 |
85 | } /> 86 | 87 | 88 |
89 |
90 | 91 | 92 |
93 | 94 |
95 |
96 |
97 | 98 | ) 99 | } 100 | 101 | export default CategoriesPage; -------------------------------------------------------------------------------- /src/assets/scss/sidebar.scss: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | width: 8%; 3 | height: 100vh; 4 | position: sticky; 5 | flex-shrink: 0; 6 | top: 0; 7 | left: 0; 8 | float: right; 9 | background-color: var(--sidebar-bg); 10 | transition: all 0.3s; 11 | &.active { 12 | width: 23%; 13 | .sidebar__col { 14 | @include flex(flex-start, space-between, column); 15 | padding-left: 31px; 16 | &-burger { 17 | display: none; 18 | } 19 | 20 | // Sidebar column logo mobile 21 | .sidebar__col-logo-mobile { 22 | @include flex(center, space-between, row); 23 | width: 100%; 24 | height: auto; 25 | margin-bottom: 50px; 26 | padding-right: 21px; 27 | .sidebar__col-logo-imgs { 28 | @include flex(center, center, row); 29 | img { 30 | &:first-child { 31 | margin-right: 12px; 32 | } 33 | } 34 | } 35 | .sidebar__col-burger-mobile { 36 | border: none; 37 | background-color: transparent; 38 | img { 39 | width: 40px; 40 | height: 40px; 41 | } 42 | } 43 | } 44 | // Sidebar icons column 45 | .sidebar__icons-col { 46 | @include flex(flex-start, center, column); 47 | .sidebar__icons-item { 48 | .sidebar__icons { 49 | @include flex(center, flex-start, row); 50 | &-text { 51 | @include font(16, 16, 700, 24px); 52 | display: block; 53 | color: var(--light-gray); 54 | margin-left: 43px; 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | @media screen and (max-width: 767px) { 62 | display: none; 63 | } 64 | &__col { 65 | @include flex(center, space-between, column); 66 | width: 100%; 67 | height: auto; 68 | padding-top: 50px; 69 | .sidebar__col-burger { 70 | border: none; 71 | cursor: pointer; 72 | margin-bottom: 50px; 73 | background-color: transparent; 74 | img { 75 | width: 40px; 76 | height: 40px; 77 | } 78 | } 79 | &-logo-mobile { 80 | display: none 81 | } 82 | .sidebar__icons-col { 83 | width: 100%; 84 | .sidebar__icons-item { 85 | width: 100%; 86 | position: relative; 87 | 88 | &.active { 89 | &::after { 90 | content: ''; 91 | position: absolute; 92 | top: 0; 93 | right: 0; 94 | width: 3px; 95 | height: 100%; 96 | border-radius: 10px; 97 | background-color: var(--main-color); 98 | } 99 | .sidebar__icons { 100 | svg path{ 101 | fill: var(--main-color); 102 | } 103 | .sidebar__icons-text { 104 | color: var(--main-color); 105 | } 106 | } 107 | } 108 | &:not(:last-child) { 109 | margin-bottom: 60px; 110 | } 111 | .sidebar__icons { 112 | @include flex(center, center, column); 113 | &-text { 114 | display: none; 115 | } 116 | @media screen and (max-width: 767px) { 117 | display: none; 118 | } 119 | } 120 | } 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /src/assets/scss/admin/drawer.scss: -------------------------------------------------------------------------------- 1 | .drawer { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | width: 100vw; 6 | height: 100vh; 7 | opacity: 0; 8 | visibility: hidden; 9 | overflow: hidden; 10 | z-index: 1; 11 | transition: all 0.3s ease-in-out; 12 | 13 | &.open { 14 | opacity: 1; 15 | visibility: visible; 16 | } 17 | 18 | &__backdrop { 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | width: 100%; 23 | height: 100%; 24 | backdrop-filter: blur(2px); 25 | background-color: rgba(0, 0, 0, 0.5); 26 | z-index: 3; 27 | } 28 | 29 | &__panel { 30 | position: absolute; 31 | right: -100%; 32 | top: 0; 33 | width: 700px; 34 | height: 100%; 35 | padding: 16px; 36 | z-index: 30; 37 | background-color: var(--white-color); 38 | transition: all 0.3s ease-in-out; 39 | 40 | .drawer__header { 41 | width: 100%; 42 | @include flex(center, space-between, row); 43 | margin-bottom: 20px; 44 | 45 | &-close { 46 | @include font(16, 16, 800, normal); 47 | border: none; 48 | padding: 5px 13px; 49 | border-radius: 15px; 50 | cursor: pointer; 51 | color: var(--white-color); 52 | background-color: var(--violet-color); 53 | transition: all 0.2s; 54 | 55 | &:hover { 56 | background-color: var(--violet-hover); 57 | } 58 | } 59 | } 60 | 61 | .drawer__body { 62 | width: 100%; 63 | 64 | form { 65 | width: 100%; 66 | @include flex(center, space-between, column); 67 | 68 | .drawer-form__group { 69 | width: 100%; 70 | @include flex(center, center, row); 71 | select { 72 | flex: 1 1 auto; 73 | width: 90%; 74 | text-overflow: ellipsis; 75 | color: rgba(0, 0, 0, 0.88); 76 | border: 1px solid #d9d9d9; 77 | border-radius: 6px; 78 | opacity: 0.8; 79 | padding: 6px; 80 | margin-bottom: 15px; 81 | background-color: var(--white-color); 82 | transition: all 0.2s; 83 | 84 | &:hover { 85 | border-color: #4096ff; 86 | border-inline-end-width: 1px; 87 | } 88 | 89 | &:focus { 90 | border-color: #4096ff; 91 | box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1); 92 | border-inline-end-width: 1px; 93 | outline: 0; 94 | } 95 | } 96 | .form-group { 97 | width: 100%; 98 | @include flex(flex-start, flex-start, column); 99 | 100 | label, 101 | input { 102 | @include font(14, 14, 400, normal); 103 | font-family: var(--main-font); 104 | } 105 | 106 | label { 107 | margin-bottom: 5px; 108 | } 109 | input, 110 | textarea, select { 111 | flex: 1 1 auto; 112 | width: 90%; 113 | text-overflow: ellipsis; 114 | color: rgba(0, 0, 0, 0.88); 115 | border: 1px solid #d9d9d9; 116 | border-radius: 6px; 117 | opacity: 0.8; 118 | padding: 6px; 119 | margin-bottom: 15px; 120 | background-color: var(--white-color); 121 | transition: all 0.2s; 122 | 123 | &:hover { 124 | border-color: #4096ff; 125 | border-inline-end-width: 1px; 126 | } 127 | 128 | &:focus { 129 | border-color: #4096ff; 130 | box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1); 131 | border-inline-end-width: 1px; 132 | outline: 0; 133 | } 134 | } 135 | 136 | &:not(:last-child) { 137 | input { 138 | margin-right: 20px; 139 | } 140 | } 141 | 142 | textarea { 143 | width: 95%; 144 | resize: none; 145 | font-family: var(--main-font); 146 | 147 | &::placeholder { 148 | font-family: var(--main-font); 149 | } 150 | } 151 | } 152 | 153 | } 154 | 155 | .admin-btn { 156 | @include font(16, 16, 500, normal); 157 | border: none; 158 | padding: 10px 30px; 159 | border-radius: 5px; 160 | cursor: pointer; 161 | color: var(--white-color); 162 | background-color: var(--violet-color); 163 | transition: all 0.2s; 164 | 165 | &:hover { 166 | background-color: var(--violet-hover); 167 | } 168 | } 169 | } 170 | } 171 | } 172 | 173 | &.open &__panel { 174 | right: 0; 175 | } 176 | } -------------------------------------------------------------------------------- /src/pages/products/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react' 2 | import PageHeader from '../../components/admin/PageHeader'; 3 | import TableList from '../../components/admin/TableList'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { addProduct, editProduct, getProducts } from '../../redux/actions/productsActions'; 6 | import Drawer from '../../components/admin/Drawer'; 7 | import { Input } from '../../components/form/Input'; 8 | import { getCategories } from '../../redux/actions/categoriesAction'; 9 | import SelectForm from '../../components/form/Select'; 10 | 11 | function ProductsPage() { 12 | const { items, loading } = useSelector((state) => state.products) 13 | const [modalOpen, setModalOpen] = useState(false) 14 | const [isEdit, setIsEdit] = useState(null) 15 | const { categories } = useSelector((state) => state) 16 | const [formLoading, setFormLoading] = useState(false) 17 | // const {items: productItems, loading: productLoading} = products 18 | const { items: categoryItems, loading: categoryLoading } = categories 19 | const dispatch = useDispatch() 20 | useEffect(() => { 21 | dispatch(getCategories()) 22 | dispatch(getProducts()) 23 | }, []) 24 | const tableColumns = [ 25 | { 26 | title: "Name", 27 | dataKey: "name" 28 | }, 29 | { 30 | title: "Price", 31 | dataKey: "price" 32 | }, 33 | { 34 | title: "Category", 35 | dataKey: "category" 36 | }, 37 | { 38 | title: "Actions", 39 | render: (el) => { 40 | return ( 41 |
42 | 43 | 44 |
45 | ) 46 | } 47 | } 48 | ] 49 | const form = useRef() 50 | const resetForm = () => { 51 | form.current.reset() 52 | } 53 | const handleModalOpen = () => { 54 | setModalOpen(true) 55 | } 56 | const handleModalClose = () => { 57 | setModalOpen(false) 58 | resetForm() 59 | setIsEdit(null) 60 | } 61 | const setValues = (data) => { 62 | for (let item in data) { 63 | const {current: {elements}} = form 64 | if(elements[item]) { 65 | elements[item].value = data[item] 66 | } 67 | } 68 | } 69 | const handleEditBtn = (el) => { 70 | setValues(el) 71 | handleModalOpen() 72 | setIsEdit(el.id) 73 | } 74 | const handleSubmit = async (e) => { 75 | e.preventDefault() 76 | const { target } = e 77 | const obj = {} 78 | for (let i = 0; i < target.elements.length-1; i++) { 79 | obj[target.elements[i].name] = target.elements[i].value 80 | } 81 | setFormLoading(true) 82 | const response = isEdit ? await editProduct(obj, isEdit) : await addProduct(obj) 83 | if(response.id) { 84 | handleModalClose() 85 | setFormLoading(false) 86 | dispatch(getProducts()) 87 | } 88 | } 89 | return ( 90 |
91 |
92 | 93 |
94 | 95 | 96 |
97 |
98 | 99 | 100 |
101 |
102 | 103 | 104 |
105 |
106 | 107 | 108 |
109 |
110 | 111 | ({ label: item.name, value: item.id }))} defaultValue={1} labelValue="category" labelName="Category" inputName="category" /> 112 |
113 |
114 |
115 | 116 |