Merge pull request #185 from duckietm/Dev

Dev
This commit is contained in:
DuckieTM
2026-06-02 12:05:00 +02:00
committed by GitHub
8 changed files with 315 additions and 138 deletions
@@ -705,4 +705,71 @@
'chatcmd.client.ejectall': 'Eject all furni', 'chatcmd.client.ejectall': 'Eject all furni',
'chatcmd.client.settings': 'Room settings', 'chatcmd.client.settings': 'Room settings',
'chatcmd.client.info': 'Client info', 'chatcmd.client.info': 'Client info',
// ------------------------------------------------------------------------
// Me-menu settings + User account settings window
// ------------------------------------------------------------------------
'usersettings.tab.general': "General",
'usersettings.tab.themes': "Themes",
'memenu.settings.other.place.multiple.objects': "Place multiple objects",
'memenu.settings.other.skip.purchase.confirmation': "Skip purchase confirmation",
'memenu.settings.other.enable.chat.window': "Enable chat window",
'memenu.settings.other.catalog.classic.style': "Catalog: classic style",
'usersettings.open.title': "User settings",
'usersettings.open.subtitle': "Password & account",
'usersettings.themes.custom': "Custom theme",
'usersettings.themes.default_option': "Default (no theme)",
'usersettings.themes.active_pieces': "Active pieces",
'usersettings.themes.invalid': "Theme invalid or unreachable — using the default.",
'usersettings.themes.none': "No themes available. Add a folder in custom-themes/ on the server.",
'usersettings.title': "User Settings",
'usersettings.account.label': "My account",
'usersettings.guest': "Guest",
'usersettings.subtitle': "Manage your account and security",
'usersettings.menu.section': "Account",
'usersettings.menu.password.title': "Reset password",
'usersettings.menu.password.desc': "Change the password used to log in.",
'usersettings.menu.email.title': "Change email",
'usersettings.menu.email.desc': "Update the email address on your account.",
'usersettings.menu.username.title': "Change username",
'usersettings.menu.username.desc': "Pick a new name. You'll need to log in again.",
'usersettings.menu.soon.title': "More coming soon",
'usersettings.menu.soon.desc': "Two-factor authentication and more.",
'usersettings.password.hint': "Use at least %count% characters. Mix upper & lowercase, numbers and symbols for a stronger password.",
'usersettings.email.hint': "For security we ask you to confirm your current password before changing the email on your account.",
'usersettings.username.hint': "Renaming will log you out and you can only rename again after 30 days. Make sure your friends know your new name!",
'usersettings.field.current_password': "Current password",
'usersettings.field.new_password': "New password",
'usersettings.field.retype_password': "Retype new password",
'usersettings.field.new_email': "New email address",
'usersettings.field.new_username': "New username",
'usersettings.username.rules': "%min%-%max% characters. Letters, numbers, dot, underscore and dash only.",
'usersettings.strength.weak': "Weak",
'usersettings.strength.fair': "Fair",
'usersettings.strength.good': "Good",
'usersettings.strength.strong': "Strong",
'usersettings.aria.show_password': "Show password",
'usersettings.aria.hide_password': "Hide password",
'usersettings.btn.cancel': "Cancel",
'usersettings.btn.saving': "Saving…",
'usersettings.btn.save_password': "Save password",
'usersettings.btn.save_email': "Save email",
'usersettings.btn.renaming': "Renaming…",
'usersettings.btn.rename': "Rename me",
'usersettings.error.fields_required': "All fields are required.",
'usersettings.error.password_min': "Password must be at least %count% characters.",
'usersettings.error.password_long': "Password is too long.",
'usersettings.error.password_mismatch': "New passwords do not match.",
'usersettings.error.password_same': "New password must be different from the current password.",
'usersettings.error.not_authenticated': "You are not authenticated. Please log in again.",
'usersettings.error.network': "Could not reach the server. Please try again.",
'usersettings.error.request_failed': "Request failed (%status%).",
'usersettings.error.email_long': "Email address is too long.",
'usersettings.error.email_invalid': "Please enter a valid email address.",
'usersettings.error.username_length': "Username must be between %min% and %max% characters.",
'usersettings.error.username_invalid': "Username may only contain letters, numbers, dot, underscore and dash.",
'usersettings.error.username_same': "New username must be different from the current one.",
'usersettings.success.password': "Password updated successfully.",
'usersettings.success.email': "Email updated successfully.",
'usersettings.success.username': "Username updated. Please log in again with your new name.",
} }
@@ -705,4 +705,71 @@
'chatcmd.client.ejectall': 'Rimuovi tutti gli arredi', 'chatcmd.client.ejectall': 'Rimuovi tutti gli arredi',
'chatcmd.client.settings': 'Impostazioni stanza', 'chatcmd.client.settings': 'Impostazioni stanza',
'chatcmd.client.info': 'Info client', 'chatcmd.client.info': 'Info client',
// ------------------------------------------------------------------------
// Me-menu settings + User account settings window
// ------------------------------------------------------------------------
'usersettings.tab.general': "Generale",
'usersettings.tab.themes': "Temi",
'memenu.settings.other.place.multiple.objects': "Posiziona più oggetti",
'memenu.settings.other.skip.purchase.confirmation': "Salta la conferma d'acquisto",
'memenu.settings.other.enable.chat.window': "Abilita finestra chat",
'memenu.settings.other.catalog.classic.style': "Catalogo: stile classico",
'usersettings.open.title': "Impostazioni utente",
'usersettings.open.subtitle': "Password e account",
'usersettings.themes.custom': "Tema personalizzato",
'usersettings.themes.default_option': "Predefinito (nessun tema)",
'usersettings.themes.active_pieces': "Elementi attivi",
'usersettings.themes.invalid': "Tema non valido o non raggiungibile — uso il predefinito.",
'usersettings.themes.none': "Nessun tema disponibile. Aggiungi una cartella in custom-themes/ sul server.",
'usersettings.title': "Impostazioni utente",
'usersettings.account.label': "Il mio account",
'usersettings.guest': "Ospite",
'usersettings.subtitle': "Gestisci il tuo account e la sicurezza",
'usersettings.menu.section': "Account",
'usersettings.menu.password.title': "Reimposta password",
'usersettings.menu.password.desc': "Cambia la password che usi per accedere.",
'usersettings.menu.email.title': "Cambia email",
'usersettings.menu.email.desc': "Aggiorna l'indirizzo email del tuo account.",
'usersettings.menu.username.title': "Cambia nome utente",
'usersettings.menu.username.desc': "Scegli un nuovo nome. Dovrai accedere di nuovo.",
'usersettings.menu.soon.title': "Altro in arrivo",
'usersettings.menu.soon.desc': "Autenticazione a due fattori e altro.",
'usersettings.password.hint': "Usa almeno %count% caratteri. Combina maiuscole e minuscole, numeri e simboli per una password più sicura.",
'usersettings.email.hint': "Per sicurezza ti chiediamo di confermare la password attuale prima di cambiare l'email del tuo account.",
'usersettings.username.hint': "Cambiando nome verrai disconnesso e potrai rinominarti di nuovo solo dopo 30 giorni. Assicurati che i tuoi amici conoscano il tuo nuovo nome!",
'usersettings.field.current_password': "Password attuale",
'usersettings.field.new_password': "Nuova password",
'usersettings.field.retype_password': "Ripeti la nuova password",
'usersettings.field.new_email': "Nuovo indirizzo email",
'usersettings.field.new_username': "Nuovo nome utente",
'usersettings.username.rules': "%min%-%max% caratteri. Solo lettere, numeri, punto, trattino basso e trattino.",
'usersettings.strength.weak': "Debole",
'usersettings.strength.fair': "Discreta",
'usersettings.strength.good': "Buona",
'usersettings.strength.strong': "Forte",
'usersettings.aria.show_password': "Mostra password",
'usersettings.aria.hide_password': "Nascondi password",
'usersettings.btn.cancel': "Annulla",
'usersettings.btn.saving': "Salvataggio…",
'usersettings.btn.save_password': "Salva password",
'usersettings.btn.save_email': "Salva email",
'usersettings.btn.renaming': "Rinomina…",
'usersettings.btn.rename': "Rinominami",
'usersettings.error.fields_required': "Tutti i campi sono obbligatori.",
'usersettings.error.password_min': "La password deve contenere almeno %count% caratteri.",
'usersettings.error.password_long': "La password è troppo lunga.",
'usersettings.error.password_mismatch': "Le nuove password non corrispondono.",
'usersettings.error.password_same': "La nuova password deve essere diversa da quella attuale.",
'usersettings.error.not_authenticated': "Non sei autenticato. Effettua di nuovo l'accesso.",
'usersettings.error.network': "Impossibile raggiungere il server. Riprova.",
'usersettings.error.request_failed': "Richiesta non riuscita (%status%).",
'usersettings.error.email_long': "L'indirizzo email è troppo lungo.",
'usersettings.error.email_invalid': "Inserisci un indirizzo email valido.",
'usersettings.error.username_length': "Il nome utente deve contenere tra %min% e %max% caratteri.",
'usersettings.error.username_invalid': "Il nome utente può contenere solo lettere, numeri, punto, trattino basso e trattino.",
'usersettings.error.username_same': "Il nuovo nome utente deve essere diverso da quello attuale.",
'usersettings.success.password': "Password aggiornata con successo.",
'usersettings.success.email': "Email aggiornata con successo.",
'usersettings.success.username': "Nome utente aggiornato. Accedi di nuovo con il tuo nuovo nome.",
} }
+77 -10
View File
@@ -372,14 +372,14 @@
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Login // Login
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
'login.username': 'Wat is jou Camwijs naam', 'login.username': 'Wat is jou habbo naam',
'login.forgot_password': 'Wachtwoord vergeten?', 'login.forgot_password': 'Wachtwoord vergeten?',
// First-time visitors card // First-time visitors card
'nitro.login.firsttime.title': 'Voor het eerst hier?', 'nitro.login.firsttime.title': 'Voor het eerst hier?',
'nitro.login.firsttime.text': 'Heb je nog geen Camwijs account?', 'nitro.login.firsttime.text': 'Heb je nog geen habbo account?',
'nitro.login.firsttime.link': 'Je kunt er hier een aanmaken', 'nitro.login.firsttime.link': 'Je kunt er hier een aanmaken',
'nitro.login.card.title': 'Aanmelden bij Camwijs', 'nitro.login.card.title': 'Aanmelden bij habbo',
// Server status checks // Server status checks
'nitro.login.server.offline.short': 'De gameserver draait momenteel niet. Probeer het zo meteen opnieuw.', 'nitro.login.server.offline.short': 'De gameserver draait momenteel niet. Probeer het zo meteen opnieuw.',
@@ -388,12 +388,12 @@
'nitro.login.server.retry': 'Opnieuw proberen', 'nitro.login.server.retry': 'Opnieuw proberen',
// Registration flow // Registration flow
'nitro.login.register.title': 'Camwijs-gegevens', 'nitro.login.register.title': 'habbo-gegevens',
'nitro.login.register.next': 'Volgende', 'nitro.login.register.next': 'Volgende',
'nitro.login.register.finish': 'Voltooien', 'nitro.login.register.finish': 'Voltooien',
'nitro.login.register.creating': 'Bezig met aanmaken…', 'nitro.login.register.creating': 'Bezig met aanmaken…',
'nitro.login.register.intro.credentials': 'Laten we je account aanmaken. Voer je e-mailadres in en kies een wachtwoord — we controleren of dit e-mailadres nog niet in gebruik is.', 'nitro.login.register.intro.credentials': 'Laten we je account aanmaken. Voer je e-mailadres in en kies een wachtwoord — we controleren of dit e-mailadres nog niet in gebruik is.',
'nitro.login.register.intro.avatar': 'Nu is het tijd om je eigen Camwijs-personage te maken! Begin met het kiezen van je Camwijs-naam.', 'nitro.login.register.intro.avatar': 'Nu is het tijd om je eigen habbo-personage te maken! Begin met het kiezen van je habbo-naam.',
'nitro.login.register.intro.room': 'Laatste stap — kies een startkamer, of sla dit over en maak later je eigen kamer.', 'nitro.login.register.intro.room': 'Laatste stap — kies een startkamer, of sla dit over en maak later je eigen kamer.',
'nitro.login.register.confirm.label': 'Bevestig wachtwoord', 'nitro.login.register.confirm.label': 'Bevestig wachtwoord',
'nitro.login.register.username.placeholder': 'HabboNaam', 'nitro.login.register.username.placeholder': 'HabboNaam',
@@ -412,8 +412,8 @@
'nitro.login.forgot.success': 'E-mail verzonden! Als er een account bij dit adres hoort, vind je binnenkort een resetlink in je inbox (controleer je spam als je binnen een minuut niets ziet).', 'nitro.login.forgot.success': 'E-mail verzonden! Als er een account bij dit adres hoort, vind je binnenkort een resetlink in je inbox (controleer je spam als je binnen een minuut niets ziet).',
// Login errors (validation + transport) // Login errors (validation + transport)
'nitro.login.error.missing_credentials': 'Voer zowel je Camwijs-naam als wachtwoord in.', 'nitro.login.error.missing_credentials': 'Voer zowel je habbo-naam als wachtwoord in.',
'nitro.login.error.invalid_credentials': 'Ongeldige Camwijs-naam of wachtwoord.', 'nitro.login.error.invalid_credentials': 'Ongeldige habbo-naam of wachtwoord.',
'nitro.login.error.too_many_attempts': 'Te veel pogingen. Probeer het opnieuw over %seconds%s.', 'nitro.login.error.too_many_attempts': 'Te veel pogingen. Probeer het opnieuw over %seconds%s.',
'nitro.login.error.turnstile': 'Voltooi de beveiligingscontrole.', 'nitro.login.error.turnstile': 'Voltooi de beveiligingscontrole.',
'nitro.login.error.server_offline': 'De gameserver draait niet. Probeer het later opnieuw.', 'nitro.login.error.server_offline': 'De gameserver draait niet. Probeer het later opnieuw.',
@@ -427,9 +427,9 @@
'nitro.login.error.password_too_short': 'Je wachtwoord moet minimaal 8 tekens lang zijn.', 'nitro.login.error.password_too_short': 'Je wachtwoord moet minimaal 8 tekens lang zijn.',
'nitro.login.error.password_mismatch': 'Wachtwoorden komen niet overeen.', 'nitro.login.error.password_mismatch': 'Wachtwoorden komen niet overeen.',
'nitro.login.error.email_taken': 'Dit e-mailadres is al in gebruik.', 'nitro.login.error.email_taken': 'Dit e-mailadres is al in gebruik.',
'nitro.login.error.missing_username': 'Kies een Camwijs-naam.', 'nitro.login.error.missing_username': 'Kies een habbo-naam.',
'nitro.login.error.username_length': 'De Camwijs-naam moet 316 tekens bevatten.', 'nitro.login.error.username_length': 'De habbo-naam moet 316 tekens bevatten.',
'nitro.login.error.username_taken': 'Deze Camwijs-naam is al in gebruik.', 'nitro.login.error.username_taken': 'Deze habbo-naam is al in gebruik.',
'nitro.login.error.missing_email': 'Voer je e-mailadres in.', 'nitro.login.error.missing_email': 'Voer je e-mailadres in.',
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@@ -707,4 +707,71 @@
'chatcmd.client.ejectall': 'Verwijder alle meubels', 'chatcmd.client.ejectall': 'Verwijder alle meubels',
'chatcmd.client.settings': 'Kamerinstellingen', 'chatcmd.client.settings': 'Kamerinstellingen',
'chatcmd.client.info': 'Client info', 'chatcmd.client.info': 'Client info',
// ------------------------------------------------------------------------
// Me-menu settings + User account settings window
// ------------------------------------------------------------------------
'usersettings.tab.general': "Algemeen",
'usersettings.tab.themes': "Thema's",
'memenu.settings.other.place.multiple.objects': "Meerdere objecten plaatsen",
'memenu.settings.other.skip.purchase.confirmation': "Aankoopbevestiging overslaan",
'memenu.settings.other.enable.chat.window': "Chatvenster inschakelen",
'memenu.settings.other.catalog.classic.style': "Catalogus: klassieke stijl",
'usersettings.open.title': "Gebruikersinstellingen",
'usersettings.open.subtitle': "Wachtwoord & account",
'usersettings.themes.custom': "Aangepast thema",
'usersettings.themes.default_option': "Standaard (geen thema)",
'usersettings.themes.active_pieces': "Actieve onderdelen",
'usersettings.themes.invalid': "Thema ongeldig of onbereikbaar — standaard wordt gebruikt.",
'usersettings.themes.none': "Geen thema's beschikbaar. Voeg een map toe in custom-themes/ op de server.",
'usersettings.title': "Gebruikersinstellingen",
'usersettings.account.label': "Mijn account",
'usersettings.guest': "Gast",
'usersettings.subtitle': "Beheer je account en beveiliging",
'usersettings.menu.section': "Account",
'usersettings.menu.password.title': "Wachtwoord wijzigen",
'usersettings.menu.password.desc': "Wijzig het wachtwoord waarmee je inlogt.",
'usersettings.menu.email.title': "E-mail wijzigen",
'usersettings.menu.email.desc': "Werk het e-mailadres van je account bij.",
'usersettings.menu.username.title': "Gebruikersnaam wijzigen",
'usersettings.menu.username.desc': "Kies een nieuwe naam. Je moet daarna opnieuw inloggen.",
'usersettings.menu.soon.title': "Meer komt binnenkort",
'usersettings.menu.soon.desc': "Tweestapsverificatie en meer.",
'usersettings.password.hint': "Gebruik minimaal %count% tekens. Combineer hoofd- en kleine letters, cijfers en symbolen voor een sterker wachtwoord.",
'usersettings.email.hint': "Voor de veiligheid vragen we je je huidige wachtwoord te bevestigen voordat je het e-mailadres van je account wijzigt.",
'usersettings.username.hint': "Door je naam te wijzigen word je uitgelogd en je kunt pas na 30 dagen opnieuw wijzigen. Zorg dat je vrienden je nieuwe naam kennen!",
'usersettings.field.current_password': "Huidig wachtwoord",
'usersettings.field.new_password': "Nieuw wachtwoord",
'usersettings.field.retype_password': "Herhaal nieuw wachtwoord",
'usersettings.field.new_email': "Nieuw e-mailadres",
'usersettings.field.new_username': "Nieuwe gebruikersnaam",
'usersettings.username.rules': "%min%-%max% tekens. Alleen letters, cijfers, punt, underscore en streepje.",
'usersettings.strength.weak': "Zwak",
'usersettings.strength.fair': "Redelijk",
'usersettings.strength.good': "Goed",
'usersettings.strength.strong': "Sterk",
'usersettings.aria.show_password': "Wachtwoord tonen",
'usersettings.aria.hide_password': "Wachtwoord verbergen",
'usersettings.btn.cancel': "Annuleren",
'usersettings.btn.saving': "Opslaan…",
'usersettings.btn.save_password': "Wachtwoord opslaan",
'usersettings.btn.save_email': "E-mail opslaan",
'usersettings.btn.renaming': "Bezig met hernoemen…",
'usersettings.btn.rename': "Hernoem mij",
'usersettings.error.fields_required': "Alle velden zijn verplicht.",
'usersettings.error.password_min': "Het wachtwoord moet minimaal %count% tekens bevatten.",
'usersettings.error.password_long': "Het wachtwoord is te lang.",
'usersettings.error.password_mismatch': "De nieuwe wachtwoorden komen niet overeen.",
'usersettings.error.password_same': "Het nieuwe wachtwoord moet anders zijn dan het huidige wachtwoord.",
'usersettings.error.not_authenticated': "Je bent niet ingelogd. Log opnieuw in.",
'usersettings.error.network': "Kan de server niet bereiken. Probeer het opnieuw.",
'usersettings.error.request_failed': "Verzoek mislukt (%status%).",
'usersettings.error.email_long': "Het e-mailadres is te lang.",
'usersettings.error.email_invalid': "Voer een geldig e-mailadres in.",
'usersettings.error.username_length': "De gebruikersnaam moet tussen %min% en %max% tekens bevatten.",
'usersettings.error.username_invalid': "De gebruikersnaam mag alleen letters, cijfers, punt, underscore en streepje bevatten.",
'usersettings.error.username_same': "De nieuwe gebruikersnaam moet anders zijn dan de huidige.",
'usersettings.success.password': "Wachtwoord succesvol bijgewerkt.",
'usersettings.success.email': "E-mail succesvol bijgewerkt.",
'usersettings.success.username': "Gebruikersnaam bijgewerkt. Log opnieuw in met je nieuwe naam.",
} }
@@ -8,9 +8,13 @@ import { FortuneWheelSettingsView } from './FortuneWheelSettingsView';
import { WheelWinReveal } from './WheelWinReveal'; import { WheelWinReveal } from './WheelWinReveal';
import { renderPrizeIcon } from './wheelPrizeIcon'; import { renderPrizeIcon } from './wheelPrizeIcon';
// Stock UI palette (white / light-blue / grey / black). // Stock UI palette (white / light-blue / grey / black). Exposed as CSS custom
const SLICE_COLORS = [ '#eef2f5', '#c3dcec' ]; // properties so a runtime theme can recolor the wheel without changing defaults
const RIM = '#4c606c'; // (the fallback values keep the stock look when no theme overrides them).
const SLICE_COLORS = [ 'var(--wheel-slice-1, #eef2f5)', 'var(--wheel-slice-2, #c3dcec)' ];
const RIM = 'var(--wheel-rim, #4c606c)';
const DIVIDER = 'var(--wheel-divider, rgba(76,96,108,0.3))';
const HUB = 'var(--wheel-hub, #eef2f5)';
const WHEEL_SIZE = 420; const WHEEL_SIZE = 420;
const ICON_RADIUS = 150; const ICON_RADIUS = 150;
const FULL_TURNS = 5; const FULL_TURNS = 5;
@@ -202,7 +206,7 @@ export const FortuneWheelView: FC<{}> = () =>
const canSpin = ((freeSpins + extraSpins) > 0) && !isSpinning && (prizes.length > 0); const canSpin = ((freeSpins + extraSpins) > 0) && !isSpinning && (prizes.length > 0);
return ( return (
<NitroCard className="w-[780px] max-w-[96vw]" uniqueKey="fortune-wheel"> <NitroCard className="wheel-card w-[780px] max-w-[96vw]" uniqueKey="fortune-wheel">
<NitroCard.Header headerText={ LocalizeText('wheel.title') } onCloseClick={ () => setIsVisible(false) } /> <NitroCard.Header headerText={ LocalizeText('wheel.title') } onCloseClick={ () => setIsVisible(false) } />
<NitroCard.Content> <NitroCard.Content>
<div className="relative"> <div className="relative">
@@ -223,7 +227,7 @@ export const FortuneWheelView: FC<{}> = () =>
<div <div
key={ `divider-${ i }` } key={ `divider-${ i }` }
className="absolute bottom-1/2 left-1/2 origin-bottom" className="absolute bottom-1/2 left-1/2 origin-bottom"
style={ { width: '2px', height: `${ WHEEL_SIZE / 2 }px`, transform: `translateX(-1px) rotate(${ i * sliceAngle }deg)`, background: 'rgba(76,96,108,0.3)' } } /> style={ { width: '2px', height: `${ WHEEL_SIZE / 2 }px`, transform: `translateX(-1px) rotate(${ i * sliceAngle }deg)`, background: DIVIDER } } />
)) } )) }
{ prizes.map((prize, i) => { prizes.map((prize, i) =>
{ {
@@ -233,13 +237,13 @@ export const FortuneWheelView: FC<{}> = () =>
key={ prize.id } key={ prize.id }
className="absolute left-1/2 top-1/2" className="absolute left-1/2 top-1/2"
style={ { transform: `rotate(${ centerAngle }deg) translateY(-${ ICON_RADIUS }px) rotate(-${ centerAngle }deg)` } }> style={ { transform: `rotate(${ centerAngle }deg) translateY(-${ ICON_RADIUS }px) rotate(-${ centerAngle }deg)` } }>
<div className="-translate-x-1/2 -translate-y-1/2"> <div className="wheel-slice-icon -translate-x-1/2 -translate-y-1/2">
{ renderPrizeIcon(prize) } { renderPrizeIcon(prize) }
</div> </div>
</div>); </div>);
}) } }) }
</div> </div>
<div className="absolute left-1/2 top-1/2 z-10 h-14 w-14 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#eef2f5] shadow-[0_0_8px_rgba(0,0,0,0.25)]" style={ { border: `4px solid ${ RIM }` } } /> <div className="absolute left-1/2 top-1/2 z-10 h-14 w-14 -translate-x-1/2 -translate-y-1/2 rounded-full shadow-[0_0_8px_rgba(0,0,0,0.25)]" style={ { border: `4px solid ${ RIM }`, background: HUB } } />
</div> </div>
</div> </div>
<Text bold className="text-[#2f6f95]">{ LocalizeText('wheel.free.today', [ 'count' ], [ freeSpins.toString() ]) }</Text> <Text bold className="text-[#2f6f95]">{ LocalizeText('wheel.free.today', [ 'count' ], [ freeSpins.toString() ]) }</Text>
@@ -1,7 +1,7 @@
import { AddLinkEventTracker, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; import { AddLinkEventTracker, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, KeyboardEvent, useEffect, useMemo, useState } from 'react'; import { FC, KeyboardEvent, useEffect, useMemo, useState } from 'react';
import { FaArrowLeft, FaCheckCircle, FaChevronRight, FaEnvelope, FaExclamationTriangle, FaEye, FaEyeSlash, FaIdBadge, FaInfoCircle, FaKey, FaShieldAlt, FaUserCog } from 'react-icons/fa'; import { FaArrowLeft, FaCheckCircle, FaChevronRight, FaEnvelope, FaExclamationTriangle, FaEye, FaEyeSlash, FaIdBadge, FaInfoCircle, FaKey, FaShieldAlt, FaUserCog } from 'react-icons/fa';
import { GetConfigurationValue, getAccessToken } from '../../api'; import { GetConfigurationValue, LocalizeText, getAccessToken } from '../../api';
import { Button, LayoutAvatarImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; import { Button, LayoutAvatarImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
const MIN_PASSWORD_LENGTH = 8; const MIN_PASSWORD_LENGTH = 8;
@@ -17,9 +17,9 @@ const MAX_USERNAME_LENGTH = 25;
type FeedbackKind = 'error' | 'success'; type FeedbackKind = 'error' | 'success';
type Section = 'menu' | 'password' | 'email' | 'username'; type Section = 'menu' | 'password' | 'email' | 'username';
const passwordStrength = (value: string): { score: number; label: string; color: string } => const passwordStrength = (value: string): { score: number; labelKey: string; color: string } =>
{ {
if(!value) return { score: 0, label: '', color: 'bg-black/10' }; if(!value) return { score: 0, labelKey: '', color: 'bg-black/10' };
let score = 0; let score = 0;
if(value.length >= MIN_PASSWORD_LENGTH) score++; if(value.length >= MIN_PASSWORD_LENGTH) score++;
@@ -28,10 +28,10 @@ const passwordStrength = (value: string): { score: number; label: string; color:
if(/\d/.test(value)) score++; if(/\d/.test(value)) score++;
if(/[^A-Za-z0-9]/.test(value)) score++; if(/[^A-Za-z0-9]/.test(value)) score++;
if(score <= 1) return { score: 1, label: 'Weak', color: 'bg-[#a81a12]' }; if(score <= 1) return { score: 1, labelKey: 'usersettings.strength.weak', color: 'bg-[#a81a12]' };
if(score === 2) return { score: 2, label: 'Fair', color: 'bg-[#ffc107]' }; if(score === 2) return { score: 2, labelKey: 'usersettings.strength.fair', color: 'bg-[#ffc107]' };
if(score === 3) return { score: 3, label: 'Good', color: 'bg-[#1e7295]' }; if(score === 3) return { score: 3, labelKey: 'usersettings.strength.good', color: 'bg-[#1e7295]' };
return { score: 4, label: 'Strong', color: 'bg-[#00800b]' }; return { score: 4, labelKey: 'usersettings.strength.strong', color: 'bg-[#00800b]' };
}; };
export const UserAccountSettingsView: FC<{}> = () => export const UserAccountSettingsView: FC<{}> = () =>
@@ -131,38 +131,38 @@ export const UserAccountSettingsView: FC<{}> = () =>
if(!currentPassword || !newPassword || !confirmPassword) if(!currentPassword || !newPassword || !confirmPassword)
{ {
setFeedback({ kind: 'error', message: 'All fields are required.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.fields_required') });
return; return;
} }
if(newPassword.length < MIN_PASSWORD_LENGTH) if(newPassword.length < MIN_PASSWORD_LENGTH)
{ {
setFeedback({ kind: 'error', message: `Password must be at least ${ MIN_PASSWORD_LENGTH } characters.` }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.password_min', [ 'count' ], [ MIN_PASSWORD_LENGTH.toString() ]) });
return; return;
} }
if(newPassword.length > MAX_PASSWORD_LENGTH) if(newPassword.length > MAX_PASSWORD_LENGTH)
{ {
setFeedback({ kind: 'error', message: 'Password is too long.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.password_long') });
return; return;
} }
if(newPassword !== confirmPassword) if(newPassword !== confirmPassword)
{ {
setFeedback({ kind: 'error', message: 'New passwords do not match.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.password_mismatch') });
return; return;
} }
if(newPassword === currentPassword) if(newPassword === currentPassword)
{ {
setFeedback({ kind: 'error', message: 'New password must be different from the current password.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.password_same') });
return; return;
} }
const token = getAccessToken(); const token = getAccessToken();
if(!token) if(!token)
{ {
setFeedback({ kind: 'error', message: 'You are not authenticated. Please log in again.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.not_authenticated') });
return; return;
} }
@@ -191,14 +191,14 @@ export const UserAccountSettingsView: FC<{}> = () =>
{ {
const message = typeof payload.error === 'string' && payload.error const message = typeof payload.error === 'string' && payload.error
? payload.error ? payload.error
: `Request failed (${ response.status }).`; : LocalizeText('usersettings.error.request_failed', [ 'status' ], [ response.status.toString() ]);
setFeedback({ kind: 'error', message }); setFeedback({ kind: 'error', message });
return; return;
} }
const message = typeof payload.message === 'string' && payload.message const message = typeof payload.message === 'string' && payload.message
? payload.message ? payload.message
: 'Password updated successfully.'; : LocalizeText('usersettings.success.password');
setFeedback({ kind: 'success', message }); setFeedback({ kind: 'success', message });
setCurrentPassword(''); setCurrentPassword('');
setNewPassword(''); setNewPassword('');
@@ -208,7 +208,7 @@ export const UserAccountSettingsView: FC<{}> = () =>
} }
catch catch
{ {
setFeedback({ kind: 'error', message: 'Could not reach the server. Please try again.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.network') });
} }
finally finally
{ {
@@ -224,26 +224,26 @@ export const UserAccountSettingsView: FC<{}> = () =>
if(!emailCurrentPassword || !newEmail) if(!emailCurrentPassword || !newEmail)
{ {
setFeedback({ kind: 'error', message: 'All fields are required.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.fields_required') });
return; return;
} }
if(newEmail.length > MAX_EMAIL_LENGTH) if(newEmail.length > MAX_EMAIL_LENGTH)
{ {
setFeedback({ kind: 'error', message: 'Email address is too long.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.email_long') });
return; return;
} }
if(!EMAIL_RE.test(newEmail)) if(!EMAIL_RE.test(newEmail))
{ {
setFeedback({ kind: 'error', message: 'Please enter a valid email address.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.email_invalid') });
return; return;
} }
const token = getAccessToken(); const token = getAccessToken();
if(!token) if(!token)
{ {
setFeedback({ kind: 'error', message: 'You are not authenticated. Please log in again.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.not_authenticated') });
return; return;
} }
@@ -272,14 +272,14 @@ export const UserAccountSettingsView: FC<{}> = () =>
{ {
const message = typeof payload.error === 'string' && payload.error const message = typeof payload.error === 'string' && payload.error
? payload.error ? payload.error
: `Request failed (${ response.status }).`; : LocalizeText('usersettings.error.request_failed', [ 'status' ], [ response.status.toString() ]);
setFeedback({ kind: 'error', message }); setFeedback({ kind: 'error', message });
return; return;
} }
const message = typeof payload.message === 'string' && payload.message const message = typeof payload.message === 'string' && payload.message
? payload.message ? payload.message
: 'Email updated successfully.'; : LocalizeText('usersettings.success.email');
setFeedback({ kind: 'success', message }); setFeedback({ kind: 'success', message });
setEmailCurrentPassword(''); setEmailCurrentPassword('');
setNewEmail(''); setNewEmail('');
@@ -287,7 +287,7 @@ export const UserAccountSettingsView: FC<{}> = () =>
} }
catch catch
{ {
setFeedback({ kind: 'error', message: 'Could not reach the server. Please try again.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.network') });
} }
finally finally
{ {
@@ -303,32 +303,32 @@ export const UserAccountSettingsView: FC<{}> = () =>
if(!usernameCurrentPassword || !newUsername) if(!usernameCurrentPassword || !newUsername)
{ {
setFeedback({ kind: 'error', message: 'All fields are required.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.fields_required') });
return; return;
} }
if(newUsername.length < MIN_USERNAME_LENGTH || newUsername.length > MAX_USERNAME_LENGTH) if(newUsername.length < MIN_USERNAME_LENGTH || newUsername.length > MAX_USERNAME_LENGTH)
{ {
setFeedback({ kind: 'error', message: `Username must be between ${ MIN_USERNAME_LENGTH } and ${ MAX_USERNAME_LENGTH } characters.` }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.username_length', [ 'min', 'max' ], [ MIN_USERNAME_LENGTH.toString(), MAX_USERNAME_LENGTH.toString() ]) });
return; return;
} }
if(!USERNAME_RE.test(newUsername)) if(!USERNAME_RE.test(newUsername))
{ {
setFeedback({ kind: 'error', message: 'Username may only contain letters, numbers, dot, underscore and dash.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.username_invalid') });
return; return;
} }
if(newUsername === session.username) if(newUsername === session.username)
{ {
setFeedback({ kind: 'error', message: 'New username must be different from the current one.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.username_same') });
return; return;
} }
const token = getAccessToken(); const token = getAccessToken();
if(!token) if(!token)
{ {
setFeedback({ kind: 'error', message: 'You are not authenticated. Please log in again.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.not_authenticated') });
return; return;
} }
@@ -357,14 +357,14 @@ export const UserAccountSettingsView: FC<{}> = () =>
{ {
const message = typeof payload.error === 'string' && payload.error const message = typeof payload.error === 'string' && payload.error
? payload.error ? payload.error
: `Request failed (${ response.status }).`; : LocalizeText('usersettings.error.request_failed', [ 'status' ], [ response.status.toString() ]);
setFeedback({ kind: 'error', message }); setFeedback({ kind: 'error', message });
return; return;
} }
const message = typeof payload.message === 'string' && payload.message const message = typeof payload.message === 'string' && payload.message
? payload.message ? payload.message
: 'Username updated. Please log in again with your new name.'; : LocalizeText('usersettings.success.username');
setFeedback({ kind: 'success', message }); setFeedback({ kind: 'success', message });
setUsernameCurrentPassword(''); setUsernameCurrentPassword('');
setNewUsername(''); setNewUsername('');
@@ -382,7 +382,7 @@ export const UserAccountSettingsView: FC<{}> = () =>
} }
catch catch
{ {
setFeedback({ kind: 'error', message: 'Could not reach the server. Please try again.' }); setFeedback({ kind: 'error', message: LocalizeText('usersettings.error.network') });
} }
finally finally
{ {
@@ -394,7 +394,7 @@ export const UserAccountSettingsView: FC<{}> = () =>
return ( return (
<NitroCardView className="user-account-settings-window w-[360px]" theme="primary-slim" uniqueKey="user-account-settings"> <NitroCardView className="user-account-settings-window w-[360px]" theme="primary-slim" uniqueKey="user-account-settings">
<NitroCardHeaderView headerText="User Settings" onCloseClick={ close } /> <NitroCardHeaderView headerText={ LocalizeText('usersettings.title') } onCloseClick={ close } />
<div className="relative flex items-center gap-3 px-3 py-2 bg-[linear-gradient(180deg,#2e8fb8_0%,#1e7295_100%)] text-white"> <div className="relative flex items-center gap-3 px-3 py-2 bg-[linear-gradient(180deg,#2e8fb8_0%,#1e7295_100%)] text-white">
<div className="absolute inset-0 opacity-20 pointer-events-none [background-image:radial-gradient(rgba(255,255,255,0.5)_1px,transparent_1px)] [background-size:6px_6px]" /> <div className="absolute inset-0 opacity-20 pointer-events-none [background-image:radial-gradient(rgba(255,255,255,0.5)_1px,transparent_1px)] [background-size:6px_6px]" />
@@ -410,16 +410,16 @@ export const UserAccountSettingsView: FC<{}> = () =>
</div> </div>
) } ) }
<div className="relative flex flex-col leading-tight"> <div className="relative flex flex-col leading-tight">
<Text small className="text-white/80 uppercase tracking-wider">My account</Text> <Text small className="text-white/80 uppercase tracking-wider">{ LocalizeText('usersettings.account.label') }</Text>
<Text bold className="text-white text-[15px]">{ session.username || 'Guest' }</Text> <Text bold className="text-white text-[15px]">{ session.username || LocalizeText('usersettings.guest') }</Text>
<Text small className="text-white/80">Manage your account and security</Text> <Text small className="text-white/80">{ LocalizeText('usersettings.subtitle') }</Text>
</div> </div>
</div> </div>
<NitroCardContentView className="flex flex-col gap-2 text-black"> <NitroCardContentView className="flex flex-col gap-2 text-black">
{ section === 'menu' && ( { section === 'menu' && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Text small className="text-black/60 uppercase tracking-wider px-1">Account</Text> <Text small className="text-black/60 uppercase tracking-wider px-1">{ LocalizeText('usersettings.menu.section') }</Text>
<button <button
type="button" type="button"
className="group flex items-center gap-3 rounded-md border border-black/10 bg-white px-3 py-2 hover:bg-[#f5fbfd] hover:border-[#1e7295] transition-colors cursor-pointer text-left" className="group flex items-center gap-3 rounded-md border border-black/10 bg-white px-3 py-2 hover:bg-[#f5fbfd] hover:border-[#1e7295] transition-colors cursor-pointer text-left"
@@ -428,8 +428,8 @@ export const UserAccountSettingsView: FC<{}> = () =>
<FaKey /> <FaKey />
</div> </div>
<div className="flex flex-col flex-1 leading-tight"> <div className="flex flex-col flex-1 leading-tight">
<Text bold>Reset password</Text> <Text bold>{ LocalizeText('usersettings.menu.password.title') }</Text>
<Text small className="text-black/60">Change the password used to log in.</Text> <Text small className="text-black/60">{ LocalizeText('usersettings.menu.password.desc') }</Text>
</div> </div>
<FaChevronRight className="text-black/40 group-hover:text-[#1e7295]" /> <FaChevronRight className="text-black/40 group-hover:text-[#1e7295]" />
</button> </button>
@@ -442,8 +442,8 @@ export const UserAccountSettingsView: FC<{}> = () =>
<FaEnvelope /> <FaEnvelope />
</div> </div>
<div className="flex flex-col flex-1 leading-tight"> <div className="flex flex-col flex-1 leading-tight">
<Text bold>Change email</Text> <Text bold>{ LocalizeText('usersettings.menu.email.title') }</Text>
<Text small className="text-black/60">Update the email address on your account.</Text> <Text small className="text-black/60">{ LocalizeText('usersettings.menu.email.desc') }</Text>
</div> </div>
<FaChevronRight className="text-black/40 group-hover:text-[#1e7295]" /> <FaChevronRight className="text-black/40 group-hover:text-[#1e7295]" />
</button> </button>
@@ -456,8 +456,8 @@ export const UserAccountSettingsView: FC<{}> = () =>
<FaIdBadge /> <FaIdBadge />
</div> </div>
<div className="flex flex-col flex-1 leading-tight"> <div className="flex flex-col flex-1 leading-tight">
<Text bold>Change username</Text> <Text bold>{ LocalizeText('usersettings.menu.username.title') }</Text>
<Text small className="text-black/60">Pick a new name. You'll need to log in again.</Text> <Text small className="text-black/60">{ LocalizeText('usersettings.menu.username.desc') }</Text>
</div> </div>
<FaChevronRight className="text-black/40 group-hover:text-[#a37800]" /> <FaChevronRight className="text-black/40 group-hover:text-[#a37800]" />
</button> </button>
@@ -467,8 +467,8 @@ export const UserAccountSettingsView: FC<{}> = () =>
<FaShieldAlt /> <FaShieldAlt />
</div> </div>
<div className="flex flex-col flex-1 leading-tight"> <div className="flex flex-col flex-1 leading-tight">
<Text bold className="text-black/60">More coming soon</Text> <Text bold className="text-black/60">{ LocalizeText('usersettings.menu.soon.title') }</Text>
<Text small className="text-black/50">Two-factor authentication and more.</Text> <Text small className="text-black/50">{ LocalizeText('usersettings.menu.soon.desc') }</Text>
</div> </div>
</div> </div>
</div> </div>
@@ -485,16 +485,16 @@ export const UserAccountSettingsView: FC<{}> = () =>
<FaArrowLeft size={ 11 } /> <FaArrowLeft size={ 11 } />
</button> </button>
<FaUserCog className="text-[#1e7295]" /> <FaUserCog className="text-[#1e7295]" />
<Text bold>Reset password</Text> <Text bold>{ LocalizeText('usersettings.menu.password.title') }</Text>
</div> </div>
<div className="flex items-start gap-2 rounded-md border border-[#1e7295]/30 bg-[#1e7295]/10 px-2 py-2 text-[11px] leading-4 text-[#0d3d52]"> <div className="flex items-start gap-2 rounded-md border border-[#1e7295]/30 bg-[#1e7295]/10 px-2 py-2 text-[11px] leading-4 text-[#0d3d52]">
<FaInfoCircle className="mt-[2px] shrink-0 text-[#1e7295]" /> <FaInfoCircle className="mt-[2px] shrink-0 text-[#1e7295]" />
<span>Use at least <strong>{ MIN_PASSWORD_LENGTH } characters</strong>. Mix upper &amp; lowercase, numbers and symbols for a stronger password.</span> <span>{ LocalizeText('usersettings.password.hint', [ 'count' ], [ MIN_PASSWORD_LENGTH.toString() ]) }</span>
</div> </div>
<label className="flex flex-col gap-1 text-[12px]"> <label className="flex flex-col gap-1 text-[12px]">
<span className="font-bold">Current password</span> <span className="font-bold">{ LocalizeText('usersettings.field.current_password') }</span>
<div className="relative flex items-center"> <div className="relative flex items-center">
<FaKey className="absolute left-2 text-black/40" size={ 12 } /> <FaKey className="absolute left-2 text-black/40" size={ 12 } />
<input <input
@@ -508,7 +508,7 @@ export const UserAccountSettingsView: FC<{}> = () =>
/> />
<button <button
type="button" type="button"
aria-label={ showCurrent ? 'Hide password' : 'Show password' } aria-label={ showCurrent ? LocalizeText('usersettings.aria.hide_password') : LocalizeText('usersettings.aria.show_password') }
onClick={ () => setShowCurrent(prev => !prev) } onClick={ () => setShowCurrent(prev => !prev) }
className="absolute right-2 text-black/40 hover:text-black/70"> className="absolute right-2 text-black/40 hover:text-black/70">
{ showCurrent ? <FaEyeSlash size={ 12 } /> : <FaEye size={ 12 } /> } { showCurrent ? <FaEyeSlash size={ 12 } /> : <FaEye size={ 12 } /> }
@@ -517,7 +517,7 @@ export const UserAccountSettingsView: FC<{}> = () =>
</label> </label>
<label className="flex flex-col gap-1 text-[12px]"> <label className="flex flex-col gap-1 text-[12px]">
<span className="font-bold">New password</span> <span className="font-bold">{ LocalizeText('usersettings.field.new_password') }</span>
<div className="relative flex items-center"> <div className="relative flex items-center">
<FaKey className="absolute left-2 text-black/40" size={ 12 } /> <FaKey className="absolute left-2 text-black/40" size={ 12 } />
<input <input
@@ -531,7 +531,7 @@ export const UserAccountSettingsView: FC<{}> = () =>
/> />
<button <button
type="button" type="button"
aria-label={ showNew ? 'Hide password' : 'Show password' } aria-label={ showNew ? LocalizeText('usersettings.aria.hide_password') : LocalizeText('usersettings.aria.show_password') }
onClick={ () => setShowNew(prev => !prev) } onClick={ () => setShowNew(prev => !prev) }
className="absolute right-2 text-black/40 hover:text-black/70"> className="absolute right-2 text-black/40 hover:text-black/70">
{ showNew ? <FaEyeSlash size={ 12 } /> : <FaEye size={ 12 } /> } { showNew ? <FaEyeSlash size={ 12 } /> : <FaEye size={ 12 } /> }
@@ -542,13 +542,13 @@ export const UserAccountSettingsView: FC<{}> = () =>
<div className="flex-1 h-1.5 rounded-full bg-black/10 overflow-hidden"> <div className="flex-1 h-1.5 rounded-full bg-black/10 overflow-hidden">
<div className={ `h-full ${ strength.color } transition-all` } style={ { width: `${ (strength.score / 4) * 100 }%` } } /> <div className={ `h-full ${ strength.color } transition-all` } style={ { width: `${ (strength.score / 4) * 100 }%` } } />
</div> </div>
<span className="text-[10px] text-black/60 w-12 text-right">{ strength.label }</span> <span className="text-[10px] text-black/60 w-12 text-right">{ strength.labelKey ? LocalizeText(strength.labelKey) : '' }</span>
</div> </div>
) } ) }
</label> </label>
<label className="flex flex-col gap-1 text-[12px]"> <label className="flex flex-col gap-1 text-[12px]">
<span className="font-bold">Retype new password</span> <span className="font-bold">{ LocalizeText('usersettings.field.retype_password') }</span>
<div className="relative flex items-center"> <div className="relative flex items-center">
<FaKey className="absolute left-2 text-black/40" size={ 12 } /> <FaKey className="absolute left-2 text-black/40" size={ 12 } />
<input <input
@@ -577,10 +577,10 @@ export const UserAccountSettingsView: FC<{}> = () =>
<div className="flex justify-end gap-2 pt-1"> <div className="flex justify-end gap-2 pt-1">
<Button variant="secondary" disabled={ submitting } onClick={ () => { resetForm(); setSection('menu'); } }> <Button variant="secondary" disabled={ submitting } onClick={ () => { resetForm(); setSection('menu'); } }>
Cancel { LocalizeText('usersettings.btn.cancel') }
</Button> </Button>
<Button variant="success" disabled={ submitting } onClick={ () => submitPasswordChange() }> <Button variant="success" disabled={ submitting } onClick={ () => submitPasswordChange() }>
{ submitting ? 'Saving' : 'Save password' } { submitting ? LocalizeText('usersettings.btn.saving') : LocalizeText('usersettings.btn.save_password') }
</Button> </Button>
</div> </div>
</div> </div>
@@ -597,16 +597,16 @@ export const UserAccountSettingsView: FC<{}> = () =>
<FaArrowLeft size={ 11 } /> <FaArrowLeft size={ 11 } />
</button> </button>
<FaEnvelope className="text-[#185d79]" /> <FaEnvelope className="text-[#185d79]" />
<Text bold>Change email</Text> <Text bold>{ LocalizeText('usersettings.menu.email.title') }</Text>
</div> </div>
<div className="flex items-start gap-2 rounded-md border border-[#1e7295]/30 bg-[#1e7295]/10 px-2 py-2 text-[11px] leading-4 text-[#0d3d52]"> <div className="flex items-start gap-2 rounded-md border border-[#1e7295]/30 bg-[#1e7295]/10 px-2 py-2 text-[11px] leading-4 text-[#0d3d52]">
<FaInfoCircle className="mt-[2px] shrink-0 text-[#1e7295]" /> <FaInfoCircle className="mt-[2px] shrink-0 text-[#1e7295]" />
<span>For security we ask you to confirm your <strong>current password</strong> before changing the email on your account.</span> <span>{ LocalizeText('usersettings.email.hint') }</span>
</div> </div>
<label className="flex flex-col gap-1 text-[12px]"> <label className="flex flex-col gap-1 text-[12px]">
<span className="font-bold">Current password</span> <span className="font-bold">{ LocalizeText('usersettings.field.current_password') }</span>
<div className="relative flex items-center"> <div className="relative flex items-center">
<FaKey className="absolute left-2 text-black/40" size={ 12 } /> <FaKey className="absolute left-2 text-black/40" size={ 12 } />
<input <input
@@ -620,7 +620,7 @@ export const UserAccountSettingsView: FC<{}> = () =>
/> />
<button <button
type="button" type="button"
aria-label={ showEmailPassword ? 'Hide password' : 'Show password' } aria-label={ showEmailPassword ? LocalizeText('usersettings.aria.hide_password') : LocalizeText('usersettings.aria.show_password') }
onClick={ () => setShowEmailPassword(prev => !prev) } onClick={ () => setShowEmailPassword(prev => !prev) }
className="absolute right-2 text-black/40 hover:text-black/70"> className="absolute right-2 text-black/40 hover:text-black/70">
{ showEmailPassword ? <FaEyeSlash size={ 12 } /> : <FaEye size={ 12 } /> } { showEmailPassword ? <FaEyeSlash size={ 12 } /> : <FaEye size={ 12 } /> }
@@ -629,7 +629,7 @@ export const UserAccountSettingsView: FC<{}> = () =>
</label> </label>
<label className="flex flex-col gap-1 text-[12px]"> <label className="flex flex-col gap-1 text-[12px]">
<span className="font-bold">New email address</span> <span className="font-bold">{ LocalizeText('usersettings.field.new_email') }</span>
<div className="relative flex items-center"> <div className="relative flex items-center">
<FaEnvelope className="absolute left-2 text-black/40" size={ 12 } /> <FaEnvelope className="absolute left-2 text-black/40" size={ 12 } />
<input <input
@@ -660,10 +660,10 @@ export const UserAccountSettingsView: FC<{}> = () =>
<div className="flex justify-end gap-2 pt-1"> <div className="flex justify-end gap-2 pt-1">
<Button variant="secondary" disabled={ submitting } onClick={ () => { resetForm(); setSection('menu'); } }> <Button variant="secondary" disabled={ submitting } onClick={ () => { resetForm(); setSection('menu'); } }>
Cancel { LocalizeText('usersettings.btn.cancel') }
</Button> </Button>
<Button variant="success" disabled={ submitting } onClick={ () => submitEmailChange() }> <Button variant="success" disabled={ submitting } onClick={ () => submitEmailChange() }>
{ submitting ? 'Saving' : 'Save email' } { submitting ? LocalizeText('usersettings.btn.saving') : LocalizeText('usersettings.btn.save_email') }
</Button> </Button>
</div> </div>
</div> </div>
@@ -680,16 +680,16 @@ export const UserAccountSettingsView: FC<{}> = () =>
<FaArrowLeft size={ 11 } /> <FaArrowLeft size={ 11 } />
</button> </button>
<FaIdBadge className="text-[#a37800]" /> <FaIdBadge className="text-[#a37800]" />
<Text bold>Change username</Text> <Text bold>{ LocalizeText('usersettings.menu.username.title') }</Text>
</div> </div>
<div className="flex items-start gap-2 rounded-md border border-[#ffc107]/50 bg-[#fff8e1] px-2 py-2 text-[11px] leading-4 text-[#5c4400]"> <div className="flex items-start gap-2 rounded-md border border-[#ffc107]/50 bg-[#fff8e1] px-2 py-2 text-[11px] leading-4 text-[#5c4400]">
<FaExclamationTriangle className="mt-[2px] shrink-0 text-[#a37800]" /> <FaExclamationTriangle className="mt-[2px] shrink-0 text-[#a37800]" />
<span>Renaming will <strong>log you out</strong> and you can only rename again after 30 days. Make sure your friends know your new name!</span> <span>{ LocalizeText('usersettings.username.hint') }</span>
</div> </div>
<label className="flex flex-col gap-1 text-[12px]"> <label className="flex flex-col gap-1 text-[12px]">
<span className="font-bold">Current password</span> <span className="font-bold">{ LocalizeText('usersettings.field.current_password') }</span>
<div className="relative flex items-center"> <div className="relative flex items-center">
<FaKey className="absolute left-2 text-black/40" size={ 12 } /> <FaKey className="absolute left-2 text-black/40" size={ 12 } />
<input <input
@@ -703,7 +703,7 @@ export const UserAccountSettingsView: FC<{}> = () =>
/> />
<button <button
type="button" type="button"
aria-label={ showUsernamePassword ? 'Hide password' : 'Show password' } aria-label={ showUsernamePassword ? LocalizeText('usersettings.aria.hide_password') : LocalizeText('usersettings.aria.show_password') }
onClick={ () => setShowUsernamePassword(prev => !prev) } onClick={ () => setShowUsernamePassword(prev => !prev) }
className="absolute right-2 text-black/40 hover:text-black/70"> className="absolute right-2 text-black/40 hover:text-black/70">
{ showUsernamePassword ? <FaEyeSlash size={ 12 } /> : <FaEye size={ 12 } /> } { showUsernamePassword ? <FaEyeSlash size={ 12 } /> : <FaEye size={ 12 } /> }
@@ -712,7 +712,7 @@ export const UserAccountSettingsView: FC<{}> = () =>
</label> </label>
<label className="flex flex-col gap-1 text-[12px]"> <label className="flex flex-col gap-1 text-[12px]">
<span className="font-bold">New username</span> <span className="font-bold">{ LocalizeText('usersettings.field.new_username') }</span>
<div className="relative flex items-center"> <div className="relative flex items-center">
<FaIdBadge className="absolute left-2 text-black/40" size={ 12 } /> <FaIdBadge className="absolute left-2 text-black/40" size={ 12 } />
<input <input
@@ -730,7 +730,7 @@ export const UserAccountSettingsView: FC<{}> = () =>
<FaCheckCircle className="absolute right-2 text-[#00800b]" size={ 12 } /> <FaCheckCircle className="absolute right-2 text-[#00800b]" size={ 12 } />
) } ) }
</div> </div>
<span className="text-[10px] text-black/50">{ MIN_USERNAME_LENGTH }-{ MAX_USERNAME_LENGTH } characters. Letters, numbers, dot, underscore and dash only.</span> <span className="text-[10px] text-black/50">{ LocalizeText('usersettings.username.rules', [ 'min', 'max' ], [ MIN_USERNAME_LENGTH.toString(), MAX_USERNAME_LENGTH.toString() ]) }</span>
</label> </label>
{ feedback && ( { feedback && (
@@ -744,10 +744,10 @@ export const UserAccountSettingsView: FC<{}> = () =>
<div className="flex justify-end gap-2 pt-1"> <div className="flex justify-end gap-2 pt-1">
<Button variant="secondary" disabled={ submitting } onClick={ () => { resetForm(); setSection('menu'); } }> <Button variant="secondary" disabled={ submitting } onClick={ () => { resetForm(); setSection('menu'); } }>
Cancel { LocalizeText('usersettings.btn.cancel') }
</Button> </Button>
<Button variant="warning" disabled={ submitting } onClick={ () => submitUsernameChange() }> <Button variant="warning" disabled={ submitting } onClick={ () => submitUsernameChange() }>
{ submitting ? 'Renaming' : 'Rename me' } { submitting ? LocalizeText('usersettings.btn.renaming') : LocalizeText('usersettings.btn.rename') }
</Button> </Button>
</div> </div>
</div> </div>
@@ -135,8 +135,8 @@ export const UserSettingsView: FC<{}> = props =>
<NitroCardHeaderView headerText={ LocalizeText('widget.memenu.settings.title') } onCloseClick={ event => processAction('close_view') } /> <NitroCardHeaderView headerText={ LocalizeText('widget.memenu.settings.title') } onCloseClick={ event => processAction('close_view') } />
<NitroCardContentView className="text-black"> <NitroCardContentView className="text-black">
<div className="flex items-center gap-1 mb-2 border-b border-black/10 pb-1"> <div className="flex items-center gap-1 mb-2 border-b border-black/10 pb-1">
<button type="button" onClick={ () => setActiveTab('general') } className={ classNames('px-3 py-1 rounded text-xs font-bold cursor-pointer transition-colors', activeTab === 'general' ? 'bg-[#1e7295] text-white' : 'bg-black/5 hover:bg-black/10') }>Generale</button> <button type="button" onClick={ () => setActiveTab('general') } className={ classNames('px-3 py-1 rounded text-xs font-bold cursor-pointer transition-colors', activeTab === 'general' ? 'bg-[#1e7295] text-white' : 'bg-black/5 hover:bg-black/10') }>{ LocalizeText('usersettings.tab.general') }</button>
<button type="button" onClick={ () => setActiveTab('themes') } className={ classNames('px-3 py-1 rounded text-xs font-bold cursor-pointer transition-colors', activeTab === 'themes' ? 'bg-[#1e7295] text-white' : 'bg-black/5 hover:bg-black/10') }>Temi</button> <button type="button" onClick={ () => setActiveTab('themes') } className={ classNames('px-3 py-1 rounded text-xs font-bold cursor-pointer transition-colors', activeTab === 'themes' ? 'bg-[#1e7295] text-white' : 'bg-black/5 hover:bg-black/10') }>{ LocalizeText('usersettings.tab.themes') }</button>
</div> </div>
{ activeTab === 'general' && <> { activeTab === 'general' && <>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
@@ -162,11 +162,11 @@ export const UserSettingsView: FC<{}> = props =>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<input checked={ chatWindowEnabled } className="form-check-input" type="checkbox" onChange={ event => setChatWindowEnabled(event.target.checked) } /> <input checked={ chatWindowEnabled } className="form-check-input" type="checkbox" onChange={ event => setChatWindowEnabled(event.target.checked) } />
<Text>Enable chat window</Text> <Text>{ LocalizeText('memenu.settings.other.enable.chat.window') }</Text>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<input checked={ catalogClassicStyle } className="form-check-input" type="checkbox" onChange={ event => setCatalogClassicStyle(event.target.checked) } /> <input checked={ catalogClassicStyle } className="form-check-input" type="checkbox" onChange={ event => setCatalogClassicStyle(event.target.checked) } />
<Text>Catalogo: stile classico</Text> <Text>{ LocalizeText('memenu.settings.other.catalog.classic.style') }</Text>
</div> </div>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
@@ -208,8 +208,8 @@ export const UserSettingsView: FC<{}> = props =>
<FaUserCog size={ 12 } /> <FaUserCog size={ 12 } />
</div> </div>
<div className="flex flex-col flex-1 leading-tight"> <div className="flex flex-col flex-1 leading-tight">
<Text bold>User settings</Text> <Text bold>{ LocalizeText('usersettings.open.title') }</Text>
<Text small className="text-black/60">Password &amp; account</Text> <Text small className="text-black/60">{ LocalizeText('usersettings.open.subtitle') }</Text>
</div> </div>
<span className="text-black/30 group-hover:text-[#1e7295] text-[10px]"></span> <span className="text-black/30 group-hover:text-[#1e7295] text-[10px]"></span>
</button> </button>
@@ -217,12 +217,12 @@ export const UserSettingsView: FC<{}> = props =>
</> } </> }
{ activeTab === 'themes' && <div className="flex flex-col gap-2"> { activeTab === 'themes' && <div className="flex flex-col gap-2">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Text bold>Tema custom</Text> <Text bold>{ LocalizeText('usersettings.themes.custom') }</Text>
<select <select
value={ activeThemeId } value={ activeThemeId }
onChange={ event => selectTheme(event.target.value) } onChange={ event => selectTheme(event.target.value) }
className="form-select rounded border border-black/15 px-2 py-1 text-sm"> className="form-select rounded border border-black/15 px-2 py-1 text-sm">
<option value="">Default (nessun tema)</option> <option value="">{ LocalizeText('usersettings.themes.default_option') }</option>
{ themes.map(theme => ( { themes.map(theme => (
<option key={ theme.id } value={ theme.id }>{ theme.name }{ theme.author ? `${ theme.author }` : '' }</option> <option key={ theme.id } value={ theme.id }>{ theme.name }{ theme.author ? `${ theme.author }` : '' }</option>
)) } )) }
@@ -230,7 +230,7 @@ export const UserSettingsView: FC<{}> = props =>
</div> </div>
{ activeThemeId && manifest && manifest.pieces.length > 0 && { activeThemeId && manifest && manifest.pieces.length > 0 &&
<div className="flex flex-col gap-1 pt-1 border-t border-black/10"> <div className="flex flex-col gap-1 pt-1 border-t border-black/10">
<Text bold>Pezzi attivi</Text> <Text bold>{ LocalizeText('usersettings.themes.active_pieces') }</Text>
{ manifest.pieces.map(piece => ( { manifest.pieces.map(piece => (
<div key={ piece.id } className="flex items-center gap-1"> <div key={ piece.id } className="flex items-center gap-1">
<input className="form-check-input" type="checkbox" checked={ activeEnabled.includes(piece.id) } onChange={ () => togglePiece(piece.id) } /> <input className="form-check-input" type="checkbox" checked={ activeEnabled.includes(piece.id) } onChange={ () => togglePiece(piece.id) } />
@@ -239,9 +239,9 @@ export const UserSettingsView: FC<{}> = props =>
)) } )) }
</div> } </div> }
{ activeThemeId && !manifest && { activeThemeId && !manifest &&
<Text small className="text-black/60">Tema non valido o non raggiungibile uso il default.</Text> } <Text small className="text-black/60">{ LocalizeText('usersettings.themes.invalid') }</Text> }
{ !themes.length && { !themes.length &&
<Text small className="text-black/60">Nessun tema disponibile. Aggiungi una cartella in custom-themes/ sul server.</Text> } <Text small className="text-black/60">{ LocalizeText('usersettings.themes.none') }</Text> }
</div> } </div> }
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
+7 -25
View File
@@ -1,4 +1,3 @@
/* ── Friends spritesheet icons ── */
.nitro-friends-spritesheet { .nitro-friends-spritesheet {
background: url('@/assets/images/friends/friends-spritesheet.png') transparent no-repeat; background: url('@/assets/images/friends/friends-spritesheet.png') transparent no-repeat;
@@ -176,9 +175,6 @@
border: 0 !important; border: 0 !important;
} }
/* The header title is rendered by the shared <Text> component, which is a
<div> (not a <span>) — so target the div too, otherwise it keeps the app
default size and shows as oversized "testoni". */
& .nitro-card-accordion-set-header span, & .nitro-card-accordion-set-header span,
& .nitro-card-accordion-set-header > div { & .nitro-card-accordion-set-header > div {
font-size: 12px !important; font-size: 12px !important;
@@ -807,20 +803,9 @@
} }
} }
/* ------------------------------------------------------------------ *
* Flat (non-nested) overrides. The rules above live inside a nested
* `.nitro-friends { & ... }` block; these are written flat so they
* apply reliably and win by source order. They fix two things the
* nested rules didn't: the oversized/overflowing friend-list heads and
* the oversized accordion section titles ("testoni").
* ------------------------------------------------------------------ */
/* Friend-list avatar: clip a small box and centre the head, same proven
recipe as the messenger head. `inset: auto` cancels the component's
`inset-0`, otherwise the 130px head fills the row and overflows. */
.nitro-friends .friends-list-avatar { .nitro-friends .friends-list-avatar {
position: relative !important; position: relative !important;
width: 34px; width: 32px;
height: 36px; height: 36px;
flex-shrink: 0; flex-shrink: 0;
overflow: hidden; overflow: hidden;
@@ -828,18 +813,15 @@
.nitro-friends .friends-list-avatar .avatar-image { .nitro-friends .friends-list-avatar .avatar-image {
position: absolute !important; position: absolute !important;
inset: auto !important; inset: 0 !important;
left: 50% !important; width: 100% !important;
top: 56% !important; height: 100% !important;
width: 54px !important;
height: 54px !important;
margin: 0 !important; margin: 0 !important;
background-position: center center !important; background-size: 66px auto !important;
transform: translate(-50%, -50%) scale(.95) !important; background-position: -16px -21px !important;
transform: none !important;
} }
/* Accordion section titles are rendered by <Text> (a <div>, not a <span>),
so size the div too — otherwise they show oversized. */
.nitro-friends .nitro-card-accordion-set-header > div, .nitro-friends .nitro-card-accordion-set-header > div,
.nitro-friends .nitro-card-accordion-set-header span { .nitro-friends .nitro-card-accordion-set-header span {
font-size: 12px !important; font-size: 12px !important;
+6 -16
View File
@@ -46,11 +46,6 @@
gap: 10px; gap: 10px;
} }
/* Mirror the room infostand exactly: a 68x135 flex column (= profile-background)
that centres the avatar horizontally and clips it; the stand/overlay sit on
top as absolute layers. The avatar keeps its component default classes
(relative w-[90px] h-[130px] left-[-2px]) so it lines up with bg + stand and
isn't crooked. Do NOT absolutely position or force width/height on it. */
.nitro-extended-profile__avatar-shell { .nitro-extended-profile__avatar-shell {
width: 68px; width: 68px;
height: 135px; height: 135px;
@@ -255,20 +250,15 @@
transform: translateY(-50%); transform: translateY(-50%);
overflow: hidden; overflow: hidden;
} }
/* Same proven recipe as the messenger head: clip a small box and centre a
54x54 avatar in it. `inset: auto` cancels the component's `inset-0` so the
width/position take effect (otherwise the head overflows huge). */
.nitro-extended-profile__relationship-head .avatar-image { .nitro-extended-profile__relationship-head .avatar-image {
position: absolute !important; position: absolute !important;
inset: auto !important; inset: 0 !important;
left: 50% !important; width: 100% !important;
top: 54% !important; height: 100% !important;
width: 50px !important;
height: 50px !important;
margin: 0 !important; margin: 0 !important;
background-position: center center !important; background-size: 60px auto !important;
transform: translate(-50%, -50%) scale(.95) !important; background-position: -14px -21px !important;
transform: none !important;
} }
.nitro-extended-profile__relationship-subcopy { .nitro-extended-profile__relationship-subcopy {