diff --git a/.circleci/config.yml b/.circleci/config.yml index 30e6fbba04..604c51da97 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -356,7 +356,7 @@ workflows: filters: branches: only: - - free + - feature-timeline-wall # This is alternate dev env for parallel testing - "build-qa": context : org-global diff --git a/__tests__/shared/components/__snapshots__/Content.jsx.snap b/__tests__/shared/components/__snapshots__/Content.jsx.snap index a4e05fa565..659f2ce576 100644 --- a/__tests__/shared/components/__snapshots__/Content.jsx.snap +++ b/__tests__/shared/components/__snapshots__/Content.jsx.snap @@ -1,1090 +1,1090 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matches shallow shapshot 1`] = ` -
-
- Build Timestamp: - Wed, 29 Nov 2017 07:40:00 GMT -
-

- Topcoder Community App -

-

- Isomorphic ReactJS App for new version of Topcoder community website. Technological stack includes: -

- -

- Main Topcoder website -

- -

- TCO Assets -

- -

- Separate Topcoder Communities -

- -

- Previews of Contentful Components -

- -

- Previews of Contentful Components with CMS SpaceName/Environment Props -

- -

- Previews of Contentful Components With environment variable override for CMS configuration. -

- -

- Sandbox -

-

- The right place to put any experimental and proof-of-concept stuff. -

- -

- Misc Examples -

