From 57091d6665c51c45dcb18cc67315c043a8b98333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=B8rlie?= Date: Thu, 31 Jul 2025 13:03:45 +0200 Subject: [PATCH 1/6] feat(UserNicknamesPanel): add ability to set IRC display nickname - Add setDisplayNickname action to call PUT /nicknames/:id/display endpoint - Add star icon button to set any nickname as display nickname - Update UI to show star/delete buttons only for non-display nicknames - Add faStar icon to fontawesome library - Refresh user profile after setting display nickname to update UI --- .../UserNicknamesPanel/UserNicknamesPanel.js | 63 ++++++++++++++----- .../UserNicknamesPanel.module.scss | 5 ++ src/store/actionTypes.js | 1 + src/store/actions/user.js | 11 ++++ src/util/fontawesome/library.js | 1 + 5 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/components/UserNicknamesPanel/UserNicknamesPanel.js b/src/components/UserNicknamesPanel/UserNicknamesPanel.js index 4e30f188..9582810f 100644 --- a/src/components/UserNicknamesPanel/UserNicknamesPanel.js +++ b/src/components/UserNicknamesPanel/UserNicknamesPanel.js @@ -7,7 +7,7 @@ import { useDispatch, useSelector } from 'react-redux' import ConfirmActionButton from '~/components/ConfirmActionButton' import AddNicknameForm from '~/components/Forms/AddNicknameForm/AddNicknameForm' import MessageBox from '~/components/MessageBox' -import { deleteNickname } from '~/store/actions/user' +import { deleteNickname, setDisplayNickname, getUserProfile } from '~/store/actions/user' import { selectUserById, withCurrentUserId, @@ -57,6 +57,19 @@ function UserNicknamesPanel () { return undefined }, [dispatch, nicknames, user]) + const handleSetDisplayNickname = useCallback(async (event) => { + const nickname = nicknames.find((nick) => { + return nick.id === event.target.name + }) + + const response = await dispatch(setDisplayNickname(nickname.id, nickname.attributes.nick)) + + if (!isError(response)) { + // Refresh user profile to get updated nicknames + await dispatch(getUserProfile()) + } + }, [dispatch, nicknames]) + const nickCount = nicknames?.length const maxNicksReached = (nickCount >= MAX_NICKS) @@ -83,24 +96,42 @@ function UserNicknamesPanel () { } { nicknames?.map((nickname) => { + const isDisplayNick = nickname.attributes?.display === nickname.attributes?.nick return (
  • {nickname.attributes?.nick} - { - // Only render for additional nicks, prevent for display nick. - nickname.attributes?.display !== nickname.attributes?.nick && ( - - - - ) - } +
    + { + // Only show set display button for non-display nicks + !isDisplayNick && ( + + + + ) + } + { + // Only render for additional nicks, prevent for display nick. + !isDisplayNick && ( + + + + ) + } +
  • ) }) ?? null diff --git a/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss b/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss index 05d302c1..f2a7a986 100644 --- a/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss +++ b/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss @@ -21,6 +21,11 @@ span { word-break: break-word; } + + div { + display: flex; + gap: 0.5rem; + } } .addNicknameFloat { diff --git a/src/store/actionTypes.js b/src/store/actionTypes.js index 27f36da7..60ffdacb 100644 --- a/src/store/actionTypes.js +++ b/src/store/actionTypes.js @@ -30,6 +30,7 @@ const nicknames = { read: 'nicknames/read', create: 'nicknames/create', delete: 'nicknames/delete', + setDisplay: 'nicknames/setDisplay', } diff --git a/src/store/actions/user.js b/src/store/actions/user.js index 1eb070be..02581569 100644 --- a/src/store/actions/user.js +++ b/src/store/actions/user.js @@ -103,3 +103,14 @@ export const changeEmail = ({ id, ...data }) => { }, ) } + +export const setDisplayNickname = (nicknameId, displayNick) => { + return frApiRequest( + actionTypes.nicknames.setDisplay, + { + url: `/nicknames/${nicknameId}/display`, + method: 'put', + data: createRequestBody('nicknames', { displayNick }), + }, + ) +} diff --git a/src/util/fontawesome/library.js b/src/util/fontawesome/library.js index 85c10420..7003a3cc 100644 --- a/src/util/fontawesome/library.js +++ b/src/util/fontawesome/library.js @@ -36,6 +36,7 @@ export { faSignature, faSpaceShuttle, faSpinner, + faStar, faSync, faTimes, faTimesCircle, From 82a1a37a9435300a717a5739b060b7033f08cab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=B8rlie?= Date: Thu, 31 Jul 2025 13:09:26 +0200 Subject: [PATCH 2/6] chore: add changeset for IRC display nickname feature --- .changeset/irc-display-nickname.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/irc-display-nickname.md diff --git a/.changeset/irc-display-nickname.md b/.changeset/irc-display-nickname.md new file mode 100644 index 00000000..6d73e511 --- /dev/null +++ b/.changeset/irc-display-nickname.md @@ -0,0 +1,9 @@ +--- +"fuelrats.com": minor +--- + +Add ability to set IRC display nickname in user profile + +- Users can now select which of their registered IRC nicknames should be their display nickname +- Added a star icon button next to each non-display nickname to set it as the display nickname +- The current display nickname is indicated by the absence of action buttons \ No newline at end of file From 1d5a84586938faacd784984a7e4e84595f269025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=B8rlie?= Date: Thu, 31 Jul 2025 13:12:34 +0200 Subject: [PATCH 3/6] fix(UserNicknamesPanel): remove trailing spaces to fix eslint --- src/components/UserNicknamesPanel/UserNicknamesPanel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/UserNicknamesPanel/UserNicknamesPanel.js b/src/components/UserNicknamesPanel/UserNicknamesPanel.js index 9582810f..5b18c8c2 100644 --- a/src/components/UserNicknamesPanel/UserNicknamesPanel.js +++ b/src/components/UserNicknamesPanel/UserNicknamesPanel.js @@ -61,9 +61,9 @@ function UserNicknamesPanel () { const nickname = nicknames.find((nick) => { return nick.id === event.target.name }) - + const response = await dispatch(setDisplayNickname(nickname.id, nickname.attributes.nick)) - + if (!isError(response)) { // Refresh user profile to get updated nicknames await dispatch(getUserProfile()) From d6d113d9828acb897d41ad7d403b511593ea9d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=B8rlie?= Date: Tue, 2 Sep 2025 18:40:59 +0200 Subject: [PATCH 4/6] refactor(UserNicknamesPanel): address PR review feedback - Consolidate conditional rendering for nickname buttons - Use filled/outline star icons to show display status - Add hover tooltips for icon buttons - Replace generic div selector with .controlContainer class --- .../UserNicknamesPanel/UserNicknamesPanel.js | 60 +++++++++++-------- .../UserNicknamesPanel.module.scss | 13 ++-- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/components/UserNicknamesPanel/UserNicknamesPanel.js b/src/components/UserNicknamesPanel/UserNicknamesPanel.js index 5b18c8c2..8714e85d 100644 --- a/src/components/UserNicknamesPanel/UserNicknamesPanel.js +++ b/src/components/UserNicknamesPanel/UserNicknamesPanel.js @@ -99,36 +99,46 @@ function UserNicknamesPanel () { const isDisplayNick = nickname.attributes?.display === nickname.attributes?.nick return (
  • - {nickname.attributes?.nick} -
    + + {nickname.attributes?.nick} { - // Only show set display button for non-display nicks - !isDisplayNick && ( - - - + isDisplayNick && ( + ) } + +
    { - // Only render for additional nicks, prevent for display nick. + // Only show buttons for non-display nicks !isDisplayNick && ( - - - + <> + + + + + + + ) }
    diff --git a/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss b/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss index f2a7a986..03ad8d0b 100644 --- a/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss +++ b/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss @@ -21,11 +21,16 @@ span { word-break: break-word; } +} - div { - display: flex; - gap: 0.5rem; - } +.controlContainer { + display: flex; + gap: 0.5rem; +} + +.displayIcon { + margin-left: 0.5rem; + color: #ffd700; } .addNicknameFloat { From 7bb727c481f15e0157c32b9db8a1a86df13b8d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=B8rlie?= Date: Tue, 2 Sep 2025 18:43:41 +0200 Subject: [PATCH 5/6] fix: resolve eslint and stylelint issues - Fix prop ordering in UserNicknamesPanel component - Fix color hex case and property order in styles --- .../UserNicknamesPanel/UserNicknamesPanel.js | 10 +++++----- .../UserNicknamesPanel/UserNicknamesPanel.module.scss | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/UserNicknamesPanel/UserNicknamesPanel.js b/src/components/UserNicknamesPanel/UserNicknamesPanel.js index 8714e85d..5df53a6d 100644 --- a/src/components/UserNicknamesPanel/UserNicknamesPanel.js +++ b/src/components/UserNicknamesPanel/UserNicknamesPanel.js @@ -104,8 +104,8 @@ function UserNicknamesPanel () { { isDisplayNick && ( ) @@ -122,9 +122,9 @@ function UserNicknamesPanel () { confirmSubText="" denyButtonText="Cancel" name={nickname.id} + title="Set as display nickname" onConfirm={handleSetDisplayNickname} - onConfirmText="" - title="Set as display nickname"> + onConfirmText=""> + onConfirmText=""> diff --git a/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss b/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss index 03ad8d0b..562cfbec 100644 --- a/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss +++ b/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss @@ -30,7 +30,8 @@ .displayIcon { margin-left: 0.5rem; - color: #ffd700; + + color: #FFD700; } .addNicknameFloat { From 64c24334b5494e4dce9fe7262a8f297a22cfbf81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=B8rlie?= Date: Mon, 8 Sep 2025 12:37:11 +0200 Subject: [PATCH 6/6] refactor(UserNicknamesPanel): improve error handling and icon implementation - Add @fortawesome/free-regular-svg-icons for outline star icon - Use filled/outline stars to distinguish current vs settable display nicknames - Add hover tooltips to icons for better UX - Replace custom error handling with standardized getResponseError utility - Create NicknameErrorBox for user-friendly error messages (404, 409, 422) - Clear errors at start of operations to prevent stale error states - Fix loading states to return proper boolean values --- .changeset/irc-display-nickname.md | 8 ++-- package.json | 1 + .../UserNicknamesPanel/NicknameErrorBox.js | 38 ++++++++++++++++ .../UserNicknamesPanel/UserNicknamesPanel.js | 44 +++++++++---------- src/util/fontawesome/library.js | 4 ++ yarn.lock | 17 +++++++ 6 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 src/components/UserNicknamesPanel/NicknameErrorBox.js diff --git a/.changeset/irc-display-nickname.md b/.changeset/irc-display-nickname.md index 6d73e511..8d38ce8e 100644 --- a/.changeset/irc-display-nickname.md +++ b/.changeset/irc-display-nickname.md @@ -4,6 +4,8 @@ Add ability to set IRC display nickname in user profile -- Users can now select which of their registered IRC nicknames should be their display nickname -- Added a star icon button next to each non-display nickname to set it as the display nickname -- The current display nickname is indicated by the absence of action buttons \ No newline at end of file +Users can now choose which of their registered IRC nicknames appears as their primary display name. In the IRC Nicknames section of your profile: + +- A filled star indicates your current display nickname +- Click the outline star next to any other nickname to set it as your new display nickname +- Hover over the star icons for helpful tooltips \ No newline at end of file diff --git a/package.json b/package.json index 42efe0bb..48a687be 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@fingerprintjs/fingerprintjs": "^3.3.1", "@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/free-brands-svg-icons": "^5.15.4", + "@fortawesome/free-regular-svg-icons": "^7.0.1", "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.16", "@fuelrats/next-adorable-avatars": "^0.4.0", diff --git a/src/components/UserNicknamesPanel/NicknameErrorBox.js b/src/components/UserNicknamesPanel/NicknameErrorBox.js new file mode 100644 index 00000000..2ced900f --- /dev/null +++ b/src/components/UserNicknamesPanel/NicknameErrorBox.js @@ -0,0 +1,38 @@ +import { HttpStatus } from '@fuelrats/web-util/http' +import PropTypes from 'prop-types' + +import ApiErrorBox from '~/components/MessageBox/ApiErrorBox' + + +function getErrorText (error) { + switch (error.code) { + case HttpStatus.CONFLICT: + return 'Nickname already registered.' + + case HttpStatus.NOT_FOUND: + return 'Nickname not found or no longer exists.' + + case HttpStatus.UNPROCESSABLE_ENTITY: + return 'Nickname format is invalid.' + + default: + return undefined + } +} + +function NicknameErrorBox (props) { + const { error } = props + + if (!error) { + return null + } + + return () +} + +NicknameErrorBox.propTypes = { + error: PropTypes.object, +} + + +export default NicknameErrorBox diff --git a/src/components/UserNicknamesPanel/UserNicknamesPanel.js b/src/components/UserNicknamesPanel/UserNicknamesPanel.js index 5df53a6d..72528764 100644 --- a/src/components/UserNicknamesPanel/UserNicknamesPanel.js +++ b/src/components/UserNicknamesPanel/UserNicknamesPanel.js @@ -1,19 +1,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { HttpStatus } from '@fuelrats/web-util/http' -import { isError } from 'flux-standard-action' import { useCallback, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import ConfirmActionButton from '~/components/ConfirmActionButton' import AddNicknameForm from '~/components/Forms/AddNicknameForm/AddNicknameForm' -import MessageBox from '~/components/MessageBox' import { deleteNickname, setDisplayNickname, getUserProfile } from '~/store/actions/user' import { selectUserById, withCurrentUserId, selectNicknamesByUserId, } from '~/store/selectors' +import getResponseError from '~/util/getResponseError' +import NicknameErrorBox from './NicknameErrorBox' import styles from './UserNicknamesPanel.module.scss' @@ -34,40 +33,37 @@ function UserNicknamesPanel () { const user = useSelector(withCurrentUserId(selectUserById)) const handleDeleteNickname = useCallback(async (event) => { + setError(null) const response = await dispatch(deleteNickname(user, nicknames.find((nick) => { return nick.id === event.target.name }))) - if (isError(response)) { - const { meta, payload } = response - let errorMessage = 'Unknown error occurred.' - - if (HttpStatus.isClientError(meta.response.status)) { - errorMessage = payload.errors?.length ? payload.errors[0].detail : 'Client communication error' - } - - if (HttpStatus.isServerError(meta.response.status)) { - errorMessage = 'Server communication error' - } - - setError(errorMessage) - return errorMessage + const responseError = getResponseError(response) + if (responseError) { + setError(responseError) + return responseError } - return undefined + return true }, [dispatch, nicknames, user]) const handleSetDisplayNickname = useCallback(async (event) => { + setError(null) const nickname = nicknames.find((nick) => { return nick.id === event.target.name }) const response = await dispatch(setDisplayNickname(nickname.id, nickname.attributes.nick)) - if (!isError(response)) { - // Refresh user profile to get updated nicknames - await dispatch(getUserProfile()) + const responseError = getResponseError(response) + if (responseError) { + setError(responseError) + return responseError } + + // Refresh user profile to get updated nicknames + await dispatch(getUserProfile()) + return true }, [dispatch, nicknames]) const nickCount = nicknames?.length @@ -85,7 +81,7 @@ function UserNicknamesPanel () {
    { error && ( - {error} + ) }
      @@ -125,7 +121,7 @@ function UserNicknamesPanel () { title="Set as display nickname" onConfirm={handleSetDisplayNickname} onConfirmText=""> - + - + ) diff --git a/src/util/fontawesome/library.js b/src/util/fontawesome/library.js index 7003a3cc..418e35bf 100644 --- a/src/util/fontawesome/library.js +++ b/src/util/fontawesome/library.js @@ -56,3 +56,7 @@ export { faPlaystation, faXbox, } from '@fortawesome/free-brands-svg-icons' + +export { + faStar as farStar, +} from '@fortawesome/free-regular-svg-icons' diff --git a/yarn.lock b/yarn.lock index c21f1043..abbab6cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2015,6 +2015,13 @@ __metadata: languageName: node linkType: hard +"@fortawesome/fontawesome-common-types@npm:7.0.1": + version: 7.0.1 + resolution: "@fortawesome/fontawesome-common-types@npm:7.0.1" + checksum: 64f7b9fb2a62220d6816c8973836143498c3e7b86559f4db63263b1d5bf98a2f5234da5a66fb6a0609f37b34a6847a5a443b037c74d131aed5e56d9c36d9956c + languageName: node + linkType: hard + "@fortawesome/fontawesome-common-types@npm:^0.2.36": version: 0.2.36 resolution: "@fortawesome/fontawesome-common-types@npm:0.2.36" @@ -2040,6 +2047,15 @@ __metadata: languageName: node linkType: hard +"@fortawesome/free-regular-svg-icons@npm:^7.0.1": + version: 7.0.1 + resolution: "@fortawesome/free-regular-svg-icons@npm:7.0.1" + dependencies: + "@fortawesome/fontawesome-common-types": 7.0.1 + checksum: 21f479508d58e1f48f928808b6f1553cb5839571149c90ad43c3601dfbd0ff67519d6913f3818397db9e352e40631a6f4aa110dfb7b34cc81d807f3f4d4da513 + languageName: node + linkType: hard + "@fortawesome/free-solid-svg-icons@npm:^5.15.4": version: 5.15.4 resolution: "@fortawesome/free-solid-svg-icons@npm:5.15.4" @@ -5852,6 +5868,7 @@ __metadata: "@fingerprintjs/fingerprintjs": ^3.3.1 "@fortawesome/fontawesome-svg-core": ^1.2.36 "@fortawesome/free-brands-svg-icons": ^5.15.4 + "@fortawesome/free-regular-svg-icons": ^7.0.1 "@fortawesome/free-solid-svg-icons": ^5.15.4 "@fortawesome/react-fontawesome": ^0.1.16 "@fuelrats/babel-plugin-classnames": ^0.3.0