Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
d752c061f6 Auto commit: 2026-02-20T12:50:14.014Z 2026-02-20 12:50:14 +00:00
5 changed files with 362 additions and 727 deletions

View File

@ -12,6 +12,7 @@ const swaggerUI = require('swagger-ui-express');
const swaggerJsDoc = require('swagger-jsdoc');
const authRoutes = require('./routes/auth');
const publicRoutes = require('./routes/public');
const fileRoutes = require('./routes/file');
const searchRoutes = require('./routes/search');
const sqlRoutes = require('./routes/sql');
@ -123,6 +124,7 @@ require('./auth/auth');
app.use(bodyParser.json());
app.use('/api/public', publicRoutes);
app.use('/api/auth', authRoutes);
app.use('/api/file', fileRoutes);
app.use('/api/pexels', pexelsRoutes);

View File

@ -0,0 +1,67 @@
const express = require('express');
const router = express.Router();
const db = require('../db/models');
const wrapAsync = require('../helpers').wrapAsync;
const { LocalAIApi } = require("../ai/LocalAIApi");
// Get Top 3 Products
router.get('/top-products', wrapAsync(async (req, res) => {
const products = await db.products.findAll({
where: { status: 'active' },
order: [['total_sales_count', 'DESC']],
limit: 3,
include: [{
model: db.file,
as: 'images',
}]
});
res.status(200).send(products);
}));
// Get FAQs for Chatbot
router.get('/faq', wrapAsync(async (req, res) => {
const faqs = await db.faq_articles.findAll({
order: [['sort_order', 'ASC']],
});
res.status(200).send(faqs);
}));
// Public Chatbot endpoint
router.post('/chatbot', wrapAsync(async (req, res) => {
const { message } = req.body;
// Fetch FAQs to provide context
const faqs = await db.faq_articles.findAll();
const faqContext = faqs.map(f => `Q: ${f.question}\nA: ${f.answer}`).join('\n\n');
const prompt = `
You are a helpful customer support assistant for our Shopify store.
Use the following FAQ information to answer the user's question.
If the answer is not in the FAQ, be polite and ask them to contact support.
FAQ Context:
${faqContext}
User Question: ${message}
`;
const resp = await LocalAIApi.createResponse(
{
input: [
{ role: "system", content: "You are a helpful customer support assistant." },
{ role: "user", content: prompt },
],
},
{ poll_interval: 2, poll_timeout: 60 }
);
if (resp.success) {
const text = LocalAIApi.extractText(resp);
res.status(200).send({ text });
} else {
res.status(500).send({ error: "AI failed to respond" });
}
}));
module.exports = router;

View File

