From de20acd02aaef6d86caf2bd5aa851c2e3053e375 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Wed, 19 Aug 2026 11:05:05 +1000 Subject: [PATCH] Fix profile links to bypass redirects --- .../Header/__snapshots__/index.jsx.snap | 4 +- .../__snapshots__/LeaderboardTable.jsx.snap | 8 +- .../__snapshots__/PodiumSpot.jsx.snap | 4 +- .../components/__snapshots__/Content.jsx.snap | 14 ++-- .../Winner/__snapshots__/index.jsx.snap | 2 +- __tests__/shared/utils/url.test.js | 40 ++++++++++ .../config/automation-config-dev.json | 2 +- .../config/automation-config-local.json | 2 +- .../config/automation-config-prod.json | 2 +- .../pages/topcoder/header/header.po.ts | 2 +- config/backup-default.js | 4 +- config/default.js | 4 +- config/production.js | 4 +- src/shared/actions/recruitCRM.js | 3 +- src/shared/components/Content/index.jsx | 9 ++- src/shared/components/GSheet/index.jsx | 3 +- src/shared/components/Gigs/GigApply/index.jsx | 5 +- .../HallOfFamePage/Champions/Track/index.jsx | 3 +- .../HallOfFamePage/Finalists/Track/index.jsx | 5 +- .../HallOfFamePage/TripWinners/Role/index.jsx | 3 +- src/shared/components/Header/index.jsx | 20 ++++- .../Leaderboard/LeaderboardTable/index.jsx | 3 +- .../Leaderboard/PodiumSpot/index.jsx | 7 +- src/shared/components/Looker/index.jsx | 3 +- .../components/MMatchLeaderboard/index.jsx | 15 ++-- .../components/ProfileBadgesPage/index.jsx | 8 +- .../ApplicationsTab/index.jsx | 3 +- .../challenge-detail/Registrants/index.jsx | 3 +- .../Submissions/SubmissionRow/index.jsx | 3 +- .../challenge-detail/Submissions/index.jsx | 3 +- .../challenge-detail/Winners/Winner/index.jsx | 3 +- .../LeaderboardAvatar/index.jsx | 3 +- .../tc-communities/Header/index.jsx | 3 +- .../blockchain/BsicHackathon/index.jsx | 75 ++++++++++--------- .../communities/iot/AssetDetail/index.jsx | 9 ++- .../tco/scoreboard/ScoreboardTable/index.jsx | 3 +- .../Contentful/MenuLoader/index.jsx | 24 +++++- src/shared/containers/Gamification/index.jsx | 3 +- src/shared/containers/Profile.jsx | 3 +- .../tc-communities/tco20/Header.jsx | 7 +- .../pending-approvals/approval-item/index.jsx | 4 +- .../events/event-item/index.jsx | 4 +- src/shared/utils/url.js | 55 ++++++++++++++ 43 files changed, 272 insertions(+), 115 deletions(-) create mode 100644 __tests__/shared/utils/url.test.js diff --git a/__tests__/shared/components/Header/__snapshots__/index.jsx.snap b/__tests__/shared/components/Header/__snapshots__/index.jsx.snap index 002a344ad..0d57415cc 100644 --- a/__tests__/shared/components/Header/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/Header/__snapshots__/index.jsx.snap @@ -32,8 +32,8 @@ exports[`Default render 1`] = ` "title": "Home", }, Object { - "href": "/members/willFilledByUserName", - "id": "myprofile", + "href": "https://profiles.topcoder-dev.com/huanner", + "id": "profile", "logged": true, "title": "My Profile", }, diff --git a/__tests__/shared/components/Leaderboard/__snapshots__/LeaderboardTable.jsx.snap b/__tests__/shared/components/Leaderboard/__snapshots__/LeaderboardTable.jsx.snap index 9f2998a10..e2437bedb 100644 --- a/__tests__/shared/components/Leaderboard/__snapshots__/LeaderboardTable.jsx.snap +++ b/__tests__/shared/components/Leaderboard/__snapshots__/LeaderboardTable.jsx.snap @@ -57,7 +57,7 @@ exports[`Matches shallow shapshot 1`] = ` className="src-shared-components-Leaderboard-LeaderboardTable-themes-___styles__col-handle___1wDhG" > @@ -80,7 +80,7 @@ exports[`Matches shallow shapshot 2`] = `
diff --git a/__tests__/shared/components/__snapshots__/Content.jsx.snap b/__tests__/shared/components/__snapshots__/Content.jsx.snap index b40694e90..afc4a0d90 100644 --- a/__tests__/shared/components/__snapshots__/Content.jsx.snap +++ b/__tests__/shared/components/__snapshots__/Content.jsx.snap @@ -260,19 +260,17 @@ exports[`Matches shallow shapshot 1`] = ` endpoint. Valid links on dev:
- Profile 1. - +

