├── public
├── favicon.ico
├── logo192.png
├── logo512.png
├── robots.txt
├── assets
│ ├── images
│ │ ├── logo.png
│ │ ├── bg-cart.webp
│ │ ├── bg-profile.webp
│ │ ├── cold-brew.webp
│ │ └── bg-main-coffee.webp
│ └── screenshots
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ ├── 4.png
│ │ └── 5.png
├── manifest.json
└── index.html
├── src
├── assets
│ ├── images
│ │ ├── product-1.webp
│ │ ├── team-work.webp
│ │ ├── placeholder-promo.jpg
│ │ ├── placeholder-image.webp
│ │ ├── placeholder-profile.jpg
│ │ ├── person-with-a-coffee.webp
│ │ ├── loading.svg
│ │ ├── partners
│ │ │ ├── netflix.svg
│ │ │ ├── discord.svg
│ │ │ ├── amazon.svg
│ │ │ ├── reddit.svg
│ │ │ └── spotify.svg
│ │ ├── not_found.svg
│ │ └── empty-box.svg
│ ├── illustrations
│ │ └── mobile-search-undraw.png
│ ├── icons
│ │ ├── burger-menu-left.svg
│ │ ├── facebook.svg
│ │ ├── check-circle.svg
│ │ ├── icon-pen.svg
│ │ ├── love.svg
│ │ ├── star.svg
│ │ ├── close.svg
│ │ ├── user.svg
│ │ ├── check.svg
│ │ ├── place.svg
│ │ ├── twitter.svg
│ │ ├── chat.svg
│ │ └── instagram.svg
│ └── logo.svg
├── pages
│ ├── Promo
│ │ └── index.jsx
│ ├── History
│ │ ├── HistoryDetail.jsx
│ │ └── index.jsx
│ ├── Profile
│ │ ├── DeletePhotoModal.jsx
│ │ └── EditPassword.jsx
│ ├── Error
│ │ └── index.jsx
│ ├── Auth
│ │ ├── index.jsx
│ │ ├── ForgotPass.jsx
│ │ ├── ResetPass.jsx
│ │ ├── Register.jsx
│ │ └── Login.jsx
│ ├── Cart
│ │ └── testpersist.jsx
│ └── Products
│ │ ├── NewProduct.jsx
│ │ ├── EditProduct.jsx
│ │ └── GetAllProducts.jsx
├── utils
│ ├── dataProvider
│ │ ├── base
│ │ │ └── index.js
│ │ ├── admin.js
│ │ ├── userPanel.js
│ │ ├── profile.js
│ │ ├── auth.js
│ │ ├── transaction.js
│ │ ├── promo.js
│ │ └── products.js
│ ├── localStorage.js
│ ├── scrollToTop.js
│ ├── documentTitle.js
│ ├── wrappers
│ │ ├── withSearchParams.js
│ │ └── protectedRoute.js
│ ├── authUtils.js
│ └── helpers.js
├── setupTests.js
├── tests
│ └── App.test.js
├── components
│ ├── Loading.jsx
│ ├── Promo
│ │ ├── PromoNotFound.jsx
│ │ └── DeletePromo.jsx
│ ├── Button.jsx
│ ├── Product
│ │ ├── ProductNotFound.jsx
│ │ └── DeleteProduct.jsx
│ ├── Modal.jsx
│ ├── Logout.jsx
│ ├── Notification.jsx
│ ├── AuthFooter.jsx
│ ├── Footer.jsx
│ └── Sidebar.jsx
├── reportWebVitals.js
├── redux
│ ├── slices
│ │ ├── index.js
│ │ ├── context.slice.js
│ │ ├── userInfo.slice.js
│ │ ├── price.slice.js
│ │ ├── profile.slice.js
│ │ └── cart.slice.js
│ └── store.js
├── styles
│ └── index.css
├── index.js
└── router.js
├── .gitignore
├── .eslintrc.json
├── target
└── npmlist.json
├── LICENSE
├── tailwind.config.js
├── package.json
└── README.md
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/public/assets/images/logo.png
--------------------------------------------------------------------------------
/public/assets/screenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/public/assets/screenshots/1.png
--------------------------------------------------------------------------------
/public/assets/screenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/public/assets/screenshots/2.png
--------------------------------------------------------------------------------
/public/assets/screenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/public/assets/screenshots/3.png
--------------------------------------------------------------------------------
/public/assets/screenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/public/assets/screenshots/4.png
--------------------------------------------------------------------------------
/public/assets/screenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/public/assets/screenshots/5.png
--------------------------------------------------------------------------------
/public/assets/images/bg-cart.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/public/assets/images/bg-cart.webp
--------------------------------------------------------------------------------
/src/assets/images/product-1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/src/assets/images/product-1.webp
--------------------------------------------------------------------------------
/src/assets/images/team-work.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/src/assets/images/team-work.webp
--------------------------------------------------------------------------------
/public/assets/images/bg-profile.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/public/assets/images/bg-profile.webp
--------------------------------------------------------------------------------
/public/assets/images/cold-brew.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/public/assets/images/cold-brew.webp
--------------------------------------------------------------------------------
/src/assets/images/placeholder-promo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/src/assets/images/placeholder-promo.jpg
--------------------------------------------------------------------------------
/public/assets/images/bg-main-coffee.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/public/assets/images/bg-main-coffee.webp
--------------------------------------------------------------------------------
/src/assets/images/placeholder-image.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/src/assets/images/placeholder-image.webp
--------------------------------------------------------------------------------
/src/assets/images/placeholder-profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/src/assets/images/placeholder-profile.jpg
--------------------------------------------------------------------------------
/src/assets/images/person-with-a-coffee.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/src/assets/images/person-with-a-coffee.webp
--------------------------------------------------------------------------------
/src/assets/illustrations/mobile-search-undraw.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k1rana/jokopi-react/HEAD/src/assets/illustrations/mobile-search-undraw.png
--------------------------------------------------------------------------------
/src/pages/Promo/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function AllPromo() {
4 | return
AllPromo
;
5 | }
6 |
7 | export default AllPromo;
8 |
--------------------------------------------------------------------------------
/src/pages/History/HistoryDetail.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function HistoryDetail() {
4 | return HistoryDetail
;
5 | }
6 |
7 | export default HistoryDetail;
8 |
--------------------------------------------------------------------------------
/src/pages/Profile/DeletePhotoModal.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function DeletePhotoModal() {
4 | return DeletePhotoModal
;
5 | }
6 |
7 | export default DeletePhotoModal;
8 |
--------------------------------------------------------------------------------
/src/utils/dataProvider/base/index.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const host = process.env.REACT_APP_BACKEND_HOST;
4 |
5 | const api = axios.create({
6 | baseURL: host,
7 | });
8 |
9 | export default api;
10 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/utils/localStorage.js:
--------------------------------------------------------------------------------
1 | export const save = (key, value) => {
2 | localStorage.setItem(key, value);
3 | };
4 | export const get = (key) => {
5 | return localStorage.getItem(key);
6 | };
7 | export const remove = (key) => {
8 | localStorage.removeItem(key);
9 | };
10 |
--------------------------------------------------------------------------------
/src/tests/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/src/utils/scrollToTop.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | import { useLocation } from 'react-router-dom';
4 |
5 | export default function ScrollToTop() {
6 | const { pathname } = useLocation();
7 |
8 | useEffect(() => {
9 | window.scrollTo(0, 0);
10 | }, [pathname]);
11 |
12 | return null;
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Loading.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import loadingImage from "../assets/images/loading.svg";
4 |
5 | function Loading() {
6 | return (
7 |
8 |
9 |

10 |
11 |
12 | );
13 | }
14 |
15 | export default Loading;
16 |
--------------------------------------------------------------------------------
/src/components/Promo/PromoNotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { connect } from "react-redux";
4 |
5 | const PromoNotFound = (props) => {
6 | return PromoNotFound
;
7 | };
8 |
9 | const mapStateToProps = (state) => ({
10 | userInfo: state.userInfo,
11 | });
12 |
13 | const mapDispatchToProps = {};
14 |
15 | export default connect(mapStateToProps, mapDispatchToProps)(PromoNotFound);
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/redux/slices/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 |
3 | import cartSlice from "./cart.slice";
4 | import contextSlice from "./context.slice";
5 | import profileSlice from "./profile.slice";
6 | import uinfoSlice from "./userInfo.slice";
7 |
8 | const reducers = combineReducers({
9 | userInfo: uinfoSlice,
10 | profile: profileSlice,
11 | cart: cartSlice,
12 | context: contextSlice,
13 | });
14 |
15 | export default reducers;
16 |
--------------------------------------------------------------------------------
/src/components/Button.jsx:
--------------------------------------------------------------------------------
1 | // Button.jsx
2 | import React from "react";
3 |
4 | const Button = ({ children, className, ...props }) => {
5 | return (
6 |
12 | );
13 | };
14 |
15 | export default Button;
16 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "plugin:react/recommended"
8 | ],
9 | "overrides": [
10 | ],
11 | "parserOptions": {
12 | "ecmaVersion": "latest",
13 | "sourceType": "module"
14 | },
15 | "plugins": [
16 | "react"
17 | ],
18 | "rules": {
19 | "react/jsx-key":"warn",
20 | "react/prop-types":"off"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/assets/icons/burger-menu-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/icons/facebook.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/utils/documentTitle.js:
--------------------------------------------------------------------------------
1 | // useDocumentTitle.js
2 | import { useEffect, useRef } from "react";
3 |
4 | function useDocumentTitle(title, prevailOnUnmount = false) {
5 | const defaultTitle = useRef(document.title);
6 |
7 | useEffect(() => {
8 | document.title = `${title} - ${process.env.REACT_APP_WEBSITE_NAME}`;
9 | }, [title]);
10 |
11 | useEffect(
12 | () => () => {
13 | if (!prevailOnUnmount) {
14 | document.title = defaultTitle.current;
15 | }
16 | },
17 | []
18 | );
19 | }
20 |
21 | export default useDocumentTitle;
22 |
--------------------------------------------------------------------------------
/src/pages/Error/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { NavLink } from 'react-router-dom';
4 |
5 | import notfoundImage from '../../assets/images/empty-box.svg';
6 |
7 | export default function NotFound() {
8 | return (
9 |
10 |
11 | Page Not Found
12 |
13 | Back to Home
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/wrappers/withSearchParams.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { useNavigate, useSearchParams } from "react-router-dom";
4 |
5 | function withSearchParams(Component) {
6 | function Wrapper(props) {
7 | const [searchParams, setSearchParams] = useSearchParams();
8 | const navigate = useNavigate();
9 | return (
10 |
16 | );
17 | }
18 | return Wrapper;
19 | }
20 |
21 | export default withSearchParams;
22 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/dataProvider/admin.js:
--------------------------------------------------------------------------------
1 | import api from "./base";
2 |
3 | export const getMonthlyReport = (token, controller) => {
4 | return api.get("/apiv1/adminPanel/monthlyReport", {
5 | headers: {
6 | Authorization: `Bearer ${token}`,
7 | },
8 | signal: controller.signal,
9 | });
10 | };
11 |
12 | export const getSellingReport = (view = "monthly", token, controller) => {
13 | return api.get("/apiv1/adminPanel/reports", {
14 | params: {
15 | view,
16 | },
17 | headers: {
18 | Authorization: `Bearer ${token}`,
19 | },
20 | signal: controller.signal,
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/src/assets/icons/check-circle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-pen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
6 |
7 | * {
8 | font-family: "Poppins", sans-serif;
9 | }
10 | @layer base {
11 | input[type="number"]::-webkit-inner-spin-button,
12 | input[type="number"]::-webkit-outer-spin-button {
13 | -webkit-appearance: none;
14 | margin: 0;
15 | }
16 | }
17 |
18 | input[type="number"] {
19 | -moz-appearance: textfield;
20 | }
21 |
22 | .global-px {
23 | @apply mx-auto px-3 max-w-sm sm:max-w-lg md:max-w-3xl lg:max-w-5xl xl:max-w-6xl;
24 | }
25 |
--------------------------------------------------------------------------------
/target/npmlist.json:
--------------------------------------------------------------------------------
1 | {"version":"0.1.0","name":"jokopi-react","dependencies":{"@reduxjs/toolkit":{"version":"1.9.3"},"@testing-library/jest-dom":{"version":"5.16.5"},"@testing-library/react":{"version":"13.4.0"},"@testing-library/user-event":{"version":"13.5.0"},"axios":{"version":"1.3.4"},"daisyui":{"version":"2.51.6"},"jwt-decode":{"version":"3.1.2"},"lodash":{"version":"4.17.21"},"react-burger-menu":{"version":"3.0.9"},"react-dom":{"version":"18.2.0"},"react-hot-toast":{"version":"2.4.0"},"react-image-crop":{"version":"10.0.9"},"react-redux":{"version":"8.0.5"},"react-router-dom":{"version":"6.9.0"},"react-scripts":{"version":"5.0.1"},"react":{"version":"18.2.0"},"redux-persist":{"version":"6.0.0"},"redux":{"version":"4.2.1"},"web-vitals":{"version":"2.1.4"}}}
--------------------------------------------------------------------------------
/src/assets/images/loading.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/redux/slices/context.slice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const contextSlice = createSlice({
4 | name: "context",
5 | initialState: {
6 | logout: false,
7 | },
8 | reducers: {
9 | toggleLogout: (prevState, action) => {
10 | return {
11 | ...prevState,
12 | logout: !prevState.logout,
13 | };
14 | },
15 | openLogout: (prevState, action) => {
16 | return {
17 | ...prevState,
18 | logout: true,
19 | };
20 | },
21 | closeLogout: (prevState, action) => {
22 | return {
23 | ...prevState,
24 | logout: false,
25 | };
26 | },
27 | },
28 | });
29 |
30 | export const contextAct = contextSlice.actions;
31 | export default contextSlice.reducer;
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | ISC License
2 |
3 | Copyright (c) 2023 Farhan Brillan W alias nyannss
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted, provided that the above
7 | copyright notice and this permission notice appear in all copies.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15 | PERFORMANCE OF THIS SOFTWARE.
16 |
--------------------------------------------------------------------------------
/src/assets/icons/love.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Product/ProductNotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { NavLink } from "react-router-dom";
4 |
5 | import lostImage from "../../assets/images/not_found.svg";
6 |
7 | function ProductNotFound() {
8 | return (
9 |
10 |
11 |
12 |
Product Not Found
13 |
14 |
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | export default ProductNotFound;
24 |
--------------------------------------------------------------------------------
/src/redux/slices/userInfo.slice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const uinfoSlice = createSlice({
4 | name: `${process.env.REACT_APP_WEBSITE_NAME}_appdata`,
5 | initialState: {
6 | token: "",
7 | newToken: "",
8 | role: "",
9 | },
10 | reducers: {
11 | assignToken: (prevState, action) => {
12 | return {
13 | ...prevState,
14 | token: action.payload,
15 | };
16 | },
17 | assignData: (prevState, action) => {
18 | return {
19 | ...prevState,
20 | role: action.payload.role,
21 | };
22 | },
23 | dismissToken: (prevState) => {
24 | return {
25 | ...prevState,
26 | token: "",
27 | role: "",
28 | };
29 | },
30 | },
31 | });
32 |
33 | export const uinfoAct = uinfoSlice.actions;
34 | export default uinfoSlice.reducer;
35 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import {
2 | FLUSH,
3 | PAUSE,
4 | PERSIST,
5 | persistReducer,
6 | persistStore,
7 | PURGE,
8 | REGISTER,
9 | REHYDRATE,
10 | } from "redux-persist";
11 | import storage from "redux-persist/lib/storage"; // defaults to localStorage for web
12 |
13 | import { configureStore } from "@reduxjs/toolkit";
14 |
15 | import reducer from "./slices";
16 |
17 | const persistConfig = {
18 | key: "jokopi_appdata",
19 | storage,
20 | blacklist: ["context"],
21 | };
22 |
23 | const persistedReducer = persistReducer(persistConfig, reducer);
24 |
25 | const store = configureStore({
26 | reducer: persistedReducer,
27 | middleware: (defaultMiddleware) => {
28 | return defaultMiddleware({
29 | serializableCheck: {
30 | ignoreActions: [PERSIST, FLUSH, REHYDRATE, PAUSE, REGISTER, PURGE],
31 | },
32 | });
33 | },
34 | });
35 | export const persistor = persistStore(store);
36 | export default store;
37 |
--------------------------------------------------------------------------------
/src/assets/icons/star.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/user.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/utils/authUtils.js:
--------------------------------------------------------------------------------
1 | import jwt_decode from "jwt-decode";
2 |
3 | import { uinfoAct } from "../redux/slices/userInfo.slice";
4 | import store from "../redux/store";
5 |
6 | export function uinfoFromRedux() {
7 | const state = store.getState();
8 | const userInfo = state.userInfo;
9 | return userInfo;
10 | }
11 |
12 | export function isAuthenticated() {
13 | const userInfo = uinfoFromRedux();
14 | if (userInfo.token.length > 0) {
15 | const decoded = jwt_decode(userInfo.token);
16 | const currentTime = Date.now() / 1000;
17 |
18 | if (decoded.exp < currentTime) {
19 | store.dispatch(uinfoAct.dismissToken());
20 | return false;
21 | }
22 |
23 | return true;
24 | } else {
25 | return false;
26 | }
27 | }
28 |
29 | export function getUserData() {
30 | const userInfo = uinfoFromRedux();
31 | if (userInfo.token.length > 0) {
32 | const decoded = jwt_decode(userInfo.token);
33 | return decoded;
34 | }
35 | return {};
36 | }
37 |
--------------------------------------------------------------------------------
/src/assets/icons/check.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/place.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/Auth/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Outlet } from 'react-router-dom';
4 |
5 | import AuthFooter from '../../components/AuthFooter';
6 |
7 | const Auth = () => {
8 | return (
9 | <>
10 |
11 |
24 |
25 | >
26 | );
27 | };
28 |
29 | export default Auth;
30 |
--------------------------------------------------------------------------------
/src/pages/Cart/testpersist.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import {
4 | useDispatch,
5 | useSelector,
6 | } from 'react-redux';
7 |
8 | import Footer from '../../components/Footer';
9 | import Header from '../../components/Header';
10 | import { uinfoAct } from '../../redux/slices/userInfo.slice';
11 |
12 | function Cart() {
13 | const token = useSelector((state) => state.userInfo);
14 | const [input, setInput] = useState(token.token);
15 | const dispatch = useDispatch();
16 | const handlerAct = (e) => {
17 | if (e.key === "Enter") {
18 | dispatch(uinfoAct.assignToken(e.target.value));
19 | }
20 | };
21 | // console.log(jwtDecode(get("jokopi-token")));
22 | return (
23 | <>
24 |
25 | {token.token}
26 |
27 | setInput(e.target.value)}
31 | onKeyDown={handlerAct}
32 | />
33 |
34 | >
35 | );
36 | }
37 |
38 | export default Cart;
39 |
--------------------------------------------------------------------------------
/src/assets/images/partners/netflix.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-filename-extension */
2 | import './styles/index.css';
3 |
4 | import React from 'react';
5 |
6 | import ReactDOM from 'react-dom/client';
7 | import { Provider } from 'react-redux';
8 | import { PersistGate } from 'redux-persist/integration/react';
9 |
10 | import { Notification } from './components/Notification';
11 | import store, { persistor } from './redux/store';
12 | import reportWebVitals from './reportWebVitals';
13 | import Router from './router';
14 |
15 | const root = ReactDOM.createRoot(document.getElementById("root"));
16 | root.render(
17 |
18 |
19 |
20 |
21 | {/* */}
22 |
23 |
24 |
25 |
26 | );
27 |
28 | // If you want to start measuring performance in your app, pass a function
29 | // to log results (for example: reportWebVitals(console.log))
30 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
31 | reportWebVitals();
32 |
--------------------------------------------------------------------------------
/src/utils/dataProvider/userPanel.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const host = process.env.REACT_APP_BACKEND_HOST;
4 |
5 | export function fetchProfile(token) {
6 | const config = {
7 | headers: { Authorization: `Bearer ${token}` },
8 | };
9 | return axios.get(`${host}/apiv1/userPanel/profile`, config);
10 | }
11 |
12 | export function addCart(product_id, cart, token) {
13 | const config = {
14 | headers: { Authorization: `Bearer ${token}` },
15 | };
16 | const data = {
17 | product_id,
18 | cart,
19 | };
20 | return axios.patch(`${host}/apiv1/userPanel/cart`, data, config);
21 | }
22 |
23 | export function getCart(token) {
24 | const config = {
25 | headers: { Authorization: `Bearer ${token}` },
26 | };
27 | return axios.get(`${host}/apiv1/userPanel/cart`, config);
28 | }
29 |
30 | export function updatePassword(oldPassword, newPassword, token) {
31 | const body = {
32 | oldPassword,
33 | newPassword,
34 | };
35 | const config = {
36 | headers: { Authorization: `Bearer ${token}` },
37 | };
38 | return axios.patch(`${host}/apiv1/auth/editPassword`, body, config);
39 | }
40 |
41 | export function updateProfile(data, token) {
42 | const config = {
43 | headers: { Authorization: `Bearer ${token}` },
44 | };
45 | return axios.patch(`${host}/apiv1/auth/editProfile`, data, config);
46 | }
47 |
--------------------------------------------------------------------------------
/src/assets/icons/twitter.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Modal.jsx:
--------------------------------------------------------------------------------
1 | // Modal.jsx
2 | import React from 'react';
3 |
4 | const Modal = ({ isOpen, onClose, children, className }) => {
5 | return (
6 | <>
7 | {isOpen && (
8 |
12 |
13 |
{
16 | e.stopPropagation();
17 | }}
18 | >
19 |
37 | {children}
38 |
39 |
40 |
41 | )}
42 | >
43 | );
44 | };
45 |
46 | export default Modal;
47 |
--------------------------------------------------------------------------------
/src/utils/dataProvider/profile.js:
--------------------------------------------------------------------------------
1 | import api from "./base";
2 |
3 | export const getProfile = (token, controller) => {
4 | return api.get("/apiv1/userPanel/profile", {
5 | headers: { Authorization: `Bearer ${token}` },
6 | signal: controller.signal,
7 | });
8 | };
9 |
10 | export const editProfile = (
11 | {
12 | image = "",
13 | display_name = "",
14 | address = "",
15 | birthdate = "",
16 | gender = "",
17 | email = "",
18 | phone_number = "",
19 | first_name = "",
20 | last_name = "",
21 | },
22 | token,
23 | controller
24 | ) => {
25 | const body = new FormData();
26 | // append
27 | // console.log(image);
28 | body.append("image", image);
29 | body.append("display_name", display_name);
30 | body.append("address", address);
31 | body.append("birthdate", JSON.stringify(birthdate));
32 | body.append("gender", gender);
33 | body.append("email", email);
34 | body.append("phone_number", phone_number);
35 | body.append("first_name", first_name);
36 | body.append("last_name", last_name);
37 |
38 | // const bodyObj = {
39 | // image,
40 | // display_name,
41 | // address,
42 | // birthdate,
43 | // gender,
44 | // email,
45 | // phone_number,
46 | // };
47 | return api.patch("/apiv1/userPanel/profile", body, {
48 | headers: {
49 | Authorization: `Bearer ${token}`,
50 | "Content-Type": "multipart/form-data",
51 | },
52 | signal: controller.signal,
53 | });
54 | };
55 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./src/**/*.{js,jsx,ts,tsx}",
5 | "./node_modules/react-tailwindcss-datepicker/dist/index.esm.js",
6 | ],
7 | theme: {
8 | extend: {
9 | backgroundImage: {
10 | history: "url('/public/assets/images/cold-brew.webp')",
11 | main: "url('/public/assets/images/bg-main-coffee.webp')",
12 | profile: "url('/public/assets/images/bg-profile.webp')",
13 | cart: "url('/public/assets/images/bg-cart.webp')",
14 | },
15 | boxShadow: {
16 | primary: "0px 6px 20px 0px #00000020;",
17 | },
18 | spacing: {
19 | 22: "7rem",
20 | },
21 | colors: {
22 | primary: "#4F5665",
23 | "primary-context": "#7C828A",
24 | secondary: "#ffba33",
25 | "secondary-200": "#f4a200",
26 | tertiary: "#6A4029",
27 | quartenary: "#0b132a",
28 | },
29 | borderWidth: {
30 | 1: "1px",
31 | },
32 | },
33 | },
34 | daisyui: {
35 | themes: [
36 | {
37 | jokopi: {
38 | primary: "#6A4029",
39 | secondary: "#ffba33",
40 | accent: "#0b132a",
41 | neutral: "#9f9f9f",
42 | "base-100": "#fff",
43 | info: "#3ABFF8",
44 | success: "#36D399",
45 | warning: "#FBBD23",
46 | error: "#F87272",
47 | "plain-white": "#FFF",
48 | },
49 | },
50 | ],
51 | },
52 | plugins: [require("daisyui")],
53 | };
54 |
--------------------------------------------------------------------------------
/src/utils/helpers.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | export const n_f = (number) => {
4 | if (!number || isNaN(number)) return 0;
5 | return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
6 | };
7 |
8 | export const short_n_f = (number) => {
9 | if (isNaN(number)) {
10 | throw new Error("Input isn't number");
11 | }
12 |
13 | const isNegative = number < 0;
14 | const absoluteNumber = Math.abs(number);
15 |
16 | const abbreviations = ["", "K", "M", "B", "T", "Q"];
17 | const base = 1000;
18 |
19 | if (absoluteNumber < base) {
20 | return isNegative ? `-${absoluteNumber}` : absoluteNumber.toString();
21 | }
22 |
23 | const exponent = Math.floor(Math.log10(absoluteNumber) / Math.log10(base));
24 | const abbreviatedNumber = (absoluteNumber / Math.pow(base, exponent)).toFixed(
25 | 2
26 | );
27 |
28 | return (isNegative ? "-" : "") + abbreviatedNumber + abbreviations[exponent];
29 | };
30 |
31 | export function formatDateTime(dateTimeString) {
32 | const dateTime = new Date(dateTimeString);
33 |
34 | const year = dateTime.getFullYear();
35 | const month = String(dateTime.getMonth() + 1).padStart(2, "0");
36 | const day = String(dateTime.getDate()).padStart(2, "0");
37 |
38 | const hours = String(dateTime.getHours()).padStart(2, "0");
39 | const minutes = String(dateTime.getMinutes()).padStart(2, "0");
40 |
41 | return `${year}-${month}-${day} ${hours}:${minutes}`;
42 | }
43 |
44 | export const getEmailUsername = (email) => {
45 | if (!email || email === "") return "Anon";
46 | const username = _.head(_.split(email, "@"));
47 | return username;
48 | };
49 |
--------------------------------------------------------------------------------
/src/utils/dataProvider/auth.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const host = process.env.REACT_APP_BACKEND_HOST;
4 |
5 | export function login(email, password, rememberMe, controller) {
6 | const body = { email, password, rememberMe };
7 | const url = `${host}/apiv1/auth/login`;
8 |
9 | return axios.post(url, body, {
10 | signal: controller.signal,
11 | });
12 | }
13 |
14 | export function register(email, password, phone_number, controller) {
15 | const body = { email, password, phone_number };
16 | const url = `${host}/apiv1/auth/register`;
17 |
18 | return axios.post(url, body, {
19 | signal: controller.signal,
20 | });
21 | }
22 |
23 | export function forgotPass(email, controller) {
24 | const body = { email };
25 | const url = `${host}/apiv1/auth/forgotPass`;
26 |
27 | return axios.post(url, body, {
28 | signal: controller.signal,
29 | });
30 | }
31 |
32 | export function verifyResetPass(verify, code, controller) {
33 | const url = `${host}/apiv1/auth/resetPass?verify=${verify}&code=${code}`;
34 |
35 | return axios.get(url, {
36 | signal: controller.signal,
37 | });
38 | }
39 |
40 | export function resetPass(verify, code, password, controller) {
41 | const url = `${host}/apiv1/auth/resetPass?verify=${verify}&code=${code}`;
42 |
43 | return axios.patch(
44 | url,
45 | { newPassword: password },
46 | {
47 | signal: controller.signal,
48 | }
49 | );
50 | }
51 |
52 | export function logoutUser(token) {
53 | const config = {
54 | headers: {
55 | Authorization: `Bearer ${token}`,
56 | },
57 | };
58 | const url = `${host}/apiv1/auth/logout`;
59 | return axios.delete(url, config);
60 | }
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jokopi-react",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.9.3",
7 | "@testing-library/jest-dom": "^5.16.5",
8 | "@testing-library/react": "^13.4.0",
9 | "@testing-library/user-event": "^13.5.0",
10 | "axios": "^1.3.4",
11 | "daisyui": "^2.51.6",
12 | "dayjs": "^1.11.8",
13 | "jwt-decode": "^3.1.2",
14 | "lodash": "^4.17.21",
15 | "react": "^18.2.0",
16 | "react-burger-menu": "^3.0.9",
17 | "react-dom": "^18.2.0",
18 | "react-hot-toast": "^2.4.0",
19 | "react-image-crop": "^10.0.9",
20 | "react-loading-skeleton": "^3.3.1",
21 | "react-redux": "^8.0.5",
22 | "react-router-dom": "^6.9.0",
23 | "react-scripts": "5.0.1",
24 | "react-tailwindcss-datepicker": "^1.6.1",
25 | "redux": "^4.2.1",
26 | "redux-persist": "^6.0.0",
27 | "victory": "^36.6.10",
28 | "web-vitals": "^2.1.4"
29 | },
30 | "devDependencies": {
31 | "eslint": "^8.36.0",
32 | "eslint-config-airbnb": "^19.0.4",
33 | "eslint-plugin-import": "^2.27.5",
34 | "eslint-plugin-jsx-a11y": "^6.7.1",
35 | "eslint-plugin-react": "^7.32.2",
36 | "eslint-plugin-react-hooks": "^4.6.0",
37 | "tailwindcss": "^3.2.7"
38 | },
39 | "scripts": {
40 | "start": "react-scripts start",
41 | "build": "react-scripts build",
42 | "test": "react-scripts test",
43 | "eject": "react-scripts eject"
44 | },
45 | "eslintConfig": {
46 | "extends": [
47 | "react-app",
48 | "react-app/jest"
49 | ]
50 | },
51 | "browserslist": {
52 | "production": [
53 | ">0.2%",
54 | "not dead",
55 | "not op_mini all"
56 | ],
57 | "development": [
58 | "last 1 chrome version",
59 | "last 1 firefox version",
60 | "last 1 safari version"
61 | ]
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/redux/slices/price.slice.js:
--------------------------------------------------------------------------------
1 | import { getSizePrice } from "../../utils/https/product";
2 |
3 | const { createAsyncThunk, createSlice } = require("@reduxjs/toolkit");
4 |
5 | const initialState = {
6 | data: [],
7 | isLoading: false,
8 | isRejected: false,
9 | isFulfilled: false,
10 | err: null,
11 | };
12 |
13 | const getPriceBySize = createAsyncThunk(
14 | "posts/getPriceBySize",
15 | async ({ controller }, { rejectWithValue, fulfillWithValue }) => {
16 | try {
17 | const result = await getSizePrice(controller);
18 | fulfillWithValue(result?.data.data);
19 | return result?.data.data;
20 | } catch (err) {
21 | // You can choose to use the message attached to err or write a custom error
22 | return rejectWithValue("Oops there seems to be an error");
23 | }
24 | }
25 | );
26 |
27 | const priceSlice = createSlice({
28 | name: "price",
29 | initialState,
30 | reducers: {},
31 | extraReducers: (builder) => {
32 | builder
33 | .addCase(getPriceBySize.pending, (prevState) => {
34 | return {
35 | ...prevState,
36 | isLoading: true,
37 | isRejected: false,
38 | isFulfilled: false,
39 | };
40 | })
41 | .addCase(getPriceBySize.fulfilled, (prevState, action) => {
42 | return {
43 | ...prevState,
44 | isLoading: false,
45 | isFulfilled: true,
46 | data: action.payload,
47 | };
48 | })
49 | .addCase(getPriceBySize.rejected, (prevState, action) => {
50 | return {
51 | ...prevState,
52 | isLoading: false,
53 | isRejected: true,
54 | err: action.payload,
55 | };
56 | });
57 | },
58 | });
59 |
60 | export const priceActions = { ...priceSlice.actions, getPriceBySize };
61 | export default priceSlice.reducer;
62 |
--------------------------------------------------------------------------------
/src/assets/icons/chat.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
31 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | jokopi
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/utils/dataProvider/transaction.js:
--------------------------------------------------------------------------------
1 | import api from './base';
2 |
3 | export const createTransaction = (
4 | {
5 | payment_id = 1,
6 | delivery_id = 1,
7 | status_id = 3,
8 | address = "Table no 4",
9 | notes = "Makkah",
10 | },
11 | products = [],
12 | token,
13 | controller
14 | ) => {
15 | const body = {
16 | payment_id,
17 | delivery_id,
18 | status_id,
19 | products,
20 | address,
21 | notes,
22 | };
23 | return api.post(`/apiv1/transactions`, body, {
24 | signal: controller.signal,
25 | headers: { Authorization: `Bearer ${token}` },
26 | });
27 | };
28 |
29 | export const getTransactions = (
30 | { status_id = 1, page = 1 },
31 | token,
32 | controller
33 | ) => {
34 | return api.get("/apiv1/transactions", {
35 | params: {
36 | status_id,
37 | page,
38 | },
39 | headers: { Authorization: `Bearer ${token}` },
40 | signal: controller.signal,
41 | });
42 | };
43 |
44 | export const setTransactionDone = (ids = [], token, controller) => {
45 | let convertedIds = ids.toString();
46 | if (typeof ids === "object") {
47 | convertedIds = ids.join(",");
48 | }
49 | console.log(convertedIds);
50 | return api.patch(
51 | "/apiv1/transactions/changeStatus",
52 | {
53 | transactions: convertedIds,
54 | },
55 | {
56 | headers: { Authorization: `Bearer ${token}` },
57 | signal: controller.signal,
58 | }
59 | );
60 | };
61 |
62 | export const getTransactionHistory = (
63 | { page = "1", limit = "9" },
64 | token,
65 | controller
66 | ) => {
67 | return api.get("/apiv1/userPanel/transactions", {
68 | params: {
69 | page,
70 | limit,
71 | },
72 | headers: { Authorization: `Bearer ${token}` },
73 | signal: controller.signal,
74 | });
75 | };
76 |
77 | export const getTransactionDetail = (transactionId, token, controller) => {
78 | return api.get(`/apiv1/transactions/${transactionId}`, {
79 | headers: { Authorization: `Bearer ${token}` },
80 | signal: controller.signal,
81 | });
82 | };
83 |
--------------------------------------------------------------------------------
/src/components/Promo/DeletePromo.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { toast } from "react-hot-toast";
4 | import { connect } from "react-redux";
5 | import { useNavigate } from "react-router-dom";
6 |
7 | import { deletePromoEntry } from "../../utils/dataProvider/promo";
8 | import Modal from "../Modal";
9 |
10 | function DeletePromo({ isOpen, onClose, promoId, userInfo }) {
11 | const [isLoading, setIsLoading] = useState(false);
12 | const controller = new AbortController();
13 | const navigate = useNavigate();
14 | const yesHandler = () => {
15 | if (isLoading) return;
16 | setIsLoading(true);
17 | deletePromoEntry(promoId, userInfo.token, controller)
18 | .then(() => {
19 | navigate("/products", { replace: true });
20 | toast.success("Promo deleted successfully");
21 | })
22 | .catch(() => {
23 | toast.error("An error ocurred");
24 | })
25 | .finally(() => setIsLoading(false));
26 | };
27 | const closeHandler = () => {
28 | if (isLoading) return;
29 | onClose();
30 | };
31 | return (
32 |
33 | Are you sure want to delete this promo?
34 |
35 | Warning: this act can't be undone!
36 |
37 |
38 |
44 |
51 |
52 |
53 | );
54 | }
55 |
56 | const mapStateToProps = (state) => ({
57 | userInfo: state.userInfo,
58 | });
59 |
60 | const mapDispatchToProps = {};
61 |
62 | export default connect(mapStateToProps, mapDispatchToProps)(DeletePromo);
63 |
--------------------------------------------------------------------------------
/src/components/Product/DeleteProduct.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { toast } from "react-hot-toast";
4 | import { connect } from "react-redux";
5 | import { useNavigate } from "react-router-dom";
6 |
7 | import { deleteProductEntry } from "../../utils/dataProvider/products";
8 | import Modal from "../Modal";
9 |
10 | function DeleteProduct({ isOpen, onClose, productId, userInfo }) {
11 | const [isLoading, setIsLoading] = useState(false);
12 | const controller = new AbortController();
13 | const navigate = useNavigate();
14 | const yesHandler = () => {
15 | if (isLoading) return;
16 | setIsLoading(true);
17 | deleteProductEntry(productId, userInfo.token, controller)
18 | .then(() => {
19 | navigate("/products", { replace: true });
20 | toast.success("Product deleted successfully");
21 | })
22 | .catch(() => {
23 | toast.error("An error ocurred");
24 | })
25 | .finally(() => setIsLoading(false));
26 | };
27 | const closeHandler = () => {
28 | if (isLoading) return;
29 | onClose();
30 | };
31 | return (
32 |
33 | Are you sure want to delete this product?
34 |
35 | Warning: this act can't be undone!
36 |
37 |
38 |
44 |
51 |
52 |
53 | );
54 | }
55 |
56 | const mapStateToProps = (state) => ({
57 | userInfo: state.userInfo,
58 | });
59 |
60 | const mapDispatchToProps = {};
61 |
62 | export default connect(mapStateToProps, mapDispatchToProps)(DeleteProduct);
63 |
--------------------------------------------------------------------------------
/src/assets/images/partners/discord.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/instagram.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Logout.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { toast } from "react-hot-toast";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import { useNavigate } from "react-router-dom";
6 |
7 | import { contextAct } from "../redux/slices/context.slice";
8 | import { profileAction } from "../redux/slices/profile.slice";
9 | import { uinfoAct } from "../redux/slices/userInfo.slice";
10 | import { logoutUser } from "../utils/dataProvider/auth";
11 | import Modal from "./Modal";
12 |
13 | function Logout() {
14 | const context = useSelector((state) => state.context);
15 | const userInfo = useSelector((state) => state.userInfo);
16 | const navigate = useNavigate();
17 | const [isLoading, setLoading] = useState(false);
18 | const dispatch = useDispatch();
19 |
20 | const logoutHandler = () => {
21 | if (isLoading) return;
22 | setLoading(true);
23 | logoutUser(userInfo.token)
24 | .then((result) => {
25 | dispatch(uinfoAct.dismissToken());
26 | dispatch(contextAct.closeLogout());
27 | profileAction.reset();
28 | toast.success("See ya, coffeeholic!");
29 | navigate("/", { replace: true });
30 | })
31 | .catch((err) => {
32 | console.log(err);
33 | toast.error("An error ocurred");
34 | })
35 | .finally(() => {
36 | setLoading(false);
37 | });
38 | };
39 |
40 | const onClose = () => {
41 | if (isLoading) return;
42 | dispatch(contextAct.closeLogout());
43 | };
44 | return (
45 | userInfo.token && (
46 |
47 | Are you sure you want to logout?
48 |
49 |
57 |
63 |
64 |
65 | )
66 | );
67 | }
68 |
69 | export default Logout;
70 |
--------------------------------------------------------------------------------
/src/components/Notification.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { toast, ToastBar, Toaster } from "react-hot-toast";
4 |
5 | export const Notification = () => {
6 | return (
7 |
21 | {(t) => (
22 |
23 | {({ icon, message }) => (
24 | <>
25 | {icon}
26 | {message}
27 | {t.type !== "loading" && (
28 |
47 | )}
48 | >
49 | )}
50 |
51 | )}
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/src/utils/wrappers/protectedRoute.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React, { useEffect, useMemo } from "react";
3 |
4 | import jwtDecode from "jwt-decode";
5 | import { toast } from "react-hot-toast";
6 | import { useDispatch, useSelector } from "react-redux";
7 | import { Navigate, Outlet, useNavigate } from "react-router-dom";
8 |
9 | import { profileAction } from "../../redux/slices/profile.slice";
10 | import { uinfoAct } from "../../redux/slices/userInfo.slice";
11 |
12 | export const CheckAuth = ({ children }) => {
13 | const { userInfo } = useSelector((state) => ({
14 | userInfo: state.userInfo,
15 | }));
16 |
17 | if (userInfo.token === "" && userInfo.token?.length < 1) {
18 | toast.error("You must login first");
19 | return ;
20 | }
21 | return ;
22 | };
23 |
24 | export const CheckNoAuth = ({ children }) => {
25 | const { userInfo } = useSelector((state) => ({
26 | userInfo: state.userInfo,
27 | }));
28 | if (userInfo.token && userInfo.token?.length > 0) {
29 | return ;
30 | }
31 | return children;
32 | };
33 |
34 | export const CheckIsAdmin = ({ children }) => {
35 | const { userInfo, profile } = useSelector((state) => ({
36 | userInfo: state.userInfo,
37 | profile: state.profile,
38 | }));
39 |
40 | if (userInfo.token === "" || Number(userInfo.role) < 2) {
41 | return ;
42 | }
43 | return ;
44 | };
45 |
46 | export const TokenHandler = () => {
47 | const { userInfo, profile } = useSelector((state) => ({
48 | userInfo: state.userInfo,
49 | profile: state.profile,
50 | }));
51 | const dispatch = useDispatch();
52 | const controller = useMemo(() => new AbortController(), []);
53 | const navigate = useNavigate();
54 | useEffect(() => {
55 | if (userInfo.token) {
56 | const decoded = jwtDecode(userInfo.token);
57 | const currentTime = Date.now() / 1000;
58 |
59 | if (decoded.exp < currentTime) {
60 | dispatch(uinfoAct.dismissToken());
61 | profileAction.reset();
62 | toast.error("Your token is expired, please log in back");
63 | }
64 |
65 | if (profile.isFulfilled) {
66 | profileAction.getProfileThunk(userInfo.token, controller);
67 | }
68 | }
69 | }, [userInfo.token]);
70 | return ;
71 | };
72 |
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/dataProvider/promo.js:
--------------------------------------------------------------------------------
1 | import api from "./base";
2 |
3 | export const createPromoEntry = (
4 | {
5 | image = "",
6 | name = "",
7 | product_id = "",
8 | desc = "",
9 | discount = "",
10 | coupon_code = "",
11 | start_date = "",
12 | end_date = "",
13 | },
14 | token,
15 | controller
16 | ) => {
17 | const bodyForm = new FormData();
18 | bodyForm.append("image", image);
19 | bodyForm.append("name", name);
20 | bodyForm.append("desc", desc);
21 | bodyForm.append("discount", discount);
22 | bodyForm.append("product_id", product_id);
23 | bodyForm.append("coupon_code", coupon_code);
24 | bodyForm.append("start_date", JSON.stringify(start_date));
25 | bodyForm.append("end_date", JSON.stringify(end_date));
26 |
27 | form.coupon_code = form.coupon_code.toUpperCase();
28 | return api.post("/apiv1/promo", form, {
29 | signal: controller.signal,
30 | headers: { Authorization: `Bearer ${token}` },
31 | });
32 | };
33 |
34 | export const getPromos = (
35 | { page = 1, limit = 4, available = "true", searchByName = "" },
36 | controller
37 | ) => {
38 | const params = {
39 | page,
40 | limit,
41 | available,
42 | searchByName,
43 | };
44 | return api.get("/apiv1/promo", {
45 | params,
46 | signal: controller.signal,
47 | });
48 | };
49 |
50 | export const editPromoEntry = (
51 | promoId,
52 | {
53 | image = "",
54 | name = "",
55 | product_id = "",
56 | desc = "",
57 | discount = "",
58 | coupon_code = "",
59 | start_date = "",
60 | end_date = "",
61 | },
62 | token,
63 | controller
64 | ) => {
65 | const bodyForm = new FormData();
66 | bodyForm.append("image", image);
67 | bodyForm.append("name", name);
68 | bodyForm.append("desc", desc);
69 | bodyForm.append("discount", discount);
70 | bodyForm.append("product_id", product_id);
71 | bodyForm.append("coupon_code", coupon_code);
72 | bodyForm.append("start_date", JSON.stringify(start_date));
73 | bodyForm.append("end_date", JSON.stringify(end_date));
74 |
75 | return api.patch(`/apiv1/promo/${promoId}`, bodyForm, {
76 | signal: controller.signal,
77 | headers: { Authorization: `Bearer ${token}` },
78 | });
79 | };
80 |
81 | export const getPromoById = (promoId, controller) => {
82 | return api.get(`/apiv1/promo/${promoId}`, { signal: controller.signal });
83 | };
84 |
85 | export const deletePromoEntry = (promoId, token, controller) => {
86 | return api.delete(`/apiv1/promo/${promoId}`, {
87 | headers: {
88 | Authorization: `Bearer ${token}`,
89 | },
90 | signal: controller.signal,
91 | });
92 | };
93 |
--------------------------------------------------------------------------------
/src/utils/dataProvider/products.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | import api from "./base";
4 |
5 | const host = process.env.REACT_APP_BACKEND_HOST;
6 |
7 | export function getAllProducts(
8 | catId = "",
9 | { orderBy = "", sort = "", searchByName = "", limit = "10", page = "1" },
10 | controller
11 | ) {
12 | const params = { orderBy, sort, searchByName, limit, page };
13 | const url = `${host}/apiv1/products?category=${catId}`;
14 |
15 | return axios.get(url, { params, signal: controller.signal });
16 | }
17 |
18 | export function getProductbyId(productId, controller) {
19 | const url = `${host}/apiv1/products/${productId}`;
20 |
21 | return axios.get(url, {
22 | signal: controller.signal,
23 | });
24 | }
25 |
26 | export const createProductEntry = (
27 | { name = "", price = "", category_id = "", desc = "", image = "" },
28 | token,
29 | controller
30 | ) => {
31 | const bodyForm = new FormData();
32 | bodyForm.append("image", image);
33 | bodyForm.append("name", name);
34 | bodyForm.append("category_id", category_id);
35 | bodyForm.append("desc", desc);
36 | bodyForm.append("price", price);
37 |
38 | // const body = {
39 | // name,
40 | // price,
41 | // category_id,
42 | // desc,
43 | // image,
44 | // };
45 | // console.log(image);
46 | return api.post("/apiv1/products", bodyForm, {
47 | headers: {
48 | Authorization: `Bearer ${token}`,
49 | "Content-Type": "multipart/form-data",
50 | },
51 | signal: controller.signal,
52 | });
53 | };
54 |
55 | export const editProductEntry = (
56 | { name = "", price = "", category_id = "", desc = "", image = "" },
57 | productId,
58 | token,
59 | controller
60 | ) => {
61 | const bodyForm = new FormData();
62 | if (image?.uri && image?.uri !== "") bodyForm.append("image", image);
63 | bodyForm.append("name", name);
64 | bodyForm.append("category_id", category_id);
65 | bodyForm.append("desc", desc);
66 | bodyForm.append("price", price);
67 |
68 | // const body = {
69 | // name,
70 | // price,
71 | // category_id,
72 | // desc,
73 | // image,
74 | // };
75 | return api.patch(`/apiv1/products/${productId}`, bodyForm, {
76 | headers: {
77 | Authorization: `Bearer ${token}`,
78 | "Content-Type": "multipart/form-data",
79 | },
80 | signal: controller.signal,
81 | });
82 | };
83 |
84 | export const deleteProductEntry = (productId, token, controller) => {
85 | return api.delete(`/apiv1/products/${productId}`, {
86 | headers: {
87 | Authorization: `Bearer ${token}`,
88 | },
89 | signal: controller.signal,
90 | });
91 | };
92 |
--------------------------------------------------------------------------------
/src/redux/slices/profile.slice.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
2 |
3 | import { getProfile } from "../../utils/dataProvider/profile";
4 |
5 | const initialState = {
6 | data: {
7 | user_id: 0,
8 | display_name: null,
9 | first_name: null,
10 | last_name: null,
11 | address: null,
12 | birthdate: null,
13 | img: null,
14 | created_at: "",
15 | email: "",
16 | phone_number: "",
17 | },
18 | isLoading: false,
19 | isRejected: false,
20 | isFulfilled: false,
21 | err: null,
22 | };
23 |
24 | const getProfileThunk = createAsyncThunk(
25 | "profile/get",
26 | async (payload, { fulfillWithValue, rejectWithValue }) => {
27 | try {
28 | const { controller, token } = payload;
29 | const response = await getProfile(token, controller);
30 | // console.log(response.data.data);
31 | if (response.status !== "200") {
32 | rejectWithValue(response.data.msg);
33 | }
34 | fulfillWithValue(response.data.data[0]);
35 | return response.data.data[0];
36 | } catch (err) {
37 | console.log(err);
38 | // store.dispatch(authAction.dismissAuth());
39 | rejectWithValue(err.message);
40 | return;
41 | }
42 | }
43 | );
44 |
45 | const profileSlice = createSlice({
46 | name: "profile",
47 | initialState,
48 | reducers: {
49 | reset: (prevState) => {
50 | return {
51 | data: {
52 | user_id: 0,
53 | display_name: null,
54 | first_name: null,
55 | last_name: null,
56 | address: null,
57 | birthdate: null,
58 | img: null,
59 | created_at: "",
60 | email: "",
61 | phone_number: "",
62 | },
63 | isLoading: false,
64 | isRejected: false,
65 | isFulfilled: false,
66 | err: null,
67 | };
68 | },
69 | },
70 | extraReducers: (builder) => {
71 | builder
72 | .addCase(getProfileThunk.pending, (prevState) => {
73 | return {
74 | ...prevState,
75 | isLoading: true,
76 | isRejected: false,
77 | isFulfilled: false,
78 | };
79 | })
80 | .addCase(getProfileThunk.fulfilled, (prevState, action) => {
81 | return {
82 | ...prevState,
83 | isLoading: false,
84 | isFulfilled: true,
85 | data: action.payload,
86 | };
87 | })
88 | .addCase(getProfileThunk.rejected, (prevState, action) => {
89 | return {
90 | ...prevState,
91 | isLoading: false,
92 | isRejected: true,
93 | err: action.payload,
94 | };
95 | });
96 | },
97 | });
98 | export const profileAction = { ...profileSlice.actions, getProfileThunk };
99 | export default profileSlice.reducer;
100 |
--------------------------------------------------------------------------------
/src/assets/images/partners/amazon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/redux/slices/cart.slice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | list: [],
5 | payment_id: "",
6 | delivery_id: "",
7 | delivery_address: "",
8 | notes: "",
9 | now: "",
10 | phone_number: "",
11 | };
12 |
13 | const cartSlice = createSlice({
14 | name: "cart",
15 | initialState,
16 | reducers: {
17 | addtoCart: (prevState, action) => {
18 | const exsistIdx = prevState.list.findIndex(
19 | (item) =>
20 | item.product_id === action.payload.product_id &&
21 | item.size_id === action.payload.size_id
22 | );
23 |
24 | const updatedItem = {
25 | ...action.payload,
26 | qty:
27 | exsistIdx !== -1
28 | ? prevState.list[exsistIdx].qty + action.payload.qty
29 | : action.payload.qty,
30 | subtotal:
31 | exsistIdx !== -1
32 | ? prevState.list[exsistIdx].subtotal + action.payload.subtotal
33 | : action.payload.subtotal,
34 | };
35 |
36 | const updatedCart =
37 | exsistIdx !== -1
38 | ? [
39 | ...prevState.list.slice(0, exsistIdx),
40 | updatedItem,
41 | ...prevState.list.slice(exsistIdx + 1),
42 | ]
43 | : [...prevState.list, updatedItem];
44 |
45 | return {
46 | ...prevState,
47 | list: updatedCart,
48 | };
49 | },
50 | incrementQty: (prevState, action) => {
51 | return {
52 | ...prevState,
53 | list: prevState.list.map((item) => {
54 | if (
55 | item.product_id === action.payload.product_id &&
56 | item.size_id === action.payload.size_id
57 | ) {
58 | return {
59 | ...item,
60 | qty: item.qty + 1,
61 | subtotal: item.subtotal + item.price,
62 | };
63 | }
64 | return item;
65 | }),
66 | };
67 | },
68 | decrementQty: (prevState, action) => {
69 | return {
70 | ...prevState,
71 | list: prevState.list.map((item) => {
72 | if (
73 | item.product_id === action.payload.product_id &&
74 | item.size_id === action.payload.size_id
75 | ) {
76 | if (item.qty === 1) {
77 | return item;
78 | }
79 | return {
80 | ...item,
81 | qty: item.qty - 1,
82 | subtotal: item.subtotal + item.price,
83 | };
84 | }
85 | return item;
86 | }),
87 | };
88 | },
89 | removeFromCart: (prevState, action) => {
90 | return {
91 | ...prevState,
92 | list: prevState.list.filter((item) => {
93 | return !(
94 | item.product_id === action.payload.product_id &&
95 | item.size_id === action.payload.size_id
96 | );
97 | }),
98 | };
99 | },
100 | resetCart: (prevState, action) => {
101 | return { ...prevState, ...initialState };
102 | },
103 | setDelivery: (prevState, action) => {
104 | return {
105 | ...prevState,
106 | ...action.payload,
107 | };
108 | },
109 | },
110 | });
111 |
112 | export const cartActions = cartSlice.actions;
113 | export default cartSlice.reducer;
114 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { BrowserRouter, Route, Routes } from "react-router-dom";
4 |
5 | import AdminDashboard from "./pages/Admin";
6 | import ManageOrder from "./pages/Admin/ManageOrder";
7 | import Auth from "./pages/Auth";
8 | import ForgotPass from "./pages/Auth/ForgotPass";
9 | import Login from "./pages/Auth/Login";
10 | import Register from "./pages/Auth/Register";
11 | import ResetPass from "./pages/Auth/ResetPass";
12 | import Cart from "./pages/Cart";
13 | import NotFound from "./pages/Error";
14 | import History from "./pages/History";
15 | import HistoryDetail from "./pages/History/HistoryDetail";
16 | import Mainpage from "./pages/Mainpage";
17 | import Products from "./pages/Products";
18 | import EditProduct from "./pages/Products/EditProduct";
19 | import NewProduct from "./pages/Products/NewProduct";
20 | import ProductDetail from "./pages/Products/ProductDetail";
21 | import Profile from "./pages/Profile";
22 | import EditPromo from "./pages/Promo/EditPromo";
23 | import NewPromo from "./pages/Promo/NewPromo";
24 | import ScrollToTop from "./utils/scrollToTop";
25 | import {
26 | CheckAuth,
27 | CheckIsAdmin,
28 | CheckNoAuth,
29 | TokenHandler,
30 | } from "./utils/wrappers/protectedRoute";
31 |
32 | // const AllRouter = createBrowserRouter(createRoutesFromElements());
33 |
34 | const Routers = () => {
35 | return (
36 |
37 |
38 |
39 | }>
40 | {/* Public Route */}
41 | } />
42 | } />
43 | }>
44 |
45 |
46 | }
49 | />
50 | } />
51 |
52 | {/* Route which must not logged in */}
53 |
57 |
58 |
59 | }
60 | >
61 | } />
62 | } />
63 | } />
64 | } />
65 | } />
66 |
67 |
68 | {/* Route which must logged in */}
69 | }>
70 | } />
71 | } />
72 | } />
73 |
74 |
75 | {/* Route which only admin */}
76 | }>
77 | } />
78 | } />
79 | } />
80 | } />
81 | } />
82 | } />
83 |
84 |
85 |
86 |
87 | );
88 | };
89 |
90 | export default Routers;
91 |
--------------------------------------------------------------------------------
/src/assets/images/partners/reddit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 | [](#tech-stack)
5 |
6 |
jokopi - Example App
7 |
8 | Open Source. Front-end.
9 |
10 |
11 | [Demo](https://jokopi-react.vercel.app/) · [Related Projects](#related-projects) · [Request Feature](#report-bug)
12 |
13 |
14 |
15 | ## Table of Contents
16 |
17 | - [Overview](#overview)
18 | - [Features](#features)
19 | - [Tech Stack](#tech-stack)
20 | - [Getting Started](#getting-started)
21 | - [Prerequisites](#prerequisites)
22 | - [Installation](#installation)
23 | - [Screenshots](#screenshots)
24 | - [Resources](#resources)
25 | - [Related Projects](#related-projects)
26 | - [License](#license)
27 | - [Report Bug](#report-bug)
28 |
29 | ## Overview
30 |
31 | jokopi is a complete open-source app coffee shop ordering.
32 |
33 | This is an example application that shows how `jokopi` is applied to a react app.
34 |
35 | Build using create react app.
36 |
37 | You can build it by yourself
38 |
39 | ### Features
40 |
41 | - Login, Register, Forgot Password, Logout
42 | - Profile
43 | - History Order
44 | - Products (Search, Sort, Filter)
45 | - Cart
46 | - Transactions
47 | - Admin Dashboard
48 | - Manage Order (Admin)
49 | - etc.
50 |
51 | ## Tech Stack
52 |
53 | - [React](https://react.dev/) & [React Router DOM](https://reactrouter.com/en/main)
54 | - [Redux](https://redux.js.org/) & [Redux Persist](https://www.npmjs.com/package/redux-persist) (Local Storage)
55 | - [TailwindCSS](https://tailwindcss.com/) & [DaisyUI](https://daisyui.com/)
56 | - [React Hot Toast](https://www.npmjs.com/package/react-hot-toast)
57 | - [Vercel](https://vercel.com/dashboard) for deploying demo
58 | - etc.
59 |
60 | ## Getting Started
61 |
62 | ### Prerequisites
63 |
64 | You need to install some software to run this project
65 |
66 | - [Node.js](https://nodejs.org/en/download) (LTS version recommended, 14 or newer)
67 |
68 | ### Installation
69 |
70 | 1. Clone this repository to your local
71 |
72 | ```bash
73 | git clone https://github.com/nyannss/jokopi-react.git
74 | ```
75 |
76 | 2. Change current directory
77 |
78 | ```bash
79 | cd jokopi-react
80 | ```
81 |
82 | 3. Install dependencies
83 |
84 | If you using npm
85 |
86 | ```bash
87 | npm install
88 | ```
89 |
90 | If you using yarn
91 |
92 | ```bash
93 | yarn
94 | ```
95 |
96 | 4. Setup environment
97 |
98 | ```env
99 | REACT_APP_BACKEND_HOST = (your rest api host)
100 | REACT_APP_WEBSITE_NAME = (your project name)
101 | ```
102 |
103 | 5. Running app
104 |
105 | ```bash
106 | npm start
107 | ```
108 |
109 | ## Screenshots
110 |
111 |
112 |

113 |

114 |

115 |

116 |

117 |
118 |
119 | ## Resources
120 |
121 | Special thanks for providing resources such as icons and images.
122 |
123 | - [Flaticon](https://flaticon.com/)
124 | - [unDraw](https://undraw.co/)
125 | - [SVGRepo](https://svgrepo.com/)
126 | - and other sources.
127 |
128 | If there are resources that belong to you, please let me know, I will write it here.
129 |
130 | ## Related Projects
131 |
132 | - [jokopi-express](https://github.com/nyannss/jokopi) - Rest API
133 | - [jokopi-react-native](https://github.com/nyannss/jokopi-react-native) - Android & iOS Application
134 |
135 | ## License
136 |
137 | This project is licensed under the ISC License. See the [LICENSE](LICENSE) file for details.
138 |
139 | ## Report Bug
140 |
141 | Any error report you can pull request
142 | or contact:
143 |
--------------------------------------------------------------------------------
/src/assets/images/partners/spotify.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/pages/Auth/ForgotPass.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | import { now } from "lodash";
4 | import toast from "react-hot-toast";
5 | import { Link } from "react-router-dom";
6 |
7 | import icon from "../../assets/jokopi.svg";
8 | import { forgotPass } from "../../utils/dataProvider/auth";
9 | import useDocumentTitle from "../../utils/documentTitle";
10 |
11 | const ForgotPass = () => {
12 | useDocumentTitle("Forgot Password");
13 |
14 | const controller = React.useMemo(() => new AbortController(), []);
15 | const [isLoading, setIsLoading] = useState(false);
16 | const [email, setEmail] = React.useState("");
17 | const [error, setError] = useState("");
18 | const [resend, setResend] = useState(0);
19 | const [time, setTime] = useState(0);
20 | const [displaycd, setDisplaycd] = useState("");
21 |
22 | function forgotPassHandler(e) {
23 | e.preventDefault(); // preventing default submit
24 | toast.dismiss(); // dismiss all toast
25 |
26 | setResend(now() + 2 * 60 * 1000); // now + 2 minutes
27 | let err = "";
28 | if (email.length < 1) {
29 | err = "Must input email!";
30 | }
31 | setError(err);
32 | if (!isLoading && err.length < 1) {
33 | setIsLoading(true);
34 | e.target.disabled = true;
35 | toast.promise(
36 | forgotPass(email, controller).then((res) => {
37 | // console.log(res.data.data.token);
38 | setResend(now() + 2 * 60 * 1000); // now + 2 minutes
39 | e.target.disabled = false;
40 | setIsLoading(false);
41 | return res.data;
42 | }),
43 | {
44 | loading: "Please wait a moment",
45 | success: "We sent a code to your email!",
46 | error: (err) => {
47 | e.target.disabled = false;
48 | setIsLoading(false);
49 | return err.response.data.msg;
50 | },
51 | }
52 | );
53 | }
54 | }
55 |
56 | function countdownFormat(ms) {
57 | const time = new Date(ms).toISOString().substr(14, 5);
58 | const timeFormat = time.replace(":", ".");
59 | return timeFormat;
60 | }
61 |
62 | useEffect(() => {
63 | const interval = setInterval(() => {
64 | if (resend > 0 && resend > now()) {
65 | const newSec = resend - 1000;
66 | setResend(newSec);
67 | setDisplaycd(countdownFormat(newSec - now()));
68 | }
69 | }, 1000);
70 | return () => clearInterval(interval);
71 | }, []);
72 |
73 | function handleChange(e) {
74 | return setEmail(e.target.value);
75 | }
76 |
77 | return (
78 | <>
79 |
80 |
81 |
82 |

83 |
jokopi.
84 |
85 |
86 |
87 |
88 |
126 | {resend >= now() ? (
127 |
128 | Click here if you didn’t receive any link in 2 minutes
129 | {displaycd}
130 |
136 |
137 | ) : (
138 | ""
139 | )}
140 |
141 | >
142 | );
143 | };
144 |
145 | export default ForgotPass;
146 |
--------------------------------------------------------------------------------
/src/pages/Profile/EditPassword.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { isEqual } from 'lodash';
4 | import toast from 'react-hot-toast';
5 | import { useSelector } from 'react-redux';
6 |
7 | import Modal from '../../components/Modal';
8 | import { updatePassword } from '../../utils/dataProvider/userPanel';
9 |
10 | function EditPassword(props) {
11 | const userInfo = useSelector((state) => state.userInfo);
12 | const [form, setForm] = useState({
13 | oldpass: "",
14 | newpass: "",
15 | newpassconf: "",
16 | });
17 | const [err, setErr] = useState({
18 | oldpass: "",
19 | newpass: "",
20 | newpassconf: "",
21 | });
22 |
23 | const formHandler = (e) => {
24 | return setForm((form) => {
25 | return {
26 | ...form,
27 | [e.target.name]: e.target.value,
28 | };
29 | });
30 | };
31 |
32 | const submitFormHandler = (e) => {
33 | e.preventDefault();
34 | const error = {
35 | oldpass: "",
36 | newpass: "",
37 | newpassconf: "",
38 | };
39 | setErr(error);
40 | if (form.oldpass.length < 1) {
41 | error.oldpass = "Required";
42 | }
43 | if (form.newpass.length < 1) {
44 | error.newpass = "Required";
45 | }
46 | if (form.newpassconf.length < 1) {
47 | error.newpassconf = "Required";
48 | }
49 | if (form.newpass.length < 8)
50 | error.newpass = "New password length minimum is 8";
51 | if (!isEqual(form.newpass, form.newpassconf))
52 | error.newpassconf = "Password and confirm password does not match";
53 | setErr(error);
54 |
55 | if (
56 | error.oldpass === "" &&
57 | error.newpass === "" &&
58 | error.newpassconf === ""
59 | ) {
60 | e.target.disabled = true;
61 | toast.promise(
62 | updatePassword(form.oldpass, form.newpass, userInfo.token).then(
63 | (res) => {
64 | return res;
65 | }
66 | ),
67 | {
68 | loading: "Please wait",
69 | success: () => {
70 | e.target.disabled = false;
71 | props.onClose();
72 | return "Edit password successful";
73 | },
74 | error: (err) => {
75 | e.target.disabled = false;
76 | return err.response.data.msg;
77 | },
78 | }
79 | );
80 | }
81 | };
82 |
83 | return (
84 |
85 |
162 |
163 | );
164 | }
165 |
166 | export default EditPassword;
167 |
--------------------------------------------------------------------------------
/src/components/AuthFooter.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Link } from 'react-router-dom';
4 |
5 | import fbLogo from '../assets/icons/facebook.svg';
6 | import igLogo from '../assets/icons/instagram.svg';
7 | import twLogo from '../assets/icons/twitter.svg';
8 | import logo from '../assets/jokopi.svg';
9 |
10 | const AuthFooter = () => {
11 | return (
12 | <>
13 |
124 | >
125 | );
126 | };
127 |
128 | export default AuthFooter;
129 |
--------------------------------------------------------------------------------
/src/pages/Auth/ResetPass.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | import toast from "react-hot-toast";
4 | import { Link, useNavigate, useSearchParams } from "react-router-dom";
5 |
6 | import notfoundImage from "../../assets/images/empty-box.svg";
7 | import loadingImage from "../../assets/images/loading.svg";
8 | import icon from "../../assets/jokopi.svg";
9 | import { resetPass, verifyResetPass } from "../../utils/dataProvider/auth";
10 |
11 | function ResetPass() {
12 | const [error, setError] = useState("");
13 | const [pass, setPass] = useState("");
14 | const [searchParams, setSearchParams] = useSearchParams();
15 | const [isLoading, setIsLoading] = useState(false);
16 | const [isNotFound, setIsNotFound] = useState(false);
17 | const navigate = useNavigate();
18 |
19 | function handleChange(e) {
20 | return setPass(e.target.value);
21 | }
22 |
23 | useEffect(() => {
24 | setIsLoading(true);
25 | const controller = new AbortController();
26 | verifyResetPass(
27 | searchParams.get("verify"),
28 | searchParams.get("code"),
29 | controller
30 | )
31 | .then((res) => {
32 | setIsLoading(false);
33 | })
34 | .catch((res) => {
35 | setIsLoading(false);
36 | setIsNotFound(true);
37 | });
38 | }, []);
39 | const Loading = (props) => {
40 | return (
41 |
42 |
43 |

44 |
45 |
46 | );
47 | };
48 | const NotFound = (props) => {
49 | return (
50 |
51 |
52 | The link has expired
53 |
54 | );
55 | };
56 |
57 | function resetPassHandler(e) {
58 | e.preventDefault(); // preventing default submit
59 | toast.dismiss(); // dismiss all toast
60 |
61 | let err = "";
62 | if (pass.length < 8) {
63 | err = "Password must be 8 character or higher";
64 | }
65 | if (!/^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]+$/.test(pass)) {
66 | err = "Password must alphanumeric";
67 | }
68 | setError(err);
69 | if (!isLoading && err.length < 1) {
70 | setIsLoading(true);
71 | e.target.disabled = true;
72 | const controller = new AbortController();
73 | toast.promise(
74 | resetPass(
75 | searchParams.get("verify"),
76 | searchParams.get("code"),
77 | pass,
78 | controller
79 | ).then((res) => {
80 | // console.log(res.data.data.token);
81 | // setResend(now() + 2 * 60 * 1000); // now + 2 minutes
82 | e.target.disabled = false;
83 | setIsLoading(false);
84 | navigate("/auth/login", { replace: true });
85 | return res.data;
86 | }),
87 | {
88 | loading: "Please wait a moment",
89 | success: "The new password has been set successfully",
90 | error: (err) => {
91 | e.target.disabled = false;
92 | setIsLoading(false);
93 | if (err.response) return err.response?.data?.msg;
94 | return err.message;
95 | },
96 | }
97 | );
98 | }
99 | }
100 | return (
101 | <>
102 |
103 |
104 |
105 |

106 |
jokopi.
107 |
108 |
109 |
110 | {isNotFound ? (
111 |
112 | ) : isLoading ? (
113 |
114 | ) : (
115 |
159 | )}
160 | >
161 | );
162 | }
163 |
164 | export default ResetPass;
165 |
--------------------------------------------------------------------------------
/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { Link } from 'react-router-dom';
4 |
5 | import fbLogo from '../assets/icons/facebook.svg';
6 | import igLogo from '../assets/icons/instagram.svg';
7 | import twLogo from '../assets/icons/twitter.svg';
8 | import logo from '../assets/jokopi.svg';
9 |
10 | class Footer extends Component {
11 | render() {
12 | return (
13 |
161 | );
162 | }
163 | }
164 |
165 | export default Footer;
166 |
--------------------------------------------------------------------------------
/src/assets/images/not_found.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { NavLink, useNavigate } from "react-router-dom";
5 |
6 | import placeholderProfile from "../assets/images/placeholder-profile.jpg";
7 | import { contextAct } from "../redux/slices/context.slice";
8 |
9 | function Sidebar({ onClose }) {
10 | const profile = useSelector((state) => state.profile);
11 | const userInfo = useSelector((state) => state.userInfo);
12 | const navigate = useNavigate();
13 | const dispatch = useDispatch();
14 |
15 | return (
16 | <>
17 |
18 |
19 |
userInfo.token && navigate("/profile")}
24 | >
25 |
26 |
27 |

34 |
35 |
36 |
37 | {userInfo.token ? (
38 | <>
39 |
{profile.data?.display_name}
40 |
{profile.data?.email}
41 | >
42 | ) : (
43 | <>
44 |
Guest
45 |
46 | {/*
47 | Login
48 | {" "}
49 | or Sign Up */}
50 |
51 | >
52 | )}
53 |
54 | {/*
55 |
61 | */}
62 |
84 |
85 |
86 |
87 | -
88 |
92 | Home
93 |
94 |
95 | -
96 |
100 | Products
101 |
102 |
103 | -
104 |
108 | Your Cart
109 |
110 |
111 | -
112 |
116 | History
117 |
118 |
119 | {Number(userInfo.role) > 1 && (
120 | <>
121 | -
122 |
126 | Admin Dashboard
127 |
128 |
129 | -
130 |
134 | Manage Order
135 |
136 |
137 | >
138 | )}
139 |
140 |
141 |
142 | {userInfo.token ? (
143 |
150 | ) : (
151 |
152 |
156 | Login
157 |
158 |
162 | Sign Up
163 |
164 |
165 | )}
166 |
167 | Copyright © 2023
168 |
169 |
170 |
171 |
172 | >
173 | );
174 | }
175 |
176 | export default Sidebar;
177 |
--------------------------------------------------------------------------------
/src/pages/Products/NewProduct.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useState } from "react";
2 |
3 | import { toast } from "react-hot-toast";
4 | import { connect } from "react-redux";
5 | import { NavLink, useNavigate } from "react-router-dom";
6 |
7 | import productPlaceholder from "../../assets/images/placeholder-image.webp";
8 | import Footer from "../../components/Footer";
9 | import Header from "../../components/Header";
10 | import Modal from "../../components/Modal";
11 | import { createProductEntry } from "../../utils/dataProvider/products";
12 | import useDocumentTitle from "../../utils/documentTitle";
13 |
14 | export const NewProduct = (props) => {
15 | useDocumentTitle("New Product");
16 | const initialState = {
17 | name: "",
18 | price: "",
19 | category_id: "",
20 | desc: "",
21 | image: "",
22 | };
23 | const [form, setForm] = useState({
24 | name: "",
25 | price: "",
26 | category_id: "",
27 | desc: "",
28 | image: "",
29 | });
30 | const [error, setError] = useState({
31 | name: "",
32 | price: "",
33 | category_id: "",
34 | desc: "",
35 | });
36 | const navigate = useNavigate();
37 | const [preview, setPreview] = useState("");
38 | const [cancel, setCancel] = useState(false);
39 |
40 | // create a preview as a side effect, whenever selected file is changed
41 | useEffect(() => {
42 | if (!form.image) {
43 | setPreview(undefined);
44 | return;
45 | }
46 |
47 | const objectUrl = URL.createObjectURL(form.image);
48 | setPreview(objectUrl);
49 |
50 | // free memory when ever this component is unmounted
51 | return () => URL.revokeObjectURL(objectUrl);
52 | }, [form.image]);
53 |
54 | const onSelectFile = (e) => {
55 | if (!e.target.files || e.target.files.length === 0) {
56 | setForm({ ...form, image: "" });
57 | return;
58 | }
59 |
60 | if (e.target.files[0].size > 2097152) {
61 | return toast.error("Files must not exceed 2 MB");
62 | }
63 |
64 | // I've kept this example simple by using the first image instead of multiple
65 | setForm({ ...form, image: e.target.files[0] });
66 | };
67 |
68 | const [isLoading, setLoading] = useState("");
69 | const controller = useMemo(() => new AbortController(), []);
70 | const formChangeHandler = (e) =>
71 | setForm({ ...form, [e.target.name]: e.target.value });
72 |
73 | const submitHandler = (e) => {
74 | e.preventDefault();
75 |
76 | if (
77 | form.category_id === "" ||
78 | form.desc === "" ||
79 | form.name === "" ||
80 | form.price === ""
81 | ) {
82 | return toast.error("Input required form");
83 | }
84 |
85 | setLoading(true);
86 | createProductEntry(form, props.userInfo.token, controller)
87 | .then((result) => {
88 | console.log(result.data);
89 | navigate(`/products/detail/${result.data.data[0].id}`, {
90 | replace: true,
91 | });
92 | toast.success("Product added successfully");
93 | })
94 | .catch((err) => {
95 | toast.error(err.message);
96 | })
97 | .finally(() => setLoading(false));
98 | };
99 | return (
100 | <>
101 | setCancel(!cancel)}>
102 | Are you sure want to reset the form?
103 |
104 |
113 |
116 |
117 |
118 |
119 |
120 |
126 |
127 |
128 |
129 |
130 |

131 |
132 |
133 |
139 |
145 |
146 |
247 |
248 |
249 |
250 | >
251 | );
252 | };
253 |
254 | const mapStateToProps = (state) => ({
255 | userInfo: state.userInfo,
256 | });
257 |
258 | const mapDispatchToProps = {};
259 |
260 | export default connect(mapStateToProps, mapDispatchToProps)(NewProduct);
261 |
--------------------------------------------------------------------------------
/src/pages/Auth/Register.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { toast } from "react-hot-toast";
4 | import { Link, useNavigate } from "react-router-dom";
5 |
6 | import icon from "../../assets/jokopi.svg";
7 | import { register } from "../../utils/dataProvider/auth";
8 | import useDocumentTitle from "../../utils/documentTitle";
9 |
10 | const Register = () => {
11 | useDocumentTitle("Register");
12 |
13 | const controller = React.useMemo(() => new AbortController(), []);
14 | const navigate = useNavigate();
15 | const [isLoading, setIsLoading] = useState(false);
16 | const [form, setForm] = React.useState({
17 | email: "",
18 | password: "",
19 | phoneNumber: "",
20 | });
21 |
22 | const [error, setError] = React.useState({
23 | email: "",
24 | password: "",
25 | phoneNumber: "",
26 | });
27 |
28 | function registerHandler(e) {
29 | e.preventDefault(); // preventing default submit
30 | toast.dismiss(); // dismiss all toast notification
31 |
32 | const valid = { email: "", password: "", phoneNumber: "" };
33 | const emailRegex =
34 | /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/g;
35 | const passRegex = /^(?=.*[0-9])(?=.*[a-z]).{8,}$/g;
36 | const phoneRegex =
37 | /^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/g;
38 |
39 | // email validation
40 | if (!form.email) valid.email = "Input your email address";
41 | else if (!form.email.match(emailRegex))
42 | valid.email = "Invalid email address";
43 |
44 | // password validation
45 | if (!form.password) valid.password = "Input your password";
46 | else if (form.password.length < 8)
47 | valid.password = "Password length minimum is 8";
48 | else if (!form.password.match(passRegex))
49 | valid.password = "Password must be combination alphanumeric";
50 |
51 | // phone validation
52 | if (!form.phoneNumber) valid.phoneNumber = "Input your phone number";
53 | else if (!form.phoneNumber.match(phoneRegex))
54 | valid.phoneNumber = "Invalid phone number";
55 |
56 | setError({
57 | email: valid.email,
58 | password: valid.password,
59 | phoneNumber: valid.phoneNumber,
60 | });
61 |
62 | if (valid.email == "" && valid.password == "" && valid.phoneNumber == "") {
63 | setIsLoading(true);
64 | e.target.disabled = true;
65 | toast.promise(
66 | register(form.email, form.password, form.phoneNumber, controller).then(
67 | (res) => {
68 | e.target.disabled = false;
69 | setIsLoading(false);
70 | return res.data.msg;
71 | }
72 | ),
73 | {
74 | loading: "Please wait a moment",
75 | success: () => {
76 | navigate("/auth/login", {
77 | replace: true,
78 | });
79 | return "Register successful! You can login now";
80 | },
81 | error: ({ response }) => {
82 | setIsLoading(false);
83 | e.target.disabled = false;
84 |
85 | return response.data.msg;
86 | },
87 | },
88 | { success: { duration: Infinity }, error: { duration: Infinity } }
89 | );
90 | }
91 | }
92 |
93 | function onChangeForm(e) {
94 | return setForm((form) => {
95 | return {
96 | ...form,
97 | [e.target.name]: e.target.value,
98 | };
99 | });
100 | }
101 |
102 | return (
103 | <>
104 |
105 |
106 |
107 |

108 |
jokopi.
109 |
110 |
111 | Login
112 |
113 |
248 | >
249 | );
250 | };
251 |
252 | export default Register;
253 |
--------------------------------------------------------------------------------
/src/pages/Auth/Login.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import jwtDecode from "jwt-decode";
4 | import toast from "react-hot-toast";
5 | import { useDispatch } from "react-redux";
6 | import { Link, useNavigate } from "react-router-dom";
7 |
8 | import icon from "../../assets/jokopi.svg";
9 | import { profileAction } from "../../redux/slices/profile.slice";
10 | import { uinfoAct } from "../../redux/slices/userInfo.slice";
11 | import { login } from "../../utils/dataProvider/auth";
12 | import useDocumentTitle from "../../utils/documentTitle";
13 |
14 | const Login = () => {
15 | const navigate = useNavigate();
16 | useDocumentTitle("Login");
17 |
18 | const controller = React.useMemo(() => new AbortController(), []);
19 | const [form, setForm] = React.useState({
20 | email: "",
21 | password: "",
22 | rememberMe: false,
23 | });
24 | const [error, setError] = React.useState({
25 | email: "",
26 | password: "",
27 | });
28 | const [isLoading, setIsLoading] = useState(false);
29 | const dispatch = useDispatch();
30 |
31 | function loginHandler(e) {
32 | e.preventDefault(); // preventing default submit
33 | toast.dismiss(); // dismiss all toast
34 | const valid = { email: "", password: "" };
35 |
36 | if (!form.email) valid.email = "Input your email address";
37 | if (!form.password) valid.password = "Input your password";
38 |
39 | setError({
40 | email: valid.email,
41 | password: valid.password,
42 | });
43 |
44 | if (valid.email == "" && valid.password == "" && !isLoading) {
45 | setIsLoading(true);
46 | toast.promise(
47 | login(form.email, form.password, form.rememberMe, controller).then(
48 | (res) => {
49 | // console.log(res.data);
50 | // console.log(res.data.data.token);
51 | dispatch(uinfoAct.assignToken(res.data.data.token));
52 | const { role } = jwtDecode(res.data.data.token);
53 | dispatch(uinfoAct.assignData({ role }));
54 | dispatch(
55 | profileAction.getProfileThunk({
56 | controller,
57 | token: res.data.data.token,
58 | })
59 | );
60 | return res.data.data.token;
61 | }
62 | ),
63 | {
64 | loading: () => {
65 | e.target.disabled = true;
66 | return "Please wait a moment";
67 | },
68 | success: () => {
69 | navigate("/products");
70 | toast.success("Welcome to jokopi!\nYou can order for now!", {
71 | icon: "👋",
72 | duration: Infinity,
73 | }); // add toast welcome
74 | return (
75 | <>
76 | Login successful!
77 |
Redirecting you
78 | >
79 | );
80 | },
81 | error: () => {
82 | setIsLoading(false);
83 | e.target.disabled = false;
84 | return "Incorrect email or password";
85 | },
86 | }
87 | );
88 | }
89 | }
90 |
91 | function onChangeForm(e) {
92 | return setForm((form) => {
93 | return {
94 | ...form,
95 | [e.target.name]: e.target.value,
96 | };
97 | });
98 | }
99 |
100 | function onCheck(e) {
101 | return setForm((form) => {
102 | return {
103 | ...form,
104 | [e.target.name]: !form[e.target.name],
105 | };
106 | });
107 | }
108 |
109 | return (
110 | <>
111 |
112 |
113 |
114 |

115 |
jokopi.
116 |
117 |
118 |
119 | Login
120 |
121 |
122 |
123 |
261 |
262 | >
263 | );
264 | };
265 |
266 | export default Login;
267 |
--------------------------------------------------------------------------------
/src/pages/Products/EditProduct.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | import { isEqual } from "lodash";
4 | import { toast } from "react-hot-toast";
5 | import { connect } from "react-redux";
6 | import { NavLink, useNavigate, useParams } from "react-router-dom";
7 |
8 | import productPlaceholder from "../../assets/images/placeholder-image.webp";
9 | import Footer from "../../components/Footer";
10 | import Header from "../../components/Header";
11 | import Loading from "../../components/Loading";
12 | import Modal from "../../components/Modal";
13 | import DeleteProduct from "../../components/Product/DeleteProduct";
14 | import ProductNotFound from "../../components/Product/ProductNotFound";
15 | import {
16 | editProductEntry,
17 | getProductbyId,
18 | } from "../../utils/dataProvider/products";
19 | import useDocumentTitle from "../../utils/documentTitle";
20 |
21 | export const EditProduct = (props) => {
22 | useDocumentTitle("Edit Product");
23 |
24 | /// states
25 | const initialState = {
26 | name: "",
27 | price: "",
28 | category_id: "",
29 | desc: "",
30 | image: "",
31 | };
32 | const [form, setForm] = useState({
33 | name: "",
34 | price: "",
35 | category_id: "",
36 | desc: "",
37 | image: "",
38 | });
39 | const [data, setData] = useState({
40 | image: "",
41 | });
42 | const [error, setError] = useState({
43 | name: "",
44 | price: "",
45 | category_id: "",
46 | desc: "",
47 | });
48 | const [isLoading, setIsLoading] = useState(true); // load data
49 | const [loading, setLoading] = useState(false); // process patch
50 |
51 | // react router dom
52 | const { productId } = useParams();
53 | const navigate = useNavigate();
54 | const controller = React.useMemo(() => new AbortController(), []);
55 |
56 | const [preview, setPreview] = useState("");
57 | const [cancel, setCancel] = useState(false);
58 | const [notFound, setNotFound] = useState(false);
59 | const [deleteModal, setDeleteModal] = useState(false);
60 |
61 | useEffect(() => {
62 | getProductbyId(productId, controller)
63 | .then((response) => {
64 | setForm(response.data.data[0]);
65 | setData({ ...response.data.data[0] });
66 | setIsLoading(false);
67 | })
68 | .catch((error) => {
69 | setNotFound(true);
70 | // console.log(error);
71 | setIsLoading(false);
72 | });
73 | }, []);
74 |
75 | useEffect(() => {
76 | if (!form.image) {
77 | setPreview(undefined);
78 | return;
79 | }
80 |
81 | const objectUrl = URL.createObjectURL(form.image);
82 | setPreview(objectUrl);
83 |
84 | return () => URL.revokeObjectURL(objectUrl);
85 | }, [form.image]);
86 |
87 | const onSelectFile = (e) => {
88 | if (!e.target.files || e.target.files.length === 0) {
89 | setForm({ ...form, image: "" });
90 | return;
91 | }
92 |
93 | if (e.target.files[0].size > 2097152) {
94 | return toast.error("Files must not exceed 2 MB");
95 | }
96 |
97 | // I've kept this example simple by using the first image instead of multiple
98 | setForm({ ...form, image: e.target.files[0] });
99 | };
100 |
101 | const formChangeHandler = (e) => {
102 | if (e.target.name === "price" && isNaN(e.target.value)) return;
103 | setForm({ ...form, [e.target.name]: e.target.value });
104 | };
105 |
106 | const submitHandler = (e) => {
107 | e.preventDefault();
108 |
109 | if (
110 | form.category_id === "" ||
111 | form.desc === "" ||
112 | form.name === "" ||
113 | form.price === ""
114 | ) {
115 | return toast.error("Input required form");
116 | }
117 |
118 | setLoading(true);
119 | editProductEntry(form, productId, props.userInfo.token, controller)
120 | .then((result) => {
121 | // console.log(result.data);
122 | navigate(`/products/detail/${result.data.data[0].id}`, {
123 | replace: true,
124 | });
125 | toast.success("Product updated successfully");
126 | })
127 | .catch((err) => {
128 | toast.error(err.message);
129 | })
130 | .finally(() => setLoading(false));
131 | };
132 |
133 | const resetHandler = () => {
134 | setForm({ ...data });
135 | setCancel(false);
136 | };
137 |
138 | const disabled = isEqual(form, data);
139 | return (
140 | <>
141 | setCancel(!cancel)}>
142 | Are you sure want to reset the form?
143 |
144 |
147 |
150 |
151 |
152 | setDeleteModal(false)}
155 | productId={productId}
156 | />
157 |
158 |
159 | {isLoading ? (
160 |
161 | ) : notFound ? (
162 |
163 | ) : (
164 |
165 |
171 |
304 |
305 | )}
306 |
307 | >
308 | );
309 | };
310 |
311 | const mapStateToProps = (state) => ({
312 | userInfo: state.userInfo,
313 | });
314 |
315 | const mapDispatchToProps = {};
316 |
317 | export default connect(mapStateToProps, mapDispatchToProps)(EditProduct);
318 |
--------------------------------------------------------------------------------
/src/pages/History/index.jsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useEffect,
3 | useMemo,
4 | useState,
5 | } from 'react';
6 |
7 | import axios from 'axios';
8 | import { useSelector } from 'react-redux';
9 | import { useSearchParams } from 'react-router-dom';
10 |
11 | import loadingImage from '../../assets/images/loading.svg';
12 | import productPlaceholder from '../../assets/images/placeholder-image.webp';
13 | import Footer from '../../components/Footer';
14 | import Header from '../../components/Header';
15 | import Modal from '../../components/Modal';
16 | import {
17 | getTransactionDetail,
18 | getTransactionHistory,
19 | } from '../../utils/dataProvider/transaction';
20 | import useDocumentTitle from '../../utils/documentTitle';
21 | import {
22 | formatDateTime,
23 | n_f,
24 | } from '../../utils/helpers';
25 |
26 | function History() {
27 | const authInfo = useSelector((state) => state.userInfo);
28 | const controller = useMemo(() => new AbortController(), []);
29 | const [searchParams, setSearchParams] = useSearchParams();
30 | const page = searchParams.get("page");
31 | const [isLoading, setIsLoading] = useState(true);
32 | const [listMeta, setListMeta] = useState({
33 | totalData: "0",
34 | perPage: 6,
35 | currentPage: 1,
36 | totalPage: 1,
37 | prev: null,
38 | next: null,
39 | });
40 | const [list, setList] = useState([]);
41 | const [detail, setDetail] = useState("");
42 | const initialValue = {
43 | isLoading: true,
44 | isError: false,
45 | id: 0,
46 | receiver_email: "",
47 | receiver_name: "",
48 | delivery_address: "",
49 | notes: "",
50 | status_id: 0,
51 | status_name: "",
52 | transaction_time: "",
53 | payment_id: 0,
54 | payment_name: "",
55 | payment_fee: 0,
56 | delivery_name: "",
57 | delivery_fee: 0,
58 | grand_total: 0,
59 | products: [],
60 | };
61 | const [dataDetail, setDataDetail] = useState({
62 | ...initialValue,
63 | });
64 | useDocumentTitle("History");
65 | const detailController = useMemo(() => new AbortController(), [detail]);
66 |
67 | const fetchDetail = async () => {
68 | if (detail === "") return;
69 | try {
70 | const result = await getTransactionDetail(
71 | detail,
72 | authInfo.token,
73 | detailController
74 | );
75 | setDataDetail({ isLoading: false, ...result.data.data[0] });
76 | } catch (error) {
77 | if (axios.isCancel(error)) return;
78 | setDataDetail({ ...detail, isLoading: false, isError: true });
79 | console.log(error);
80 | }
81 | };
82 |
83 | useEffect(() => {
84 | if (detail === "") return;
85 | fetchDetail();
86 | return () => {
87 | detailController.abort();
88 | setDataDetail({ ...initialValue });
89 | };
90 | }, [detail]);
91 |
92 | useEffect(() => {
93 | if (page && (page < 1 || isNaN(page))) {
94 | setSearchParams({ page: 1 });
95 | return;
96 | }
97 | window.scrollTo(0, 0);
98 |
99 | setIsLoading(true);
100 | getTransactionHistory({ page: page || 1 }, authInfo.token, controller)
101 | .then((result) => {
102 | setList(result.data.data);
103 | setIsLoading(false);
104 | setListMeta(result.data.meta);
105 | })
106 | .catch(() => {
107 | setIsLoading(false);
108 | setList([]);
109 | });
110 | }, [page]);
111 |
112 | return (
113 | <>
114 |
115 | setDetail("")}
118 | className={"w-max max-w-md md:max-w-none"}
119 | >
120 | {dataDetail.isLoading ? (
121 |
122 | ) : (
123 |
124 |
159 |
192 |
193 | )}
194 |
195 |
196 |
197 |
198 |
199 | Let′s see what you have bought!
200 |
201 |
Select items to see detail
202 |
203 | {/* */}
214 | {!isLoading ? (
215 | <>
216 |
217 | {list.map((item, key) => (
218 | setDetail(item.id)}
221 | key={key}
222 | >
223 |
224 |

234 |
235 |
236 |
237 | {item.products[0].product_name}
238 | {item.products.length > 1 && (
239 |
240 | + {item.products.length - 1} more
241 |
242 | )}
243 |
244 |
245 | IDR {n_f(item.grand_total)}
246 |
247 |
{item.status_name}
248 |
249 | {/*
*/}
253 |
254 | ))}
255 |
256 |
257 |
258 | {listMeta.prev && (
259 |
269 | )}
270 |
273 | {listMeta.next && (
274 |
284 | )}
285 |
286 |
287 | >
288 | ) : (
289 |
290 |
291 |
292 | )}
293 |
294 |
295 |
296 | >
297 | );
298 | }
299 |
300 | export default History;
301 |
--------------------------------------------------------------------------------
/src/assets/images/empty-box.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/Products/GetAllProducts.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React, { useEffect, useState } from "react";
3 |
4 | import axios from "axios";
5 | import { useSelector } from "react-redux";
6 | import {
7 | Link,
8 | NavLink,
9 | useLocation,
10 | useNavigate,
11 | useParams,
12 | } from "react-router-dom";
13 |
14 | import penIcon from "../../assets/icons/icon-pen.svg";
15 | import emptyBox from "../../assets/images/empty.svg";
16 | import loadingImage from "../../assets/images/loading.svg";
17 | import productPlaceholder from "../../assets/images/placeholder-image.webp";
18 | import { getAllProducts } from "../../utils/dataProvider/products";
19 | import { n_f } from "../../utils/helpers";
20 | import withSearchParams from "../../utils/wrappers/withSearchParams.js";
21 |
22 | function GetAllProducts(props) {
23 | {
24 | const [products, setProducts] = useState([]);
25 | const [meta, setMeta] = useState({});
26 | const [isLoading, setIsLoading] = useState(true);
27 | const [inputPage, setInputPage] = useState(1);
28 | const userInfo = useSelector((state) => state.userInfo);
29 | const { catId } = useParams();
30 | const { searchParams, setSearchParams } = props;
31 | const { sort, setSort } = props;
32 |
33 | function getProducts(catId, searchParams, controller) {
34 | const sort = searchParams.get("sort");
35 | const orderBy = searchParams.get("orderBy");
36 | const searchByName = searchParams.get("q");
37 | setIsLoading(true);
38 |
39 | getAllProducts(
40 | catId,
41 | { sort, limit: 8, searchByName, orderBy, page },
42 | controller
43 | )
44 | .then((response) => response.data)
45 | .then((data) => {
46 | setProducts(data.data);
47 | setMeta(data.meta);
48 | setIsLoading(false);
49 | })
50 | .catch((err) => {
51 | if (axios.isCancel(err)) return;
52 | setIsLoading(false);
53 | setProducts([]);
54 | setMeta({});
55 | });
56 | }
57 |
58 | // const controller = React.useMemo(() => new AbortController(), [catId]);
59 | const page = searchParams.get("page");
60 | if (searchParams.has("page") && (page < 1 || isNaN(page))) {
61 | setSearchParams({ page: 1 });
62 | }
63 |
64 | const paginatorPress = (e) => {
65 | if (e.key === "Enter") {
66 | window.scrollTo({ top: 0, behavior: "smooth" });
67 | const page =
68 | meta.totalPage < e.target.value ? meta.totalPage : e.target.value;
69 | setSearchParams({ page });
70 | }
71 | };
72 |
73 | // const controller = useMemo(
74 | // () => new AbortController(),
75 | // [catId, page, searchParams]
76 | // );
77 |
78 | const navigate = useNavigate();
79 | const location = useLocation();
80 |
81 | const navigateWithParams = (newParams) => {
82 | const searchParams = new URLSearchParams(location.search);
83 | Object.entries(newParams).forEach(([key, value]) =>
84 | searchParams.set(key, value)
85 | );
86 | navigate(`${location.pathname}?${searchParams}`);
87 | };
88 |
89 | const handleNextClick = () => {
90 | window.scrollTo({ top: 0, behavior: "smooth" });
91 | navigateWithParams({ page: parseInt(meta.currentPage) + 1 });
92 | };
93 |
94 | const handlePrevClick = () => {
95 | window.scrollTo({ top: 0, behavior: "smooth" });
96 | navigateWithParams({ page: parseInt(meta.currentPage) - 1 });
97 | };
98 |
99 | useEffect(() => {
100 | const controller = new AbortController();
101 | setInputPage(!page ? 1 : page);
102 |
103 | // Fetch new products
104 | getProducts(catId, searchParams, controller);
105 |
106 | return () => {
107 | console.log(catId);
108 | controller.abort();
109 | setIsLoading(true);
110 | };
111 | }, [catId, page, searchParams]);
112 |
113 | if (isLoading)
114 | return (
115 |
116 |
117 |
118 | );
119 |
120 | if (products.length < 1) {
121 | return (
122 |
123 |
124 |

125 |
126 |
127 |
128 | We're sorry, it seems our products have gone into hiding.
129 |
130 |
We'll try to coax them out soon.
131 |
132 |
133 | );
134 | }
135 |
136 | return (
137 | <>
138 |
139 | {products.map((product) => (
140 |
141 |
142 |
147 | {/*
148 | 20%
149 |
*/}
150 |
151 |
152 | {product.name}
153 |
154 |
155 | IDR {n_f(product.price)}
156 |
157 | {Number(userInfo.role) > 1 && (
158 |
162 |
163 |
164 | )}
165 |
166 |
167 |
168 | ))}
169 |
170 |
171 |
175 | {meta.prev ? (
176 | -
177 |
191 |
192 | ) : (
193 | ""
194 | )}
195 |
196 | {meta.next ? (
197 | -
198 |
213 |
214 | ) : (
215 | ""
216 | )}
217 |
218 |
219 |
Page
220 |
setInputPage(e.target.value)}
227 | onKeyDown={paginatorPress}
228 | className="w-10 h-6 border border-gray-200 bg-white rounded-sm p-1 text-center appearance-none focus:outline-none focus:ring-1 focus:ring-secondary text-sm"
229 | />
230 |
of
231 |
{meta.totalPage}
232 | {meta.prev ? (
233 |
247 | ) : (
248 | ""
249 | )}
250 |
251 | {meta.next ? (
252 |
266 | ) : (
267 | ""
268 | )}
269 |
270 |
271 | >
272 | );
273 | }
274 | }
275 |
276 | export default withSearchParams(GetAllProducts);
277 |
--------------------------------------------------------------------------------