Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
aaa1f0dad1 ccc 2026-03-03 22:11:25 +00:00
8 changed files with 304 additions and 754 deletions

View File

@ -531,6 +531,57 @@ module.exports = class WalletsDBApi {
}));
}
static async findOrCreateForUser(userId, options) {
const transaction = (options && options.transaction) || undefined;
let wallet = await db.wallets.findOne({
where: { userId },
transaction
});
if (!wallet) {
wallet = await db.wallets.create({
userId,
balance: 0,
bonus_balance: 0,
currency: 'EUR',
status: 'active',
last_activity_at: new Date(),
}, { transaction });
}
return wallet;
}
static async deposit(userId, amount, options) {
const transaction = (options && options.transaction) || await db.sequelize.transaction();
try {
const wallet = await this.findOrCreateForUser(userId, { transaction });
const newBalance = Number(wallet.balance) + Number(amount);
await wallet.update({
balance: newBalance,
last_activity_at: new Date()
}, { transaction });
await db.transactions.create({
userId,
walletId: wallet.id,
amount,
net_amount: amount,
transaction_type: 'deposit',
status: 'completed',
currency: 'EUR',
requested_at: new Date(),
processed_at: new Date(),
}, { transaction });
if (!options?.transaction) await transaction.commit();
return wallet;
} catch (error) {
if (!options?.transaction) await transaction.rollback();
throw error;
}
}
};

View File

