This commit is contained in:
Flatlogic Bot 2026-01-16 14:00:00 +00:00
parent 38bdf4c3c5
commit 0ad3ce27a4
12 changed files with 263 additions and 79 deletions

View File

@ -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)) } },

View File

@ -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 (
<CardBoxModal
title="Login"
isActive={isActive}
onClose={onClose}
>
<Formik
initialValues={{ email: 'admin@flatlogic.com', password: 'eea84746' }}
onSubmit={handleSubmit}
>
<Form>
<FormField label="Login" help="Please enter your login">
<Field name="email" />
</FormField>
<div className="relative">
<FormField label="Password" help="Please enter your password">
<Field name="password" type={showPassword ? 'text' : 'password'} />
</FormField>
<div
className="absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer"
onClick={togglePasswordVisibility}
>
<BaseIcon
className="text-gray-500 hover:text-gray-700"
size={20}
path={showPassword ? mdiEyeOff : mdiEye}
/>
</div>
</div>
<BaseButtons>
<BaseButton
className="w-full"
type="submit"
label={isFetching ? 'Loading...' : 'Login'}
color="info"
disabled={isFetching}
/>
</BaseButtons>
</Form>
</Formik>
</CardBoxModal>
);
};
export default LoginModal;

View File

@ -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 (
<nav
className={`${className} top-0 inset-x-0 fixed ${bgColor} h-14 z-30 transition-position w-screen lg:w-auto dark:bg-dark-800`}
@ -54,7 +63,7 @@ export default function NavBar({ menu, className = '', children }: Props) {
isMenuNavBarActive ? 'block' : 'hidden'
} flex items-center max-h-screen-menu overflow-y-auto lg:overflow-visible absolute w-screen top-14 left-0 ${bgColor} shadow-lg lg:w-auto lg:flex lg:static lg:shadow-none dark:bg-dark-800`}
>
<NavBarMenuList menu={menu} cartItemsCount={cartItemsCount} />
<NavBarMenuList menu={menuToShow} cartItemsCount={cartItemsCount} openLoginModal={openLoginModal} />
</div>
</div>
</nav>

View File

@ -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}
</span>
{item.label === 'Cart' && cartItemsCount > 0 && (
<div className="bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
{cartItemsCount}
</div>
)}
{item.isCurrentUser && <UserAvatarCurrentUser className="w-6 h-6 mr-3 inline-flex" />}
{item.menu && (
<BaseIcon

View File

@ -4,15 +4,16 @@ import NavBarItem from './NavBarItem'
type Props = {
menu: MenuNavBarItem[],
cartItemsCount?: number
cartItemsCount?: number,
openLoginModal?: () => void
}
export default function NavBarMenuList({ menu, cartItemsCount }: Props) {
export default function NavBarMenuList({ menu, cartItemsCount, openLoginModal }: Props) {
return (
<>
{menu.map((item, index) => (
<div key={index}>
<NavBarItem item={item} cartItemsCount={cartItemsCount} />
<NavBarItem item={item} cartItemsCount={cartItemsCount} openLoginModal={openLoginModal} />
</div>
))}
</>

View File

@ -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 (
<ModalContext.Provider value={{ isLoginModalActive, openLoginModal, closeLoginModal }}>
{children}
</ModalContext.Provider>
);
};

View File

@ -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 (
<div className={darkMode ? 'dark' : ''}>
<div className={`${bgColor} dark:bg-slate-800 dark:text-slate-100 pt-14`}>
<NavBar menu={menuNavBar} />
{children}
<LoginModal isActive={isLoginModalActive} onClose={closeLoginModal} />
</div>
</div>
);
}

View File

@ -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',

View File

@ -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 (
<Provider store={store}>
{getLayout(
<>
<Head>
<meta name="description" content={description} />
<ModalProvider>
{getLayout(
<>
<Head>
<meta name="description" content={description} />
<meta property="og:url" content={url} />
<meta property="og:site_name" content="https://flatlogic.com/" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content={imageWidth} />
<meta property="og:image:height" content={imageHeight} />
<meta property="og:url" content={url} />
<meta property="og:site_name" content="https://flatlogic.com/" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content={imageWidth} />
<meta property="og:image:height" content={imageHeight} />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image:src" content={image} />
<meta property="twitter:image:width" content={imageWidth} />
<meta property="twitter:image:height" content={imageHeight} />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image:src" content={image} />
<meta property="twitter:image:width" content={imageWidth} />
<meta property="twitter:image:height" content={imageHeight} />
<link rel="icon" href="/favicon.svg" />
</Head>
<link rel="icon" href="/favicon.svg" />
</Head>
<ErrorBoundary>
<Component {...pageProps} />
</ErrorBoundary>
<IntroGuide
steps={steps}
stepsName={stepName}
stepsEnabled={stepsEnabled}
onExit={handleExit}
/>
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
</>
)}
<ErrorBoundary>
<Component {...pageProps} />
</ErrorBoundary>
<IntroGuide
steps={steps}
stepsName={stepName}
stepsEnabled={stepsEnabled}
onExit={handleExit}
/>
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
</>
)}
</ModalProvider>
</Provider>
)
}

View File

@ -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 = () => {
<SectionMain>
<SectionTitleLineWithButton icon={mdiCart} title={'Shopping Cart'} main>
</SectionTitleLineWithButton>
<CardBox>
<>
<p className={'block font-bold mb-2'}>Order Items</p>
<CardBox
<div
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<div className={'overflow-x-auto'}>
<table>
<thead>
<tr>
@ -57,7 +59,7 @@ const CartPage = () => {
pendingOrder.order_items_order.map((item: any) => (
<tr key={item.id}>
<td data-label="product_name">
{item.product_name}
{item.product.name}
</td>
<td data-label="quantity">
{item.quantity}
@ -74,14 +76,13 @@ const CartPage = () => {
</table>
</div>
{!pendingOrder?.order_items_order?.length && <div className={'text-center py-4'}>Your cart is empty</div>}
</CardBox>
</div>
</>
<BaseButton
color='info'
label='Checkout'
onClick={() => router.push('/checkout')}
/>
</CardBox>
</SectionMain>
</>
);
@ -89,11 +90,11 @@ const CartPage = () => {
CartPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
<LayoutShop
>
{page}
</LayoutAuthenticated>
</LayoutShop>
)
}
export default CartPage;
export default CartPage;

View File

@ -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 (
<LayoutGuest>
<>
<Head>
<title>{getPageTitle('Shop')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiStorefrontOutline} title="Our Products" main>
</SectionTitleLineWithButton>
<SectionTitleLineWithButton icon={mdiStorefrontOutline} title="Our Products" main />
{loading && <div>Loading...</div>}
@ -89,8 +88,12 @@ const IndexPage = () => {
</div>
)}
</SectionMain>
</LayoutGuest>
</>
);
};
IndexPage.getLayout = function getLayout(page) {
return <LayoutShop>{page}</LayoutShop>;
};
export default IndexPage;

View File

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