Compare commits

...

3 Commits

Author SHA1 Message Date
Flatlogic Bot
0ad3ce27a4 v3 2026-01-16 14:00:00 +00:00
Flatlogic Bot
38bdf4c3c5 v2 2026-01-16 13:23:43 +00:00
Flatlogic Bot
eee374359b v1 2026-01-16 13:20:05 +00:00
17 changed files with 531 additions and 523 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

@ -106,7 +106,7 @@ app.use('/api/customers', passport.authenticate('jwt', {session: false}), custom
app.use('/api/categories', passport.authenticate('jwt', {session: false}), categoriesRoutes);
app.use('/api/products', passport.authenticate('jwt', {session: false}), productsRoutes);
app.use('/api/products', productsRoutes);
app.use('/api/orders', passport.authenticate('jwt', {session: false}), ordersRoutes);

View File

@ -15,6 +15,93 @@ const {
checkCrudPermissions,
} = require('../middlewares/check-permissions');
/**
* @swagger
* /api/products:
* get:
* tags: [Products]
* summary: Get all products
* description: Get all products
* responses:
* 200:
* description: Products list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Products"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/', wrapAsync(async (req, res) => {
const filetype = req.query.filetype
const currentUser = req.currentUser;
const payload = await ProductsDBApi.findAll(
req.query, { currentUser }
);
if (filetype && filetype === 'csv') {
const fields = ['id','name','sku','description',
'stock',
'price',
];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
res.status(200).attachment(csv);
res.send(csv)
} catch (err) {
console.error(err);
}
} else {
res.status(200).send(payload);
}
}));
/**
* @swagger
* /api/products/{id}:
* get:
* tags: [Products]
* summary: Get selected item
* description: Get selected item
* parameters:
* - in: path
* name: id
* description: ID of item to get
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Selected item successfully received
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Products"
* 400:
* description: Invalid ID supplied
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await ProductsDBApi.findBy(
{ id: req.params.id },
);
res.status(200).send(payload);
}));
router.use(checkCrudPermissions('products'));
@ -108,7 +195,7 @@ router.post('/', wrapAsync(async (req, res) => {
* content:
* application/json:
* schema:
* properties:
* properties:
* data:
* description: Data of the updated items
* type: array
@ -267,174 +354,7 @@ router.post('/deleteByIds', wrapAsync(async (req, res) => {
res.status(200).send(payload);
}));
/**
* @swagger
* /api/products:
* get:
* security:
* - bearerAuth: []
* tags: [Products]
* summary: Get all products
* description: Get all products
* responses:
* 200:
* description: Products list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Products"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/', wrapAsync(async (req, res) => {
const filetype = req.query.filetype
const currentUser = req.currentUser;
const payload = await ProductsDBApi.findAll(
req.query, { currentUser }
);
if (filetype && filetype === 'csv') {
const fields = ['id','name','sku','description',
'stock',
'price',
];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
res.status(200).attachment(csv);
res.send(csv)
} catch (err) {
console.error(err);
}
} else {
res.status(200).send(payload);
}
}));
/**
* @swagger
* /api/products/count:
* get:
* security:
* - bearerAuth: []
* tags: [Products]
* summary: Count all products
* description: Count all products
* responses:
* 200:
* description: Products count successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Products"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/count', wrapAsync(async (req, res) => {
const currentUser = req.currentUser;
const payload = await ProductsDBApi.findAll(
req.query,
null,
{ countOnly: true, currentUser }
);
res.status(200).send(payload);
}));
/**
* @swagger
* /api/products/autocomplete:
* get:
* security:
* - bearerAuth: []
* tags: [Products]
* summary: Find all products that match search criteria
* description: Find all products that match search criteria
* responses:
* 200:
* description: Products list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Products"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Data not found
* 500:
* description: Some server error
*/
router.get('/autocomplete', async (req, res) => {
const payload = await ProductsDBApi.findAllAutocomplete(
req.query.query,
req.query.limit,
req.query.offset,
);
res.status(200).send(payload);
});
/**
* @swagger
* /api/products/{id}:
* get:
* security:
* - bearerAuth: []
* tags: [Products]
* summary: Get selected item
* description: Get selected item
* parameters:
* - in: path
* name: id
* description: ID of item to get
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Selected item successfully received
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Products"
* 400:
* description: Invalid ID supplied
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Item not found
* 500:
* description: Some server error
*/
router.get('/:id', wrapAsync(async (req, res) => {
const payload = await ProductsDBApi.findBy(
{ id: req.params.id },
);
res.status(200).send(payload);
}));
router.use('/', require('../helpers').commonErrorHandler);

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,11 +1,12 @@
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'
import NavBarItemPlain from './NavBarItemPlain'
import NavBarMenuList from './NavBarMenuList'
import { MenuNavBarItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks';
import { useAppSelector } from '../stores/hooks';
import { ModalContext } from '../context/ModalContext';
type Props = {
menu: MenuNavBarItem[]
@ -17,6 +18,12 @@ export default function NavBar({ menu, className = '', children }: Props) {
const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false)
const [isScrolled, setIsScrolled] = useState(false);
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;
useEffect(() => {
const handleScroll = () => {
@ -33,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`}
@ -49,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} />
<NavBarMenuList menu={menuToShow} cartItemsCount={cartItemsCount} openLoginModal={openLoginModal} />
</div>
</div>
</nav>

View File

@ -14,10 +14,12 @@ import { useRouter } from 'next/router';
import ClickOutside from "./ClickOutside";
type Props = {
item: MenuNavBarItem
item: MenuNavBarItem,
cartItemsCount?: number,
openLoginModal?: () => void
}
export default function NavBarItem({ item }: Props) {
export default function NavBarItem({ item, cartItemsCount, openLoginModal }: Props) {
const router = useRouter();
const dispatch = useAppDispatch();
const excludedRef = useRef(null);
@ -58,9 +60,12 @@ export default function NavBarItem({ item }: Props) {
dispatch(setDarkMode(null))
}
if (item.isLogin) {
openLoginModal();
}
if(item.isLogout) {
dispatch(logoutUser())
router.push('/login')
}
}
@ -94,6 +99,11 @@ export default function NavBarItem({ item }: Props) {
>
{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

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

View File

@ -103,7 +103,7 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
const generateFilterRequests = useMemo(() => {
let request = '&';
filterItems.forEach((item) => {
filterItems?.forEach((item) => {
const isRangeFilter = filters.find(
(filter) =>
filter.title === item.fields.selectedField &&

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,10 +10,17 @@ import {
mdiThemeLightDark,
mdiGithub,
mdiVuejs,
mdiCart,
mdiLogin,
} from '@mdi/js'
import { MenuNavBarItem } from './interfaces'
const menuNavBar: MenuNavBarItem[] = [
{
icon: mdiCart,
label: 'Cart',
href: '/cart',
},
{
isCurrentUser: true,
menu: [
@ -38,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>
)
}

100
frontend/src/pages/cart.tsx Normal file
View File

@ -0,0 +1,100 @@
import React, { ReactElement, useEffect } from 'react';
import Head from 'next/head'
import {
useAppDispatch,
useAppSelector
} from "../stores/hooks";
import { useRouter } from "next/router";
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 = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { orders } = useAppSelector((state) => state.orders)
const { currentUser } = useAppSelector((state) => state.auth)
const pendingOrder =
orders &&
orders.find(
(order) => order.status === 'Pending' && order.customer?.id === currentUser?.user.id,
);
useEffect(() => {
if (currentUser) {
dispatch(fetchOrders({ query: '?status=Pending' }));
}
}, [dispatch, currentUser]);
return (
<>
<Head>
<title>{getPageTitle('Shopping Cart')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiCart} title={'Shopping Cart'} main>
</SectionTitleLineWithButton>
<>
<p className={'block font-bold mb-2'}>Order Items</p>
<div
className='mb-6 border border-gray-300 rounded overflow-hidden'
>
<div className={'overflow-x-auto'}>
<table>
<thead>
<tr>
<th>Product</th>
<th>Quantity</th>
<th>Price</th>
<th>Subtotal</th>
</tr>
</thead>
<tbody>
{pendingOrder && pendingOrder.order_items_order && Array.isArray(pendingOrder.order_items_order) &&
pendingOrder.order_items_order.map((item: any) => (
<tr key={item.id}>
<td data-label="product_name">
{item.product.name}
</td>
<td data-label="quantity">
{item.quantity}
</td>
<td data-label="unit_price">
{item.unit_price}
</td>
<td data-label="subtotal">
{item.subtotal}
</td>
</tr>
))}
</tbody>
</table>
</div>
{!pendingOrder?.order_items_order?.length && <div className={'text-center py-4'}>Your cart is empty</div>}
</div>
</>
<BaseButton
color='info'
label='Checkout'
onClick={() => router.push('/checkout')}
/>
</SectionMain>
</>
);
};
CartPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutShop
>
{page}
</LayoutShop>
)
}
export default CartPage;

View File

@ -7,6 +7,8 @@ import LayoutAuthenticated from '../layouts/Authenticated'
import SectionMain from '../components/SectionMain'
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
import BaseIcon from "../components/BaseIcon";
import ProductsTable from '../components/Products/TableProducts';
import { useSampleClients } from '../hooks/sampleData';
import { getPageTitle } from '../config'
import Link from "next/link";
@ -22,6 +24,7 @@ const Dashboard = () => {
const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
const { clients } = useSampleClients();
const loadingMessage = 'Loading...';
@ -141,6 +144,13 @@ const Dashboard = () => {
{!!rolesWidgets.length && <hr className='my-6 text-skyBlueTheme-mainBG ' />}
<SectionTitleLineWithButton
icon={icon.mdiPackageVariant}
title="Products"
></SectionTitleLineWithButton>
<div className="mb-6">
<ProductsTable clients={clients} />
</div>
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>

View File

@ -1,166 +1,99 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import React, { useEffect, useContext } from 'react';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { fetch as fetchProducts } from '../stores/products/productsSlice';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
import Head from 'next/head';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { mdiStorefrontOutline } from '@mdi/js';
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);
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('left');
const textColor = useAppSelector((state) => state.style.linkColor);
useEffect(() => {
dispatch(fetchProducts({}));
dispatch(fetchOrders({}));
}, [dispatch]);
const title = 'Store Operations Dashboard'
const handleAddToCart = async (product) => {
if (!currentUser) {
openLoginModal();
return;
}
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
let pendingOrder = orders.find(order => order.status === 'Pending' && order.customer?.id === currentUser.id);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
if (!pendingOrder) {
const orderData = {
customer: currentUser.id,
status: 'Pending',
};
const newOrderAction = await dispatch(createOrder(orderData));
if (newOrderAction.payload) {
pendingOrder = newOrderAction.payload;
}
}
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
if (pendingOrder) {
const orderItemData = {
order: pendingOrder.id,
product: product.id,
quantity: 1,
unit_price: product.price,
subtotal: product.price,
};
await dispatch(createOrderItem(orderItemData));
}
};
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<>
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('Shop')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiStorefrontOutline} title="Our Products" main />
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your Store Operations Dashboard app!"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
{loading && <div>Loading...</div>}
</BaseButtons>
</CardBox>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
</div>
{!loading && products && (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{products.map((product) => (
<CardBox key={product.id}>
<div className="h-48 overflow-hidden bg-gray-100 dark:bg-slate-800 flex items-center justify-center">
{product.images && product.images.length > 0 ? (
<img src={product.images[0].publicUrl} alt={product.name} className="w-full h-full object-cover"/>
) : (
<span className="text-gray-400">No Image</span>
)}
</div>
<div className="p-4">
<h4 className="text-xl font-bold">{product.name}</h4>
<p className="text-gray-500">${product.price}</p>
<button onClick={() => handleAddToCart(product)} className="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Add to Cart
</button>
</div>
</CardBox>
))}
</div>
)}
</SectionMain>
</>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
IndexPage.getLayout = function getLayout(page) {
return <LayoutShop>{page}</LayoutShop>;
};
export default IndexPage;

View File

@ -1,166 +1,18 @@
import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import TableProducts from '../../components/Products/TableProducts'
import BaseButton from '../../components/BaseButton'
import axios from "axios";
import Link from "next/link";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import CardBoxModal from "../../components/CardBoxModal";
import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/products/productsSlice';
import { useSampleClients } from '../../hooks/sampleData';
import { getPageTitle } from '../../config';
import ProductsTable from '../../components/Products/TableProducts';
import LayoutAuthenticated from '../../layouts/Authenticated';
import {hasPermission} from "../../helpers/userPermissions";
const ProductsTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'Name', title: 'name'},{label: 'SKU', title: 'sku'},{label: 'Description', title: 'description'},
{label: 'Stock', title: 'stock', number: 'true'},
{label: 'Price', title: 'price', number: 'true'},
{label: 'Category', title: 'category'},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_PRODUCTS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
};
const getProductsCSV = async () => {
const response = await axios({url: '/products?filetype=csv', method: 'GET',responseType: 'blob'});
const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'productsCSV.csv'
link.click()
};
const onModalConfirm = async () => {
if (!csvFile) return;
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
const ProductsPage = () => {
const { clients } = useSampleClients();
return (
<>
<Head>
<title>{getPageTitle('Products')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Products" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/products/products-new'} color='info' label='New Item'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getProductsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
</CardBox>
<CardBox className="mb-6" hasTable>
<TableProducts
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
showGrid={false}
/>
</CardBox>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
</CardBoxModal>
</>
)
}
<LayoutAuthenticated>
<title>{getPageTitle('Products')}</title>
<ProductsTable clients={clients} />
</LayoutAuthenticated>
);
};
ProductsTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_PRODUCTS'}
>
{page}
</LayoutAuthenticated>
)
}
export default ProductsTablesPage
export default ProductsPage;

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;