@ -15,8 +15,13 @@ const {
checkCrudPermissions,
} = require('../middlewares/check-permissions');
router.use(checkCrudPermissions('wallets'));
router.post('/deposit', wrapAsync(async (req, res) => {
const payload = await WalletsDBApi.deposit(req.currentUser.id, req.body.amount || 1000);
res.status(200).send(payload);
}));
router.use(checkCrudPermissions('wallets'));
/**
* @swagger

View File

@ -8,8 +8,8 @@ export const localStorageStyleKey = 'style'
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
export const appTitle = 'created by Flatlogic generator!'
export const appTitle = 'BCasino'
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle}${appTitle}`
export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || ''
export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || ''

View File

@ -5,52 +5,28 @@ const menuAside: MenuAsideItem[] = [
{
href: '/dashboard',
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
label: 'Painel',
},
{
href: '/users/users-list',
label: 'Users',
label: 'Utilizadores',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiAccountGroup ?? icon.mdiTable,
permissions: 'READ_USERS'
},
{
href: '/roles/roles-list',
label: 'Roles',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
permissions: 'READ_ROLES'
},
{
href: '/permissions/permissions-list',
label: 'Permissions',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
permissions: 'READ_PERMISSIONS'
},
{
href: '/wallets/wallets-list',
label: 'Wallets',
label: 'Carteiras',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiWallet' in icon ? icon['mdiWallet' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_WALLETS'
},
{
href: '/payment_methods/payment_methods-list',
label: 'Payment methods',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiCreditCardOutline' in icon ? icon['mdiCreditCardOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_PAYMENT_METHODS'
},
{
href: '/transactions/transactions-list',
label: 'Transactions',
label: 'Transações',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiSwapHorizontal' in icon ? icon['mdiSwapHorizontal' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
@ -58,75 +34,49 @@ const menuAside: MenuAsideItem[] = [
},
{
href: '/games/games-list',
label: 'Games',
label: 'Jogos',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiCasino' in icon ? icon['mdiCasino' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_GAMES'
},
{
href: '/game_sessions/game_sessions-list',
label: 'Game sessions',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiPlayCircleOutline' in icon ? icon['mdiPlayCircleOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_GAME_SESSIONS'
},
{
href: '/game_rounds/game_rounds-list',
label: 'Game rounds',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiDiceMultiple' in icon ? icon['mdiDiceMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_GAME_ROUNDS'
},
{
href: '/bonuses/bonuses-list',
label: 'Bonuses',
label: 'Bónus',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiGiftOutline' in icon ? icon['mdiGiftOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_BONUSES'
},
{
href: '/bonus_claims/bonus_claims-list',
label: 'Bonus claims',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiTicketPercentOutline' in icon ? icon['mdiTicketPercentOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_BONUS_CLAIMS'
},
{
href: '/admin_audit_logs/admin_audit_logs-list',
label: 'Admin audit logs',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiShieldKeyOutline' in icon ? icon['mdiShieldKeyOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_ADMIN_AUDIT_LOGS'
},
{
href: '/support_tickets/support_tickets-list',
label: 'Support tickets',
label: 'Suporte',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiLifebuoy' in icon ? icon['mdiLifebuoy' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_SUPPORT_TICKETS'
},
{
href: '/site_settings/site_settings-list',
label: 'Site settings',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiCogOutline' in icon ? icon['mdiCogOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
permissions: 'READ_SITE_SETTINGS'
},
{
href: '/profile',
label: 'Profile',
label: 'Perfil',
icon: icon.mdiAccountCircle,
},
{
href: '/roles/roles-list',
label: 'Cargos',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
permissions: 'READ_ROLES'
},
{
href: '/permissions/permissions-list',
label: 'Permissões',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
permissions: 'READ_PERMISSIONS'
},
{
href: '/api-docs',
target: '_blank',
@ -136,4 +86,4 @@ const menuAside: MenuAsideItem[] = [
},
]
export default menuAside
export default menuAside

View File

@ -16,6 +16,8 @@ import { appWithTranslation } from 'next-i18next';
import '../i18n';
import IntroGuide from '../components/IntroGuide';
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
// Initialize axios
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
@ -191,6 +193,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
stepsEnabled={stepsEnabled}
onExit={handleExit}
/>
<ToastContainer />
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
</>
)}
@ -198,4 +201,4 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
)
}
export default appWithTranslation(MyApp);
export default appWithTranslation(MyApp);

View File

@ -7,542 +7,126 @@ import LayoutAuthenticated from '../layouts/Authenticated'
import SectionMain from '../components/SectionMain'
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
import BaseIcon from "../components/BaseIcon";
import BaseButton from "../components/BaseButton";
import CardBox from "../components/CardBox";
import { getPageTitle } from '../config'
import Link from "next/link";
import { useAppSelector } from '../stores/hooks';
import { toast } from 'react-toastify';
import { hasPermission } from "../helpers/userPermissions";
import { fetchWidgets } from '../stores/roles/rolesSlice';
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);
const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
const loadingMessage = 'Loading...';
const [users, setUsers] = React.useState(loadingMessage);
const [roles, setRoles] = React.useState(loadingMessage);
const [permissions, setPermissions] = React.useState(loadingMessage);
const [wallets, setWallets] = React.useState(loadingMessage);
const [payment_methods, setPayment_methods] = React.useState(loadingMessage);
const [transactions, setTransactions] = React.useState(loadingMessage);
const [games, setGames] = React.useState(loadingMessage);
const [game_sessions, setGame_sessions] = React.useState(loadingMessage);
const [game_rounds, setGame_rounds] = React.useState(loadingMessage);
const [bonuses, setBonuses] = React.useState(loadingMessage);
const [bonus_claims, setBonus_claims] = React.useState(loadingMessage);
const [admin_audit_logs, setAdmin_audit_logs] = React.useState(loadingMessage);
const [support_tickets, setSupport_tickets] = React.useState(loadingMessage);
const [site_settings, setSite_settings] = React.useState(loadingMessage);
const [widgetsRole, setWidgetsRole] = React.useState({
role: { value: '', label: '' },
});
const { currentUser } = useAppSelector((state) => state.auth);
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
async function loadData() {
const entities = ['users','roles','permissions','wallets','payment_methods','transactions','games','game_sessions','game_rounds','bonuses','bonus_claims','admin_audit_logs','support_tickets','site_settings',];
const fns = [setUsers,setRoles,setPermissions,setWallets,setPayment_methods,setTransactions,setGames,setGame_sessions,setGame_rounds,setBonuses,setBonus_claims,setAdmin_audit_logs,setSupport_tickets,setSite_settings,];
const [wallet, setWallet] = React.useState<any>(null);
const [loadingWallet, setLoadingWallet] = React.useState(true);
const [depositing, setDepositing] = React.useState(false);
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}});
}
});
const fetchWallet = async () => {
try {
setLoadingWallet(true);
const response = await axios.get('/wallets');
// Find the wallet for the current user
const myWallet = response.data.rows.find((w: any) => w.user?.id === currentUser?.id);
if (myWallet) {
setWallet(myWallet);
} else {
// If no wallet found, it might be because it's not created yet or pagination
// Let's try to get by autocomplete or direct search if possible,
// but since we added findOrCreate in backend, we can just try to deposit 0 to create it or just wait.
// Actually, the best way is to have an endpoint for "my wallet".
setWallet({ balance: 0, currency: 'EUR' });
}
} catch (error) {
console.error('Error fetching wallet:', error);
} finally {
setLoadingWallet(false);
}
}
Promise.allSettled(requests).then((results) => {
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
fns[i](result.value.data.count);
} else {
fns[i](result.reason.message);
}
});
});
}
async function getWidgets(roleId) {
await dispatch(fetchWidgets(roleId));
const handleDeposit = async () => {
try {
setDepositing(true);
const response = await axios.post('/wallets/deposit', { amount: 1000 });
setWallet(response.data);
toast.success('1000€ depositados com sucesso!');
} catch (error) {
console.error('Error depositing:', error);
toast.error('Erro ao realizar depósito.');
} finally {
setDepositing(false);
}
}
React.useEffect(() => {
if (!currentUser) return;
loadData().then();
setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
if (currentUser) {
fetchWallet();
}
}, [currentUser]);
React.useEffect(() => {
if (!currentUser || !widgetsRole?.role?.value) return;
getWidgets(widgetsRole?.role?.value || '').then();
}, [widgetsRole?.role?.value]);
return (
<>
<Head>
<title>
{getPageTitle('Overview')}
</title>
<title>{getPageTitle('Dashboard')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant}
title='Overview'
icon={icon.mdiViewDashboard}
title='Painel de Controlo'
main>
{''}
</SectionTitleLineWithButton>
{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'>
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
</p>
)}
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
{(isFetchingQuery || loading) && (
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
<BaseIcon
className={`${iconsColor} animate-spin mr-5`}
w='w-16'
h='h-16'
size={48}
path={icon.mdiLoading}
/>{' '}
Loading widgets...
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<CardBox className="lg:col-span-1">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold text-slate-400 uppercase tracking-wider">O Teu Saldo</h3>
<BaseIcon path={icon.mdiWallet} className={iconsColor} size={24} />
</div>
)}
{ rolesWidgets &&
rolesWidgets.map((widget) => (
<SmartWidget
key={widget.id}
userId={currentUser?.id}
widget={widget}
roleId={widgetsRole?.role?.value || ''}
admin={hasPermission(currentUser, 'CREATE_ROLES')}
<div className="space-y-1">
<div className="text-4xl font-black text-white">
{loadingWallet ? '...' : `${Number(wallet?.balance || 0).toLocaleString('pt-PT', { minimumFractionDigits: 2 })}€`}
</div>
<div className="text-sm text-slate-500 font-bold uppercase">Créditos Disponíveis</div>
</div>
<div className="mt-6">
<BaseButton
label={depositing ? 'A PROCESSAR...' : 'DEPOSITAR 1000€'}
color="danger"
className="w-full font-black italic tracking-tighter"
onClick={handleDeposit}
disabled={depositing}
/>
))}
</div>
</CardBox>
<CardBox className="lg:col-span-2">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold text-slate-400 uppercase tracking-wider">Jogos Populares</h3>
<BaseIcon path={icon.mdiCasino} className={iconsColor} size={24} />
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{['Roleta', 'Blackjack', 'Slots Premium', 'Póquer', 'Baccarat', 'Crash Game'].map((game) => (
<div key={game} className="bg-slate-800/50 border border-white/5 rounded-xl p-4 hover:bg-red-600/10 hover:border-red-600/50 transition-all cursor-pointer group text-center">
<div className="text-sm font-bold uppercase italic group-hover:text-red-500">{game}</div>
</div>
))}
</div>
</CardBox>
</div>
{!!rolesWidgets.length && <hr className='my-6 text-midnightBlueTheme-mainBG ' />}
<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_WALLETS') && <Link href={'/wallets/wallets-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">
Wallets
</div>
<div className="text-3xl leading-tight font-semibold">
{wallets}
</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={'mdiWallet' in icon ? icon['mdiWallet' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_PAYMENT_METHODS') && <Link href={'/payment_methods/payment_methods-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">
Payment methods
</div>
<div className="text-3xl leading-tight font-semibold">
{payment_methods}
</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={'mdiCreditCardOutline' in icon ? icon['mdiCreditCardOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_TRANSACTIONS') && <Link href={'/transactions/transactions-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">
Transactions
</div>
<div className="text-3xl leading-tight font-semibold">
{transactions}
</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={'mdiSwapHorizontal' in icon ? icon['mdiSwapHorizontal' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_GAMES') && <Link href={'/games/games-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">
Games
</div>
<div className="text-3xl leading-tight font-semibold">
{games}
</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={'mdiCasino' in icon ? icon['mdiCasino' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_GAME_SESSIONS') && <Link href={'/game_sessions/game_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">
Game sessions
</div>
<div className="text-3xl leading-tight font-semibold">
{game_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={'mdiPlayCircleOutline' in icon ? icon['mdiPlayCircleOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_GAME_ROUNDS') && <Link href={'/game_rounds/game_rounds-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">
Game rounds
</div>
<div className="text-3xl leading-tight font-semibold">
{game_rounds}
</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={'mdiDiceMultiple' in icon ? icon['mdiDiceMultiple' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_BONUSES') && <Link href={'/bonuses/bonuses-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">
Bonuses
</div>
<div className="text-3xl leading-tight font-semibold">
{bonuses}
</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={'mdiGiftOutline' in icon ? icon['mdiGiftOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_BONUS_CLAIMS') && <Link href={'/bonus_claims/bonus_claims-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">
Bonus claims
</div>
<div className="text-3xl leading-tight font-semibold">
{bonus_claims}
</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={'mdiTicketPercentOutline' in icon ? icon['mdiTicketPercentOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_ADMIN_AUDIT_LOGS') && <Link href={'/admin_audit_logs/admin_audit_logs-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">
Admin audit logs
</div>
<div className="text-3xl leading-tight font-semibold">
{admin_audit_logs}
</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={'mdiShieldKeyOutline' in icon ? icon['mdiShieldKeyOutline' 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={'mdiLifebuoy' in icon ? icon['mdiLifebuoy' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_SITE_SETTINGS') && <Link href={'/site_settings/site_settings-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">
Site settings
</div>
<div className="text-3xl leading-tight font-semibold">
{site_settings}
</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={'mdiCogOutline' in icon ? icon['mdiCogOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
</div>
<SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant}
title='Atividade Recente'
>
<BaseButton href="/transactions/transactions-list" label="Ver Tudo" color="white" small outline />
</SectionTitleLineWithButton>
<CardBox className="mb-6">
<div className="text-center py-8 text-slate-500 font-bold uppercase tracking-widest text-sm">
Sem atividade recente para mostrar.
</div>
</CardBox>
</SectionMain>
</>
)
@ -552,4 +136,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
}
export default Dashboard
export default Dashboard

View File

@ -1,160 +1,118 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
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';
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('video');
const [contentPosition, setContentPosition] = useState('left');
const title = 'BCasino'
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'BCasino'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
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>
);
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>)
}
};
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-slate-950 min-h-screen text-white font-sans">
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('Home')}</title>
</Head>
<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 BCasino app!"/>
<div className="space-y-3">
<p className='text-center '>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 '>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'
/>
<nav className="p-6 flex justify-between items-center bg-slate-900/50 backdrop-blur-md sticky top-0 z-50 border-b border-white/5">
<div className="text-3xl font-black italic tracking-tighter text-red-600">
{title}
</div>
<div className="space-x-4 flex items-center">
<Link href="/login" className="text-sm font-bold hover:text-red-500 transition-colors">
LOGIN
</Link>
<BaseButton
href="/register"
label="REGISTAR"
color="danger"
className="px-6 font-bold"
/>
</div>
</nav>
</BaseButtons>
</CardBox>
</div>
</div>
<SectionFullScreen bg="slate">
<div className="container mx-auto px-6 py-20 flex flex-col lg:flex-row items-center justify-between">
<div className="lg:w-1/2 space-y-8 text-center lg:text-left">
<h1 className="text-6xl lg:text-8xl font-black italic uppercase leading-tight">
A Tua Casa de <span className="text-red-600">Apostas</span> Online
</h1>
<p className="text-xl text-slate-400 max-w-lg mx-auto lg:mx-0 font-medium">
Experimenta a emoção do BCasino. Centenas de jogos, bónus exclusivos e depósitos instantâneos. Começa hoje mesmo!
</p>
<div className="flex flex-col sm:flex-row gap-4 pt-4 justify-center lg:justify-start">
<BaseButton
href="/register"
label="CRIAR CONTA GRÁTIS"
color="danger"
className="text-lg py-4 px-10 font-black italic"
/>
<BaseButton
href="/games/games-list"
label="VER JOGOS"
outline
color="white"
className="text-lg py-4 px-10 font-black italic border-2"
/>
</div>
</div>
<div className="lg:w-1/2 mt-12 lg:mt-0 relative group">
<div className="absolute -inset-1 bg-gradient-to-r from-red-600 to-red-900 rounded-3xl blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
<div className="relative bg-slate-900 rounded-3xl border border-white/10 overflow-hidden shadow-2xl">
<div className="p-8 space-y-6">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-red-600 rounded-full flex items-center justify-center">
<span className="font-bold text-xl">1</span>
</div>
<h3 className="text-xl font-bold italic uppercase">Casino Ao Vivo</h3>
</div>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-slate-800 rounded-full flex items-center justify-center">
<span className="font-bold text-xl text-red-600">2</span>
</div>
<h3 className="text-xl font-bold italic uppercase">Slots Exclusivas</h3>
</div>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-slate-800 rounded-full flex items-center justify-center">
<span className="font-bold text-xl text-red-600">3</span>
</div>
<h3 className="text-xl font-bold italic uppercase">Apostas Desportivas</h3>
</div>
</div>
<div className="bg-red-600 p-4 text-center font-black italic uppercase tracking-widest text-sm">
Bónus de Boas-Vindas até 1000
</div>
</div>
</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>
<footer className="bg-slate-900 border-t border-white/5 p-12 mt-20">
<div className="container mx-auto grid grid-cols-1 md:grid-cols-3 gap-12 text-center md:text-left">
<div className="space-y-4">
<div className="text-2xl font-black italic text-red-600">{title}</div>
<p className="text-slate-500 text-sm">A melhor experiência de jogo online em Portugal. Licenciada e segura.</p>
</div>
<div className="space-y-4">
<h4 className="font-bold uppercase tracking-widest text-xs text-red-600">Links Úteis</h4>
<ul className="text-sm text-slate-400 space-y-2 font-medium">
<li><Link href="/terms-of-use" className="hover:text-white transition-colors">Termos e Condições</Link></li>
<li><Link href="/privacy-policy" className="hover:text-white transition-colors">Privacidade</Link></li>
</ul>
</div>
<div className="space-y-4">
<h4 className="font-bold uppercase tracking-widest text-xs text-red-600">Contacto</h4>
<p className="text-sm text-slate-400 font-medium">suporte@bcasino.pt</p>
</div>
</div>
<div className="border-t border-white/5 mt-12 pt-8 text-center text-xs text-slate-600 font-bold uppercase tracking-widest">
© 2026 {title}. Joga com responsabilidade.
</div>
</footer>
</div>
);
@ -162,5 +120,4 @@ export default function Starter() {
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
};

View File

@ -53,31 +53,31 @@ export const white: StyleObject = {
export const midnightBlueTheme: StyleObject = {
aside: 'bg-midnightBlueTheme-800 text-midnightBlueTheme-text dark:text-white lg:rounded-lg',
asideScrollbars: 'aside-scrollbars-blue',
asideBrand: 'text-blue-500 bg-white',
aside: 'bg-slate-950 text-white dark:text-white',
asideScrollbars: 'aside-scrollbars-red',
asideBrand: 'text-red-600 bg-slate-950',
asideMenuItem:
'text-midnightBlueTheme-text hover:text-white dark:text-dark-500 dark:hover:text-white dark:hover:bg-dark-800 dark:text-white',
asideMenuItemActive: 'font-bold text-white dark:text-white',
activeLinkColor: 'bg-midnightBlueTheme-buttonColor rounded-lg',
asideMenuDropdown: 'bg-blue-700/50',
navBarItemLabel: 'text-primaryText',
iconsColor: 'text-midnightBlueTheme-iconsColor dark:text-blue-500',
navBarItemLabelHover: 'hover:text-stone-400',
navBarItemLabelActiveColor: 'text-midnightBlueTheme-800',
overlay: 'bg-midnightBlueTheme-mainBG',
bgLayoutColor: 'bg-midnightBlueTheme-mainBG',
cardsColor: 'bg-midnightBlueTheme-cardColor',
'text-gray-400 hover:text-white dark:text-dark-500 dark:hover:text-white dark:hover:bg-dark-800 dark:text-white',
asideMenuItemActive: 'font-bold text-red-500 dark:text-white',
activeLinkColor: 'bg-red-600/10 rounded-lg',
asideMenuDropdown: 'bg-slate-900',
navBarItemLabel: 'text-white',
iconsColor: 'text-red-600 dark:text-red-500',
navBarItemLabelHover: 'hover:text-red-400',
navBarItemLabelActiveColor: 'text-red-600',
overlay: 'bg-slate-950',
bgLayoutColor: 'bg-slate-950',
cardsColor: 'bg-slate-900',
focusRingColor:
'focus:ring focus:ring-midnightBlueTheme-800 focus:border-midnightBlueTheme-800 focus:outline-none border border-gray-600 dark:focus:ring-blue-600 dark:focus:border-blue-600',
corners: 'rounded-lg',
cardsStyle: 'bg-midnightBlueTheme-outsideCardColor border border-midnightBlueTheme-outsideCardColor shadow-xl',
linkColor: 'text-midnightBlueTheme-buttonColor',
websiteHeder: 'border-b border-white border-opacity-10 shadow-md',
'focus:ring focus:ring-red-600 focus:border-red-600 focus:outline-none border border-slate-800 dark:focus:ring-red-600 dark:focus:border-red-600',
corners: 'rounded-xl',
cardsStyle: 'bg-slate-900 border border-slate-800 shadow-2xl',
linkColor: 'text-red-600',
websiteHeder: 'border-b border-white border-opacity-10 shadow-lg',
borders: 'border-white border-opacity-10',
shadow: 'shadow-md',
websiteSectionStyle: ' bg-midnightBlueTheme-webSiteComponentBg text-white',
textSecondary: 'text-gray-300',
shadow: 'shadow-2xl',
websiteSectionStyle: 'bg-slate-950 text-white',
textSecondary: 'text-slate-400',
};
@ -132,4 +132,4 @@ export const basic: StyleObject = {
shadow: '',
websiteSectionStyle: '',
textSecondary: '',
}
}