diff --git a/public/i18n/de.json b/public/i18n/de.json
index e3f42ef..73a67a2 100644
--- a/public/i18n/de.json
+++ b/public/i18n/de.json
@@ -29,7 +29,9 @@
"ERROR_GENERIC": "Registrierung fehlgeschlagen. Bitte erneut versuchen.",
"ERROR_EMAIL_IN_USE": "Diese Email-Adresse ist bereits registriert.",
"ERROR_INVALID_INVITE_CODE": "Der Einladungscode ist ungültig oder wurde bereits verwendet.",
- "ERROR_STEAM_ID_IN_USE": "Diese Steam-ID ist bereits mit einem anderen Konto verknüpft."
+ "ERROR_STEAM_ID_IN_USE": "Diese Steam-ID ist bereits mit einem anderen Konto verknüpft.",
+ "DISPLAY_NAME": "Anzeigename (optional)",
+ "ERROR_DISPLAY_NAME_IN_USE": "Dieser Anzeigename ist schon vergeben."
},
"SETTINGS": {
"TITLE": "Einstellungen",
@@ -50,7 +52,13 @@
"STEAM_ID": "Steam-ID",
"SAVE_STEAM_ID": "Steam-ID speichern",
"SAVED": "Einstellungen gespeichert.",
- "ERROR_SAVE": "Einstellungen konnten nicht gespeichert werden."
+ "ERROR_SAVE": "Einstellungen konnten nicht gespeichert werden.",
+ "PROFILE": "Profil",
+ "DISPLAY_NAME": "Anzeigename",
+ "DISPLAY_NAME_PLACEHOLDER": "z. B. Lobo",
+ "DISPLAY_NAME_HINT": "3–24 Zeichen: Buchstaben, Zahlen, Leerzeichen, _ . -",
+ "SAVE_PROFILE": "Profil speichern",
+ "ERROR_DISPLAY_NAME_IN_USE": "Dieser Anzeigename ist schon vergeben."
},
"VALIDATION": {
"EMAIL_REQUIRED": "Email ist erforderlich.",
@@ -61,7 +69,8 @@
"INVITE_CODE_REQUIRED": "Einladungscode ist erforderlich.",
"STEAM_ID_REQUIRED": "Steam-ID ist erforderlich.",
"STEAM_ID_INVALID": "Steam-ID muss eine positive Zahl sein.",
- "LANGUAGE_INVALID": "Nicht unterstützte Sprache."
+ "LANGUAGE_INVALID": "Nicht unterstützte Sprache.",
+ "DISPLAY_NAME_INVALID": "3–24 Zeichen: Buchstaben, Zahlen, Leerzeichen, _ . - (kein Leerzeichen am Anfang oder Ende)."
},
"INFO": {
"ERROR": {
diff --git a/public/i18n/en.json b/public/i18n/en.json
index 3bab297..334a73d 100644
--- a/public/i18n/en.json
+++ b/public/i18n/en.json
@@ -29,7 +29,9 @@
"ERROR_GENERIC": "Registration failed. Please try again.",
"ERROR_EMAIL_IN_USE": "This email address is already registered.",
"ERROR_INVALID_INVITE_CODE": "The invite code is invalid or has already been used.",
- "ERROR_STEAM_ID_IN_USE": "This Steam ID is already linked to another account."
+ "ERROR_STEAM_ID_IN_USE": "This Steam ID is already linked to another account.",
+ "DISPLAY_NAME": "Display name (optional)",
+ "ERROR_DISPLAY_NAME_IN_USE": "This display name is already taken."
},
"SETTINGS": {
"TITLE": "Settings",
@@ -50,7 +52,13 @@
"STEAM_ID": "Steam ID",
"SAVE_STEAM_ID": "Save Steam ID",
"SAVED": "Settings saved.",
- "ERROR_SAVE": "Could not save settings."
+ "ERROR_SAVE": "Could not save settings.",
+ "PROFILE": "Profile",
+ "DISPLAY_NAME": "Display name",
+ "DISPLAY_NAME_PLACEHOLDER": "e.g. Lobo",
+ "DISPLAY_NAME_HINT": "3–24 characters: letters, digits, spaces, _ . -",
+ "SAVE_PROFILE": "Save profile",
+ "ERROR_DISPLAY_NAME_IN_USE": "This display name is already taken."
},
"VALIDATION": {
"EMAIL_REQUIRED": "Email is required.",
@@ -61,7 +69,8 @@
"INVITE_CODE_REQUIRED": "Invite code is required.",
"STEAM_ID_REQUIRED": "Steam ID is required.",
"STEAM_ID_INVALID": "Steam ID must be a positive number.",
- "LANGUAGE_INVALID": "Unsupported language."
+ "LANGUAGE_INVALID": "Unsupported language.",
+ "DISPLAY_NAME_INVALID": "3–24 characters: letters, digits, spaces, _ . - (no space at the start or end)."
},
"INFO": {
"ERROR": {
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
index 1274b26..a5bd0a5 100644
--- a/src/pages/Dashboard.jsx
+++ b/src/pages/Dashboard.jsx
@@ -127,7 +127,7 @@ const Dashboard = () => {
{t('DASHBOARD.WELCOME_BACK')}
- {t('DASHBOARD.HELLO')} {userData?.email?.split('@')[0]} 👋
+ {t('DASHBOARD.HELLO')} {userData?.displayName ?? userData?.email?.split('@')[0]} 👋
{hasSteamId ? (
diff --git a/src/pages/RegistrationPage.jsx b/src/pages/RegistrationPage.jsx
index f0acf58..20ac609 100644
--- a/src/pages/RegistrationPage.jsx
+++ b/src/pages/RegistrationPage.jsx
@@ -17,6 +17,7 @@ const BACKEND_ERROR_KEY = {
EMAIL_ALREADY_IN_USE: 'REGISTRATION.ERROR_EMAIL_IN_USE',
INVALID_INVITATION_CODE: 'REGISTRATION.ERROR_INVALID_INVITE_CODE',
STEAM_ID_ALREADY_IN_USE: 'REGISTRATION.ERROR_STEAM_ID_IN_USE',
+ DISPLAY_NAME_ALREADY_IN_USE: 'REGISTRATION.ERROR_DISPLAY_NAME_IN_USE',
};
export default function RegistrationPage() {
@@ -36,6 +37,7 @@ export default function RegistrationPage() {
passwordConfirmation: '',
inviteCode: '',
steamId: '',
+ displayName: '',
},
});
@@ -51,6 +53,7 @@ export default function RegistrationPage() {
password: values.password,
inviteCode: values.inviteCode,
steamId: values.steamId,
+ displayName: values.displayName,
}),
});
@@ -122,6 +125,16 @@ export default function RegistrationPage() {
/>
+
+
+
+
{
@@ -86,6 +87,7 @@ export default function SettingsPage() {
useDarkTheme: !!data.settings?.useDarkTheme,
language: data.settings?.language || 'en',
steamId: data.steamId ?? '',
+ displayName: data.displayName ?? '',
});
} catch (err) {
console.error('Settings load error:', err);
@@ -146,6 +148,7 @@ export default function SettingsPage() {
useDarkTheme: next,
language: i18n.language,
steamId: profile?.steamId ?? null,
+ displayName: profile?.displayName ?? null,
});
};
@@ -155,17 +158,19 @@ export default function SettingsPage() {
useDarkTheme: isDarkMode,
language: next,
steamId: profile?.steamId ?? null,
+ displayName: profile?.displayName ?? null,
});
};
- const onSubmitSteamId = async (values) => {
+ const onSubmitProfile = async (values) => {
const result = await persistSettings({
useDarkTheme: isDarkMode,
language: i18n.language,
steamId: values.steamId,
+ displayName: values.displayName,
});
if (result.ok) {
- setProfile((p) => (p ? {...p, steamId: values.steamId} : p));
+ setProfile((p) => (p ? {...p, steamId: values.steamId, displayName: values.displayName} : p));
}
};
@@ -240,10 +245,21 @@ export default function SettingsPage() {
- {t('SETTINGS.STEAM')}
+ {t('SETTINGS.PROFILE')}
-
diff --git a/src/utils/validation.js b/src/utils/validation.js
index 24addee..b7e0a35 100644
--- a/src/utils/validation.js
+++ b/src/utils/validation.js
@@ -22,6 +22,17 @@ const steamIdField = z
.min(1, 'VALIDATION.STEAM_ID_REQUIRED')
.regex(/^\d{17}$/, 'VALIDATION.STEAM_ID_INVALID');
+// Same rule as the Auth service: 3-24 letters, digits, space, _ . - and no space at the ends.
+// An empty field means "no display name" and is sent as null.
+const displayNameField = z
+ .string()
+ .trim()
+ .optional()
+ .transform((v) => (v ? v : null))
+ .refine((v) => v === null || /^[\p{L}\p{N}_.-][\p{L}\p{N} _.-]{1,22}[\p{L}\p{N}_.-]$/u.test(v), {
+ message: 'VALIDATION.DISPLAY_NAME_INVALID',
+ });
+
export const registrationSchema = z
.object({
email: emailField,
@@ -29,6 +40,7 @@ export const registrationSchema = z
passwordConfirmation: z.string().min(1, 'VALIDATION.PASSWORD_CONFIRMATION_REQUIRED'),
inviteCode: inviteCodeField,
steamId: steamIdField,
+ displayName: displayNameField,
})
.refine((d) => d.password === d.passwordConfirmation, {
path: ['passwordConfirmation'],
@@ -39,4 +51,5 @@ export const settingsSchema = z.object({
useDarkTheme: z.boolean(),
language: z.enum(['en', 'de'], {message: 'VALIDATION.LANGUAGE_INVALID'}),
steamId: steamIdField,
+ displayName: displayNameField,
});
diff --git a/src/utils/validation.test.js b/src/utils/validation.test.js
index 310e76c..f24445e 100644
--- a/src/utils/validation.test.js
+++ b/src/utils/validation.test.js
@@ -28,3 +28,20 @@ describe('steamId validation', () => {
expect(result.steamId).toBe('76561198005679788');
});
});
+
+describe('displayName validation', () => {
+ const withName = (displayName) => ({...settings('76561198005679788'), displayName});
+
+ it('turns an empty field into null (no display name)', () => {
+ expect(settingsSchema.parse(withName(' ')).displayName).toBeNull();
+ });
+
+ it('trims and keeps a valid name', () => {
+ expect(settingsSchema.parse(withName(' Lobo Corp_1 ')).displayName).toBe('Lobo Corp_1');
+ });
+
+ it.each(['ab', '