- Profile 2. - +
  • Stand-alone terms of use page: ‌ diff --git a/__tests__/shared/components/challenge-detail/Winners/Winner/__snapshots__/index.jsx.snap b/__tests__/shared/components/challenge-detail/Winners/Winner/__snapshots__/index.jsx.snap index cfbac2705..28bfc3ad9 100644 --- a/__tests__/shared/components/challenge-detail/Winners/Winner/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/challenge-detail/Winners/Winner/__snapshots__/index.jsx.snap @@ -34,7 +34,7 @@ exports[`Matches shallow shapshot shapshot 1 1`] = `
    test diff --git a/__tests__/shared/utils/url.test.js b/__tests__/shared/utils/url.test.js new file mode 100644 index 000000000..47eebe8aa --- /dev/null +++ b/__tests__/shared/utils/url.test.js @@ -0,0 +1,40 @@ +import { + getMemberProfileUrl, + getMenuWithDirectProfileLink, + updateLegacyProfileLinks, +} from 'utils/url'; + +describe('member profile URLs', () => { + it('uses the configured profile application URL', () => { + expect(getMemberProfileUrl('tourist')) + .toBe('https://profiles.topcoder-dev.com/tourist'); + }); + + it('normalizes the profile entry in a navigation menu', () => { + const menu = [{ + id: 'community', + secondaryMenu: [{ id: 'myprofile', href: '/members/placeholder' }], + }]; + + expect(getMenuWithDirectProfileLink(menu, 'tourist')[0].secondaryMenu[0]) + .toEqual({ + id: 'profile', + href: 'https://profiles.topcoder-dev.com/tourist', + }); + }); + + it('updates only an exact legacy profile anchor rendered by navigation', () => { + const root = document.createElement('nav'); + root.innerHTML = [ + 'Profile', + 'Badges', + ].join(''); + + updateLegacyProfileLinks(root, 'tourist'); + + expect(root.querySelector('a').getAttribute('href')) + .toBe('https://profiles.topcoder-dev.com/tourist'); + expect(root.querySelectorAll('a')[1].getAttribute('href')) + .toBe('/members/tourist/badges'); + }); +}); diff --git a/automated-smoke-test/config/automation-config-dev.json b/automated-smoke-test/config/automation-config-dev.json index 9ac9bf711..d249ce956 100644 --- a/automated-smoke-test/config/automation-config-dev.json +++ b/automated-smoke-test/config/automation-config-dev.json @@ -34,7 +34,7 @@ }, "subMenuUrlsAfterLogin": { "dashboard": "https://community-app.topcoder-dev.com/my-dashboard", - "myProfile": "https://community-app.topcoder-dev.com/members/tester1234", + "myProfile": "https://profiles.topcoder-dev.com/tester1234", "payments": "https://community.topcoder-dev.com/PactsMemberServlet?module=PaymentHistory&full_list=false", "competitiveProgramming": "https://community-app.topcoder-dev.com/community/arena", "forums": "https://apps.topcoder-dev.com/forums/" diff --git a/automated-smoke-test/config/automation-config-local.json b/automated-smoke-test/config/automation-config-local.json index c2ca342f5..925be12c4 100644 --- a/automated-smoke-test/config/automation-config-local.json +++ b/automated-smoke-test/config/automation-config-local.json @@ -34,7 +34,7 @@ }, "subMenuUrlsAfterLogin": { "dashboard": "http://localhost:3000/my-dashboard", - "myProfile": "http://localhost:3000/members/CustomerUser", + "myProfile": "https://profiles.topcoder-dev.com/CustomerUser", "payments": "https://community.topcoder.com/PactsMemberServlet?module=PaymentHistory&full_list=false", "forums": "https://apps.topcoder.com/forums/" }, diff --git a/automated-smoke-test/config/automation-config-prod.json b/automated-smoke-test/config/automation-config-prod.json index d05c56008..f31b77b8e 100644 --- a/automated-smoke-test/config/automation-config-prod.json +++ b/automated-smoke-test/config/automation-config-prod.json @@ -34,7 +34,7 @@ }, "subMenuUrlsAfterLogin": { "dashboard": "https://www.topcoder.com/my-dashboard", - "myProfile": "https://www.topcoder.com/members/CustomerUser", + "myProfile": "https://profiles.topcoder.com/CustomerUser", "payments": "https://community.topcoder.com/PactsMemberServlet?module=PaymentHistory&full_list=false", "forums": "https://apps.topcoder.com/forums/" }, diff --git a/automated-smoke-test/page-objects/pages/topcoder/header/header.po.ts b/automated-smoke-test/page-objects/pages/topcoder/header/header.po.ts index 45e41a47e..7c3ac18e7 100644 --- a/automated-smoke-test/page-objects/pages/topcoder/header/header.po.ts +++ b/automated-smoke-test/page-objects/pages/topcoder/header/header.po.ts @@ -134,7 +134,7 @@ export class HeaderPage { * Gets the notification bell icon */ private async getBellIcon() { - const xpath = `//a[contains(@href, '/members/${ConfigHelper.getUserName()}')]/parent::div/parent::div/div[position()=1]`; + const xpath = `//a[contains(@href, 'profiles.topcoder') and contains(@href, '/${ConfigHelper.getUserName()}')]/parent::div/parent::div/div[position()=1]`; const els = await ElementHelper.getAllElementsByXPath(xpath); return els[1]; } diff --git a/config/backup-default.js b/config/backup-default.js index 2b0d74756..0d1ce386e 100644 --- a/config/backup-default.js +++ b/config/backup-default.js @@ -310,9 +310,9 @@ module.exports = { logged: true, }, { - id: 'myprofile', + id: 'profile', title: 'My Profile', - href: '/members/willFilledByUserName', + href: 'https://profiles.topcoder-dev.com/willFilledByUserName', logged: true, }, { diff --git a/config/default.js b/config/default.js index ec826e361..91b955c89 100644 --- a/config/default.js +++ b/config/default.js @@ -314,9 +314,9 @@ module.exports = { logged: true, }, { - id: 'myprofile', + id: 'profile', title: 'My Profile', - href: '/members/willFilledByUserName', + href: 'https://profiles.topcoder-dev.com/willFilledByUserName', logged: true, }, { diff --git a/config/production.js b/config/production.js index f805153f7..17a98d876 100644 --- a/config/production.js +++ b/config/production.js @@ -100,9 +100,9 @@ module.exports = { logged: true, }, { - id: 'myprofile', + id: 'profile', title: 'My Profile', - href: '/members/willFilledByUserName?ref=nav', + href: 'https://profiles.topcoder.com/willFilledByUserName', logged: true, }, { diff --git a/src/shared/actions/recruitCRM.js b/src/shared/actions/recruitCRM.js index 1597b45fa..ba1d83d67 100644 --- a/src/shared/actions/recruitCRM.js +++ b/src/shared/actions/recruitCRM.js @@ -2,6 +2,7 @@ import { redux } from 'topcoder-react-utils'; import Service from 'services/recruitCRM'; import _ from 'lodash'; import { getCustomField } from 'utils/gigs'; +import { getMemberProfileUrl } from 'utils/url'; /** * Jobs page fetch init @@ -86,7 +87,7 @@ function normalizeRecruitPayload(job, payload) { custom_fields: [ { field_id: 1, - value: payload.tcProfileLink || (payload.handle ? `https://topcoder.com/members/${payload.handle}` : ''), + value: payload.tcProfileLink || (payload.handle ? getMemberProfileUrl(payload.handle) : ''), }, { field_id: 2, diff --git a/src/shared/components/Content/index.jsx b/src/shared/components/Content/index.jsx index a2e2d4681..415606fa9 100644 --- a/src/shared/components/Content/index.jsx +++ b/src/shared/components/Content/index.jsx @@ -7,6 +7,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { isomorphy } from 'topcoder-react-utils'; +import { getMemberProfileUrl } from 'utils/url'; import './style.scss'; @@ -247,13 +248,13 @@ export default function Content() { endpoint. Valid links on dev:
    - + Profile 1. - +
    - + Profile 2. - +
  • Stand-alone terms of use page: diff --git a/src/shared/components/GSheet/index.jsx b/src/shared/components/GSheet/index.jsx index c09a00fa4..545f9de7e 100644 --- a/src/shared/components/GSheet/index.jsx +++ b/src/shared/components/GSheet/index.jsx @@ -9,6 +9,7 @@ import _ from 'lodash'; import React, { Component } from 'react'; import { Scrollbars } from 'react-custom-scrollbars'; import cn from 'classnames'; +import { getMemberProfileUrl } from 'utils/url'; import './style.scss'; export default class GSheet extends Component { @@ -88,7 +89,7 @@ export default class GSheet extends Component { { (config.pick || sheet.rows[0]._sheet.headerValues).map(c => ( - {c.toLowerCase() === 'handle' ? ({record[c]}) : record[c]} + {c.toLowerCase() === 'handle' ? ({record[c]}) : record[c]} )) } diff --git a/src/shared/components/Gigs/GigApply/index.jsx b/src/shared/components/Gigs/GigApply/index.jsx index f3ea35051..55db5d652 100644 --- a/src/shared/components/Gigs/GigApply/index.jsx +++ b/src/shared/components/Gigs/GigApply/index.jsx @@ -12,6 +12,7 @@ import DropdownSkills from 'components/GUIKit/DropdownSkills'; import RadioButton from 'components/GUIKit/RadioButton'; import Checkbox from 'components/GUIKit/Checkbox'; import { getCustomField } from 'utils/gigs'; +import { getMemberProfileUrl } from 'utils/url'; import Modal from 'components/Contentful/Modal'; import FilestackFilePicker from 'components/GUIKit/FilePicker'; import Dropdown from 'components/GUIKit/Dropdown'; @@ -238,11 +239,11 @@ export default function GigApply(props) { readonly /> onFormInputChange('tcProfileLink', val)} errorMsg={formErrors.tcProfileLink} - value={formData.handle ? `https://topcoder.com/members/${formData.handle}` : null} + value={formData.handle ? getMemberProfileUrl(formData.handle) : null} readonly /> diff --git a/src/shared/components/HallOfFamePage/Champions/Track/index.jsx b/src/shared/components/HallOfFamePage/Champions/Track/index.jsx index d73836c18..3da691d2c 100644 --- a/src/shared/components/HallOfFamePage/Champions/Track/index.jsx +++ b/src/shared/components/HallOfFamePage/Champions/Track/index.jsx @@ -5,6 +5,7 @@ import React from 'react'; import PT from 'prop-types'; import _ from 'lodash'; import { themr } from 'react-css-super-themr'; +import { getMemberProfileUrl } from 'utils/url'; import defaultStyles from './styles.scss'; @@ -22,7 +23,7 @@ const Track = ({ data.fields.members.map(member => (
    {member.fields.handle} diff --git a/src/shared/components/HallOfFamePage/Finalists/Track/index.jsx b/src/shared/components/HallOfFamePage/Finalists/Track/index.jsx index a218eb622..1f88dc3bf 100644 --- a/src/shared/components/HallOfFamePage/Finalists/Track/index.jsx +++ b/src/shared/components/HallOfFamePage/Finalists/Track/index.jsx @@ -6,6 +6,7 @@ import React from 'react'; import PT from 'prop-types'; import _ from 'lodash'; import { themr } from 'react-css-super-themr'; +import { getMemberProfileUrl } from 'utils/url'; import defaultStyles from './styles.scss'; @@ -20,7 +21,7 @@ const Track = ({ Winner Portrait
    {data.fields.champion.fields.handle} @@ -34,7 +35,7 @@ const Track = ({ data.fields.members.map(member => (
    {member.fields.handle} diff --git a/src/shared/components/HallOfFamePage/TripWinners/Role/index.jsx b/src/shared/components/HallOfFamePage/TripWinners/Role/index.jsx index eff66c168..eac15097e 100644 --- a/src/shared/components/HallOfFamePage/TripWinners/Role/index.jsx +++ b/src/shared/components/HallOfFamePage/TripWinners/Role/index.jsx @@ -5,6 +5,7 @@ import React from 'react'; import PT from 'prop-types'; import _ from 'lodash'; import { themr } from 'react-css-super-themr'; +import { getMemberProfileUrl } from 'utils/url'; import defaultStyles from './styles.scss'; @@ -17,7 +18,7 @@ const Role = ({ data, theme }) => ( data.members.map(member => (
    {member.fields.handle} diff --git a/src/shared/components/Header/index.jsx b/src/shared/components/Header/index.jsx index c8e2e2d6f..5960e45e1 100644 --- a/src/shared/components/Header/index.jsx +++ b/src/shared/components/Header/index.jsx @@ -1,8 +1,12 @@ import _ from 'lodash'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import PT from 'prop-types'; import { config } from 'topcoder-react-utils'; import Logo from 'assets/images/TC-logo-new.svg'; +import { + getMenuWithDirectProfileLink, + updateLegacyProfileLinks, +} from 'utils/url'; import { tracking } from '../../actions'; import './style.scss'; @@ -23,6 +27,7 @@ const Header = ({ profile, auth, notifications, loadNotifications, markNotificationAsRead, markAllNotificationAsRead, markAllNotificationAsSeen, dismissChallengeNotifications, headerMenu, }) => { + const headerRef = useRef(null); const [activeLevel1Id, setActiveLevel1Id] = useState(); const [path, setPath] = useState(); const [openMore, setOpenMore] = useState(true); @@ -51,10 +56,19 @@ const Header = ({ normalizedProfile = null; } + const menu = getMenuWithDirectProfileLink( + headerMenu || config.HEADER_MENU, + profile && profile.handle, + ); + useEffect(() => { setPath(window.location.pathname + window.location.search); }, []); + useEffect(() => { + updateLegacyProfileLinks(headerRef.current, profile && profile.handle); + }, [profile]); + /* * Load Notifications and Init Google Analytics */ @@ -71,9 +85,9 @@ const Header = ({ if (TopNavRef) { return ( -
    +
    ) : ( @@ -261,7 +262,7 @@ export default function PodiumSpot(props) { ) : ( {competitor['member_profile_basic.handle'] || competitor.handle} @@ -287,7 +288,7 @@ export default function PodiumSpot(props) { ) : ( diff --git a/src/shared/components/Looker/index.jsx b/src/shared/components/Looker/index.jsx index 9aac606e2..4f89b1cff 100644 --- a/src/shared/components/Looker/index.jsx +++ b/src/shared/components/Looker/index.jsx @@ -22,6 +22,7 @@ import _ from 'lodash'; import React, { Component } from 'react'; import { fixStyle } from 'utils/contentful'; import { getRatingColor } from 'utils/tc'; +import { getMemberProfileUrl } from 'utils/url'; import cn from 'classnames'; import { Scrollbars } from 'react-custom-scrollbars'; import './style.scss'; @@ -208,7 +209,7 @@ export default class Looker extends Component { return value ? ( {memberLinks ? ( - + {value} ) : value} diff --git a/src/shared/components/MMatchLeaderboard/index.jsx b/src/shared/components/MMatchLeaderboard/index.jsx index 24c5c112b..9b6037cf9 100644 --- a/src/shared/components/MMatchLeaderboard/index.jsx +++ b/src/shared/components/MMatchLeaderboard/index.jsx @@ -22,6 +22,7 @@ import _ from 'lodash'; import React, { Component } from 'react'; import { fixStyle } from 'utils/contentful'; import { getRatingColor } from 'utils/tc'; +import { getMemberProfileUrl } from 'utils/url'; import cn from 'classnames'; import { Scrollbars } from 'react-custom-scrollbars'; import { config } from 'topcoder-react-utils'; @@ -94,10 +95,10 @@ export default class MMLeaderboard extends Component {
    @@ -107,10 +108,10 @@ export default class MMLeaderboard extends Component {
    @@ -120,7 +121,7 @@ export default class MMLeaderboard extends Component { {_.slice(data, 3, 7).map(member => (
    {member.rank}.  - {member.createdBy} + {member.createdBy} {member.score}
    ))} @@ -131,7 +132,7 @@ export default class MMLeaderboard extends Component { {_.slice(data, 7, 10).map(member => (
    {member.rank}.  - {member.createdBy} + {member.createdBy} {member.score}
    ))} @@ -249,7 +250,7 @@ export default class MMLeaderboard extends Component { } return value ? ( - {memberLinks ? ({value}) : value} + {memberLinks ? ({value}) : value} ) : null; }) diff --git a/src/shared/components/ProfileBadgesPage/index.jsx b/src/shared/components/ProfileBadgesPage/index.jsx index e1015a16d..93b95676b 100644 --- a/src/shared/components/ProfileBadgesPage/index.jsx +++ b/src/shared/components/ProfileBadgesPage/index.jsx @@ -1,11 +1,11 @@ import React, { useState } from 'react'; import PT from 'prop-types'; -import { Link } from 'react-router-dom'; import { get } from 'lodash'; import { Modal } from 'topcoder-react-ui-kit'; import IconClose from 'assets/images/tc-edu/icon-close-big.svg'; import FallBackAwardIcon from 'assets/images/default-award.svg'; import md from 'utils/markdown'; +import { getMemberProfileUrl } from 'utils/url'; import { format } from 'date-fns'; import AwardModal from '../ProfilePage/Awards/AwardModal'; @@ -17,8 +17,8 @@ const ProfileBadges = ({ badges, handleParam }) => { return (
    - { /> Return to Profile - +
    COMMUNITY AWARDS & HONORS
    diff --git a/src/shared/components/ReviewOpportunityDetailsPage/ApplicationsTab/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/ApplicationsTab/index.jsx index c45d2d9cb..bffc20a2f 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/ApplicationsTab/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/ApplicationsTab/index.jsx @@ -5,6 +5,7 @@ import moment from 'moment'; import React from 'react'; import PT from 'prop-types'; import _ from 'lodash'; +import { getMemberProfileUrl } from 'utils/url'; import './styles.scss'; @@ -33,7 +34,7 @@ const ApplicationsTab = ({ applications }) => ( && applications.filter(app => app.status !== 'CANCELLED').map(app => (
    diff --git a/src/shared/components/challenge-detail/Registrants/index.jsx b/src/shared/components/challenge-detail/Registrants/index.jsx index 9b24d8989..eb0baf674 100644 --- a/src/shared/components/challenge-detail/Registrants/index.jsx +++ b/src/shared/components/challenge-detail/Registrants/index.jsx @@ -10,6 +10,7 @@ import _ from 'lodash'; import cn from 'classnames'; import { getRatingLevel } from 'utils/tc'; import { getTrackName } from 'utils/challenge'; +import { getMemberProfileUrl } from 'utils/url'; import sortList from 'utils/challenge-detail/sort'; import DateSortIcon from 'assets/images/icon-date-sort.svg'; @@ -464,7 +465,7 @@ export default class Registrants extends React.Component {
    diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx b/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx index d73d35177..cb7ad5734 100644 --- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx @@ -7,6 +7,7 @@ import React from 'react'; import PT from 'prop-types'; import _ from 'lodash'; import { CHALLENGE_STATUS, getRatingLevel } from 'utils/tc'; +import { getMemberProfileUrl } from 'utils/url'; import { Modal } from 'topcoder-react-ui-kit'; import IconClose from 'assets/images/icon-close-green.svg'; import moment from 'moment'; @@ -123,7 +124,7 @@ export default function SubmissionRow({ const ratingLevelStyle = `col level-${getRatingLevel(rating)}`; const memberHandle = member || ''; const memberDisplay = memberHandle || '-'; - const memberProfileUrl = memberHandle ? `${window.origin}/members/${memberHandle}` : null; + const memberProfileUrl = memberHandle ? getMemberProfileUrl(memberHandle) : null; const memberLinkTarget = `${_.includes(window.origin, 'www') ? '_self' : '_blank'}`; const memberForHistory = memberHandle || memberDisplay; const latestSubmissionId = latestSubmission.submissionId || latestSubmission.id || 'N/A'; diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx index bd2645737..7ef57dd08 100644 --- a/src/shared/components/challenge-detail/Submissions/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/index.jsx @@ -25,6 +25,7 @@ import { shouldShowFinalMmResults as resolveShouldShowFinalMmResults } from 'uti import { getSubmissionId } from 'utils/submissions'; import { compressFiles } from 'utils/files'; import { buildMmSubmissionData } from 'utils/mm-review-summations'; +import { getMemberProfileUrl } from 'utils/url'; import { getChallengeSubmissions as getChallengeSubmissionsService } from 'services/submissions'; import sortList from 'utils/challenge-detail/sort'; @@ -1008,7 +1009,7 @@ class SubmissionsComponent extends React.Component { {`#${s.id}`}
    diff --git a/src/shared/components/challenge-listing/LeaderboardAvatar/index.jsx b/src/shared/components/challenge-listing/LeaderboardAvatar/index.jsx index 92820e3b5..74c3d2348 100644 --- a/src/shared/components/challenge-listing/LeaderboardAvatar/index.jsx +++ b/src/shared/components/challenge-listing/LeaderboardAvatar/index.jsx @@ -5,6 +5,7 @@ import PT from 'prop-types'; import { config, Link } from 'topcoder-react-utils'; import _ from 'lodash'; import { formatOrdinals } from 'utils/challenge-listing/helper'; +import { getMemberProfileUrl } from 'utils/url'; import './style.scss'; /* TODO: Should be functional component! */ @@ -21,7 +22,7 @@ class LeaderboardAvatar extends Component { onClick, plusOne, url, } = this.props; const { member } = this.state; - const targetURL = url || `${window.origin}/members/${member.handle}`; + const targetURL = url || getMemberProfileUrl(member.handle); let { photoURL } = member; if (photoURL) { /* Note: 50px is larger than we really need here (the avatar size is diff --git a/src/shared/components/tc-communities/Header/index.jsx b/src/shared/components/tc-communities/Header/index.jsx index b3dd7915f..1d8222fc0 100644 --- a/src/shared/components/tc-communities/Header/index.jsx +++ b/src/shared/components/tc-communities/Header/index.jsx @@ -19,6 +19,7 @@ import { isomorphy, } from 'topcoder-react-utils'; import { getRatingColor } from 'utils/tc'; +import { getMemberProfileUrl } from 'utils/url'; import Dropdown from 'components/tc-communities/Dropdown'; import { themr } from 'react-css-super-themr'; import Menu from 'components/Contentful/Menu'; @@ -74,7 +75,7 @@ function Header(props) { let userSubMenu; if (profile) { - let profileLink = `${meta ? _.replace(BASE_URL, 'www', meta.subdomains[0]) : BASE_URL}/members/${normalizedProfile.handle}`; + let profileLink = getMemberProfileUrl(normalizedProfile.handle); let paymentsLink = `${config.URL.COMMUNITY}/PactsMemberServlet?module=PaymentHistory&full_list=false`; // Handle Wipro specific links (PS-257) diff --git a/src/shared/components/tc-communities/communities/blockchain/BsicHackathon/index.jsx b/src/shared/components/tc-communities/communities/blockchain/BsicHackathon/index.jsx index 4b5cd19cc..a121e1b6d 100644 --- a/src/shared/components/tc-communities/communities/blockchain/BsicHackathon/index.jsx +++ b/src/shared/components/tc-communities/communities/blockchain/BsicHackathon/index.jsx @@ -1,6 +1,7 @@ import JoinCommunity from 'containers/tc-communities/JoinCommunity'; import React from 'react'; import { Link } from 'topcoder-react-utils'; +import { getMemberProfileUrl } from 'utils/url'; import HeadBannerImage from 'assets/images/communities/blockchain/bsic-hackathon/head-banner.jpg'; @@ -187,42 +188,42 @@ export default function BsicHackathon() { Trevor Campbell ( tdcampbell ), Todd Schultz ( tschultz1216 ), Sam Blumenthal ( sam.blumenthal ), Gurmeet Budhraja ( khojsolutions ), Madhu Machavarapu ( 94madhu94 ), Urja Pawar ( urjapawar @@ -253,21 +254,21 @@ export default function BsicHackathon() { Elisa Pasquali ( epg ), Ana Zamfir ( ancaz ), Ioana Stanescu ( osninja @@ -318,14 +319,14 @@ export default function BsicHackathon() { Val Denay Mack ( valem21 ), Gael Gundin ( wapinpana @@ -356,42 +357,42 @@ export default function BsicHackathon() { Rahul Bansal ( rahulb ), Siddharth Swarnkar ( siddharthsoni ), Sundari Narayan Swami ( sswami ), Bhavyaa Rastogi ( krusherz ), Carlos Rojas Noveron ( CarlosNoveron ), Dr. Hugh Gosnell ( hugh_g @@ -441,42 +442,42 @@ export default function BsicHackathon() { Sajida Zouarhi ( SajZ ), Noah Basri ( Noah-Basri ), Maroussia Arnault ( Marou_Kid ), Amelia Lintern-Smith ( amelials ), Clément Massonnaud ( CleMass ), Mathieu Vincens ( Mathvincens @@ -507,35 +508,35 @@ export default function BsicHackathon() { Alexander S. Blum ( ablumbc ), Meredith Finkelstein ( msrobot0 ), Seth Weiner ( sethweiner ), Chris Jaroszewski ( Cjaroszewski ) , Peter Lyons ( Anemas971 @@ -590,21 +591,21 @@ export default function BsicHackathon() { Philipp Beer ( Depose ), Lewis Daly ( lwilld ), Billy Garrison ( BillyGarrison @@ -632,42 +633,42 @@ export default function BsicHackathon() { Ylli Vllasolli ( Team-Thor ), Charlotte Stephens ( Charlotte-Thor ), Bayo Akins ( Bayoakins ), Erica Sundberg ( e5r34t ), Stephen Jackson ( stevejaxon ), Ife Nkechukwu ( ife-thor diff --git a/src/shared/components/tc-communities/communities/iot/AssetDetail/index.jsx b/src/shared/components/tc-communities/communities/iot/AssetDetail/index.jsx index 35f9fc5a5..37fb2903c 100644 --- a/src/shared/components/tc-communities/communities/iot/AssetDetail/index.jsx +++ b/src/shared/components/tc-communities/communities/iot/AssetDetail/index.jsx @@ -6,6 +6,7 @@ import Error404 from 'components/Error404'; import React from 'react'; import PT from 'prop-types'; +import { getMemberProfileUrl } from 'utils/url'; import { PrimaryButton } from 'topcoder-react-ui-kit'; @@ -21,6 +22,10 @@ export default function AssetDetail({ }) { const assets = assetsData.filter(item => item.id === assetId); const asset = assets[0]; + const abstract = asset && asset.abstract.replace( + /https:\/\/www\.topcoder\.com\/members\/([^/'"]+)\/?/g, + (_legacyUrl, handle) => getMemberProfileUrl(handle), + ); return ( typeof asset === 'undefined' ? @@ -37,7 +42,7 @@ export default function AssetDetail({

    Asset Details

    -
    +

    { asset.githubUrl } @@ -59,7 +64,7 @@ export default function AssetDetail({ Topcoder Winner - + { asset.author.name }

    diff --git a/src/shared/components/tco/scoreboard/ScoreboardTable/index.jsx b/src/shared/components/tco/scoreboard/ScoreboardTable/index.jsx index 9a97b310b..7d8115bac 100644 --- a/src/shared/components/tco/scoreboard/ScoreboardTable/index.jsx +++ b/src/shared/components/tco/scoreboard/ScoreboardTable/index.jsx @@ -21,6 +21,7 @@ import React from 'react'; import PT from 'prop-types'; import _ from 'lodash'; +import { getMemberProfileUrl } from 'utils/url'; import codeFields from 'shared/fields/submissionCodeFields.json'; import designFields from 'shared/fields/submissionDesignFields.json'; @@ -53,7 +54,7 @@ export default function ScoreboardTable(props) { {submission.handle} diff --git a/src/shared/containers/Contentful/MenuLoader/index.jsx b/src/shared/containers/Contentful/MenuLoader/index.jsx index ed695ecb7..f908f34b9 100644 --- a/src/shared/containers/Contentful/MenuLoader/index.jsx +++ b/src/shared/containers/Contentful/MenuLoader/index.jsx @@ -12,6 +12,10 @@ import { connect } from 'react-redux'; import Logo from 'assets/images/tc-logo.svg'; import { isomorphy, config } from 'topcoder-react-utils'; import actions from 'actions/contentful'; +import { + getMenuWithDirectProfileLink, + updateLegacyProfileLinks, +} from 'utils/url'; class MenuLoaderContainer extends React.Component { @@ -51,6 +55,18 @@ class MenuLoaderContainer extends React.Component { baseUrl, }); } + this.refreshProfileLinks(); + } + + componentDidUpdate() { + this.refreshProfileLinks(); + } + + refreshProfileLinks() { + const { auth } = this.props; + if (isomorphy.isClientSide()) { + updateLegacyProfileLinks(document, _.get(auth, 'profile.handle')); + } } handleChangeLevel1Id(menuId) { @@ -87,12 +103,16 @@ class MenuLoaderContainer extends React.Component { } else { normalizedProfile = null; } + const menuWithDirectProfileLink = getMenuWithDirectProfileLink( + menu, + _.get(auth, 'profile.handle'), + ); if (fields.theme.indexOf('TCO22') !== -1) { // eslint-disable-next-line global-require const { TopNav: TopNavTCO, LoginNav: LoginNavTCO } = require('navigation-component-tco'); return ( - + {auth.profile.handle} diff --git a/src/shared/containers/timeline-wall/pending-approvals/approval-item/index.jsx b/src/shared/containers/timeline-wall/pending-approvals/approval-item/index.jsx index ae613a439..eee39c2d6 100644 --- a/src/shared/containers/timeline-wall/pending-approvals/approval-item/index.jsx +++ b/src/shared/containers/timeline-wall/pending-approvals/approval-item/index.jsx @@ -11,7 +11,7 @@ import ModalDeleteConfirmation from '../../modal-delete-confirmation'; import ModalPhotoViewer from '../../modal-photo-viewer'; import './styles.scss'; -import { DEFAULT_AVATAR_URL } from '../../../../utils/url'; +import { DEFAULT_AVATAR_URL, getMemberProfileUrl } from '../../../../utils/url'; function ApprovalItem({ className, event, removeEvent, userAvatars, deleteEvent, onApproveEvent, @@ -36,7 +36,7 @@ function ApprovalItem({
    avatar {eventItem.createdBy} diff --git a/src/shared/utils/url.js b/src/shared/utils/url.js index 117cb7794..eddd1db1e 100644 --- a/src/shared/utils/url.js +++ b/src/shared/utils/url.js @@ -146,6 +146,61 @@ export function removeTrailingSlash(url) { : url; } +/** + * Get the direct URL for a member profile. + * + * @param {String} handle Topcoder member handle. + * @return {String} URL of the member's profile in the configured environment. + */ +export function getMemberProfileUrl(handle) { + return `${config.MEMBER_PROFILE_REDIRECT_URL}/${handle}`; +} + +/** + * Point the profile entry in a navigation menu directly to the profile app. + * The id is changed because navigation-component replaces "myprofile" URLs + * with its legacy /members/:handle route. + * + * @param {Array} menu Navigation menu configuration. + * @param {String} handle Topcoder member handle. + * @return {Array} Navigation menu with a direct member profile link. + */ +export function getMenuWithDirectProfileLink(menu, handle) { + if (!handle) return menu; + + return menu.map(item => (item.secondaryMenu ? { + ...item, + secondaryMenu: item.secondaryMenu.map(secondaryItem => ( + ['myprofile', 'profile'].includes(secondaryItem.id) + ? { + ...secondaryItem, + id: 'profile', + href: getMemberProfileUrl(handle), + } + : secondaryItem + )), + } : item)); +} + +/** + * Update exact legacy profile anchors rendered by navigation-component. + * + * @param {Element|Document} root Root node containing navigation links. + * @param {String} handle Topcoder member handle. + * @return {void} + */ +export function updateLegacyProfileLinks(root, handle) { + if (!root || !handle) return; + + const legacyProfilePath = `/members/${handle}`; + _.forEach(root.querySelectorAll('a[href]'), (link) => { + const href = link.getAttribute('href'); + if (href && href.replace(/\/$/, '') === legacyProfilePath) { + link.setAttribute('href', getMemberProfileUrl(handle)); + } + }); +} + /** * Get Payment page url from header menu. *