diff --git a/package.json b/package.json index 6e48c45c2..605cbb2b0 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@datadog/browser-logs": "^4.21.2", "@heroicons/react": "^1.0.6", "@popperjs/core": "^2.11.8", + "@segment/analytics-next": "^1.53.3", "@sprig-technologies/sprig-browser": "^2.20.1", "@storybook/addon-actions": "^7.0.5", "@storybook/react": "^7.0.5", @@ -163,7 +164,6 @@ "@types/redux-logger": "^3.0.9", "@types/redux-promise": "^0.5.29", "@types/sanitize-html": "^2.6.2", - "@types/segment-analytics": "^0.0.34", "@types/systemjs": "^6.1.1", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.30.6", diff --git a/src/apps/platform/src/PlatformApp.tsx b/src/apps/platform/src/PlatformApp.tsx index dd89390fb..ae62f3d5a 100644 --- a/src/apps/platform/src/PlatformApp.tsx +++ b/src/apps/platform/src/PlatformApp.tsx @@ -1,13 +1,19 @@ import { FC } from 'react' import { toast, ToastContainer } from 'react-toastify' +import { AnalyticsBrowser } from '@segment/analytics-next' import { useViewportUnitsFix } from '~/libs/shared' +import { EnvironmentConfig } from '~/config' import { AppFooter } from './components/app-footer' import { AppHeader } from './components/app-header' import { Providers } from './providers' import { PlatformRouter } from './platform-router' +if (EnvironmentConfig.SEGMENT_ANALYTICS_KEY) { + window.tcSegment = AnalyticsBrowser.load({ writeKey: EnvironmentConfig.SEGMENT_ANALYTICS_KEY }) as any +} + const PlatformApp: FC<{}> = () => { useViewportUnitsFix() diff --git a/src/apps/platform/src/components/app-header/AppHeader.tsx b/src/apps/platform/src/components/app-header/AppHeader.tsx index df44f1d9e..c16722fb4 100644 --- a/src/apps/platform/src/components/app-header/AppHeader.tsx +++ b/src/apps/platform/src/components/app-header/AppHeader.tsx @@ -17,13 +17,13 @@ import classNames from 'classnames' import { EnvironmentConfig, PageSubheaderPortalId } from '~/config' import { authUrlLogin, - authUrlLogout, authUrlSignup, profileContext, ProfileContextData, routerContext, RouterContextData, } from '~/libs/core' +import { ConfigContextValue, useConfigContext } from '~/libs/shared' import UniNavSnippet from './universal-nav-snippet' @@ -39,6 +39,7 @@ const AppHeader: FC<{}> = () => { const { activeToolName, activeToolRoute }: RouterContextData = useContext(routerContext) const { profile, initialized: profileReady }: ProfileContextData = useContext(profileContext) + const { logoutUrl }: ConfigContextValue = useConfigContext() const [ready, setReady]: [boolean, Dispatch>] = useState(false) const headerInit: MutableRefObject = useRef(false) const navElementId: string = PageSubheaderPortalId @@ -95,7 +96,7 @@ const AppHeader: FC<{}> = () => { }, onReady() { setReady(true) }, signIn() { window.location.href = authUrlLogin() }, - signOut() { window.location.href = authUrlLogout }, + signOut() { window.location.href = logoutUrl }, signUp() { window.location.href = authUrlSignup() }, toolName: activeToolName, toolRoot: activeToolRoute, @@ -110,6 +111,7 @@ const AppHeader: FC<{}> = () => { navigationHandler, userInfo, profileReady, + logoutUrl, ]) // update uni-nav's tool details @@ -141,12 +143,14 @@ const AppHeader: FC<{}> = () => { navElementId, { ...userInfo, + signOut() { window.location.href = logoutUrl }, }, ) }, [ profileReady, userInfo, navElementId, + logoutUrl, ]) return ( diff --git a/src/apps/platform/src/providers/Providers.tsx b/src/apps/platform/src/providers/Providers.tsx index 1795e393b..b16709bb2 100644 --- a/src/apps/platform/src/providers/Providers.tsx +++ b/src/apps/platform/src/providers/Providers.tsx @@ -1,6 +1,7 @@ import { FC, ReactNode } from 'react' -import { ProfileProvider } from '~/libs/core' +import { authUrlLogout, ProfileProvider } from '~/libs/core' +import { ConfigContextProvider } from '~/libs/shared' import { PlatformRouterProvider } from './platform-router.provider' @@ -9,11 +10,13 @@ interface ProvidersProps { } const Providers: FC = props => ( - - - {props.children} - - + + + + {props.children} + + + ) export default Providers diff --git a/src/apps/profiles/src/ProfilesApp.tsx b/src/apps/profiles/src/ProfilesApp.tsx index 1761eb1f0..c31acfd42 100644 --- a/src/apps/profiles/src/ProfilesApp.tsx +++ b/src/apps/profiles/src/ProfilesApp.tsx @@ -1,14 +1,19 @@ -import { FC, useContext } from 'react' +import { FC, useContext, useEffect } from 'react' import { Outlet, Routes } from 'react-router-dom' -import { routerContext, RouterContextData } from '~/libs/core' -import { SharedSwrConfig } from '~/libs/shared' +import { authUrlLogin, authUrlLogoutFn, routerContext, RouterContextData } from '~/libs/core' +import { ConfigContextValue, SharedSwrConfig, useConfigContext } from '~/libs/shared' -import { toolTitle } from './profiles.routes' +import { absoluteRootRoute, toolTitle } from './profiles.routes' const ProfilesApp: FC<{}> = () => { + const { setLogoutUrl }: ConfigContextValue = useConfigContext() const { getChildRoutes }: RouterContextData = useContext(routerContext) + useEffect(() => { + setLogoutUrl(authUrlLogoutFn(authUrlLogin(absoluteRootRoute))) + }, [setLogoutUrl]) + return ( diff --git a/src/apps/profiles/src/lib/helpers.ts b/src/apps/profiles/src/lib/helpers.ts index 6f644ffcd..64ebf07b9 100644 --- a/src/apps/profiles/src/lib/helpers.ts +++ b/src/apps/profiles/src/lib/helpers.ts @@ -80,11 +80,23 @@ export function subTrackLabelToHumanName(label: string): string { } export function isValidURL(urlToValidate: string): boolean { + const pattern = new RegExp( + '^(https?:\\/\\/)?' // protocol + + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' // domain name + + '((\\d{1,3}\\.){3}\\d{1,3}))' // OR IP (v4) address + + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' // port and path + + '(\\?[;&a-z\\d%_.~+=-]*)?' // query string + + '(\\#[-a-z\\d_]*)?$', // fragment locator + 'i', + ) + let url: URL try { url = new URL(urlToValidate) } catch (e) { - return false + // try to validate with regex + // as sometimes new URL is wonky with some urls + return pattern.test(urlToValidate) } if (!url.protocol || !url.hostname) { diff --git a/src/apps/profiles/src/member-badges/MemberBadgesPage.module.scss b/src/apps/profiles/src/member-badges/MemberBadgesPage.module.scss index eecfc1193..88b1d4d1d 100644 --- a/src/apps/profiles/src/member-badges/MemberBadgesPage.module.scss +++ b/src/apps/profiles/src/member-badges/MemberBadgesPage.module.scss @@ -2,16 +2,7 @@ .container { .backLink { - text-transform: uppercase; - color: $turq-160; - font-weight: $font-weight-bold; margin: $sp-6 0; - display: flex; - align-items: center; - - svg { - margin-right: $sp-2; - } } .badgesWrapper { @@ -67,4 +58,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/apps/profiles/src/member-badges/MemberBadgesPage.tsx b/src/apps/profiles/src/member-badges/MemberBadgesPage.tsx index 136ce00c0..ae271f591 100644 --- a/src/apps/profiles/src/member-badges/MemberBadgesPage.tsx +++ b/src/apps/profiles/src/member-badges/MemberBadgesPage.tsx @@ -1,9 +1,9 @@ import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react' -import { Link, Params, useParams } from 'react-router-dom' +import { Params, useNavigate, useParams } from 'react-router-dom' import { bind } from 'lodash' import { profileGetPublicAsync, useMemberBadges, UserBadge, UserBadgesResponse, UserProfile } from '~/libs/core' -import { ContentLayout, LoadingSpinner } from '~/libs/ui' +import { Button, ContentLayout, IconSolid, LoadingSpinner } from '~/libs/ui' import { MemberBadgeModal } from '../components' @@ -11,6 +11,7 @@ import styles from './MemberBadgesPage.module.scss' const MemberBadgesPage: FC<{}> = () => { const routeParams: Params = useParams() + const navigate = useNavigate() const [profile, setProfile]: [ UserProfile | undefined, @@ -32,6 +33,10 @@ const MemberBadgesPage: FC<{}> = () => { setSelectedBadge(badge) }, []) + const handleBackBtn = useCallback(() => { + navigate(-1) + }, [navigate]) + useEffect(() => { if (routeParams.memberHandle) { profileGetPublicAsync(routeParams.memberHandle) @@ -55,24 +60,17 @@ const MemberBadgesPage: FC<{}> = () => { - - + - Return to Profile - + Return to profile + +

COMMUNITY AWARDS & HONORS

diff --git a/src/apps/profiles/src/member-profile/MemberProfilePage.tsx b/src/apps/profiles/src/member-profile/MemberProfilePage.tsx index eebf6e893..206207faf 100644 --- a/src/apps/profiles/src/member-profile/MemberProfilePage.tsx +++ b/src/apps/profiles/src/member-profile/MemberProfilePage.tsx @@ -1,7 +1,9 @@ import { Dispatch, FC, SetStateAction, useCallback, useContext, useEffect, useState } from 'react' import { Params, useNavigate, useParams } from 'react-router-dom' +import { AxiosError } from 'axios' import { profileContext, ProfileContextData, profileGetPublicAsync, UserProfile } from '~/libs/core' +import { TALENT_SEARCH_PATHS } from '~/apps/talent-search' import { LoadingSpinner } from '~/libs/ui' import { notifyUniNavi, triggerSprigSurvey } from '../lib' @@ -34,7 +36,11 @@ const MemberProfilePage: FC<{}> = () => { setProfile({ ...userProfile } as UserProfile) setProfileReady(true) }) - // TODO: NOT FOUND PAGE redirect/dispaly + .catch((e: AxiosError) => { + if (e.code === AxiosError.ERR_BAD_REQUEST && e.response?.status === 404) { + window.location.href = `${TALENT_SEARCH_PATHS.absoluteUrl}?memberNotFound` + } + }) } }, [routeParams.memberHandle]) diff --git a/src/apps/profiles/src/member-profile/about-me/AboutMe.tsx b/src/apps/profiles/src/member-profile/about-me/AboutMe.tsx index 8934d50ed..69be73a23 100644 --- a/src/apps/profiles/src/member-profile/about-me/AboutMe.tsx +++ b/src/apps/profiles/src/member-profile/about-me/AboutMe.tsx @@ -3,7 +3,7 @@ import { useSearchParams } from 'react-router-dom' import { KeyedMutator } from 'swr' import classNames from 'classnames' -import { useMemberTraits, UserProfile, UserTraitIds, UserTraits } from '~/libs/core' +import { useMemberTraits, UserProfile, UserTrait, UserTraitIds, UserTraits } from '~/libs/core' import { AddButton, EditMemberPropertyBtn, EmptySection } from '../../components' import { EDIT_MODE_QUERY_PARAM, profileEditModes } from '../../config' @@ -38,6 +38,11 @@ const AboutMe: FC = (props: AboutMeProps) => { props.profile && !props.profile.description ), [props.profile]) + const namesAndHandleAppearanceData: UserTrait | undefined + = useMemo(() => memberPersonalizationTraits?.[0]?.traits?.data?.find( + (trait: UserTrait) => trait.namesAndHandleAppearance, + ), [memberPersonalizationTraits]) + useEffect(() => { if (props.authProfile && editMode === profileEditModes.aboutMe) { setIsEditMode(true) @@ -70,7 +75,11 @@ const AboutMe: FC = (props: AboutMeProps) => { {' '} I'm {' '} - {props.profile?.firstName || props.profile?.handle} + { + namesAndHandleAppearanceData?.namesAndHandleAppearance === 'handleOnly' + ? props.profile?.handle + : props.profile?.firstName + }

