├── 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 |
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 |
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 |
17 | )
18 | export default RemoveIcon
19 |
--------------------------------------------------------------------------------
/src/assets/images/icons/ProductsIcon.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | const ProductIcon = (props) => (
3 |
15 | )
16 | export default ProductIcon
--------------------------------------------------------------------------------
/src/assets/images/icons/PizzaCategory.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | const PizzaCategoryIcon = (props) => (
3 |
15 | )
16 | export default PizzaCategoryIcon
--------------------------------------------------------------------------------
/src/assets/images/icons/CategoryIcon.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | const CategoryIcon = (props) => (
3 |
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 |
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 | | {item.title} |
11 | )
12 | })}
13 |
14 |
15 | {
16 | loading ? "loading" :
17 | {data.map((dataItem, i) => {
18 | return (
19 |
20 | {columns.map((columnItem, k) => {
21 | return (
22 | | {Boolean(columnItem.dataKey) ? dataItem[columnItem.dataKey] : columnItem.render(dataItem)} |
23 | )
24 | })}
25 |
26 | )
27 | })}
28 |
29 | }
30 |
31 | )
32 | }
33 |
34 | export default TableList
--------------------------------------------------------------------------------
/src/assets/images/icons/DessertCategory.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | const DessertIcon = (props) => (
3 |
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 |
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 |
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 |
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 |

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 |
6 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/icons/burger_menu2.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/images/icons/WatermelonCategory.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | const WatermelonIcon = (props) => (
3 |
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 |
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 |
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 |

26 |

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

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 |

24 |
25 |
26 |
27 |

28 |

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 |
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 |
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 |
121 |
122 |
123 |
124 | )
125 | }
126 |
127 | export default ProductsPage;
--------------------------------------------------------------------------------
/src/assets/scss/basket.scss:
--------------------------------------------------------------------------------
1 | .basket {
2 | width: 30%;
3 | height: 100vh;
4 | background-color: var(--sidebar-bg);
5 | @media screen and (max-width: 1440px) {
6 | display: none;
7 | }
8 | &__row {
9 | @include flex(flex-start, flex-start, column);
10 | width: 100%;
11 | height: 100vh;
12 | padding: 33px 24px 21px 24px;
13 | @import "./order.scss";
14 | .basket__title {
15 | @include font(26, 26, 700, 30px);
16 | color: var(--black-color);
17 | margin-bottom: 35px
18 | }
19 | .basket__products {
20 | @include flex(center, center, column);
21 | width: 100%;
22 | &-desc {
23 | width: 100%;
24 | @include flex(center, space-between, row);
25 | &-text {
26 | @include flex(flex-start, center, column);
27 | margin-bottom: 35px;
28 | &1 {
29 | @include font(20, 20, 600, 30px);
30 | color: var(--black-color);
31 | }
32 | &2 {
33 | @include font(16, 16, 500, 24px);
34 | color: var(--gray-color);
35 | }
36 | }
37 | &-img {
38 | border: none;
39 | background-color: transparent;
40 | cursor: pointer;
41 | }
42 | }
43 | &-items {
44 | width: 100%;
45 | height: 100%;
46 | @include flex(flex-start, center, column);
47 | .basket__products-item {
48 | width: 100%;
49 | @include flex(flex-start, space-between, row);
50 | border-radius: 16px;
51 | background-color: var(--white-color);
52 | padding: 12px 18px 12px 12px;
53 | &:not(:last-child) {
54 | margin-bottom: 25px;
55 | }
56 | &-img {
57 | width: 110px;
58 | height: 110px;
59 | border-radius: 16px;
60 | object-fit: cover;
61 | }
62 | &-text {
63 | @include flex(flex-start, center, column);
64 | .basket__products-item-span {
65 | @include flex(flex-start, center, column);
66 | margin-bottom: 27px;
67 | .basket__products-item-name {
68 | @include font(16, 16, 500, 24px);
69 | color: var(--gray-color);
70 | }
71 | .basket__products-item-price {
72 | @include font(16, 16, 500, 24px);
73 | color: var(--gray-btn-color)
74 | }
75 | }
76 | .basket__products-item-quantity {
77 | @include flex(center, space-between, row);
78 | border-radius: 9px;
79 | background-color: var(--btn-bg-color);
80 | width: 80px;
81 | height: 35px;
82 | button {
83 | border: none;
84 | background-color: transparent;
85 | padding: 7px 10px;
86 | transition: all 0.3s;
87 | border-radius: 9px;
88 | &:hover {
89 | background-color: var(--gray-plus-color);
90 | }
91 | }
92 | }
93 | }
94 | &-remove {
95 | padding: 12px;
96 | cursor: pointer;
97 | border-radius: 50px;
98 | transition: all 0.3s;
99 | svg path {
100 | transition: all 0.3s;
101 | }
102 | &:hover {
103 | background-color: var(--gray-plus-color);
104 | svg path {
105 | fill: var(--main-color);
106 | }
107 | }
108 | }
109 | }
110 | }
111 | }
112 | .basket__order {
113 | height: 100%;
114 | width: 100%;
115 | @include flex(center, flex-end, column);
116 |
117 | &-price {
118 | width: 100%;
119 | @include flex(center, center, column);
120 | &-text {
121 | width: 100%;
122 | @include flex(center, space-between, row);
123 | &:not(:last-child) {
124 | margin-bottom: 5px
125 | }
126 | &1 {
127 | @include font(16, 16, 600, 21px);
128 | color: var(--gray-btn-color);
129 | }
130 | &2 {
131 | @include font(18, 18, 700, 24px);
132 | color: var(--dark-black-color);
133 | }
134 | }
135 | }
136 | &-submit {
137 | @include flex(center, center, unset);
138 | width: 100%;
139 | height: 60px;
140 | border-radius: 50px;
141 | border: none;
142 | margin-top: 20px;
143 | cursor: pointer;
144 | color: var(--white-color);
145 | background-color: var(--order-bg-color);
146 | }
147 | }
148 | }
149 | }
--------------------------------------------------------------------------------
/src/assets/images/icons/sidebar__logo-text.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/scss/main.scss:
--------------------------------------------------------------------------------
1 | @import "./global/settings";
2 | @import "./global/normalize";
3 | @import "./global/variables";
4 | @import "./mixins/mixins";
5 | @import "./pages.scss";
6 |
7 | .wrapper {
8 | width: 100%;
9 | display: flex;
10 | // @include flex(flex-start, space-between, row);
11 | // Sidebar
12 | @import "./sidebar.scss";
13 | .layout {
14 | position: relative;
15 | width: 62%;
16 | @media screen and (max-width: 1440px) {
17 | width: 100%;
18 | }
19 | .main-layout__row {
20 | @include flex(center, space-between, row);
21 | width: 100%;
22 | // Main
23 | .main {
24 | position: relative;
25 | @include flex(flex-start, unset, unset);
26 | flex: 1 1 auto;
27 | width: 1200px;
28 | height: 100vh;
29 | padding: 33px 45px 0;
30 | @media screen and (min-width: 767px) and (max-width: 1440px) {
31 | width: 92%;
32 | }
33 | @media screen and (max-width: 767px) {
34 | width: 100%;
35 | padding: 33px 24px;
36 | }
37 | &-row {
38 | width: 100%;
39 | @include flex(flex-start, center, column);
40 | .main__search {
41 | width: 100%;
42 | height: 50px;
43 | @include flex(center, flex-start, row);
44 | border: none;
45 | border-radius: 10px;
46 | background-color: var(--sidebar-bg);
47 | padding: 13px 24px;
48 | margin-bottom: 40px;
49 | @media screen and (max-width: 767px) {
50 | display: none;
51 | }
52 | img {
53 | width: 23px;
54 | height: 23px;
55 | margin-right: 18px;
56 | }
57 | input {
58 | width: 90%;
59 | height: 100%;
60 | border: none;
61 | @include font(18, 18, 500, 24px);
62 | color: var(--black-color);
63 | background-color: transparent;
64 | &::placeholder {
65 | @include font(16, 16, 400, 24px);
66 | color: var(--light-gray);
67 | }
68 | &:focus {
69 | outline: none
70 | }
71 | }
72 | }
73 | .main__search-mobile {
74 | display: none;
75 | @media screen and (max-width: 767px) {
76 | width: 100%;
77 | @include flex(center, space-between, unset);
78 | margin-bottom: 40px;
79 | }
80 | img {
81 | width: 29px;
82 | height: 29px
83 | }
84 | }
85 | .main__title {
86 | @include flex(flex-start, center, column);
87 | width: 100%;
88 | margin-bottom: 27px;
89 | &-text1 {
90 | @include font(26, 26, 700, 39px);
91 | color: var(--black-color);
92 | }
93 | &-text2 {
94 | @include font(16, 16, 600, 24px);
95 | color: var(--light-gray);
96 | }
97 | }
98 | @import "./skeleton.scss";
99 | .main__category {
100 | width: 100%;
101 | position: relative;
102 | @include flex(center, space-between, row);
103 | &::after {
104 | content: '';
105 | height: 2px;
106 | width: 100%;
107 | margin-top: 12px;
108 | position: absolute;
109 | bottom: 0;
110 | background-color: var(--sidebar-bg);
111 | @media screen and (max-width: 767px) {
112 | display: none;
113 | }
114 | }
115 | &-item {
116 | @include flex(center, center, row);
117 | margin-bottom: 15px;
118 | cursor: pointer;
119 | border: none;
120 | background-color: transparent;
121 | &.active {
122 | svg path {
123 | fill: var(--main-color);
124 | }
125 | .main__category-text {
126 | color: var(--main-color);
127 | }
128 | }
129 | @media screen and (max-width: 630px) {
130 | background-color: var(--gray-bg-color);
131 | border-radius: 16px;
132 | }
133 | svg {
134 | width: 24px;
135 | height: 24px;
136 | margin-right: 15px;
137 | @media screen and (max-width: 630px) {
138 | width: 60px;
139 | height: 60px;
140 | padding: 15px;
141 | margin-right: 0;
142 | }
143 | &.active {
144 | background-color: var(--main-color);
145 | color: var(--white-color);
146 | }
147 | }
148 | .main__category-text {
149 | @include font(20, 20, 600, 27px);
150 | // &:first-child {
151 | // color: var(--main-color);
152 | // }
153 | color: var(--black-color);
154 | @media screen and (max-width: 630px) {
155 | display: none;
156 | }
157 | }
158 | }
159 | }
160 | .main__products {
161 | width: 100%;
162 | @include flex(center, flex-start, row);
163 | flex-wrap: wrap;
164 | margin-top: 35px;
165 | border-radius: 16px;
166 | gap: 20px 20px;
167 | .main__product {
168 | position: relative;
169 | width: 260px;
170 | @include flex(flex-start, center, column);
171 | border-radius: 16px;
172 | background-color: var(--white-color);
173 | box-shadow: 4px 4px 12px rgba(0, 0, 0, 0.1);
174 | @media screen and (max-width: 630px) {
175 | width: 156px;
176 | }
177 | &-like {
178 | position: absolute;
179 | top: 0;
180 | right: 0;
181 | background-color: transparent;
182 | border: 1px solid transparent;
183 | padding: 10px 10px 0 0;
184 | &.active {
185 | svg path {
186 | fill: var(--main-color);
187 | opacity: 1;
188 | &:hover {
189 | fill: var(--main-color);
190 | }
191 | }
192 | }
193 | svg {
194 | width: 30px;
195 | height: 30px;
196 | path {
197 | fill: #dddddd;
198 | stroke: var(--black-color);
199 | stroke-width: 1.5px;
200 | stroke-opacity: .8;
201 | opacity: 0.7;
202 | transition: all .2s;
203 | &:hover {
204 | fill: #ffffff;
205 | opacity: 1;
206 | stroke-opacity: 1;
207 | }
208 | }
209 | }
210 | }
211 | img {
212 | width: 260px;
213 | height: 163px;
214 | object-fit: cover;
215 | border-radius: 16px 16px 0px 0px;
216 | @media screen and (max-width: 630px) {
217 | width: 156px;
218 | height: 157px;
219 | object-fit: cover;
220 | }
221 | }
222 |
223 | &-detail {
224 | width: 100%;
225 | @include flex(flex-end, center, column);
226 | .main__product-col {
227 | width: 100%;
228 | @include flex(flex-start, center, column);
229 | padding-top: 15px;
230 | padding-left: 15px;
231 | .main__product-name {
232 | @include font(16, 13, 500, 24px);
233 | color: var(--gray-color);
234 | }
235 | .main__product-price {
236 | @include font(16, 14, 600, 24px);
237 | color: var(--main-color);
238 | }
239 | }
240 | .main__product-add {
241 | @include flex(center, center, unset);
242 | width: 40px;
243 | height: 40px;
244 | cursor: pointer;
245 | margin: 0 10px 11px 0;
246 | font-size: 18px;
247 | border: 1px solid var(--gray-btn-color);
248 | border-radius: 16px;
249 | color: var(--gray-btn-color);
250 | background-color: transparent;
251 | transition: all 0.3s;
252 | &:hover {
253 | background-color: var(--black-color);
254 | }
255 | }
256 | }
257 | }
258 | }
259 | }
260 | }
261 | }
262 | }
263 | }
--------------------------------------------------------------------------------