- -
-`; +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches shallow shapshot 1`] = ` +
+
+ Build Timestamp: + Wed, 29 Nov 2017 07:40:00 GMT +
+

+ Topcoder Community App +

+

+ Isomorphic ReactJS App for new version of Topcoder community website. Technological stack includes: +

+ +

+ Main Topcoder website +

+ +

+ TCO Assets +

+ +

+ Separate Topcoder Communities +

+ +

+ Previews of Contentful Components +

+ +

+ Previews of Contentful Components with CMS SpaceName/Environment Props +

+ +

+ Previews of Contentful Components With environment variable override for CMS configuration. +

+ +

+ Sandbox +

+

+ The right place to put any experimental and proof-of-concept stuff. +

+ +

+ Misc Examples +

+ +
+`; diff --git a/config/default.js b/config/default.js index 251ef00b10..604fe4a34b 100644 --- a/config/default.js +++ b/config/default.js @@ -167,6 +167,7 @@ module.exports = { SUBDOMAIN_PROFILE_CONFIG: [{ groupId: '20000000', communityId: 'wipro', communityName: 'topgear', userProfile: 'https://topgear-app.wipro.com/user-details', }], + TIMELNE_EVENT_API: 'https://api.topcoder-dev.com/v5/timeline-wall', }, /* Information about Topcoder user groups can be cached in various places. @@ -463,4 +464,8 @@ module.exports = { }, PLATFORMUI_SITE_URL: 'https://platform-ui.topcoder-dev.com', DICE_VERIFY_URL: 'https://accounts-auth0.topcoder-dev.com', + TIMELINE: { + REJECTION_EVENT_REASONS: ['Duplicate Event'], + ALLOWED_FILETYPES: ['image/jpeg', 'image/png', 'video/mp4', 'video/x-msvideo', 'video/webm'], + }, }; diff --git a/config/production.js b/config/production.js index 6219d55882..1d94a3fab5 100644 --- a/config/production.js +++ b/config/production.js @@ -62,6 +62,7 @@ module.exports = { }, EMAIL_VERIFY_URL: 'http://www.topcoder.com/settings/account/changeEmail', THRIVE_FEED: 'https://topcoder.com/api/feeds/thrive', + TIMELNE_EVENT_API: 'https://api.topcoder.com/v5/timeline-wall', }, /* Filestack configuration for uploading Submissions * These are for the production back end */ diff --git a/src/assets/images/btn-delete-photo.svg b/src/assets/images/btn-delete-photo.svg new file mode 100644 index 0000000000..9531563316 --- /dev/null +++ b/src/assets/images/btn-delete-photo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/timeline-wall/btn-close.svg b/src/assets/images/timeline-wall/btn-close.svg new file mode 100644 index 0000000000..17213ef3c6 --- /dev/null +++ b/src/assets/images/timeline-wall/btn-close.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/timeline-wall/cheveron-down-blue.svg b/src/assets/images/timeline-wall/cheveron-down-blue.svg new file mode 100644 index 0000000000..95b98daba0 --- /dev/null +++ b/src/assets/images/timeline-wall/cheveron-down-blue.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/timeline-wall/cheveron-down.svg b/src/assets/images/timeline-wall/cheveron-down.svg new file mode 100644 index 0000000000..e12e58cb11 --- /dev/null +++ b/src/assets/images/timeline-wall/cheveron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/timeline-wall/cheveron-left.svg b/src/assets/images/timeline-wall/cheveron-left.svg new file mode 100644 index 0000000000..65cd4e1584 --- /dev/null +++ b/src/assets/images/timeline-wall/cheveron-left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/timeline-wall/cheveron-right.svg b/src/assets/images/timeline-wall/cheveron-right.svg new file mode 100644 index 0000000000..359e44d6e7 --- /dev/null +++ b/src/assets/images/timeline-wall/cheveron-right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/timeline-wall/icon-photo.svg b/src/assets/images/timeline-wall/icon-photo.svg new file mode 100644 index 0000000000..d473f17698 --- /dev/null +++ b/src/assets/images/timeline-wall/icon-photo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/timeline-wall/icon-video.svg b/src/assets/images/timeline-wall/icon-video.svg new file mode 100644 index 0000000000..5edbcd07e5 --- /dev/null +++ b/src/assets/images/timeline-wall/icon-video.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/timeline-wall/tooltip-down.svg b/src/assets/images/timeline-wall/tooltip-down.svg new file mode 100644 index 0000000000..16da0878e9 --- /dev/null +++ b/src/assets/images/timeline-wall/tooltip-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/timeline-wall/tooltip-left-mobile-blue.svg b/src/assets/images/timeline-wall/tooltip-left-mobile-blue.svg new file mode 100644 index 0000000000..7813b2b172 --- /dev/null +++ b/src/assets/images/timeline-wall/tooltip-left-mobile-blue.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/timeline-wall/tooltip-left.svg b/src/assets/images/timeline-wall/tooltip-left.svg new file mode 100644 index 0000000000..eb5ec8b2e2 --- /dev/null +++ b/src/assets/images/timeline-wall/tooltip-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/timeline-wall/tooltip-right.svg b/src/assets/images/timeline-wall/tooltip-right.svg new file mode 100644 index 0000000000..85856cd0a8 --- /dev/null +++ b/src/assets/images/timeline-wall/tooltip-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/timeline-wall/top-banner-mobile.png b/src/assets/images/timeline-wall/top-banner-mobile.png new file mode 100644 index 0000000000..1481fb1ff7 Binary files /dev/null and b/src/assets/images/timeline-wall/top-banner-mobile.png differ diff --git a/src/assets/images/timeline-wall/top-banner.png b/src/assets/images/timeline-wall/top-banner.png new file mode 100644 index 0000000000..ba5ec29ba6 Binary files /dev/null and b/src/assets/images/timeline-wall/top-banner.png differ diff --git a/src/shared/actions/timelineWall.js b/src/shared/actions/timelineWall.js new file mode 100644 index 0000000000..7dd3e2d7cd --- /dev/null +++ b/src/shared/actions/timelineWall.js @@ -0,0 +1,77 @@ +/** + * Actions related to topcoder timeline wall + */ + +import _ from 'lodash'; +import { createActions } from 'redux-actions'; +import { + getTimelineEvents, getPendingApprovals, getUserDetails, createEvent, + getUserAvatar, +} from '../services/timelineWall'; + +/** + * Fetch timeline events + */ +async function getEventsDone() { + return getTimelineEvents(); +} + +/** + * Fetch pending approvals + * + * @param {String} tokenV3 + * + * @returns {Promise} + */ +async function getPendingApprovalsDone(tokenV3) { + return getPendingApprovals(tokenV3); +} + +/** + * Check if logged in user is in the configured Admin. + * + * @param {String} tokenV3 + * + * @returns {Promise} + */ +async function getUserDetailsDone(tokenV3) { + return getUserDetails(tokenV3); +} + +/** + * Creates new event for timeline + * + * @param {Object} body event body + * @param {String} tokenV3 + * + * @returns {Promise} + */ +async function createNewEventDone(tokenV3, body) { + return createEvent(tokenV3, body); +} + +/** + * Get user avatar url + * + * @param {String} handle + * + * @returns {Promise} + */ +async function fetchUserAvatarDone(handle) { + return getUserAvatar(handle); +} + +export default createActions({ + TIMELINE: { + FETCH_USER_AVATAR_INIT: _.noop, + FETCH_USER_AVATAR_DONE: fetchUserAvatarDone, + GET_USER_DETAILS_INIT: _.noop, + GET_USER_DETAILS_DONE: getUserDetailsDone, + FETCH_TIMELINE_EVENTS_INIT: _.noop, + FETCH_TIMELINE_EVENTS_DONE: getEventsDone, + FETCH_PENDING_APPROVALS_INIT: _.noop, + FETCH_PENDING_APPROVALS_DONE: getPendingApprovalsDone, + CREATE_NEW_EVENT_INIT: _.noop, + CREATE_NEW_EVENT_DONE: createNewEventDone, + }, +}); diff --git a/src/shared/components/GUIKit/PhotoVideoItem/index.jsx b/src/shared/components/GUIKit/PhotoVideoItem/index.jsx new file mode 100644 index 0000000000..6a8344c05d --- /dev/null +++ b/src/shared/components/GUIKit/PhotoVideoItem/index.jsx @@ -0,0 +1,133 @@ +import React, { useEffect, useState, useRef } from 'react'; +import PT from 'prop-types'; +import { isImage, isVideo } from 'utils/url'; +import BtnPlay from 'assets/images/button_play_video.svg'; +import cn from 'classnames'; + + +import './styles.scss'; + +function getVideoCover(file, seekTo = 0.0) { + return new Promise((resolve) => { + // load the file to a video player + const videoPlayer = document.createElement('video'); + videoPlayer.setAttribute('src', URL.createObjectURL(file)); + videoPlayer.load(); + videoPlayer.addEventListener('error', () => { + resolve(''); + }); + // load metadata of the video to get video duration and dimensions + videoPlayer.addEventListener('loadedmetadata', () => { + // seek to user defined timestamp (in seconds) if possible + if (videoPlayer.duration < seekTo) { + resolve(''); + return; + } + // delay seeking or else 'seeked' event won't fire on Safari + setTimeout(() => { + videoPlayer.currentTime = seekTo; + }, 200); + // extract video thumbnail once seeking is complete + videoPlayer.addEventListener('seeked', () => { + // define a canvas to have the same dimension as the video + const canvas = document.createElement('canvas'); + canvas.width = videoPlayer.videoWidth; + canvas.height = videoPlayer.videoHeight; + // draw the video frame to canvas + const ctx = canvas.getContext('2d'); + ctx.drawImage(videoPlayer, 0, 0, canvas.width, canvas.height); + const thumnail = canvas.toDataURL('image/jpeg', 0.75); + resolve(thumnail); + }); + }); + }); +} + +function PhotoVideoItem({ + className, onClick, file, notSelectable, url, isUrlPhoto, videoThumnailUrl, +}) { + const [imageUrl, setImageUrl] = useState(''); + const [isThisVideo, setIsThisVideo] = useState(false); + const isMounted = useRef(true); + + useEffect(() => () => { + isMounted.current = false; + }, []); + + useEffect(() => { + if (file && isImage(file.name)) { + const reader = new FileReader(); + reader.onloadend = () => { + if (isMounted.current) { + setImageUrl(reader.result); + } + }; + reader.readAsDataURL(file); + } else if (file && isVideo(file.name)) { + setIsThisVideo(true); + getVideoCover(file).then((videoUrl) => { + if (isMounted.current) { + setImageUrl(videoUrl); + } + }); + } + }, [file]); + + useEffect(() => { + if (url) { + if (isUrlPhoto) { + setImageUrl(url); + setIsThisVideo(false); + } else { + setImageUrl(videoThumnailUrl); + setIsThisVideo(true); + } + } + }, [url]); + + return ( + + ); +} + +/** + * Default values for Props + */ +PhotoVideoItem.defaultProps = { + className: '', + onClick: () => { }, + file: null, + notSelectable: false, + url: '', + videoThumnailUrl: '', + isUrlPhoto: true, +}; + +/** + * Prop Validation + */ +PhotoVideoItem.propTypes = { + className: PT.string, + onClick: PT.func, + file: PT.any, + notSelectable: PT.bool, + url: PT.string, + videoThumnailUrl: PT.string, + isUrlPhoto: PT.bool, +}; + +export default PhotoVideoItem; diff --git a/src/shared/components/GUIKit/PhotoVideoItem/styles.scss b/src/shared/components/GUIKit/PhotoVideoItem/styles.scss new file mode 100644 index 0000000000..2ab0380c89 --- /dev/null +++ b/src/shared/components/GUIKit/PhotoVideoItem/styles.scss @@ -0,0 +1,30 @@ +@import '~styles/mixins'; + +.container { + display: flex; + max-height: 100%; + width: 100%; + align-items: center; + justify-content: center; + border: none; + padding: 0; + margin: 0; + position: relative; + background-color: transparent; + + &.not-selectable { + cursor: unset; + } +} + +.img-container { + height: 100%; + width: 100%; + object-fit: cover; +} + +.btn-play { + position: absolute; + top: calc(50% - 12px); + left: calc(50% - 12px); +} diff --git a/src/shared/components/GUIKit/PhotoVideoPicker/index.jsx b/src/shared/components/GUIKit/PhotoVideoPicker/index.jsx new file mode 100644 index 0000000000..988b2e617b --- /dev/null +++ b/src/shared/components/GUIKit/PhotoVideoPicker/index.jsx @@ -0,0 +1,169 @@ +import React from 'react'; +import PT from 'prop-types'; +import Dropzone from 'react-dropzone'; +import cn from 'classnames'; +import BtnDeletePhoto from 'assets/images/btn-delete-photo.svg'; +import PhotoVideoItem from '../PhotoVideoItem'; + +import './styles.scss'; + +/** + * PhotoVideoPicker component + */ +function PhotoVideoPicker({ + onFilePick, + btnText, + infoText, + infoTextMobile, + options, + errorMsg, + className, + inputOptions, + file, +}) { + const renderUpload = () => ( + +

+ {infoText} +

+ +
+ ); + + return ( + + { + onFilePick([ + ...file, + ...acceptedFiles, + ]); + }} + {...options} + accept={['image/jpeg', 'image/png', 'video/mp4', 'video/x-msvideo', 'video/webm']} + maxFiles={3} + > + {({ getRootProps, getInputProps }) => ( +
+ { + file && file.length ? ( +
+ {file.map((fileInfo, index) => ( +
+ +
+ ))} +
+ ) : null + } + { + file.length <= 3 ? ( + + +
+ + { + file && file.length ? ( +
+ {file.map((fileInfo, index) => ( +
+ +
+ ))} + { + file.length < 3 && ( +
+ {renderUpload()} +
+ ) + } +
+ ) : renderUpload() + } + + { + (file || []).length < 3 && ( + +

+ {infoTextMobile} +

+ +
+ ) + } +
+ +
+ ) : null + } +
+ )} +
+ {errorMsg ? ({errorMsg}) : null} +
+ ); +} + +PhotoVideoPicker.defaultProps = { + infoText: '', + infoTextMobile: '', + btnText: 'SELECT A FILE', + options: {}, + errorMsg: '', + className: '', + inputOptions: {}, + file: [], +}; + +/** + * Prop Validation + */ +PhotoVideoPicker.propTypes = { + infoText: PT.string, + infoTextMobile: PT.string, + btnText: PT.string, + onFilePick: PT.func.isRequired, + options: PT.shape(), + errorMsg: PT.string, + className: PT.string, + inputOptions: PT.shape(), + file: PT.arrayOf(PT.shape()), +}; + +export default PhotoVideoPicker; diff --git a/src/shared/components/GUIKit/PhotoVideoPicker/styles.scss b/src/shared/components/GUIKit/PhotoVideoPicker/styles.scss new file mode 100644 index 0000000000..8713f81db3 --- /dev/null +++ b/src/shared/components/GUIKit/PhotoVideoPicker/styles.scss @@ -0,0 +1,156 @@ +@import '~components/buttons/themed/tc.scss'; +@import '~components/GUIKit/Assets/Styles/default'; + +.container { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + background-color: #fff; + border-radius: 6px; + min-height: 164px; + background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='6' ry='6' stroke='gray' stroke-width='1' stroke-dasharray='5%2c5' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e"); + outline: none !important; + padding: 0; + + &.hasError { + background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='6' ry='6' stroke='red' stroke-width='1' stroke-dasharray='5%2c5' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e"); + } + + &.hasFile { + background-image: none; + + @media (max-width: 768px) { + background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='6' ry='6' stroke='gray' stroke-width='1' stroke-dasharray='5%2c5' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e"); + } + } + + .btn { + outline: none !important; + + @include primary-white; + @include sm; + + line-height: 24px !important; + + &:hover { + @include primary-white; + } + } + + .infoText { + font-family: Roboto, sans-serif; + font-size: 14px !important; + line-height: 22px !important; + margin: 0 !important; + display: flex; + flex-direction: column; + align-items: center; + white-space: pre-wrap; + text-align: center; + color: $color-black-60; + margin-bottom: 16px !important; + + @media (max-width: 768px) { + margin-bottom: 8px !important; + } + + > span { + display: flex; + margin: 19px 0; + } + + &.withFile { + color: #2a2a2a; + margin: 40px 0 !important; + } + } +} + +.errorMessage { + display: block; + + @include errorMessage; +} + +.photo-list { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + column-gap: 8px; + row-gap: 8px; + height: 100%; + width: 100%; + + &.show-mobile { + @media (max-width: 768px) { + display: grid !important; + } + } + + @media (max-width: 768px) { + margin-bottom: 8px; + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + + .photo-item { + height: 100%; + border: 1px solid $color-black-40; + border-radius: 4px; + overflow: hidden; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + position: relative; + + &.browse-button { + display: flex; + flex-direction: column; + width: 100%; + padding: 0 10px; + } + + @media (max-width: 768px) { + height: 100px; + border: none; + border-radius: 8px; + } + + .btn-delete { + position: absolute; + border: none; + padding: 0; + background-color: transparent; + top: 34px; + right: 8px; + + @media (max-width: 768px) { + top: 10px; + right: 10px; + width: 28px; + height: 28px; + } + } + } +} + +.wrapper-container { + display: flex; + flex-direction: column; +} + +.hide-mobile { + @media (max-width: 768px) { + display: none !important; + } +} + +.hide-desktop { + display: none !important; +} + +.show-mobile { + @media (max-width: 768px) { + display: flex !important; + } +} diff --git a/src/shared/components/ProfilePage/Awards/AwardBadge/index.jsx b/src/shared/components/ProfilePage/Awards/AwardBadge/index.jsx index 1903b27c2f..924c88aab3 100644 --- a/src/shared/components/ProfilePage/Awards/AwardBadge/index.jsx +++ b/src/shared/components/ProfilePage/Awards/AwardBadge/index.jsx @@ -1,3 +1,4 @@ +/* eslint-disable react/no-danger */ import React from 'react'; import PT from 'prop-types'; import FallBackAwardIcon from 'assets/images/default-award.svg'; diff --git a/src/shared/components/Settings/FormField/index.jsx b/src/shared/components/Settings/FormField/index.jsx index b4fad3d841..39a0ac31ac 100644 --- a/src/shared/components/Settings/FormField/index.jsx +++ b/src/shared/components/Settings/FormField/index.jsx @@ -9,9 +9,9 @@ import cn from 'classnames'; import './styles.scss'; const FormField = ({ - children, label = '', disabled, style, required, isTextarea, + children, label = '', disabled, style, required, isTextarea, className = '', }) => ( -
+
{label} @@ -24,6 +24,7 @@ const FormField = ({ FormField.defaultProps = { label: '', + className: '', children: null, disabled: false, style: {}, @@ -33,6 +34,7 @@ FormField.defaultProps = { FormField.propTypes = { label: PT.string, + className: PT.string, children: PT.node, disabled: PT.bool, style: PT.object, diff --git a/src/shared/components/Settings/FormInputText/index.jsx b/src/shared/components/Settings/FormInputText/index.jsx index 7b69238f63..b23a3f230c 100644 --- a/src/shared/components/Settings/FormInputText/index.jsx +++ b/src/shared/components/Settings/FormInputText/index.jsx @@ -9,23 +9,41 @@ import cn from 'classnames'; import './styles.scss'; const FormInputText = ({ - styleName, type, ...props -}) => ( - -); + styleName, type, showChartCount, ...props +}) => { + const { value, maxLength } = props; + + return ( + + {showChartCount ? ( + + {(value && value.length) || 0} + /{maxLength} + + ) : null} + + + ); +}; FormInputText.defaultProps = { styleName: '', type: 'text', + value: null, + maxLength: null, + showChartCount: false, }; FormInputText.propTypes = { styleName: PT.string, type: PT.string, + value: PT.string, + maxLength: PT.string, + showChartCount: PT.bool, }; export default FormInputText; diff --git a/src/shared/components/Settings/FormInputText/styles.scss b/src/shared/components/Settings/FormInputText/styles.scss index a51a2f8ec7..5d1e4759e8 100644 --- a/src/shared/components/Settings/FormInputText/styles.scss +++ b/src/shared/components/Settings/FormInputText/styles.scss @@ -31,3 +31,17 @@ border-radius: 4px !important; } } + +.char-count { + @include roboto-regular; + + position: absolute; + right: 0; + bottom: -15px; + font-size: 11px; + padding-right: 0; +} + +.grey { + color: $tc-gray-40; +} diff --git a/src/shared/components/Settings/FormInputTextArea/index.jsx b/src/shared/components/Settings/FormInputTextArea/index.jsx index 2ceac81392..dceed5e279 100644 --- a/src/shared/components/Settings/FormInputTextArea/index.jsx +++ b/src/shared/components/Settings/FormInputTextArea/index.jsx @@ -8,17 +8,30 @@ import PT from 'prop-types'; import cn from 'classnames'; import './styles.scss'; -const FormInputTextArea = ({ styleName, ...props }) => { - const { value, maxLength } = props; +const FormInputTextArea = ({ + styleName, showChartCount, showBottomChartCount, ...props +}) => { + const { + value, + maxLength, + } = props; return (
- - {(value && value.length) || 0} - -  / {maxLength} + {showBottomChartCount ? ( + + {(value && value.length) || 0} + /{maxLength} - + ) : null} + {showChartCount ? ( + + {(value && value.length) || 0} + +  / {maxLength} + + + ) : null}