From 47aa2c5a6b693090290c6da6bdb43977e46aa446 Mon Sep 17 00:00:00 2001 From: ssura1 Date: Mon, 2 Feb 2026 15:51:21 +0000 Subject: [PATCH] add username taken suggestions w/ button --- public/src/client/register.js | 92 ++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/public/src/client/register.js b/public/src/client/register.js index f989901e7b..39c77cd8ab 100644 --- a/public/src/client/register.js +++ b/public/src/client/register.js @@ -135,7 +135,7 @@ define('forum/register', [ if (results.every(obj => obj.status === 'rejected')) { showSuccess(usernameInput, username_notify, successIcon); } else { - showError(usernameInput, username_notify, '[[error:username-taken]]'); + showErrorWithSuggestions(usernameInput, username_notify, username); } callback(); @@ -205,6 +205,96 @@ define('forum/register', [ }); } + async function generateUsernameSuggestions(baseUsername, count = 3) { + const suggestions = []; + const baseName = baseUsername.replace(/\d+$/, ''); // Remove trailing numbers + + // Generate simple suggestions by appending numbers + for (let i = 1; i <= count + 10; i++) { + const suggestion = baseName + i; + if (await isUsernameAvailable(suggestion)) { + suggestions.push(suggestion); + if (suggestions.length === count) { + break; + } + } + } + + // If not enough suggestions, try with underscores + if (suggestions.length < count) { + for (let i = 1; i <= count + 10; i++) { + const suggestion = baseName + '_' + i; + if (await isUsernameAvailable(suggestion)) { + suggestions.push(suggestion); + if (suggestions.length === count) { + break; + } + } + } + } + + return suggestions; + } + + async function isUsernameAvailable(username) { + const userslug = slugify(username); + + // Check username length requirements + if (username.length < ajaxify.data.minimumUsernameLength || + userslug.length < ajaxify.data.minimumUsernameLength || + username.length > ajaxify.data.maximumUsernameLength) { + return false; + } + + // Check if valid username + if (!utils.isUserNameValid(username) || !userslug) { + return false; + } + + try { + const results = await Promise.allSettled([ + api.head(`/users/bySlug/${userslug}`, {}), + api.head(`/groups/${username}`, {}), + ]); + return results.every(obj => obj.status === 'rejected'); + } catch (err) { + return false; + } + } + + function showErrorWithSuggestions(input, element, username) { + translator.translate('[[error:username-taken]]', function (msg) { + input.attr('aria-invalid', 'true'); + element.html('
' + msg + '
'); + element.parent() + .removeClass('register-success') + .addClass('register-danger'); + element.show(); + }); + validationError = true; + + // Generate and display suggestions + generateUsernameSuggestions(username).then((suggestions) => { + if (suggestions.length > 0) { + translator.translate('Try these alternatives:', function (suggestMsg) { + let suggestionsHtml = '
' + suggestMsg + '
'; + suggestions.forEach((suggestion) => { + suggestionsHtml += '' + suggestion + ''; + }); + suggestionsHtml += '
'; + element.append(suggestionsHtml); + + // Add click handlers to suggestions + $('.username-suggestion').on('click', function () { + const suggestedUsername = $(this).text(); + $('#username').val(suggestedUsername).trigger('blur'); + $('#yourUsername').text(slugify(suggestedUsername)); + }); + }); + } + }); + } + function handleLanguageOverride() { if (!app.user.uid && config.defaultLang !== config.userLang) { const formEl = $('[component="register/local"]');