Void-Genesis: JWT-Handling, Szenenwechsel & Lobby-UI
@@ -146,7 +146,8 @@ export const login = async (req, res) => {
|
|||||||
}
|
}
|
||||||
if (error.code === 'ERR_EMAIL_NOT_VERIFIED') {
|
if (error.code === 'ERR_EMAIL_NOT_VERIFIED') {
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
error: 'ERR_EMAIL_NOT_VERIFIED'
|
error: 'ERR_EMAIL_NOT_VERIFIED',
|
||||||
|
email: error.email
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +180,8 @@ export const refresh = async (req, res) => {
|
|||||||
// Das neue Access-Token an das Frontend zurück geben
|
// Das neue Access-Token an das Frontend zurück geben
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
accessToken: result.accessToken
|
accessToken: result.accessToken,
|
||||||
|
user: result.user
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -198,4 +200,119 @@ export const refresh = async (req, res) => {
|
|||||||
error: 'ERR_INTERNAL_SERVER'
|
error: 'ERR_INTERNAL_SERVER'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller-Methode für die Resend-OTP-Route
|
||||||
|
* Wird aufgerufen, wenn der Nutzer im Frontend auf "Code erneut senden" klickt
|
||||||
|
*/
|
||||||
|
export const resendOTP = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email } = req.body;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'ERR_MISSING_FIELDS'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aufruf unseres Service
|
||||||
|
await authService.resendOTP(email);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
// MSG_OTP_RESENT: Ein neuer Code wurde an deine E-Mail gesendet.
|
||||||
|
message: 'MSG_OTP_RESENT'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (['ERR_USER_NOT_FOUND', 'ERR_ALREADY_VERIFIED', 'ERR_COOLDOWN_ACTIVE'].includes(error.code)) {
|
||||||
|
const statusCode = error.code === 'ERR_COOLDOWN_ACTIVE' ? 429 : 400;
|
||||||
|
return res.status(error.code).json({
|
||||||
|
error: error.code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[AuthController] Fehler beim erneuten Senden des OTP:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'ERR_INTERNAL_SERVER'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller-Methode für die Passwort vergessen anfrage
|
||||||
|
*/
|
||||||
|
export const requestPasswordReset = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email } = req.body;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'ERR_MISSING_FIELDS'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auch wenn die E-Mail nicht existiert, geben wir oft ein Success zurück,
|
||||||
|
// damit Angreifer nicht testen können, welche E-Mails registriert sind.
|
||||||
|
// In unserem Service werfen wir aber ERR_USER_NOT_FOUND. Wir fangen das unten ab.
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'MSG_RESET_EMAIL_SENT' // Muss später ins i18n
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Wenn der Nutzer nicht existiert, tun wir für das Frontend trotzdem so, als wäre
|
||||||
|
// die E-Mail rausgegangen (Sicherheitsbest-Practice).
|
||||||
|
if (error.code === 'ERR_USER_NOT_FOUND') {
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'MSG_RESET_EMAIL_SENT'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 'ERR_COOLDOWN_ACTIVE') {
|
||||||
|
return res.status(429).json({
|
||||||
|
error: error.code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[AuthController] Fehler bei requestPasswordReset:', error);
|
||||||
|
return res.status(500).json({ error: 'ERR_INTERNAL_SERVER' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller-Methode für das finale Setzen des neuen Passworts.
|
||||||
|
*/
|
||||||
|
export const resetPassword = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email, otpCode, newPassword } = req.body;
|
||||||
|
|
||||||
|
// Basis-Validierung
|
||||||
|
if (!email || !otpCode || !newPassword) {
|
||||||
|
return res.status(400).json({ error: 'ERR_MISSING_FIELDS' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort-Richtlinie prüfen (wie bei der Registrierung)
|
||||||
|
if (newPassword.length < 8) {
|
||||||
|
return res.status(400).json({ error: 'ERR_PASSWORD_TOO_SHORT' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service aufrufen
|
||||||
|
await authService.resetPassword(email, otpCode, newPassword);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'MSG_PASSWORD_RESET_SUCCESS' // Muss später ins i18n
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (['ERR_USER_NOT_FOUND', 'ERR_INVALID_OTP', 'ERR_OTP_EXPIRED'].includes(error.code)) {
|
||||||
|
return res.status(400).json({ error: error.code });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[AuthController] Fehler bei resetPassword:', error);
|
||||||
|
return res.status(500).json({ error: 'ERR_INTERNAL_SERVER' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -7,5 +7,8 @@ router.post('/register', authController.register);
|
|||||||
router.post('/verify-email', authController.verifyEmail);
|
router.post('/verify-email', authController.verifyEmail);
|
||||||
router.post('/login', authController.login);
|
router.post('/login', authController.login);
|
||||||
router.post('/refresh', authController.refresh);
|
router.post('/refresh', authController.refresh);
|
||||||
|
router.post('/resend-otp', authController.resendOTP);
|
||||||
|
router.post('/forgot-password', authController.requestPasswordReset);
|
||||||
|
router.post('/reset-password', authController.resetPassword);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -202,7 +202,7 @@ export const loginUser = async (identifier, password) => {
|
|||||||
try {
|
try {
|
||||||
// Nutzer suchen
|
// Nutzer suchen
|
||||||
const accountResult = await client.query(`
|
const accountResult = await client.query(`
|
||||||
SELECT id, username, email, password_hash, is_email_verified, is_admin
|
SELECT id, username, email, password_hash, is_email_verified, is_admin, res_premium
|
||||||
FROM lobby_accounts
|
FROM lobby_accounts
|
||||||
WHERE LOWER(username) = LOWER($1) OR email = LOWER($1)`,
|
WHERE LOWER(username) = LOWER($1) OR email = LOWER($1)`,
|
||||||
[identifier]
|
[identifier]
|
||||||
@@ -223,6 +223,7 @@ export const loginUser = async (identifier, password) => {
|
|||||||
// ERR_EMAIL_NOT_VERIFIED: Bitte bestätige zuerst deine E-Mail-Addresse
|
// ERR_EMAIL_NOT_VERIFIED: Bitte bestätige zuerst deine E-Mail-Addresse
|
||||||
const error = new Error('Email not verified');
|
const error = new Error('Email not verified');
|
||||||
error.code = 'ERR_EMAIL_NOT_VERIFIED';
|
error.code = 'ERR_EMAIL_NOT_VERIFIED';
|
||||||
|
error.email = account.email;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +268,9 @@ export const loginUser = async (identifier, password) => {
|
|||||||
refreshToken,
|
refreshToken,
|
||||||
user: {
|
user: {
|
||||||
id: account.id,
|
id: account.id,
|
||||||
username: account.username
|
username: account.username,
|
||||||
|
email: account.email,
|
||||||
|
res_premium: account.res_premium
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
@@ -308,7 +311,7 @@ export const refreshAccessToken = async (refreshToken) => {
|
|||||||
// Das behebt das "Stale Data" Problem. Falls der Admin Status geändert wurde,
|
// Das behebt das "Stale Data" Problem. Falls der Admin Status geändert wurde,
|
||||||
// steht er im neuen Token sofort korrekt drin, ohne die DB bei jedem API.Call zu belasten
|
// steht er im neuen Token sofort korrekt drin, ohne die DB bei jedem API.Call zu belasten
|
||||||
const accountResult = await client.query(
|
const accountResult = await client.query(
|
||||||
'SELECT id, username, is_admin FROM lobby_accounts WHERE id = $1',
|
'SELECT id, username, email, is_admin, res_premium FROM lobby_accounts WHERE id = $1',
|
||||||
[accountId]
|
[accountId]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -333,7 +336,15 @@ export const refreshAccessToken = async (refreshToken) => {
|
|||||||
{ expiresIn: '15m' }
|
{ expiresIn: '15m' }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { accessToken };
|
return {
|
||||||
|
accessToken,
|
||||||
|
user: {
|
||||||
|
id: account.id,
|
||||||
|
username: account.username,
|
||||||
|
email: account.email,
|
||||||
|
res_premium: res_premium
|
||||||
|
}
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Abfangen von JWT-spezifischen Fehlern (z.B. Token abgelaufen oder Signatur falsch)
|
// Abfangen von JWT-spezifischen Fehlern (z.B. Token abgelaufen oder Signatur falsch)
|
||||||
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError' || error.name === 'SyntaxError') {
|
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError' || error.name === 'SyntaxError') {
|
||||||
@@ -347,4 +358,249 @@ export const refreshAccessToken = async (refreshToken) => {
|
|||||||
} finally {
|
} finally {
|
||||||
client.release();
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet einen neuen OTP-Code an den Nutzer, falls der alte abgelaufen ist oder nicht ankam.
|
||||||
|
* Vorherige, ungenutzte Codes werdeb zur Sicherheit invalidiert (gelöscht)
|
||||||
|
*
|
||||||
|
* @param {string} email - Die E-Mail Addresse des Nutzers
|
||||||
|
* @returns {boolean} - true, wenn die E-Mail erfolgreich versendet wurde.
|
||||||
|
*/
|
||||||
|
export const resendOTP = async (email) => {
|
||||||
|
const client = await pool.connect();
|
||||||
|
const lowerCaseEmail = email.toLowerCase();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
|
// Prüfen, ob der Nutzer überhaupt existiert
|
||||||
|
const accountResult = await client.query(
|
||||||
|
'SELECT id, username, is_email_verified FROM lobby_accounts WHERE email = $1',
|
||||||
|
[lowerCaseEmail]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (accountResult.rows.length === 0) {
|
||||||
|
// Aus Sicherheitsgründen verraten wir dem Frontend nicht zwingend,
|
||||||
|
// dass die E-Mail nicht existieren, aber für unser internes Error-Handling
|
||||||
|
// werfen wir einen spezifischen Fehler.
|
||||||
|
const error = new Error('User nor found');
|
||||||
|
error.code = 'ERR:USER_NOT_FOUND';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = accountResult.rows[0];
|
||||||
|
|
||||||
|
// Prüfen, ob der Account nicht vielleicht schon verifiziert ist
|
||||||
|
// Wenn ja, macht ein neuer Code keinen Sinn.
|
||||||
|
if (account.is_email_verified) {
|
||||||
|
const error = new Error('Already verified');
|
||||||
|
error.code = 'ERR_ALREADY_VERIFIED';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastOtpResult = await client.query(`
|
||||||
|
SELECT created_at FROM auth_otps WHERE account_id = $1 AND type = 'register' ORDER BY created_at DESC LIMIT 1`,
|
||||||
|
[account.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lastOtpResult.rows.length > 0) {
|
||||||
|
const lastCreatedTime = new Date(lastOtpResult.rows[0].created_at).getTime();
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const timeDifference = currentTime - lastCreatedTime;
|
||||||
|
|
||||||
|
if (timeDifference < 120000) {
|
||||||
|
const error = new Error('Cooldown active');
|
||||||
|
error.code = 'ERR_COOLDOWN_ACTIVE';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zur Sichherheit alle alten, noch aktiven Registierungs-OTPs dieses Nutzers löschen.
|
||||||
|
// So verhindern wir, dass alte Codes in Postfächern noch funktionieren und minimieren
|
||||||
|
// Spam-Risiken.
|
||||||
|
await client.query(
|
||||||
|
`DELETE FROM auth_otps WHERE account_id = $1 AND type = 'register'`,
|
||||||
|
[account.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Einen frischen, 6-stelligen Cide generieren
|
||||||
|
const newOtpCode = generateOTP();
|
||||||
|
|
||||||
|
// Den neuen Code in die Datenbank eintragen
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
|
||||||
|
VALUES ($1, $2, 'register', NOW() + INTERVAL '15 minutes')`,
|
||||||
|
[account.id, newOtpCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. Die neue E-Mail zusammenbauen und versenden
|
||||||
|
const mailHtml = `
|
||||||
|
<h2>Hallo ${account.username}, hier ist dein neuer Code!</h2>
|
||||||
|
<p>Du hast einen neuen Verifizierungscode für Void-Genesis angefordert:</p>
|
||||||
|
<h1 style="color: #00f0ff; letter-spacing: 5px;">${newOtpCode}</h1>
|
||||||
|
<p>Dieser Code ist ab jetzt wieder 15 Minuten lang gültig.</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
await sendEmail(lowerCaseEmail, account.username, 'Void-Genesis - Dein neue Code', mailHtml);
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet den Prozess zum Zurücksetzten des Passworts.
|
||||||
|
* Generiert einen speziellen OTP-Code und sendet ihn per E-Mail.
|
||||||
|
*
|
||||||
|
* @param {string} email - Die Email-Adresse des Nutzers
|
||||||
|
* @returns {boolean} - true, wenn die E-Mail erfolgreich versendet wurde.
|
||||||
|
*/
|
||||||
|
export const requestPasswordReset = async (email) => {
|
||||||
|
const client = await pool.connect();
|
||||||
|
const lowerCaseEmail = email.toLowerCase();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
|
const accountResult = await client.query(
|
||||||
|
'SELECT id, username FROM lobby_accounts WHERE email = $1',
|
||||||
|
[lowerCaseEmail]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (accountResult.rows.length === 0) {
|
||||||
|
const error = new Error('User not found');
|
||||||
|
error.code = 'ERR_USER_NOT_FOUND';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastOtpResult = await client.query(`
|
||||||
|
SELECT created_at FROM auth_otps WHERE account_id = $1 AND type = 'password_reset' ORDER BY created_at DESC LIMIT 1`,
|
||||||
|
[account.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lastOtpResult.rows.length > 0) {
|
||||||
|
const lastCreatedTime = new Date(lastOtpResult.rows[0].created_at).getTime();
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const timeDifference = currentTime - lastCreatedTime;
|
||||||
|
|
||||||
|
if (timeDifference < 120000) {
|
||||||
|
const error = new Error('Cooldown active');
|
||||||
|
error.code = 'ERR_COOLDOWN_ACTIVE';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = accountResult.rows[0];
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`DELETE FROM auth_otps WHERE account_id = $1 AND type = 'password_reset'`,
|
||||||
|
[account.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetCode = generateOTP();
|
||||||
|
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
|
||||||
|
VALUES ($1, $2m 'password_reset' NOW() + INTERVAL '15 minutes')`,
|
||||||
|
[account.id, resetCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
const mailHtml = `
|
||||||
|
<h2>Hallo ${account.username},</h2>
|
||||||
|
<p>Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.</p>
|
||||||
|
<p>Dein Code lautet:</p>
|
||||||
|
<h1 style="color: #00f0ff; letter-spacing: 5px;">${resetCode}</h1>
|
||||||
|
<p>Dieser Code ist 15 Minuten lang gültig. Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
await sendEmail(lowerCaseEmail, account.username, 'Void-Genesis - Passwort zurücksetzen', mailHtml);
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt das neue Passwort, soffern der OTP-Code korrekt und nicht abgelaufen ist
|
||||||
|
*
|
||||||
|
* @param {string} email - Die E-Mail Addresse des Nutzers
|
||||||
|
* @param {string} otpCode - Der 6-stellige Code aus der E-Mail
|
||||||
|
* @param {string} newPassword - das neue Klartext passwort
|
||||||
|
* @returns {boolean} - true wenn das Passwort erfolgreich geändert wurde
|
||||||
|
*/
|
||||||
|
export const resetPassword = async (email, otpCode, newPassword) => {
|
||||||
|
const client = await pool.connect();
|
||||||
|
const lowerCaseEmail = email.toLowerCase();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
|
const accountResult = await client.query(
|
||||||
|
'SELECT id FROM lobby_accounts WHERE email = $1'
|
||||||
|
[lowerCaseEmail]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (accountResult.rows.length === 0) {
|
||||||
|
const error = new Error('User not found');
|
||||||
|
error.code = 'ERR_USER_NOT_FOUND';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = accountResult.rows[0];
|
||||||
|
|
||||||
|
const otpResult = await client.query(`
|
||||||
|
SELECT id, expires_at FROM auth_otps
|
||||||
|
WHERE account_id = $1, otp_code = $2, AND type = 'password_reset'`,
|
||||||
|
[account.id, otpCo0de]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (otpResult.rows.length === 0) {
|
||||||
|
const error = new Error('Invalid OTP');
|
||||||
|
error.code = 'ERR_INVALID_OTP';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const otpRecord = otpResult.rows[0];
|
||||||
|
|
||||||
|
if (new Date() > new Date(otpRecord.expires_at)) {
|
||||||
|
const error = new Error('OTP expired');
|
||||||
|
error.code = 'ERR_OTP_EXPIRED';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordHash = await argon2.hash(newPassword);
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
'UPDATE lobby_accounts SET password_hash = $1 WHERE id = $2',
|
||||||
|
[passwordHash, account.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
'DELETE FROM refresh_tokens WHERE account_id = $1',
|
||||||
|
[account.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -17,45 +17,110 @@
|
|||||||
<form id="register-form" autocomplete="off">
|
<form id="register-form" autocomplete="off">
|
||||||
|
|
||||||
<div id="tab-container">
|
<div id="tab-container">
|
||||||
<button type="button" class="tab-btn active" id="tab-login">Login</button>
|
<button type="button" class="tab-btn active" id="tab-login" data-i18n="tab_login">Login</button>
|
||||||
<button type="button" class="tab-btn" id="tab-register">Registrieren</button>
|
<button type="button" class="tab-btn" id="tab-register" data-i18n="tab_register">Registrieren</button> </div>
|
||||||
|
|
||||||
|
<p id="otp-instruction" data-i18n="txt_otp_instruction">
|
||||||
|
Bitte gib den 6-stelligen Code aus deiner E-Mail ein.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="username">Benutzername</label>
|
||||||
|
<input type="text" id="username" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group" id="group-email">
|
||||||
|
<label for="email">E-Mail</label>
|
||||||
|
<input type="email" id="email" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="password">Passwort</label>
|
||||||
|
<input type="password" id="password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox-group" id="group-remember" style="display: none;">
|
||||||
|
<input type="checkbox" id="remember">
|
||||||
|
<label for="remember" data-i18n="lbl_remember">Angemeldet bleiben</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox-group" id="group-agb">
|
||||||
|
<input type="checkbox" id="agb" required>
|
||||||
|
<label for="agb" data-i18n="lbl_agb">Ich akzeptiere die AGB & Spielregeln</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="msg-box"></div>
|
||||||
|
|
||||||
|
<button type="submit" id="btn-register" data-i18n="btn_register">Jetzt kostenlos registrieren</button>
|
||||||
|
|
||||||
|
<a href="#" id="resend-otp-link" data-i18n="link_resend_otp" style="display: none;">Code erneut senden</a>
|
||||||
|
<a href="#" id="forgot-password-link" data-i18n="link_forgot_password" style="display: none;">Passwort vergessen?</a>
|
||||||
|
<a href="#" id="back-link" data-i18n="link_back" style="display: none;">« Zurück</a>
|
||||||
|
|
||||||
|
<a href="https://discord.gg/ppjXBE4DbP" target="_blank" id="discord-link"></a>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="lobby-ui">
|
||||||
|
<div id="top-bar">
|
||||||
|
|
||||||
|
<div class="top-bar-left"></div>
|
||||||
|
|
||||||
|
<div class="top-bar-right">
|
||||||
|
|
||||||
|
<div class="custom-dropdown" id="user-dropdown" style="display: none;">
|
||||||
|
<div class="dropdown-toggle" id="user-toggle">
|
||||||
|
<span id="lobby-username">Commander</span>
|
||||||
|
<span class="dropdown-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<ul class="dropdown-menu user-menu" id="user-menu">
|
||||||
|
<li class="dropdown-header disabled">
|
||||||
|
<small>E-Mail</small><br>
|
||||||
|
<span id="lobby-email">ladend...</span>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-item disabled">E-Mail ändern</li>
|
||||||
|
<li class="dropdown-item disabled">Benutzername ändern</li>
|
||||||
|
<li class="dropdown-divider"></li>
|
||||||
|
<li class="dropdown-item" id="btn-change-pw">Passwort ändern</li>
|
||||||
|
<li class="dropdown-divider"></li>
|
||||||
|
<li class="dropdown-header disabled">
|
||||||
|
<small>Premium Währung</small><br>
|
||||||
|
<span class="premium-text">Res_Premium: <span id="lobby-premium">-1</span></span>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-item disabled">Res_Premium aufladen</li>
|
||||||
|
<li class="dropdown-divider"></li>
|
||||||
|
<li class="dropdown-item logout-item" id="btn-logout">Logout</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="custom-dropdown" id="lang-dropdown">
|
||||||
|
<div class="dropdown-toggle" id="lang-toggle">
|
||||||
|
<span class="flag-icon" id="current-flag">🇩🇪</span>
|
||||||
|
<span id="current-lang-text">DE</span>
|
||||||
|
<span class="dropdown-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<ul class="dropdown-menu" id="lang-menu">
|
||||||
|
<li class="dropdown-item" data-lang="de">
|
||||||
|
<span class="flag-icon">🇩🇪</span> Deutsch
|
||||||
|
</li>
|
||||||
|
<li class="dropdown-item" data-lang="en">
|
||||||
|
<span class="flag-icon">🇬🇧</span> English
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="input-group">
|
<footer id="game-footer">
|
||||||
<label for="username">Benutzername</label>
|
<div class="footer-left">v 0.0.3.0-dev</div>
|
||||||
<input type="text" id="username" placeholder="Benutzername / E-Mail" required>
|
<div class="footer-center">© 2026 Void-Genesis</div>
|
||||||
</div>
|
<div class="footer-right">
|
||||||
|
<a href="https://void-genesis.de/" target="_blank" data-i18n="link_imprint">Impressum</a> |
|
||||||
<div class="input-group" id="group-email">
|
<a href="https://void-genesis.de/" target="_blank" data-i18n="link_privacy">Datenschutz</a>
|
||||||
<label for="email">E-Mail</label>
|
</div>
|
||||||
<input type="email" id="email" placeholder="Enter E-Mail" required>
|
</footer>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input-group">
|
|
||||||
<label for="password">Passwort</label>
|
|
||||||
<input type="password" id="password" placeholder="Enter Password" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="checkbox-group" id="group-remember" style="display: none;">
|
|
||||||
<input type="checkbox" id="remember">
|
|
||||||
<label for="remember">Angemeldet bleiben</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="msg-box"></div>
|
|
||||||
|
|
||||||
<button type="submit" id="btn-register">REGISTRIEREN</button>
|
|
||||||
|
|
||||||
<a href="https://discord.gg/ppjXBE4DbP" target="_blank" id="discord-link"></a>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<footer id="game-footer">
|
|
||||||
<div class="footer-left">v 0.0.1.0-dev</div>
|
|
||||||
<div class="footer-center">© 2026 Void-Genesis</div>
|
|
||||||
<div class="footer-right">
|
|
||||||
<a href="https://void-genesis.de/" target="_blank">Impressum</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="src/main.js"></script>
|
<script type="module" src="src/main.js"></script>
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* Dieses Objekt enthält alle Textbausteine für das Frontend.
|
||||||
|
* Wenn wir später weitere Sprachen hinzufügen (z.B. 'es' für Spanisch),
|
||||||
|
* müssen wir nur dieses Objekt erweitern, ohne den restlichen Code anzufassen.
|
||||||
|
*/
|
||||||
|
export const translations = {
|
||||||
|
de: {
|
||||||
|
// --- UI Elemente ---
|
||||||
|
tab_login: "Login",
|
||||||
|
tab_register: "Registrieren",
|
||||||
|
btn_login: "Login",
|
||||||
|
btn_register: "Jetzt kostenlos registrieren",
|
||||||
|
link_forgot_password: "Passwort vergessen?",
|
||||||
|
btn_reset_request: "Code anfordern",
|
||||||
|
btn_reset_password: "Passwort ändern",
|
||||||
|
discord_text: "Offizieller Discord Server",
|
||||||
|
link_back: "« Zurück",
|
||||||
|
|
||||||
|
// --- Platzhalter (Placeholders) ---
|
||||||
|
ph_username_login: "Benutzername / E-Mail",
|
||||||
|
ph_username_reg: "Benutzername eingeben",
|
||||||
|
ph_email: "E-Mail eingeben",
|
||||||
|
ph_password: "Passwort eingeben",
|
||||||
|
ph_otp: "6-stelliger Code",
|
||||||
|
|
||||||
|
// --- Labels ---
|
||||||
|
lbl_agb: "Ich akzeptiere die AGB & Spielregeln",
|
||||||
|
lbl_remember: "Angemeldet bleiben",
|
||||||
|
|
||||||
|
// --- OTP Modus ---
|
||||||
|
txt_otp_instruction: "Bitte gib den 6-stelligen Code aus deiner E-Mail ein.",
|
||||||
|
txt_reset_instruction_1: "Um dein Passwort zurückzusetzen, gib bitte deine E-Mail-Adresse ein.",
|
||||||
|
txt_reset_instruction_2: "Bitte gib den Code aus der E-Mail sowie dein neues Passwort ein.",
|
||||||
|
btn_verify: "BESTÄTIGEN",
|
||||||
|
msg_verify_success: "Verifizierung erfolgreich! Du kannst dich nun einloggen.",
|
||||||
|
link_resend_otp: "Code erneut senden",
|
||||||
|
|
||||||
|
// --- Footer ---
|
||||||
|
link_imprint: "Impressum",
|
||||||
|
link_privacy: "Datenschutz",
|
||||||
|
|
||||||
|
// --- Backend Fehlermeldungen (Mapping) ---
|
||||||
|
ERR_MISSING_FIELDS: "Bitte alle Felder ausfüllen.",
|
||||||
|
ERR_PASSWORD_TOO_SHORT: "Das Passwort muss mindestens 8 Zeichen lang sein.",
|
||||||
|
ERR_USERNAME_TOO_SHORT: "Der Benutzername muss mindestens 3 Zeichen lang sein.",
|
||||||
|
ERR_INVALID_EMAIL: "Das E-Mail-Format ist ungültig.",
|
||||||
|
ERR_USERNAME_TAKEN: "Dieser Benutzername ist bereits vergeben.",
|
||||||
|
ERR_EMAIL_TAKEN: "Diese E-Mail-Adresse ist bereits vergeben.",
|
||||||
|
ERR_INVALID_CREDENTIALS: "Benutzername/E-Mail oder Passwort falsch.",
|
||||||
|
ERR_EMAIL_NOT_VERIFIED: "Bitte bestätige zuerst deine E-Mail-Adresse.",
|
||||||
|
ERR_INTERNAL_SERVER: "Ein interner Serverfehler ist aufgetreten.",
|
||||||
|
ERR_INVALID_OTP: "Der eingegebene Code ist falsch. Bitte überprüfe deine Eingabe.",
|
||||||
|
ERR_OTP_EXPIRED: "Dieser Code ist abgelaufen. Bitte fordere einen neuen Code an.",
|
||||||
|
ERR_COOLDOWN_ACTIVE: "Bitte warte 2 Minuten, bevor du einen neuen Code anforderst.",
|
||||||
|
|
||||||
|
// --- Erfolgsmeldungen ---
|
||||||
|
MSG_REGISTER_SUCCESS: "Registrierung erfolgreich. Bitte prüfe deine E-Mails.",
|
||||||
|
MSG_LOGIN_SUCCESS: "Erfolgreich eingeloggt!",
|
||||||
|
MSG_OTP_RESENT: "Ein neuer Code wurde an deine E-Mail gesendet.",
|
||||||
|
MSG_RESET_EMAIL_SENT: "Code gesendet. Bitte prüfe deine E-Mails.",
|
||||||
|
MSG_PASSWORD_RESET_SUCCESS: "Passwort geändert! Du kannst dich nun einloggen.",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
// --- UI Elements ---
|
||||||
|
tab_login: "Login",
|
||||||
|
tab_register: "Register",
|
||||||
|
btn_login: "LOGIN",
|
||||||
|
btn_register: "REGISTER",
|
||||||
|
discord_text: "Official Discord Server",
|
||||||
|
link_forgot_password: "Forgot Password?",
|
||||||
|
btn_reset_request: "REQUEST CODE",
|
||||||
|
btn_reset_password: "CHANGE PASSWORD",
|
||||||
|
link_back: "« back",
|
||||||
|
|
||||||
|
// --- Placeholders ---
|
||||||
|
ph_username_login: "Username / E-Mail",
|
||||||
|
ph_username_reg: "Enter Username",
|
||||||
|
ph_email: "Enter E-Mail",
|
||||||
|
ph_password: "Enter Password",
|
||||||
|
ph_otp: "6-digit code",
|
||||||
|
|
||||||
|
// --- Labels ---
|
||||||
|
lbl_agb: "I accept the Terms & Rules",
|
||||||
|
lbl_remember: "Keep me logged in",
|
||||||
|
|
||||||
|
// --- OTP Modus ---
|
||||||
|
txt_otp_instruction: "Please enter the 6-digit code from your e-mail.",
|
||||||
|
txt_reset_instruction_1: "To reset your password, please enter your e-mail address.",
|
||||||
|
txt_reset_instruction_2: "Please enter the code from the e-mail and your new password.",
|
||||||
|
btn_verify: "VERIFY",
|
||||||
|
msg_verify_success: "Verification successful! You can now log in.",
|
||||||
|
link_resend_otp: "Resend Code",
|
||||||
|
|
||||||
|
// --- Footer ---
|
||||||
|
link_imprint: "Imprint",
|
||||||
|
link_privacy: "Privacy Policy",
|
||||||
|
|
||||||
|
// --- Backend Error Messages (Mapping) ---
|
||||||
|
ERR_MISSING_FIELDS: "Please fill in all fields.",
|
||||||
|
ERR_PASSWORD_TOO_SHORT: "Password must be at least 8 characters long.",
|
||||||
|
ERR_USERNAME_TOO_SHORT: "Username must be at least 3 characters long.",
|
||||||
|
ERR_INVALID_EMAIL: "Invalid e-mail format.",
|
||||||
|
ERR_USERNAME_TAKEN: "This username is already taken.",
|
||||||
|
ERR_EMAIL_TAKEN: "This e-mail address is already taken.",
|
||||||
|
ERR_INVALID_CREDENTIALS: "Invalid username/e-mail or password.",
|
||||||
|
ERR_EMAIL_NOT_VERIFIED: "Please verify your e-mail address first.",
|
||||||
|
ERR_INTERNAL_SERVER: "An internal server error occurred.",
|
||||||
|
ERR_INVALID_OTP: "The entered code is incorrect. Please check your input.",
|
||||||
|
ERR_OTP_EXPIRED: "This code has expired. Please request a new code.",
|
||||||
|
ERR_COOLDOWN_ACTIVE: "Please wait 2 minutes before requesting a new code.",
|
||||||
|
|
||||||
|
// --- Success Messages ---
|
||||||
|
MSG_REGISTER_SUCCESS: "Registration successful. Please check your e-mails.",
|
||||||
|
MSG_LOGIN_SUCCESS: "Successfully logged in!",
|
||||||
|
MSG_OTP_RESENT: "A new code has been sent to your e-mail.",
|
||||||
|
MSG_RESET_EMAIL_SENT: "Code sent. Please check your e-mails.",
|
||||||
|
MSG_PASSWORD_RESET_SUCCESS: "Password changed! You can now log in.",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die aktuell gewählte Sprache.
|
||||||
|
* Später können wir das auch im LocalStorage des Browsers speichern,
|
||||||
|
* damit sich das Spiel die Sprache des Nutzers merkt.
|
||||||
|
*/
|
||||||
|
export let currentLang = localStorage.getItem('void_language') || 'de';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion, um die Sprache zu wechseln.
|
||||||
|
*/
|
||||||
|
export function setLanguage(lang) {
|
||||||
|
if (translations[lang]) {
|
||||||
|
currentLang = lang;
|
||||||
|
localStorage.setItem('void_language', lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt den passenden Text für den übergebenen Schlüssel (Key).
|
||||||
|
* Fällt auf den Key selbst zurück, falls keine Übersetzung gefunden wird
|
||||||
|
* (hilft beim Debuggen, um fehlende Übersetzungen zu erkennen).
|
||||||
|
*/
|
||||||
|
export function t(key) {
|
||||||
|
return translations[currentLang][key] || key;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Application, Assets, Sprite, NineSliceSprite, Container, TilingSprite, Text, wordWrap } from 'pixi.js';
|
import { t, setLanguage, currentLang } from './i18n.js';
|
||||||
|
import { Application, Assets, Sprite, NineSliceSprite, Container, TilingSprite, Text, wordWrap, fragmentGPUTemplate } from 'pixi.js';
|
||||||
|
|
||||||
async function initGame() {
|
async function initGame() {
|
||||||
const app = new Application();
|
const app = new Application();
|
||||||
@@ -16,7 +17,8 @@ async function initGame() {
|
|||||||
try {
|
try {
|
||||||
const assetsToLoad = {
|
const assetsToLoad = {
|
||||||
//background: 'src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpookyComputer_01.png',
|
//background: 'src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpookyComputer_01.png',
|
||||||
background: 'src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpaceStation_01.png',
|
//background: 'src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpaceStation_01.png',
|
||||||
|
background: 'src/raw-assets/backgrounds/Background_A.png',
|
||||||
|
|
||||||
uiAtlas: 'src/assets/ui-atlas.json'
|
uiAtlas: 'src/assets/ui-atlas.json'
|
||||||
};
|
};
|
||||||
@@ -27,13 +29,28 @@ async function initGame() {
|
|||||||
await document.fonts.load('16px "UI Font"');
|
await document.fonts.load('16px "UI Font"');
|
||||||
await document.fonts.load('16px "Logo Font"');
|
await document.fonts.load('16px "Logo Font"');
|
||||||
|
|
||||||
|
updateTranslations();
|
||||||
|
|
||||||
buildScene(app, backgroundTexture, uiSpritesheet);
|
buildScene(app, backgroundTexture, uiSpritesheet);
|
||||||
|
|
||||||
|
document.getElementById('username').placeholder = t('ph_username_reg');
|
||||||
|
document.getElementById('email').placeholder = t('ph_email');
|
||||||
|
document.getElementById('password').placeholder = t('ph_password');
|
||||||
|
document.getElementById('btn-register').textContent = t('btn_register');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Fehler beim Laden der Assets:", error);
|
console.error("Fehler beim Laden der Assets:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateTranslations() {
|
||||||
|
const elements = document.querySelectorAll('[data-i18n]');
|
||||||
|
elements.forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n');
|
||||||
|
el.textContent = t(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function buildScene(app, bgTexture, sheet) {
|
function buildScene(app, bgTexture, sheet) {
|
||||||
// --- Farb-Variablen (pixiJS) ---
|
// --- Farb-Variablen (pixiJS) ---
|
||||||
const COLOR_PANEL_BG = 0x0b192c; // Dunkelblau für Panel & Inputs
|
const COLOR_PANEL_BG = 0x0b192c; // Dunkelblau für Panel & Inputs
|
||||||
@@ -109,11 +126,11 @@ function buildScene(app, bgTexture, sheet) {
|
|||||||
// --- Hexagon pattern
|
// --- Hexagon pattern
|
||||||
const pattern = new TilingSprite({
|
const pattern = new TilingSprite({
|
||||||
texture: sheet.textures[patternBgName],
|
texture: sheet.textures[patternBgName],
|
||||||
width: 500,
|
width: 430,
|
||||||
height: 520,
|
height: 520,
|
||||||
});
|
});
|
||||||
pattern.tileScale.set(0.3);
|
pattern.tileScale.set(0.3);
|
||||||
pattern.position.set(0, -10);
|
pattern.position.set(25, -10);
|
||||||
pattern.alpha = 0.05;
|
pattern.alpha = 0.05;
|
||||||
pattern.tint = COLOR_PATTERN;
|
pattern.tint = COLOR_PATTERN;
|
||||||
uiContainer.addChild(pattern);
|
uiContainer.addChild(pattern);
|
||||||
@@ -163,7 +180,7 @@ function buildScene(app, bgTexture, sheet) {
|
|||||||
const inputEmail = createUIItem(115, 200, 300, 50, frameInput, iconEmailName, COLOR_PANEL_BG, 0.8);
|
const inputEmail = createUIItem(115, 200, 300, 50, frameInput, iconEmailName, COLOR_PANEL_BG, 0.8);
|
||||||
const inputLock = createUIItem(115, 265, 300, 50, frameInput, iconLockName, COLOR_PANEL_BG, 0.8);
|
const inputLock = createUIItem(115, 265, 300, 50, frameInput, iconLockName, COLOR_PANEL_BG, 0.8);
|
||||||
|
|
||||||
const btnRegister = createUIItem(65, 370, 370, 55, frameBtn, null, COLOR_BTN_DEFAULT, 0.6);
|
const btnRegister = createUIItem(65, 390, 370, 55, frameBtn, null, COLOR_BTN_DEFAULT, 0.6);
|
||||||
|
|
||||||
// --- Discord Panel ---
|
// --- Discord Panel ---
|
||||||
const discordContainer = new Container();
|
const discordContainer = new Container();
|
||||||
@@ -210,16 +227,16 @@ function buildScene(app, bgTexture, sheet) {
|
|||||||
discordContainer.addChild(discordIcon);
|
discordContainer.addChild(discordIcon);
|
||||||
|
|
||||||
const discordText = new Text({
|
const discordText = new Text({
|
||||||
text: 'Offizieller Discord Server',
|
text: t('discord_text'),
|
||||||
style: {
|
style: {
|
||||||
fontFamily: 'UI Font',
|
fontFamily: 'UI Font',
|
||||||
fontSize: 14,
|
fontSize: 20,
|
||||||
fill: COLOR_TAB_INACTIVE,
|
fill: COLOR_TAB_INACTIVE,
|
||||||
wordWrap: false,
|
wordWrap: false,
|
||||||
wordWrapWidth: 120
|
wordWrapWidth: 120
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
discordText.position.set(60, 25);
|
discordText.position.set(60, 20);
|
||||||
discordContainer.addChild(discordText);
|
discordContainer.addChild(discordText);
|
||||||
|
|
||||||
uiContainer.addChild(discordContainer);
|
uiContainer.addChild(discordContainer);
|
||||||
@@ -228,6 +245,12 @@ function buildScene(app, bgTexture, sheet) {
|
|||||||
const htmlTabRegistert = document.getElementById('tab-register');
|
const htmlTabRegistert = document.getElementById('tab-register');
|
||||||
const htmlButton = document.getElementById('btn-register');
|
const htmlButton = document.getElementById('btn-register');
|
||||||
let isLoginMode = false;
|
let isLoginMode = false;
|
||||||
|
let isForgotPasswordMode = false;
|
||||||
|
let isResetPasswordMode = false;
|
||||||
|
let isOtpMode = false;
|
||||||
|
let pendingVerificationEmail = '';
|
||||||
|
let resetEmail = '';
|
||||||
|
let cooldownTimer = null;
|
||||||
|
|
||||||
htmlTabLogin.addEventListener('mouseenter', () => {
|
htmlTabLogin.addEventListener('mouseenter', () => {
|
||||||
if (!isLoginMode) {
|
if (!isLoginMode) {
|
||||||
@@ -282,6 +305,7 @@ function buildScene(app, bgTexture, sheet) {
|
|||||||
function setLoginMode() {
|
function setLoginMode() {
|
||||||
if (isLoginMode) return;
|
if (isLoginMode) return;
|
||||||
isLoginMode = true;
|
isLoginMode = true;
|
||||||
|
clearForm();
|
||||||
|
|
||||||
tabLogin.fill.tint = COLOR_TAB_ACTIVE;
|
tabLogin.fill.tint = COLOR_TAB_ACTIVE;
|
||||||
tabLogin.fill.alpha = 0.6;
|
tabLogin.fill.alpha = 0.6;
|
||||||
@@ -295,11 +319,17 @@ function buildScene(app, bgTexture, sheet) {
|
|||||||
|
|
||||||
inputLock.container.y = 200;
|
inputLock.container.y = 200;
|
||||||
document.getElementById('password').style.top = '200px';
|
document.getElementById('password').style.top = '200px';
|
||||||
|
document.getElementById('forgot-password-link').style.display = 'block';
|
||||||
|
|
||||||
|
document.getElementById('email').required = false;
|
||||||
|
|
||||||
document.getElementById('group-remember').style.display = 'flex';
|
document.getElementById('group-remember').style.display = 'flex';
|
||||||
|
document.getElementById('group-agb').style.display = 'none';
|
||||||
|
document.getElementById('agb').required = false;
|
||||||
|
|
||||||
htmlButton.textContent = 'LOGIN';
|
htmlButton.textContent = t('btn_login');
|
||||||
document.getElementById('username').placeholder = 'Username / E-Mail';
|
document.getElementById('username').placeholder = t('ph_username_login');
|
||||||
|
document.getElementById('password').placeholder = t('ph_password');
|
||||||
|
|
||||||
document.getElementById('msg-box').textContent = '';
|
document.getElementById('msg-box').textContent = '';
|
||||||
}
|
}
|
||||||
@@ -307,6 +337,7 @@ function buildScene(app, bgTexture, sheet) {
|
|||||||
function setRegisterMode() {
|
function setRegisterMode() {
|
||||||
if (!isLoginMode) return;
|
if (!isLoginMode) return;
|
||||||
isLoginMode = false;
|
isLoginMode = false;
|
||||||
|
clearForm();
|
||||||
|
|
||||||
tabRegister.fill.tint = COLOR_TAB_ACTIVE;
|
tabRegister.fill.tint = COLOR_TAB_ACTIVE;
|
||||||
tabRegister.fill.alpha = 0.6;
|
tabRegister.fill.alpha = 0.6;
|
||||||
@@ -321,11 +352,160 @@ function buildScene(app, bgTexture, sheet) {
|
|||||||
inputLock.container.y = 265;
|
inputLock.container.y = 265;
|
||||||
document.getElementById('password').style.top = '265px';
|
document.getElementById('password').style.top = '265px';
|
||||||
|
|
||||||
|
document.getElementById('email').required = true;
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('group-remember').style.display = 'none';
|
document.getElementById('group-remember').style.display = 'none';
|
||||||
|
|
||||||
htmlButton.textContent = 'REGISTRIEREN';
|
document.getElementById('group-agb').style.display = 'flex';
|
||||||
document.getElementById('username').placeholder = 'Enter Username';
|
document.getElementById('agb').required = true;
|
||||||
|
|
||||||
|
htmlButton.textContent = t('btn_register');
|
||||||
|
document.getElementById('username').placeholder = t('ph_username_reg');
|
||||||
|
document.getElementById('email').placeholder = t('ph_email');
|
||||||
|
document.getElementById('password').placeholder = t('ph_password');
|
||||||
|
|
||||||
|
document.getElementById('msg-box').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOtpMode(email) {
|
||||||
|
isOtpMode = true;
|
||||||
|
pendingVerificationEmail = email;
|
||||||
|
clearForm();
|
||||||
|
|
||||||
|
// Tabs verstecken
|
||||||
|
document.getElementById('tab-container').style.display = 'none';
|
||||||
|
tabLogin.container.visible = false;
|
||||||
|
tabRegister.container.visible = false;
|
||||||
|
|
||||||
|
// Username-Feld verstecken
|
||||||
|
document.getElementById('username').style.display = 'none';
|
||||||
|
document.getElementById('username').required = false;
|
||||||
|
inputUser.container.visible = false;
|
||||||
|
|
||||||
|
// Email-Feld verstecken
|
||||||
|
document.getElementById('group-email').style.display = 'none';
|
||||||
|
document.getElementById('email').required = false;
|
||||||
|
inputEmail.container.visible = false;
|
||||||
|
|
||||||
|
// Das Erklärungstext-Feld einblenden
|
||||||
|
const instructionEl = document.getElementById('otp-instruction');
|
||||||
|
instructionEl.style.display = 'block';
|
||||||
|
instructionEl.textContent = t('txt_otp_instruction');
|
||||||
|
|
||||||
|
// Den "Code erneut senden" Link einblenden
|
||||||
|
document.getElementById('resend-otp-link').style.display = 'block';
|
||||||
|
|
||||||
|
// Das Passwort-Feld in die Mitte rücken und zum OTP-Feld umwandeln
|
||||||
|
// Wir nutzen das Schluss-Icon, das passt perfekt zum otp
|
||||||
|
inputLock.container.y = 200;
|
||||||
|
document.getElementById('password').style.top = '200px';
|
||||||
|
|
||||||
|
// WICHTIG: Typ auf 'text äbderb damit der Nutzer die Zahlen sieht
|
||||||
|
document.getElementById('password').type = 'text';
|
||||||
|
document.getElementById('password').placeholder = t('ph_otp');
|
||||||
|
document.getElementById('password').value = '';
|
||||||
|
|
||||||
|
// Checkboxen aufräumen
|
||||||
|
document.getElementById('group-agb').style.display = 'none';
|
||||||
|
document.getElementById('agb').required = false;
|
||||||
|
|
||||||
|
// Die "Angemeldet bleiben" Checkbox lassen wir optional drin
|
||||||
|
document.getElementById('group-remember').style.display = 'none';
|
||||||
|
|
||||||
|
// Button aktuallisieren
|
||||||
|
htmlButton.textContent = t('btn_verify');
|
||||||
|
document.getElementById('msg-box').textContent = '';
|
||||||
|
|
||||||
|
document.getElementById('back-link').style.display = 'block';
|
||||||
|
|
||||||
|
// Debug Only
|
||||||
|
//console.log("OTP EMAIL", email);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setForgotPasswordMode() {
|
||||||
|
isLoginMode = false;
|
||||||
|
isOtpMode = false;
|
||||||
|
isForgotPasswordMode = true;
|
||||||
|
isResetPasswordMode = false;
|
||||||
|
|
||||||
|
clearForm();
|
||||||
|
|
||||||
|
// Tabs verstecken
|
||||||
|
document.getElementById('tab-container').style.display = 'none';
|
||||||
|
tabLogin.container.visible = false;
|
||||||
|
tabRegister.container.visible = false;
|
||||||
|
|
||||||
|
// Username-Feld verstecken
|
||||||
|
document.getElementById('username').style.display = 'none';
|
||||||
|
document.getElementById('username').required = false;
|
||||||
|
inputUser.container.visible = false;
|
||||||
|
|
||||||
|
// Passwort-Feld verstecken
|
||||||
|
document.getElementById('password').style.display = 'none';
|
||||||
|
document.getElementById('password').required = false;
|
||||||
|
inputLock.container.visible = false;
|
||||||
|
|
||||||
|
document.getElementById('group-email').style.display = 'block';
|
||||||
|
document.getElementById('email').type = 'email';
|
||||||
|
document.getElementById('email').required = true;
|
||||||
|
document.getElementById('email').style.top = '200px';
|
||||||
|
inputEmail.container.visible = true;
|
||||||
|
inputEmail.container.y = 200;
|
||||||
|
|
||||||
|
document.getElementById('group-remember').style.display = 'none';
|
||||||
|
document.getElementById('group-agb').style.display = 'none';
|
||||||
|
document.getElementById('forgot-password-link').style.display = 'none';
|
||||||
|
|
||||||
|
const instructionEl = document.getElementById('otp-instruction');
|
||||||
|
instructionEl.style.display = 'block';
|
||||||
|
instructionEl.textContent = t('txt_reset_instruction_1');
|
||||||
|
|
||||||
|
document.getElementById('back-link').style.display = 'block';
|
||||||
|
|
||||||
|
htmlButton.textContent = t('btn_reset_request');
|
||||||
|
document.getElementById('msg-box').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setResetPasswordMode(email) {
|
||||||
|
isForgotPasswordMode = false;
|
||||||
|
isResetPasswordMode = true;
|
||||||
|
resetEmail = email;
|
||||||
|
|
||||||
|
clearForm();
|
||||||
|
|
||||||
|
// Tabs verstecken
|
||||||
|
document.getElementById('tab-container').style.display = 'none';
|
||||||
|
tabLogin.container.visible = false;
|
||||||
|
tabRegister.container.visible = false;
|
||||||
|
|
||||||
|
// Username-Feld verstecken
|
||||||
|
document.getElementById('username').style.display = 'none';
|
||||||
|
document.getElementById('username').required = false;
|
||||||
|
inputUser.container.visible = false;
|
||||||
|
|
||||||
|
document.getElementById('group-email').style.display = 'block';
|
||||||
|
document.getElementById('email').type = 'text';
|
||||||
|
document.getElementById('email').placeholder = t('ph_otp');
|
||||||
|
document.getElementById('email').required = true;
|
||||||
|
document.getElementById('email').style.top = '200px';
|
||||||
|
inputEmail.container.y = 200;
|
||||||
|
|
||||||
|
document.getElementById('password').style.display = 'block';
|
||||||
|
document.getElementById('password').required = true;
|
||||||
|
document.getElementById('password').placeholder = t('ph_password');
|
||||||
|
document.getElementById('password').style.top = '265px';
|
||||||
|
inputLock.container.visible = true;
|
||||||
|
inputLock.container.y = 265;
|
||||||
|
|
||||||
|
const instructionEl = document.getElementById('otp-instruction');
|
||||||
|
instructionEl.style.display = 'block';
|
||||||
|
instructionEl.style.top = '150px';
|
||||||
|
instructionEl.textContent = t('txt_reset_instruction_2');
|
||||||
|
|
||||||
|
document.getElementById('back-link').style.display = 'block';
|
||||||
|
|
||||||
|
htmlButton.textContent = t('btn_reset_password');
|
||||||
document.getElementById('msg-box').textContent = '';
|
document.getElementById('msg-box').textContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,8 +553,8 @@ function buildScene(app, bgTexture, sheet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
const screenWidth = app.screen.width;
|
const screenWidth = window.innerWidth;
|
||||||
const screenHeight = app.screen.height;
|
const screenHeight = window.innerHeight;
|
||||||
|
|
||||||
const scaleX = screenWidth / background.texture.width;
|
const scaleX = screenWidth / background.texture.width;
|
||||||
const scaleY = screenHeight / background.texture.height;
|
const scaleY = screenHeight / background.texture.height;
|
||||||
@@ -383,18 +563,608 @@ function buildScene(app, bgTexture, sheet) {
|
|||||||
background.x = (screenWidth - background.width) / 2;
|
background.x = (screenWidth - background.width) / 2;
|
||||||
background.y = (screenHeight - background.height) / 2;
|
background.y = (screenHeight - background.height) / 2;
|
||||||
|
|
||||||
uiContainer.x = (screenWidth - panelFrame.width) / 2;
|
let panelScale = 1.0;
|
||||||
uiContainer.y = (screenHeight - panelFrame.height) / 2;
|
let panelOffsetY = 0;
|
||||||
|
|
||||||
|
if (screenHeight <= 500){
|
||||||
|
panelScale = 0.4;
|
||||||
|
panelOffsetY = 0;
|
||||||
|
discordContainer.position.set(-420, 350);
|
||||||
|
logoContainer.position.set(-300, 150);
|
||||||
|
logoContainer.scale.set(1.3);
|
||||||
|
} else if (screenWidth <= 600) {
|
||||||
|
panelScale = 0.7;
|
||||||
|
panelOffsetY = 0;
|
||||||
|
discordContainer.position.set(125, 520);
|
||||||
|
logoContainer.position.set(250, -120);
|
||||||
|
logoContainer.scale.set(0.7);
|
||||||
|
} else {
|
||||||
|
panelScale = 1.0;
|
||||||
|
panelOffsetY = 0;
|
||||||
|
discordContainer.position.set(640, 380);
|
||||||
|
logoContainer.position.set(250, -120);
|
||||||
|
logoContainer.scale.set(1.0);
|
||||||
|
|
||||||
|
if (screenHeight <= 1050) {
|
||||||
|
panelOffsetY = 80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uiContainer.scale.set(panelScale);
|
||||||
|
uiContainer.x = (screenWidth - (panelFrame.width * panelScale)) / 2;
|
||||||
|
uiContainer.y = ((screenHeight - (panelFrame.height * panelScale)) / 2) + panelOffsetY;
|
||||||
|
|
||||||
|
const htmlFooter = document.getElementById('game-footer');
|
||||||
|
const footerHeight = (htmlFooter && htmlFooter.style.display !== 'none') ? htmlFooter.offsetHeight : 0;
|
||||||
|
|
||||||
|
const targetFooterHeight = Math.max(footerHeight, 40);
|
||||||
|
footerBackground.height = targetFooterHeight;
|
||||||
|
footerPattern.height = targetFooterHeight;
|
||||||
|
|
||||||
footerBackground.width = screenWidth;
|
footerBackground.width = screenWidth;
|
||||||
footerPattern.width = screenWidth;
|
footerPattern.width = screenWidth;
|
||||||
footerBackgroundContainer.y = screenHeight - 40;
|
|
||||||
|
footerBackgroundContainer.y = screenHeight - footerHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlTabLogin.addEventListener('click', setLoginMode);
|
htmlTabLogin.addEventListener('click', setLoginMode);
|
||||||
htmlTabRegistert.addEventListener('click', setRegisterMode);
|
htmlTabRegistert.addEventListener('click', setRegisterMode);
|
||||||
app.renderer.on('resize', resize);
|
app.renderer.on('resize', resize);
|
||||||
resize();
|
resize();
|
||||||
|
|
||||||
|
// API-Anbindung (login & Registrierung)
|
||||||
|
const API_BASE_URL = 'https://api-dev.void-genesis.de/api/auth';
|
||||||
|
const form = document.getElementById('register-form');
|
||||||
|
const msgBox = document.getElementById('msg-box');
|
||||||
|
|
||||||
|
// Wir fangen das 'submit' Event des HTML-Formulars ab
|
||||||
|
// Das Event feuert, wenn der Nutzer auf den Button klickt oder Enter drückt
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const usernameVal = document.getElementById('username').value;
|
||||||
|
const emailVal = document.getElementById('email').value;
|
||||||
|
const passwordVal = document.getElementById('password').value;
|
||||||
|
|
||||||
|
msgBox.textContent = '...';
|
||||||
|
msgBox.style.color = 'var(--color-text-main)';
|
||||||
|
|
||||||
|
if (isForgotPasswordMode) {
|
||||||
|
try {
|
||||||
|
const response = await fetch (`${API_BASE_URL}/forgot-password`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: emailVal
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
msgBox.style.color = 'var(--color-error)';
|
||||||
|
msgBox.textContent = t(data.error) || t('ERR_INTERNAL_SERVER');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBox.style.color = 'var(--color-green-bright)';
|
||||||
|
msgBox.textContent = t(data.message);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setResetPasswordMode(emailVal);
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Netzwerkfehler bei Passwort-Reset-Anfrage:". error);
|
||||||
|
msgBox.style.color = 'var(--color-error)';
|
||||||
|
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isResetPasswordMode) {
|
||||||
|
try {
|
||||||
|
const response = await fetch (`${API_BASE_URL}/reset-password`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: resetEmail,
|
||||||
|
newPassword: passwordVal
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
msgBox.style.color = 'var(--color-error)';
|
||||||
|
msgBox.textContent = t(data.error) || t('ERR_INTERNAL_SERVER');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBox.style.color = 'var(--color-green-bright)';
|
||||||
|
msgBox.textContent = t(data.message);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Netzwerkfehler bei Passwort-Reset-Anfrage:". error);
|
||||||
|
msgBox.style.color = 'var(--color-error)';
|
||||||
|
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOtpMode) {
|
||||||
|
try {
|
||||||
|
const response = await fetch (`${API_BASE_URL}/verify-email`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: pendingVerificationEmail,
|
||||||
|
otpCode: passwordVal
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
msgBox.style.color = 'var(--color-error)';
|
||||||
|
msgBox.textContent = t(data.error) || t('ERR_INTERNAL_SERVER');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBox.style.color = 'var(--color-green-bright)';
|
||||||
|
msgBox.textContent = t('msg_verify_success');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Netzwerkfehler bei Verifizieung:". error);
|
||||||
|
msgBox.style.color = 'var(--color-error)';
|
||||||
|
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoginMode) {
|
||||||
|
try {
|
||||||
|
const response = await fetch (`${API_BASE_URL}/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
identifier: usernameVal,
|
||||||
|
password: passwordVal
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
msgBox.style.color = 'var(--color-error)';
|
||||||
|
msgBox.textContent = t(data.error) || t('ERR_INTERNAL_SERVER');
|
||||||
|
|
||||||
|
if (data.error === 'ERR_EMAIL_NOT_VERIFIED') {
|
||||||
|
setTimeout(() => {
|
||||||
|
setOtpMode(data.email);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBox.style.color = 'var(--color-green-bright)';
|
||||||
|
msgBox.textContent = t(data.message);
|
||||||
|
|
||||||
|
|
||||||
|
// TODO JWT Tokens im LocalStorage/SessionStorage ablegen und Szene wechseln
|
||||||
|
const rememberMe = document.getElementById('remember').checked;
|
||||||
|
sessionStorage.setItem('void_access_token', data.accessToken);
|
||||||
|
sessionStorage.setItem('void_user', JSON.stringify(data.user));
|
||||||
|
|
||||||
|
if (rememberMe) {
|
||||||
|
localStorage.setItem('void_refresh_token', data.refreshToken);
|
||||||
|
sessionStorage.removeItem('void_refresh_token');
|
||||||
|
} else {
|
||||||
|
sessionStorage.setItem('void_refresh_token', data.refreshToken);
|
||||||
|
localStorage.removeItem('void_refresh_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug only
|
||||||
|
//console.log("Login erfolgreich! Tokens wurden verarbeitet.");
|
||||||
|
|
||||||
|
transitionToLobby();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Netzwerkfehler beim Login:". error);
|
||||||
|
msgBox.style.color = 'var(--color-error)';
|
||||||
|
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||||
|
}
|
||||||
|
} else { // Register Mode
|
||||||
|
try {
|
||||||
|
const response = await fetch (`${API_BASE_URL}/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: usernameVal,
|
||||||
|
email: emailVal,
|
||||||
|
password: passwordVal
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
msgBox.style.color = 'var(--color-error)';
|
||||||
|
msgBox.textContent = t(data.error) || t('ERR_INTERNAL_SERVER');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBox.style.color = 'var(--color-green-bright)';
|
||||||
|
msgBox.textContent = t(data.message);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setOtpMode(emailVal);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Debug only
|
||||||
|
console.log("Registrierung erfolgreich! Account ID:", data.accountId);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Netzwerkfehler bei Registrierung:". error);
|
||||||
|
msgBox.style.color = 'var(--color-error)';
|
||||||
|
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function clearForm() {
|
||||||
|
document.getElementById('username').value = '';
|
||||||
|
document.getElementById('email').value = '';
|
||||||
|
document.getElementById('password').value = '';
|
||||||
|
document.getElementById('remember').checked = false;
|
||||||
|
document.getElementById('agb').checked = false;
|
||||||
|
|
||||||
|
document.getElementById('tab-container').style.display = 'block';
|
||||||
|
tabLogin.container.visible = true;
|
||||||
|
tabRegister.container.visible = true;
|
||||||
|
|
||||||
|
document.getElementById('username').style.display = 'block';
|
||||||
|
inputUser.container.visible = true;
|
||||||
|
|
||||||
|
const pwField = document.getElementById('password');
|
||||||
|
pwField.style.display = 'block';
|
||||||
|
pwField.type = 'password';
|
||||||
|
inputLock.container.visible = true;
|
||||||
|
|
||||||
|
document.getElementById('otp-instruction').style.display = 'none';
|
||||||
|
document.getElementById('resend-otp-link').style.display = 'none';
|
||||||
|
document.getElementById('forgot-password-link').style.display = 'none';
|
||||||
|
document.getElementById('back-link').style.display = 'none';
|
||||||
|
|
||||||
|
if (cooldownTimer) {
|
||||||
|
clearInterval(cooldownTimer);
|
||||||
|
cooldownTimer = null;
|
||||||
|
}
|
||||||
|
const resendLink = document.getElementById('resend-otp-link');
|
||||||
|
if(resendLink) {
|
||||||
|
resendLink.style.pointerEvents = 'auto';
|
||||||
|
resendLink.style.opacity = '1.0';
|
||||||
|
resendLink.textContent = t('link_resend_otp');
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgBox = document.getElementById('msg-box');
|
||||||
|
msgBox.textContent = '';
|
||||||
|
msgBox.style.color = 'var(--color-text-main)';
|
||||||
|
}
|
||||||
|
|
||||||
|
const resendOtpLink = document.getElementById('resend-otp-link');
|
||||||
|
if (resendOtpLink) {
|
||||||
|
resendOtpLink.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!pendingVerificationEmail) return;
|
||||||
|
|
||||||
|
msgBox.textContent = '...';
|
||||||
|
msgBox.style.color = 'var(--color-text-main)';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/resend-otp`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email: pendingVerificationEmail })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
msgBox.style.color = 'var(--color-error)';
|
||||||
|
msgBox.textContent = t(data.error) || t('ERR_INTERNAL_SERVER');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBox.style.color = 'var(--color-green-bright)';
|
||||||
|
msgBox.textContent = t(data.message);
|
||||||
|
|
||||||
|
startVisualCountdown(resendOtpLink, 'link_resend_otp', 120);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Netzwerkfehler beim erneuten Senden des OTP:", error);
|
||||||
|
msgBox.style.color = 'var(--color-error)';
|
||||||
|
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const forgotPasswordLink = document.getElementById('forgot-password-link');
|
||||||
|
if (forgotPasswordLink) {
|
||||||
|
forgotPasswordLink.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setForgotPasswordMode();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const backLink = document.getElementById('back-link');
|
||||||
|
if (backLink) {
|
||||||
|
backLink.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
pendingVerificationEmail = '';
|
||||||
|
resetEmail = '';
|
||||||
|
|
||||||
|
isOtpMode = false;
|
||||||
|
isForgotPasswordMode = false;
|
||||||
|
isResetPasswordMode = false;
|
||||||
|
isLoginMode = false;
|
||||||
|
|
||||||
|
setLoginMode();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet einen visuellen Countdown auf einem übergebenen Link-Element.
|
||||||
|
* Sperrt währenddessen die Klickbarkeit
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} linkElement - Das HTML A-Tag (zB resendOtpLink)
|
||||||
|
* @param {string} orginalTextKey - Der i18n Key für den ursprünglichen Text
|
||||||
|
* @param {number} durationSconds - Die Dauer in Sekunden (hier 120 für 2Minuten)
|
||||||
|
*/
|
||||||
|
function startVisualCountdown(linkElement, orginalTextKey, durationSconds) {
|
||||||
|
if (cooldownTimer) clearInterval(cooldownTimer);
|
||||||
|
let remaining = durationSconds;
|
||||||
|
|
||||||
|
linkElement.style.pointerEvents = 'none';
|
||||||
|
linkElement.style.opacity = '0.5';
|
||||||
|
|
||||||
|
const updateText = () => {
|
||||||
|
const minutes = Math.floor(remaining / 60);
|
||||||
|
const seconds = remaining % 60;
|
||||||
|
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||||
|
linkElement.textContent = `${t(orginalTextKey)} (${timeString})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateText();
|
||||||
|
|
||||||
|
cooldownTimer = setInterval(() => {
|
||||||
|
remaining--;
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
clearInterval(cooldownTimer);
|
||||||
|
cooldownTimer = null;
|
||||||
|
linkElement.style.pointerEvents = 'auto';
|
||||||
|
linkElement.style.opacity = '1.0';
|
||||||
|
linkElement.textContent = t(orginalTextKey);
|
||||||
|
} else {
|
||||||
|
updateText();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diese Funktion wird aufgerufenm wenn der Login erfolgreich war und die Tokens
|
||||||
|
* sicher gespeichert sind, Sie baut das HTML-Formulat ab und bereitet PixiJS
|
||||||
|
* auf die Lobby-Ansicht vor
|
||||||
|
*/
|
||||||
|
async function transitionToLobby() {
|
||||||
|
const formElement = document.getElementById('register-form');
|
||||||
|
if (formElement) {
|
||||||
|
formElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
//const footerElement = document.getElementById('game-footer');
|
||||||
|
//if (footerElement) {
|
||||||
|
// footerElement.style.display = 'none';
|
||||||
|
//}
|
||||||
|
|
||||||
|
uiContainer.visible = false;
|
||||||
|
//footerBackgroundContainer.visible = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lobbyBGPath = 'src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpookyComputer_01.png';
|
||||||
|
const lobbyBGTexture = await Assets.load(lobbyBGPath);
|
||||||
|
background.texture = lobbyBGTexture;
|
||||||
|
resize();
|
||||||
|
|
||||||
|
//console.log("Szenenwechsel zur Lobby erfolgreich abgeschlossen!");
|
||||||
|
buildLobbyUI();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fehler beim Laden des Lobby-Hintergrunds:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blendet die Top-Bar ein, setzt den Spilernamen und aktiviert den Logout
|
||||||
|
*/
|
||||||
|
function buildLobbyUI() {
|
||||||
|
// User-Menü (das beim Login versteckt war) einblenden
|
||||||
|
const userDropdown = document.getElementById('user-dropdown');
|
||||||
|
if (userDropdown) {
|
||||||
|
userDropdown.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gespeicherte User-Daten einsetzen
|
||||||
|
const userDataStr = sessionStorage.getItem('void_user');
|
||||||
|
if (userDataStr) {
|
||||||
|
try {
|
||||||
|
const userData = JSON.parse(userDataStr);
|
||||||
|
const usernameEl = document.getElementById('lobby-username');
|
||||||
|
if (usernameEl && userData.username) {
|
||||||
|
usernameEl.textContent = userData.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platzhalter, bis das Backend die echten Werte im JWT/Login mitsendet
|
||||||
|
const emailEl = document.getElementById('lobby-email');
|
||||||
|
if (emailEl && userData.email) {
|
||||||
|
emailEl.textContent = userData.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
const premiumEl = document.getElementById('lobby-premium');
|
||||||
|
if (premiumEl && userData.res_premium !== undefined) {
|
||||||
|
premiumEl.textContent = userData.res_premium;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Fehler beim Auslesen der User-Daten:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft beim Start der Anwendung, ob der Nutzer bereits eingeloggt ist.
|
||||||
|
* Nutz das Refresh-Token, um ein neues Access-Token anzufordern.
|
||||||
|
*/
|
||||||
|
async function checkAuthOnStartup() {
|
||||||
|
const refreshToken = localStorage.getItem('void_refresh_token') || sessionStorage.getItem('void_refresh_token');
|
||||||
|
|
||||||
|
if (!refreshToken) { return }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch (`${API_BASE_URL}/refresh`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ refreshToken: refreshToken })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (response.ok && data.accessToken) {
|
||||||
|
sessionStorage.setItem('void_access_token', data.accessToken);
|
||||||
|
|
||||||
|
if (data.user) {
|
||||||
|
sessionStorage.setItem('void_user', JSON.stringify(data.user));
|
||||||
|
}
|
||||||
|
|
||||||
|
transitionToLobby();
|
||||||
|
} else {
|
||||||
|
// Fallback: Das Backend hat das Token abgelehnt (abgelaufen, ungültig oder in der DB gelöscht)
|
||||||
|
// Wir putzen die ungültigen Tokens aus dem Speicher, um Fehler zu vermeiden.
|
||||||
|
console.warn("Auto-Login fehlgeschlagen. Token sind ungültig und werden gelöscht.");
|
||||||
|
sessionStorage.removeItem('void_access_token');
|
||||||
|
sessionStorage.removeItem('void_refresh_token');
|
||||||
|
localStorage.removeItem('void_refresh_token');
|
||||||
|
sessionStorage.removeItem('void_user');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Netzwerkfehler beim Auto-Login:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisiert alle Dropdowns und die Sprachauswahl.
|
||||||
|
* Diese Funktion wird direkt beim Start der App ausgeführt,
|
||||||
|
* damit das Sprach-Menü auch auf dem Login-Screen funktioniert.
|
||||||
|
*/
|
||||||
|
function initDropdowns() {
|
||||||
|
const langToggle = document.getElementById('lang-toggle');
|
||||||
|
const langMenu = document.getElementById('lang-menu');
|
||||||
|
const userToggle = document.getElementById('user-toggle');
|
||||||
|
const userMenu = document.getElementById('user-menu');
|
||||||
|
|
||||||
|
const closeAllDropdowns = () => {
|
||||||
|
if (langMenu) langMenu.classList.remove('show');
|
||||||
|
if (userMenu) userMenu.classList.remove('show');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (langToggle && langMenu) {
|
||||||
|
langToggle.onclick = (e) => {
|
||||||
|
e.stopPropagation(); // Stoppt das Event, damit der document-Klick (unten) es nicht sofort wieder schließt
|
||||||
|
const isShowing = langMenu.classList.contains('show');
|
||||||
|
closeAllDropdowns();
|
||||||
|
if (!isShowing) langMenu.classList.add('show');
|
||||||
|
};
|
||||||
|
// Ein Klick IN das geöffnete Menü soll es nicht sofort schließen
|
||||||
|
langMenu.onclick = (e) => e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userToggle && userMenu) {
|
||||||
|
userToggle.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const isShowing = userMenu.classList.contains('show');
|
||||||
|
closeAllDropdowns();
|
||||||
|
if (!isShowing) userMenu.classList.add('show');
|
||||||
|
};
|
||||||
|
userMenu.onclick = (e) => e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', closeAllDropdowns);
|
||||||
|
|
||||||
|
if (langMenu) {
|
||||||
|
const langItems = langMenu.querySelectorAll('.dropdown-item');
|
||||||
|
langItems.forEach(item => {
|
||||||
|
item.onclick = () => {
|
||||||
|
const selectedLang = item.getAttribute('data-lang');
|
||||||
|
|
||||||
|
document.getElementById('current-lang-text').textContent = selectedLang.toUpperCase();
|
||||||
|
document.getElementById('current-flag').textContent = item.querySelector('.flag-icon').textContent;
|
||||||
|
|
||||||
|
setLanguage(selectedLang);
|
||||||
|
|
||||||
|
updateTranslations();
|
||||||
|
|
||||||
|
// 4. Platzhaltertexte der Input-Felder dynamisch aktualisieren
|
||||||
|
const userField = document.getElementById('username');
|
||||||
|
if (userField) userField.placeholder = isLoginMode ? t('ph_username_login') : t('ph_username_reg');
|
||||||
|
const emailField = document.getElementById('email');
|
||||||
|
if (emailField) emailField.placeholder = t('ph_email');
|
||||||
|
const passField = document.getElementById('password');
|
||||||
|
if (passField) passField.placeholder = isOtpMode || isResetPasswordMode ? t('ph_otp') : t('ph_password');
|
||||||
|
|
||||||
|
closeAllDropdowns();
|
||||||
|
//console.log("Sprache live gewechselt zu:", selectedLang);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Logout-Logik ---
|
||||||
|
const btnLogout = document.getElementById('btn-logout');
|
||||||
|
if (btnLogout) {
|
||||||
|
btnLogout.onclick = () => {
|
||||||
|
sessionStorage.removeItem('void_access_token');
|
||||||
|
sessionStorage.removeItem('void_refresh_token');
|
||||||
|
localStorage.removeItem('void_refresh_token');
|
||||||
|
sessionStorage.removeItem('void_user');
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (langMenu) {
|
||||||
|
const activeLangItem = langMenu.querySelector(`.dropdown-item[data-lang="${currentLang}"]`);
|
||||||
|
if (activeLangItem) {
|
||||||
|
// Flagge und Text auslesen und in die sichtbare Top-Bar setzen
|
||||||
|
const flag = activeLangItem.querySelector('.flag-icon').textContent;
|
||||||
|
const langText = document.getElementById('current-lang-text');
|
||||||
|
const flagIcon = document.getElementById('current-flag');
|
||||||
|
|
||||||
|
if (langText) langText.textContent = currentLang.toUpperCase();
|
||||||
|
if (flagIcon) flagIcon.textContent = flag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAuthOnStartup();
|
||||||
|
initDropdowns();
|
||||||
}
|
}
|
||||||
|
|
||||||
initGame();
|
initGame();
|
||||||
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
@@ -2,7 +2,7 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'UI Font';
|
font-family: 'UI Font';
|
||||||
/* .otf Dateien bekommen das Format 'opentype', .ttf Dateien 'truetype' */
|
/* .otf Dateien bekommen das Format 'opentype', .ttf Dateien 'truetype' */
|
||||||
src: url('src/assets/fonts/Gugi-Regular.ttf') format('truetype');
|
src: url('src/assets/fonts/WDXLLubrifontSC-Regular.ttf') format('truetype');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
@@ -40,17 +40,19 @@ body, html {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: var(--color-bg-page);
|
background-color: var(--color-bg-page);
|
||||||
|
font-family: var(--font-main);
|
||||||
|
|
||||||
/* Wir nutzen nun unsere lokale Variable */
|
/* NEU: Verbietet mobilen Browsern das eigenmächtige Vergrößern der Schrift */
|
||||||
font-family: var(--font-main);
|
-webkit-text-size-adjust: 100%;
|
||||||
|
text-size-adjust: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#game-wrapper {
|
#game-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pixi-container {
|
#pixi-container {
|
||||||
@@ -70,16 +72,16 @@ body, html {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#register-form {
|
#register-form {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 500px;
|
height: 500px;
|
||||||
position: relative;
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group label {
|
.input-group label {
|
||||||
@@ -130,7 +132,7 @@ body, html {
|
|||||||
width: 370px;
|
width: 370px;
|
||||||
height: 55px;
|
height: 55px;
|
||||||
left: 65px;
|
left: 65px;
|
||||||
top: 370px;
|
top: 390px;
|
||||||
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -138,7 +140,7 @@ body, html {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
text-transform: uppercase;
|
/*text-transform: uppercase;*/
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
@@ -187,12 +189,25 @@ body, html {
|
|||||||
.checkbox-group {
|
.checkbox-group {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 115px;
|
left: 115px;
|
||||||
top: 275px;
|
/*top: 275px; */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#group-remember {
|
||||||
|
/* Position für den Login-Modus.
|
||||||
|
Das Passwortfeld liegt hier bei 200px, 275px passt also perfekt. */
|
||||||
|
top: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#group-agb {
|
||||||
|
/* Position für den Registrierungs-Modus.
|
||||||
|
Das Passwortfeld liegt hier tiefer (265px + 50px Höhe).
|
||||||
|
Wir setzen die Checkbox darunter. */
|
||||||
|
top: 325px;
|
||||||
|
}
|
||||||
|
|
||||||
.checkbox-group label {
|
.checkbox-group label {
|
||||||
color: var(--color-text-main);
|
color: var(--color-text-main);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -218,9 +233,21 @@ body, html {
|
|||||||
left: 2px;
|
left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#otp-instruction {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 120px;
|
||||||
|
left: 65px;
|
||||||
|
width: 370px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text-main);
|
||||||
|
font-size: 15px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#msg-box {
|
#msg-box {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 330px;
|
top: 350px;
|
||||||
left: 65px;
|
left: 65px;
|
||||||
width: 370px;
|
width: 370px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
@@ -233,6 +260,48 @@ body, html {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- NEU: RESEND OTP LINK --- */
|
||||||
|
#resend-otp-link {
|
||||||
|
position: absolute;
|
||||||
|
top: 250px;
|
||||||
|
left: 65px;
|
||||||
|
width: 370px;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text-inactive);
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: underline; /* Unterstrichen, damit es als Link erkennbar ist */
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
/* WICHTIG: Erlaubt das Anklicken, da der Container darüber (ui-layer) pointer-events: none hat */
|
||||||
|
pointer-events: auto;
|
||||||
|
transition: color 0.2s ease, text-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#resend-otp-link:hover {
|
||||||
|
color: var(--color-cyan-bright);
|
||||||
|
text-shadow: 0 0 8px var(--color-cyan-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
#forgot-password-link {
|
||||||
|
position: absolute;
|
||||||
|
/* Wir positionieren ihn unter dem Passwort-Feld (200px) auf Höhe der Checkboxen */
|
||||||
|
top: 255px;
|
||||||
|
left: 120px;
|
||||||
|
|
||||||
|
color: var(--color-text-inactive);
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto; /* Wichtig für die Klickbarkeit durch den ui-layer */
|
||||||
|
transition: color 0.2s ease, text-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forgot-password-link:hover {
|
||||||
|
color: var(--color-cyan-bright);
|
||||||
|
text-shadow: 0 0 8px var(--color-cyan-bright);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- NEU: DISCORD LINK --- */
|
/* --- NEU: DISCORD LINK --- */
|
||||||
#discord-link {
|
#discord-link {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -296,4 +365,249 @@ body, html {
|
|||||||
#game-footer a:hover {
|
#game-footer a:hover {
|
||||||
color: var(--color-cyan-bright);
|
color: var(--color-cyan-bright);
|
||||||
text-shadow: 0 0 8px var(--color-cyan-bright);
|
text-shadow: 0 0 8px var(--color-cyan-bright);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#back-link {
|
||||||
|
position: absolute;
|
||||||
|
/* Wir positionieren ihn oben links, wo sonst die Tabs sitzen */
|
||||||
|
top: 70px;
|
||||||
|
left: 65px;
|
||||||
|
|
||||||
|
color: var(--color-text-inactive);
|
||||||
|
font-size: 15px;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto; /* Wichtig für Klickbarkeit */
|
||||||
|
transition: color 0.2s ease, text-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#back-link:hover {
|
||||||
|
color: var(--color-cyan-bright);
|
||||||
|
text-shadow: 0 0 8px var(--color-cyan-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
#lobby-ui {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top-bar {
|
||||||
|
width: 100%; height: 50px;
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: none;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
padding: 0 20px; box-sizing: border-box;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.top-bar-right {
|
||||||
|
display: flex; align-items: center; gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Grundgerüst der Custom Dropdowns --- */
|
||||||
|
.custom-dropdown {
|
||||||
|
position: relative;
|
||||||
|
font-family: var(--font-main);
|
||||||
|
color: var(--color-text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Der sichtbare Klick-Bereich (Toggle) */
|
||||||
|
.dropdown-toggle {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none; /* Verhindert das versehentliche Markieren von Text */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle:hover {
|
||||||
|
border: 1px solid var(--color-cyan-dark);
|
||||||
|
background: rgba(0, 240, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-arrow {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--color-cyan-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Das aufklappbare Menü --- */
|
||||||
|
.dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%; /* Direkt unter dem Toggle anheften */
|
||||||
|
right: 0; /* Rechtsbündig abschließen */
|
||||||
|
margin-top: 10px;
|
||||||
|
background: rgba(11, 25, 44, 0.95);
|
||||||
|
border: 1px solid var(--color-cyan-dark);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
|
||||||
|
list-style: none; /* Aufzählungszeichen entfernen */
|
||||||
|
padding: 5px 0;
|
||||||
|
min-width: 150px;
|
||||||
|
|
||||||
|
/* Standardmäßig unsichtbar und keine Klicks annehmend */
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wenn die Klasse 'show' per JS hinzugefügt wird, klappt das Menü auf */
|
||||||
|
.dropdown-menu.show {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Das User-Menü braucht etwas mehr Platz für die langen Texte */
|
||||||
|
.user-menu {
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Einzelne Menü-Einträge --- */
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 10px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.2s ease, color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover:not(.disabled) {
|
||||||
|
background: rgba(0, 240, 255, 0.1);
|
||||||
|
color: var(--color-cyan-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Ausgegraute Einträge (Disabled) --- */
|
||||||
|
.dropdown-item.disabled, .dropdown-header.disabled {
|
||||||
|
color: var(--color-text-inactive);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-header {
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.premium-text {
|
||||||
|
color: var(--color-cyan-bright);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Trennlinie im Menü --- */
|
||||||
|
.dropdown-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--color-cyan-dark);
|
||||||
|
margin: 5px 0;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Spezielles Styling für den Logout-Button --- */
|
||||||
|
.logout-item {
|
||||||
|
color: var(--color-error);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-item:hover {
|
||||||
|
background: rgba(255, 68, 68, 0.1) !important;
|
||||||
|
color: var(--color-error) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
/* --- 1. Login/Registrierungs-Formular skalieren --- */
|
||||||
|
#register-form {
|
||||||
|
transform: translate(-50%, -50%) scale(0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 2. Top-Bar in der Lobby optimieren --- */
|
||||||
|
#top-bar {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-left .lobby-logo {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-info {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#lobby-username {
|
||||||
|
font-size: 14px;
|
||||||
|
max-width: 80px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-logout {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 3. Footer aufräumen --- */
|
||||||
|
#game-footer {
|
||||||
|
flex-direction: column;
|
||||||
|
height: auto;
|
||||||
|
padding: 10px 0;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-left, .footer-center, .footer-right {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#discord-link {
|
||||||
|
left: 125px;
|
||||||
|
top: 520px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 1050px) and (min-width: 601px) {
|
||||||
|
#register-form {
|
||||||
|
/* Wir schieben das HTML-Formular aus der exakten Mitte um 80px nach unten.
|
||||||
|
So hat das Logo (das in PixiJS darüber liegt) genug Luft zum Atmen. */
|
||||||
|
top: calc(50% + 80px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 500px) {
|
||||||
|
/* --- Formular noch kleiner machen, damit es vertikal passt --- */
|
||||||
|
#register-form {
|
||||||
|
/* scale(0.5) macht das Formular klein genug für Handys im Querformat */
|
||||||
|
transform: translate(-50%, -50%) scale(0.4);
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Discord-Button wie im normalen Handy-Modus nach unten klappen --- */
|
||||||
|
#discord-link {
|
||||||
|
left: -420px;
|
||||||
|
top: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Footer extrem flach machen --- */
|
||||||
|
/* Im Querformat haben wir in der Breite Platz, aber in der Höhe nicht.
|
||||||
|
Daher ordnen wir den Text wieder als Reihe (row) an. */
|
||||||
|
#game-footer {
|
||||||
|
flex-direction: row;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-left, .footer-center, .footer-right {
|
||||||
|
text-align: center;
|
||||||
|
width: 33%;
|
||||||
|
font-size: 10px; /* Schrift minimal verkleinern */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||