diff --git a/.config/headers.config.js b/.config/headers.config.js index 3c9ec1fea..1fd77ce54 100644 --- a/.config/headers.config.js +++ b/.config/headers.config.js @@ -20,7 +20,11 @@ module.exports = (env) => { 'style-src': ["'self'", "'unsafe-inline'", 'fonts.googleapis.com'], 'img-src': ["'self'", '*.wp.com', 'blob:', 'data:'], 'frame-src': ['https://js.stripe.com'], + 'child-src': false, + 'prefetch-src': false, + 'worker-src': false, }, + xssProtection: '1; mode=block;', }), }, ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 76dd6f34a..b0633d600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ For detailed rules of this file, see [Changelog Rules](#changelog-rules) ### ✨ Added * A new page to assist with editing QMS locale files has been added. - [#370][] * A general purpose JSON object editor has been implemented. this should come in handy at some point! - [#370][] +* User avatars are now customizable through our new Avatar uploader! - [#332][], [#375][] ### ⚡ Changed * Error readout for unknown API Errors has been improved. - [#328][] @@ -40,9 +41,11 @@ For detailed rules of this file, see [Changelog Rules](#changelog-rules) [#328]: https://github.com/FuelRats/fuelrats.com/pull/328 [#329]: https://github.com/FuelRats/fuelrats.com/pull/329 [#330]: https://github.com/FuelRats/fuelrats.com/pull/330 +[#332]: https://github.com/FuelRats/fuelrats.com/pull/332 [#333]: https://github.com/FuelRats/fuelrats.com/pull/333 [#370]: https://github.com/FuelRats/fuelrats.com/pull/370 [#373]: https://github.com/FuelRats/fuelrats.com/pull/373 +[#375]: https://github.com/FuelRats/fuelrats.com/pull/375 [Unreleased]: https://github.com/FuelRats/fuelrats.com/compare/v2.13.0...HEAD diff --git a/src/components/MessageBox/MessageBox.js b/src/components/MessageBox/MessageBox.js index f25d58e34..7cbdd24ac 100644 --- a/src/components/MessageBox/MessageBox.js +++ b/src/components/MessageBox/MessageBox.js @@ -31,7 +31,7 @@ function MessageBox (props) { } return ( -
+
) } - +
{displayRat?.attributes?.name ?? email.split('@')[0]} diff --git a/src/components/ProfileUserAvatar/ProfileUserAvatar.js b/src/components/ProfileUserAvatar/ProfileUserAvatar.js index 1747799fe..0e92212f7 100644 --- a/src/components/ProfileUserAvatar/ProfileUserAvatar.js +++ b/src/components/ProfileUserAvatar/ProfileUserAvatar.js @@ -1,15 +1,22 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import Image from 'next/image' -import { useState, useCallback } from 'react' +import PropTypes from 'prop-types' +import { useState, useCallback, useMemo } from 'react' import useSelectorWithProps from '~/hooks/useSelectorWithProps' -import { selectAvatarByUserId, withCurrentUserId } from '~/store/selectors' +import { selectAvatarUrlByUserId, withCurrentUserId } from '~/store/selectors' import UploadAvatarModal from '../UploadAvatarModal' import styles from './ProfileUserAvatar.module.scss' -function ProfileUserAvatar () { - const userAvatar = useSelectorWithProps({ size: 170 }, withCurrentUserId(selectAvatarByUserId)) +const faIconLgSize = 100 +const faIconMdSize = 64 + +function ProfileUserAvatar ({ + canEdit, + size = 170, +}) { + const userAvatarUrl = useSelectorWithProps({ size }, withCurrentUserId(selectAvatarUrlByUserId)) const [showUploadAvatar, setShowUploadAvatar] = useState(false) @@ -19,29 +26,61 @@ function ProfileUserAvatar () { }) }, []) + const handleAvatarModalClose = useCallback(() => { + return setShowUploadAvatar(false) + }, []) + + const sizeMeta = useMemo(() => { + let icon = undefined + + if (size >= faIconLgSize) { + icon = '3x' + } else if (size >= faIconMdSize) { + icon = '2x' + } + + return { + style: { + width: `${size}px`, + height: `${size}px`, + }, + icon, + } + }, [size]) + return ( <>
-
+
User's avatar -
-
-
- + height={size} + src={userAvatarUrl} + width={size} /> + { + canEdit && ( + + ) + }
+ onClose={handleAvatarModalClose} /> ) } +ProfileUserAvatar.propTypes = { + canEdit: PropTypes.bool, + size: PropTypes.number, +} + export default ProfileUserAvatar diff --git a/src/components/ProfileUserAvatar/ProfileUserAvatar.module.scss b/src/components/ProfileUserAvatar/ProfileUserAvatar.module.scss index 2d5127d43..fda9c053c 100644 --- a/src/components/ProfileUserAvatar/ProfileUserAvatar.module.scss +++ b/src/components/ProfileUserAvatar/ProfileUserAvatar.module.scss @@ -1,18 +1,30 @@ @import '../../scss/colors'; .userAvatar { - position: relative; - grid-area: avatar; + + :global(.avatar) { + position: relative; + } } .userAvatarEdit { + --background-color: #{rgba($black, 0.7)}; + --background-color-hover: var(--background-color); + backdrop-filter: blur(5px); + position: absolute; top: 0; left: 0; + z-index: 1; - width: 170px; - height: 170px; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + + border: 0.2rem solid $red; + border-radius: 50%; opacity: 0; transition: opacity 0.2s; @@ -24,28 +36,6 @@ } } -.editBack { - border: 0.2rem solid $red; - border-radius: 50%; - - background: $white; - opacity: 0.8; -} - -.editFace { - margin: 0; - padding: 0; - - border: none; - - color: $black; - background: none; -} - .editFace svg { - position: absolute; - right: 0; - bottom: 0; - - color: $black; + color: $red; } diff --git a/src/components/Slider/Slider.js b/src/components/Slider/Slider.js new file mode 100644 index 000000000..f44102db0 --- /dev/null +++ b/src/components/Slider/Slider.js @@ -0,0 +1,74 @@ +/*** + * The renderSliderHandle function within the following component is based on the default handle prop value in . + * https://github.com/react-component/slider/blob/master/src/common/createSlider.tsx#L32 + * + * It is licensed under the MIT License below. + * + * The MIT License (MIT) + * Copyright (c) 2015-present Alipay.com, https://www.alipay.com/ + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import RCSlider, { Handle } from 'rc-slider' +import { useCallback } from 'react' + + + + + +export default function Slider (props) { + const { + Component = RCSlider, + handleIcon: iconId, + iconProps = {}, + ...restProps + } = props + + const renderSliderHandle = useCallback((sliderProps) => { + const { index, ...restSliderProps } = sliderProps + delete restSliderProps.dragging + if (restSliderProps.value === null) { + return null + } + + return ( + + { + iconId && ( + + ) + } + + ) + }, [iconProps, iconId]) + + return ( + + ) +} diff --git a/src/components/Slider/index.js b/src/components/Slider/index.js new file mode 100644 index 000000000..19a6da94b --- /dev/null +++ b/src/components/Slider/index.js @@ -0,0 +1 @@ +export { default } from './Slider' diff --git a/src/components/Switch/Switch.module.scss b/src/components/Switch/Switch.module.scss index a315ec4a7..af9e6bbde 100644 --- a/src/components/Switch/Switch.module.scss +++ b/src/components/Switch/Switch.module.scss @@ -75,6 +75,8 @@ border-radius: 50%; + color: $black; + will-change: transform; background: $red; diff --git a/src/components/UploadAvatarModal/UploadAvatarMessageBox.js b/src/components/UploadAvatarModal/UploadAvatarMessageBox.js index 0128496ad..c3496dbf8 100644 --- a/src/components/UploadAvatarModal/UploadAvatarMessageBox.js +++ b/src/components/UploadAvatarModal/UploadAvatarMessageBox.js @@ -19,23 +19,27 @@ function getErrorText (error) { } function UploadAvatarMessageBox (props) { - const { result } = props + const { result, className } = props return result.success ? ( - - {'Avatar Updated!'} + + {'You avatar has been updated!'} +
+ {'This window will now automatically close...'}
) : ( ) } UploadAvatarMessageBox.propTypes = { - result: PropTypes.object, + className: PropTypes.string, + result: PropTypes.object.isRequired, } export default UploadAvatarMessageBox diff --git a/src/components/UploadAvatarModal/UploadAvatarModal.js b/src/components/UploadAvatarModal/UploadAvatarModal.js index c0055e18c..e62888376 100644 --- a/src/components/UploadAvatarModal/UploadAvatarModal.js +++ b/src/components/UploadAvatarModal/UploadAvatarModal.js @@ -1,11 +1,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import PropTypes from 'prop-types' -import Slider from 'rc-slider' import { useState, useCallback } from 'react' import Cropper from 'react-easy-crop' import { useDispatch } from 'react-redux' import asModal, { ModalContent, ModalFooter } from '~/components/asModal' +import ProfileUserAvatar from '~/components/ProfileUserAvatar' +import Slider from '~/components/Slider' import { updateAvatar } from '~/store/actions/user' import getResponseError from '~/util/getResponseError' @@ -17,6 +18,19 @@ const MAX_FILE_SIZE = 26214400 // Server upload max is 25M const SUBMIT_AUTO_CLOSE_DELAY_TIME = 3000 const HALF_CIRCLE = 180 // Conversion of rat to deg +// eslint-disable-next-line no-magic-numbers -- Numbers correspond to degree marks to be placed +const rotationMarks = [0, 45, 90, 135, 180].reduce((acc, value) => { + acc[0 - value] = {} + acc[value] = {} + return acc +}, {}) + +// eslint-disable-next-line no-magic-numbers -- Numbers correspond to zoom marks +const zoomMarks = [1, 1.5, 2, 2.5, 3].reduce((acc, value) => { + acc[value] = {} + return acc +}, {}) + function UploadAvatarModal (props) { const { @@ -24,8 +38,9 @@ function UploadAvatarModal (props) { isOpen, } = props - const [result, setResult] = useState({}) + const [result, setResult] = useState({ }) + const [inputDragActive, setInputDragActive] = useState(false) const [upImg, setUpImg] = useState() // eslint-disable-next-line id-length -- Required by react-easy-crop const [crop, handleSetCrop] = useState({ x: 0, y: 0 }) @@ -35,6 +50,13 @@ function UploadAvatarModal (props) { const [submitReady, setSubmitReady] = useState(false) const [submitting, setSubmitting] = useState(false) + const onCropChange = useCallback((croppedArea) => { + if (!submitting) { + setSubmitReady(false) + handleSetCrop(croppedArea) + } + }, [submitting]) + const onCropComplete = useCallback((croppedArea, cap) => { setCroppedAreaPixels(cap) setSubmitReady(true) @@ -44,7 +66,11 @@ function UploadAvatarModal (props) { if (event.target.files && event.target.files.length > 0) { const reader = new FileReader() reader.addEventListener('load', () => { - return setUpImg(reader.result) + // eslint-disable-next-line id-length -- Required by react-easy-crop + handleSetCrop({ x: 0, y: 0 }) + handleSetZoom(1) + handleSetRotation(0) + setUpImg(reader.result) }) reader.readAsDataURL(event.target.files[0]) } @@ -74,6 +100,10 @@ function UploadAvatarModal (props) { } }, [dispatch, isOpen, onClose]) + const onBack = useCallback(() => { + setUpImg(null) + }, []) + const onSubmit = useCallback(() => { try { const image = new Image() @@ -152,76 +182,127 @@ function UploadAvatarModal (props) { } }, [croppedAreaPixels, rotation, submit, upImg]) + const onInputDragEnter = useCallback(() => { + setInputDragActive(true) + }, []) + const onInputDragLeave = useCallback(() => { + setInputDragActive(false) + }, []) + + const cropClasses = { + containerClassName: submitting ? 'disabled' : '', + } + return ( - -
- - -
-
-
-
- -
-
- + { + result.success && ( +
+
-
- -
-
-
-
- -
-
-
- + ) + } + +
+ { + Boolean(!upImg && !result.success) && ( + + ) + } + { + Boolean(upImg && !result.success) && ( +
+
+ +
+
+
+ +
+
+ + +
+
-
- + ) + } +
+ + { + !result.success && ( + +
+ { + upImg && ( + + ) + }
-
- +
+
-
-
-
- -
-
- -
- + + ) + } ) } diff --git a/src/components/UploadAvatarModal/UploadAvatarModal.module.scss b/src/components/UploadAvatarModal/UploadAvatarModal.module.scss index 3ac4897b5..a3c660bb3 100644 --- a/src/components/UploadAvatarModal/UploadAvatarModal.module.scss +++ b/src/components/UploadAvatarModal/UploadAvatarModal.module.scss @@ -1,18 +1,31 @@ -.zoomAndCrop { +// Specificity assurance +:global(.message).message { + margin: 2rem auto; +} + +.avatarPreview { display: flex; + justify-content: center; + + width: 100%; + margin: 2rem 0 0; +} + +.body { + .zoomAndCrop { + display: flex; + + margin: 0 3.4rem 0 0; + } + .zoomSliderBox { display: flex; flex-direction: column; - width: 30px; - margin-bottom: 30px; - - .zoomSliderIcon { - height: 30px; - padding: 5px none; - } + width: 3.4rem; + margin-bottom: 3.2rem; } .sliderControl { @@ -29,14 +42,7 @@ .rotateSliderBox { display: flex; - .sliderControl { - padding-top: 5px; - } - - .rotateSliderIcon { - width: 30px; - padding: none 5px; - } + padding-top: 1.8rem; } .rotateAndCrop { diff --git a/src/components/UserDecalPanel/UserDecalPanel.module.scss b/src/components/UserDecalPanel/UserDecalPanel.module.scss index ff850eba9..b8f132e3f 100644 --- a/src/components/UserDecalPanel/UserDecalPanel.module.scss +++ b/src/components/UserDecalPanel/UserDecalPanel.module.scss @@ -1,3 +1,4 @@ +@import '../../scss/fonts'; @import '../../scss/colors'; .panelContent { @@ -29,7 +30,7 @@ .decalCode { flex: 1 0 auto; - font-family: monospace; + font-family: $monospace-fonts; } .decalClaimedAt { diff --git a/src/components/UserMenu/UserMenu.js b/src/components/UserMenu/UserMenu.js index fa7bbcf63..356afc1de 100644 --- a/src/components/UserMenu/UserMenu.js +++ b/src/components/UserMenu/UserMenu.js @@ -8,7 +8,7 @@ import { logout } from '~/store/actions/session' import { selectSession, selectUserById, - selectAvatarByUserId, + selectAvatarUrlByUserId, withCurrentUserId, selectCurrentUserCanEditAllRescues, } from '~/store/selectors' @@ -26,7 +26,7 @@ function UserMenu () { const { loggedIn } = useSelector(selectSession) const userCanSeeRescueAdmin = useSelector(selectCurrentUserCanEditAllRescues) const user = useSelector(withCurrentUserId(selectUserById)) - const userAvatar = useSelectorWithProps({ size: 64 }, withCurrentUserId(selectAvatarByUserId)) + const userAvatarUrl = useSelectorWithProps({ size: 64 }, withCurrentUserId(selectAvatarUrlByUserId)) const dispatch = useDispatch() @@ -62,7 +62,7 @@ function UserMenu () { unoptimized alt="User's avatar" height={64} - src={userAvatar} + src={userAvatarUrl} width={64} /> ) } diff --git a/src/scss/_components.scss b/src/scss/_components.scss index 8f9a396e3..e41205b92 100644 --- a/src/scss/_components.scss +++ b/src/scss/_components.scss @@ -12,6 +12,7 @@ @import 'components/comments'; @import 'components/donate-form'; @import 'components/error'; +@import 'components/file-dropzone'; @import 'components/grid'; @import 'components/header'; @import 'components/hero'; diff --git a/src/scss/_lib.scss b/src/scss/_lib.scss index 1a568489c..0914d5824 100644 --- a/src/scss/_lib.scss +++ b/src/scss/_lib.scss @@ -1,13 +1,13 @@ -/****************************************************************************** \ +/****************************************************************************** \ FontAwesome -\ ******************************************************************************/ +\ ******************************************************************************/ @import '@fortawesome/fontawesome-svg-core/styles.css'; -/****************************************************************************** \ +/****************************************************************************** \ kbd fun \ ******************************************************************************/ @import 'lib/kbdfun'; @@ -16,9 +16,9 @@ -/****************************************************************************** \ +/****************************************************************************** \ NProgress -\ ******************************************************************************/ +\ ******************************************************************************/ @import 'nprogress/nprogress.css'; @import 'lib/nprogress'; @@ -26,9 +26,9 @@ -/****************************************************************************** \ +/****************************************************************************** \ React Table -\ ******************************************************************************/ +\ ******************************************************************************/ @import 'react-table-6/react-table.css'; @import 'lib/react-table'; @@ -36,16 +36,26 @@ -/****************************************************************************** \ +/****************************************************************************** \ Stripe -\ ******************************************************************************/ +\ ******************************************************************************/ @import 'lib/stripe'; -/****************************************************************************** \ +/****************************************************************************** \ RC-Slider -\ ******************************************************************************/ +\ ******************************************************************************/ @import 'rc-slider/assets/index.css'; +@import 'lib/rc-slider'; + + + + + +/****************************************************************************** \ + react-easy-crop +\ ******************************************************************************/ +@import 'lib/react-easy-crop'; diff --git a/src/scss/components/_avatar.scss b/src/scss/components/_avatar.scss index 917d06320..1dd72bbe5 100644 --- a/src/scss/components/_avatar.scss +++ b/src/scss/components/_avatar.scss @@ -9,7 +9,7 @@ border: 0.2rem solid $red; border-radius: 50%; - background-color: $white; + background-color: $red-darker; overflow: hidden; @@ -32,17 +32,4 @@ width: 17rem; height: 17rem; } - - &:empty::before { - align-items: center; - justify-content: center; - - content: attr(data-letter); - width: 100%; - - background: $white; - color: $red; - - font-size: 2em; - } } diff --git a/src/scss/components/_file-dropzone.scss b/src/scss/components/_file-dropzone.scss new file mode 100644 index 000000000..0da2ced3e --- /dev/null +++ b/src/scss/components/_file-dropzone.scss @@ -0,0 +1,51 @@ +label.file-dropzone { + display: block; + position: relative; + + width: 100%; + padding: 4rem; + + border: 2px dashed rgba($red-darkened, 0.4); + + color: $red; + background: rgba($red, 0.2); + transition: + border-color 0.1s ease, + background 0.1s ease, + font-size 0.15s ease, + color 0.1s ease; + + font-size: 1.5em; + font-weight: 600; + font-family: $title-fonts; + text-align: center; + + cursor: pointer; + + &.active { + border-color: rgba($green-darkened, 0.4); + + color: $green; + background: rgba($green, 0.2); + + + font-size: 1.75em; + } + + small { + font-size: 0.7em; + } + + input { + position: absolute; + top: 0; + left: 0; + + width: 100%; + height: 100%; + + opacity: 0; + + cursor: pointer; + } +} diff --git a/src/scss/lib/_rc-slider.scss b/src/scss/lib/_rc-slider.scss new file mode 100644 index 000000000..3ae4ca3c8 --- /dev/null +++ b/src/scss/lib/_rc-slider.scss @@ -0,0 +1,86 @@ +.rc-slider { + &.rc-slider-disabled { + background-color: transparent; + + .rc-slider-track { + background: rgba($grey-darkened, 0.2); + } + + .rc-slider-handle { + background: $grey-muted; + } + + .rc-slider-dot { + border-color: $grey-muted; + } + } + + &.rc-slider-vertical { + .rc-slider-handle { + margin-left: -0.8rem; + } + + .rc-slider-track, + .rc-slider-rail { + width: 0.6rem; + height: 100%; + + border-radius: 1rem; + } + + .rc-slider-dot { + left: 3px; + } + } + + .rc-slider-rail { + height: 0.6rem; + + border-radius: 1rem; + + background: rgba($grey-darkened, 0.25); + } + + .rc-slider-track { + height: 0.6rem; + + border-radius: 1rem; + + background: rgba($red-darkened, 0.2); + } + + .rc-slider-dot { + bottom: -3px; + + border-color: $red-darker; + + background: transparent; + } + + .rc-slider-handle { + width: 2.2rem; + height: 2.2rem; + margin-top: -0.8rem; + + border: none; + + background: $red; + + .rc-slider-handle-icon { + position: absolute; + top: 0; + left: 0; + + width: 2.2rem; + height: 2.2rem; + padding: 0.5rem; + + color: $black; + } + } + + .rc-slider-mark-text, + .rc-slider-mark-text.rc-slider-mark-text-active { + color: $grey; + } +} diff --git a/src/scss/lib/_react-easy-crop.scss b/src/scss/lib/_react-easy-crop.scss new file mode 100644 index 000000000..ad7b2521b --- /dev/null +++ b/src/scss/lib/_react-easy-crop.scss @@ -0,0 +1,3 @@ +.reactEasyCrop_Container.disabled { + cursor: not-allowed; +} diff --git a/src/store/selectors/clients.js b/src/store/selectors/clients.js index 90f1ffb47..54dcf9df6 100644 --- a/src/store/selectors/clients.js +++ b/src/store/selectors/clients.js @@ -1,6 +1,6 @@ import { createCachedSelector } from 're-reselect' -import { getUserId } from './users' +import { getUserIdProp } from './users' const selectClients = (state) => { @@ -8,13 +8,13 @@ const selectClients = (state) => { } const selectClientsByUserId = createCachedSelector( - [selectClients, getUserId], + [selectClients, getUserIdProp], (clients, userId) => { return Object.values(clients).filter((client) => { return client.relationships.user?.data?.id === userId }) ?? [] }, -)(getUserId) +)(getUserIdProp) export { selectClients, diff --git a/src/store/selectors/decals.js b/src/store/selectors/decals.js index 855924116..d0239d350 100644 --- a/src/store/selectors/decals.js +++ b/src/store/selectors/decals.js @@ -4,7 +4,7 @@ import safeParseInt from '~/util/safeParseInt' import { withCurrentUserId } from './session' -import { getUserId, selectUserById } from './users' +import { getUserIdProp, selectUserById } from './users' @@ -39,4 +39,4 @@ export const selectDecalsByUserId = createCachedSelector( } return undefined }, -)(getUserId) +)(getUserIdProp) diff --git a/src/store/selectors/groups.js b/src/store/selectors/groups.js index a51e15490..042b1aaf6 100644 --- a/src/store/selectors/groups.js +++ b/src/store/selectors/groups.js @@ -1,6 +1,6 @@ import { createCachedSelector } from 're-reselect' -import { getUserId, selectUserById } from './users' +import { getUserIdProp, selectUserById } from './users' @@ -26,4 +26,4 @@ export const selectGroupsByUserId = createCachedSelector( } return [] }, -)(getUserId) +)(getUserIdProp) diff --git a/src/store/selectors/nicknames.js b/src/store/selectors/nicknames.js index 21df0f057..0e637e259 100644 --- a/src/store/selectors/nicknames.js +++ b/src/store/selectors/nicknames.js @@ -1,6 +1,6 @@ import { createCachedSelector } from 're-reselect' -import { getUserId, selectUserById } from './users' +import { getUserIdProp, selectUserById } from './users' @@ -26,4 +26,4 @@ export const selectNicknamesByUserId = createCachedSelector( } return [] }, -)(getUserId) +)(getUserIdProp) diff --git a/src/store/selectors/statistics.js b/src/store/selectors/statistics.js index 12705512b..ddc965a13 100644 --- a/src/store/selectors/statistics.js +++ b/src/store/selectors/statistics.js @@ -1,6 +1,6 @@ import { createCachedSelector } from 're-reselect' -import { selectUserRatsRelationship, getUserId } from './users' +import { selectUserRatsRelationship, getUserIdProp } from './users' export const selectLeaderboard = (state) => { return state.leaderboard.entries @@ -21,7 +21,7 @@ export const selectRatStatisticsById = (state, { ratId }) => { export const selectUserStatisticsById = createCachedSelector( - [getUserId, selectUserRatsRelationship, selectRatStatistics], + [getUserIdProp, selectUserRatsRelationship, selectRatStatistics], (userId, userRats, ratStatistics) => { // if the user doesn't exist, or there's no rat data, or there's no statistics for the first rat, return null. // Since the only way of getting a rat's statistics is by requesting for all rats of a user, we can assume we don't have any if we're missing one. @@ -54,4 +54,4 @@ export const selectUserStatisticsById = createCachedSelector( }, {}), } }, -)(getUserId) +)(getUserIdProp) diff --git a/src/store/selectors/users.js b/src/store/selectors/users.js index d4445f6a8..fe78f91b8 100644 --- a/src/store/selectors/users.js +++ b/src/store/selectors/users.js @@ -15,11 +15,15 @@ const AVATAR_DEFAULT_SIZE = 256 -const getScope = (_, props) => { +const getScopeProp = (_, props) => { return props?.scope } -export const getUserId = (_, props) => { +const getSizeProp = (_, props) => { + return props?.size ?? AVATAR_DEFAULT_SIZE +} + +export const getUserIdProp = (_, props) => { return props?.userId } @@ -40,16 +44,23 @@ export const selectUserDisplayRatRelationship = (state, props) => { } export const selectAvatarByUserId = (state, props) => { - const user = selectUserById(state, props) + return selectUserById(state, props)?.relationships.avatar?.data ?? undefined +} - if (!user) { - return undefined - } +export const selectAvatarUrlByUserId = createCachedSelector( + [selectAvatarByUserId, getSizeProp, getUserIdProp], + (avatar, size, userId) => { + if (userId === 'null') { + return undefined + } - return user.attributes.image - ? `/api/fr/users/${user.id}/image` - : `/api/avatars/${user.id}/${props.size ?? AVATAR_DEFAULT_SIZE}` -} + return avatar + ? `/api/fr/users/${userId}/image?v=${avatar.id}&size=${size}` + : `/api/avatars/${userId}/${size}` + }, +)((_, props) => { + return `${getUserIdProp(_, props)}-${getSizeProp(_, props)}` +}) export const selectCurrentUserScopes = (state) => { return withCurrentUserId(selectUserById)(state)?.meta?.permissions ?? [] @@ -66,7 +77,7 @@ export const selectCurrentUserScopes = (state) => { export const selectCurrentUserHasScope = createCachedSelector( [ selectCurrentUserScopes, - getScope, + getScopeProp, ], (userScopes, scope) => { if (!scope) { diff --git a/src/util/fontawesome/library.js b/src/util/fontawesome/library.js index 21ac33c64..6b53efe61 100644 --- a/src/util/fontawesome/library.js +++ b/src/util/fontawesome/library.js @@ -19,20 +19,18 @@ export { faExclamationCircle, faEye, faEyeSlash, + faFileUpload, faFirstAid, faFolder, faFolderOpen, faHandsHelping, faIdCardAlt, - faInfoCircle, - faMoneyBill, faMouse, faPen, faPlus, faRocket, faShieldAlt, faShoePrints, - faShoppingCart, faSignature, faSpaceShuttle, faSpinner, @@ -40,14 +38,10 @@ export { faTimes, faTimesCircle, faTrash, - faTruck, - faUndo, + faSearch, faUser, faUserSecret, faUpload, - faSearchPlus, - faSearchMinus, - faRedo, } from '@fortawesome/free-solid-svg-icons' export {