diff --git a/.changeset/irc-display-nickname.md b/.changeset/irc-display-nickname.md new file mode 100644 index 00000000..8d38ce8e --- /dev/null +++ b/.changeset/irc-display-nickname.md @@ -0,0 +1,11 @@ +--- +"fuelrats.com": minor +--- + +Add ability to set IRC display nickname in user profile + +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 4e30f188..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 } from '~/store/actions/user' +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,28 +33,38 @@ 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.' + const responseError = getResponseError(response) + if (responseError) { + setError(responseError) + return responseError + } - if (HttpStatus.isClientError(meta.response.status)) { - errorMessage = payload.errors?.length ? payload.errors[0].detail : 'Client communication error' - } + return true + }, [dispatch, nicknames, user]) - if (HttpStatus.isServerError(meta.response.status)) { - errorMessage = 'Server communication error' - } + const handleSetDisplayNickname = useCallback(async (event) => { + setError(null) + const nickname = nicknames.find((nick) => { + return nick.id === event.target.name + }) - setError(errorMessage) - return errorMessage + const response = await dispatch(setDisplayNickname(nickname.id, nickname.attributes.nick)) + + const responseError = getResponseError(response) + if (responseError) { + setError(responseError) + return responseError } - return undefined - }, [dispatch, nicknames, user]) + // Refresh user profile to get updated nicknames + await dispatch(getUserProfile()) + return true + }, [dispatch, nicknames]) const nickCount = nicknames?.length const maxNicksReached = (nickCount >= MAX_NICKS) @@ -72,7 +81,7 @@ function UserNicknamesPanel () {
{ error && ( - {error} + ) }
    @@ -83,24 +92,52 @@ 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 && ( - - - - ) - } + + {nickname.attributes?.nick} + { + isDisplayNick && ( + + ) + } + +
    + { + // Only show buttons for non-display nicks + !isDisplayNick && ( + <> + + + + + + + + ) + } +
  • ) }) ?? null diff --git a/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss b/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss index 05d302c1..562cfbec 100644 --- a/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss +++ b/src/components/UserNicknamesPanel/UserNicknamesPanel.module.scss @@ -23,6 +23,17 @@ } } +.controlContainer { + display: flex; + gap: 0.5rem; +} + +.displayIcon { + margin-left: 0.5rem; + + color: #FFD700; +} + .addNicknameFloat { position: relative; 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..418e35bf 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, @@ -55,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