diff --git a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkForm/LinkForm.tsx b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkForm/LinkForm.tsx index 6158c708d..795d24344 100644 --- a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkForm/LinkForm.tsx +++ b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkForm/LinkForm.tsx @@ -52,10 +52,22 @@ const LinkForm: FC = props => { return } - props.onSave({ - name: selectedLinkType, - url: trim(selectedLinkURL) || '', - }) + let absoluteURL = trim(selectedLinkURL) || '' + + if (absoluteURL.indexOf('://') > 0 || absoluteURL.indexOf('//') === 0) { + + props.onSave({ + name: selectedLinkType, + url: absoluteURL, + }) + } else { + absoluteURL = `https://${absoluteURL}` + + props.onSave({ + name: selectedLinkType, + url: absoluteURL, + }) + } } function handleDiscardClick(): void { diff --git a/src/apps/profiles/src/member-profile/local-info/ModifyLocationModal/ModifyLocationModal.tsx b/src/apps/profiles/src/member-profile/local-info/ModifyLocationModal/ModifyLocationModal.tsx index 8ee76d0cd..646181fcd 100644 --- a/src/apps/profiles/src/member-profile/local-info/ModifyLocationModal/ModifyLocationModal.tsx +++ b/src/apps/profiles/src/member-profile/local-info/ModifyLocationModal/ModifyLocationModal.tsx @@ -1,5 +1,5 @@ import { Dispatch, FC, SetStateAction, useMemo, useState } from 'react' -import { bind, trim } from 'lodash' +import { bind, omit, trim } from 'lodash' import { toast } from 'react-toastify' import { BaseModal, Button, InputSelect, InputText } from '~/libs/ui' @@ -18,6 +18,10 @@ interface ModifyLocationModalProps { profile: UserProfile } +const OMIT_ADDRESS_KEYS_ON_UPDATE = [ + 'createdAt', 'createdBy', 'updatedAt', 'updatedBy', +] + const ModifyLocationModal: FC = (props: ModifyLocationModalProps) => { const countryLookup: CountryLookup[] | undefined = useCountryLookup() @@ -59,7 +63,7 @@ const ModifyLocationModal: FC = (props: ModifyLocation props.profile.handle, { addresses: [{ - ...props.profile.addresses ? props.profile.addresses[0] : {}, + ...props.profile.addresses ? omit(props.profile.addresses[0], OMIT_ADDRESS_KEYS_ON_UPDATE) : {}, city: trim(formValues.city), }], competitionCountryCode: formValues.country, diff --git a/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx b/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx index 24af66654..290cd1cd8 100644 --- a/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx +++ b/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx @@ -76,7 +76,9 @@ const ProfilePageLayout: FC = (props: ProfilePageLayoutP refreshProfile={props.refreshProfile} /> - + {props.profile.userId === props.authProfile?.userId && ( + + )}
{props.authProfile?.handle === props.profile.handle && ( diff --git a/src/apps/profiles/src/member-profile/profile-header/HiringFormModal/HiringFormModal.module.scss b/src/apps/profiles/src/member-profile/profile-header/HiringFormModal/HiringFormModal.module.scss new file mode 100644 index 000000000..2053a2e51 --- /dev/null +++ b/src/apps/profiles/src/member-profile/profile-header/HiringFormModal/HiringFormModal.module.scss @@ -0,0 +1,14 @@ +@import "@libs/ui/styles/includes"; + +.successSubmit { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + + gap: $sp-2; + + > svg { + color: $turq-160; + } +} diff --git a/src/apps/profiles/src/member-profile/profile-header/HiringFormModal/HiringFormModal.tsx b/src/apps/profiles/src/member-profile/profile-header/HiringFormModal/HiringFormModal.tsx new file mode 100644 index 000000000..89f6a50e3 --- /dev/null +++ b/src/apps/profiles/src/member-profile/profile-header/HiringFormModal/HiringFormModal.tsx @@ -0,0 +1,159 @@ +import { FC, useCallback, useEffect, useState } from 'react' +import { isEmpty } from 'lodash' + +import { UserProfile } from '~/libs/core' +import { + BaseModal, + Button, + Form, + formGetInputModel, + FormInputModel, + FormValue, + IconOutline, + LoadingSpinner, +} from '~/libs/ui' + +import { hiringFormDef, HiringFormField } from './hiring-form.config' +import styles from './HiringFormModal.module.scss' + +interface HiringFormModalProps { + onClose: () => void + authProfile: UserProfile | undefined + profile: UserProfile + searchedSkills: string[] +} + +function populateIframeForm(profile: UserProfile, formValues: any | undefined, searchedSkills: string[]): string { + const formUrl = `https://go.topcoder.com/talent-search-intake?handle=${profile.handle}` + + if (formValues) { + return `${formUrl} +&first_name=${formValues.firstName} +&last_name=${formValues.lastName} +&email=${formValues.email} +&phone_number=${formValues.phone} +&company=${formValues.company} +&searched_skills=${searchedSkills.join(',')}` + } + + return formUrl +} + +function renderSuccessModal(onClose: HiringFormModalProps['onClose']): JSX.Element { + return ( + } + > +
+ +
+ Your request has been submitted +
+
+ We're excited to learn more about you and the work you want done. + A Topcoder Solutions Expert will reach out to you via email or phone very shortly. +
+ +
+
+ ) +} + +const HiringFormModal: FC = (props: HiringFormModalProps) => { + const [loading, setLoading] = useState(false) + const [submitted, setSubmitted] = useState(false) + const [formValues, setFormValues] = useState({}) + + const generateRequest = useCallback((inputs: ReadonlyArray): FormValue => { + const firstName: string + = formGetInputModel(inputs, HiringFormField.first).value as string + const lastName: string + = formGetInputModel(inputs, HiringFormField.last).value as string + const email: string + = formGetInputModel(inputs, HiringFormField.email).value as string + const phone: string + = formGetInputModel(inputs, HiringFormField.phone).value as string + const company: string + = formGetInputModel(inputs, HiringFormField.company).value as string + return { + company, + email, + firstName, + lastName, + phone, + } + }, []) + + const saveAsync = useCallback(async (request: FormValue): Promise => { + setLoading(true) + setFormValues(request) + }, []) + + useEffect(() => { + function listenUnbounceSubmit(ev: any): void { + if (ev.origin !== 'https://go.topcoder.com' || ev.data !== 'unbounceUserFormSubmitted') { + return + } + + setSubmitted(true) + setLoading(false) + } + + window.addEventListener('message', listenUnbounceSubmit, false) + + return () => window.removeEventListener('message', listenUnbounceSubmit, false) + }, []) + + function renderIframe(): JSX.Element { + return ( +