Compare commits

..

No commits in common. "ai-dev" and "master" have entirely different histories.

8 changed files with 780 additions and 330 deletions

View File

@ -531,57 +531,6 @@ 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,14 +15,9 @@ const {
checkCrudPermissions, checkCrudPermissions,
} = require('../middlewares/check-permissions'); } = require('../middlewares/check-permissions');
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')); router.use(checkCrudPermissions('wallets'));
/** /**
* @swagger * @swagger
* components: * components:

View File

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

View File

@ -5,65 +5,20 @@ const menuAside: MenuAsideItem[] = [
{ {
href: '/dashboard', href: '/dashboard',
icon: icon.mdiViewDashboardOutline, icon: icon.mdiViewDashboardOutline,
label: 'Painel', label: 'Dashboard',
}, },
{ {
href: '/users/users-list', href: '/users/users-list',
label: 'Utilizadores', label: 'Users',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: icon.mdiAccountGroup ?? icon.mdiTable, icon: icon.mdiAccountGroup ?? icon.mdiTable,
permissions: 'READ_USERS' permissions: 'READ_USERS'
}, },
{
href: '/wallets/wallets-list',
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: '/transactions/transactions-list',
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,
permissions: 'READ_TRANSACTIONS'
},
{
href: '/games/games-list',
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: '/bonuses/bonuses-list',
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: '/support_tickets/support_tickets-list',
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: '/profile',
label: 'Perfil',
icon: icon.mdiAccountCircle,
},
{ {
href: '/roles/roles-list', href: '/roles/roles-list',
label: 'Cargos', label: 'Roles',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable, icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
@ -71,12 +26,107 @@ const menuAside: MenuAsideItem[] = [
}, },
{ {
href: '/permissions/permissions-list', href: '/permissions/permissions-list',
label: 'Permissões', label: 'Permissions',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
permissions: 'READ_PERMISSIONS' permissions: 'READ_PERMISSIONS'
}, },
{
href: '/wallets/wallets-list',
label: 'Wallets',
// 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',
// 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,
permissions: 'READ_TRANSACTIONS'
},
{
href: '/games/games-list',
label: 'Games',
// 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',
// 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',
// 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',
icon: icon.mdiAccountCircle,
},
{ {
href: '/api-docs', href: '/api-docs',
target: '_blank', target: '_blank',

View File

@ -16,8 +16,6 @@ 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 { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
// Initialize axios // Initialize axios
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
@ -193,7 +191,6 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
stepsEnabled={stepsEnabled} stepsEnabled={stepsEnabled}
onExit={handleExit} onExit={handleExit}
/> />
<ToastContainer />
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />} {(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
</> </>
)} )}

View File

@ -7,126 +7,542 @@ 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 BaseButton from "../components/BaseButton";
import CardBox from "../components/CardBox";
import { getPageTitle } from '../config' import { getPageTitle } from '../config'
import { useAppSelector } from '../stores/hooks'; import Link from "next/link";
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 Dashboard = () => {
const dispatch = useAppDispatch();
const iconsColor = useAppSelector((state) => state.style.iconsColor); 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 { currentUser } = useAppSelector((state) => state.auth);
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
const [wallet, setWallet] = React.useState<any>(null); const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
const [loadingWallet, setLoadingWallet] = React.useState(true);
const [depositing, setDepositing] = React.useState(false);
const fetchWallet = async () => {
try { async function loadData() {
setLoadingWallet(true); 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 response = await axios.get('/wallets'); const fns = [setUsers,setRoles,setPermissions,setWallets,setPayment_methods,setTransactions,setGames,setGame_sessions,setGame_rounds,setBonuses,setBonus_claims,setAdmin_audit_logs,setSupport_tickets,setSite_settings,];
// Find the wallet for the current user
const myWallet = response.data.rows.find((w: any) => w.user?.id === currentUser?.id); const requests = entities.map((entity, index) => {
if (myWallet) {
setWallet(myWallet); if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
} else { return axios.get(`/${entity.toLowerCase()}/count`);
// If no wallet found, it might be because it's not created yet or pagination } else {
// Let's try to get by autocomplete or direct search if possible, fns[index](null);
// but since we added findOrCreate in backend, we can just try to deposit 0 to create it or just wait. return Promise.resolve({data: {count: null}});
// 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); Promise.allSettled(requests).then((results) => {
} finally { results.forEach((result, i) => {
setLoadingWallet(false); if (result.status === 'fulfilled') {
} fns[i](result.value.data.count);
} else {
fns[i](result.reason.message);
}
});
});
} }
const handleDeposit = async () => { async function getWidgets(roleId) {
try { await dispatch(fetchWidgets(roleId));
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 } });
}, [currentUser]);
React.useEffect(() => { React.useEffect(() => {
if (currentUser) { if (!currentUser || !widgetsRole?.role?.value) return;
fetchWallet(); getWidgets(widgetsRole?.role?.value || '').then();
} }, [widgetsRole?.role?.value]);
}, [currentUser]);
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Dashboard')}</title> <title>
{getPageTitle('Overview')}
</title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton <SectionTitleLineWithButton
icon={icon.mdiViewDashboard} icon={icon.mdiChartTimelineVariant}
title='Painel de Controlo' title='Overview'
main> main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6"> {hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
<CardBox className="lg:col-span-1"> currentUser={currentUser}
<div className="flex items-center justify-between mb-4"> isFetchingQuery={isFetchingQuery}
<h3 className="text-lg font-bold text-slate-400 uppercase tracking-wider">O Teu Saldo</h3> setWidgetsRole={setWidgetsRole}
<BaseIcon path={icon.mdiWallet} className={iconsColor} size={24} /> widgetsRole={widgetsRole}
</div> />}
<div className="space-y-1"> {!!rolesWidgets.length &&
<div className="text-4xl font-black text-white"> hasPermission(currentUser, 'CREATE_ROLES') && (
{loadingWallet ? '...' : `${Number(wallet?.balance || 0).toLocaleString('pt-PT', { minimumFractionDigits: 2 })}€`} <p className=' text-gray-500 dark:text-gray-400 mb-4'>
</div> {`${widgetsRole?.role?.label || 'Users'}'s widgets`}
<div className="text-sm text-slate-500 font-bold uppercase">Créditos Disponíveis</div> </p>
</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='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
<div className="flex items-center justify-between mb-4"> {(isFetchingQuery || loading) && (
<h3 className="text-lg font-bold text-slate-400 uppercase tracking-wider">Jogos Populares</h3> <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 path={icon.mdiCasino} className={iconsColor} size={24} /> <BaseIcon
className={`${iconsColor} animate-spin mr-5`}
w='w-16'
h='h-16'
size={48}
path={icon.mdiLoading}
/>{' '}
Loading widgets...
</div> </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"> { rolesWidgets &&
<div className="text-sm font-bold uppercase italic group-hover:text-red-500">{game}</div> rolesWidgets.map((widget) => (
</div> <SmartWidget
))} key={widget.id}
</div> userId={currentUser?.id}
</CardBox> widget={widget}
roleId={widgetsRole?.role?.value || ''}
admin={hasPermission(currentUser, 'CREATE_ROLES')}
/>
))}
</div> </div>
<SectionTitleLineWithButton {!!rolesWidgets.length && <hr className='my-6 text-midnightBlueTheme-mainBG ' />}
icon={icon.mdiChartTimelineVariant}
title='Atividade Recente'
>
<BaseButton href="/transactions/transactions-list" label="Ver Tudo" color="white" small outline />
</SectionTitleLineWithButton>
<CardBox className="mb-6"> <div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 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>
{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>
</SectionMain> </SectionMain>
</> </>
) )

View File

@ -1,118 +1,160 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import BaseButton from '../components/BaseButton'; import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen'; import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest'; 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 { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
export default function Starter() { export default function Starter() {
const title = 'BCasino' 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 textColor = useAppSelector((state) => state.style.linkColor); 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 ( return (
<div className="bg-slate-950 min-h-screen text-white font-sans"> <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('Home')}</title> <title>{getPageTitle('Starter Page')}</title>
</Head> </Head>
<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"> <SectionFullScreen bg='violet'>
<div className="text-3xl font-black italic tracking-tighter text-red-600"> <div
{title} className={`flex ${
</div> contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
<div className="space-x-4 flex items-center"> } min-h-screen w-full`}
<Link href="/login" className="text-sm font-bold hover:text-red-500 transition-colors"> >
LOGIN {contentType === 'image' && contentPosition !== 'background'
</Link> ? imageBlock(illustrationImage)
<BaseButton : null}
href="/register" {contentType === 'video' && contentPosition !== 'background'
label="REGISTAR" ? videoBlock(illustrationVideo)
color="danger" : null}
className="px-6 font-bold" <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'>
</div> <CardBoxComponentTitle title="Welcome to your BCasino app!"/>
</nav>
<SectionFullScreen bg="slate"> <div className="space-y-3">
<div className="container mx-auto px-6 py-20 flex flex-col lg:flex-row items-center justify-between"> <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>
<div className="lg:w-1/2 space-y-8 text-center lg:text-left"> <p className='text-center '>For guides and documentation please check
<h1 className="text-6xl lg:text-8xl font-black italic uppercase leading-tight"> your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
A Tua Casa de <span className="text-red-600">Apostas</span> Online </div>
</h1>
<p className="text-xl text-slate-400 max-w-lg mx-auto lg:mx-0 font-medium"> <BaseButtons>
Experimenta a emoção do BCasino. Centenas de jogos, bónus exclusivos e depósitos instantâneos. Começa hoje mesmo! <BaseButton
</p> href='/login'
<div className="flex flex-col sm:flex-row gap-4 pt-4 justify-center lg:justify-start"> label='Login'
<BaseButton color='info'
href="/register" className='w-full'
label="CRIAR CONTA GRÁTIS" />
color="danger"
className="text-lg py-4 px-10 font-black italic" </BaseButtons>
/> </CardBox>
<BaseButton </div>
href="/games/games-list" </div>
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> </SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<footer className="bg-slate-900 border-t border-white/5 p-12 mt-20"> <p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<div className="container mx-auto grid grid-cols-1 md:grid-cols-3 gap-12 text-center md:text-left"> <Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
<div className="space-y-4"> Privacy Policy
<div className="text-2xl font-black italic text-red-600">{title}</div> </Link>
<p className="text-slate-500 text-sm">A melhor experiência de jogo online em Portugal. Licenciada e segura.</p> </div>
</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> </div>
); );
@ -121,3 +163,4 @@ export default function Starter() {
Starter.getLayout = function getLayout(page: ReactElement) { Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>; return <LayoutGuest>{page}</LayoutGuest>;
}; };

View File

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