Aluga Games Plataform
This commit is contained in:
parent
45cc629ea0
commit
aff6a89b95
@ -16,91 +16,59 @@ module.exports = function(sequelize, DataTypes) {
|
||||
|
||||
project_name: {
|
||||
type: DataTypes.TEXT,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
project_status: {
|
||||
type: DataTypes.ENUM,
|
||||
|
||||
|
||||
|
||||
values: [
|
||||
|
||||
"idea",
|
||||
|
||||
|
||||
"generating",
|
||||
|
||||
|
||||
"building",
|
||||
|
||||
|
||||
"testing",
|
||||
|
||||
|
||||
"ready",
|
||||
|
||||
|
||||
"failed"
|
||||
|
||||
"idea",
|
||||
"generating",
|
||||
"building",
|
||||
"testing",
|
||||
"ready",
|
||||
"failed"
|
||||
],
|
||||
|
||||
},
|
||||
|
||||
target_dimension: {
|
||||
type: DataTypes.ENUM,
|
||||
|
||||
|
||||
|
||||
values: [
|
||||
|
||||
"2d",
|
||||
|
||||
|
||||
"3d",
|
||||
|
||||
|
||||
"mixed"
|
||||
|
||||
"2d",
|
||||
"3d",
|
||||
"mixed"
|
||||
],
|
||||
|
||||
},
|
||||
|
||||
game_concept: {
|
||||
type: DataTypes.TEXT,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
design_document: {
|
||||
type: DataTypes.TEXT,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
configuration_notes: {
|
||||
type: DataTypes.TEXT,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
requested_at: {
|
||||
type: DataTypes.DATE,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
completed_at: {
|
||||
type: DataTypes.DATE,
|
||||
|
||||
|
||||
},
|
||||
|
||||
play_url: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
download_url_pc: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
download_url_mobile: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
importHash: {
|
||||
@ -117,30 +85,6 @@ completed_at: {
|
||||
);
|
||||
|
||||
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, {
|
||||
as: 'owner_user',
|
||||
foreignKey: {
|
||||
@ -157,8 +101,6 @@ completed_at: {
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
|
||||
|
||||
db.ai_game_projects.hasMany(db.file, {
|
||||
as: 'project_files',
|
||||
foreignKey: 'belongsToId',
|
||||
@ -169,7 +111,6 @@ completed_at: {
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
db.ai_game_projects.belongsTo(db.users, {
|
||||
as: 'createdBy',
|
||||
});
|
||||
@ -179,9 +120,5 @@ completed_at: {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
return ai_game_projects;
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
@ -4,31 +4,10 @@ const passport = require('passport');
|
||||
const config = require('../config');
|
||||
const AuthService = require('../services/auth');
|
||||
const ForbiddenError = require('../services/notifications/errors/forbidden');
|
||||
const EmailSender = require('../services/email');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
|
||||
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
|
||||
* tags:
|
||||
@ -36,32 +15,6 @@ const router = express.Router();
|
||||
* description: Authorization operations
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/auth/signin/local:
|
||||
* post:
|
||||
* tags: [Auth]
|
||||
* summary: Logs user into the system
|
||||
* description: Logs user into the system
|
||||
* requestBody:
|
||||
* description: Set valid user email and password
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Auth"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Successful login
|
||||
* 400:
|
||||
* description: Invalid username/password supplied
|
||||
* x-codegen-request-body-name: body
|
||||
*/
|
||||
|
||||
router.post('/signin/local', wrapAsync(async (req, res) => {
|
||||
const payload = await AuthService.signin(req.body.email, req.body.password, req,);
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/auth/signin/private-key:
|
||||
@ -91,6 +44,35 @@ router.post('/signin/private-key', wrapAsync(async (req, res) => {
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /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);
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/auth/me:
|
||||
@ -104,10 +86,8 @@ router.post('/signin/private-key', wrapAsync(async (req, res) => {
|
||||
* 200:
|
||||
* description: Successful retrieval of current authorized user data
|
||||
* 400:
|
||||
* description: Invalid username/password supplied
|
||||
* x-codegen-request-body-name: body
|
||||
* description: Invalid token supplied
|
||||
*/
|
||||
|
||||
router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) => {
|
||||
if (!req.currentUser || !req.currentUser.id) {
|
||||
throw new ForbiddenError();
|
||||
@ -118,68 +98,6 @@ router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) =>
|
||||
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) => {
|
||||
if (!req.currentUser || !req.currentUser.id) {
|
||||
throw new ForbiddenError();
|
||||
@ -190,47 +108,6 @@ router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync
|
||||
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);
|
||||
|
||||
function socialRedirect(res, state, token, config) {
|
||||
res.redirect(config.uiUrl + "/login?token=" + token);
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
||||
@ -48,22 +48,22 @@ module.exports = class Ai_game_projectsService {
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
// 2. Trigger AI generation in the background (or wait for it if prompt is short)
|
||||
// For this implementation, we'll wait to ensure the user sees progress
|
||||
const prompt = `You are an expert game developer. Create a comprehensive Game Design Document (GDD) for a ${data.target_dimension || '2D'} game based on the following concept: "${data.game_concept}".
|
||||
// 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
|
||||
2. Story & Setting
|
||||
3. Technical Requirements
|
||||
4. Asset List (Characters, Environments, UI)
|
||||
5. Monetization Strategy
|
||||
1. Game Mechanics (Fully defined)
|
||||
2. Level Structure
|
||||
3. Technical Build Specs (PCs, Smartphones, Tablets)
|
||||
4. Asset Manifest
|
||||
5. Logic Flow (Input, Physics, UI)
|
||||
|
||||
Output the GDD in markdown format.`;
|
||||
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 game development assistant.' },
|
||||
{ role: 'system', content: 'You are an advanced autonomous game development engine.' },
|
||||
{ role: 'user', content: prompt }
|
||||
],
|
||||
options: { poll_interval: 5, poll_timeout: 300 }
|
||||
@ -72,13 +72,28 @@ module.exports = class Ai_game_projectsService {
|
||||
if (aiResponse.success) {
|
||||
const designDoc = LocalAIApi.extractText(aiResponse);
|
||||
|
||||
// 3. Update the record with generated content
|
||||
// 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 }
|
||||
);
|
||||
@ -102,32 +117,26 @@ module.exports = class Ai_game_projectsService {
|
||||
|
||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
await processFile(req, res);
|
||||
const bufferStream = new stream.PassThrough();
|
||||
const results = [];
|
||||
|
||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
||||
|
||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8"));
|
||||
await new Promise((resolve, reject) => {
|
||||
bufferStream
|
||||
.pipe(csv())
|
||||
.on('data', (data) => results.push(data))
|
||||
.on('end', async () => {
|
||||
console.log('CSV results', results);
|
||||
resolve();
|
||||
})
|
||||
.on('error', (error) => reject(error));
|
||||
})
|
||||
|
||||
await Ai_game_projectsDBApi.bulkImport(results, {
|
||||
transaction,
|
||||
ignoreDuplicates: true,
|
||||
validate: true,
|
||||
currentUser: req.currentUser
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
@ -138,29 +147,11 @@ module.exports = class Ai_game_projectsService {
|
||||
static async update(data, id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
let ai_game_projects = await Ai_game_projectsDBApi.findBy(
|
||||
{id},
|
||||
{transaction},
|
||||
);
|
||||
|
||||
if (!ai_game_projects) {
|
||||
throw new ValidationError(
|
||||
'ai_game_projectsNotFound',
|
||||
);
|
||||
}
|
||||
|
||||
const updatedAi_game_projects = await Ai_game_projectsDBApi.update(
|
||||
id,
|
||||
data,
|
||||
{
|
||||
currentUser,
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
|
||||
let ai_game_projects = await Ai_game_projectsDBApi.findBy({id}, {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();
|
||||
return updatedAi_game_projects;
|
||||
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
@ -169,13 +160,8 @@ module.exports = class Ai_game_projectsService {
|
||||
|
||||
static async deleteByIds(ids, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
await Ai_game_projectsDBApi.deleteByIds(ids, {
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Ai_game_projectsDBApi.deleteByIds(ids, { currentUser, transaction });
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
@ -185,22 +171,12 @@ module.exports = class Ai_game_projectsService {
|
||||
|
||||
static async remove(id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
await Ai_game_projectsDBApi.remove(
|
||||
id,
|
||||
{
|
||||
currentUser,
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
|
||||
await Ai_game_projectsDBApi.remove(id, { currentUser, transaction });
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
@ -2,136 +2,19 @@ const UsersDBApi = require('../db/api/users');
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const ForbiddenError = require('./notifications/errors/forbidden');
|
||||
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 helpers = require('../helpers');
|
||||
const db = require('../db/models');
|
||||
|
||||
class Auth {
|
||||
static async signup(email, password, options = {}, host) {
|
||||
const user = await UsersDBApi.findBy({email});
|
||||
|
||||
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);
|
||||
// Disabled as per user request to remove account creation options
|
||||
throw new ValidationError('auth.signupDisabled');
|
||||
}
|
||||
|
||||
static async signin(email, password, options = {}) {
|
||||
const user = await UsersDBApi.findBy({email});
|
||||
|
||||
if (!user) {
|
||||
throw new ValidationError(
|
||||
'auth.userNotFound',
|
||||
);
|
||||
}
|
||||
|
||||
if (user.disabled) {
|
||||
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 = {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email
|
||||
}
|
||||
};
|
||||
|
||||
return helpers.jwtSign(data);
|
||||
// Disabled as per user request to remove email/password login
|
||||
throw new ValidationError('auth.signinDisabled');
|
||||
}
|
||||
|
||||
static async signinWithPrivateKey(privateKey, options = {}) {
|
||||
@ -156,181 +39,62 @@ class Auth {
|
||||
const data = {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email
|
||||
email: user.email,
|
||||
role: 'admin' // Explicitly marking as admin
|
||||
}
|
||||
};
|
||||
|
||||
return helpers.jwtSign(data);
|
||||
}
|
||||
|
||||
static async sendEmailAddressVerificationEmail(
|
||||
email,
|
||||
host,
|
||||
) {
|
||||
|
||||
|
||||
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',
|
||||
);
|
||||
static async signinWithAccessCode(code, options = {}) {
|
||||
// Users use a 6-digit code to access the platform
|
||||
if (!code || code.length !== 6 || !/^\d+$/.test(code)) {
|
||||
throw new ValidationError('auth.invalidAccessCode');
|
||||
}
|
||||
|
||||
const emailAddressVerificationEmail = new EmailAddressVerificationEmail(
|
||||
email,
|
||||
link,
|
||||
);
|
||||
// For common users, we don't necessarily need a persistent user record
|
||||
// in the same way, but we can return a JWT that identifies them by their code/session
|
||||
const data = {
|
||||
user: {
|
||||
id: `guest_${code}`,
|
||||
email: `guest_${code}@platform.com`,
|
||||
role: 'user',
|
||||
guestId: code
|
||||
}
|
||||
};
|
||||
|
||||
return new EmailSender(
|
||||
emailAddressVerificationEmail,
|
||||
).send();
|
||||
return helpers.jwtSign(data);
|
||||
}
|
||||
|
||||
static async sendPasswordResetEmail(email, type = 'register', host) {
|
||||
|
||||
|
||||
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 sendEmailAddressVerificationEmail() {
|
||||
throw new ValidationError('auth.featureDisabled');
|
||||
}
|
||||
|
||||
static async verifyEmail(token, options = {}) {
|
||||
const user = await UsersDBApi.findByEmailVerificationToken(
|
||||
token,
|
||||
options,
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
throw new ValidationError(
|
||||
'auth.emailAddressVerificationEmail.invalidToken',
|
||||
);
|
||||
}
|
||||
|
||||
return UsersDBApi.markEmailVerified(
|
||||
user.id,
|
||||
options,
|
||||
);
|
||||
static async sendPasswordResetEmail() {
|
||||
throw new ValidationError('auth.featureDisabled');
|
||||
}
|
||||
|
||||
static async passwordUpdate(currentPassword, newPassword, options) {
|
||||
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 verifyEmail() {
|
||||
throw new ValidationError('auth.featureDisabled');
|
||||
}
|
||||
|
||||
static async passwordReset(
|
||||
token,
|
||||
password,
|
||||
options = {},
|
||||
) {
|
||||
const user = await UsersDBApi.findByPasswordResetToken(
|
||||
token,
|
||||
options,
|
||||
);
|
||||
static async passwordUpdate() {
|
||||
throw new ValidationError('auth.featureDisabled');
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new ValidationError(
|
||||
'auth.passwordReset.invalidToken',
|
||||
);
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(
|
||||
password,
|
||||
config.bcrypt.saltRounds,
|
||||
);
|
||||
|
||||
return UsersDBApi.updatePassword(
|
||||
user.id,
|
||||
hashedPassword,
|
||||
options,
|
||||
);
|
||||
static async passwordReset() {
|
||||
throw new ValidationError('auth.featureDisabled');
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
try {
|
||||
await UsersDBApi.findBy(
|
||||
{id: currentUser.id},
|
||||
{transaction},
|
||||
);
|
||||
|
||||
await UsersDBApi.update(
|
||||
currentUser.id,
|
||||
data,
|
||||
{
|
||||
currentUser,
|
||||
transaction
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
await UsersDBApi.update(currentUser.id, data, { currentUser, transaction });
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
@ -339,4 +103,4 @@ class Auth {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Auth;
|
||||
module.exports = Auth;
|
||||
|
||||
@ -2,6 +2,7 @@ import React from 'react'
|
||||
import { MenuAsideItem } from '../interfaces'
|
||||
import AsideMenuLayer from './AsideMenuLayer'
|
||||
import OverlayLayer from './OverlayLayer'
|
||||
import { useAppSelector } from '../stores/hooks'
|
||||
|
||||
type Props = {
|
||||
menu: MenuAsideItem[]
|
||||
@ -15,10 +16,24 @@ export default function AsideMenu({
|
||||
isAsideLgActive = false,
|
||||
...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 (
|
||||
<>
|
||||
<AsideMenuLayer
|
||||
menu={props.menu}
|
||||
menu={filteredMenu}
|
||||
className={`${isAsideMobileExpanded ? 'left-0' : '-left-60 lg:left-0'} ${
|
||||
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
|
||||
}`}
|
||||
@ -27,4 +42,4 @@ export default function AsideMenu({
|
||||
{isAsideLgActive && <OverlayLayer zIndex="z-30" onClick={props.onAsideLgClose} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,100 +1,83 @@
|
||||
import React, {useEffect, useRef} from 'react'
|
||||
import React, { ReactNode, useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||
import BaseDivider from './BaseDivider'
|
||||
import BaseIcon from './BaseIcon'
|
||||
import UserAvatarCurrentUser from './UserAvatarCurrentUser'
|
||||
import NavBarMenuList from './NavBarMenuList'
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||
import { MenuNavBarItem } from '../interfaces'
|
||||
import { setDarkMode } from '../stores/styleSlice'
|
||||
import { logoutUser } from '../stores/authSlice'
|
||||
import { useRouter } from 'next/router';
|
||||
import ClickOutside from "./ClickOutside";
|
||||
import { setAsideLgActive } from '../stores/styleSlice'
|
||||
|
||||
type Props = {
|
||||
item: MenuNavBarItem
|
||||
}
|
||||
|
||||
export default function NavBarItem({ item }: Props) {
|
||||
const router = useRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
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 dispatch = useAppDispatch()
|
||||
const router = useRouter()
|
||||
const { currentUser } = useAppSelector((state) => state.auth)
|
||||
|
||||
const [isDropdownActive, setIsDropdownActive] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
return () => setIsDropdownActive(false);
|
||||
}, [router.pathname]);
|
||||
const activeClassAddon =
|
||||
item.href && router.asPath === item.href ? 'text-blue-600 dark:text-slate-400' : ''
|
||||
|
||||
const componentClass = [
|
||||
'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 wrapperClass = `block lg:flex items-center relative cursor-pointer ${
|
||||
item.menu ? 'bg-gray-100 lg:bg-transparent dark:bg-slate-800 lg:dark:bg-transparent' : ''
|
||||
}`
|
||||
|
||||
const itemLabel = item.isCurrentUser ? userName : item.label
|
||||
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 getLabel = () => {
|
||||
if (item.isCurrentUser) {
|
||||
if (currentUser) {
|
||||
return currentUser.firstName || currentUser.email
|
||||
}
|
||||
return 'Guest'
|
||||
}
|
||||
return item.label
|
||||
}
|
||||
|
||||
const handleMenuClick = () => {
|
||||
if (item.menu) {
|
||||
setIsDropdownActive(!isDropdownActive)
|
||||
}
|
||||
|
||||
if (item.isToggleLightDark) {
|
||||
dispatch(setDarkMode(null))
|
||||
}
|
||||
|
||||
if(item.isLogout) {
|
||||
dispatch(logoutUser())
|
||||
router.push('/login')
|
||||
if (item.isLogout) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
const getItemId = (label) => {
|
||||
switch (label) {
|
||||
case 'Light/Dark':
|
||||
return 'themeToggle';
|
||||
case 'Log out':
|
||||
return 'logout';
|
||||
default:
|
||||
return undefined;
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
setIsDropdownActive(false)
|
||||
}
|
||||
};
|
||||
|
||||
router.events.on('routeChangeStart', handleRouteChange)
|
||||
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', handleRouteChange)
|
||||
}
|
||||
}, [router.events])
|
||||
|
||||
const NavBarItemComponentContents = (
|
||||
<>
|
||||
<div
|
||||
id={getItemId(itemLabel)}
|
||||
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'
|
||||
className={`${baseClass} ${
|
||||
isDropdownActive ? 'lg:bg-gray-100 lg:dark:bg-slate-800' : ''
|
||||
}`}
|
||||
onClick={handleMenuClick}
|
||||
>
|
||||
{item.icon && <BaseIcon path={item.icon} size={22} className="transition-colors" />}
|
||||
<span
|
||||
className={`px-2 transition-colors w-40 grow ${
|
||||
item.isDesktopNoLabel && item.icon ? 'lg:hidden' : ''
|
||||
}`}
|
||||
>
|
||||
{itemLabel}
|
||||
</span>
|
||||
{item.isCurrentUser && <UserAvatarCurrentUser className="w-6 h-6 mr-3 inline-flex" />}
|
||||
{item.icon && <BaseIcon path={item.icon} className="transition-colors" />}
|
||||
<span
|
||||
className={`px-2 transition-colors ${
|
||||
item.menu ? 'lg:hidden' : ''
|
||||
} xl:inline-flex`}
|
||||
>
|
||||
{getLabel()}
|
||||
</span>
|
||||
{item.menu && (
|
||||
<BaseIcon
|
||||
path={isDropdownActive ? mdiChevronUp : mdiChevronDown}
|
||||
@ -106,27 +89,21 @@ export default function NavBarItem({ item }: Props) {
|
||||
<div
|
||||
className={`${
|
||||
!isDropdownActive ? 'lg:hidden' : ''
|
||||
} 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`}
|
||||
} 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`}
|
||||
>
|
||||
<ClickOutside onClickOutside={() => setIsDropdownActive(false)} excludedElements={[excludedRef]}>
|
||||
<NavBarMenuList menu={item.menu} />
|
||||
</ClickOutside>
|
||||
<NavBarMenuList menu={item.menu} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
if (item.isDivider) {
|
||||
return <BaseDivider navBar />
|
||||
}
|
||||
|
||||
if (item.href) {
|
||||
return (
|
||||
<Link href={item.href} target={item.target} className={componentClass}>
|
||||
<Link href={item.href} target={item.target} className={wrapperClass}>
|
||||
{NavBarItemComponentContents}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
|
||||
}
|
||||
return <div className={wrapperClass}>{NavBarItemComponentContents}</div>
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
import React, { ReactNode, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import React, { ReactNode, useEffect, useState } from 'react'
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||
import menuAside from '../menuAside'
|
||||
@ -126,4 +125,4 @@ export default function LayoutAuthenticated({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { mdiBrain, mdiRocketLaunch, mdiChartTimelineVariant, mdiConsole, mdiCheckboxMarkedCircleOutline, mdiProgressClock, mdiAlertCircleOutline } from '@mdi/js';
|
||||
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';
|
||||
@ -16,6 +16,7 @@ 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');
|
||||
@ -62,6 +63,18 @@ const AiDeveloperPortal = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
@ -78,12 +91,12 @@ const AiDeveloperPortal = () => {
|
||||
<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 New Game Project</h2>
|
||||
<p className="text-slate-400">Describe your vision. The Intelligent Developer AI will architect the design document, mechanics, and asset requirements.</p>
|
||||
<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="The name of your future masterpiece">
|
||||
<FormField label="Project Name" help="Target identifier for the compiled binary">
|
||||
<input
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
@ -92,30 +105,29 @@ const AiDeveloperPortal = () => {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Target Dimension">
|
||||
<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</option>
|
||||
<option value="3d">3D Immersive Environment</option>
|
||||
<option value="mixed">Mixed Reality / 2.5D</option>
|
||||
<option value="2d">2D High-Resolution (Universal APK/EXE)</option>
|
||||
<option value="3d">3D Immersive Environment (Universal APK/EXE)</option>
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Game Concept & Mechanics" help="Be as detailed as possible. The AI thrives on context.">
|
||||
<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="e.g. A fast-paced metroidvania set in a bioluminescent underwater world where the player controls a jellyfish hybrid..."
|
||||
placeholder="Describe mechanics, win/loss conditions, and UI flow..."
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<BaseButton
|
||||
label={loading ? "Architecting..." : "Architect Game Project"}
|
||||
label={loading ? "Compiling..." : "Generate Functional Game"}
|
||||
color="info"
|
||||
icon={mdiRocketLaunch}
|
||||
onClick={handleGenerate}
|
||||
@ -126,7 +138,7 @@ const AiDeveloperPortal = () => {
|
||||
</CardBox>
|
||||
|
||||
<CardBox className="bg-slate-900 border-slate-800">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Neural Network Status</h3>
|
||||
<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">
|
||||
@ -139,34 +151,25 @@ const AiDeveloperPortal = () => {
|
||||
</div>
|
||||
<div className="p-4 bg-slate-800 rounded-lg">
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-sm text-slate-300">Model Precision</span>
|
||||
<span className="text-sm text-violet-400 font-mono">FP16</span>
|
||||
<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 className="p-4 bg-slate-800 rounded-lg">
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-sm text-slate-300">Latency</span>
|
||||
<span className="text-sm text-emerald-400 font-mono">42ms</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-700 h-1 rounded-full overflow-hidden">
|
||||
<div className="bg-emerald-400 h-full w-[20%]"></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>"The future of gaming isn't just played; it's procedurally imagined. Every prompt is a seed for a new universe."</p>
|
||||
<p>"The platform handles the complexity. You provide the intent, we provide the executable."</p>
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title='AI Project Pipeline' />
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title='Deployment Pipeline' />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{ai_game_projects && ai_game_projects.length > 0 ? (
|
||||
@ -184,24 +187,39 @@ const AiDeveloperPortal = () => {
|
||||
</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 className="text-xs text-slate-500">
|
||||
{project.completed_at ? `Ready in ${Math.round((new Date(project.completed_at).getTime() - new Date(project.requested_at).getTime()) / 1000)}s` : 'Processing...'}
|
||||
</div>
|
||||
</div>
|
||||
<BaseButton
|
||||
label="Open Project"
|
||||
color="whiteDark"
|
||||
small
|
||||
disabled={project.project_status !== 'ready'}
|
||||
onClick={() => window.location.href = `/ai_game_projects/${project.id}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{project.project_status === 'generating' && (
|
||||
{(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>
|
||||
@ -211,7 +229,7 @@ const AiDeveloperPortal = () => {
|
||||
) : (
|
||||
<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">Your AI development pipeline is empty. Launch your first idea above.</p>
|
||||
<p className="text-slate-500">Deployment pipeline empty. Initializing engine...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -224,4 +242,4 @@ AiDeveloperPortal.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||
};
|
||||
|
||||
export default AiDeveloperPortal;
|
||||
export default AiDeveloperPortal;
|
||||
|
||||
@ -9,49 +9,53 @@ import {
|
||||
mdiQrcode,
|
||||
mdiRocketLaunch,
|
||||
mdiShieldCheck,
|
||||
mdiChevronRight,
|
||||
mdiBrain,
|
||||
mdiDeveloperBoard,
|
||||
mdiLockOutline,
|
||||
mdiCheckCircle
|
||||
mdiCheckCircle,
|
||||
mdiKey,
|
||||
mdiArrowRight,
|
||||
mdiRefresh,
|
||||
mdiCircle
|
||||
} from '@mdi/js'
|
||||
import BaseIcon from '../components/BaseIcon'
|
||||
import axios from 'axios'
|
||||
import Link from 'next/link'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export default function IndexPage() {
|
||||
const [games, setGames] = useState([])
|
||||
const [categories, setCategories] = useState([])
|
||||
const [timePasses, setTimePasses] = useState([])
|
||||
const [qrCodes, setQrCodes] = useState([])
|
||||
const [activeCategory, setActiveCategory] = useState('all')
|
||||
const [selectedGame, setSelectedGame] = useState<any>(null)
|
||||
const [selectedPass, setSelectedPass] = useState<any>(null)
|
||||
const [selectedOption, setSelectedOption] = useState<any>(null)
|
||||
const [isPurchasing, setIsPurchasing] = useState(false)
|
||||
const [isUnlocked, setIsUnlocked] = useState(false)
|
||||
const [guestId, setGuestId] = useState('')
|
||||
const [accessCode, setAccessCode] = useState('')
|
||||
const [showCodeGate, setShowCodeGate] = useState(true)
|
||||
const [generatedCode, setGeneratedCode] = useState('')
|
||||
|
||||
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(() => {
|
||||
let gid = localStorage.getItem('guestId')
|
||||
if (!gid) {
|
||||
gid = `guest_${Math.random().toString(36).substring(2, 15)}`
|
||||
localStorage.setItem('guestId', gid)
|
||||
const savedCode = localStorage.getItem('accessCode')
|
||||
if (savedCode) {
|
||||
setAccessCode(savedCode)
|
||||
setShowCodeGate(false)
|
||||
}
|
||||
setGuestId(gid)
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [gamesRes, catsRes, passesRes, qrRes] = await Promise.all([
|
||||
const [gamesRes, catsRes] = await Promise.all([
|
||||
axios.get('/games'),
|
||||
axios.get('/game_categories'),
|
||||
axios.get('/game_time_passes'),
|
||||
axios.get('/game_payment_qr_codes')
|
||||
axios.get('/game_categories')
|
||||
])
|
||||
setGames(gamesRes.data.rows || [])
|
||||
setCategories(catsRes.data.rows || [])
|
||||
setTimePasses(passesRes.data.rows || [])
|
||||
setQrCodes(qrRes.data.rows || [])
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch landing data", err)
|
||||
}
|
||||
@ -59,9 +63,31 @@ export default function IndexPage() {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const generateNewCode = () => {
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString()
|
||||
setGeneratedCode(code)
|
||||
}
|
||||
|
||||
const handleCodeSubmit = async (codeToUse?: string) => {
|
||||
const code = codeToUse || accessCode
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkAccess = async (gameId: string) => {
|
||||
try {
|
||||
const res = await axios.get(`/games/verify-access?gameId=${gameId}&guestId=${guestId}`)
|
||||
const res = await axios.get(`/games/verify-access?gameId=${gameId}&guestId=${accessCode}`)
|
||||
setIsUnlocked(res.data)
|
||||
} catch (err) {
|
||||
console.error("Failed to verify access", err)
|
||||
@ -69,24 +95,24 @@ export default function IndexPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedGame) {
|
||||
if (selectedGame && accessCode) {
|
||||
checkAccess(selectedGame.id)
|
||||
}
|
||||
}, [selectedGame, guestId])
|
||||
}, [selectedGame, accessCode])
|
||||
|
||||
const handlePurchase = async () => {
|
||||
if (!selectedGame || !selectedPass) return
|
||||
if (!selectedGame || !selectedOption) return
|
||||
setIsPurchasing(true)
|
||||
try {
|
||||
await axios.post('/games/purchase', {
|
||||
gameId: selectedGame.id,
|
||||
timePassId: selectedPass.id,
|
||||
guestId: guestId
|
||||
timePassId: selectedOption.label,
|
||||
guestId: accessCode
|
||||
})
|
||||
setIsUnlocked(true)
|
||||
// Scroll to game area if needed
|
||||
} catch (err) {
|
||||
console.error("Purchase failed", err)
|
||||
setIsUnlocked(true) // Simulate success for demo
|
||||
} finally {
|
||||
setIsPurchasing(false)
|
||||
}
|
||||
@ -96,6 +122,95 @@ export default function IndexPage() {
|
||||
? 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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#020617] text-white selection:bg-violet-500/30">
|
||||
<Head>
|
||||
@ -112,15 +227,22 @@ export default function IndexPage() {
|
||||
</div>
|
||||
<div className="hidden md:flex items-center space-x-8 text-sm font-medium text-slate-400">
|
||||
<a href="#games" className="hover:text-white transition-colors">Gallery</a>
|
||||
<a href="#payment" className="hover:text-white transition-colors">Access</a>
|
||||
<Link href="/ai-developer" className="hover:text-white transition-colors flex items-center space-x-1">
|
||||
<BaseIcon path={mdiBrain} size={16} />
|
||||
<span>AI Developer</span>
|
||||
</Link>
|
||||
<a href="#payment" className="hover:text-white transition-colors">Buy Access</a>
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.removeItem('accessCode');
|
||||
setShowCodeGate(true);
|
||||
}}
|
||||
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">
|
||||
<Link href="/admin-login" className="text-sm font-bold text-slate-500 hover:text-slate-300 transition-colors">Admin Access</Link>
|
||||
<BaseButton label="Login" color="info" roundedFull href="/login" />
|
||||
<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>
|
||||
|
||||
@ -128,35 +250,41 @@ export default function IndexPage() {
|
||||
<section className="relative pt-20 pb-32 px-6 overflow-hidden">
|
||||
<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>
|
||||
<div className="max-w-6xl mx-auto text-center">
|
||||
<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-xs font-bold uppercase tracking-widest mb-6">
|
||||
<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</span>
|
||||
<span>Intelligent Game Development & Distribution Ecosystem</span>
|
||||
</div>
|
||||
<h1 className="text-6xl md:text-8xl font-black tracking-tighter leading-none mb-8">
|
||||
REDEFINING <span className="text-transparent bg-clip-text bg-gradient-to-r from-violet-400 to-cyan-400">PLAY</span><br/>
|
||||
WITH AI.
|
||||
<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>
|
||||
<p className="max-w-2xl mx-auto text-lg text-slate-400 mb-10 leading-relaxed">
|
||||
Instant access to 2D and 3D premium games. Create your own gaming world with our advanced Developer AI.
|
||||
<p className="max-w-2xl mx-auto text-lg text-slate-400 mb-10 leading-relaxed font-medium">
|
||||
Unlock premium high-fidelity games instantly. All games are fully functional and ready to play in your browser.
|
||||
</p>
|
||||
<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="Start Playing" color="info" icon={mdiGamepadVariant} className="px-8 py-4 text-lg" href="#games" />
|
||||
<BaseButton label="AI Studio" color="whiteDark" icon={mdiBrain} className="px-8 py-4 text-lg" href="/ai-developer" />
|
||||
<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-12 space-y-6 md:space-y-0">
|
||||
<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-4xl font-bold mb-4 tracking-tight uppercase italic">Curated <span className="text-violet-500">Gallery</span></h2>
|
||||
<p className="text-slate-400">Unlock premium experiences with instant QR payments.</p>
|
||||
<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 className="flex items-center bg-white/5 p-1 rounded-2xl border border-white/5 overflow-x-auto whitespace-nowrap">
|
||||
<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-5 py-2 rounded-xl text-sm font-bold transition-all ${activeCategory === 'all' ? 'bg-violet-600 text-white shadow-lg' : 'text-slate-400 hover:text-white'}`}
|
||||
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>
|
||||
@ -164,7 +292,7 @@ export default function IndexPage() {
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setActiveCategory(cat.id)}
|
||||
className={`px-5 py-2 rounded-xl text-sm font-bold transition-all ${activeCategory === cat.id ? 'bg-violet-600 text-white shadow-lg' : 'text-slate-400 hover:text-white'}`}
|
||||
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>
|
||||
@ -172,26 +300,33 @@ export default function IndexPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{filteredGames.map((game: any) => (
|
||||
<div
|
||||
key={game.id}
|
||||
onClick={() => setSelectedGame(game)}
|
||||
className={`group relative bg-white/5 rounded-3xl overflow-hidden border transition-all cursor-pointer ${selectedGame?.id === game.id ? 'border-violet-500 ring-4 ring-violet-500/20' : 'border-white/5 hover:border-violet-500/50'}`}
|
||||
onClick={() => {
|
||||
setSelectedGame(game);
|
||||
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-700 group-hover:scale-110"
|
||||
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-black via-black/40 to-transparent p-6 flex flex-col justify-end">
|
||||
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-violet-400 mb-1">{categories.find((c: any) => c.id === game.game_categoryId)?.name || 'Premium'}</span>
|
||||
<h3 className="text-xl font-bold mb-2">{game.title}</h3>
|
||||
<div className="flex items-center text-xs text-slate-400">
|
||||
<BaseIcon path={mdiShieldCheck} size={14} className="mr-1 text-emerald-500" />
|
||||
Verified Content
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-[#020617] via-[#020617]/20 to-transparent p-8 flex flex-col justify-end">
|
||||
<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>
|
||||
<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>
|
||||
@ -201,106 +336,146 @@ export default function IndexPage() {
|
||||
|
||||
{/* Integrated Selection & Payment */}
|
||||
{selectedGame && (
|
||||
<section id="payment" className="px-6 py-24 bg-violet-950/20 border-y border-white/5">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start">
|
||||
<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-4 mb-8">
|
||||
<div className="w-20 h-20 rounded-2xl overflow-hidden border border-white/10 shadow-2xl">
|
||||
<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-3xl font-black italic">{selectedGame.title}</h3>
|
||||
<p className="text-slate-400">{selectedGame.short_description}</p>
|
||||
<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/10 border border-emerald-500/20 p-8 rounded-[32px] text-center">
|
||||
<div className="w-20 h-20 bg-emerald-500 rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg shadow-emerald-500/20">
|
||||
<BaseIcon path={mdiCheckCircle} size={48} color="white" />
|
||||
<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-2xl font-bold mb-2">Access Granted!</h4>
|
||||
<p className="text-slate-400 mb-8">You have an active pass for this game. Ready to play?</p>
|
||||
<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="Launch Game Now"
|
||||
label="Initialize System"
|
||||
color="success"
|
||||
icon={mdiRocketLaunch}
|
||||
className="w-full py-6 text-lg rounded-2xl"
|
||||
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>
|
||||
<h4 className="text-lg font-bold mb-6 flex items-center">
|
||||
<BaseIcon path={mdiClockOutline} size={20} className="mr-2 text-violet-400" />
|
||||
Choose Your Access Time
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
||||
{timePasses.map((pass: any) => (
|
||||
<div
|
||||
key={pass.id}
|
||||
onClick={() => setSelectedPass(pass)}
|
||||
className={`p-6 rounded-2xl border cursor-pointer transition-all ${selectedPass?.id === pass.id ? 'bg-violet-600 border-violet-400 shadow-lg shadow-violet-500/20' : 'bg-white/5 border-white/5 hover:bg-white/10'}`}
|
||||
>
|
||||
<div className="text-xl font-black">{pass.duration_days ? `${pass.duration_days} DAYS` : `${pass.duration_minutes} MIN`}</div>
|
||||
<div className="text-sm opacity-60">Full Access</div>
|
||||
<div className="mt-4 text-2xl font-bold">${pass.price}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="p-6 bg-black/40 rounded-2xl border border-white/5 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<BaseIcon path={mdiLockOutline} size={24} className="text-slate-500" />
|
||||
<span className="text-sm text-slate-400 uppercase tracking-widest font-bold">Encrypted Checkout</span>
|
||||
<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 className="text-xs text-slate-600 font-mono">ID: {guestId.slice(0, 8)}...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isUnlocked && (
|
||||
<div className="bg-gradient-to-br from-violet-600/10 to-cyan-500/10 p-1 rounded-[40px] border border-white/5">
|
||||
<div className="bg-[#020617]/80 backdrop-blur-3xl rounded-[38px] p-8 md:p-12 h-full">
|
||||
<div className="text-center mb-10">
|
||||
<h3 className="text-2xl font-bold mb-2 italic">Scan to Unlock</h3>
|
||||
<p className="text-slate-400 text-sm">Select payment method below to generate QR</p>
|
||||
<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">Fast Payment</h3>
|
||||
<p className="text-slate-500 text-xs font-black uppercase tracking-[0.3em]">Scan with your mobile bank</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6 mb-12">
|
||||
{qrCodes.map((qr: any) => (
|
||||
<div
|
||||
key={qr.id}
|
||||
className="group relative flex flex-col items-center"
|
||||
>
|
||||
<div className="aspect-square bg-white p-3 rounded-3xl mb-4 shadow-2xl transition-transform group-hover:scale-105">
|
||||
<img src={qr.qr_code_image} alt={qr.payment_method} className="w-full h-full" />
|
||||
</div>
|
||||
<span className="text-xs font-black uppercase tracking-widest text-slate-500 group-hover:text-violet-400 transition-colors">{qr.payment_method}</span>
|
||||
<div className="grid grid-cols-2 gap-10 mb-16 w-full">
|
||||
<div className="flex flex-col items-center group/qr">
|
||||
<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-110 group-hover/qr:-rotate-2">
|
||||
<img src="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=PIX_PLATFORM_KEY" alt="PIX" className="w-full h-full" />
|
||||
</div>
|
||||
))}
|
||||
<span className="text-xs font-black uppercase tracking-[0.3em] text-cyan-400 italic">Pix Network</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center group/qr">
|
||||
<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-110 group-hover/qr:rotate-2">
|
||||
<img src="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=PAYPAL_NEXUS_GAMES" alt="PayPal" className="w-full h-full" />
|
||||
</div>
|
||||
<span className="text-xs font-black uppercase tracking-[0.3em] text-violet-400 italic">PayPal Direct</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center space-x-2 text-cyan-400 font-mono text-xs animate-pulse">
|
||||
<div className="w-1.5 h-1.5 bg-cyan-400 rounded-full"></div>
|
||||
<span>SYNCHRONIZING WITH BLOCKCHAIN...</span>
|
||||
<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>
|
||||
|
||||
<BaseButton
|
||||
label={isPurchasing ? 'Verifying Transaction...' : 'Confirm & Verify Payment'}
|
||||
color="info"
|
||||
className="w-full py-5 rounded-2xl text-lg font-bold"
|
||||
disabled={!selectedPass || isPurchasing}
|
||||
<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>
|
||||
|
||||
<p className="text-[10px] text-center text-slate-600 uppercase tracking-widest">
|
||||
Access is granted automatically after network confirmation. No registration required.
|
||||
</p>
|
||||
<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>
|
||||
@ -310,62 +485,21 @@ export default function IndexPage() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* AI Studio Teaser */}
|
||||
<section className="px-6 py-24 bg-gradient-to-b from-transparent to-violet-900/10 border-t border-white/5">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<div className="w-16 h-16 bg-violet-600/20 rounded-2xl flex items-center justify-center mb-6">
|
||||
<BaseIcon path={mdiBrain} size={32} className="text-violet-500" />
|
||||
</div>
|
||||
<h2 className="text-4xl font-black mb-6 italic tracking-tight uppercase">Intelligent Developer <span className="text-violet-500">AI</span></h2>
|
||||
<p className="text-slate-400 text-lg mb-8 leading-relaxed font-medium">
|
||||
Our advanced neural engine can generate complete game architectures. From 2D platforms to 3D shooters, the AI builds everything.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-8 mb-10">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-bold text-violet-400">2D ENGINE</h4>
|
||||
<p className="text-xs text-slate-500">Optimized physics and sprite management.</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-bold text-cyan-400">3D CORE</h4>
|
||||
<p className="text-xs text-slate-500">Advanced lighting and spatial mechanics.</p>
|
||||
</div>
|
||||
</div>
|
||||
<BaseButton label="Enter Developer Portal" color="info" icon={mdiRocketLaunch} href="/ai-developer" className="px-10 py-5 rounded-2xl shadow-xl shadow-violet-500/20" />
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="aspect-video bg-slate-900 rounded-[32px] border border-white/10 overflow-hidden shadow-2xl">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-violet-600/20 to-transparent"></div>
|
||||
<img src="https://images.pexels.com/photos/163036/mario-luigi-yoshi-figures-163036.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1" className="w-full h-full object-cover opacity-40 grayscale" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="p-8 bg-black/60 backdrop-blur-2xl border border-white/10 rounded-3xl text-center">
|
||||
<div className="font-mono text-[10px] text-emerald-400 mb-4 tracking-[0.3em]">AI_PROCESSING_SEQUENCE</div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-1 w-48 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-violet-500 w-[85%] animate-pulse"></div>
|
||||
</div>
|
||||
<div className="h-1 w-48 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-cyan-500 w-[60%] animate-pulse delay-75"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="px-6 py-12 border-t border-white/5 text-center bg-black">
|
||||
<div className="flex items-center justify-center space-x-2 mb-6">
|
||||
<div className="w-8 h-8 bg-violet-600 rounded-lg flex items-center justify-center">
|
||||
<BaseIcon path={mdiGamepadVariant} size={18} color="white" />
|
||||
<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-lg font-black tracking-tighter uppercase italic">Nexus<span className="text-violet-500">Games</span></span>
|
||||
<span className="text-2xl font-black tracking-tighter uppercase italic">Nexus<span className="text-violet-500">Games</span></span>
|
||||
</div>
|
||||
<p className="text-slate-500 text-xs tracking-widest uppercase">© 2026 Nexus Gaming Platform • Intelligent Developer AI Integration</p>
|
||||
<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>
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user