diff --git a/src/apps/profiles/src/member-profile/links/MemberLinks.tsx b/src/apps/profiles/src/member-profile/links/MemberLinks.tsx index e7b1f4a75..4631b9678 100644 --- a/src/apps/profiles/src/member-profile/links/MemberLinks.tsx +++ b/src/apps/profiles/src/member-profile/links/MemberLinks.tsx @@ -32,6 +32,8 @@ export function renderLinkIcon(linkName: string): JSX.Element { return case 'Twitter': return + case 'X / Twitter': + return case 'LinkedIn': return case 'Instagram': @@ -94,7 +96,7 @@ const MemberLinks: FC = (props: MemberLinksProps) => { href={trait.url} target='_blank' rel='noreferrer' - key={`link-${trait.name}`} + key={`link-${trait.name}-${trait.url}`} > {renderLinkIcon(trait.name)} diff --git a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkEntry/LinkEntry.module.scss b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkEntry/LinkEntry.module.scss deleted file mode 100644 index fc2123ab6..000000000 --- a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkEntry/LinkEntry.module.scss +++ /dev/null @@ -1,66 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.linkItemWrap { - display: flex; - align-items: center; - justify-content: flex-start; - margin-bottom: $sp-2; - - > svg { - width: 24px; - height: 24px; - - path:not([stroke-linecap]) { - fill: $black-100; - } - - } -} - -.linkItem { - border-radius: 4px; - padding: $sp-2 $sp-4; - border: 1px solid $black-40; - flex: 1; - display: flex; - align-items: center; - justify-content: space-between; - margin-left: $sp-4; - - button { - padding-right: 0; - - &.button { - margin-left: $sp-2; - padding-left: $sp-2; - padding-right: $sp-2; - } - - svg { - @include icon-xxl; - } - } -} - -.linkLabelWrap { - display: flex; - flex-direction: column; - align-items: flex-start; - flex: 1 1; - - small { - font-size: 11px; - line-height: 11px; - font-weight: $font-weight-medium; - color: $turq-160; - margin-bottom: $sp-1; - } - - p { - word-break: break-all; - } -} - -.btnsWrap { - flex: 0 0 auto; -} diff --git a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkEntry/LinkEntry.tsx b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkEntry/LinkEntry.tsx deleted file mode 100644 index 5eac162f8..000000000 --- a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkEntry/LinkEntry.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { FC, useState } from 'react' - -import { Button, IconOutline } from '~/libs/ui' -import { UserTrait } from '~/libs/core' - -import { renderLinkIcon } from '../../MemberLinks' -import { LinkForm, UserLink } from '../LinkForm' - -import styles from './LinkEntry.module.scss' - -interface LinkEntryProps { - index: number - link: UserTrait - onSave: (link: UserTrait, index: number) => Promise - onRemove: (link: UserTrait) => void -} - -const LinkEntry: FC = props => { - const [isEditing, setIsEditing] = useState(!(props.link.name || props.link.url)) - const isNew = !props.link.name && !props.link.url - - function handleReomveClick(): void { - props.onRemove(props.link) - } - - async function handleOnSave(link: UserLink): Promise { - if (!await props.onSave(link, props.index)) { - return - } - - setIsEditing(false) - } - - function toggleIsEditing(): void { - setIsEditing(editMode => !editMode) - } - - return !isEditing ? ( -
- {renderLinkIcon(props.link.name)} -
-
- {props.link.name} -

{props.link.url}

-
-
-
-
-
- ) : ( - - ) -} - -export default LinkEntry diff --git a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkEntry/index.ts b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkEntry/index.ts deleted file mode 100644 index 07c1b1292..000000000 --- a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkEntry/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as LinkEntry } from './LinkEntry' diff --git a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkForm/LinkForm.module.scss b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkForm/LinkForm.module.scss index 79809bc16..59c4e8a7a 100644 --- a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkForm/LinkForm.module.scss +++ b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/LinkForm/LinkForm.module.scss @@ -7,20 +7,29 @@ .formWrap { display: flex; flex-direction: column; - margin-top: $sp-2; - @include ltelg { - :global(.input-wrapper) { - margin-bottom: $sp-2; - } + :global(input) { + margin-top: $sp-1; + } + + :global(.input-el) { + margin-bottom: 0; } + } .form { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; + > svg { + height: 24px; + width: 24px; + margin-top: 14px; + margin-right: $sp-4; + } + &>div:nth-child(1) { margin-right: $sp-2; min-width: 150px; @@ -33,16 +42,15 @@ flex: 1; } &>button { - margin-bottom: $sp-45; - svg { @include icon-xxl; } &.button { - margin-left: $sp-2; + margin-left: $sp-4; padding-left: $sp-2; padding-right: $sp-2; + padding-top: 11px; } } } 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 795d24344..f6fea3acb 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 @@ -1,11 +1,21 @@ import { trim } from 'lodash' -import { FC, useEffect, useState } from 'react' +import { + FC, + forwardRef, + ForwardRefExoticComponent, + SVGProps, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react' import classNames from 'classnames' import { Button, IconOutline, InputSelect, InputText } from '~/libs/ui' -import { linkTypes } from '../link-types.config' +import { additionalLinkTypes } from '../link-types.config' import { isValidURL } from '../../../../lib' +import { renderLinkIcon } from '../../MemberLinks' import styles from './LinkForm.module.scss' @@ -15,40 +25,88 @@ export interface UserLink { } interface LinkFormProps { - isNew: boolean link?: UserLink + allowEditType?: boolean + classNames?: string + placeholder?: string onSave: (link: UserLink) => void - onDiscard: () => void + onRemove?: () => void + removeIcon?: FC> + hideRemoveIcon?: boolean + labelUrlField?: string + disabled?: boolean } -const LinkForm: FC = props => { +export type LinkFormHandle = { + validateForm: () => void; + resetForm: () => void; +}; + +const LinkForm: ForwardRefExoticComponent< + LinkFormProps & React.RefAttributes +> = forwardRef((props, ref) => { const [formErrors, setFormErrors] = useState<{ [key: string]: string }>({}) const [selectedLinkType, setSelectedLinkType] = useState() const [selectedLinkURL, setSelectedLinkURL] = useState() + const [shouldValidateForm, setShouldValidateForm] = useState(false) + const canShowTypeError = useRef(false) + const canShowUrlError = useRef(false) + + useEffect(() => { + if (shouldValidateForm) { + handleFormAction() + } + }, [selectedLinkType, selectedLinkURL, shouldValidateForm]) + + useImperativeHandle(ref, () => ({ + resetForm() { + setShouldValidateForm(false) + setFormErrors({}) + canShowTypeError.current = false + canShowUrlError.current = false + }, + validateForm() { + canShowTypeError.current = true + canShowUrlError.current = true + handleFormAction() + }, + })) function handleSelectedLinkTypeChange(event: React.ChangeEvent): void { + canShowTypeError.current = true setSelectedLinkType(event.target.value) + setShouldValidateForm(true) } function handleURLChange(event: React.ChangeEvent): void { + canShowUrlError.current = true setSelectedLinkURL(event.target.value) + setShouldValidateForm(true) } - function handleFormAction(): void { + function getFormError(): boolean { setFormErrors({}) + let isError = false if (!selectedLinkType) { - setFormErrors({ selectedLinkType: 'Please select a link type' }) - return + isError = true + if (canShowTypeError.current) { + setFormErrors({ selectedLinkType: 'Please select a link type' }) + } } - if (!trim(selectedLinkURL)) { - setFormErrors({ url: 'Please enter a URL' }) - return + if (selectedLinkURL && trim(selectedLinkURL) && !isValidURL(selectedLinkURL as string)) { + isError = true + if (canShowUrlError.current) { + setFormErrors({ url: 'Invalid URL' }) + } } - if (!isValidURL(selectedLinkURL as string)) { - setFormErrors({ url: 'Invalid URL' }) + return isError + } + + function handleFormAction(): void { + if (getFormError()) { return } @@ -57,36 +115,19 @@ const LinkForm: FC = props => { if (absoluteURL.indexOf('://') > 0 || absoluteURL.indexOf('//') === 0) { props.onSave({ - name: selectedLinkType, + name: selectedLinkType ?? '', url: absoluteURL, }) } else { - absoluteURL = `https://${absoluteURL}` + absoluteURL = absoluteURL ? `https://${absoluteURL}` : '' props.onSave({ - name: selectedLinkType, + name: selectedLinkType ?? '', url: absoluteURL, }) } } - function handleDiscardClick(): void { - setFormErrors({}) - props.onDiscard() - - if (!props.link) { - return - } - - if (selectedLinkType !== props.link.name) { - setSelectedLinkType(props.link.name) - } - - if (selectedLinkURL !== props.link.url) { - setSelectedLinkURL(props.link.url) - } - } - useEffect(() => { if (!props.link) { return @@ -103,47 +144,48 @@ const LinkForm: FC = props => { }, [props.link?.name, props.link?.url]) return ( -
+
- + {props.allowEditType ? ( + + ) : ( + renderLinkIcon(selectedLinkType || '') + )} -
) -} +}) export default LinkForm diff --git a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/ModifyMemberLinksModal.module.scss b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/ModifyMemberLinksModal.module.scss index c7d7df524..508303ddb 100644 --- a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/ModifyMemberLinksModal.module.scss +++ b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/ModifyMemberLinksModal.module.scss @@ -1,5 +1,10 @@ @import '@libs/ui/styles/includes'; +.container { + display: flex; + flex-direction: column; +} + .memberLinksModalBody { min-height: 300px; } @@ -11,21 +16,13 @@ } .links { - margin-bottom: $sp-4; - padding: $sp-4 0 $sp-2; - - @include ltelg { - display: flex; - flex-direction: column; - } - - &.noLinks { - margin-bottom: 0; - padding: 0; - border: none; - } + display: flex; + flex-direction: column; + gap: $sp-4; + margin-top: $sp-4; } .formCTAs { - margin-top: $sp-2; + margin-top: $sp-4; + margin-bottom: $sp-4; } diff --git a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/ModifyMemberLinksModal.tsx b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/ModifyMemberLinksModal.tsx index 6b0e82792..67ff61a7e 100644 --- a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/ModifyMemberLinksModal.tsx +++ b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/ModifyMemberLinksModal.tsx @@ -1,9 +1,9 @@ -import { reject } from 'lodash' -import { FC, useMemo, useRef, useState } from 'react' +import { cloneDeep, findIndex, isEqual, omit, reject, uniqBy } from 'lodash' +import { FC, useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'react-toastify' import classNames from 'classnames' -import { BaseModal, Button } from '~/libs/ui' +import { BaseModal, Button, IconOutline } from '~/libs/ui' import { updateOrCreateMemberTraitsAsync, UserProfile, @@ -12,7 +12,8 @@ import { UserTraitIds, } from '~/libs/core' -import { LinkEntry } from './LinkEntry' +import { LinkForm, UserLink } from './LinkForm' +import { LinkFormHandle } from './LinkForm/LinkForm' import styles from './ModifyMemberLinksModal.module.scss' interface ModifyMemberLinksModalProps { @@ -27,47 +28,108 @@ const ModifyMemberLinksModal: FC = (props: ModifyMe const inputRef = useRef() const [isSaving, setIsSaving] = useState(false) - const [hasChanges, setHasChanges] = useState(false) const [currentMemberLinks, setCurrentMemberLinks] = useState( - props.memberLinks?.length ? props.memberLinks : [{}], + [], ) + const [defaultLinkedIn, setDefaultLinkedIn] = useState({ + name: 'LinkedIn', + url: '', + }) + const [defaultGitHub, setDefaultGitHub] = useState({ + name: 'GitHub', + url: '', + }) + const [defaultInstagram, setDefaultInstagram] = useState({ + name: 'Instagram', + url: '', + }) + const [defaultLink, setDefaultLink] = useState({ + name: '', + url: '', + }) - const hasNewInput = useMemo(() => ( - !!currentMemberLinks?.find(d => (!d.name && !d.url)) - ), [currentMemberLinks]) + const updatedLinks = useMemo(() => uniqBy( + [ + defaultLinkedIn, + defaultGitHub, + defaultInstagram, + defaultLink, + ...currentMemberLinks, + ].filter( + l => l.name && l.url, + ), + e => `${e.name}-${e.url}`, + ) + .map( + item => omit(item, ['id']), + ), [defaultLinkedIn, defaultGitHub, defaultInstagram, defaultLink, currentMemberLinks]) + const hasChanges = useMemo(() => !isEqual(updatedLinks, props.memberLinks), [updatedLinks]) - function handleAddAdditional(): void { - if (hasNewInput) { - return + const addNewLinkRef = useRef(null) + + useEffect(() => { + const memberLinks = [ + ...cloneDeep(props.memberLinks ?? []), + ] + const firstLinkedInIndex = findIndex(memberLinks, { + name: 'LinkedIn', + }) + if (firstLinkedInIndex >= 0) { + setDefaultLinkedIn(memberLinks.splice(firstLinkedInIndex, 1)[0]) + } + + const firstGitHubIndex = findIndex(memberLinks, { + name: 'GitHub', + }) + if (firstGitHubIndex >= 0) { + setDefaultGitHub(memberLinks.splice(firstGitHubIndex, 1)[0]) + } + + const firstInstagramIndex = findIndex(memberLinks, { + name: 'Instagram', + }) + if (firstInstagramIndex >= 0) { + setDefaultInstagram(memberLinks.splice(firstInstagramIndex, 1)[0]) } - setCurrentMemberLinks(links => (links ?? []).concat({ name: '', url: '' })) + if (memberLinks.length > 0) { + setDefaultLink(memberLinks.splice(0, 1)[0]) + } + + setCurrentMemberLinks(memberLinks.map((item: UserTrait, index: number) => ({ + ...item, + id: `id-${index}-${(new Date()) + .getTime()}`, + }))) + + }, [props.memberLinks]) + + function handleAddAdditional(): void { + setCurrentMemberLinks(links => [...links, { + id: `id-${(new Date()) + .getTime()}`, + ...defaultLink, + }]) + setDefaultLink({ + name: '', + url: '', + }) + addNewLinkRef.current?.resetForm() } - function handleRemoveLink(trait: UserTrait): void { + function handleRemoveLink(index: number): void { + currentMemberLinks.splice(index, 1) setCurrentMemberLinks( - currentMemberLinks?.filter((item: UserTrait) => item.url !== trait.url), + [ + ...currentMemberLinks, + ], ) - setHasChanges(true) } - async function handleSaveLink(link: UserTrait, index: number): Promise { - const existingLinkItemIndex = currentMemberLinks?.findIndex((item: UserTrait) => ( - item.url?.toLowerCase() === link?.url?.toLowerCase() - )) ?? -1 - const isDuplicateLink = existingLinkItemIndex > -1 && existingLinkItemIndex !== index - - if (isDuplicateLink) { - toast.info('Link already exists', { position: toast.POSITION.BOTTOM_RIGHT }) - return undefined - } - + function handleSaveLink(link: UserTrait, index: number): void { setCurrentMemberLinks(links => (links ?? []).map((l, i) => ( i === index ? link : l ))) - - setHasChanges(true) - return link } function handleLinksSave(): void { @@ -76,10 +138,6 @@ const ModifyMemberLinksModal: FC = (props: ModifyMe const updatedPersonalizationTraits: UserTrait[] = reject(props.memberPersonalizationTraitsFullData, (trait: UserTrait) => trait.links) - const updatedLinks: UserTrait[] = [ - ...(currentMemberLinks || []), - ].filter(l => l.name && l.url) - updateOrCreateMemberTraitsAsync(props.profile.handle, [{ categoryName: UserTraitCategoryNames.personalization, traitId: UserTraitIds.personalization, @@ -129,28 +187,102 @@ const ModifyMemberLinksModal: FC = (props: ModifyMe

Provide links to your social accounts.

-
- { - currentMemberLinks?.map((trait: UserTrait, i: number) => ( - - )) - } +
+ + + +
+ +
+ + + +
+
- {!hasNewInput && ( -
-