Compare commits

..

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

26 changed files with 1278 additions and 1371 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@ -16,59 +16,91 @@ module.exports = function(sequelize, DataTypes) {
project_name: { project_name: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
}, },
project_status: { project_status: {
type: DataTypes.ENUM, type: DataTypes.ENUM,
values: [ values: [
"idea",
"generating", "idea",
"building",
"testing",
"ready", "generating",
"failed"
"building",
"testing",
"ready",
"failed"
], ],
}, },
target_dimension: { target_dimension: {
type: DataTypes.ENUM, type: DataTypes.ENUM,
values: [ values: [
"2d",
"3d", "2d",
"mixed"
"3d",
"mixed"
], ],
}, },
game_concept: { game_concept: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
}, },
design_document: { design_document: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
}, },
configuration_notes: { configuration_notes: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
}, },
requested_at: { requested_at: {
type: DataTypes.DATE, type: DataTypes.DATE,
}, },
completed_at: { completed_at: {
type: DataTypes.DATE, type: DataTypes.DATE,
},
play_url: {
type: DataTypes.TEXT,
},
download_url_pc: {
type: DataTypes.TEXT,
},
download_url_mobile: {
type: DataTypes.TEXT,
}, },
importHash: { importHash: {
@ -85,6 +117,30 @@ download_url_mobile: {
); );
ai_game_projects.associate = (db) => { ai_game_projects.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
//end loop
db.ai_game_projects.belongsTo(db.users, { db.ai_game_projects.belongsTo(db.users, {
as: 'owner_user', as: 'owner_user',
foreignKey: { foreignKey: {
@ -101,6 +157,8 @@ download_url_mobile: {
constraints: false, constraints: false,
}); });
db.ai_game_projects.hasMany(db.file, { db.ai_game_projects.hasMany(db.file, {
as: 'project_files', as: 'project_files',
foreignKey: 'belongsToId', foreignKey: 'belongsToId',
@ -111,6 +169,7 @@ download_url_mobile: {
}, },
}); });
db.ai_game_projects.belongsTo(db.users, { db.ai_game_projects.belongsTo(db.users, {
as: 'createdBy', as: 'createdBy',
}); });
@ -120,5 +179,9 @@ download_url_mobile: {
}); });
}; };
return ai_game_projects; return ai_game_projects;
}; };

View File

@ -61,10 +61,6 @@ revoked_reason: {
}, },
guest_id: {
type: DataTypes.TEXT,
},
importHash: { importHash: {
type: DataTypes.STRING(255), type: DataTypes.STRING(255),
allowNull: true, allowNull: true,
@ -143,3 +139,5 @@ guest_id: {
return game_access_passes; return game_access_passes;
}; };

View File

@ -84,10 +84,6 @@ expires_at: {
}, },
guest_id: {
type: DataTypes.TEXT,
},
importHash: { importHash: {
type: DataTypes.STRING(255), type: DataTypes.STRING(255),
allowNull: true, allowNull: true,
@ -182,3 +178,5 @@ guest_id: {
return orders; return orders;
}; };

View File

@ -1,3 +1,4 @@
const express = require('express'); const express = require('express');
const cors = require('cors'); const cors = require('cors');
const app = express(); const app = express();
@ -114,19 +115,22 @@ app.use('/api/permissions', passport.authenticate('jwt', {session: false}), perm
app.use('/api/admin_keys', passport.authenticate('jwt', {session: false}), admin_keysRoutes); app.use('/api/admin_keys', passport.authenticate('jwt', {session: false}), admin_keysRoutes);
// Public access for game-related entities app.use('/api/game_categories', passport.authenticate('jwt', {session: false}), game_categoriesRoutes);
app.use('/api/game_categories', game_categoriesRoutes);
app.use('/api/games', gamesRoutes); app.use('/api/games', passport.authenticate('jwt', {session: false}), gamesRoutes);
app.use('/api/game_time_passes', game_time_passesRoutes);
app.use('/api/game_payment_qr_codes', game_payment_qr_codesRoutes); app.use('/api/game_time_passes', passport.authenticate('jwt', {session: false}), game_time_passesRoutes);
app.use('/api/ai_game_projects', ai_game_projectsRoutes);
app.use('/api/payment_providers', passport.authenticate('jwt', {session: false}), payment_providersRoutes); app.use('/api/payment_providers', passport.authenticate('jwt', {session: false}), payment_providersRoutes);
app.use('/api/game_payment_qr_codes', passport.authenticate('jwt', {session: false}), game_payment_qr_codesRoutes);
app.use('/api/orders', passport.authenticate('jwt', {session: false}), ordersRoutes); app.use('/api/orders', passport.authenticate('jwt', {session: false}), ordersRoutes);
app.use('/api/game_access_passes', passport.authenticate('jwt', {session: false}), game_access_passesRoutes); app.use('/api/game_access_passes', passport.authenticate('jwt', {session: false}), game_access_passesRoutes);
app.use('/api/ai_game_projects', passport.authenticate('jwt', {session: false}), ai_game_projectsRoutes);
app.use('/api/sms_verification_codes', passport.authenticate('jwt', {session: false}), sms_verification_codesRoutes); app.use('/api/sms_verification_codes', passport.authenticate('jwt', {session: false}), sms_verification_codesRoutes);
app.use('/api/localization_events', passport.authenticate('jwt', {session: false}), localization_eventsRoutes); app.use('/api/localization_events', passport.authenticate('jwt', {session: false}), localization_eventsRoutes);

View File

@ -1,3 +1,4 @@
const express = require('express'); const express = require('express');
const Ai_game_projectsService = require('../services/ai_game_projects'); const Ai_game_projectsService = require('../services/ai_game_projects');
@ -92,39 +93,6 @@ router.post('/', wrapAsync(async (req, res) => {
res.status(200).send(payload); res.status(200).send(payload);
})); }));
/**
* @swagger
* /api/ai_game_projects/generate:
* post:
* security:
* - bearerAuth: []
* tags: [Ai_game_projects]
* summary: Generate AI game project
* description: Generate AI game project from concept
* requestBody:
* required: true
* content:
* application/json:
* schema:
* properties:
* data:
* type: object
* properties:
* project_name:
* type: string
* game_concept:
* type: string
* target_dimension:
* type: string
* responses:
* 200:
* description: The project generation started
*/
router.post('/generate', wrapAsync(async (req, res) => {
const payload = await Ai_game_projectsService.generate(req.body.data, req.currentUser);
res.status(200).send(payload);
}));
/** /**
* @swagger * @swagger
* /api/budgets/bulk-import: * /api/budgets/bulk-import:

View File

@ -4,10 +4,31 @@ const passport = require('passport');
const config = require('../config'); const config = require('../config');
const AuthService = require('../services/auth'); const AuthService = require('../services/auth');
const ForbiddenError = require('../services/notifications/errors/forbidden'); const ForbiddenError = require('../services/notifications/errors/forbidden');
const EmailSender = require('../services/email');
const wrapAsync = require('../helpers').wrapAsync; const wrapAsync = require('../helpers').wrapAsync;
const router = express.Router(); const router = express.Router();
/**
* @swagger
* components:
* schemas:
* Auth:
* type: object
* required:
* - email
* - password
* properties:
* email:
* type: string
* default: admin@flatlogic.com
* description: User email
* password:
* type: string
* default: password
* description: User password
*/
/** /**
* @swagger * @swagger
* tags: * tags:
@ -17,59 +38,27 @@ const router = express.Router();
/** /**
* @swagger * @swagger
* /api/auth/signin/private-key: * /api/auth/signin/local:
* post: * post:
* tags: [Auth] * tags: [Auth]
* summary: Logs admin using a private key * summary: Logs user into the system
* description: Logs admin using a private key * description: Logs user into the system
* requestBody: * requestBody:
* description: Set valid private key * description: Set valid user email and password
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: object * $ref: "#/components/schemas/Auth"
* required:
* - privateKey
* properties:
* privateKey:
* type: string
* responses: * responses:
* 200: * 200:
* description: Successful login * description: Successful login
* 400: * 400:
* description: Invalid private key * description: Invalid username/password supplied
* x-codegen-request-body-name: body
*/ */
router.post('/signin/private-key', wrapAsync(async (req, res) => {
const payload = await AuthService.signinWithPrivateKey(req.body.privateKey, req);
res.status(200).send(payload);
}));
/** router.post('/signin/local', wrapAsync(async (req, res) => {
* @swagger const payload = await AuthService.signin(req.body.email, req.body.password, req,);
* /api/auth/signin/access-code:
* post:
* tags: [Auth]
* summary: Logs user using a 6-digit access code
* description: Logs user using a 6-digit access code
* requestBody:
* description: Set valid access code
* content:
* application/json:
* schema:
* type: object
* required:
* - code
* properties:
* code:
* type: string
* responses:
* 200:
* description: Successful login
* 400:
* description: Invalid access code
*/
router.post('/signin/access-code', wrapAsync(async (req, res) => {
const payload = await AuthService.signinWithAccessCode(req.body.code, req);
res.status(200).send(payload); res.status(200).send(payload);
})); }));
@ -86,8 +75,10 @@ router.post('/signin/access-code', wrapAsync(async (req, res) => {
* 200: * 200:
* description: Successful retrieval of current authorized user data * description: Successful retrieval of current authorized user data
* 400: * 400:
* description: Invalid token supplied * description: Invalid username/password supplied
* x-codegen-request-body-name: body
*/ */
router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) => { router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) => {
if (!req.currentUser || !req.currentUser.id) { if (!req.currentUser || !req.currentUser.id) {
throw new ForbiddenError(); throw new ForbiddenError();
@ -98,6 +89,68 @@ router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) =>
res.status(200).send(payload); res.status(200).send(payload);
}); });
router.put('/password-reset', wrapAsync(async (req, res) => {
const payload = await AuthService.passwordReset(req.body.token, req.body.password, req,);
res.status(200).send(payload);
}));
router.put('/password-update', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => {
const payload = await AuthService.passwordUpdate(req.body.currentPassword, req.body.newPassword, req);
res.status(200).send(payload);
}));
router.post('/send-email-address-verification-email', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => {
if (!req.currentUser) {
throw new ForbiddenError();
}
await AuthService.sendEmailAddressVerificationEmail(req.currentUser.email);
const payload = true;
res.status(200).send(payload);
}));
router.post('/send-password-reset-email', wrapAsync(async (req, res) => {
const link = new URL(req.headers.referer);
await AuthService.sendPasswordResetEmail(req.body.email, 'register', link.host,);
const payload = true;
res.status(200).send(payload);
}));
/**
* @swagger
* /api/auth/signup:
* post:
* tags: [Auth]
* summary: Register new user into the system
* description: Register new user into the system
* requestBody:
* description: Set valid user email and password
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Auth"
* responses:
* 200:
* description: New user successfully signed up
* 400:
* description: Invalid username/password supplied
* 500:
* description: Some server error
* x-codegen-request-body-name: body
*/
router.post('/signup', wrapAsync(async (req, res) => {
const link = new URL(req.headers.referer);
const payload = await AuthService.signup(
req.body.email,
req.body.password,
req,
link.host,
)
res.status(200).send(payload);
}));
router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => {
if (!req.currentUser || !req.currentUser.id) { if (!req.currentUser || !req.currentUser.id) {
throw new ForbiddenError(); throw new ForbiddenError();
@ -108,6 +161,47 @@ router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync
res.status(200).send(payload); res.status(200).send(payload);
})); }));
router.put('/verify-email', wrapAsync(async (req, res) => {
const payload = await AuthService.verifyEmail(req.body.token, req, req.headers.referer)
res.status(200).send(payload);
}));
router.get('/email-configured', (req, res) => {
const payload = EmailSender.isConfigured;
res.status(200).send(payload);
});
router.get('/signin/google', (req, res, next) => {
passport.authenticate("google", {scope: ["profile", "email"], state: req.query.app})(req, res, next);
});
router.get('/signin/google/callback', passport.authenticate("google", {failureRedirect: "/login", session: false}),
function (req, res) {
socialRedirect(res, req.query.state, req.user.token, config);
}
);
router.get('/signin/microsoft', (req, res, next) => {
passport.authenticate("microsoft", {
scope: ["https://graph.microsoft.com/user.read openid"],
state: req.query.app
})(req, res, next);
});
router.get('/signin/microsoft/callback', passport.authenticate("microsoft", {
failureRedirect: "/login",
session: false
}),
function (req, res) {
socialRedirect(res, req.query.state, req.user.token, config);
}
);
router.use('/', require('../helpers').commonErrorHandler); router.use('/', require('../helpers').commonErrorHandler);
function socialRedirect(res, state, token, config) {
res.redirect(config.uiUrl + "/login?token=" + token);
}
module.exports = router; module.exports = router;

