The Great Refactoring
This commit is contained in:
@@ -228,7 +228,7 @@ export const resendOTP = async (req, res) => {
|
||||
} 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({
|
||||
return res.status(statusCode).json({
|
||||
error: error.code
|
||||
});
|
||||
}
|
||||
@@ -253,6 +253,8 @@ export const requestPasswordReset = async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
await authService.requestPasswordReset(email);
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -37,11 +37,12 @@ export const getSectorsList = async(req, res) => {
|
||||
ORDER BY created_at ASC
|
||||
`, [accountId]);
|
||||
|
||||
|
||||
// Daten platzhalter
|
||||
const mapSectorData = (sector) => ({
|
||||
...sector,
|
||||
online_players: Math.floor(Math.random() * 150) + 10,
|
||||
planets_count: 1337
|
||||
planets_count: (Math.floor(Math.random() * 150) + 10) * 12
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -482,7 +482,7 @@ export const requestPasswordReset = async (email) => {
|
||||
|
||||
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]
|
||||
[accountResult.id]
|
||||
);
|
||||
|
||||
if (lastOtpResult.rows.length > 0) {
|
||||
@@ -508,7 +508,7 @@ export const requestPasswordReset = async (email) => {
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
|
||||
VALUES ($1, $2m 'password_reset' NOW() + INTERVAL '15 minutes')`,
|
||||
VALUES ($1, $2, 'password_reset', NOW() + INTERVAL '15 minutes')`,
|
||||
[account.id, resetCode]
|
||||
);
|
||||
|
||||
@@ -550,7 +550,7 @@ export const resetPassword = async (email, otpCode, newPassword) => {
|
||||
await client.query('BEGIN');
|
||||
|
||||
const accountResult = await client.query(
|
||||
'SELECT id FROM lobby_accounts WHERE email = $1'
|
||||
'SELECT id FROM lobby_accounts WHERE email = $1',
|
||||
[lowerCaseEmail]
|
||||
);
|
||||
|
||||
@@ -564,8 +564,8 @@ export const resetPassword = async (email, otpCode, newPassword) => {
|
||||
|
||||
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]
|
||||
WHERE account_id = $1 AND otp_code = $2 AND type = 'password_reset'`,
|
||||
[account.id, otpCode]
|
||||
);
|
||||
|
||||
if (otpResult.rows.length === 0) {
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Die Basis-URL für alle Authentifizierungs-Anfragen.
|
||||
* Zentralisiert, damit wir sie bei Server-Umzügen nur an einer Stelle ändern müssen.
|
||||
*/
|
||||
const API_BASE_URL = 'https://api-dev.void-genesis.de/api/auth';
|
||||
|
||||
/**
|
||||
* Führt den Login-Request an das Backend aus.
|
||||
*
|
||||
* @param {string} identifier - Benutzername oder E-Mail-Addresse
|
||||
* @param {string} password - Das eingegebene Klartext-Passwort
|
||||
*
|
||||
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
|
||||
*/
|
||||
export async function loginUser(identifier, password) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ identifier, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return { ok: response.ok, data };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Netzwerkfehler beim Login:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt den Registrierungs-Request an das Backend aus.
|
||||
*
|
||||
* @param {string} username - Der gewünschete Benutzername
|
||||
* @param {string} email - Die E-Mail-Addresse
|
||||
* @param {string} password - Das Klartext-Passwort
|
||||
*
|
||||
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
|
||||
*/
|
||||
export async function registerUser(username, email, password) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, email, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return { ok: response.ok, data };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Netzwerkfehler bei der Registrierung:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifiziert die E-Mail-Addresse mit dem 6-stelligen OTP Code.
|
||||
*
|
||||
* @param {string} email - Die E-Mail-Addresse des Nutzers
|
||||
* @param {string} otpCode - Der eingegebende 6-stellige Code aus der E-Mail
|
||||
*
|
||||
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
|
||||
*/
|
||||
export async function verifyEmailUser(email, otpCode) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/verify-email`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, otpCode })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return { ok: response.ok, data };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Netzwerkfehler bei der Verifizierung:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fordert einen neuen OTP-Code an, falls der alte abgelaufen ist (Resend-Funktion)
|
||||
*
|
||||
* @param {string} email - Die E-Mail-Addresse des Nutzers
|
||||
*
|
||||
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
|
||||
*/
|
||||
export async function resendOTPCode(email) {
|
||||
try {
|
||||
console.log ("Email:", email);
|
||||
const response = await fetch(`${API_BASE_URL}/resend-otp`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return { ok: response.ok, data };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Netzwerkfehler beim erneutem Senden des OTP:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Leitet den Prozess zum Zurücksetzen des Passworts ein (sendet die Email mit Code).
|
||||
*
|
||||
* @param {string} email - Die E-Mail-Addresse des Nutzers
|
||||
*
|
||||
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
|
||||
*/
|
||||
export async function requestPasswordResetUser(email) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/forgot-password`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return { ok: response.ok, data };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Netzwerkfehler bei, Passwort-Reset-Anfrage:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt das Passwort final zurück.
|
||||
*
|
||||
* @param {string} email - Die E-Mail-Addresse des Nutzers
|
||||
* @param {string} otpCode - Der 6-stellige Code aus der Reset-E-Mail
|
||||
* @param {string} newPassword - Das neu gewählte Passwort
|
||||
*
|
||||
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
|
||||
*/
|
||||
export async function resetPasswordUser(email, otpCode, newPassword) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/reset-password`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, otpCode, newPassword })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return { ok: response.ok, data };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Netzwerkfehler bei, Passwort-Reset:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nutzt das Refresh-Token, um ein neues Access-Token anzufordern (Auto-Login).
|
||||
*
|
||||
* @param {string} refreshToken
|
||||
*
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function refreshAccessTokenUser(refreshToken) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return { ok: response.ok, data };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Netzwerkfehler beim Token-Refresh:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Die Basis-URL für alle Sektor-spezifischen Anfragen an das Backend.
|
||||
* Auch hier zentralisiert, um zukünftige Server-Umzüge zu erleichtern.
|
||||
*/
|
||||
const API_BASE_URL = 'https://api-dev.void-genesis.de/api/sectors';
|
||||
|
||||
/**
|
||||
* Ruft die Liste aller verfügbaren Sektoren vom Backend ab.
|
||||
* Da diese Route geschützt ist, müssen wir das Access-Token mitsenden.
|
||||
*
|
||||
* @param {string} token - Das JWT Access Token des aktuell eingeloggten Nutzers
|
||||
*
|
||||
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
|
||||
*/
|
||||
export async function fetchSectorsList(token) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/list`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return { ok: response.ok, data };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Netzwerkfehler beim Sektor-Abruf:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Application } from "pixi.js";
|
||||
|
||||
export const app = new Application();
|
||||
|
||||
let manuelResizeCallback = null;
|
||||
|
||||
/**
|
||||
* Startet die PixiJS-Engine und hängt das Canvas in das HTML-Dokument ein.
|
||||
*
|
||||
* @param {string} containerId - Die HTML-ID des divs, in das PixiJS gerendert werden soll.
|
||||
*/
|
||||
export async function initPixi(containerId) {
|
||||
await app.init({
|
||||
resizeTo: window,
|
||||
backgroundAlpha: 0,
|
||||
antialias: true,
|
||||
resolution: window.devicePixelRatio || 1,
|
||||
autoDensity: true
|
||||
});
|
||||
document.getElementById(containerId).appendChild(app.canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Übernimmt die gesammte responsive Skalierungslogik für unsere PixiJS-Element.
|
||||
* Wir übergeben hier alle Container, die flexible auf Fenstergrößen reagieren müssen.
|
||||
*
|
||||
* @param {Object} refs - Ein Paket mit allen UI-Containern.
|
||||
*/
|
||||
export function setupResize(refs) {
|
||||
const {
|
||||
background, discordContainer, logoContainer, uiContainer,
|
||||
panelFrame, lobbyPixiContainer, footerBackground, footerPattern,
|
||||
footerBackgroundContainer
|
||||
} = refs;
|
||||
|
||||
function resize() {
|
||||
const screenWidth = window.innerWidth;
|
||||
const screenHeight = window.innerHeight;
|
||||
|
||||
if (background && background.texture) {
|
||||
const scaleX = screenWidth / background.texture.width;
|
||||
const scaleY = screenHeight / background.texture.height;
|
||||
const scale = Math.max(scaleX, scaleY);
|
||||
background.scale.set(scale);
|
||||
background.x = (screenWidth - background.width) / 2;
|
||||
background.y = (screenHeight - background.height) / 2;
|
||||
}
|
||||
|
||||
let panelScale = 1.0;
|
||||
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;
|
||||
|
||||
let lobbyScale = 1.0;
|
||||
if (screenWidth <= 1300) {
|
||||
lobbyScale = screenWidth / 1300;
|
||||
}
|
||||
if (screenHeight <= 750) {
|
||||
const hScale = screenHeight / 750;
|
||||
if (hScale < lobbyScale) lobbyScale = hScale;
|
||||
}
|
||||
|
||||
lobbyPixiContainer.scale.set(lobbyScale);
|
||||
lobbyPixiContainer.x = (screenWidth - (1240 * lobbyScale)) / 2;
|
||||
lobbyPixiContainer.y = (screenHeight - (650 * lobbyScale)) / 2;
|
||||
|
||||
const sectorHtmlLayer = document.getElementById('sector-browser-layer');
|
||||
if (sectorHtmlLayer) {
|
||||
sectorHtmlLayer.style.transform = `translate(-50%, -50%) scale(${lobbyScale})`;
|
||||
}
|
||||
|
||||
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;
|
||||
footerPattern.width = screenWidth;
|
||||
|
||||
footerBackgroundContainer.y = screenHeight - footerHeight;
|
||||
}
|
||||
|
||||
app.renderer.on('resize', resize);
|
||||
manuelResizeCallback = resize;
|
||||
resize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Löst eine manuelle Neuberechnung des Layout aus (wichtig nach dem Wechsel zur Lobby)
|
||||
*/
|
||||
export function triggerResize() {
|
||||
if (manuelResizeCallback) {
|
||||
manuelResizeCallback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Hier liegen alle Styles für den Anmelde- und Registrierungsprozess.
|
||||
* Das umfasst das Formular, die eingabefelder, buttons, Checkboxen und die Hilfslinks
|
||||
*/
|
||||
|
||||
#register-form {
|
||||
pointer-events: auto;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
height: 50px;
|
||||
left: 115px;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 15px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--color-text-main);
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
transition: text-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
#username { top: 135px; }
|
||||
#email { top: 200px; }
|
||||
#password { top: 265px; }
|
||||
|
||||
.input-group input::placeholder {
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
text-shadow: 0 0 8px var(--color-cyan-dark);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#btn-register {
|
||||
position: absolute;
|
||||
width: 370px;
|
||||
height: 55px;
|
||||
left: 65px;
|
||||
top: 390px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-text-main);
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: inherit;
|
||||
letter-spacing: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#btn-register:hover {
|
||||
background: transparent;
|
||||
color: var(--color-text-main);
|
||||
text-shadow: 0 0 15px var(--color-text-main), 0 0 30px var(--color-green-bright);
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
position: absolute;
|
||||
height: 40px;
|
||||
width: 180px;
|
||||
top: 60px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-text-inactive);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
#tab-login { left: 65px; }
|
||||
#tab-register { left: 255px; }
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--color-text-main);
|
||||
text-shadow: 0 0 8px var(--color-cyan-bright);
|
||||
}
|
||||
|
||||
.tab-btn:hover:not(.active) {
|
||||
color: var(--color-text-main);
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
position: absolute;
|
||||
left: 115px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#group-remember { top: 300px; }
|
||||
#group-agb { top: 325px; }
|
||||
|
||||
.checkbox-group label {
|
||||
color: var(--color-text-main);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1px solid var(--color-cyan-dark);
|
||||
background: var(--color-checkbox-bg);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"]:checked::after {
|
||||
content: '✔';
|
||||
position: absolute;
|
||||
color: var(--color-cyan-bright);
|
||||
font-size: 14px;
|
||||
top: -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 {
|
||||
position: absolute;
|
||||
top: 350px;
|
||||
left: 65px;
|
||||
width: 370px;
|
||||
height: 30px;
|
||||
color: var(--color-error);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#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;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: color 0.2s ease, text-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
#resend-otp-link:hover, #forgot-password-link:hover, #back-link:hover {
|
||||
color: var(--color-cyan-bright);
|
||||
text-shadow: 0 0 8px var(--color-cyan-bright);
|
||||
}
|
||||
|
||||
#forgot-password-link {
|
||||
position: absolute;
|
||||
top: 255px;
|
||||
left: 120px;
|
||||
color: var(--color-text-inactive);
|
||||
font-size: 14px;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: color 0.2s ease, text-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
#discord-link {
|
||||
position: absolute;
|
||||
left: 640px;
|
||||
top: 380px;
|
||||
width: 280px;
|
||||
height: 85px;
|
||||
background: transparent;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#back-link {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
left: 65px;
|
||||
color: var(--color-text-inactive);
|
||||
font-size: 15px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: color 0.2s ease, text-shadow 0.2s ease;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Beinhaltet das absolute Grundgerüst der Seite.
|
||||
* Reset-Regeln uns die Positionierung der Haupt-Container (Z-Index Ebenen).
|
||||
*/
|
||||
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg-page);
|
||||
font-family: var(--font-main);
|
||||
|
||||
/* NEU: Verbietet mobilen Browsern das eigenmächtige Vergrößern der Schrift */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
#game-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#pixi-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#ui-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
* Hier liegen alle Styles für die Lobb-Ansicht (nach dem Login)
|
||||
* Das umfasst die Top-Bar, Dropdowns, der Footer und die Sektor-Karten
|
||||
*/
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
.custom-dropdown {
|
||||
position: relative;
|
||||
font-family: var(--font-main);
|
||||
color: var(--color-text-main);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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; }
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
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;
|
||||
padding: 5px 0;
|
||||
min-width: 150px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdown-menu.show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.user-menu { min-width: 220px; }
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background: var(--color-cyan-dark);
|
||||
margin: 5px 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
#game-footer {
|
||||
position: absolute; bottom: 0; left: 0; width: 100%;
|
||||
height: 40px;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 0 40px; box-sizing: border-box;
|
||||
color: var(--color-text-inactive);
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
border-top: 1px solid var(--color-text-inactive);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.footer-left { flex: 1; text-align: left; }
|
||||
.footer-center { flex: 1; text-align: center; }
|
||||
.footer-right { flex: 1; text-align: right; }
|
||||
|
||||
#game-footer a {
|
||||
color: var(--color-text-inactive); text-decoration: none;
|
||||
transition: color 0.2s ease, text-shadow 0.2s ease;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#game-footer a:hover {
|
||||
color: var(--color-cyan-bright);
|
||||
text-shadow: 0 0 8px var(--color-cyan-bright);
|
||||
}
|
||||
|
||||
#sector-browser-layer {
|
||||
position: absolute;
|
||||
width: 1240px;
|
||||
height: 650px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#sector-tab-container {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
#sector-tab-container .tab-btn {
|
||||
position: relative;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
#tab-my-sectors { left: 45px; }
|
||||
#tab-new-sectors { left: 235px; }
|
||||
|
||||
#sector-list-container {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
width: 100%;
|
||||
height: 540px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 20px 40px;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 270px);
|
||||
gap: 20px 25px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.sector-card-link {
|
||||
display: block;
|
||||
width: 270px;
|
||||
height: 90px;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-main);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sector-card-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 15px 25px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sector-row-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.sector-details-icon {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-inactive);
|
||||
transition: transform 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.sector-card-link:hover .sector-details-icon {
|
||||
color: var(--color-cyan-bright);
|
||||
transform: scale(1.2) rotate(10deg);
|
||||
}
|
||||
|
||||
.sector-status {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
color: var(--color-green-bright);
|
||||
text-shadow: 0 0 8px rgba(0, 255, 0, 0.6);
|
||||
}
|
||||
|
||||
.status-maintenance {
|
||||
color: var(--color-error);
|
||||
text-shadow: 0 0 8px rgba(255, 68, 68, 0.6);
|
||||
}
|
||||
|
||||
.sector-row-mid {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-placeholder);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.sector-row-bottom {
|
||||
font-size: 12px;
|
||||
color: var(--color-cyan-bright);
|
||||
margin-top: 4px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#sector-list-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
#sector-list-container::-webkit-scrollbar-track {
|
||||
background: rgba(11, 25, 44, 0.3);
|
||||
}
|
||||
|
||||
#sector-list-container::-webkit-scrollbar-thumb {
|
||||
background: var(--color-cyan-dark);
|
||||
border-radius: 3px;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Hier definieren wir auschließlich globale Variable und laden Schriftarten.
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'UI Font';
|
||||
/* .otf Dateien bekommen das Format 'opentype', .ttf Dateien 'truetype' */
|
||||
src: url('../assets/fonts/WDXLLubrifontSC-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Logo Font';
|
||||
src: url('../assets/fonts/Quantico-Bold.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
:root {
|
||||
--color-bg-page: #000000;
|
||||
--color-text-main: #ffffff;
|
||||
--color-text-placeholder: rgba(255, 255, 255, 0.4);
|
||||
--color-text-inactive: rgba(255, 255, 255, 0.5);
|
||||
|
||||
--color-cyan-bright: #00f0ff;
|
||||
--color-cyan-dark: #00a0aa;
|
||||
--color-green-bright: #00ff00;
|
||||
--color-error: #ff4444;
|
||||
--color-checkbox-bg: rgba(11, 25, 44, 0.8);
|
||||
|
||||
--font-main: 'UI Font', sans-serif;
|
||||
}
|
||||
+448
-1381
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
import { Container, NineSliceSprite, Graphics, Sprite } from "pixi.js";
|
||||
import { LOGIN_COLORS } from "./login-scene";
|
||||
|
||||
/**
|
||||
* Baut das statische Grundgerüst des Sektor-Browsers (Hintergrund & Scroll-Maske)
|
||||
*/
|
||||
export function buildLobbyBase(lobbyPixiContainer, sheet) {
|
||||
const sectorBrowserBg = new NineSliceSprite({
|
||||
texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_11_Background'],
|
||||
leftWidth: 500,
|
||||
topHeight: 250,
|
||||
rightWidth: 500,
|
||||
bottomHeight: 250
|
||||
});
|
||||
sectorBrowserBg.width = 1240,
|
||||
sectorBrowserBg.height = 650;
|
||||
sectorBrowserBg.alpha = 0.8;
|
||||
sectorBrowserBg.tint = LOGIN_COLORS.PANEL_BG;
|
||||
lobbyPixiContainer.addChild(sectorBrowserBg);
|
||||
|
||||
const sectorPixiContainer = new Container();
|
||||
lobbyPixiContainer.addChild(sectorPixiContainer);
|
||||
|
||||
const scrollMask = new Graphics();
|
||||
scrollMask.rect(30, 80, 1180, 540);
|
||||
scrollMask.fill(0xffffff);
|
||||
lobbyPixiContainer.addChild(scrollMask);
|
||||
sectorPixiContainer.mask = scrollMask;
|
||||
|
||||
return sectorPixiContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen Sektor-Tab (Meine Sektoren / Neue Sektoren)
|
||||
*/
|
||||
export function createSectorTabPixi(sheet, x, y, tint, alpha) {
|
||||
const itemContainer = new Container();
|
||||
itemContainer.position.set(x, y);
|
||||
|
||||
const fill = new NineSliceSprite({
|
||||
texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Background_07'],
|
||||
leftWidth: 120,
|
||||
topHeight: 120,
|
||||
rightWidth: 120,
|
||||
bottomHeight: 120
|
||||
});
|
||||
fill.width = 180;
|
||||
fill.height = 40;
|
||||
fill.tint = tint;
|
||||
fill.alpha = alpha;
|
||||
itemContainer.addChild(fill);
|
||||
|
||||
const frame = new NineSliceSprite({
|
||||
texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Selected_10'],
|
||||
leftWidth: 100,
|
||||
topHeight: 120,
|
||||
rightWidth: 140,
|
||||
bottomHeight: 120
|
||||
});
|
||||
frame.width = 180;
|
||||
frame.height = 40;
|
||||
frame.alpha = 0.8;
|
||||
itemContainer.addChild(frame);
|
||||
|
||||
return itemContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert die PixiJS-Hintergründe und Icons für EINE Sektor-Karte.
|
||||
* Gibt Referenzen zurück, damit wir später Hover-Effekte im HTML-Link auslösen können.
|
||||
*/
|
||||
export function createSectorCardPixi(sheet, sectorPixiContainer, posX, posY) {
|
||||
const cardWidth = 270;
|
||||
const cardHeight = 90;
|
||||
|
||||
const bgSprite = new NineSliceSprite({
|
||||
texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_11_Background'],
|
||||
leftWidth: 200,
|
||||
topHeight: 200,
|
||||
rightWidth: 200,
|
||||
bottomHeight: 200
|
||||
});
|
||||
bgSprite.width = cardWidth;
|
||||
bgSprite.height = cardHeight;
|
||||
bgSprite.position.set(posX, posY);
|
||||
bgSprite.tint = 0x0b192c;
|
||||
bgSprite.alpha = 0.6;
|
||||
sectorPixiContainer.addChild(bgSprite);
|
||||
|
||||
const frameSprite = new NineSliceSprite({
|
||||
texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_11'],
|
||||
leftWidth: 200,
|
||||
topHeight: 200,
|
||||
rightWidth: 200,
|
||||
bottomHeight: 200
|
||||
});
|
||||
frameSprite.width = cardWidth;
|
||||
frameSprite.height = cardHeight;
|
||||
frameSprite.position.set(posX, posY);
|
||||
frameSprite.alpha = 0.8;
|
||||
sectorPixiContainer.addChild(frameSprite);
|
||||
|
||||
const iconDetails = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Message_01_Clean']);
|
||||
iconDetails.anchor.set(0.5);
|
||||
iconDetails.position.set(posX + 25, posY + 27);
|
||||
iconDetails.scale.set(0.08); iconDetails.tint = 0xaaaaaa;
|
||||
sectorPixiContainer.addChild(iconDetails);
|
||||
|
||||
const iconAge = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Settings_01_Clean']);
|
||||
iconAge.anchor.set(0.5);
|
||||
iconAge.position.set(posX + 130, posY + 26);
|
||||
iconAge.scale.set(0.08); iconAge.tint = 0xaaaaaa;
|
||||
sectorPixiContainer.addChild(iconAge);
|
||||
|
||||
const iconPlayers = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Multiplayer_01_Clean']);
|
||||
iconPlayers.anchor.set(0.5);
|
||||
iconPlayers.position.set(posX + 25, posY + 62);
|
||||
iconPlayers.scale.set(0.08); iconPlayers.tint = 0xaaaaaa;
|
||||
sectorPixiContainer.addChild(iconPlayers);
|
||||
|
||||
const iconPlanets = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Planet_02_Clean']);
|
||||
iconPlanets.anchor.set(0.5);
|
||||
iconPlanets.position.set(posX + 155, posY + 62);
|
||||
iconPlanets.scale.set(0.08); iconPlanets.tint = 0xaaaaaa;
|
||||
sectorPixiContainer.addChild(iconPlanets);
|
||||
|
||||
return { bgSprite, frameSprite, iconDetails, iconPlayers, iconPlanets, iconAge };
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import { Container, Sprite, NineSliceSprite, TilingSprite, Text } from "pixi.js";
|
||||
import { t } from "../i18n.js";
|
||||
|
||||
export const LOGIN_COLORS = {
|
||||
PANEL_BG: 0x0b192c,
|
||||
PATTERN: 0x00f0ff,
|
||||
TAB_ACTIVE: 0x00f0ff,
|
||||
TAB_INACTIVE: 0xaaaaaa,
|
||||
TAB_HOVER: 0xffffff,
|
||||
BTN_DEFAULT: 0x00aa00,
|
||||
BTN_HOVER: 0x00ff00,
|
||||
LOGO: 0x00f0ff
|
||||
};
|
||||
|
||||
/**
|
||||
* Baut alle visuellen PixiJS-Elemente für den Login-Screen.
|
||||
*
|
||||
* @param {Container} uiContainer - Der Haupt-Container für die Login-Ebene
|
||||
* @param {Object} sheet - Das geladene Sprite-Sheet
|
||||
*
|
||||
* @returns {Object} - Ein Paket mit allen wichtigen Element-Referenzen für die main.js
|
||||
*/
|
||||
export function buildLoginScene(uiContainer, sheet) {
|
||||
const frameInput = 'SPR_SciFiMenus_Menu_Item_Selected_19';
|
||||
const frameBtn = 'SPR_SciFiMenus_Menu_Item_Selected_10';
|
||||
const menuBgName = 'SPR_SciFiMenus_Menu_Item_Background_07';
|
||||
const panelBgName = 'SPR_SciFiMenus_Frame_Box_Large_02_Background';
|
||||
const patternBgName = 'SPR_SciFiMenus_Background_Hexagons_01';
|
||||
const panelFmName = 'SPR_SciFiMenus_Frame_Box_Large_02';
|
||||
|
||||
const iconUserName = 'ICON_SciFiMenus_Menu_Solo_01_Clean';
|
||||
const iconEmailName = 'ICON_SciFi_Map_Message';
|
||||
const iconLockName = 'ICON_SciFiMenus_Menu_Lock_Closed_01_Clean';
|
||||
const logoRingName = 'SPR_SciFiMenus_Ring_Medium_13';
|
||||
|
||||
const discordFrameName = 'SPR_SciFiMenus_Frame_Box_Large_12';
|
||||
const discordBgName = 'SPR_SciFiMenus_Frame_Box_Large_12_Background';
|
||||
const discordIconName = 'ICON_Social_Discord';
|
||||
|
||||
// --- Hilfsfunktion zum Erstellen der Input/Button-Rahmen ---
|
||||
function createUIItem(x, y, width, height, frameTextureName, iconTextureName, fillTint, fillAlpha) {
|
||||
const itemContainer = new Container();
|
||||
itemContainer.position.set(x, y);
|
||||
|
||||
const fill = new NineSliceSprite({
|
||||
texture: sheet.textures[menuBgName],
|
||||
leftWidth: 120,
|
||||
topHeight: 120,
|
||||
rightWidth: 120,
|
||||
bottomHeight: 120
|
||||
});
|
||||
|
||||
fill.width = width;
|
||||
fill.height = height;
|
||||
fill.tint = fillTint;
|
||||
fill.alpha = fillAlpha;
|
||||
itemContainer.addChild(fill);
|
||||
|
||||
const frame = new NineSliceSprite({
|
||||
texture: sheet.textures[frameTextureName],
|
||||
leftWidth: 100,
|
||||
topHeight: 120,
|
||||
rightWidth: 140,
|
||||
bottomHeight: 120
|
||||
});
|
||||
|
||||
frame.width = width;
|
||||
frame.height = height;
|
||||
frame.alpha = 0.8;
|
||||
itemContainer.addChild(frame);
|
||||
|
||||
if (iconTextureName) {
|
||||
const icon = new Sprite(sheet.textures[iconTextureName]);
|
||||
icon.position.set(-45, (height - (icon.height * 0.15)) / 2 - 5);
|
||||
icon.scale.set(0.15);
|
||||
itemContainer.addChild(icon);
|
||||
}
|
||||
|
||||
uiContainer.addChild(itemContainer);
|
||||
return { container: itemContainer, fill, frame };
|
||||
}
|
||||
|
||||
const panelBackground = new Sprite(sheet.textures[panelBgName]);
|
||||
panelBackground.tint = LOGIN_COLORS.PANEL_BG;
|
||||
panelBackground.width = 500;
|
||||
panelBackground.height = 520;
|
||||
panelBackground.position.set(0, -10);
|
||||
uiContainer.addChild(panelBackground);
|
||||
|
||||
const pattern = new TilingSprite({
|
||||
texture: sheet.textures[patternBgName],
|
||||
width: 430,
|
||||
height: 520,
|
||||
});
|
||||
pattern.tileScale.set(0.3);
|
||||
pattern.position.set(25, -10);
|
||||
pattern.alpha = 0.05;
|
||||
pattern.tint = LOGIN_COLORS.PATTERN;
|
||||
uiContainer.addChild(pattern);
|
||||
|
||||
const panelFrame = new NineSliceSprite({
|
||||
texture: sheet.textures[panelFmName],
|
||||
leftWidth: 510,
|
||||
topHeight: 510,
|
||||
rightWidth: 510,
|
||||
bottomHeight: 510
|
||||
});
|
||||
panelFrame.width = 500;
|
||||
panelFrame.height = 500;
|
||||
uiContainer.addChild(panelFrame);
|
||||
|
||||
// --- Logo ---
|
||||
const logoContainer = new Container();
|
||||
logoContainer.position.set(250, -120);
|
||||
|
||||
const logoRing = new Sprite(sheet.textures[logoRingName]);
|
||||
logoRing.anchor.set(0.5);
|
||||
logoRing.tint = LOGIN_COLORS.LOGO;
|
||||
logoRing.alpha = 1.0;
|
||||
logoRing.scale.set(0.2);
|
||||
logoContainer.addChild(logoRing);
|
||||
|
||||
const logoText = new Text({
|
||||
text: 'Void-Genesis',
|
||||
style: {
|
||||
fontFamily: 'Logo Font',
|
||||
fontSize: 50,
|
||||
fill: LOGIN_COLORS.LOGO,
|
||||
letterSpacing: 8,
|
||||
}
|
||||
});
|
||||
logoText.anchor.set(0.5);
|
||||
logoContainer.addChild(logoText);
|
||||
uiContainer.addChild(logoContainer);
|
||||
|
||||
const tabLogin = createUIItem(65, 60, 180, 40, frameBtn, null, LOGIN_COLORS.INACTIVE, 0.6);
|
||||
const tabRegister = createUIItem(255, 60, 180, 40, frameBtn, null, LOGIN_COLORS.ACTIVE, 0.4);
|
||||
const inputUser = createUIItem(115, 135, 300, 50, frameInput, iconUserName, LOGIN_COLORS.PANEL_BG, 0.8);
|
||||
const inputEmail = createUIItem(115, 200, 300, 50, frameInput, iconEmailName, LOGIN_COLORS.PANEL_BG, 0.8);
|
||||
const inputLock = createUIItem(115, 265, 300, 50, frameInput, iconLockName, LOGIN_COLORS.PANEL_BG, 0.8);
|
||||
const btnRegister = createUIItem(65, 390, 370, 55, frameBtn, null, LOGIN_COLORS.BTN_DEFAULT, 0.6);
|
||||
|
||||
const discordContainer = new Container();
|
||||
discordContainer.position.set(640, 380);
|
||||
|
||||
const discordBackground = new NineSliceSprite({
|
||||
texture: sheet.textures[discordBgName],
|
||||
leftWidth: 0,
|
||||
topHeight: 0,
|
||||
rightWidth: 0,
|
||||
bottomHeight: 0
|
||||
});
|
||||
discordBackground.tint = LOGIN_COLORS.PANEL_BG;
|
||||
discordBackground.width = 255;
|
||||
discordBackground.height = 75;
|
||||
discordContainer.addChild(discordBackground);
|
||||
|
||||
const discordPattern = new TilingSprite({
|
||||
texture: sheet.textures[patternBgName],
|
||||
width: 255,
|
||||
height: 75,
|
||||
});
|
||||
discordPattern.tileScale.set(0.3);
|
||||
discordPattern.tint = LOGIN_COLORS.PATTERN;
|
||||
discordPattern.alpha = 0.05;
|
||||
discordContainer.addChild(discordPattern);
|
||||
|
||||
const discordFrame = new NineSliceSprite({
|
||||
texture: sheet.textures[discordFrameName],
|
||||
leftWidth: 0,
|
||||
topHeight: 0,
|
||||
rightWidth: 0,
|
||||
bottomHeight: 0
|
||||
});
|
||||
discordFrame.width = 280;
|
||||
discordFrame.height = 85;
|
||||
discordFrame.position.set(-15, -10);
|
||||
discordContainer.addChild(discordFrame);
|
||||
|
||||
const discordIcon = new Sprite(sheet.textures[discordIconName]);
|
||||
discordIcon.position.set(10, 15);
|
||||
discordIcon.scale.set(0.15);
|
||||
discordIcon.tint = LOGIN_COLORS.TAB_INACTIVE;
|
||||
discordContainer.addChild(discordIcon);
|
||||
|
||||
const discordText = new Text({
|
||||
text: t('discord_text'),
|
||||
style: {
|
||||
fontFamily: 'UI Font',
|
||||
fontSize: 20,
|
||||
fill: LOGIN_COLORS.TAB_INACTIVE,
|
||||
wordWrap: false,
|
||||
wordWrapWidth: 120
|
||||
}
|
||||
});
|
||||
discordText.position.set(60, 20);
|
||||
discordContainer.addChild(discordText);
|
||||
uiContainer.addChild(discordContainer);
|
||||
|
||||
return {
|
||||
tabLogin, tabRegister, inputUser, inputEmail, inputLock, btnRegister,
|
||||
discordIcon, discordText, logoContainer, discordContainer, panelFrame
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { setLanguage, currentLang, t } from "../i18n.js";
|
||||
|
||||
/**
|
||||
* Sucht alle HTML-Elemente mir dem Attribut 'data-i18n'
|
||||
* und aktualisiert ihren Textinhalt basierend auf der aktuellen Sprache.
|
||||
*/
|
||||
export function updateTranslations() {
|
||||
const elements = document.querySelectorAll('[data-i18n]');
|
||||
elements.forEach(el => {
|
||||
const key = el.getAttribute('data-i18n');
|
||||
el.textContent = t(key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert alle Dropdowns und die Sprachauswahl.
|
||||
*
|
||||
* @param {Function} updatePlaceholdersCallback - Wird aufgrufen, wenn
|
||||
* die Sprache gewechselt wurde,
|
||||
* um Platzhalter dynamisch anzupassen.
|
||||
*/
|
||||
export function initDropdowns(updatePlaceholdersCallback) {
|
||||
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();
|
||||
const isShowing = langMenu.classList.contains('show');
|
||||
closeAllDropdowns();
|
||||
if (!isShowing) langMenu.classList.add('show');
|
||||
};
|
||||
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();
|
||||
|
||||
if (updatePlaceholdersCallback) {
|
||||
updatePlaceholdersCallback();
|
||||
}
|
||||
|
||||
closeAllDropdowns();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
import { t } from "../i18n.js";
|
||||
|
||||
// Speichert den Zustand zentral, damit die man.js darauf zugreifen kann
|
||||
export const formState = {
|
||||
isLoginMode: false,
|
||||
isForgetPasswordMode: false,
|
||||
isResetPasswordMode: false,
|
||||
isOtpMode: false,
|
||||
pendingVerificationEmail: '',
|
||||
resetEmail: '',
|
||||
cooldownTimer: null
|
||||
};
|
||||
|
||||
// Hier wird die PixiJS-Referenz gespeichertm die wir beim Initialisieren übergeben bekommen.
|
||||
let p = {};
|
||||
|
||||
/**
|
||||
* Initialisiert die Formular-Steuerunf und regeistiert alle Event-Listener.
|
||||
*
|
||||
* @param {Object} pixiRefs - Ein Objekt mit allen zu steuernden PixiJS-Elementen und Farben.
|
||||
*/
|
||||
export function initForms(pixiRefs) {
|
||||
p = pixiRefs;
|
||||
|
||||
const htmlTabLogin = document.getElementById('tab-login');
|
||||
const htmlTabRegistert = document.getElementById('tab-register');
|
||||
const htmlButton = document.getElementById('btn-register');
|
||||
const htmlDiscord = document.getElementById('discord-link');
|
||||
|
||||
// --- Hover Effekte synchronisieren (HTML -> PixiJS) ---
|
||||
htmlTabLogin.addEventListener('mouseenter', () => {
|
||||
if (!formState.isLoginMode) {
|
||||
p.tabLogin.fill.tint = p.COLOR_TAB_HOVER;
|
||||
p.tabLogin.fill.alpha = 0.8;
|
||||
}
|
||||
});
|
||||
htmlTabLogin.addEventListener('mouseleave', () => {
|
||||
if (!formState.isLoginMode) {
|
||||
p.tabLogin.fill.tint = p.COLOR_TAB_INACTIVE;
|
||||
p.tabLogin.fill.alpha = 0.4;
|
||||
}
|
||||
});
|
||||
|
||||
htmlTabRegistert.addEventListener('mouseenter', () => {
|
||||
if (formState.isLoginMode) {
|
||||
p.tabRegister.fill.tint = p.COLOR_TAB_HOVER;
|
||||
p.tabRegister.fill.alpha = 0.8;
|
||||
}
|
||||
});
|
||||
htmlTabRegistert.addEventListener('mouseleave', () => {
|
||||
if (formState.isLoginMode) {
|
||||
p.tabRegister.fill.tint = p.COLOR_TAB_INACTIVE;
|
||||
p.tabRegister.fill.alpha = 0.4;
|
||||
}
|
||||
});
|
||||
|
||||
htmlButton.addEventListener('mouseenter', () => {
|
||||
p.btnRegister.fill.tint = p.COLOR_BTN_HOVER;
|
||||
p.btnRegister.fill.alpha = 1.0;
|
||||
});
|
||||
htmlButton.addEventListener('mouseleave', () => {
|
||||
p.btnRegister.fill.tint = p.COLOR_BTN_DEFAULT;
|
||||
p.btnRegister.fill.alpha = 0.6;
|
||||
});
|
||||
|
||||
if (htmlDiscord) {
|
||||
htmlDiscord.addEventListener('mouseenter', () => {
|
||||
p.discordIcon.tint = p.COLOR_TAB_HOVER;
|
||||
p.discordText.style.fill = p.COLOR_TAB_HOVER;
|
||||
});
|
||||
htmlDiscord.addEventListener('mouseleave', () => {
|
||||
p.discordIcon.tint = p.COLOR_TAB_INACTIVE;
|
||||
p.discordText.style.fill = p.COLOR_TAB_INACTIVE;
|
||||
});
|
||||
}
|
||||
|
||||
htmlTabLogin.addEventListener('click', setLoginMode);
|
||||
htmlTabRegistert.addEventListener('click', setRegisterMode);
|
||||
|
||||
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();
|
||||
formState.pendingVerificationEmail = '';
|
||||
formState.resetEmail = '';
|
||||
formState.isOtpMode = false;
|
||||
formState.isForgotPasswordMode = false;
|
||||
formState.isResetPasswordMode = false;
|
||||
formState.isLoginMode = false;
|
||||
setLoginMode();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Leert die Formulardaten bei einem TabWechsel
|
||||
*/
|
||||
export 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';
|
||||
p.tabLogin.container.visible = true;
|
||||
p.tabRegister.container.visible = true;
|
||||
|
||||
document.getElementById('username').style.display = 'block';
|
||||
p.inputUser.container.visible = true;
|
||||
|
||||
const pwField = document.getElementById('password');
|
||||
pwField.style.display = 'block';
|
||||
pwField.type = 'password';
|
||||
p.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 (formState.cooldownTimer) {
|
||||
clearInterval(formState.cooldownTimer);
|
||||
formState.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)';
|
||||
}
|
||||
|
||||
export function setLoginMode() {
|
||||
if (formState.isLoginMode) return;
|
||||
formState.isLoginMode = true;
|
||||
clearForm();
|
||||
|
||||
p.tabLogin.fill.tint = p.COLOR_TAB_ACTIVE;
|
||||
p.tabLogin.fill.alpha = 0.6;
|
||||
p.tabRegister.fill.tint = p.COLOR_TAB_INACTIVE;
|
||||
p.tabRegister.fill.alpha = 0.4;
|
||||
|
||||
document.getElementById('tab-login').classList.add('active');
|
||||
document.getElementById('tab-register').classList.remove('active');
|
||||
|
||||
p.inputEmail.container.visible = false;
|
||||
document.getElementById('group-email').style.display = 'none';
|
||||
|
||||
p.inputLock.container.y = 200;
|
||||
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-agb').style.display = 'none';
|
||||
document.getElementById('agb').required = false;
|
||||
|
||||
document.getElementById('btn-register').textContent = t('btn_login');
|
||||
document.getElementById('username').placeholder = t('ph_username_login');
|
||||
document.getElementById('password').placeholder = t('ph_password');
|
||||
document.getElementById('msg-box').textContent = '';
|
||||
}
|
||||
|
||||
export function setRegisterMode() {
|
||||
if (!formState.isLoginMode) return;
|
||||
formState.isLoginMode = false;
|
||||
clearForm();
|
||||
|
||||
p.tabRegister.fill.tint = p.COLOR_TAB_ACTIVE;
|
||||
p.tabRegister.fill.alpha = 0.6;
|
||||
p.tabLogin.fill.tint = p.COLOR_TAB_INACTIVE;
|
||||
p.tabLogin.fill.alpha = 0.4;
|
||||
|
||||
document.getElementById('tab-register').classList.add('active');
|
||||
document.getElementById('tab-login').classList.remove('active');
|
||||
|
||||
p.inputEmail.container.visible = true;
|
||||
document.getElementById('group-email').style.display = 'block';
|
||||
|
||||
p.inputLock.container.y = 265;
|
||||
document.getElementById('password').style.top = '265px';
|
||||
|
||||
document.getElementById('email').required = true;
|
||||
|
||||
document.getElementById('group-remember').style.display = 'none';
|
||||
document.getElementById('group-agb').style.display = 'flex';
|
||||
document.getElementById('agb').required = true;
|
||||
|
||||
document.getElementById('btn-register').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 = '';
|
||||
}
|
||||
|
||||
export function setOtpMode(email) {
|
||||
formState.isOtpMode = true;
|
||||
formState.pendingVerificationEmail = email;
|
||||
clearForm();
|
||||
|
||||
document.getElementById('tab-container').style.display = 'none';
|
||||
p.tabLogin.container.visible = false;
|
||||
p.tabRegister.container.visible = false;
|
||||
|
||||
document.getElementById('username').style.display = 'none';
|
||||
document.getElementById('username').required = false;
|
||||
p.inputUser.container.visible = false;
|
||||
|
||||
document.getElementById('group-email').style.display = 'none';
|
||||
document.getElementById('email').required = false;
|
||||
p.inputEmail.container.visible = false;
|
||||
const instructionEl = document.getElementById('otp-instruction');
|
||||
instructionEl.style.display = 'block';
|
||||
instructionEl.textContent = t('txt_otp_instruction');
|
||||
|
||||
document.getElementById('resend-otp-link').style.display = 'block';
|
||||
|
||||
p.inputLock.container.y = 200;
|
||||
document.getElementById('password').style.top = '200px';
|
||||
document.getElementById('password').type = 'text';
|
||||
document.getElementById('password').placeholder = t('ph_otp');
|
||||
document.getElementById('password').value = '';
|
||||
|
||||
document.getElementById('group-agb').style.display = 'none';
|
||||
document.getElementById('agb').required = false;
|
||||
document.getElementById('group-remember').style.display = 'none';
|
||||
|
||||
document.getElementById('msg-box').textContent = '';
|
||||
document.getElementById('btn-register').textContent = t('btn_verify');
|
||||
document.getElementById('back-link').style.display = 'block';
|
||||
}
|
||||
|
||||
export function setForgotPasswordMode() {
|
||||
formState.isLoginMode = false;
|
||||
formState.isOtpMode = false;
|
||||
formState.isForgotPasswordMode = true;
|
||||
formState.isResetPasswordMode = false;
|
||||
clearForm();
|
||||
|
||||
document.getElementById('tab-container').style.display = 'none';
|
||||
p.tabLogin.container.visible = false;
|
||||
p.tabRegister.container.visible = false;
|
||||
|
||||
document.getElementById('username').style.display = 'none';
|
||||
document.getElementById('username').required = false;
|
||||
p.inputUser.container.visible = false;
|
||||
|
||||
document.getElementById('password').style.display = 'none';
|
||||
document.getElementById('password').required = false;
|
||||
p.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';
|
||||
p.inputEmail.container.visible = true;
|
||||
p.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('btn-register').textContent = t('btn_reset_request');
|
||||
document.getElementById('back-link').style.display = 'block';
|
||||
document.getElementById('msg-box').textContent = '';
|
||||
}
|
||||
|
||||
export function setResetPasswordMode(email) {
|
||||
formState.isForgotPasswordMode = false;
|
||||
formState.isResetPasswordMode = true;
|
||||
formState.resetEmail = email;
|
||||
clearForm();
|
||||
|
||||
document.getElementById('tab-container').style.display = 'none';
|
||||
p.tabLogin.container.visible = false;
|
||||
p.tabRegister.container.visible = false;
|
||||
|
||||
document.getElementById('username').style.display = 'none';
|
||||
document.getElementById('username').required = false;
|
||||
p.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';
|
||||
p.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';
|
||||
p.inputLock.container.visible = true;
|
||||
p.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';
|
||||
document.getElementById('btn-register').textContent = t('btn_reset_password');
|
||||
document.getElementById('msg-box').textContent = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
export function startVisualCountdown(linkElement, orginalTextKey, durationSconds) {
|
||||
if (formState.cooldownTimer) clearInterval(formState.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();
|
||||
|
||||
formState.cooldownTimer = setInterval(() => {
|
||||
remaining--;
|
||||
|
||||
if (remaining <= 0) {
|
||||
clearInterval(formState.cooldownTimer);
|
||||
formState.cooldownTimer = null;
|
||||
linkElement.style.pointerEvents = 'auto';
|
||||
linkElement.style.opacity = '1.0';
|
||||
linkElement.textContent = t(orginalTextKey);
|
||||
} else {
|
||||
updateText();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
@@ -1,524 +1,8 @@
|
||||
/* --- LOKALE SCHRIFTART LADEN --- */
|
||||
@font-face {
|
||||
font-family: 'UI Font';
|
||||
/* .otf Dateien bekommen das Format 'opentype', .ttf Dateien 'truetype' */
|
||||
src: url('src/assets/fonts/WDXLLubrifontSC-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@import url('./src/css/variables.css');
|
||||
@import url('./src/css/layout.css');
|
||||
@import url('./src/css/auth.css');
|
||||
@import url('./src/css/lobby.css');
|
||||
|
||||
/* 2. Die exklusive Schrift für das Void-Genesis Logo */
|
||||
@font-face {
|
||||
font-family: 'Logo Font';
|
||||
src: url('src/assets/fonts/Quantico-Bold.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* --- FARB- & FONT-VARIABLEN (CSS) --- */
|
||||
:root {
|
||||
/* ... deine bisherigen Farb-Variablen ... */
|
||||
--color-bg-page: #000000;
|
||||
--color-text-main: #ffffff;
|
||||
--color-text-placeholder: rgba(255, 255, 255, 0.4);
|
||||
--color-text-inactive: rgba(255, 255, 255, 0.5);
|
||||
|
||||
--color-cyan-bright: #00f0ff;
|
||||
--color-cyan-dark: #00a0aa;
|
||||
--color-green-bright: #00ff00;
|
||||
--color-error: #ff4444;
|
||||
|
||||
--color-checkbox-bg: rgba(11, 25, 44, 0.8);
|
||||
|
||||
/* NEU: Wir definieren die Schriftart zentral */
|
||||
--font-main: 'UI Font', sans-serif;
|
||||
}
|
||||
|
||||
/* Reset von Standard-Abständen */
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg-page);
|
||||
font-family: var(--font-main);
|
||||
|
||||
/* NEU: Verbietet mobilen Browsern das eigenmächtige Vergrößern der Schrift */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
#game-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#pixi-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#ui-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#register-form {
|
||||
pointer-events: auto;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
height: 50px;
|
||||
left: 115px;
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: 10px 15px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--color-text-main);
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
transition: text-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
#username {
|
||||
top: 135px;
|
||||
}
|
||||
|
||||
#email {
|
||||
top: 200px;
|
||||
}
|
||||
|
||||
#password {
|
||||
top: 265px;
|
||||
}
|
||||
|
||||
.input-group input::placeholder {
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
text-shadow: 0 0 8px var(--color-cyan-dark);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#btn-register {
|
||||
position: absolute;
|
||||
width: 370px;
|
||||
height: 55px;
|
||||
left: 65px;
|
||||
top: 390px;
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-text-main);
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: inherit;
|
||||
/*text-transform: uppercase;*/
|
||||
letter-spacing: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#btn-register:hover {
|
||||
background: transparent;
|
||||
color: var(--color-text-main);
|
||||
text-shadow: 0 0 15px var(--color-text-main), 0 0 30px var(--color-green-bright);
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
position: absolute;
|
||||
height: 40px;
|
||||
width: 180px;
|
||||
top: 60px;
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-text-inactive);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
#tab-login {
|
||||
left: 65px;
|
||||
}
|
||||
|
||||
#tab-register {
|
||||
left: 255px;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--color-text-main);
|
||||
text-shadow: 0 0 8px var(--color-cyan-bright);
|
||||
}
|
||||
|
||||
.tab-btn:hover:not(.active) {
|
||||
color: var(--color-text-main);
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
position: absolute;
|
||||
left: 115px;
|
||||
/*top: 275px; */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
color: var(--color-text-main);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1px solid var(--color-cyan-dark);
|
||||
background: var(--color-checkbox-bg);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"]:checked::after {
|
||||
content: '✔';
|
||||
position: absolute;
|
||||
color: var(--color-cyan-bright);
|
||||
font-size: 14px;
|
||||
top: -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 {
|
||||
position: absolute;
|
||||
top: 350px;
|
||||
left: 65px;
|
||||
width: 370px;
|
||||
height: 30px;
|
||||
color: var(--color-error);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: 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 --- */
|
||||
#discord-link {
|
||||
position: absolute;
|
||||
|
||||
/* WICHTIG: Trage hier exakt die gleichen Werte ein,
|
||||
die du in der main.js beim 'discordContainer.position.set(X, Y)' hast! */
|
||||
left: 640px; /* Positiver Wert, da es jetzt rechts vom 500px Panel liegt */
|
||||
top: 380px; /* Y-Koordinate deines Panels */
|
||||
|
||||
/* Die Größe deines PixiJS Discord-Panels */
|
||||
width: 280px;
|
||||
height: 85px;
|
||||
|
||||
/* Komplett unsichtbar machen, wir wollen nur das Pixi-Bild darunter sehen */
|
||||
background: transparent;
|
||||
text-decoration: none;
|
||||
|
||||
/* Sorgt dafür, dass die Maus zur Klick-Hand wird */
|
||||
cursor: pointer;
|
||||
/* Aktiviert die Klickbarkeit für diese Schicht */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* --- NEU: FOOTER STYLING --- */
|
||||
#game-footer {
|
||||
position: absolute; bottom: 0; left: 0; width: 100%;
|
||||
/* Höhe des Footers (wird auch im Pixi Kachel-Sprite genutzt) */
|
||||
height: 40px;
|
||||
|
||||
/* Z-Index hochsetzen, damit der Text über dem Pixi-Hintergrund liegt */
|
||||
z-index: 20;
|
||||
/* pointer-events: none deaktivieren, damit wir den Impressum-Link anklicken können! */
|
||||
pointer-events: none;
|
||||
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 0 40px; box-sizing: border-box;
|
||||
|
||||
color: var(--color-text-inactive);
|
||||
font-size: 12px;
|
||||
/* Wir nutzen nicht mehr 'Courier New', sondern unsere UI-Schrift */
|
||||
font-family: inherit;
|
||||
|
||||
/* GRAFISCHE ABSETZUNG: Wir fügen eine leuchtende Cyan-Neon-Linie oben hinzu. */
|
||||
border-top: 1px solid var(--color-text-inactive);
|
||||
/* Und nutzen den Pixi Kachel-Hintergrund, um den Sci-Fi Look zu übernehmen. */
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Flexbox-Verteilung für die drei Footer-Bereiche */
|
||||
.footer-left { flex: 1; text-align: left; }
|
||||
.footer-center { flex: 1; text-align: center; }
|
||||
.footer-right { flex: 1; text-align: right; }
|
||||
|
||||
#game-footer a {
|
||||
color: var(--color-text-inactive); text-decoration: none;
|
||||
transition: color 0.2s ease, text-shadow 0.2s ease;
|
||||
/* Wir reaktivieren Klicks für den Link */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#game-footer a:hover {
|
||||
color: 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 --- */
|
||||
@@ -610,150 +94,3 @@ body, html {
|
||||
font-size: 10px; /* Schrift minimal verkleinern */
|
||||
}
|
||||
}
|
||||
|
||||
/* Haupt-Container für die Sektor-Auswahl */
|
||||
#sector-browser-layer {
|
||||
position: absolute;
|
||||
width: 1240px; /* Breit genug für 4 Karten nebeneinander */
|
||||
height: 650px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
/* Der Scale wird jetzt dynamisch von JS gesteuert! */
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Positionierung der Sektor-Tabs */
|
||||
#sector-tab-container {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
|
||||
/* Flexbox übernimmt das automatische Zentrieren */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* Wir überschreiben die festen Koordinaten vom Login-Screen */
|
||||
#sector-tab-container .tab-btn {
|
||||
position: relative;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
#tab-my-sectors { left: 45px; }
|
||||
#tab-new-sectors { left: 235px; }
|
||||
|
||||
/* Grid-Bereich für die Sektor-Karten (Scrollbar bei Überlauf) */
|
||||
#sector-list-container {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
width: 100%;
|
||||
height: 540px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden; /* NEU: Verhindert horizontales Scrollen strikt */
|
||||
|
||||
/* KORREKTUR: Von 45px auf 40px reduziert, damit wir bei exakt 1235px landen */
|
||||
padding: 20px 40px;
|
||||
box-sizing: border-box;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 270px);
|
||||
gap: 20px 25px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Styling der einzelnen Sektor-Karte als vollwertiger Link */
|
||||
.sector-card-link {
|
||||
display: block;
|
||||
width: 270px;
|
||||
height: 90px;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-main);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Flex-Layout für den Inhalt der Karte */
|
||||
.sector-card-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 15px 25px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Zeile 1: Name, Lupe und Status */
|
||||
.sector-row-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Das Unicode-Lupen-Symbol mit speziellem Hover-Effekt */
|
||||
.sector-details-icon {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-inactive);
|
||||
transition: transform 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Bei Hover rotiert und leuchtet die Lupe leicht auf */
|
||||
.sector-card-link:hover .sector-details-icon {
|
||||
color: var(--color-cyan-bright);
|
||||
transform: scale(1.2) rotate(10deg);
|
||||
}
|
||||
|
||||
/* Status-Anzeige (Online / Wartung) */
|
||||
.sector-status {
|
||||
margin-left: auto; /* Schiebt den Status ganz nach rechts */
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.status-online {
|
||||
color: var(--color-green-bright);
|
||||
text-shadow: 0 0 8px rgba(0, 255, 0, 0.6);
|
||||
}
|
||||
.status-maintenance {
|
||||
color: var(--color-error);
|
||||
text-shadow: 0 0 8px rgba(255, 68, 68, 0.6);
|
||||
}
|
||||
|
||||
/* Zeile 2: Spieleranzahl & Planetenanzahl */
|
||||
.sector-row-mid {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-placeholder);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Zeile 3: Lokaler Username (falls vorhanden) */
|
||||
.sector-row-bottom {
|
||||
font-size: 12px;
|
||||
color: var(--color-cyan-bright);
|
||||
margin-top: 4px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar für die Sektor-Liste im Sci-Fi Look */
|
||||
#sector-list-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px; /* NEU: Falls je wieder ein x-Scrollbar auftaucht, ist er wenigstens nur 6px hoch */
|
||||
}
|
||||
#sector-list-container::-webkit-scrollbar-track {
|
||||
background: rgba(11, 25, 44, 0.3);
|
||||
}
|
||||
#sector-list-container::-webkit-scrollbar-thumb {
|
||||
background: var(--color-cyan-dark);
|
||||
border-radius: 3px;
|
||||
}
|
||||
Reference in New Issue
Block a user