Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ad3ce27a4 | ||
|
|
38bdf4c3c5 | ||
|
|
eee374359b |
@ -291,11 +291,20 @@ module.exports = class OrdersDBApi {
|
|||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
{
|
||||||
|
model: db.order_items,
|
||||||
|
as: 'order_items_order',
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: db.products,
|
||||||
|
as: 'product',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
model: db.customers,
|
model: db.customers,
|
||||||
as: 'customer',
|
as: 'customer',
|
||||||
|
required: false,
|
||||||
where: filter.customer ? {
|
where: filter.customer ? {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ id: { [Op.in]: filter.customer.split('|').map(term => Utils.uuid(term)) } },
|
{ id: { [Op.in]: filter.customer.split('|').map(term => Utils.uuid(term)) } },
|
||||||
|
|||||||
@ -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/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);
|
app.use('/api/orders', passport.authenticate('jwt', {session: false}), ordersRoutes);
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,93 @@ const {
|
|||||||
checkCrudPermissions,
|
checkCrudPermissions,
|
||||||
} = require('../middlewares/check-permissions');
|
} = 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'));
|
router.use(checkCrudPermissions('products'));
|
||||||
|
|
||||||
|
|
||||||
@ -108,7 +195,7 @@ router.post('/', wrapAsync(async (req, res) => {
|
|||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* properties:
|
* properties:
|
||||||
* data:
|
* data:
|
||||||
* description: Data of the updated items
|
* description: Data of the updated items
|
||||||
* type: array
|
* type: array
|
||||||
@ -267,174 +354,7 @@ router.post('/deleteByIds', wrapAsync(async (req, res) => {
|
|||||||
res.status(200).send(payload);
|
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);
|
router.use('/', require('../helpers').commonErrorHandler);
|
||||||
|
|
||||||
|
|||||||
84
frontend/src/components/LoginModal.tsx
Normal file
84
frontend/src/components/LoginModal.tsx
Normal 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;
|
||||||
@ -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 { mdiClose, mdiDotsVertical } from '@mdi/js'
|
||||||
import { containerMaxW } from '../config'
|
import { containerMaxW } from '../config'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
import NavBarItemPlain from './NavBarItemPlain'
|
import NavBarItemPlain from './NavBarItemPlain'
|
||||||
import NavBarMenuList from './NavBarMenuList'
|
import NavBarMenuList from './NavBarMenuList'
|
||||||
import { MenuNavBarItem } from '../interfaces'
|
import { MenuNavBarItem } from '../interfaces'
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import { useAppSelector } from '../stores/hooks';
|
||||||
|
import { ModalContext } from '../context/ModalContext';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
menu: MenuNavBarItem[]
|
menu: MenuNavBarItem[]
|
||||||
@ -17,6 +18,12 @@ export default function NavBar({ menu, className = '', children }: Props) {
|
|||||||
const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false)
|
const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false)
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
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(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
@ -33,6 +40,13 @@ export default function NavBar({ menu, className = '', children }: Props) {
|
|||||||
setIsMenuNavBarActive(!isMenuNavBarActive)
|
setIsMenuNavBarActive(!isMenuNavBarActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const menuToShow = menu.filter((item) => {
|
||||||
|
if (currentUser) {
|
||||||
|
return !item.isLogin
|
||||||
|
}
|
||||||
|
return !item.isLogout && !item.isCurrentUser
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<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`}
|
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'
|
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`}
|
} 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>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -14,10 +14,12 @@ import { useRouter } from 'next/router';
|
|||||||
import ClickOutside from "./ClickOutside";
|
import ClickOutside from "./ClickOutside";
|
||||||
|
|
||||||
type Props = {
|
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 router = useRouter();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const excludedRef = useRef(null);
|
const excludedRef = useRef(null);
|
||||||
@ -58,9 +60,12 @@ export default function NavBarItem({ item }: Props) {
|
|||||||
dispatch(setDarkMode(null))
|
dispatch(setDarkMode(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.isLogin) {
|
||||||
|
openLoginModal();
|
||||||
|
}
|
||||||
|
|
||||||
if(item.isLogout) {
|
if(item.isLogout) {
|
||||||
dispatch(logoutUser())
|
dispatch(logoutUser())
|
||||||
router.push('/login')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +99,11 @@ export default function NavBarItem({ item }: Props) {
|
|||||||
>
|
>
|
||||||
{itemLabel}
|
{itemLabel}
|
||||||
</span>
|
</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.isCurrentUser && <UserAvatarCurrentUser className="w-6 h-6 mr-3 inline-flex" />}
|
||||||
{item.menu && (
|
{item.menu && (
|
||||||
<BaseIcon
|
<BaseIcon
|
||||||
|
|||||||
@ -3,15 +3,17 @@ import { MenuNavBarItem } from '../interfaces'
|
|||||||
import NavBarItem from './NavBarItem'
|
import NavBarItem from './NavBarItem'
|
||||||
|
|
||||||
type Props = {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{menu.map((item, index) => (
|
{menu.map((item, index) => (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<NavBarItem item={item} />
|
<NavBarItem item={item} cartItemsCount={cartItemsCount} openLoginModal={openLoginModal} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -103,7 +103,7 @@ const TableSampleProducts = ({ filterItems, setFilterItems, filters, showGrid })
|
|||||||
|
|
||||||
const generateFilterRequests = useMemo(() => {
|
const generateFilterRequests = useMemo(() => {
|
||||||
let request = '&';
|
let request = '&';
|
||||||
filterItems.forEach((item) => {
|
filterItems?.forEach((item) => {
|
||||||
const isRangeFilter = filters.find(
|
const isRangeFilter = filters.find(
|
||||||
(filter) =>
|
(filter) =>
|
||||||
filter.title === item.fields.selectedField &&
|
filter.title === item.fields.selectedField &&
|
||||||
|
|||||||
30
frontend/src/context/ModalContext.tsx
Normal file
30
frontend/src/context/ModalContext.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
27
frontend/src/layouts/LayoutShop.tsx
Normal file
27
frontend/src/layouts/LayoutShop.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -10,10 +10,17 @@ import {
|
|||||||
mdiThemeLightDark,
|
mdiThemeLightDark,
|
||||||
mdiGithub,
|
mdiGithub,
|
||||||
mdiVuejs,
|
mdiVuejs,
|
||||||
|
mdiCart,
|
||||||
|
mdiLogin,
|
||||||
} from '@mdi/js'
|
} from '@mdi/js'
|
||||||
import { MenuNavBarItem } from './interfaces'
|
import { MenuNavBarItem } from './interfaces'
|
||||||
|
|
||||||
const menuNavBar: MenuNavBarItem[] = [
|
const menuNavBar: MenuNavBarItem[] = [
|
||||||
|
{
|
||||||
|
icon: mdiCart,
|
||||||
|
label: 'Cart',
|
||||||
|
href: '/cart',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
isCurrentUser: true,
|
isCurrentUser: true,
|
||||||
menu: [
|
menu: [
|
||||||
@ -38,6 +45,11 @@ const menuNavBar: MenuNavBarItem[] = [
|
|||||||
isDesktopNoLabel: true,
|
isDesktopNoLabel: true,
|
||||||
isToggleLightDark: true,
|
isToggleLightDark: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: mdiLogin,
|
||||||
|
label: 'Login',
|
||||||
|
isLogin: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: mdiLogout,
|
icon: mdiLogout,
|
||||||
label: 'Log out',
|
label: 'Log out',
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { appWithTranslation } from 'next-i18next';
|
|||||||
import '../i18n';
|
import '../i18n';
|
||||||
import IntroGuide from '../components/IntroGuide';
|
import IntroGuide from '../components/IntroGuide';
|
||||||
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
|
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
|
||||||
|
import { ModalProvider } from '../context/ModalContext';
|
||||||
|
|
||||||
// Initialize axios
|
// Initialize axios
|
||||||
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
|
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
|
||||||
@ -158,42 +159,44 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
{getLayout(
|
<ModalProvider>
|
||||||
<>
|
{getLayout(
|
||||||
<Head>
|
<>
|
||||||
<meta name="description" content={description} />
|
<Head>
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
|
||||||
<meta property="og:url" content={url} />
|
<meta property="og:url" content={url} />
|
||||||
<meta property="og:site_name" content="https://flatlogic.com/" />
|
<meta property="og:site_name" content="https://flatlogic.com/" />
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
<meta property="og:image" content={image} />
|
<meta property="og:image" content={image} />
|
||||||
<meta property="og:image:type" content="image/png" />
|
<meta property="og:image:type" content="image/png" />
|
||||||
<meta property="og:image:width" content={imageWidth} />
|
<meta property="og:image:width" content={imageWidth} />
|
||||||
<meta property="og:image:height" content={imageHeight} />
|
<meta property="og:image:height" content={imageHeight} />
|
||||||
|
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
<meta property="twitter:title" content={title} />
|
<meta property="twitter:title" content={title} />
|
||||||
<meta property="twitter:description" content={description} />
|
<meta property="twitter:description" content={description} />
|
||||||
<meta property="twitter:image:src" content={image} />
|
<meta property="twitter:image:src" content={image} />
|
||||||
<meta property="twitter:image:width" content={imageWidth} />
|
<meta property="twitter:image:width" content={imageWidth} />
|
||||||
<meta property="twitter:image:height" content={imageHeight} />
|
<meta property="twitter:image:height" content={imageHeight} />
|
||||||
|
|
||||||
<link rel="icon" href="/favicon.svg" />
|
<link rel="icon" href="/favicon.svg" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<IntroGuide
|
<IntroGuide
|
||||||
steps={steps}
|
steps={steps}
|
||||||
stepsName={stepName}
|
stepsName={stepName}
|
||||||
stepsEnabled={stepsEnabled}
|
stepsEnabled={stepsEnabled}
|
||||||
onExit={handleExit}
|
onExit={handleExit}
|
||||||
/>
|
/>
|
||||||
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
|
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</ModalProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
100
frontend/src/pages/cart.tsx
Normal file
100
frontend/src/pages/cart.tsx
Normal 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;
|
||||||
@ -7,6 +7,8 @@ import LayoutAuthenticated from '../layouts/Authenticated'
|
|||||||
import SectionMain from '../components/SectionMain'
|
import SectionMain from '../components/SectionMain'
|
||||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
||||||
import BaseIcon from "../components/BaseIcon";
|
import BaseIcon from "../components/BaseIcon";
|
||||||
|
import ProductsTable from '../components/Products/TableProducts';
|
||||||
|
import { useSampleClients } from '../hooks/sampleData';
|
||||||
import { getPageTitle } from '../config'
|
import { getPageTitle } from '../config'
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
@ -22,6 +24,7 @@ const Dashboard = () => {
|
|||||||
const corners = useAppSelector((state) => state.style.corners);
|
const corners = useAppSelector((state) => state.style.corners);
|
||||||
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
|
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
|
||||||
|
|
||||||
|
const { clients } = useSampleClients();
|
||||||
const loadingMessage = 'Loading...';
|
const loadingMessage = 'Loading...';
|
||||||
|
|
||||||
|
|
||||||
@ -141,6 +144,13 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
{!!rolesWidgets.length && <hr className='my-6 text-skyBlueTheme-mainBG ' />}
|
{!!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'>
|
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,166 +1,99 @@
|
|||||||
|
import React, { useEffect, useContext } from 'react';
|
||||||
import React, { useEffect, useState } from 'react';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
import type { ReactElement } from 'react';
|
import { fetch as fetchProducts } from '../stores/products/productsSlice';
|
||||||
import Head from 'next/head';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import BaseButton from '../components/BaseButton';
|
|
||||||
import CardBox from '../components/CardBox';
|
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 { getPageTitle } from '../config';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import Head from 'next/head';
|
||||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
import SectionMain from '../components/SectionMain';
|
||||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
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() {
|
useEffect(() => {
|
||||||
const [illustrationImage, setIllustrationImage] = useState({
|
dispatch(fetchProducts({}));
|
||||||
src: undefined,
|
dispatch(fetchOrders({}));
|
||||||
photographer: undefined,
|
}, [dispatch]);
|
||||||
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);
|
|
||||||
|
|
||||||
const title = 'Store Operations Dashboard'
|
const handleAddToCart = async (product) => {
|
||||||
|
if (!currentUser) {
|
||||||
|
openLoginModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
let pendingOrder = orders.find(order => order.status === 'Pending' && order.customer?.id === currentUser.id);
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
const image = await getPexelsImage();
|
|
||||||
const video = await getPexelsVideo();
|
|
||||||
setIllustrationImage(image);
|
|
||||||
setIllustrationVideo(video);
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const imageBlock = (image) => (
|
if (!pendingOrder) {
|
||||||
<div
|
const orderData = {
|
||||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
customer: currentUser.id,
|
||||||
style={{
|
status: 'Pending',
|
||||||
backgroundImage: `${
|
};
|
||||||
image
|
const newOrderAction = await dispatch(createOrder(orderData));
|
||||||
? `url(${image?.src?.original})`
|
if (newOrderAction.payload) {
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
pendingOrder = newOrderAction.payload;
|
||||||
}`,
|
}
|
||||||
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>
|
|
||||||
);
|
|
||||||
|
|
||||||
const videoBlock = (video) => {
|
if (pendingOrder) {
|
||||||
if (video?.video_files?.length > 0) {
|
const orderItemData = {
|
||||||
return (
|
order: pendingOrder.id,
|
||||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
product: product.id,
|
||||||
<video
|
quantity: 1,
|
||||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
unit_price: product.price,
|
||||||
autoPlay
|
subtotal: product.price,
|
||||||
loop
|
};
|
||||||
muted
|
await dispatch(createOrderItem(orderItemData));
|
||||||
>
|
}
|
||||||
<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>)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
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>
|
<Head>
|
||||||
<title>{getPageTitle('Starter Page')}</title>
|
<title>{getPageTitle('Shop')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={mdiStorefrontOutline} title="Our Products" main />
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
{loading && <div>Loading...</div>}
|
||||||
<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'
|
|
||||||
/>
|
|
||||||
|
|
||||||
</BaseButtons>
|
{!loading && products && (
|
||||||
</CardBox>
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||||
</div>
|
{products.map((product) => (
|
||||||
</div>
|
<CardBox key={product.id}>
|
||||||
</SectionFullScreen>
|
<div className="h-48 overflow-hidden bg-gray-100 dark:bg-slate-800 flex items-center justify-center">
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
{product.images && product.images.length > 0 ? (
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
<img src={product.images[0].publicUrl} alt={product.name} className="w-full h-full object-cover"/>
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
) : (
|
||||||
Privacy Policy
|
<span className="text-gray-400">No Image</span>
|
||||||
</Link>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
</div>
|
<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;
|
||||||
@ -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 ProductsPage = () => {
|
||||||
|
const { clients } = useSampleClients();
|
||||||
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<LayoutAuthenticated>
|
||||||
<Head>
|
<title>{getPageTitle('Products')}</title>
|
||||||
<title>{getPageTitle('Products')}</title>
|
<ProductsTable clients={clients} />
|
||||||
</Head>
|
</LayoutAuthenticated>
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ProductsTablesPage.getLayout = function getLayout(page: ReactElement) {
|
export default ProductsPage;
|
||||||
return (
|
|
||||||
<LayoutAuthenticated
|
|
||||||
|
|
||||||
permission={'READ_PRODUCTS'}
|
|
||||||
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</LayoutAuthenticated>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProductsTablesPage
|
|
||||||
|
|||||||
@ -86,8 +86,10 @@ export const authSlice = createSlice({
|
|||||||
const token = action.payload;
|
const token = action.payload;
|
||||||
const user = jwt.decode(token);
|
const user = jwt.decode(token);
|
||||||
|
|
||||||
|
state.isFetching = false;
|
||||||
state.errorMessage = '';
|
state.errorMessage = '';
|
||||||
state.token = token;
|
state.token = token;
|
||||||
|
state.currentUser = user;
|
||||||
localStorage.setItem('token', token);
|
localStorage.setItem('token', token);
|
||||||
localStorage.setItem('user', JSON.stringify(user));
|
localStorage.setItem('user', JSON.stringify(user));
|
||||||
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
|
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user