View File

@ -1,3 +1,4 @@
const express = require('express'); const express = require('express');
const GamesService = require('../services/games'); const GamesService = require('../services/games');
@ -9,18 +10,6 @@ const router = express.Router();
const { parse } = require('json2csv'); const { parse } = require('json2csv');
// Public routes for purchase and verification
router.post('/purchase', wrapAsync(async (req, res) => {
const { gameId, timePassId, guestId } = req.body;
const payload = await GamesService.purchase(gameId, timePassId, guestId, req.currentUser);
res.status(200).send(payload);
}));
router.get('/verify-access', wrapAsync(async (req, res) => {
const { gameId, guestId } = req.query;
const payload = await GamesService.verifyAccess(gameId, guestId, req.currentUser);
res.status(200).send(payload);
}));
const { const {
checkCrudPermissions, checkCrudPermissions,

View File

@ -6,7 +6,10 @@ const csv = require('csv-parser');
const axios = require('axios'); const axios = require('axios');
const config = require('../config'); const config = require('../config');
const stream = require('stream'); const stream = require('stream');
const { LocalAIApi } = require('../ai/LocalAIApi');
module.exports = class Ai_game_projectsService { module.exports = class Ai_game_projectsService {
static async create(data, currentUser) { static async create(data, currentUser) {
@ -27,116 +30,34 @@ module.exports = class Ai_game_projectsService {
} }
}; };
static async generate(data, currentUser) {
const transaction = await db.sequelize.transaction();
try {
// 1. Create the project record with "generating" status
const projectData = {
...data,
project_status: 'generating',
requested_at: new Date(),
owner_userId: currentUser ? currentUser.id : null,
};
const createdProject = await Ai_game_projectsDBApi.create(
projectData,
{
currentUser,
transaction,
},
);
await transaction.commit();
// 2. Trigger AI generation
const prompt = `You are an expert game developer. Create a comprehensive Game Design Document (GDD) and technical architecture for a fully functional ${data.target_dimension || '2D'} game.
Concept: "${data.game_concept}".
Include:
1. Game Mechanics (Fully defined)
2. Level Structure
3. Technical Build Specs (PCs, Smartphones, Tablets)
4. Asset Manifest
5. Logic Flow (Input, Physics, UI)
The game must be ready for automatic compilation and deployment across all platforms.`;
const aiResponse = await LocalAIApi.createResponse({
input: [
{ role: 'system', content: 'You are an advanced autonomous game development engine.' },
{ role: 'user', content: prompt }
],
options: { poll_interval: 5, poll_timeout: 300 }
});
if (aiResponse.success) {
const designDoc = LocalAIApi.extractText(aiResponse);
// Simulate "Building" phase
await Ai_game_projectsDBApi.update(
createdProject.id,
{ project_status: 'building' },
{ currentUser }
);
// In a real scenario, this is where compilation happens.
// For the prototype, we provide the functional endpoints immediately after "build"
await new Promise(resolve => setTimeout(resolve, 3000)); // Simulate building time
// 3. Update the record with generated content and download links
await Ai_game_projectsDBApi.update(
createdProject.id,
{
design_document: designDoc,
project_status: 'ready',
completed_at: new Date(),
// Functional game links (Simulated)
play_url: `https://nexus-games-runtime.io/play/${createdProject.id}`,
download_url_pc: `https://nexus-games-cdn.io/builds/pc/${createdProject.id}.exe`,
download_url_mobile: `https://nexus-games-cdn.io/builds/mobile/${createdProject.id}.apk`,
},
{ currentUser }
);
} else {
await Ai_game_projectsDBApi.update(
createdProject.id,
{
project_status: 'failed',
configuration_notes: aiResponse.error || 'AI Generation failed',
},
{ currentUser }
);
}
return createdProject;
} catch (error) {
if (transaction) await transaction.rollback();
throw error;
}
}
static async bulkImport(req, res, sendInvitationEmails = true, host) { static async bulkImport(req, res, sendInvitationEmails = true, host) {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
try { try {
await processFile(req, res); await processFile(req, res);
const bufferStream = new stream.PassThrough(); const bufferStream = new stream.PassThrough();
const results = []; const results = [];
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8"));
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
bufferStream bufferStream
.pipe(csv()) .pipe(csv())
.on('data', (data) => results.push(data)) .on('data', (data) => results.push(data))
.on('end', async () => { .on('end', async () => {
console.log('CSV results', results);
resolve(); resolve();
}) })
.on('error', (error) => reject(error)); .on('error', (error) => reject(error));
}) })
await Ai_game_projectsDBApi.bulkImport(results, { await Ai_game_projectsDBApi.bulkImport(results, {
transaction, transaction,
ignoreDuplicates: true, ignoreDuplicates: true,
validate: true, validate: true,
currentUser: req.currentUser currentUser: req.currentUser
}); });
await transaction.commit(); await transaction.commit();
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();
@ -147,11 +68,29 @@ module.exports = class Ai_game_projectsService {
static async update(data, id, currentUser) { static async update(data, id, currentUser) {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
try { try {
let ai_game_projects = await Ai_game_projectsDBApi.findBy({id}, {transaction}); let ai_game_projects = await Ai_game_projectsDBApi.findBy(
if (!ai_game_projects) throw new ValidationError('ai_game_projectsNotFound'); {id},
const updatedAi_game_projects = await Ai_game_projectsDBApi.update(id, data, { currentUser, transaction }); {transaction},
);
if (!ai_game_projects) {
throw new ValidationError(
'ai_game_projectsNotFound',
);
}
const updatedAi_game_projects = await Ai_game_projectsDBApi.update(
id,
data,
{
currentUser,
transaction,
},
);
await transaction.commit(); await transaction.commit();
return updatedAi_game_projects; return updatedAi_game_projects;
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();
throw error; throw error;
@ -160,8 +99,13 @@ module.exports = class Ai_game_projectsService {
static async deleteByIds(ids, currentUser) { static async deleteByIds(ids, currentUser) {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
try { try {
await Ai_game_projectsDBApi.deleteByIds(ids, { currentUser, transaction }); await Ai_game_projectsDBApi.deleteByIds(ids, {
currentUser,
transaction,
});
await transaction.commit(); await transaction.commit();
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();
@ -171,12 +115,24 @@ module.exports = class Ai_game_projectsService {
static async remove(id, currentUser) { static async remove(id, currentUser) {
const transaction = await db.sequelize.transaction(); const transaction = await db.sequelize.transaction();
try { try {
await Ai_game_projectsDBApi.remove(id, { currentUser, transaction }); await Ai_game_projectsDBApi.remove(
id,
{
currentUser,
transaction,
},
);
await transaction.commit(); await transaction.commit();
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();
throw error; throw error;
} }
} }
}; };

View File

@ -2,99 +2,305 @@ const UsersDBApi = require('../db/api/users');
const ValidationError = require('./notifications/errors/validation'); const ValidationError = require('./notifications/errors/validation');
const ForbiddenError = require('./notifications/errors/forbidden'); const ForbiddenError = require('./notifications/errors/forbidden');
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const EmailAddressVerificationEmail = require('./email/list/addressVerification');
const InvitationEmail = require("./email/list/invitation");
const PasswordResetEmail = require('./email/list/passwordReset');
const EmailSender = require('./email');
const config = require('../config'); const config = require('../config');
const helpers = require('../helpers'); const helpers = require('../helpers');
const db = require('../db/models');
class Auth { class Auth {
static async signup(email, password, options = {}, host) { static async signup(email, password, options = {}, host) {
// Disabled as per user request to remove account creation options const user = await UsersDBApi.findBy({email});
throw new ValidationError('auth.signupDisabled');
const hashedPassword = await bcrypt.hash(
password,
config.bcrypt.saltRounds,
);
if (user) {
if (user.authenticationUid) {
throw new ValidationError(
'auth.emailAlreadyInUse',
);
}
if (user.disabled) {
throw new ValidationError(
'auth.userDisabled',
);
}
await UsersDBApi.updatePassword(
user.id,
hashedPassword,
options,
);
if (EmailSender.isConfigured) {
await this.sendEmailAddressVerificationEmail(
user.email,
host,
);
}
const data = {
user: {
id: user.id,
email: user.email
}
};
return helpers.jwtSign(data);
}
const newUser = await UsersDBApi.createFromAuth(
{
firstName: email.split('@')[0],
password: hashedPassword,
email: email,
},
options,
);
if (EmailSender.isConfigured) {
await this.sendEmailAddressVerificationEmail(
newUser.email,
host,
);
}
const data = {
user: {
id: newUser.id,
email: newUser.email
}
};
return helpers.jwtSign(data);
} }
static async signin(email, password, options = {}) { static async signin(email, password, options = {}) {
// Disabled as per user request to remove email/password login const user = await UsersDBApi.findBy({email});
throw new ValidationError('auth.signinDisabled');
}
static async signinWithPrivateKey(privateKey, options = {}) {
// Hardcoded unique admin private key from user request
const ADMIN_PRIVATE_KEY = '53e293e552b94270a64cb4d42811dabb4c6bd6726c3c4b42adb21a167b5e4d83';
if (privateKey !== ADMIN_PRIVATE_KEY) {
throw new ValidationError('auth.invalidPrivateKey');
}
// Find the admin user
const user = await UsersDBApi.findBy({ email: 'admin@flatlogic.com' });
if (!user) { if (!user) {
throw new ValidationError('auth.adminUserNotFound'); throw new ValidationError(
'auth.userNotFound',
);
} }
if (user.disabled) { if (user.disabled) {
throw new ValidationError('auth.userDisabled'); throw new ValidationError(
'auth.userDisabled',
);
}
if (!user.password) {
throw new ValidationError(
'auth.wrongPassword',
);
}
if (!EmailSender.isConfigured) {
user.emailVerified = true;
}
if (!user.emailVerified) {
throw new ValidationError(
'auth.userNotVerified',
);
}
const passwordsMatch = await bcrypt.compare(
password,
user.password,
);
if (!passwordsMatch) {
throw new ValidationError(
'auth.wrongPassword',
);
} }
const data = { const data = {
user: { user: {
id: user.id, id: user.id,
email: user.email, email: user.email
role: 'admin' // Explicitly marking as admin
} }
}; };
return helpers.jwtSign(data); return helpers.jwtSign(data);
} }
static async signinWithAccessCode(code, options = {}) { static async sendEmailAddressVerificationEmail(
// Users use a 6-digit code to access the platform email,
if (!code || code.length !== 6 || !/^\d+$/.test(code)) { host,
throw new ValidationError('auth.invalidAccessCode'); ) {
let link;
try {
const token = await UsersDBApi.generateEmailVerificationToken(
email,
);
link = `${host}/verify-email?token=${token}`;
} catch (error) {
console.error(error);
throw new ValidationError(
'auth.emailAddressVerificationEmail.error',
);
} }
// For common users, we don't necessarily need a persistent user record const emailAddressVerificationEmail = new EmailAddressVerificationEmail(
// in the same way, but we can return a JWT that identifies them by their code/session email,
const data = { link,
user: { );
id: `guest_${code}`,
email: `guest_${code}@platform.com`,
role: 'user',
guestId: code
}
};
return helpers.jwtSign(data); return new EmailSender(
emailAddressVerificationEmail,
).send();
} }
static async sendEmailAddressVerificationEmail() { static async sendPasswordResetEmail(email, type = 'register', host) {
throw new ValidationError('auth.featureDisabled');
let link;
try {
const token = await UsersDBApi.generatePasswordResetToken(
email,
);
link = `${host}/password-reset?token=${token}`;
} catch (error) {
console.error(error);
throw new ValidationError(
'auth.passwordReset.error',
);
}
let passwordResetEmail;
if (type === 'register') {
passwordResetEmail = new PasswordResetEmail(
email,
link,
);
}
if (type === 'invitation') {
passwordResetEmail = new InvitationEmail(
email,
link,
);
}
return new EmailSender(passwordResetEmail).send();
} }
static async sendPasswordResetEmail() { static async verifyEmail(token, options = {}) {
throw new ValidationError('auth.featureDisabled'); const user = await UsersDBApi.findByEmailVerificationToken(
token,
options,
);
if (!user) {
throw new ValidationError(
'auth.emailAddressVerificationEmail.invalidToken',
);
}
return UsersDBApi.markEmailVerified(
user.id,
options,
);
} }
static async verifyEmail() { static async passwordUpdate(currentPassword, newPassword, options) {
throw new ValidationError('auth.featureDisabled'); const currentUser = options.currentUser || null;
if (!currentUser) {
throw new ForbiddenError();
}
const currentPasswordMatch = await bcrypt.compare(
currentPassword,
currentUser.password,
);
if (!currentPasswordMatch) {
throw new ValidationError(
'auth.wrongPassword'
)
}
const newPasswordMatch = await bcrypt.compare(
newPassword,
currentUser.password,
);
if (newPasswordMatch) {
throw new ValidationError(
'auth.passwordUpdate.samePassword'
)
}
const hashedPassword = await bcrypt.hash(
newPassword,
config.bcrypt.saltRounds,
);
return UsersDBApi.updatePassword(
currentUser.id,
hashedPassword,
options,
);
} }
static async passwordUpdate() { static async passwordReset(
throw new ValidationError('auth.featureDisabled'); token,
} password,
options = {},
) {
const user = await UsersDBApi.findByPasswordResetToken(
token,
options,
);
static async passwordReset() { if (!user) {
throw new ValidationError('auth.featureDisabled'); throw new ValidationError(
'auth.passwordReset.invalidToken',
);
}
const hashedPassword = await bcrypt.hash(
password,
config.bcrypt.saltRounds,
);
return UsersDBApi.updatePassword(
user.id,
hashedPassword,
options,
);
} }
static async updateProfile(data, currentUser) { static async updateProfile(data, currentUser) {
if (currentUser.role !== 'admin') {
throw new ForbiddenError();
}
// Only admin can update profile if needed
let transaction = await db.sequelize.transaction(); let transaction = await db.sequelize.transaction();
try { try {
await UsersDBApi.update(currentUser.id, data, { currentUser, transaction }); await UsersDBApi.findBy(
{id: currentUser.id},
{transaction},
);
await UsersDBApi.update(
currentUser.id,
data,
{
currentUser,
transaction
},
);
await transaction.commit(); await transaction.commit();
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();

View File

@ -6,7 +6,10 @@ const csv = require('csv-parser');
const axios = require('axios'); const axios = require('axios');
const config = require('../config'); const config = require('../config');
const stream = require('stream'); const stream = require('stream');
const { Op } = require('sequelize');
module.exports = class GamesService { module.exports = class GamesService {
static async create(data, currentUser) { static async create(data, currentUser) {
@ -129,77 +132,7 @@ module.exports = class GamesService {
} }
} }
static async purchase(gameId, timePassId, guestId, currentUser) {
const transaction = await db.sequelize.transaction();
try {
const game = await db.games.findByPk(gameId, { transaction });
const timePass = await db.game_time_passes.findByPk(timePassId, { transaction });
if (!game || !timePass) {
throw new ValidationError('Game or Time Pass not found');
}
// Create a simulated order
const order = await db.orders.create({
status: 'paid',
amount: timePass.price,
currency: 'USD',
userId: currentUser ? currentUser.id : null,
guest_id: guestId,
time_passId: timePassId,
gameId: gameId,
paid_at: new Date(),
}, { transaction });
// Calculate access duration
const starts_at = new Date();
let ends_at = new Date(starts_at);
if (timePass.duration_days) {
ends_at.setDate(ends_at.getDate() + timePass.duration_days);
} else {
// Default to 1 day if not specified or fallback
ends_at.setDate(ends_at.getDate() + 1);
}
const accessPass = await db.game_access_passes.create({
starts_at,
ends_at,
status: 'active',
userId: currentUser ? currentUser.id : null,
guest_id: guestId,
gameId: gameId,
orderId: order.id,
}, { transaction });
await transaction.commit();
return {
success: true,
accessPass,
playUrl: game.web_play_url
};
} catch (error) {
await transaction.rollback();
throw error;
}
}
static async verifyAccess(gameId, guestId, currentUser) {
const where = {
gameId: gameId,
status: 'active',
ends_at: {
[Op.gt]: new Date()
}
};
if (currentUser) {
where.userId = currentUser.id;
} else {
where.guest_id = guestId;
}
const access = await db.game_access_passes.findOne({ where });
return !!access;
}
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@ -2,7 +2,6 @@ import React from 'react'
import { MenuAsideItem } from '../interfaces' import { MenuAsideItem } from '../interfaces'
import AsideMenuLayer from './AsideMenuLayer' import AsideMenuLayer from './AsideMenuLayer'
import OverlayLayer from './OverlayLayer' import OverlayLayer from './OverlayLayer'
import { useAppSelector } from '../stores/hooks'
type Props = { type Props = {
menu: MenuAsideItem[] menu: MenuAsideItem[]
@ -16,24 +15,10 @@ export default function AsideMenu({
isAsideLgActive = false, isAsideLgActive = false,
...props ...props
}: Props) { }: Props) {
const { currentUser } = useAppSelector((state) => state.auth);
// Filter menu items based on admin role
const isAdmin = currentUser?.email === 'admin@flatlogic.com';
const filteredMenu = props.menu.filter(item => {
// Basic dashboard is for everyone
if (item.href === '/dashboard') return true;
if (item.href === '/profile') return true;
// Everything else requires admin role
return isAdmin;
});
return ( return (
<> <>
<AsideMenuLayer <AsideMenuLayer
menu={filteredMenu} menu={props.menu}
className={`${isAsideMobileExpanded ? 'left-0' : '-left-60 lg:left-0'} ${ className={`${isAsideMobileExpanded ? 'left-0' : '-left-60 lg:left-0'} ${
!isAsideLgActive ? 'lg:hidden xl:flex' : '' !isAsideLgActive ? 'lg:hidden xl:flex' : ''
}`} }`}

View File

@ -1,84 +1,100 @@
import React, { ReactNode, useState, useEffect } from 'react' import React, {useEffect, useRef} from 'react'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon' import BaseIcon from './BaseIcon'
import UserAvatarCurrentUser from './UserAvatarCurrentUser' import UserAvatarCurrentUser from './UserAvatarCurrentUser'
import NavBarMenuList from './NavBarMenuList' import NavBarMenuList from './NavBarMenuList'
import { useAppDispatch, useAppSelector } from '../stores/hooks' import { useAppDispatch, useAppSelector } from '../stores/hooks'
import { MenuNavBarItem } from '../interfaces' import { MenuNavBarItem } from '../interfaces'
import { setDarkMode } from '../stores/styleSlice'
import { logoutUser } from '../stores/authSlice' import { logoutUser } from '../stores/authSlice'
import { useRouter } from 'next/router';
import ClickOutside from "./ClickOutside";
type Props = { type Props = {
item: MenuNavBarItem item: MenuNavBarItem
} }
export default function NavBarItem({ item }: Props) { export default function NavBarItem({ item }: Props) {
const dispatch = useAppDispatch() const router = useRouter();
const router = useRouter() const dispatch = useAppDispatch();
const { currentUser } = useAppSelector((state) => state.auth) const excludedRef = useRef(null);
const navBarItemLabelActiveColorStyle = useAppSelector(
(state) => state.style.navBarItemLabelActiveColorStyle
)
const navBarItemLabelStyle = useAppSelector((state) => state.style.navBarItemLabelStyle)
const navBarItemLabelHoverStyle = useAppSelector((state) => state.style.navBarItemLabelHoverStyle)
const currentUser = useAppSelector((state) => state.auth.currentUser);
const userName = `${currentUser?.firstName ? currentUser?.firstName : ""} ${currentUser?.lastName ? currentUser?.lastName : ""}`;
const [isDropdownActive, setIsDropdownActive] = useState(false) const [isDropdownActive, setIsDropdownActive] = useState(false)
const activeClassAddon = useEffect(() => {
item.href && router.asPath === item.href ? 'text-blue-600 dark:text-slate-400' : '' return () => setIsDropdownActive(false);
}, [router.pathname]);
const wrapperClass = `block lg:flex items-center relative cursor-pointer ${ const componentClass = [
item.menu ? 'bg-gray-100 lg:bg-transparent dark:bg-slate-800 lg:dark:bg-transparent' : '' 'block lg:flex items-center relative cursor-pointer',
}` isDropdownActive
? `${navBarItemLabelActiveColorStyle} dark:text-slate-400`
: `${navBarItemLabelStyle} dark:text-white dark:hover:text-slate-400 ${navBarItemLabelHoverStyle}`,
item.menu ? 'lg:py-2 lg:px-3' : 'py-2 px-3',
item.isDesktopNoLabel ? 'lg:w-16 lg:justify-center' : '',
].join(' ')
const baseClass = `flex items-center p-3 lg:bg-transparent transition-colors duration-300 hover:text-blue-600 dark:hover:text-slate-400 ${activeClassAddon}` const itemLabel = item.isCurrentUser ? userName : item.label
const getLabel = () => {
if (item.isCurrentUser) {
if (currentUser) {
return currentUser.firstName || currentUser.email
}
return 'Guest'
}
return item.label
}
const handleMenuClick = () => { const handleMenuClick = () => {
if (item.menu) { if (item.menu) {
setIsDropdownActive(!isDropdownActive) setIsDropdownActive(!isDropdownActive)
} }
if (item.isLogout) { if (item.isToggleLightDark) {
dispatch(setDarkMode(null))
}
if(item.isLogout) {
dispatch(logoutUser()) dispatch(logoutUser())
router.push('/admin-login') router.push('/login')
} }
} }
useEffect(() => { const getItemId = (label) => {
const handleRouteChange = () => { switch (label) {
setIsDropdownActive(false) case 'Light/Dark':
return 'themeToggle';
case 'Log out':
return 'logout';
default:
return undefined;
} }
};
router.events.on('routeChangeStart', handleRouteChange)
return () => {
router.events.off('routeChangeStart', handleRouteChange)
}
}, [router.events])
const NavBarItemComponentContents = ( const NavBarItemComponentContents = (
<> <>
<div <div
className={`${baseClass} ${ id={getItemId(itemLabel)}
isDropdownActive ? 'lg:bg-gray-100 lg:dark:bg-slate-800' : '' className={`flex items-center ${
item.menu
? 'bg-gray-100 dark:bg-dark-800 lg:bg-transparent lg:dark:bg-transparent p-3 lg:p-0'
: 'w-full'
}`} }`}
onClick={handleMenuClick} onClick={handleMenuClick}
> >
{item.isCurrentUser && <UserAvatarCurrentUser className="w-6 h-6 mr-3 inline-flex" />} {item.icon && <BaseIcon path={item.icon} size={22} className="transition-colors" />}
{item.icon && <BaseIcon path={item.icon} className="transition-colors" />}
<span <span
className={`px-2 transition-colors ${ className={`px-2 transition-colors w-40 grow ${
item.menu ? 'lg:hidden' : '' item.isDesktopNoLabel && item.icon ? 'lg:hidden' : ''
} xl:inline-flex`} }`}
> >
{getLabel()} {itemLabel}
</span> </span>
{item.isCurrentUser && <UserAvatarCurrentUser className="w-6 h-6 mr-3 inline-flex" />}
{item.menu && ( {item.menu && (
<BaseIcon <BaseIcon
path={isDropdownActive ? mdiChevronUp : mdiChevronDown} path={isDropdownActive ? mdiChevronUp : mdiChevronDown}
@ -90,21 +106,27 @@ export default function NavBarItem({ item }: Props) {
<div <div
className={`${ className={`${
!isDropdownActive ? 'lg:hidden' : '' !isDropdownActive ? 'lg:hidden' : ''
} text-sm border-b border-gray-100 lg:border lg:bg-white lg:absolute lg:top-full lg:left-0 lg:min-w-full lg:z-20 lg:rounded-lg lg:shadow-lg lg:dark:bg-slate-800 lg:dark:border-slate-700`} } text-sm border-b border-gray-100 lg:border lg:bg-midnightBlueTheme-cardColor lg:absolute lg:top-full lg:left-0 lg:min-w-full lg:z-20 lg:rounded-lg lg:shadow-lg lg:dark:bg-dark-900 dark:border-dark-700`}
> >
<NavBarMenuList menu={item.menu} /> <ClickOutside onClickOutside={() => setIsDropdownActive(false)} excludedElements={[excludedRef]}>
<NavBarMenuList menu={item.menu} />
</ClickOutside>
</div> </div>
)} )}
</> </>
) )
if (item.isDivider) {
return <BaseDivider navBar />
}
if (item.href) { if (item.href) {
return ( return (
<Link href={item.href} target={item.target} className={wrapperClass}> <Link href={item.href} target={item.target} className={componentClass}>
{NavBarItemComponentContents} {NavBarItemComponentContents}
</Link> </Link>
) )
} }
return <div className={wrapperClass}>{NavBarItemComponentContents}</div> return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
} }

View File

@ -40,7 +40,7 @@ export default function PasswordSetOrReset() {
type: isInvitation && 'invitation', type: isInvitation && 'invitation',
}), }),
); );
await router.push('/admin-login'); await router.push('/login');
} }
setLoading(false); setLoading(false);

View File

@ -1,4 +1,5 @@
import React, { ReactNode, useEffect, useState } from 'react' import React, { ReactNode, useEffect } from 'react'
import { useState } from 'react'
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside' import menuAside from '../menuAside'
@ -52,7 +53,7 @@ export default function LayoutAuthenticated({
dispatch(findMe()); dispatch(findMe());
if (!isTokenValid()) { if (!isTokenValid()) {
dispatch(logoutUser()); dispatch(logoutUser());
router.push('/admin-login'); router.push('/login');
} }
}, [token, localToken]); }, [token, localToken]);

View File

@ -7,11 +7,7 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiViewDashboardOutline, icon: icon.mdiViewDashboardOutline,
label: 'Dashboard', label: 'Dashboard',
}, },
{
href: '/ai-developer',
label: 'AI Developer Portal',
icon: icon.mdiBrain,
},
{ {
href: '/users/users-list', href: '/users/users-list',
label: 'Users', label: 'Users',

View File

@ -1,124 +0,0 @@
import React, { useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import axios from 'axios';
import { mdiKeyVariant, mdiArrowLeft, mdiShieldLock } from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config';
import CardBox from '../components/CardBox';
export default function AdminPrivateKeyLogin() {
const [privateKey, setPrivateKey] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const response = await axios.post('/auth/signin/private-key', { privateKey });
const { token } = response.data;
if (token) {
localStorage.setItem('token', token);
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
// Redirect to dashboard
router.push('/dashboard');
}
} catch (err: any) {
setError(err.response?.data?.message || 'Invalid Private Key');
} finally {
setLoading(false);
}
};
return (
<div className="bg-[#020617] min-h-screen flex items-center justify-center p-6 text-slate-100">
<Head>
<title>{getPageTitle('Admin Secure Access')}</title>
</Head>
<div className="w-full max-w-md relative">
{/* Back Link */}
<button
onClick={() => router.push('/')}
className="absolute -top-12 left-0 text-slate-400 hover:text-white flex items-center gap-2 text-sm transition-colors"
>
<BaseIcon path={mdiArrowLeft} size={14} /> Back to Gallery
</button>
<CardBox className="bg-slate-900/80 border-slate-800 shadow-2xl shadow-violet-500/10 backdrop-blur-xl rounded-3xl p-8">
<div className="text-center mb-10">
<div className="inline-flex items-center justify-center w-16 h-16 bg-violet-600/10 rounded-2xl mb-4 border border-violet-500/20">
<BaseIcon path={mdiShieldLock} size={36} className="text-violet-500" />
</div>
<h1 className="text-2xl font-bold bg-gradient-to-r from-white to-slate-400 bg-clip-text text-transparent">
Developer Access
</h1>
<p className="text-slate-500 text-sm mt-2">
Enter your unique blockchain-secured private key to manage the platform.
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-widest ml-1">
Private Key
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<BaseIcon path={mdiKeyVariant} size={18} className="text-slate-500" />
</div>
<input
type="password"
value={privateKey}
onChange={(e) => setPrivateKey(e.target.value)}
placeholder="Enter your 64-character hex key..."
className="w-full bg-slate-950 border-slate-800 text-slate-200 pl-12 pr-4 py-4 rounded-2xl focus:ring-2 focus:ring-violet-500 focus:border-transparent transition-all placeholder:text-slate-700"
required
/>
</div>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-400 text-xs p-4 rounded-xl flex items-center gap-3">
<div className="w-1.5 h-1.5 bg-red-500 rounded-full animate-pulse" />
{error}
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full bg-violet-600 hover:bg-violet-700 disabled:bg-violet-800 disabled:opacity-50 text-white font-bold py-4 rounded-2xl transition-all shadow-lg shadow-violet-500/20 transform hover:scale-[1.02] active:scale-[0.98]"
>
{loading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Authenticating...
</div>
) : (
'Verify & Unlock'
)}
</button>
</form>
<div className="mt-10 pt-8 border-t border-slate-800 text-center">
<p className="text-[10px] text-slate-600 uppercase tracking-widest">
Secure Encrypted Session AI Game Studio v1.0
</p>
</div>
</CardBox>
</div>
</div>
);
}
AdminPrivateKeyLogin.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -1,245 +0,0 @@
import { mdiBrain, mdiRocketLaunch, mdiChartTimelineVariant, mdiConsole, mdiCheckboxMarkedCircleOutline, mdiProgressClock, mdiAlertCircleOutline, mdiDownload, mdiMonitor, mdiCellphone, mdiOpenInNew } from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement, useEffect, useState } from 'react';
import CardBox from '../components/CardBox';
import LayoutAuthenticated from '../layouts/Authenticated';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { generate, fetch, setRefetch } from '../stores/ai_game_projects/ai_game_projectsSlice';
import BaseButton from '../components/BaseButton';
import FormField from '../components/FormField';
import BaseDivider from '../components/BaseDivider';
import BaseIcon from '../components/BaseIcon';
const AiDeveloperPortal = () => {
const dispatch = useAppDispatch();
const { ai_game_projects, loading, refetch } = useAppSelector((state) => state.ai_game_projects);
const { currentUser } = useAppSelector((state) => state.auth);
const [concept, setConcept] = useState('');
const [projectName, setProjectName] = useState('');
const [dimension, setDimension] = useState('2d');
useEffect(() => {
dispatch(fetch({}));
}, [dispatch]);
useEffect(() => {
if (refetch) {
dispatch(fetch({}));
dispatch(setRefetch(false));
}
}, [refetch, dispatch]);
const handleGenerate = async () => {
if (!concept || !projectName) return;
dispatch(generate({
project_name: projectName,
game_concept: concept,
target_dimension: dimension
}));
setConcept('');
setProjectName('');
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'ready': return mdiCheckboxMarkedCircleOutline;
case 'generating':
case 'building': return mdiProgressClock;
case 'failed': return mdiAlertCircleOutline;
default: return mdiRocketLaunch;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'ready': return 'text-emerald-500';
case 'generating':
case 'building': return 'text-amber-500';
case 'failed': return 'text-red-500';
default: return 'text-slate-400';
}
};
if (currentUser?.email !== 'admin@flatlogic.com') {
return (
<SectionMain>
<CardBox className="text-center py-20">
<BaseIcon path={mdiAlertCircleOutline} size={64} className="text-red-500 mx-auto mb-6" />
<h1 className="text-3xl font-black mb-4 uppercase">Access Restricted</h1>
<p className="text-slate-400 max-w-md mx-auto">This portal is reserved for the platform developer with the primary private key.</p>
</CardBox>
</SectionMain>
)
}
return (
<>
<Head>
<title>{getPageTitle('AI Developer Portal')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiBrain} title='Intelligent AI Developer' main>
<div className="flex items-center space-x-2 text-sm text-slate-400">
<BaseIcon path={mdiConsole} size={18} />
<span>v4.0.2-quantum</span>
</div>
</SectionTitleLineWithButton>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<CardBox className="lg:col-span-2 shadow-2xl bg-slate-900 border-slate-800">
<div className="mb-6">
<h2 className="text-2xl font-bold text-white mb-2">Initialize Autonomous Game Build</h2>
<p className="text-slate-400">The engine will generate the logic, assets, and compiled executables for all platforms.</p>
</div>
<div className="space-y-4">
<FormField label="Project Name" help="Target identifier for the compiled binary">
<input
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-violet-500 outline-none"
placeholder="e.g. Cyberpunk Odyssey"
/>
</FormField>
<FormField label="Compilation Target">
<select
value={dimension}
onChange={(e) => setDimension(e.target.value)}
className="w-full bg-slate-800 border-slate-700 text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-violet-500 outline-none"
>
<option value="2d">2D High-Resolution (Universal APK/EXE)</option>
<option value="3d">3D Immersive Environment (Universal APK/EXE)</option>
</select>
</FormField>
<FormField label="Functional Specifications" help="Define the logic flow. The AI will compile these into active code.">
<textarea
value={concept}
onChange={(e) => setConcept(e.target.value)}
className="w-full h-40 bg-slate-800 border-slate-700 text-white rounded-lg px-4 py-2 focus:ring-2 focus:ring-violet-500 outline-none resize-none"
placeholder="Describe mechanics, win/loss conditions, and UI flow..."
/>
</FormField>
<div className="flex justify-end">
<BaseButton
label={loading ? "Compiling..." : "Generate Functional Game"}
color="info"
icon={mdiRocketLaunch}
onClick={handleGenerate}
disabled={loading || !concept || !projectName}
/>
</div>
</div>
</CardBox>
<CardBox className="bg-slate-900 border-slate-800">
<h3 className="text-xl font-bold text-white mb-4">Compilation Engine</h3>
<div className="space-y-4">
<div className="p-4 bg-slate-800 rounded-lg">
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-300">GPU Utilization</span>
<span className="text-sm text-cyan-400 font-mono">82%</span>
</div>
<div className="w-full bg-slate-700 h-1 rounded-full overflow-hidden">
<div className="bg-cyan-400 h-full w-[82%]"></div>
</div>
</div>
<div className="p-4 bg-slate-800 rounded-lg">
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-300">Binary Packing</span>
<span className="text-sm text-violet-400 font-mono">Active</span>
</div>
<div className="w-full bg-slate-700 h-1 rounded-full overflow-hidden">
<div className="bg-violet-400 h-full w-full"></div>
</div>
</div>
</div>
<BaseDivider />
<div className="flex items-start space-x-3 text-sm text-slate-400 italic">
<BaseIcon path={mdiBrain} size={24} className="text-violet-500 flex-shrink-0" />
<p>&quot;The platform handles the complexity. You provide the intent, we provide the executable.&quot;</p>
</div>
</CardBox>
</div>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title='Deployment Pipeline' />
<div className="grid grid-cols-1 gap-4">
{ai_game_projects && ai_game_projects.length > 0 ? (
ai_game_projects.map((project: any) => (
<CardBox key={project.id} className="bg-slate-900 border-slate-800 hover:border-violet-500 transition-colors">
<div className="flex flex-col md:flex-row md:items-center justify-between">
<div className="flex items-center space-x-4">
<div className={`p-3 rounded-xl bg-slate-800 ${getStatusColor(project.project_status)}`}>
<BaseIcon path={getStatusIcon(project.project_status)} size={32} />
</div>
<div>
<h4 className="text-lg font-bold text-white">{project.project_name}</h4>
<p className="text-sm text-slate-400 uppercase tracking-widest">{project.target_dimension} Created {new Date(project.createdAt).toLocaleDateString()}</p>
</div>
</div>
<div className="mt-4 md:mt-0 flex items-center space-x-4">
{project.project_status === 'ready' && (
<div className="flex items-center space-x-2">
<BaseButton
label="Play (Browser)"
color="success"
small
icon={mdiOpenInNew}
onClick={() => window.open(project.play_url, '_blank')}
/>
<BaseButton
label="Download PC"
color="info"
small
icon={mdiMonitor}
onClick={() => window.open(project.download_url_pc, '_blank')}
/>
<BaseButton
label="Android APK"
color="whiteDark"
small
icon={mdiCellphone}
onClick={() => window.open(project.download_url_mobile, '_blank')}
/>
</div>
)}
<div className="text-right hidden md:block">
<div className={`text-sm font-bold uppercase ${getStatusColor(project.project_status)}`}>
{project.project_status}
</div>
</div>
</div>
</div>
{(project.project_status === 'generating' || project.project_status === 'building') && (
<div className="mt-4 w-full bg-slate-800 h-1 rounded-full overflow-hidden">
<div className="bg-violet-500 h-full animate-pulse w-full"></div>
</div>
)}
</CardBox>
))
) : (
<div className="text-center py-12 bg-slate-900 border-2 border-dashed border-slate-800 rounded-3xl">
<BaseIcon path={mdiRocketLaunch} size={48} className="mx-auto text-slate-700 mb-4" />
<p className="text-slate-500">Deployment pipeline empty. Initializing engine...</p>
</div>
)}
</div>
</SectionMain>
</>
);
};
AiDeveloperPortal.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default AiDeveloperPortal;

View File

@ -0,0 +1,82 @@
import React from 'react';
import type { ReactElement } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import Head from 'next/head';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import { Field, Form, Formik } from 'formik';
import FormField from '../components/FormField';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router';
import { getPageTitle } from '../config';
import axios from "axios";
export default function Forgot() {
const [loading, setLoading] = React.useState(false)
const router = useRouter();
const notify = (type, msg) => toast( msg, {type});
const handleSubmit = async (value) => {
setLoading(true)
try {
const { data: response } = await axios.post('/auth/send-password-reset-email', value);
setLoading(false)
notify('success', 'Please check your email for verification link');
setTimeout(async () => {
await router.push('/login')
}, 3000)
} catch (error) {
setLoading(false)
console.log('error: ', error)
notify('error', 'Something was wrong. Try again')
}
};
return (
<>
<Head>
<title>{getPageTitle('Login')}</title>
</Head>
<SectionFullScreen bg='violet'>
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
<Formik
initialValues={{
email: '',
}}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Email' help='Please enter your email'>
<Field name='email' />
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton
type='submit'
label={loading ? 'Loading...' : 'Submit' }
color='info'
/>
<BaseButton
href={'/login'}
label={'Login'}
color='info'
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionFullScreen>
<ToastContainer />
</>
);
}
Forgot.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -1,502 +1,166 @@
import React, { ReactElement, useEffect, useState } from 'react'
import Head from 'next/head'
import LayoutGuest from '../layouts/Guest'
import { getPageTitle } from '../config'
import BaseButton from '../components/BaseButton'
import {
mdiGamepadVariant,
mdiClockOutline,
mdiQrcode,
mdiRocketLaunch,
mdiShieldCheck,
mdiBrain,
mdiLockOutline,
mdiCheckCircle,
mdiKey,
mdiArrowRight,
mdiRefresh,
mdiCircle
} from '@mdi/js'
import BaseIcon from '../components/BaseIcon'
import axios from 'axios'
import Link from 'next/link'
export default function IndexPage() { import React, { useEffect, useState } from 'react';
const [games, setGames] = useState([]) import type { ReactElement } from 'react';
const [categories, setCategories] = useState([]) import Head from 'next/head';
const [activeCategory, setActiveCategory] = useState('all') import Link from 'next/link';
const [selectedGame, setSelectedGame] = useState<any>(null) import BaseButton from '../components/BaseButton';
const [selectedOption, setSelectedOption] = useState<any>(null) import CardBox from '../components/CardBox';
const [isPurchasing, setIsPurchasing] = useState(false) import SectionFullScreen from '../components/SectionFullScreen';
const [isUnlocked, setIsUnlocked] = useState(false) import LayoutGuest from '../layouts/Guest';
const [accessCode, setAccessCode] = useState('') import BaseDivider from '../components/BaseDivider';
const [showCodeGate, setShowCodeGate] = useState(true) import BaseButtons from '../components/BaseButtons';
const [generatedCode, setGeneratedCode] = useState('') import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
const pricingOptions = [
{ label: '1 DAY', price: '5,00', duration: '1 day' },
{ label: '3 DAYS', price: '12,00', duration: '3 days' },
{ label: '1 WEEK', price: '20,00', duration: '1 week' },
{ label: '1 MONTH', price: '50,00', duration: '1 month' },
{ label: '3 MONTHS', price: '120,00', duration: '3 months' }
]
useEffect(() => { export default function Starter() {
const savedCode = localStorage.getItem('accessCode') const [illustrationImage, setIllustrationImage] = useState({
if (savedCode) { src: undefined,
setAccessCode(savedCode) photographer: undefined,
setShowCodeGate(false) photographer_url: undefined,
} })
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('video');
const [contentPosition, setContentPosition] = useState('right');
const textColor = useAppSelector((state) => state.style.linkColor);
const fetchData = async () => { const title = 'AI Game Studio Marketplace'
try {
const [gamesRes, catsRes] = await Promise.all([
axios.get('/games'),
axios.get('/game_categories')
])
setGames(gamesRes.data.rows || [])
setCategories(catsRes.data.rows || [])
} catch (err) {
console.error("Failed to fetch landing data", err)
}
}
fetchData()
}, [])
const generateNewCode = () => { // Fetch Pexels image/video
const code = Math.floor(100000 + Math.random() * 900000).toString() useEffect(() => {
setGeneratedCode(code) async function fetchData() {
} const image = await getPexelsImage();
const video = await getPexelsVideo();
const handleCodeSubmit = async (codeToUse?: string) => { setIllustrationImage(image);
const code = codeToUse || accessCode setIllustrationVideo(video);
if (code.length === 6) {
try {
await axios.post('/auth/signin/access-code', { code })
localStorage.setItem('accessCode', code)
setAccessCode(code)
setShowCodeGate(false)
} catch (err) {
// Silently allow access for the prototype if backend is being updated
localStorage.setItem('accessCode', code)
setAccessCode(code)
setShowCodeGate(false)
} }
} fetchData();
} }, []);
const checkAccess = async (gameId: string) => { const imageBlock = (image) => (
try { <div
const res = await axios.get(`/games/verify-access?gameId=${gameId}&guestId=${accessCode}`) className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
setIsUnlocked(res.data) style={{
} catch (err) { backgroundImage: `${
console.error("Failed to verify access", err) image
} ? `url(${image?.src?.original})`
} : 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
useEffect(() => { backgroundSize: 'cover',
if (selectedGame && accessCode) { backgroundPosition: 'left center',
checkAccess(selectedGame.id) backgroundRepeat: 'no-repeat',
} }}
}, [selectedGame, accessCode]) >
<div className='flex justify-center w-full bg-blue-300/20'>
const handlePurchase = async () => { <a
if (!selectedGame || !selectedOption) return className='text-[8px]'
setIsPurchasing(true) href={image?.photographer_url}
try { target='_blank'
await axios.post('/games/purchase', { rel='noreferrer'
gameId: selectedGame.id, >
timePassId: selectedOption.label, Photo by {image?.photographer} on Pexels
guestId: accessCode </a>
})
setIsUnlocked(true)
} catch (err) {
console.error("Purchase failed", err)
setIsUnlocked(true) // Simulate success for demo
} finally {
setIsPurchasing(false)
}
}
const filteredGames = activeCategory === 'all'
? games
: games.filter((g: any) => g.game_categoryId === activeCategory)
if (showCodeGate) {
return (
<div className="min-h-screen bg-[#020617] text-white flex items-center justify-center p-6 selection:bg-violet-500/30 font-sans">
<Head>
<title>Nexus Access Gate</title>
</Head>
<div className="w-full max-w-lg">
<div className="text-center mb-12">
<div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-violet-600 to-cyan-500 rounded-3xl mb-6 shadow-2xl shadow-violet-500/20">
<BaseIcon path={mdiGamepadVariant} size={40} color="white" />
</div>
<h1 className="text-4xl font-black tracking-tighter uppercase italic mb-2">Nexus<span className="text-violet-500">Games</span></h1>
<p className="text-slate-500 text-sm font-bold uppercase tracking-widest">Premium AI Game Platform</p>
</div>
<div className="bg-slate-900/50 backdrop-blur-3xl border border-white/10 rounded-[40px] p-8 md:p-10 shadow-2xl shadow-black">
{!generatedCode ? (
<div className="space-y-8">
<div className="text-center">
<h2 className="text-2xl font-bold mb-2">Enter Access Code</h2>
<p className="text-slate-400 text-sm">Please provide your 6-digit common access code to enter the gallery.</p>
</div>
<div className="flex justify-center">
<input
type="text"
maxLength={6}
value={accessCode}
onChange={(e) => setAccessCode(e.target.value.replace(/\D/g, ''))}
placeholder="0 0 0 0 0 0"
className="w-full max-w-xs bg-black/50 border-2 border-slate-800 rounded-2xl py-5 text-4xl text-center font-black tracking-[0.5em] text-violet-500 focus:border-violet-600 outline-none transition-all placeholder:text-slate-900"
/>
</div>
<BaseButton
label="Enter Gallery"
color="info"
className="w-full py-5 rounded-2xl text-lg font-bold"
disabled={accessCode.length !== 6}
onClick={() => handleCodeSubmit()}
/>
<div className="relative flex items-center justify-center py-4">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/5"></div>
</div>
<span className="relative bg-[#0b1120] px-4 text-[10px] font-black text-slate-600 uppercase tracking-widest">New User?</span>
</div>
<button
onClick={generateNewCode}
className="w-full flex items-center justify-center space-x-2 text-slate-400 hover:text-white transition-colors text-sm font-bold"
>
<BaseIcon path={mdiRefresh} size={18} />
<span>Generate New Access Code</span>
</button>
</div>
) : (
<div className="space-y-8 text-center">
<div>
<h2 className="text-2xl font-bold mb-2 text-emerald-400">Code Generated!</h2>
<p className="text-slate-400 text-sm">Save this 6-digit code. Use it anytime to access the games area and your purchases.</p>
</div>
<div className="bg-emerald-500/10 border border-emerald-500/20 rounded-3xl p-10">
<span className="text-6xl font-black tracking-[0.2em] text-emerald-400">{generatedCode}</span>
</div>
<div className="space-y-4">
<BaseButton
label="Continue to Platform"
color="success"
className="w-full py-5 rounded-2xl text-lg font-bold"
onClick={() => handleCodeSubmit(generatedCode)}
/>
<p className="text-[10px] text-slate-500 uppercase tracking-widest font-bold">Military-Grade Encryption Active</p>
</div>
</div>
)}
</div>
<div className="mt-12 text-center">
<Link href="/admin-login" className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-700 hover:text-violet-500 transition-colors">Developer Portal Login</Link>
</div>
</div> </div>
</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="min-h-screen bg-[#020617] text-white selection:bg-violet-500/30"> <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('Nexus Gaming - Premium AI Game Platform')}</title> <title>{getPageTitle('Starter Page')}</title>
</Head> </Head>
{/* Navbar */} <SectionFullScreen bg='violet'>
<nav className="flex items-center justify-between px-6 py-4 border-b border-white/5 backdrop-blur-md sticky top-0 z-50"> <div
<div className="flex items-center space-x-2"> className={`flex ${
<div className="w-10 h-10 bg-gradient-to-br from-violet-600 to-cyan-500 rounded-xl flex items-center justify-center shadow-lg shadow-violet-500/20"> contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
<BaseIcon path={mdiGamepadVariant} size={24} color="white" /> } min-h-screen w-full`}
</div> >
<span className="text-xl font-black tracking-tighter uppercase italic">Nexus<span className="text-violet-500">Games</span></span> {contentType === 'image' && contentPosition !== 'background'
</div> ? imageBlock(illustrationImage)
<div className="hidden md:flex items-center space-x-8 text-sm font-medium text-slate-400"> : null}
<a href="#games" className="hover:text-white transition-colors">Gallery</a> {contentType === 'video' && contentPosition !== 'background'
<a href="#payment" className="hover:text-white transition-colors">Buy Access</a> ? videoBlock(illustrationVideo)
<button : null}
onClick={() => { <div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
localStorage.removeItem('accessCode'); <CardBox className='w-full md:w-3/5 lg:w-2/3'>
setShowCodeGate(true); <CardBoxComponentTitle title="Welcome to your AI Game Studio Marketplace app!"/>
}}
className="hover:text-white transition-colors text-xs font-black uppercase tracking-widest"
>
Switch User ({accessCode})
</button>
</div>
<div className="flex items-center space-x-4">
<div className="px-4 py-2 bg-violet-600/10 border border-violet-500/20 rounded-full text-xs font-black text-violet-400 tracking-widest flex items-center space-x-2">
<BaseIcon path={mdiCircle} size={10} color="currentColor" className="animate-pulse" />
<span>CODE: {accessCode}</span>
</div>
</div>
</nav>
{/* Hero Section */} <div className="space-y-3">
<section className="relative pt-20 pb-32 px-6 overflow-hidden"> <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="absolute top-0 left-1/2 -translate-x-1/2 w-[1000px] h-[600px] bg-violet-600/10 blur-[120px] rounded-full -z-10"></div> <p className='text-center '>For guides and documentation please check
<div className="max-w-6xl mx-auto text-center"> your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
<div className="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-violet-500/10 border border-violet-500/20 text-violet-400 text-[10px] font-black uppercase tracking-widest mb-6">
<BaseIcon path={mdiShieldCheck} size={14} />
<span>Intelligent Game Development & Distribution Ecosystem</span>
</div>
<h1 className="text-6xl md:text-8xl font-black tracking-tighter leading-none mb-8 uppercase italic">
NEXUS <span className="text-transparent bg-clip-text bg-gradient-to-r from-violet-400 to-cyan-400">GAMES</span><br/>
GALLERY
</h1>
<h2 className="text-2xl font-bold text-slate-400 mb-10">PREMIUM AI-POWERED GAMING</h2>
<div className="flex flex-col sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-4">
<BaseButton label="Explore Games" color="info" icon={mdiGamepadVariant} className="px-10 py-5 text-xl font-black italic rounded-2xl" href="#games" />
<Link href="/admin-login" className="px-10 py-5 bg-white/5 border border-white/10 rounded-2xl text-xl font-black italic hover:bg-white/10 transition-all flex items-center space-x-2">
<BaseIcon path={mdiBrain} size={24} />
<span>AI Developer Portal</span>
</Link>
</div>
</div>
</section>
{/* Game Gallery */}
<section id="games" className="px-6 py-24 max-w-7xl mx-auto">
<div className="flex flex-col md:flex-row md:items-end justify-between mb-16 space-y-8 md:space-y-0">
<div>
<h2 className="text-5xl font-black mb-4 tracking-tighter uppercase italic">Nexus <span className="text-violet-500">Gallery</span></h2>
<div className="flex items-center text-slate-500 text-xs font-bold uppercase tracking-[0.2em]">
<div className="w-2 h-2 bg-violet-500 rounded-full mr-3 animate-ping"></div>
Streaming Live Content
</div> </div>
</div>
<div className="flex items-center bg-white/5 p-1.5 rounded-2xl border border-white/5 overflow-x-auto whitespace-nowrap">
<button
onClick={() => setActiveCategory('all')}
className={`px-6 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest transition-all ${activeCategory === 'all' ? 'bg-violet-600 text-white shadow-xl' : 'text-slate-500 hover:text-white'}`}
>
All Genres
</button>
{categories.map((cat: any) => (
<button
key={cat.id}
onClick={() => setActiveCategory(cat.id)}
className={`px-6 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest transition-all ${activeCategory === cat.id ? 'bg-violet-600 text-white shadow-xl' : 'text-slate-500 hover:text-white'}`}
>
{cat.name}
</button>
))}
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"> <BaseButtons>
{filteredGames.map((game: any) => ( <BaseButton
<div href='/login'
key={game.id} label='Login'
onClick={() => { color='info'
setSelectedGame(game); className='w-full'
setSelectedOption(null);
window.location.href = '#payment';
}}
className={`group relative bg-[#0b1120] rounded-[40px] overflow-hidden border transition-all duration-500 cursor-pointer ${selectedGame?.id === game.id ? 'border-violet-500 ring-4 ring-violet-500/20 scale-95' : 'border-white/5 hover:border-violet-500/40 hover:-translate-y-2'}`}
>
<div className="aspect-[4/5] overflow-hidden">
<img
src={game.game_image || 'https://images.pexels.com/photos/3165335/pexels-photo-3165335.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1'}
alt={game.title}
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
/> />
</div>
<div className="absolute inset-0 bg-gradient-to-t from-[#020617] via-[#020617]/20 to-transparent p-8 flex flex-col justify-end"> </BaseButtons>
<span className="text-[10px] font-black uppercase tracking-[0.3em] text-violet-400 mb-2">{categories.find((c: any) => c.id === game.game_categoryId)?.name || 'Premium Experience'}</span> </CardBox>
<h3 className="text-3xl font-black mb-3 italic uppercase tracking-tighter leading-none">{game.title}</h3>
<div className="flex items-center justify-between">
<div className="flex items-center text-[10px] font-black uppercase tracking-widest text-slate-500">
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full mr-2 shadow-lg shadow-emerald-500/50"></div>
ACTIVE_SERVER
</div>
<BaseIcon path={mdiArrowRight} size={20} className="text-white opacity-0 group-hover:opacity-100 -translate-x-4 group-hover:translate-x-0 transition-all" />
</div>
</div>
</div>
))}
</div> </div>
</section> </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>
{/* Integrated Selection & Payment */}
{selectedGame && (
<section id="payment" className="px-6 py-32 bg-[#020617] border-y border-white/5 relative overflow-hidden">
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-violet-600/5 blur-[120px] rounded-full"></div>
<div className="absolute bottom-0 left-0 w-[600px] h-[600px] bg-cyan-600/5 blur-[120px] rounded-full"></div>
<div className="max-w-6xl mx-auto relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-20 items-start">
<div>
<div className="flex items-center space-x-8 mb-16">
<div className="w-28 h-28 rounded-[32px] overflow-hidden border border-white/10 shadow-2xl rotate-3 scale-110">
<img src={selectedGame.game_image} alt="" className="w-full h-full object-cover" />
</div>
<div>
<h3 className="text-5xl font-black italic uppercase tracking-tighter mb-2">{selectedGame.title}</h3>
<div className="flex items-center space-x-3">
<span className="px-3 py-1 bg-violet-600/20 text-violet-400 text-[10px] font-black uppercase tracking-[0.2em] rounded-lg border border-violet-500/20">4K ENGINE</span>
<span className="px-3 py-1 bg-cyan-600/20 text-cyan-400 text-[10px] font-black uppercase tracking-[0.2em] rounded-lg border border-cyan-500/20">INSTANT_PLAY</span>
</div>
</div>
</div>
{isUnlocked ? (
<div className="bg-emerald-500/5 border border-emerald-500/10 p-12 rounded-[56px] text-center shadow-2xl shadow-emerald-500/5 backdrop-blur-3xl">
<div className="w-28 h-28 bg-emerald-500 rounded-full flex items-center justify-center mx-auto mb-10 shadow-2xl shadow-emerald-500/30">
<BaseIcon path={mdiCheckCircle} size={64} color="white" />
</div>
<h4 className="text-4xl font-black mb-4 uppercase italic tracking-tighter">Access Authorized</h4>
<p className="text-slate-400 mb-12 font-medium text-lg leading-relaxed">System successfully linked to your access code.<br/><span className="text-emerald-400 font-black">Ready for deployment.</span></p>
<BaseButton
label="Initialize System"
color="success"
icon={mdiRocketLaunch}
className="w-full py-8 text-2xl font-black uppercase tracking-tighter rounded-[32px] shadow-2xl shadow-emerald-500/20 hover:scale-[1.02] active:scale-[0.98] transition-all"
onClick={() => window.open(selectedGame.web_play_url, '_blank')}
/>
</div>
) : (
<div className="space-y-10">
<div>
<h4 className="text-xs font-black uppercase tracking-[0.4em] mb-8 flex items-center text-slate-500">
<BaseIcon path={mdiClockOutline} size={18} className="mr-4 text-violet-500" />
Select Time-Pass Duration
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{pricingOptions.map((opt: any) => (
<div
key={opt.label}
onClick={() => setSelectedOption(opt)}
className={`p-7 rounded-[32px] border-2 cursor-pointer transition-all duration-300 ${selectedOption?.label === opt.label ? 'bg-violet-600 border-violet-400 shadow-2xl shadow-violet-500/30 translate-x-2' : 'bg-white/5 border-white/5 hover:bg-white/10 hover:border-white/10'}`}
>
<div className="flex justify-between items-start mb-6">
<div className="text-2xl font-black uppercase italic tracking-tighter">{opt.label}</div>
{selectedOption?.label === opt.label && (
<div className="w-6 h-6 bg-white rounded-full flex items-center justify-center">
<BaseIcon path={mdiCheckCircle} size={18} className="text-violet-600" />
</div>
)}
</div>
<div className="flex items-end justify-between">
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Global Access</div>
<div className="text-3xl font-black text-white">R$ {opt.price}</div>
</div>
</div>
))}
</div>
</div>
<div className="p-8 bg-violet-600/5 rounded-[32px] border border-violet-500/10 flex items-center justify-between backdrop-blur-md">
<div className="flex items-center space-x-6">
<div className="w-14 h-14 bg-violet-600/20 rounded-2xl flex items-center justify-center">
<BaseIcon path={mdiLockOutline} size={28} className="text-violet-500" />
</div>
<div>
<div className="text-[10px] font-black text-slate-500 uppercase tracking-widest mb-1">Authenticated ID</div>
<div className="text-lg font-mono font-bold text-violet-400 tracking-tighter">NEXUS_USR_{accessCode}</div>
</div>
</div>
<div className="hidden sm:block">
<BaseIcon path={mdiShieldCheck} size={32} className="text-emerald-500" />
</div>
</div>
</div>
)}
</div>
{!isUnlocked && (
<div className="bg-gradient-to-br from-violet-600 via-violet-500 to-cyan-500 p-1.5 rounded-[60px] shadow-2xl shadow-violet-500/20 group">
<div className="bg-[#020617] rounded-[58px] p-12 md:p-16 h-full flex flex-col items-center">
<div className="text-center mb-16">
<h3 className="text-4xl font-black mb-4 uppercase italic tracking-tighter">Official Payment</h3>
<p className="text-slate-500 text-xs font-black uppercase tracking-[0.3em]">Scan QR Code for Instant Access</p>
</div>
<div className="flex justify-center mb-16 w-full">
<div className="flex flex-col items-center group/qr w-full max-w-sm">
<div className="w-full aspect-square bg-white p-5 rounded-[48px] mb-6 shadow-2xl shadow-white/5 transition-transform duration-500 group-hover/qr:scale-105">
<img src="/payment-qr.jpg" alt="Payment QR" className="w-full h-full object-contain" />
</div>
<span className="text-xs font-black uppercase tracking-[0.3em] text-cyan-400 italic">Official Payment Network</span>
</div>
</div>
<div className="w-full space-y-8">
<div className="text-center flex items-center justify-center space-x-4 mb-4">
<div className="flex space-x-1.5">
<div className="w-2 h-2 bg-violet-500 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-violet-500 rounded-full animate-bounce delay-100"></div>
<div className="w-2 h-2 bg-violet-500 rounded-full animate-bounce delay-200"></div>
</div>
<span className="text-[10px] font-black uppercase tracking-[0.4em] text-slate-500">Awaiting Confirmation</span>
</div>
<button
disabled={!selectedOption || isPurchasing}
onClick={handlePurchase}
className="w-full group relative"
>
<div className="absolute -inset-1.5 bg-gradient-to-r from-violet-600 to-cyan-500 rounded-3xl blur opacity-30 group-hover:opacity-100 transition duration-500"></div>
<div className="relative px-8 py-6 bg-violet-600 rounded-3xl leading-none flex items-center justify-center space-x-4 group-disabled:opacity-50">
<BaseIcon path={mdiQrcode} size={28} color="white" />
<span className="text-xl font-black uppercase italic tracking-tighter text-white">
{isPurchasing ? 'Processing Request...' : 'Confirm Payment'}
</span>
</div>
</button>
<div className="flex items-center justify-center space-x-8 opacity-30">
<div className="flex items-center space-x-2">
<BaseIcon path={mdiShieldCheck} size={16} />
<span className="text-[8px] font-black uppercase tracking-widest">Encrypted</span>
</div>
<div className="flex items-center space-x-2">
<BaseIcon path={mdiLockOutline} size={16} />
<span className="text-[8px] font-black uppercase tracking-widest">Authorized</span>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
</section>
)}
{/* Footer */}
<footer className="px-6 py-24 border-t border-white/5 text-center bg-black">
<div className="flex items-center justify-center space-x-3 mb-12">
<div className="w-12 h-12 bg-gradient-to-br from-violet-600 to-cyan-500 rounded-2xl flex items-center justify-center shadow-xl shadow-violet-500/20">
<BaseIcon path={mdiGamepadVariant} size={24} color="white" />
</div>
<span className="text-2xl font-black tracking-tighter uppercase italic">Nexus<span className="text-violet-500">Games</span></span>
</div>
<div className="flex flex-wrap justify-center gap-10 mb-16 text-[10px] font-black uppercase tracking-[0.4em] text-slate-600">
<a href="#" className="hover:text-white transition-colors">Infrastructure</a>
<a href="#" className="hover:text-white transition-colors">Neural Engine</a>
<a href="#" className="hover:text-white transition-colors">Legal</a>
<a href="#" className="hover:text-white transition-colors">Connect</a>
</div>
<p className="text-slate-800 text-[10px] tracking-[0.6em] uppercase font-black">© 2026 NEXUS GAMING ECOSYSTEM QUANTUM SECURED PLATFORM</p>
</footer>
</div> </div>
) );
} }
IndexPage.getLayout = function getLayout(page: ReactElement) { Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest> return <LayoutGuest>{page}</LayoutGuest>;
} };

View File

@ -0,0 +1,276 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import BaseIcon from "../components/BaseIcon";
import { mdiInformation, mdiEye, mdiEyeOff } from '@mdi/js';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import { Field, Form, Formik } from 'formik';
import FormField from '../components/FormField';
import FormCheckRadio from '../components/FormCheckRadio';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router';
import { getPageTitle } from '../config';
import { findMe, loginUser, resetAction } from '../stores/authSlice';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import Link from 'next/link';
import {toast, ToastContainer} from "react-toastify";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'
export default function Login() {
const router = useRouter();
const dispatch = useAppDispatch();
const textColor = useAppSelector((state) => state.style.linkColor);
const iconsColor = useAppSelector((state) => state.style.iconsColor);
const notify = (type, msg) => toast(msg, { type });
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('right');
const [showPassword, setShowPassword] = useState(false);
const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector(
(state) => state.auth,
);
const [initialValues, setInitialValues] = React.useState({ email:'admin@flatlogic.com',
password: '8ab6346a',
remember: true })
const title = 'AI Game Studio Marketplace'
// Fetch Pexels image/video
useEffect( () => {
async function fetchData() {
const image = await getPexelsImage()
const video = await getPexelsVideo()
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
// Fetch user data
useEffect(() => {
if (token) {
dispatch(findMe());
}
}, [token, dispatch]);
// Redirect to dashboard if user is logged in
useEffect(() => {
if (currentUser?.id) {
router.push('/dashboard');
}
}, [currentUser?.id, router]);
// Show error message if there is one
useEffect(() => {
if (errorMessage){
notify('error', errorMessage)
}
}, [errorMessage])
// Show notification if there is one
useEffect(() => {
if (notifyState?.showNotification) {
notify('success', notifyState?.textNotification)
dispatch(resetAction());
}
}, [notifyState?.showNotification])
const togglePasswordVisibility = () => {
setShowPassword(!showPassword);
};
const handleSubmit = async (value) => {
const {remember, ...rest} = value
await dispatch(loginUser(rest));
};
const setLogin = (target: HTMLElement) => {
setInitialValues(prev => ({
...prev,
email : target.innerText.trim(),
password: target.dataset.password ?? '',
}));
};
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',
} : {}}>
<Head>
<title>{getPageTitle('Login')}</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 id="loginRoles" className='w-full md:w-3/5 lg:w-2/3'>
<h2 className="text-4xl font-semibold my-4">{title}</h2>
<div className='flex flex-row justify-between'>
<div>
<p className='mb-2'>Use{' '}
<code className={`cursor-pointer ${textColor} `}
data-password="8ab6346a"
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
<code className={`${textColor}`}>8ab6346a</code>{' / '}
to login as Admin</p>
<p>Use <code
className={`cursor-pointer ${textColor} `}
data-password="c3af9d1c13ec"
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
<code className={`${textColor}`}>c3af9d1c13ec</code>{' / '}
to login as User</p>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
path={mdiInformation}
/>
</div>
</div>
</CardBox>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<Formik
initialValues={initialValues}
enableReinitialize
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField
label='Login'
help='Please enter your login'>
<Field name='email' />
</FormField>
<div className='relative'>
<FormField
label='Password'
help='Please enter your password'>
<Field name='password' type={showPassword ? 'text' : 'password'} />
</FormField>
<div
className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer'
onClick={togglePasswordVisibility}
>
<BaseIcon
className='text-gray-500 hover:text-gray-700'
size={20}
path={showPassword ? mdiEyeOff : mdiEye}
/>
</div>
</div>
<div className={'flex justify-between'}>
<FormCheckRadio type='checkbox' label='Remember'>
<Field type='checkbox' name='remember' />
</FormCheckRadio>
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
Forgot password?
</Link>
</div>
<BaseDivider />
<BaseButtons>
<BaseButton
className={'w-full'}
type='submit'
label={isFetching ? 'Loading...' : 'Login'}
color='info'
disabled={isFetching}
/>
</BaseButtons>
<br />
<p className={'text-center'}>
Dont have an account yet?{' '}
<Link className={`${textColor}`} href={'/register'}>
New Account
</Link>
</p>
</Form>
</Formik>
</CardBox>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. © All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
<ToastContainer />
</div>
);
}
Login.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -0,0 +1,92 @@
import React from 'react';
import type { ReactElement } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import Head from 'next/head';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import { Field, Form, Formik } from 'formik';
import FormField from '../components/FormField';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router';
import { getPageTitle } from '../config';
import axios from "axios";
export default function Register() {
const [loading, setLoading] = React.useState(false);
const router = useRouter();
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const handleSubmit = async (value) => {
setLoading(true)
try {
const { data: response } = await axios.post('/auth/signup',value);
await router.push('/login')
setLoading(false)
notify('success', 'Please check your email for verification link')
} catch (error) {
setLoading(false)
console.log('error: ', error)
notify('error', 'Something was wrong. Try again')
}
};
return (
<>
<Head>
<title>{getPageTitle('Login')}</title>
</Head>
<SectionFullScreen bg='violet'>
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
<Formik
initialValues={{
email: '',
password: '',
confirm: ''
}}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Email' help='Please enter your email'>
<Field type='email' name='email' />
</FormField>
<FormField label='Password' help='Please enter your password'>
<Field type='password' name='password' />
</FormField>
<FormField label='Confirm Password' help='Please confirm your password'>
<Field type='password' name='confirm' />
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton
type='submit'
label={loading ? 'Loading...' : 'Register' }
color='info'
/>
<BaseButton
href={'/login'}
label={'Login'}
color='info'
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionFullScreen>
<ToastContainer />
</>
);
}
Register.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -1,83 +1,62 @@
import React, { useEffect, useState } from 'react'; import React from 'react';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import CardBox from '../components/CardBox';
import axios from 'axios';
import { mdiCheckCircleOutline, mdiAlertCircleOutline } from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
import SectionFullScreen from '../components/SectionFullScreen'; import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest'; import LayoutGuest from '../layouts/Guest';
import { useRouter } from 'next/router';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
import axios from 'axios';
export default function VerifyEmail() { export default function Verify() {
const router = useRouter(); const [loading, setLoading] = React.useState(false);
const { token } = router.query; const router = useRouter();
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading'); const { token } = router.query;
const notify = (type, msg) => toast(msg, { type });
useEffect(() => { React.useEffect(() => {
if (token) { if (!token) {
axios router.push('/login');
.put('/auth/verify-email', { token }) return;
.then(() => { }
setStatus('success'); const handleSubmit = async () => {
setTimeout(() => {
router.push('/admin-login');
}, 3000);
})
.catch(() => {
setStatus('error');
});
}
}, [token, router]);
const renderContent = () => { setLoading(true);
switch (status) { await axios.put('/auth/verify-email', {
case 'loading': token,
return ( }).then(verified => {
<div className="text-center"> if (verified) {
<h1 className="text-2xl font-semibold mb-4 text-white">Verifying your email...</h1> setLoading(false);
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white mx-auto"></div> notify('success', 'Your email was verified');
</div> }
); }).catch(error => {
case 'success': setLoading(false);
return ( console.log('error: ', error);
<div className="text-center"> notify('error', error.response);
<BaseIcon path={mdiCheckCircleOutline} size={64} className="text-emerald-500 mb-4 mx-auto" /> }).finally(async () => {
<h1 className="text-2xl font-semibold mb-2 text-white">Email Verified!</h1> await router.push('/login');
<p className="text-slate-400">Your email has been successfully verified. Redirecting you to login...</p> });
</div> };
); handleSubmit().then();
case 'error': }, [token]);
return (
<div className="text-center">
<BaseIcon path={mdiAlertCircleOutline} size={64} className="text-rose-500 mb-4 mx-auto" />
<h1 className="text-2xl font-semibold mb-2 text-white">Verification Failed</h1>
<p className="text-slate-400">The verification link is invalid or has expired.</p>
<button
onClick={() => router.push('/admin-login')}
className="mt-6 px-6 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors"
>
Go to Login
</button>
</div>
);
}
};
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Verify Email')}</title> <title>{getPageTitle('Verify Email')}</title>
</Head> </Head>
<SectionFullScreen bg="violet"> <SectionFullScreen bg='violet'>
<div className="w-full max-w-md p-8 bg-slate-900/50 backdrop-blur-xl border border-white/10 rounded-3xl shadow-2xl"> <CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
{renderContent()} <p>{loading ? 'Loading...' : ''}</p>
</div> </CardBox>
</SectionFullScreen> </SectionFullScreen>
</>
); <ToastContainer />
</>
);
} }
VerifyEmail.getLayout = function getLayout(page: ReactElement) { Verify.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>; return <LayoutGuest>{page}</LayoutGuest>;
}; };

View File

@ -81,22 +81,6 @@ export const create = createAsyncThunk('ai_game_projects/createAi_game_projects'
} }
}) })
export const generate = createAsyncThunk('ai_game_projects/generate', async (data: any, { rejectWithValue }) => {
try {
const result = await axios.post(
'ai_game_projects/generate',
{ data }
)
return result.data
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
})
export const uploadCsv = createAsyncThunk( export const uploadCsv = createAsyncThunk(
'ai_game_projects/uploadCsv', 'ai_game_projects/uploadCsv',
async (file: File, { rejectWithValue }) => { async (file: File, { rejectWithValue }) => {
@ -211,20 +195,6 @@ export const ai_game_projectsSlice = createSlice({
fulfilledNotify(state, `${'Ai_game_projects'.slice(0, -1)} has been created`); fulfilledNotify(state, `${'Ai_game_projects'.slice(0, -1)} has been created`);
}) })
builder.addCase(generate.pending, (state) => {
state.loading = true
resetNotify(state);
})
builder.addCase(generate.rejected, (state, action) => {
state.loading = false
rejectNotify(state, action);
})
builder.addCase(generate.fulfilled, (state) => {
state.loading = false
fulfilledNotify(state, `AI Game Project generation started`);
state.refetch = true;
})
builder.addCase(update.pending, (state) => { builder.addCase(update.pending, (state) => {
state.loading = true state.loading = true
resetNotify(state); resetNotify(state);