diff --git a/backend/src/db/api/orders.js b/backend/src/db/api/orders.js index 5bba851..ec5ce9d 100644 --- a/backend/src/db/api/orders.js +++ b/backend/src/db/api/orders.js @@ -291,11 +291,20 @@ module.exports = class OrdersDBApi { const transaction = (options && options.transaction) || undefined; let include = [ - + { + model: db.order_items, + as: 'order_items_order', + include: [ + { + model: db.products, + as: 'product', + } + ] + }, { model: db.customers, as: 'customer', - + required: false, where: filter.customer ? { [Op.or]: [ { id: { [Op.in]: filter.customer.split('|').map(term => Utils.uuid(term)) } }, diff --git a/frontend/src/components/LoginModal.tsx b/frontend/src/components/LoginModal.tsx new file mode 100644 index 0000000..e8e86d9 --- /dev/null +++ b/frontend/src/components/LoginModal.tsx @@ -0,0 +1,84 @@ + +import React, { useState } from 'react'; +import { Field, Form, Formik } from 'formik'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import { loginUser } from '../stores/authSlice'; +import CardBoxModal from './CardBoxModal'; +import FormField from './FormField'; +import BaseButton from './BaseButton'; +import BaseButtons from './BaseButtons'; +import { mdiEye, mdiEyeOff } from '@mdi/js'; +import BaseIcon from './BaseIcon'; + +type Props = { + isActive: boolean; + onClose: () => void; +}; + +const LoginModal = ({ isActive, onClose }: Props) => { + const dispatch = useAppDispatch(); + const { isFetching } = useAppSelector((state) => state.auth); + const [showPassword, setShowPassword] = useState(false); + + const handleSubmit = (values) => { + dispatch(loginUser(values)) + .unwrap() + .then(() => { + onClose(); + }) + .catch(() => { + // error is handled by slice + }); + }; + + const togglePasswordVisibility = () => { + setShowPassword(!showPassword); + }; + + return ( + + +
+ + + + +
+ + + +
+ +
+
+ + + + +
+
+
+ ); +}; + +export default LoginModal; diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx index 3c09ec4..05e928d 100644 --- a/frontend/src/components/NavBar.tsx +++ b/frontend/src/components/NavBar.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState, useEffect } from 'react' +import React, { ReactNode, useState, useEffect, useContext } from 'react' import { mdiClose, mdiDotsVertical } from '@mdi/js' import { containerMaxW } from '../config' import BaseIcon from './BaseIcon' @@ -6,6 +6,7 @@ import NavBarItemPlain from './NavBarItemPlain' import NavBarMenuList from './NavBarMenuList' import { MenuNavBarItem } from '../interfaces' import { useAppSelector } from '../stores/hooks'; +import { ModalContext } from '../context/ModalContext'; type Props = { menu: MenuNavBarItem[] @@ -19,6 +20,7 @@ export default function NavBar({ menu, className = '', children }: Props) { const bgColor = useAppSelector((state) => state.style.bgLayoutColor); const { orders } = useAppSelector((state) => state.orders) const { currentUser } = useAppSelector((state) => state.auth) + const { openLoginModal } = useContext(ModalContext); const pendingOrder = orders.find(order => order.status === 'pending' && order.customer?.id === currentUser?.id); const cartItemsCount = pendingOrder?.order_items_order?.reduce((acc, item) => acc + item.quantity, 0) || 0; @@ -38,6 +40,13 @@ export default function NavBar({ menu, className = '', children }: Props) { setIsMenuNavBarActive(!isMenuNavBarActive) } + const menuToShow = menu.filter((item) => { + if (currentUser) { + return !item.isLogin + } + return !item.isLogout && !item.isCurrentUser + }) + return ( diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index e80061c..756625b 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -15,10 +15,11 @@ import ClickOutside from "./ClickOutside"; type Props = { item: MenuNavBarItem, - cartItemsCount?: number + cartItemsCount?: number, + openLoginModal?: () => void } -export default function NavBarItem({ item, cartItemsCount }: Props) { +export default function NavBarItem({ item, cartItemsCount, openLoginModal }: Props) { const router = useRouter(); const dispatch = useAppDispatch(); const excludedRef = useRef(null); @@ -59,9 +60,12 @@ export default function NavBarItem({ item, cartItemsCount }: Props) { dispatch(setDarkMode(null)) } + if (item.isLogin) { + openLoginModal(); + } + if(item.isLogout) { dispatch(logoutUser()) - router.push('/login') } } @@ -93,8 +97,13 @@ export default function NavBarItem({ item, cartItemsCount }: Props) { item.isDesktopNoLabel && item.icon ? 'lg:hidden' : '' }`} > - {itemLabel} {item.label === 'Cart' && cartItemsCount > 0 && `(${cartItemsCount})`} + {itemLabel} + {item.label === 'Cart' && cartItemsCount > 0 && ( +
+ {cartItemsCount} +
+ )} {item.isCurrentUser && } {item.menu && ( void } -export default function NavBarMenuList({ menu, cartItemsCount }: Props) { +export default function NavBarMenuList({ menu, cartItemsCount, openLoginModal }: Props) { return ( <> {menu.map((item, index) => (
- +
))} diff --git a/frontend/src/context/ModalContext.tsx b/frontend/src/context/ModalContext.tsx new file mode 100644 index 0000000..9337c8f --- /dev/null +++ b/frontend/src/context/ModalContext.tsx @@ -0,0 +1,30 @@ + +import React, { createContext, useState, ReactNode } from 'react'; + +export const ModalContext = createContext({ + isLoginModalActive: false, + openLoginModal: () => {}, + closeLoginModal: () => {}, +}); + +type Props = { + children: ReactNode; +}; + +export const ModalProvider = ({ children }: Props) => { + const [isLoginModalActive, setIsLoginModalActive] = useState(false); + + const openLoginModal = () => { + setIsLoginModalActive(true); + }; + + const closeLoginModal = () => { + setIsLoginModalActive(false); + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/layouts/LayoutShop.tsx b/frontend/src/layouts/LayoutShop.tsx new file mode 100644 index 0000000..2e65d27 --- /dev/null +++ b/frontend/src/layouts/LayoutShop.tsx @@ -0,0 +1,27 @@ + +import React, { ReactNode, useContext } from 'react'; +import { useAppSelector } from '../stores/hooks'; +import NavBar from '../components/NavBar'; +import menuNavBar from '../menuNavBar'; +import LoginModal from '../components/LoginModal'; +import { ModalContext } from '../context/ModalContext'; + +type Props = { + children: ReactNode; +}; + +export default function LayoutShop({ children }: Props) { + const darkMode = useAppSelector((state) => state.style.darkMode); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const { isLoginModalActive, closeLoginModal } = useContext(ModalContext); + + return ( +
+
+ + {children} + +
+
+ ); +} diff --git a/frontend/src/menuNavBar.ts b/frontend/src/menuNavBar.ts index be5b4b6..023b761 100644 --- a/frontend/src/menuNavBar.ts +++ b/frontend/src/menuNavBar.ts @@ -10,7 +10,8 @@ import { mdiThemeLightDark, mdiGithub, mdiVuejs, - mdiCart + mdiCart, + mdiLogin, } from '@mdi/js' import { MenuNavBarItem } from './interfaces' @@ -44,6 +45,11 @@ const menuNavBar: MenuNavBarItem[] = [ isDesktopNoLabel: true, isToggleLightDark: true, }, + { + icon: mdiLogin, + label: 'Login', + isLogin: true, + }, { icon: mdiLogout, label: 'Log out', diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 494a671..6b60c33 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -16,6 +16,7 @@ import { appWithTranslation } from 'next-i18next'; import '../i18n'; import IntroGuide from '../components/IntroGuide'; import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps'; +import { ModalProvider } from '../context/ModalContext'; // Initialize axios axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API @@ -158,42 +159,44 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { return ( - {getLayout( - <> - - + + {getLayout( + <> + + - - - - - - - - + + + + + + + + - - - - - - + + + + + + - - + + - - - - - {(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && } - - )} + + + + + {(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && } + + )} + ) } diff --git a/frontend/src/pages/cart.tsx b/frontend/src/pages/cart.tsx index 516af72..bd53d38 100644 --- a/frontend/src/pages/cart.tsx +++ b/frontend/src/pages/cart.tsx @@ -1,18 +1,16 @@ - import React, { ReactElement, useEffect } from 'react'; import Head from 'next/head' import { useAppDispatch, useAppSelector -} from "../../stores/hooks"; +} from "../stores/hooks"; import { useRouter } from "next/router"; -import { fetch as fetchOrders } from '../../stores/orders/ordersSlice' -import LayoutAuthenticated from "../../layouts/Authenticated"; -import { getPageTitle } from "../../config"; -import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton"; -import SectionMain from "../../components/SectionMain"; -import CardBox from "../../components/CardBox"; -import BaseButton from "../../components/BaseButton"; +import { fetch as fetchOrders } from '../stores/orders/ordersSlice' +import LayoutShop from "../layouts/LayoutShop"; +import { getPageTitle } from "../config"; +import SectionTitleLineWithButton from "../components/SectionTitleLineWithButton"; +import SectionMain from "../components/SectionMain"; +import BaseButton from "../components/BaseButton"; import { mdiCart } from "@mdi/js"; const CartPage = () => { @@ -21,11 +19,17 @@ const CartPage = () => { const { orders } = useAppSelector((state) => state.orders) const { currentUser } = useAppSelector((state) => state.auth) - const pendingOrder = orders.find(order => order.status === 'pending' && order.customer?.id === currentUser?.id); + const pendingOrder = + orders && + orders.find( + (order) => order.status === 'Pending' && order.customer?.id === currentUser?.user.id, + ); useEffect(() => { - dispatch(fetchOrders({ query: '?status=pending' })); - }, [dispatch]); + if (currentUser) { + dispatch(fetchOrders({ query: '?status=Pending' })); + } + }, [dispatch, currentUser]); return ( <> @@ -35,14 +39,12 @@ const CartPage = () => { - <>

Order Items

- -
+
@@ -57,7 +59,7 @@ const CartPage = () => { pendingOrder.order_items_order.map((item: any) => (
- {item.product_name} + {item.product.name} {item.quantity} @@ -74,14 +76,13 @@ const CartPage = () => {
{!pendingOrder?.order_items_order?.length &&
Your cart is empty
} - +
router.push('/checkout')} /> -
); @@ -89,11 +90,11 @@ const CartPage = () => { CartPage.getLayout = function getLayout(page: ReactElement) { return ( - {page} - + ) } -export default CartPage; +export default CartPage; \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index cfeca57..c64e369 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,38 +1,41 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useContext } from 'react'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { fetch as fetchProducts } from '../stores/products/productsSlice'; -import LayoutGuest from '../layouts/Guest'; import CardBox from '../components/CardBox'; import { getPageTitle } from '../config'; import Head from 'next/head'; import SectionMain from '../components/SectionMain'; import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; import { mdiStorefrontOutline } from '@mdi/js'; -import { create as createOrder } from '../stores/orders/ordersSlice'; +import { create as createOrder, fetch as fetchOrders } from '../stores/orders/ordersSlice'; import { create as createOrderItem } from '../stores/order_items/order_itemsSlice'; +import { ModalContext } from '../context/ModalContext'; +import LayoutShop from '../layouts/LayoutShop'; const IndexPage = () => { const dispatch = useAppDispatch(); const { products, loading } = useAppSelector((state) => state.products); const { orders } = useAppSelector((state) => state.orders); const { currentUser } = useAppSelector((state) => state.auth); + const { openLoginModal } = useContext(ModalContext); useEffect(() => { dispatch(fetchProducts({})); + dispatch(fetchOrders({})); }, [dispatch]); const handleAddToCart = async (product) => { if (!currentUser) { - alert('Please login to add items to your cart.'); + openLoginModal(); return; } - let pendingOrder = orders.find(order => order.status === 'pending' && order.customer?.id === currentUser.id); + let pendingOrder = orders.find(order => order.status === 'Pending' && order.customer?.id === currentUser.id); if (!pendingOrder) { const orderData = { customer: currentUser.id, - status: 'pending', + status: 'Pending', }; const newOrderAction = await dispatch(createOrder(orderData)); if (newOrderAction.payload) { @@ -52,17 +55,13 @@ const IndexPage = () => { } }; - - - return ( - + <> {getPageTitle('Shop')} - - + {loading &&
Loading...
} @@ -89,8 +88,12 @@ const IndexPage = () => { )}
-
+ ); }; +IndexPage.getLayout = function getLayout(page) { + return {page}; +}; + export default IndexPage; \ No newline at end of file diff --git a/frontend/src/stores/authSlice.ts b/frontend/src/stores/authSlice.ts index 2eaa1f2..6960459 100644 --- a/frontend/src/stores/authSlice.ts +++ b/frontend/src/stores/authSlice.ts @@ -86,8 +86,10 @@ export const authSlice = createSlice({ const token = action.payload; const user = jwt.decode(token); + state.isFetching = false; state.errorMessage = ''; state.token = token; + state.currentUser = user; localStorage.setItem('token', token); localStorage.setItem('user', JSON.stringify(user)); axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;