@ -0,0 +1,106 @@
import React, { useState, useEffect } from 'react';
import { mdiChatProcessing, mdiClose, mdiSend } from '@mdi/js';
import BaseIcon from './BaseIcon';
import axios from 'axios';
const ChatbotWidget = () => {
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState([
{ role: 'bot', text: 'Hello! How can I help you today?' },
]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const toggleChat = () => setIsOpen(!isOpen);
const sendMessage = async () => {
if (!input.trim()) return;
const userMessage = { role: 'user', text: input };
setMessages((prev) => [...prev, userMessage]);
setInput('');
setIsLoading(true);
try {
const response = await axios.post('/public/chatbot', { message: input });
const botMessage = { role: 'bot', text: response.data.text || "I'm sorry, I couldn't understand that." };
setMessages((prev) => [...prev, botMessage]);
} catch (error) {
console.error('Chatbot error:', error);
setMessages((prev) => [...prev, { role: 'bot', text: 'Sorry, I am having trouble connecting right now.' }]);
} finally {
setIsLoading(false);
}
};
return (
<div className="fixed bottom-6 right-6 z-50">
{!isOpen && (
<button
onClick={toggleChat}
className="bg-blue-600 hover:bg-blue-700 text-white rounded-full p-4 shadow-lg transition-transform transform hover:scale-110"
>
<BaseIcon path={mdiChatProcessing} size={32} />
</button>
)}
{isOpen && (
<div className="bg-white rounded-2xl shadow-2xl w-80 md:w-96 flex flex-col overflow-hidden border border-gray-100 h-[500px]">
<div className="bg-blue-600 p-4 text-white flex justify-between items-center">
<h3 className="font-bold">Store Assistant</h3>
<button onClick={toggleChat} className="hover:opacity-75">
<BaseIcon path={mdiClose} size={24} />
</button>
</div>
<div className="flex-1 p-4 overflow-y-auto space-y-4 bg-gray-50">
{messages.map((msg, idx) => (
<div
key={idx}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[80%] p-3 rounded-2xl text-sm ${
msg.role === 'user'
? 'bg-blue-600 text-white rounded-tr-none'
: 'bg-white text-gray-800 shadow-sm border border-gray-200 rounded-tl-none'
}`}
>
{msg.text}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white p-3 rounded-2xl text-sm shadow-sm border border-gray-200 animate-pulse">
Typing...
</div>
</div>
)}
</div>
<div className="p-4 bg-white border-t flex space-x-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
placeholder="Type your question..."
className="flex-1 border rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={sendMessage}
disabled={isLoading}
className="bg-blue-600 text-white rounded-full p-2 hover:bg-blue-700 disabled:opacity-50"
>
<BaseIcon path={mdiSend} size={20} />
</button>
</div>
</div>
)}
</div>
);
};
export default ChatbotWidget;

View File

@ -1,6 +1,6 @@
import * as icon from '@mdi/js';
import Head from 'next/head'
import React from 'react'
import React, { useState, useEffect } from 'react'
import axios from 'axios';
import type { ReactElement } from 'react'
import LayoutAuthenticated from '../layouts/Authenticated'
@ -9,6 +9,8 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
import BaseIcon from "../components/BaseIcon";
import { getPageTitle } from '../config'
import Link from "next/link";
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import { hasPermission } from "../helpers/userPermissions";
import { fetchWidgets } from '../stores/roles/rolesSlice';
@ -16,6 +18,7 @@ import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
const Dashboard = () => {
const dispatch = useAppDispatch();
const iconsColor = useAppSelector((state) => state.style.iconsColor);
@ -24,34 +27,34 @@ const Dashboard = () => {
const loadingMessage = 'Loading...';
const [users, setUsers] = useState(loadingMessage);
const [roles, setRoles] = useState(loadingMessage);
const [permissions, setPermissions] = useState(loadingMessage);
const [organizations, setOrganizations] = useState(loadingMessage);
const [stores, setStores] = useState(loadingMessage);
const [pages, setPages] = useState(loadingMessage);
const [collections, setCollections] = useState(loadingMessage);
const [products, setProducts] = useState(loadingMessage);
const [product_variants, setProduct_variants] = useState(loadingMessage);
const [customers, setCustomers] = useState(loadingMessage);
const [addresses, setAddresses] = useState(loadingMessage);
const [orders, setOrders] = useState(loadingMessage);
const [order_items, setOrder_items] = useState(loadingMessage);
const [fulfillments, setFulfillments] = useState(loadingMessage);
const [payments, setPayments] = useState(loadingMessage);
const [discount_codes, setDiscount_codes] = useState(loadingMessage);
const [refunds, setRefunds] = useState(loadingMessage);
const [inventory_adjustments, setInventory_adjustments] = useState(loadingMessage);
const [faq_articles, setFaq_articles] = useState(loadingMessage);
const [chatbot_configs, setChatbot_configs] = useState(loadingMessage);
const [chat_sessions, setChat_sessions] = useState(loadingMessage);
const [chat_messages, setChat_messages] = useState(loadingMessage);
const [support_tickets, setSupport_tickets] = useState(loadingMessage);
const [shopify_sync_jobs, setShopify_sync_jobs] = useState(loadingMessage);
const [users, setUsers] = React.useState(loadingMessage);
const [roles, setRoles] = React.useState(loadingMessage);
const [permissions, setPermissions] = React.useState(loadingMessage);
const [organizations, setOrganizations] = React.useState(loadingMessage);
const [stores, setStores] = React.useState(loadingMessage);
const [pages, setPages] = React.useState(loadingMessage);
const [collections, setCollections] = React.useState(loadingMessage);
const [products, setProducts] = React.useState(loadingMessage);
const [product_variants, setProduct_variants] = React.useState(loadingMessage);
const [customers, setCustomers] = React.useState(loadingMessage);
const [addresses, setAddresses] = React.useState(loadingMessage);
const [orders, setOrders] = React.useState(loadingMessage);
const [order_items, setOrder_items] = React.useState(loadingMessage);
const [fulfillments, setFulfillments] = React.useState(loadingMessage);
const [payments, setPayments] = React.useState(loadingMessage);
const [discount_codes, setDiscount_codes] = React.useState(loadingMessage);
const [refunds, setRefunds] = React.useState(loadingMessage);
const [inventory_adjustments, setInventory_adjustments] = React.useState(loadingMessage);
const [faq_articles, setFaq_articles] = React.useState(loadingMessage);
const [chatbot_configs, setChatbot_configs] = React.useState(loadingMessage);
const [chat_sessions, setChat_sessions] = React.useState(loadingMessage);
const [chat_messages, setChat_messages] = React.useState(loadingMessage);
const [support_tickets, setSupport_tickets] = React.useState(loadingMessage);
const [shopify_sync_jobs, setShopify_sync_jobs] = React.useState(loadingMessage);
const [topProducts, setTopProducts] = useState([]);
const [widgetsRole, setWidgetsRole] = React.useState({
const [widgetsRole, setWidgetsRole] = useState({
role: { value: '', label: '' },
});
const { currentUser } = useAppSelector((state) => state.auth);
@ -59,22 +62,17 @@ const Dashboard = () => {
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
const organizationId = currentUser?.organizations?.id;
async function loadData() {
const entities = ['users','roles','permissions','organizations','stores','pages','collections','products','product_variants','customers','addresses','orders','order_items','fulfillments','payments','discount_codes','refunds','inventory_adjustments','faq_articles','chatbot_configs','chat_sessions','chat_messages','support_tickets','shopify_sync_jobs',];
const fns = [setUsers,setRoles,setPermissions,setOrganizations,setStores,setPages,setCollections,setProducts,setProduct_variants,setCustomers,setAddresses,setOrders,setOrder_items,setFulfillments,setPayments,setDiscount_codes,setRefunds,setInventory_adjustments,setFaq_articles,setChatbot_configs,setChat_sessions,setChat_messages,setSupport_tickets,setShopify_sync_jobs,];
const requests = entities.map((entity, index) => {
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
return axios.get(`/${entity.toLowerCase()}/count`);
} else {
fns[index](null);
return Promise.resolve({data: {count: null}});
}
});
Promise.allSettled(requests).then((results) => {
@ -86,18 +84,26 @@ const Dashboard = () => {
}
});
});
try {
const res = await axios.get('/public/top-products');
setTopProducts(res.data);
} catch (err) {
console.error('Failed to fetch top products:', err);
}
}
async function getWidgets(roleId) {
await dispatch(fetchWidgets(roleId));
}
React.useEffect(() => {
useEffect(() => {
if (!currentUser) return;
loadData().then();
setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
}, [currentUser]);
React.useEffect(() => {
useEffect(() => {
if (!currentUser || !widgetsRole?.role?.value) return;
getWidgets(widgetsRole?.role?.value || '').then();
}, [widgetsRole?.role?.value]);
@ -117,15 +123,52 @@ const Dashboard = () => {
{''}
</SectionTitleLineWithButton>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<CardBox className="lg:col-span-2">
<SectionTitleLineWithButton icon={icon.mdiStar} title="Top 3 Bestselling Products" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{topProducts.map((product) => (
<div key={product.id} className="border rounded-xl p-4 flex flex-col items-center text-center space-y-2">
<div className="w-16 h-16 bg-blue-50 rounded-lg flex items-center justify-center text-blue-600">
<BaseIcon path={icon.mdiPackageVariant} size={32} />
</div>
<div className="font-bold text-sm truncate w-full">{product.product_title}</div>
<div className="text-xs text-gray-500">Sales: {product.total_sales_count || 0}</div>
<div className="text-xs font-mono text-blue-600 font-bold">Rank #{product.current_sales_rank || 'N/A'}</div>
</div>
))}
{topProducts.length === 0 && <div className="col-span-3 text-center py-8 text-gray-400">No sales data available yet.</div>}
</div>
</CardBox>
<CardBox>
<SectionTitleLineWithButton icon={icon.mdiRobot} title="Support Bot Status" />
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">AI Agent</span>
<span className="text-green-500 text-xs font-bold uppercase tracking-wider flex items-center">
<span className="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></span>
Online
</span>
</div>
<div className="p-3 bg-gray-50 rounded-lg text-xs text-gray-600 leading-relaxed italic">
&quot;Our AI bot is currently helping customers using information from your FAQ articles.&quot;
</div>
<BaseButton href="/faq_articles/faq_articles-list" label="Update FAQ Context" color="lightDark" small className="w-full" />
</div>
</CardBox>
</div>
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
currentUser={currentUser}
isFetchingQuery={isFetchingQuery}
setWidgetsRole={setWidgetsRole}
widgetsRole={widgetsRole}
/>}
{!!rolesWidgets.length &&
hasPermission(currentUser, 'CREATE_ROLES') && (
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
<p className='text-gray-500 dark:text-gray-400 mb-4'>
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
</p>
)}
@ -158,682 +201,54 @@ const Dashboard = () => {
{!!rolesWidgets.length && <hr className='my-6 ' />}
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Users
</div>
<div className="text-3xl leading-tight font-semibold">
{users}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiAccountGroup || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Roles
</div>
<div className="text-3xl leading-tight font-semibold">
{roles}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Permissions
</div>
<div className="text-3xl leading-tight font-semibold">
{permissions}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiShieldAccountOutline || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_ORGANIZATIONS') && <Link href={'/organizations/organizations-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Organizations
</div>
<div className="text-3xl leading-tight font-semibold">
{organizations}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_STORES') && <Link href={'/stores/stores-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Stores
</div>
<div className="text-3xl leading-tight font-semibold">
{stores}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiStore' in icon ? icon['mdiStore' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_PAGES') && <Link href={'/pages/pages-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Pages
</div>
<div className="text-3xl leading-tight font-semibold">
{pages}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiFileDocument' in icon ? icon['mdiFileDocument' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_COLLECTIONS') && <Link href={'/collections/collections-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Collections
</div>
<div className="text-3xl leading-tight font-semibold">
{collections}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiFolderMultipleImage' in icon ? icon['mdiFolderMultipleImage' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6'>
{hasPermission(currentUser, 'READ_PRODUCTS') && <Link href={'/products/products-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Products
</div>
<div className="text-3xl leading-tight font-semibold">
{products}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiPackageVariant' in icon ? icon['mdiPackageVariant' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_PRODUCT_VARIANTS') && <Link href={'/product_variants/product_variants-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Product variants
</div>
<div className="text-3xl leading-tight font-semibold">
{product_variants}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiShape' in icon ? icon['mdiShape' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_CUSTOMERS') && <Link href={'/customers/customers-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Customers
</div>
<div className="text-3xl leading-tight font-semibold">
{customers}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiAccountGroup' in icon ? icon['mdiAccountGroup' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_ADDRESSES') && <Link href={'/addresses/addresses-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Addresses
</div>
<div className="text-3xl leading-tight font-semibold">
{addresses}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiMapMarker' in icon ? icon['mdiMapMarker' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">Products</div>
<div className="text-3xl leading-tight font-semibold">{products}</div>
</div>
<BaseIcon className={`${iconsColor}`} w="w-16" h="h-16" size={48} path={icon.mdiPackageVariant} />
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_ORDERS') && <Link href={'/orders/orders-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Orders
</div>
<div className="text-3xl leading-tight font-semibold">
{orders}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiCart' in icon ? icon['mdiCart' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">Orders</div>
<div className="text-3xl leading-tight font-semibold">{orders}</div>
</div>
<BaseIcon className={`${iconsColor}`} w="w-16" h="h-16" size={48} path={icon.mdiCart} />
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_ORDER_ITEMS') && <Link href={'/order_items/order_items-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
{hasPermission(currentUser, 'READ_CUSTOMERS') && <Link href={'/customers/customers-list'}>
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Order items
</div>
<div className="text-3xl leading-tight font-semibold">
{order_items}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiFormatListBulleted' in icon ? icon['mdiFormatListBulleted' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_FULFILLMENTS') && <Link href={'/fulfillments/fulfillments-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Fulfillments
</div>
<div className="text-3xl leading-tight font-semibold">
{fulfillments}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiTruckDelivery' in icon ? icon['mdiTruckDelivery' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">Customers</div>
<div className="text-3xl leading-tight font-semibold">{customers}</div>
</div>
<BaseIcon className={`${iconsColor}`} w="w-16" h="h-16" size={48} path={icon.mdiAccountGroup} />
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_PAYMENTS') && <Link href={'/payments/payments-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Payments
</div>
<div className="text-3xl leading-tight font-semibold">
{payments}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiCreditCard' in icon ? icon['mdiCreditCard' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">Payments</div>
<div className="text-3xl leading-tight font-semibold">{payments}</div>
</div>
<BaseIcon className={`${iconsColor}`} w="w-16" h="h-16" size={48} path={icon.mdiCreditCard} />
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_DISCOUNT_CODES') && <Link href={'/discount_codes/discount_codes-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Discount codes
</div>
<div className="text-3xl leading-tight font-semibold">
{discount_codes}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiTag' in icon ? icon['mdiTag' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_REFUNDS') && <Link href={'/refunds/refunds-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Refunds
</div>
<div className="text-3xl leading-tight font-semibold">
{refunds}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiCashRefund' in icon ? icon['mdiCashRefund' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_INVENTORY_ADJUSTMENTS') && <Link href={'/inventory_adjustments/inventory_adjustments-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Inventory adjustments
</div>
<div className="text-3xl leading-tight font-semibold">
{inventory_adjustments}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiWarehouse' in icon ? icon['mdiWarehouse' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_FAQ_ARTICLES') && <Link href={'/faq_articles/faq_articles-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Faq articles
</div>
<div className="text-3xl leading-tight font-semibold">
{faq_articles}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiHelpCircle' in icon ? icon['mdiHelpCircle' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_CHATBOT_CONFIGS') && <Link href={'/chatbot_configs/chatbot_configs-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Chatbot configs
</div>
<div className="text-3xl leading-tight font-semibold">
{chatbot_configs}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiRobot' in icon ? icon['mdiRobot' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_CHAT_SESSIONS') && <Link href={'/chat_sessions/chat_sessions-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Chat sessions
</div>
<div className="text-3xl leading-tight font-semibold">
{chat_sessions}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiChat' in icon ? icon['mdiChat' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_CHAT_MESSAGES') && <Link href={'/chat_messages/chat_messages-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Chat messages
</div>
<div className="text-3xl leading-tight font-semibold">
{chat_messages}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiMessageText' in icon ? icon['mdiMessageText' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_SUPPORT_TICKETS') && <Link href={'/support_tickets/support_tickets-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Support tickets
</div>
<div className="text-3xl leading-tight font-semibold">
{support_tickets}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiTicketAccount' in icon ? icon['mdiTicketAccount' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_SHOPIFY_SYNC_JOBS') && <Link href={'/shopify_sync_jobs/shopify_sync_jobs-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Shopify sync jobs
</div>
<div className="text-3xl leading-tight font-semibold">
{shopify_sync_jobs}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiSync' in icon ? icon['mdiSync' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
</div>
</SectionMain>
</>

View File

@ -1,4 +1,3 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
@ -13,7 +12,12 @@ import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
import BaseIcon from '../components/BaseIcon';
import axios from 'axios';
import ChatbotWidget from '../components/ChatbotWidget';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { mdiShopping, mdiStar } from '@mdi/js';
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
@ -24,17 +28,25 @@ export default function Starter() {
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('video');
const [contentPosition, setContentPosition] = useState('left');
const [topProducts, setTopProducts] = useState([]);
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'Shopify Ops Dashboard'
// Fetch Pexels image/video
// Fetch Pexels image/video and top products
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
try {
const res = await axios.get('/public/top-products');
setTopProducts(res.data);
} catch (err) {
console.error('Failed to fetch top products:', err);
}
}
fetchData();
}, []);
@ -94,24 +106,9 @@ export default function Starter() {
};
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',
}
: {}
}
>
<div className="bg-white min-h-screen">
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('Store Home')}</title>
</Head>
<SectionFullScreen bg='violet'>
@ -126,36 +123,85 @@ export default function Starter() {
{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 Shopify Ops 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 className='flex items-center justify-center flex-col space-y-8 w-full lg:w-full p-6'>
<div className="text-center space-y-4 max-w-2xl">
<h1 className="text-5xl font-black text-gray-900 tracking-tight leading-tight">
Modern Operations for your <span className="text-blue-600">Shopify Store</span>
</h1>
<p className="text-xl text-gray-500 font-medium">
The all-in-one dashboard to manage your products, orders, and customer support with AI.
</p>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
<CardBox className='w-full md:w-3/5 lg:w-2/3 shadow-xl border-none'>
<CardBoxComponentTitle title="Manage your Business"/>
</BaseButtons>
</CardBox>
<div className="space-y-4">
<p className='text-center text-gray-500'>Access your products, customers, and order lifecycle in one integrated workspace.</p>
</div>
<BaseButtons className="mt-6 flex justify-center">
<BaseButton
href='/login'
label='Go to Dashboard'
color='info'
roundedFull
className='px-12 py-3 text-lg font-bold'
/>
</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/'>
<SectionMain>
<SectionTitleLineWithButton icon={mdiStar} title="Top 3 Bestsellers" main>
{''}
</SectionTitleLineWithButton>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mt-6">
{topProducts.length > 0 ? topProducts.map((product) => (
<div key={product.id} className="group relative bg-white border border-gray-100 rounded-3xl p-6 shadow-sm hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2">
<div className="aspect-square w-full overflow-hidden rounded-2xl bg-gray-100 mb-6">
{product.images?.[0] ? (
<img
src={`/api/file/download?id=${product.images[0].id}`}
alt={product.product_title}
className="h-full w-full object-cover object-center group-hover:scale-110 transition-transform duration-500"
/>
) : (
<div className="flex items-center justify-center h-full bg-blue-50 text-blue-200">
<BaseIcon path={mdiShopping} size={64} />
</div>
)}
</div>
<div className="space-y-2">
<h3 className="text-lg font-bold text-gray-900 group-hover:text-blue-600 transition-colors">{product.product_title}</h3>
<p className="text-sm text-gray-500 line-clamp-2">{product.description}</p>
<div className="pt-4 flex items-center justify-between">
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
Bestseller
</span>
<span className="text-xs text-gray-400 font-mono uppercase tracking-wider">#{product.current_sales_rank || 'N/A'}</span>
</div>
</div>
</div>
)) : (
<div className="col-span-3 text-center py-12 text-gray-400 italic">
Loading featured products...
</div>
)}
</div>
</SectionMain>
<div className='bg-gray-900 text-white flex flex-col text-center justify-center md:flex-row py-12'>
<p className='text-sm text-gray-400'>© 2026 <span className="text-white font-bold">{title}</span>. All rights reserved</p>
<Link className='ml-4 text-sm text-gray-400 hover:text-white underline underline-offset-4' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
<ChatbotWidget />
</div>
);
}
@ -163,4 +209,3 @@ export default function Starter() {
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};