\n {this.sponsoredIntegrations()}\n {this.enhancedIntegrations()}\n
\n );\n }\n}\n\nPartnerIntegrationsList.defaultProps = {\n selected: null\n};\n\nPartnerIntegrationsList.propTypes = {\n athleteId: PropTypes.number.isRequired,\n onValueChanged: PropTypes.func.isRequired,\n selected: PropTypes.string,\n sponsored: PropTypes.shape({}).isRequired,\n enhanced: PropTypes.shape({}).isRequired\n};\n\nexport default connect(\n ({ athleteId, sponsored, enhanced, selected }) => ({\n athleteId,\n sponsored,\n enhanced,\n selected\n }),\n { onValueChanged: valueChanged }\n)(PartnerIntegrationsList);\n","import React from 'react';\nimport { Provider } from 'react-redux';\nimport { createStore, applyMiddleware } from 'redux';\nimport thunk from 'redux-thunk';\n\nimport reducer from './reducers';\n\nimport PartnerIntegrationsList from './components/PartnerIntegrationsList';\n\nclass PartnerIntegrations extends React.Component {\n store = createStore(\n reducer,\n { ...this.props, selected: null, disabled: false },\n applyMiddleware(thunk)\n );\n\n render() {\n return (\n \n
this.toggleMenu()}\n onKeyDown={() => this.toggleMenu()}\n />\n {this.state.expanded && (\n
\n {this.state.actions.map((action) => (\n - \n \n
\n ))}\n
\n )}\n
\n );\n }\n}\n\nActionsDropdown.propTypes = {\n actions: PropTypes.arrayOf(PropTypes.shape({})).isRequired\n};\n\nexport default ActionsDropdown;\n","// extracted by mini-css-extract-plugin\nexport default {\"container\":\"Post--container--TMD7o\",\"delete\":\"Post--delete--bjh+W\",\"avatar-wrapper\":\"Post--avatar-wrapper--zGd69\",\"avatarWrapper\":\"Post--avatar-wrapper--zGd69\",\"header\":\"Post--header--8DQuo\",\"author-name\":\"Post--author-name--6nfjJ\",\"authorName\":\"Post--author-name--6nfjJ\",\"actions\":\"Post--actions--rk9BP\",\"body\":\"Post--body--lfsoe\",\"comments\":\"Post--comments--3a+Lb\"};","import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport I18n from 'utils/I18n';\nimport Cldr from 'utils/Cldr';\n\nimport Comments from '../Comments';\nimport ActionsDropdown from '../ActionsDropdown';\n\nimport styles from './styles.scss';\n\nconst I18nPrefix = 'components.comments.';\nconst I18nKey = {\n report: `${I18nPrefix}report`,\n delete: `${I18nPrefix}delete`\n};\n\nconst Post = ({\n id,\n avatar,\n authorId,\n authorName,\n timestamp,\n deletable,\n bodyText,\n comments,\n deletePost,\n reportPost,\n currentAthlete\n}) => {\n const getAction = () => {\n if (deletable) {\n return { text: I18n.t(I18nKey.delete), onClick: () => deletePost(id) };\n }\n return { text: I18n.t(I18nKey.report), onClick: () => reportPost(id) };\n };\n return (\n
\n
{avatar}
\n
\n
\n
\n {Cldr.formatTimespan(timestamp)}\n
\n
\n\n {Object.keys(getAction()).length !== 0 && (\n
\n )}\n
\n {(comments.length > 0 || currentAthlete) && (\n
\n )}\n
\n );\n};\n\nPost.defaultProps = {\n comments: [],\n currentAthlete: null\n};\n\nPost.propTypes = {\n id: PropTypes.number.isRequired,\n avatar: PropTypes.element.isRequired,\n authorId: PropTypes.number.isRequired,\n authorName: PropTypes.string.isRequired,\n timestamp: PropTypes.number.isRequired,\n deletable: PropTypes.bool.isRequired,\n bodyText: PropTypes.string.isRequired,\n comments: PropTypes.arrayOf(PropTypes.shape({})),\n deletePost: PropTypes.func.isRequired,\n reportPost: PropTypes.func.isRequired,\n currentAthlete: PropTypes.shape({})\n};\n\nexport default Post;\n","// extracted by mini-css-extract-plugin\nexport default {\"container\":\"Discussions--container--LirNp\",\"button-wrapper\":\"Discussions--button-wrapper--48o+i\",\"buttonWrapper\":\"Discussions--button-wrapper--48o+i\",\"discussion-list\":\"Discussions--discussion-list--5rl5S\",\"discussionList\":\"Discussions--discussion-list--5rl5S\",\"indent\":\"Discussions--indent--nPQWw\",\"empty-state\":\"Discussions--empty-state--rf4lV\",\"emptyState\":\"Discussions--empty-state--rf4lV\"};","import React from 'react';\nimport PropTypes from 'prop-types';\nimport Button from '@strava/ui/Button';\nimport Spinner from '@strava/ui/Spinner';\nimport Avatar from '@strava/ui/Avatar';\n\nimport createNetworkingClient from 'utils/networking-client';\nimport I18n from 'utils/I18n';\n\nimport CreatePost from './components/CreatePost';\nimport Post from './components/Post';\n\nimport styles from './styles.scss';\n\nconst I18nPrefix = 'components.discussions';\n\nclass Discussion extends React.Component {\n state = {\n isLoading: true,\n posts: [],\n totalPosts: null,\n page: null,\n discussionPath: this.props.discussionPath,\n error: false,\n currentAthlete: this.props.currentAthlete\n };\n\n componentDidMount() {\n this.fetchData();\n }\n\n fetchData = (params = {}) => {\n const { discussionPath } = this.state;\n const instance = createNetworkingClient();\n this.setState({ isLoading: true });\n\n instance\n .get(discussionPath, {\n params,\n headers: { Accept: 'text/javascript' }\n })\n .then((response) => {\n if (response && response.status === 200) {\n this.onFetchSuccess(response.data);\n } else {\n this.onFetchFail();\n }\n })\n .catch(() => this.onFetchFail());\n };\n\n onFetchFail = () => {\n this.setState({ isLoading: false, error: true });\n };\n\n onFetchSuccess = (data) => {\n this.setState((state) => ({\n isLoading: false,\n posts: [...state.posts, ...data.posts],\n totalPosts: data.total_posts,\n page: data.page\n }));\n };\n\n onCreateSuccess = (createdPost) => {\n this.setState((prevState) => ({\n posts: [createdPost, ...prevState.posts],\n totalPosts: prevState.totalPosts + 1\n }));\n };\n\n deletePost = (postId) => {\n const { discussionPath } = this.state;\n const instance = createNetworkingClient();\n\n instance\n .delete(`${discussionPath}/${postId}`)\n .then((response) => {\n if (response && response.status === 200) {\n this.onDeleteSuccess(response.data);\n } else {\n this.onDeleteFail();\n }\n })\n .catch(() => this.onDeleteFail());\n };\n\n onDeleteFail = () => {\n throw new Error('Something went wrong deleting your post');\n };\n\n onDeleteSuccess = (data) => {\n const { posts, totalPosts } = this.state;\n this.setState({\n posts: posts.filter((item) => item.id !== parseInt(data.id, 10)),\n totalPosts: totalPosts - 1\n });\n };\n\n reportPost = (postId) => {\n window.location = `/posts/${postId}/feedback`;\n };\n\n onSeeMore = () => {\n const { page } = this.state;\n this.fetchData({ page: page + 1 });\n };\n\n renderButtonOrSpinner = () => {\n const { isLoading, posts, totalPosts, error } = this.state;\n\n if (isLoading) {\n return (\n
\n \n
\n );\n }\n if (posts.length < totalPosts && !isLoading) {\n return (\n
\n \n
\n );\n }\n return null;\n };\n\n renderPosts = () => {\n const { posts, currentAthlete } = this.state;\n\n return posts.map((post) => (\n
\n }\n authorId={post.author.id}\n authorName={post.author.name}\n timestamp={post.ts}\n deletable={post.deletable}\n bodyText={post.text}\n className={styles.post}\n comments={post.comments}\n deletePost={this.deletePost}\n reportPost={this.reportPost}\n currentAthlete={currentAthlete}\n />\n ));\n };\n\n render() {\n const { posts, currentAthlete, discussionPath, isLoading } = this.state;\n\n return (\n
\n {currentAthlete && (\n
\n )}\n
\n {posts.length === 0 && !isLoading ? (\n
\n {I18n.t(`${I18nPrefix}.empty_state`)}\n
\n ) : (\n this.renderPosts()\n )}\n
\n {this.renderButtonOrSpinner()}\n
\n );\n }\n}\n\nDiscussion.defaultProps = {\n currentAthlete: null\n};\n\nDiscussion.propTypes = {\n currentAthlete: PropTypes.shape({}),\n discussionPath: PropTypes.string.isRequired\n};\n\nexport default Discussion;\n","// extracted by mini-css-extract-plugin\nexport default {\"line\":\"CollapsibleSection--line--Gjb6T\",\"subsection\":\"CollapsibleSection--subsection--S7K8c\",\"dark\":\"CollapsibleSection--dark--RJ5au\",\"light\":\"CollapsibleSection--light--srjAr\",\"subsection-container\":\"CollapsibleSection--subsection-container--xemH+\",\"subsectionContainer\":\"CollapsibleSection--subsection-container--xemH+\",\"section-image\":\"CollapsibleSection--section-image--+3bOl\",\"sectionImage\":\"CollapsibleSection--section-image--+3bOl\"};","import { useEffect, useState, useRef } from 'react';\nimport I18n from 'utils/I18n';\nimport createNetworkingClient from 'utils/networking-client';\n\nimport { isEqual } from 'lodash-es';\n\nexport const STATUS = {\n idle: 'idle',\n pending: 'pending',\n resolved: 'resolved',\n rejected: 'rejected',\n restricted: 'restricted'\n};\n\nexport const CATEGORIES = {\n gender: 'gender', // Overall tab has gender filters - overall, men, and women\n following: 'following',\n club: 'club',\n country: 'country',\n age: 'age',\n weight: 'weight'\n};\n\nexport const PREMIUM_CATEGORIES = ['age', 'weight'];\n\nconst I18N_PREFIX = 'strava.challenges.challenge_detail.leaderboard';\nconst noResultsMsg = () => I18n.t(`${I18N_PREFIX}.no_results_found`);\n\nconst unprocessableClubMsg = () => I18n.t(`${I18N_PREFIX}.unprocessable_club`);\nconst errorState = () => {\n return {\n data: { entries: [], viewingAthletePublicEntry: null },\n status: STATUS.rejected,\n message: I18n.t(`${I18N_PREFIX}.error_message`)\n };\n};\n\nfunction useDeepCompareMemoize(value) {\n const prev = useRef();\n const id = useRef(0);\n if (!isEqual(value, prev.current)) {\n id.current += 1;\n prev.current = value;\n }\n return id.current;\n}\n\nconst useFetchLeaderboardData = ({\n weightMeasurementUnit,\n challengeId,\n joined,\n subscribed,\n params\n}) => {\n const [state, setState] = useState({\n status: STATUS.idle,\n data: {\n entries: [],\n viewingAthletePublicEntry: null\n },\n message: null\n });\n\n const buildParams = ({\n before,\n after,\n category,\n sub_category: subCategory,\n limit\n }) => {\n let paramCategory = category;\n if (category === 'weight') {\n paramCategory =\n weightMeasurementUnit === 'kg' ? 'metric_weight' : 'imperial_weight';\n }\n const base = {\n category: paramCategory,\n sub_category: subCategory,\n limit\n };\n\n if (after) {\n return {\n ...base,\n after_athlete_id: after.athlete_id,\n after_rank: after.rank,\n after_value: after.value\n };\n }\n if (before) {\n return {\n ...base,\n before_athlete_id: before.athlete_id,\n before_rank: before.rank,\n before_value: before.value\n };\n }\n return base;\n };\n\n useEffect(() => {\n if (!subscribed && PREMIUM_CATEGORIES.includes(params.category)) {\n setState({\n data: { entries: [], viewingAthletePublicEntry: null },\n status: STATUS.restricted,\n message: I18n.t(`${I18N_PREFIX}.subscribe_weight_and_age`)\n });\n } else if (\n params.category === CATEGORIES.club &&\n params.sub_category == null\n ) {\n // club subcategory not available, athlete is not in any clubs\n const noClubsState = {\n data: { entries: [], viewingAthletePublicEntry: null },\n status: STATUS.resolved,\n message: noResultsMsg()\n };\n setState(noClubsState);\n } else {\n setState({\n status: STATUS.pending,\n data: state.data\n });\n const url = `/frontend/challenges/${challengeId}/leaderboard`;\n createNetworkingClient()\n .get(url, { params: buildParams(params) })\n .then((response) => {\n if (response && response.status === 200) {\n const { showLeaderboard } = response.data;\n // all responses with leaderboards\n if (showLeaderboard) {\n const successState = {\n data: response.data,\n status: STATUS.resolved,\n message:\n response.data.entries.length === 0 ? noResultsMsg() : null\n };\n setState(successState);\n }\n // large clubs that do not have leaderboards\n else if (!showLeaderboard && params.category === CATEGORIES.club) {\n const unprocessableClubState = {\n data: {\n ...response.data,\n entries: [],\n viewingAthletePublicEntry: null\n },\n status: STATUS.resolved,\n message: unprocessableClubMsg()\n };\n setState(unprocessableClubState);\n } else {\n // fallback for all other requests that do not have leaderboards\n setState(errorState());\n }\n } else {\n // Response status codes other than 200\n setState(errorState());\n }\n })\n .catch(() => {\n setState(errorState());\n });\n }\n }, [challengeId, joined, useDeepCompareMemoize(params)]);\n\n return state;\n};\n\nexport default useFetchLeaderboardData;\n","// extracted by mini-css-extract-plugin\nexport default {\"container\":\"CurrentPlace--container--2iZvO\"};","import React from 'react';\nimport PropTypes from 'prop-types';\nimport Cldr from 'utils/Cldr';\nimport styles from './styles.scss';\n\nconst CurrentPlace = ({ currentPlaceLabel, viewingAthleteRank, size }) => (\n
\n {`${currentPlaceLabel}: ${\n viewingAthleteRank ? Cldr.formatDecimal(viewingAthleteRank) : '--'\n } / ${size ? Cldr.formatDecimal(size) : '--'}`}\n
\n);\n\nCurrentPlace.defaultProps = {\n viewingAthleteRank: null,\n size: null\n};\n\nCurrentPlace.propTypes = {\n viewingAthleteRank: PropTypes.number,\n size: PropTypes.number,\n currentPlaceLabel: PropTypes.string.isRequired\n};\n\nexport default CurrentPlace;\n","// extracted by mini-css-extract-plugin\nexport default {\"container\":\"CategoryTabs--container--KjQZu\",\"tabs\":\"CategoryTabs--tabs--ePwKS\",\"tab-label\":\"CategoryTabs--tab-label--iOPNQ\",\"tabLabel\":\"CategoryTabs--tab-label--iOPNQ\",\"sub-category-filter\":\"CategoryTabs--sub-category-filter--iiVdI\",\"subCategoryFilter\":\"CategoryTabs--sub-category-filter--iiVdI\"};","import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Tabs, TabList, Tab } from '@strava/ui/Tabs';\nimport DropdownSelect from '@strava/ui/DropdownSelect';\nimport BadgesMulticolorSummitXsmall from '@strava/icons/BadgesMulticolorSummitXsmall';\nimport styles from './styles.scss';\n\nconst CategoryTabs = ({\n currentCategory,\n currentSubcategory,\n categories,\n onTabChangeCallback,\n onFilterChangeCallback\n}) => {\n const currentCategoryProps = categories[currentCategory];\n\n const currentCategoryHasFilters =\n currentSubcategory &&\n currentCategoryProps &&\n currentCategoryProps.subCategories;\n\n let filterOptions;\n if (currentCategoryHasFilters) {\n const { subCategories } = currentCategoryProps;\n filterOptions = Object.keys(subCategories).map((key) => ({\n value: key,\n label: subCategories[key]\n }));\n }\n\n return (\n
\n \n
\n
\n {Object.keys(categories).map((tabKey) => (\n \n \n {categories[tabKey].premium && (\n
\n \n
\n )}\n
{categories[tabKey].title}
\n
\n \n ))}\n \n
\n
\n {currentCategoryHasFilters && (\n onFilterChangeCallback(option.value)}\n value={\n filterOptions.filter((o) => o.value === currentSubcategory)[0]\n }\n ariaLabel=\"filter\"\n />\n )}\n
\n
\n \n );\n};\n\nCategoryTabs.defaultProps = {\n categories: { empty: { title: 'warning - no categories specified' } },\n currentSubcategory: null,\n currentCategory: null,\n onFilterChangeCallback: () => {},\n onTabChangeCallback: () => {}\n};\n\nCategoryTabs.propTypes = {\n categories: PropTypes.shape({}),\n onFilterChangeCallback: PropTypes.func,\n onTabChangeCallback: PropTypes.func,\n currentCategory: PropTypes.string,\n currentSubcategory: PropTypes.string\n};\n\nexport default CategoryTabs;\n","// extracted by mini-css-extract-plugin\nexport default {\"error\":\"LeaderboardsTable--error--RqMLb\",\"message-row\":\"LeaderboardsTable--message-row--MjbSX\",\"messageRow\":\"LeaderboardsTable--message-row--MjbSX\",\"paywall-banner\":\"LeaderboardsTable--paywall-banner--+ZfDR\",\"paywallBanner\":\"LeaderboardsTable--paywall-banner--+ZfDR\",\"image\":\"LeaderboardsTable--image--Ks6vg\",\"message\":\"LeaderboardsTable--message--UtBcn\",\"sub-title\":\"LeaderboardsTable--sub-title--RcY4O\",\"subTitle\":\"LeaderboardsTable--sub-title--RcY4O\",\"call-to-action\":\"LeaderboardsTable--call-to-action--5y+0M\",\"callToAction\":\"LeaderboardsTable--call-to-action--5y+0M\",\"table-container\":\"LeaderboardsTable--table-container--JxwWf\",\"tableContainer\":\"LeaderboardsTable--table-container--JxwWf\",\"leaderboard-entry\":\"LeaderboardsTable--leaderboard-entry--ydqYD\",\"leaderboardEntry\":\"LeaderboardsTable--leaderboard-entry--ydqYD\",\"overall-rank\":\"LeaderboardsTable--overall-rank--uXO4u\",\"overallRank\":\"LeaderboardsTable--overall-rank--uXO4u\",\"distance\":\"LeaderboardsTable--distance--LT5ge\",\"dimension\":\"LeaderboardsTable--dimension--MaD7n\",\"progress\":\"LeaderboardsTable--progress--gDVxf\",\"name\":\"LeaderboardsTable--name--wDPtH\",\"activity-count\":\"LeaderboardsTable--activity-count--r+ITw\",\"activityCount\":\"LeaderboardsTable--activity-count--r+ITw\",\"filtered-entry-place\":\"LeaderboardsTable--filtered-entry-place--2h3zG\",\"filteredEntryPlace\":\"LeaderboardsTable--filtered-entry-place--2h3zG\",\"spinner\":\"LeaderboardsTable--spinner--dhvSA\",\"pagination\":\"LeaderboardsTable--pagination--7BOiw\",\"pagination-label\":\"LeaderboardsTable--pagination-label--QEHTK\",\"paginationLabel\":\"LeaderboardsTable--pagination-label--QEHTK\"};","import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport ButtonGroup from '@strava/ui/ButtonGroup';\nimport Button, { LinkButton } from '@strava/ui/Button';\nimport Spinner from '@strava/ui/Spinner';\nimport ActionsArrowLeftNormalXsmall from '@strava/icons/ActionsArrowLeftNormalXsmall';\nimport ActionsArrowRightNormalXsmall from '@strava/icons/ActionsArrowRightNormalXsmall';\nimport I18n from 'utils/I18n';\nimport styles from './styles.scss';\nimport { STATUS } from '../useFetchLeaderboardData';\n\nconst thead = (columnHeaders) => (\n
\n \n {columnHeaders.map((columnHeader) => (\n \n {columnHeader.label}\n | \n ))}\n
\n \n);\n\nconst messageRow = (numColumns, message, isError) => (\n
\n \n \n {message}\n \n | \n
\n);\n\nconst paywallMessage = (numColumns, message, onSubscribeCallback) => (\n
\n \n \n \n \n \n {I18n.t(\n 'strava.challenges.challenge_detail.leaderboard.premium_leaderboards'\n )}\n \n {message} \n \n \n {I18n.t(\n 'strava.challenges.challenge_detail.leaderboard.subscribe'\n )}\n \n \n \n \n | \n
\n);\n\nconst tbody = (\n resultRows,\n columnHeaders,\n status,\n message,\n onSubscribeCallback\n) => {\n const numColumns = columnHeaders.length;\n // Successful response with result rows\n if (status === STATUS.resolved && resultRows.length !== 0) {\n return resultRows.map((row) => (\n
\n {row.value.map((col) => (\n \n {col.value}\n | \n ))}\n
\n ));\n }\n // Successful response with no result rows\n if (\n [STATUS.resolved, STATUS.idle].includes(status) &&\n resultRows.length === 0\n ) {\n return messageRow(numColumns, message, false);\n }\n // Failed request\n if (status === STATUS.rejected) {\n return messageRow(numColumns, message, true);\n }\n\n // Restricted for unsubscribers\n if (status === STATUS.restricted) {\n return paywallMessage(numColumns, message, onSubscribeCallback);\n }\n\n // Loading...\n return (\n
\n \n \n \n \n | \n
\n );\n};\n\nconst tfoot = (\n resultRows,\n status,\n columnHeaders,\n isFirstPage,\n isLastPage,\n onNextPageCallback,\n onPrevPageCallback,\n paginationLabel\n) => {\n if (\n [STATUS.resolved, STATUS.idle].includes(status) &&\n resultRows.length !== 0\n ) {\n return (\n
\n \n \n \n {paginationLabel && (\n \n {paginationLabel}\n \n )}\n \n \n \n \n \n | \n
\n \n );\n }\n return null;\n};\n\nconst LeaderboardsTable = ({\n resultRows,\n status,\n columnHeaders,\n onNextPageCallback,\n onPrevPageCallback,\n onSubscribeCallback,\n isFirstPage,\n isLastPage,\n message,\n paginationLabel,\n className\n}) => (\n
\n
\n {thead(columnHeaders)}\n \n {tbody(resultRows, columnHeaders, status, message, onSubscribeCallback)}\n \n {tfoot(\n resultRows,\n status,\n columnHeaders,\n isFirstPage,\n isLastPage,\n onNextPageCallback,\n onPrevPageCallback,\n paginationLabel\n )}\n
\n
\n);\n\nLeaderboardsTable.defaultProps = {\n onNextPageCallback: () => {},\n onPrevPageCallback: () => {},\n onSubscribeCallback: () => {},\n resultRows: [],\n columnHeaders: [],\n status: STATUS.idle,\n isLastPage: false,\n isFirstPage: false,\n message: '',\n paginationLabel: null,\n className: null\n};\n\nLeaderboardsTable.propTypes = {\n columnHeaders: PropTypes.arrayOf(\n PropTypes.shape({\n style: PropTypes.string,\n label: PropTypes.string,\n styleName: PropTypes.string\n })\n ),\n resultRows: PropTypes.arrayOf(\n PropTypes.shape({\n id: PropTypes.number,\n value: PropTypes.arrayOf(\n PropTypes.shape({ id: PropTypes.number, value: PropTypes.any })\n )\n })\n ),\n onNextPageCallback: PropTypes.func,\n onPrevPageCallback: PropTypes.func,\n onSubscribeCallback: PropTypes.func,\n status: PropTypes.oneOf(Object.keys(STATUS)),\n isLastPage: PropTypes.bool,\n isFirstPage: PropTypes.bool,\n message: PropTypes.string,\n paginationLabel: PropTypes.string,\n className: PropTypes.string\n};\n\nexport default LeaderboardsTable;\n","// extracted by mini-css-extract-plugin\nexport default {\"container\":\"Leaderboard--container--PJWVx\",\"progress\":\"Leaderboard--progress--Bq2yF\",\"bar\":\"Leaderboard--bar--inhxs\",\"percent\":\"Leaderboard--percent--6RliV\",\"avatar-and-name-container\":\"Leaderboard--avatar-and-name-container--PqA52\",\"avatarAndNameContainer\":\"Leaderboard--avatar-and-name-container--PqA52\",\"avatar\":\"Leaderboard--avatar--xrmSr\",\"details\":\"Leaderboard--details--iggfk\",\"name\":\"Leaderboard--name--nNWT5\",\"location\":\"Leaderboard--location--qCpmz\",\"privacy-section\":\"Leaderboard--privacy-section--PTdz2\",\"privacySection\":\"Leaderboard--privacy-section--PTdz2\",\"copy\":\"Leaderboard--copy--OJoTc\"};","import React, { useEffect, useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport I18n from 'utils/I18n';\nimport Avatar from '@strava/ui/Avatar';\nimport { trackV2 } from 'utils/analytics';\nimport Cldr from 'utils/Cldr';\nimport { countries, lookup } from 'country-data';\nimport ActionsLockClosedNormalSmall from '@strava/icons/ActionsLockClosedNormalSmall';\nimport useFetchLeaderboardData, {\n STATUS,\n CATEGORIES,\n PREMIUM_CATEGORIES\n} from './useFetchLeaderboardData';\nimport CurrentPlace from './CurrentPlace';\nimport CategoryTabs from './CategoryTabs';\nimport LeaderboardsTable from './LeaderboardsTable';\nimport ProgressBar from '../ProgressBar';\n\nimport styles from './styles.scss';\n\nconst I18N_PREFIX = 'strava.challenges.challenge_detail.leaderboard';\n\n// Overall tab - subcategory filters\nconst GENDER_SUB_CATEGORIES = {\n overall: 'overall',\n men: 'men',\n women: 'women'\n};\n\n// Age Group - subcategory filters\nconst AGE_SUB_CATEGORIES = {\n bucket_0_19: '0_19',\n bucket_20_24: '20_24',\n bucket_25_34: '25_34',\n bucket_35_44: '35_44',\n bucket_45_54: '45_54',\n bucket_55_64: '55_64',\n bucket_65_69: '65_69',\n bucket_70_74: '70_74',\n bucket_75_plus: '75_plus'\n};\n\nconst COUNTRY_SUB_CATEGORIES = countries.all\n .filter((c) => c.alpha3 !== '' && c.status === 'assigned')\n .map((c) => ({\n name: c.name,\n code: c.alpha3\n }))\n .sort((c1, c2) => {\n return c1.name.localeCompare(c2.name);\n });\n\nconst DEFAULT_PAGE_LIMIT = 20;\n\nconst Leaderboard = ({ currentAthlete, challengeId, limit, joined }) => {\n const [params, setParams] = useState({\n category: CATEGORIES.gender,\n sub_category: GENDER_SUB_CATEGORIES.overall,\n limit\n });\n\n const weightMeasurementUnit =\n (currentAthlete && currentAthlete.weight_measurement_unit) || 'lbs';\n\n const { data, status, message } = useFetchLeaderboardData({\n challengeId,\n joined,\n params,\n weightMeasurementUnit,\n subscribed: (currentAthlete && currentAthlete.subscribed) || false\n });\n const {\n entries,\n viewingAthletePublicEntry,\n activityType,\n size,\n dimension,\n showActivityCount,\n challengeType,\n isTeamChallenge\n } = data;\n\n const weightCategories = () => {\n if (weightMeasurementUnit === 'kg') {\n return [\n '0_54_kg',\n '55_64_kg',\n '65_74_kg',\n '75_84_kg',\n '85_94_kg',\n '95_104_kg',\n '105_114_kg',\n '115_plus_kg'\n ];\n }\n return [\n '0_124_lb',\n '125_149_lb',\n '150_164_lb',\n '165_179_lb',\n '180_199_lb',\n '200_224_lb',\n '225_249_lb',\n '250_plus_lb'\n ];\n };\n\n const defaultClub = currentAthlete && Object.keys(currentAthlete.clubs)[0];\n\n const trackAnalytics = (elementData, category, subCategory) => {\n trackV2({\n page: 'challenge_details',\n category: 'challenges',\n ...elementData,\n properties: {\n viewing_athlete_id: currentAthlete ? currentAthlete.id : null,\n challenge_id: challengeId,\n leaderboard_category_type: category,\n leaderboard_subcategory_type: subCategory\n }\n });\n };\n\n useEffect(() => {\n trackAnalytics(\n {\n element: 'challenge_leaderboards',\n action: 'screen_enter'\n },\n params.category,\n params.sub_category\n );\n }, []);\n\n const currentAthleteCountryISO = () => {\n const country = lookup.countries({ name: currentAthlete.country })[0];\n return country ? country.alpha3 : 'USA';\n };\n\n const activityCountLabel = () => {\n if (activityType === 'run') return I18n.t(`${I18N_PREFIX}.columns.runs`);\n if (activityType === 'ride') return I18n.t(`${I18N_PREFIX}.columns.rides`);\n return I18n.t(`${I18N_PREFIX}.columns.activities`);\n };\n\n const dimensionLabel = () => {\n if (dimension === 'elevation_gain' || dimension === 'elevation_loss')\n return I18n.t(`${I18N_PREFIX}.columns.elevation`);\n if (dimension === 'elapsed_time' || dimension === 'moving_time')\n return I18n.t(`${I18N_PREFIX}.columns.time`);\n if (dimension === 'distance')\n return I18n.t(`${I18N_PREFIX}.columns.distance`);\n if (dimension === 'avg_pace_min_distance')\n return I18n.t(`${I18N_PREFIX}.columns.pace`);\n return '';\n };\n\n // Overall filter is technically not a filter since that is a superset of\n // all the entries. The other filters are a subset.\n const hasFilteredEntrySubset = () => {\n return !(\n params.category === CATEGORIES.gender &&\n params.sub_category === GENDER_SUB_CATEGORIES.overall\n );\n };\n\n const showDistanceColumn = dimension === 'avg_pace_min_distance';\n\n const showProgressColumn =\n challengeType !== 'BestActivityChallenge' && !isTeamChallenge;\n\n const columnHeaders = () => {\n const headers = [\n {\n id: 1,\n label: I18n.t(`${I18N_PREFIX}.columns.overall`),\n styleName: 'overallRank'\n },\n {\n id: 2,\n label: I18n.t(`${I18N_PREFIX}.columns.name`),\n styleName: 'name'\n },\n { id: 4, label: dimensionLabel(), styleName: 'dimension' }\n ];\n if (showProgressColumn) {\n headers.push({\n id: 5,\n label: I18n.t(`${I18N_PREFIX}.columns.progress`),\n styleName: 'progress'\n });\n }\n\n if (hasFilteredEntrySubset()) {\n headers.splice(1, 0, {\n id: 6,\n label: I18n.t(`${I18N_PREFIX}.columns.place`),\n styleName: 'filteredEntryPlace'\n });\n // Readjust name column\n headers[2].style = styles.nameShort;\n }\n // hide/show activity count column\n if (showActivityCount) {\n headers.splice(hasFilteredEntrySubset() ? 3 : 2, 0, {\n id: 3,\n label: activityCountLabel(),\n styleName: 'activityCount'\n });\n }\n // hide/show extra distance column\n if (showDistanceColumn) {\n headers.splice(hasFilteredEntrySubset() ? 3 : 2, 0, {\n id: 7,\n label: I18n.t(`${I18N_PREFIX}.columns.distance`),\n styleName: 'distance'\n });\n }\n\n return headers;\n };\n\n const progressBar = (progressPercentage, progressFraction) => {\n return (\n
\n
1 ? 1 : progressFraction || 0}\n />\n {progressPercentage}
\n \n );\n };\n\n const avatarColumn = (athlete) => (\n
\n
\n
\n
\n
{athlete.location}
\n
\n
\n );\n\n const dimensionValueColumn = (entry) => {\n if (entry.activityLink) {\n return
{entry.dimensionValue};\n }\n\n return entry.dimensionValue;\n };\n\n const isViewingAthleteEntry = (entryAthleteId) =>\n currentAthlete && entryAthleteId === currentAthlete.id;\n\n const extractColumnValuesForEntry = (entry) => {\n const displayRowColumns = [\n {\n id: 1,\n value: (entry && Cldr.formatDecimal(entry.overallRank)) || '...'\n },\n { id: 2, value: (entry && avatarColumn(entry.athlete)) || '...' },\n { id: 4, value: (entry && dimensionValueColumn(entry)) || '...' }\n ];\n if (showProgressColumn) {\n displayRowColumns.push({\n id: 5,\n value:\n (entry &&\n progressBar(entry.progressPercentage, entry.progressFraction)) ||\n '...'\n });\n }\n if (hasFilteredEntrySubset()) {\n displayRowColumns.splice(1, 0, {\n id: 6,\n value: (entry && Cldr.formatDecimal(entry.place)) || '--'\n });\n }\n // hide/show activity count column\n if (showActivityCount) {\n displayRowColumns.splice(hasFilteredEntrySubset() ? 3 : 2, 0, {\n id: 3,\n value: (entry && Cldr.formatDecimal(entry.activityCount)) || '...'\n });\n }\n // hide/show distance column\n if (showDistanceColumn) {\n displayRowColumns.splice(hasFilteredEntrySubset() ? 3 : 2, 0, {\n id: 7,\n value: (entry && entry.formattedDistance) || '--'\n });\n }\n return displayRowColumns;\n };\n\n const rowsWithDisplayValuesOnly = () => {\n const displayRows = [];\n let viewingAthleteIsOnPage = false;\n entries.forEach((entry, index) => {\n const isViewingAthlete = isViewingAthleteEntry(entry.athlete.id);\n if (isViewingAthlete) {\n viewingAthleteIsOnPage = true;\n }\n const row = {\n id: index,\n highlight: isViewingAthlete\n };\n row.value = extractColumnValuesForEntry(entry);\n displayRows.push(row);\n });\n // Build contextual position row for viewing athlete if:\n // 1. Viewing athlete public entry is present - athlete is participating in challenge\n // 2. Viewing athlete entry does not fall into current page ranking range\n // 3. Page has result rows\n // 4. Viewing athlete entry is on a later page (one of the next-pages)\n if (\n viewingAthletePublicEntry &&\n !viewingAthleteIsOnPage &&\n entries.length > 0 &&\n entries.slice(-1)[0].place < viewingAthletePublicEntry.place\n ) {\n // dummy row\n displayRows.push({\n id: entries.length + 1,\n highlight: false,\n value: extractColumnValuesForEntry()\n });\n displayRows.push({\n id: entries.length + 2,\n highlight: true,\n value: extractColumnValuesForEntry(viewingAthletePublicEntry)\n });\n }\n return displayRows;\n };\n\n // Builds leaderboard categories and their respective sub-category filter labels\n // This dictionary is used to populate leaderboard tabs and the values in the filter\n // dropdown for that particular category.\n // eg.\n // { gender: {\n // title:\"Overall\",\n // subCategories:{male: \"Male\", female: \"Female\", overall: \"Overall\" ...}\n // } }\n const categories = () => {\n const categoriesMap = {};\n // logged out athletes should only see overall tab\n if (currentAthlete) {\n Object.keys(CATEGORIES).forEach((category) => {\n categoriesMap[category] = {\n title: I18n.t(`${I18N_PREFIX}.categories.${category}`),\n premium: PREMIUM_CATEGORIES.includes(category)\n };\n });\n } else {\n categoriesMap[CATEGORIES.gender] = {\n title: I18n.t(`${I18N_PREFIX}.categories.gender`),\n premium: PREMIUM_CATEGORIES.includes(CATEGORIES.gender)\n };\n }\n // populate categories with their respective sub_categories\n Object.keys(categoriesMap).forEach((category) => {\n const subCategoryMap = {};\n\n switch (category) {\n // Overall tab - subcategoy\n case CATEGORIES.gender:\n Object.keys(GENDER_SUB_CATEGORIES).forEach((subCategory) => {\n subCategoryMap[subCategory] = I18n.t(\n `${I18N_PREFIX}.sub_categories.gender.${subCategory}`\n );\n });\n break;\n // Age tab - subcategory filter options\n case CATEGORIES.age:\n Object.values(AGE_SUB_CATEGORIES).forEach((subCategory) => {\n subCategoryMap[subCategory] = I18n.t(\n `${I18N_PREFIX}.sub_categories.age.${subCategory}`\n );\n });\n break;\n // Country tab - subcategory filter options\n case CATEGORIES.country:\n COUNTRY_SUB_CATEGORIES.forEach((f) => {\n subCategoryMap[f.code] = f.name;\n });\n break;\n // Clubs tab\n case CATEGORIES.club:\n Object.keys(currentAthlete.clubs).forEach((clubId) => {\n subCategoryMap[clubId] = currentAthlete.clubs[clubId];\n });\n break;\n // Weight tab\n case CATEGORIES.weight:\n weightCategories().forEach((weightCategory) => {\n subCategoryMap[weightCategory] = I18n.t(\n `${I18N_PREFIX}.sub_categories.weight.${weightCategory}`\n );\n });\n break;\n default:\n }\n categoriesMap[category].subCategories = subCategoryMap;\n });\n return categoriesMap;\n };\n\n const handleSubscribeClick = (e) => {\n e.preventDefault();\n trackAnalytics(\n {\n element: 'subscribe_btn',\n action: 'click'\n },\n params.category,\n params.sub_category\n );\n\n window.location = e.target.href;\n };\n\n const onNextClick = () => {\n const row = entries.slice(-1)[0];\n setParams((prevParams) => {\n // Remove before from params (can only navigate in one direction)\n const { before, ...rest } = prevParams;\n return {\n ...rest,\n after: {\n rank: row.place,\n value: row.value,\n athlete_id: row.athlete.id\n }\n };\n });\n trackAnalytics(\n {\n element: 'pagination_next_btn',\n action: 'click'\n },\n params.category,\n params.sub_category\n );\n };\n\n const onPrevClick = () => {\n const row = entries[0];\n setParams((prevParams) => {\n // Remove after from params (can only navigate in one direction)\n const { after, ...rest } = prevParams;\n return {\n ...rest,\n before: {\n rank: row.place,\n value: row.value,\n athlete_id: row.athlete.id\n }\n };\n });\n trackAnalytics(\n {\n element: 'pagination_prev_btn',\n action: 'click'\n },\n params.category,\n params.sub_category\n );\n };\n\n // Leaderboard tabs [ Overall, I'm Following, Clubs, Country etc... ]\n const onTabChange = (i) => {\n // Reset pagination progress and current filter when tab is changed\n const { before, after, sub_category: _, ...rest } = params;\n const newParams = { ...rest };\n switch (i) {\n case 0:\n newParams.category = CATEGORIES.gender;\n newParams.sub_category = GENDER_SUB_CATEGORIES.overall;\n break;\n case 1:\n // Following tab (has no filters)\n newParams.category = CATEGORIES.following;\n break;\n case 2:\n newParams.category = CATEGORIES.club;\n if (defaultClub) {\n newParams.sub_category = defaultClub;\n }\n break;\n case 3:\n newParams.category = CATEGORIES.country;\n newParams.sub_category = currentAthleteCountryISO();\n break;\n case 4:\n newParams.category = CATEGORIES.age;\n newParams.sub_category = currentAthlete.ageGroup;\n break;\n case 5:\n newParams.category = CATEGORIES.weight;\n newParams.sub_category = currentAthlete.weight_group.concat(\n weightMeasurementUnit === 'kg' ? '_kg' : '_lb'\n );\n break;\n default:\n }\n\n setParams(newParams);\n trackAnalytics(\n {\n element: 'category_tab',\n action: 'click'\n },\n newParams.category,\n newParams.sub_category\n );\n };\n\n const onFilterChange = (subCategory) => {\n // Reset pagination progress when filter is changed\n const { before, after, ...rest } = params;\n setParams({\n ...rest,\n sub_category: subCategory\n });\n trackAnalytics(\n { element: 'leaderboard_filter', action: 'click' },\n params.category,\n subCategory\n );\n };\n\n const tableColumnHeaders = columnHeaders();\n\n const buildPaginationLabel = () => {\n if (status === STATUS.resolved && entries.length > 0) {\n return I18n.t(`${I18N_PREFIX}.pagination_label`, {\n topRowRank: Cldr.formatDecimal(entries[0].place) || '--',\n bottomRowRank: Cldr.formatDecimal(entries.slice(-1)[0].place) || '--',\n leaderboardSize: Cldr.formatDecimal(size) || '--'\n });\n }\n return `--`;\n };\n\n const leaderboardData = {\n currentCategory: params.category,\n currentSubcategory: params.sub_category,\n viewingAthleteRank:\n viewingAthletePublicEntry && viewingAthletePublicEntry.place,\n categories: categories(),\n columnHeaders: tableColumnHeaders,\n size,\n currentPlaceLabel: I18n.t(`${I18N_PREFIX}.current_place`),\n status,\n limit: params.limit,\n message,\n resultRows: rowsWithDisplayValuesOnly(),\n isLastPage: entries.length > 0 && entries.slice(-1)[0].place === size,\n isFirstPage: entries.length > 0 && entries[0].place === 1,\n paginationLabel: buildPaginationLabel()\n };\n\n return (\n
\n {!isTeamChallenge &&
}\n
\n
\n
\n {I18n.t(\n `${I18N_PREFIX}.${isTeamChallenge ? 'team_privacy' : 'privacy_v2'}`\n )}\n
\n
\n
\n
\n
\n );\n};\nLeaderboard.defaultProps = {\n currentAthlete: null,\n limit: DEFAULT_PAGE_LIMIT,\n joined: false\n};\n\nLeaderboard.propTypes = {\n currentAthlete: PropTypes.shape({\n id: PropTypes.number,\n ageGroup: PropTypes.string,\n country: PropTypes.string,\n clubs: PropTypes.shape({}),\n weight_measurement_unit: PropTypes.string,\n weight_group: PropTypes.string,\n subscribed: PropTypes.bool\n }),\n challengeId: PropTypes.number.isRequired,\n limit: PropTypes.number,\n joined: PropTypes.bool\n};\n\nexport default Leaderboard;\nexport { I18N_PREFIX };\n","import React from 'react';\nimport PropTypes from 'prop-types';\nimport Collapsible from 'react-collapsible';\n\nimport CollapsibleTitleWithCaret from 'components/CollapsibleTitleWithCaret';\nimport Discussion from 'components/Discussions';\nimport { connect } from 'react-redux';\nimport QualifyingActivities from '../QualifyingActivities';\nimport styles from './styles.scss';\n\nimport Leaderboard from '../Leaderboard';\n\nconst CollapsibleSection = ({\n challengeId,\n sections,\n currentAthlete,\n joined\n}) => {\n const createMarkup = (html) => ({ __html: html });\n\n const buildSections = () =>\n sections.map((section) => (\n
\n
\n }\n triggerWhenOpen={\n
\n }\n >\n
\n {section.content.map((content) => (\n
\n {content.heading && (\n
\n {content.heading}\n
\n )}\n\n {/* HTML is being (dangerously) rendered because this description is\n added by an admin from a dashboard on admin.strava.com */}\n {content.text && (\n
\n )}\n\n {content.imageUrl && (\n

\n )}\n\n {/* Leaderboard */}\n {content.key === 'leaderboard' && (\n
\n )}\n\n {/* Qualifying activities */}\n {content.qualifyingActivities && (\n
\n )}\n\n {/* Discussions */}\n {content.key === 'discussions' && (\n
\n )}\n
\n ))}\n
\n \n
\n
\n ));\n\n return (\n
\n {sections.length > 0 && buildSections(sections)}\n
\n );\n};\n\nCollapsibleSection.defaultProps = {\n currentAthlete: null,\n joined: false\n};\n\nCollapsibleSection.propTypes = {\n challengeId: PropTypes.number.isRequired,\n sections: PropTypes.arrayOf(\n PropTypes.shape({\n title: PropTypes.string.isRequired,\n content: PropTypes.arrayOf(\n PropTypes.shape({\n key: PropTypes.string.isRequired,\n heading: PropTypes.string,\n text: PropTypes.string,\n imageUrl: PropTypes.string,\n imageTitle: PropTypes.string,\n qualifyingActivities: PropTypes.arrayOf(PropTypes.shape({}))\n })\n ).isRequired,\n openOnLoad: PropTypes.bool.isRequired\n })\n ).isRequired,\n currentAthlete: PropTypes.shape({}),\n joined: PropTypes.bool\n};\n\nexport default connect((state) => ({\n joined: state.joined\n}))(CollapsibleSection);\n\nexport { CollapsibleSection };\n","// extracted by mini-css-extract-plugin\nexport default {\"container\":\"Facepile--container--2nJHN\",\"avatar-list\":\"Facepile--avatar-list--4j4R0\",\"avatarList\":\"Facepile--avatar-list--4j4R0\",\"avatar\":\"Facepile--avatar--1V7OV\",\"avatar-img\":\"Facepile--avatar-img--rr1lE\",\"avatarImg\":\"Facepile--avatar-img--rr1lE\",\"message\":\"Facepile--message--WDyJB\",\"xsmall\":\"Facepile--xsmall--5nxJ7\",\"small\":\"Facepile--small--BEBrE\"};","import React from 'react';\nimport PropTypes from 'prop-types';\nimport Avatar from '@strava/ui/Avatar';\n\nimport styles from './styles.scss';\n\nconst messageTextStyleClass = (size) => {\n return size === 'small' ? `text-subhead` : `text-caption1`;\n};\n\nconst Facepile = ({ athletes, message, size, className, ...props }) => (\n
\n
\n {athletes.map((athlete) => (\n - \n \n
\n ))}\n
\n {message && (\n
\n {message}\n
\n )}\n
\n);\n\nFacepile.propTypes = {\n athletes: PropTypes.arrayOf(\n PropTypes.shape({\n id: PropTypes.number.isRequired,\n displayName: PropTypes.string.isRequired,\n profileImg: PropTypes.string.isRequired\n })\n ).isRequired,\n message: PropTypes.string,\n size: PropTypes.oneOf(['xsmall', 'small']),\n className: PropTypes.string\n};\n\nFacepile.defaultProps = {\n message: null,\n size: null,\n className: ''\n};\n\nexport default Facepile;\n","import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Tabs as ReactTabs } from 'react-tabs';\n\nconst Tabs = ({ children, className, defaultIndex, ...props }) => (\n
\n {children}\n \n);\n\nTabs.tabsRole = 'Tabs';\n\nTabs.defaultProps = {\n className: '',\n defaultIndex: 0\n};\n\nTabs.propTypes = {\n className: PropTypes.string,\n defaultIndex: PropTypes.number,\n children: PropTypes.arrayOf(PropTypes.element).isRequired\n};\n\nexport default Tabs;\n","// extracted by mini-css-extract-plugin\nexport default {\"tab-list\":\"TabList--tab-list--39knt\",\"tabList\":\"TabList--tab-list--39knt\"};","import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { TabList as ReactTabList } from 'react-tabs';\n\nimport styles from './styles.scss';\n\nconst TabList = ({ children, className, ...props }) => (\n
\n {children}\n \n);\n\nTabList.tabsRole = 'TabList';\n\nTabList.defaultProps = {\n className: ''\n};\n\nTabList.propTypes = {\n className: PropTypes.string,\n children: PropTypes.node.isRequired\n};\n\nexport default TabList;\n","// extracted by mini-css-extract-plugin\nexport default {\"tab\":\"Tab--tab--oamEu\",\"selected\":\"Tab--selected--Bb4GU\"};","import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Tab as ReactTab } from 'react-tabs';\n\nimport styles from './styles.scss';\n\nconst Tab = ({ children, className, selectedClassName, ...props }) => (\n
\n {children}\n \n);\n\nTab.tabsRole = 'Tab';\n\nTab.defaultProps = {\n className: '',\n selectedClassName: ''\n};\n\nTab.propTypes = {\n className: PropTypes.string,\n selectedClassName: PropTypes.string,\n children: PropTypes.node.isRequired\n};\n\nexport default Tab;\n","import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { TabPanel as ReactTabPanel } from 'react-tabs';\n\nconst TabPanel = ({ children, className, selected, ...props }) => (\n
\n {children}\n \n);\n\nTabPanel.tabsRole = 'TabPanel';\n\nTabPanel.defaultProps = {\n className: '',\n selected: false\n};\n\nTabPanel.propTypes = {\n selected: PropTypes.bool,\n className: PropTypes.string,\n children: PropTypes.node.isRequired\n};\n\nexport default TabPanel;\n","// extracted by mini-css-extract-plugin\nexport default {\"modal-dialog\":\"FriendsList--modal-dialog--FU+d6\",\"modalDialog\":\"FriendsList--modal-dialog--FU+d6\",\"modal-content\":\"FriendsList--modal-content--IBsDM\",\"modalContent\":\"FriendsList--modal-content--IBsDM\",\"close-button\":\"FriendsList--close-button--7otO2\",\"closeButton\":\"FriendsList--close-button--7otO2\",\"modal-header\":\"FriendsList--modal-header--ToyG-\",\"modalHeader\":\"FriendsList--modal-header--ToyG-\",\"challenge-logo\":\"FriendsList--challenge-logo--2HQTx\",\"challengeLogo\":\"FriendsList--challenge-logo--2HQTx\",\"challenge-name\":\"FriendsList--challenge-name--ZFotm\",\"challengeName\":\"FriendsList--challenge-name--ZFotm\",\"tab-list\":\"FriendsList--tab-list--ZfSqo\",\"tabList\":\"FriendsList--tab-list--ZfSqo\",\"active-tab\":\"FriendsList--active-tab--BHC7x\",\"activeTab\":\"FriendsList--active-tab--BHC7x\",\"spinner\":\"FriendsList--spinner--Fa9v0\",\"friend\":\"FriendsList--friend--Cfz+W\",\"friend-text\":\"FriendsList--friend-text--dgbeL\",\"friendText\":\"FriendsList--friend-text--dgbeL\",\"tab-panel\":\"FriendsList--tab-panel--0eyUV\",\"tabPanel\":\"FriendsList--tab-panel--0eyUV\"};","import React from 'react';\nimport PropTypes from 'prop-types';\nimport Spinner from '@strava/ui/Spinner';\nimport Avatar from '@strava/ui/Avatar';\n\nimport MediaQuery, { breakpoints } from 'utils/media-query';\n\nimport Modal from 'components/shared/Modal';\nimport { Tabs, TabList, Tab, TabPanel } from 'components/shared/tabs';\n\nimport styles from './styles.scss';\n\nconst renderFriends = (friends) =>\n friends.map((friend) => (\n
\n \n \n \n ));\n\nconst FriendsList = ({\n modalIsOpen,\n friendsRequested,\n friends,\n closeModal,\n onModalOpen,\n challengeLogo,\n challengeName\n}) => (\n
\n \n
\n
\n

\n {/* Bigger screens - tablets & desktop */}\n
\n \n {challengeName}\n
\n \n\n {/* Smaller screens - mobile */}\n
\n \n {challengeName}\n
\n \n
\n
\n
\n \n {`Friends ${\n friends ? `(${friends.length})` : ''\n }`}\n \n \n \n {friendsRequested ? (\n - \n \n
\n ) : (\n friends && renderFriends(friends)\n )}\n
\n \n \n
\n
\n \n);\n\nFriendsList.defaultProps = {\n friends: null\n};\n\nFriendsList.propTypes = {\n modalIsOpen: PropTypes.bool.isRequired,\n friendsRequested: PropTypes.bool.isRequired,\n friends: PropTypes.arrayOf(\n PropTypes.shape({\n name: PropTypes.string,\n avatarSrc: PropTypes.string,\n athletePath: PropTypes.string,\n location: PropTypes.string\n })\n ),\n closeModal: PropTypes.func.isRequired,\n onModalOpen: PropTypes.func.isRequired,\n challengeLogo: PropTypes.string.isRequired,\n challengeName: PropTypes.string.isRequired\n};\n\nexport default FriendsList;\n","// extracted by mini-css-extract-plugin\nexport default {\"facepile-wrapper\":\"Followers--facepile-wrapper--usVMO\",\"facepileWrapper\":\"Followers--facepile-wrapper--usVMO\",\"facepile\":\"Followers--facepile--AgKJn\"};","import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport createNetworkingClient from 'utils/networking-client';\nimport I18n from 'utils/I18n';\n\nimport Facepile from 'components/shared/Facepile';\n\nimport FriendsList from '../FriendsList';\n\nimport styles from './styles.scss';\n\nconst I18nPrefix = 'strava.challenges.challenge_detail';\n\nclass Followers extends React.Component {\n state = {\n modalIsOpen: false,\n friendsRequested: false,\n friends: null\n };\n\n onModalOpen = () => {\n const { challengeId } = this.props;\n const { friends } = this.state;\n\n if (!friends) {\n this.setState({ friendsRequested: true });\n const instance = createNetworkingClient();\n\n instance\n .get(`/challenges/${challengeId}/friends`)\n .then((response) => {\n if (response && response.status === 200) {\n this.onSuccess(response.data);\n } else {\n this.onFail();\n }\n })\n .catch(() => {\n this.onFail();\n });\n }\n };\n\n onSuccess = (data) => {\n this.setState({ friendsRequested: false, friends: data });\n };\n\n onFail = () => {\n this.setState({ friendsRequested: false });\n throw new Error('Something went wrong...');\n };\n\n openModal = () => {\n this.setState({ modalIsOpen: true });\n };\n\n closeModal = () => {\n this.setState({ modalIsOpen: false });\n };\n\n render() {\n const { modalIsOpen, friendsRequested, friends } = this.state;\n const {\n followersCount,\n followers,\n challengeLogo,\n challengeName,\n size,\n className\n } = this.props;\n\n return (\n
\n );\n }\n}\n\nFollowers.defaultProps = {\n size: 'small',\n className: null\n};\n\nFollowers.propTypes = {\n challengeId: PropTypes.number.isRequired,\n challengeLogo: PropTypes.string.isRequired,\n challengeName: PropTypes.string.isRequired,\n followersCount: PropTypes.number.isRequired,\n followers: PropTypes.arrayOf(PropTypes.shape({})).isRequired,\n size: PropTypes.string,\n className: PropTypes.string\n};\n\nexport default Followers;\n","import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\n\nimport ProgressBar from '../../../shared/components/ProgressBar';\nimport StreaksCalendar from '../../../shared/components/StreaksCalendar';\n\nexport const Progress = ({ streaksCalendar, data }) => {\n if (!Object.keys(data).length) {\n return null;\n }\n\n return (\n
\n {streaksCalendar ? (\n
\n ) : (\n
\n )}\n
\n );\n};\n\nProgress.propTypes = {\n streaksCalendar: PropTypes.bool.isRequired,\n data: PropTypes.shape({}).isRequired\n};\n\nexport default connect(({ progress }) => ({\n data: progress.data,\n streaksCalendar: progress.streaksCalendar\n}))(Progress);\n","export function validateEmail(email) {\n return /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(\n email\n );\n}\n\nexport default { validateEmail };\n","// extracted by mini-css-extract-plugin\nexport default {\"container\":\"EmailInput--container--vek-4\",\"error\":\"EmailInput--error--xUGR-\"};","import React, { Component } from 'react';\nimport CreatableSelect from 'react-select/creatable';\nimport PropTypes from 'prop-types';\n\nimport { validateEmail } from 'utils/validators';\nimport I18n from 'utils/I18n';\n\nimport styles from './styles.scss';\n\nconst I18nPrefix = 'strava.challenges.challenge_detail';\n\nconst createOption = (label) => {\n const email = label.trim().replace(',', '');\n\n return { label: email, value: email };\n};\n\nconst customStyles = {\n container: (provided) => ({\n ...provided,\n width: '100%',\n flex: 1,\n minHeight: 40\n }),\n control: (provided) => ({\n ...provided,\n border: 'solid 1px #ccccd0',\n boxShadow: '0 1px 4px 0 rgba(0, 0, 0, 0.11)',\n borderRadius: 2,\n minHeight: 40\n }),\n menu: (provided) => ({\n ...provided,\n marginTop: 0,\n borderRadius: '0 0 2px 2px',\n border: 'solid 1px #ccccd0',\n borderTop: 'none',\n boxShadow: '0px 1px 3px 2px rgba(0,0,0,.1)'\n }),\n multiValue: (provided) => ({\n ...provided,\n backgroundColor: '#F0F0F5'\n }),\n multiValueRemove: (provided) => ({\n ...provided,\n borderTopLeftRadius: 0,\n borderBottomLeftRadius: 0,\n color: 'inherit',\n ':hover': {\n backgroundColor: '#6D6D78',\n color: '#fff'\n }\n })\n};\n\nclass EmailInput extends Component {\n state = {\n inputValue: '',\n invalidInput: false,\n value: []\n };\n\n handleChange = (value) => {\n const { handleAddressesChange } = this.props;\n\n this.setState({ value });\n handleAddressesChange(value);\n };\n\n handleInputChange = (inputValue) => {\n this.setState({ inputValue });\n };\n\n validateInput = () => {\n const { inputValue } = this.state;\n\n return validateEmail(inputValue);\n };\n\n updateValue = () => {\n const { inputValue, value } = this.state;\n const { handleAddressesChange } = this.props;\n let newValue;\n\n if (!this.validateInput(inputValue)) {\n this.setState({ inputValue: '', invalidInput: true });\n return;\n }\n if (!value) {\n newValue = [createOption(inputValue)];\n } else {\n newValue = [...value, createOption(inputValue)];\n }\n this.setState({\n inputValue: '',\n value: newValue,\n invalidInput: false\n });\n handleAddressesChange(newValue);\n };\n\n handleKeyDown = (event) => {\n const { inputValue, value } = this.state;\n\n if (!inputValue) return;\n if (value && value.length >= 4) return;\n // eslint-disable-next-line default-case\n switch (event.key) {\n case 'Enter':\n case ',':\n case ' ':\n this.updateValue();\n event.preventDefault();\n }\n };\n\n handleBlur = () => {\n const { inputValue } = this.state;\n\n if (!inputValue) return;\n this.updateValue();\n };\n\n render() {\n const { inputValue, value, invalidInput } = this.state;\n return (\n
\n
\n {invalidInput && (\n
\n {I18n.t(`${I18nPrefix}.invalid_email`)}\n
\n )}\n
\n );\n }\n}\n\nEmailInput.propTypes = {\n handleAddressesChange: PropTypes.func.isRequired\n};\n\nexport default EmailInput;\n","// extracted by mini-css-extract-plugin\nexport default {\"above-input\":\"Email--above-input--alcu4\",\"aboveInput\":\"Email--above-input--alcu4\",\"select-input\":\"Email--select-input--4YL59\",\"selectInput\":\"Email--select-input--4YL59\",\"submit-button\":\"Email--submit-button--BxtP2\",\"submitButton\":\"Email--submit-button--BxtP2\",\"spinner\":\"Email--spinner--kaQzG\",\"message\":\"Email--message--h1tDs\",\"error\":\"Email--error--Ao1b7\",\"success\":\"Email--success--UDj1l\"};","import React from 'react';\nimport PropTypes from 'prop-types';\nimport Button from '@strava/ui/Button';\nimport Spinner from '@strava/ui/Spinner';\n\nimport createNetworkingClient from 'utils/networking-client';\nimport I18n from 'utils/I18n';\n\nimport EmailInput from '../EmailInput';\n\nimport styles from './styles.scss';\n\nconst I18nPrefix = 'strava.challenges.challenge_detail';\n\nclass Email extends React.Component {\n state = {\n submitRequested: false,\n submitSucceeded: false,\n errorMessage: '',\n successMessage: '',\n addresses: []\n };\n\n handleAddressesChange = (addresses) => {\n if (!addresses) {\n this.setState({ addresses: [] });\n return;\n }\n const emails = addresses.map((address) => address.value);\n this.setState({ addresses: emails });\n };\n\n handleSubmit = () => {\n const { addresses } = this.state;\n const { challengeId, challengeName, currentAthlete } = this.props;\n\n this.setState({ submitRequested: true });\n\n const message = I18n.t(`${I18nPrefix}.invite_message`, {\n athleteName: currentAthlete.name,\n challengeName\n });\n\n const instance = createNetworkingClient();\n\n instance\n .post(`/challenges/${challengeId}/share/send_email`, {\n emails: addresses,\n message\n })\n .then((response) => {\n if (response && response.status === 200) {\n this.onSubmitSuccess(response.data);\n } else {\n this.onSubmitFail();\n }\n })\n .catch(() => {\n this.onSubmitFail();\n });\n };\n\n onSubmitSuccess = () => {\n this.setState({\n submitRequested: false,\n submitSucceeded: true,\n errorMessage: '',\n successMessage: I18n.t(`${I18nPrefix}.success_message`)\n });\n\n setTimeout(() => {\n const { closeModal } = this.props;\n closeModal();\n }, 2000);\n };\n\n onSubmitFail = () => {\n this.setState({\n submitRequested: false,\n errorMessage: I18n.t(`${I18nPrefix}.error_message`)\n });\n };\n\n renderMessage = () => {\n const { errorMessage, successMessage } = this.state;\n\n if (errorMessage) {\n return (\n
{errorMessage}
\n );\n }\n\n if (successMessage) {\n return (\n
\n {successMessage}\n
\n );\n }\n\n return null;\n };\n\n render() {\n const { addresses, submitRequested, submitSucceeded } = this.state;\n const { challengeName, currentAthlete } = this.props;\n\n return (\n <>\n {this.renderMessage()}\n {submitRequested ? (\n
\n \n
\n ) : (\n !submitSucceeded && (\n
\n
\n {I18n.t(`${I18nPrefix}.to`)}\n \n {I18n.t(`${I18nPrefix}.limit`)}\n \n
\n
\n \n
\n
\n
{I18n.t(`${I18nPrefix}.message`)}
\n
\n {I18n.t(`${I18nPrefix}.invite_message`, {\n athleteName: currentAthlete.name,\n challengeName\n })}\n
\n
\n
\n
\n )\n )}\n >\n );\n }\n}\n\nEmail.propTypes = {\n closeModal: PropTypes.func.isRequired,\n challengeId: PropTypes.number.isRequired,\n currentAthlete: PropTypes.shape({\n name: PropTypes.string.isRequired\n }).isRequired,\n challengeName: PropTypes.string.isRequired\n};\n\nexport default Email;\n","// extracted by mini-css-extract-plugin\nexport default {\"container\":\"Select--container--Krxee\",\"avatar\":\"Select--avatar--YN9aw\"};","import React from 'react';\nimport PropTypes from 'prop-types';\nimport Select from 'react-select';\nimport Avatar from '@strava/ui/Avatar';\n\nimport I18n from 'utils/I18n';\n\nimport styles from './styles.scss';\n\nconst I18nPrefix = 'strava.challenges.challenge_detail';\n\nconst FriendOption = ({ name, picture }) => (\n
\n);\n\nFriendOption.propTypes = {\n name: PropTypes.string.isRequired,\n picture: PropTypes.string.isRequired\n};\n\nexport const FriendSelect = ({ options, handleChange }) => {\n const customStyles = {\n container: (provided) => ({\n ...provided,\n flex: 1,\n minHeight: 40\n }),\n control: (provided) => ({\n ...provided,\n border: 'solid 1px #ccccd0',\n boxShadow: '0 1px 4px 0 rgba(0, 0, 0, 0.11)',\n borderRadius: 2,\n minHeight: 40\n }),\n menu: (provided) => ({\n ...provided,\n marginTop: 0,\n borderRadius: '0 0 2px 2px',\n border: 'solid 1px #ccccd0',\n borderTop: 'none',\n boxShadow: '0px 1px 3px 2px rgba(0,0,0,.1)'\n }),\n multiValue: (provided) => ({\n ...provided,\n backgroundColor: '#F0F0F5'\n }),\n multiValueRemove: (provided) => ({\n ...provided,\n borderTopLeftRadius: 0,\n borderBottomLeftRadius: 0,\n color: 'inherit',\n ':hover': {\n backgroundColor: '#6D6D78',\n color: '#fff'\n }\n })\n };\n\n return (\n