computer-scienceangular-roadmapbackend-roadmapblockchain-roadmapdba-roadmapdeveloper-roadmapdevops-roadmapfrontend-roadmapgo-roadmaphactoberfestjava-roadmapjavascript-roadmapnodejs-roadmappython-roadmapqa-roadmapreact-roadmaproadmapstudy-planvue-roadmapweb3-roadmap
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
599 lines
20 KiB
599 lines
20 KiB
import { type FormEvent, useEffect, useState } from 'react'; |
|
import { httpGet, httpPatch } from '../../lib/http'; |
|
import { pageProgressMessage } from '../../stores/page'; |
|
import type { |
|
AllowedCustomRoadmapVisibility, |
|
AllowedProfileVisibility, |
|
AllowedRoadmapVisibility, |
|
UserDocument, |
|
} from '../../api/user'; |
|
import { SelectionButton } from '../RoadCard/SelectionButton'; |
|
import { |
|
ArrowUpRight, |
|
Check, |
|
CheckCircle, |
|
Copy, |
|
Eye, |
|
EyeOff, |
|
FileBadge, |
|
Trophy, |
|
} from 'lucide-react'; |
|
import { useToast } from '../../hooks/use-toast'; |
|
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx'; |
|
import { VisibilityDropdown } from './VisibilityDropdown.tsx'; |
|
import { ProfileUsername } from './ProfileUsername.tsx'; |
|
import UploadProfilePicture from './UploadProfilePicture.tsx'; |
|
import { SkillProfileAlert } from './SkillProfileAlert.tsx'; |
|
import { useCopyText } from '../../hooks/use-copy-text.ts'; |
|
import { cn } from '../../lib/classname.ts'; |
|
|
|
type RoadmapType = { |
|
id: string; |
|
title: string; |
|
isCustomResource: boolean; |
|
}; |
|
|
|
type GetProfileSettingsResponse = Pick< |
|
UserDocument, |
|
'username' | 'profileVisibility' | 'publicConfig' | 'links' |
|
>; |
|
|
|
export function UpdatePublicProfileForm() { |
|
const [profileVisibility, setProfileVisibility] = |
|
useState<AllowedProfileVisibility>('public'); |
|
|
|
const toast = useToast(); |
|
|
|
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false); |
|
const [publicProfileUrl, setPublicProfileUrl] = useState(''); |
|
const [isAvailableForHire, setIsAvailableForHire] = useState(false); |
|
const [isEmailVisible, setIsEmailVisible] = useState(true); |
|
const [headline, setHeadline] = useState(''); |
|
const [username, setUsername] = useState(''); |
|
const [email, setEmail] = useState(''); |
|
const [roadmapVisibility, setRoadmapVisibility] = |
|
useState<AllowedRoadmapVisibility>('all'); |
|
const [customRoadmapVisibility, setCustomRoadmapVisibility] = |
|
useState<AllowedCustomRoadmapVisibility>('all'); |
|
const [roadmaps, setRoadmaps] = useState<string[]>([]); |
|
const [customRoadmaps, setCustomRoadmaps] = useState<string[]>([]); |
|
|
|
const [currentUsername, setCurrentUsername] = useState(''); |
|
const [name, setName] = useState(''); |
|
|
|
const [avatar, setAvatar] = useState(''); |
|
const [github, setGithub] = useState(''); |
|
const [twitter, setTwitter] = useState(''); |
|
const [linkedin, setLinkedin] = useState(''); |
|
const [dailydev, setDailydev] = useState(''); |
|
const [website, setWebsite] = useState(''); |
|
|
|
const [profileRoadmaps, setProfileRoadmaps] = useState<RoadmapType[]>([]); |
|
|
|
const [isLoading, setIsLoading] = useState(false); |
|
|
|
const { isCopied, copyText } = useCopyText(); |
|
|
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { |
|
e.preventDefault(); |
|
setIsLoading(true); |
|
|
|
const { response, error } = await httpPatch( |
|
`${import.meta.env.PUBLIC_API_URL}/v1-update-public-profile-config`, |
|
{ |
|
isAvailableForHire, |
|
isEmailVisible, |
|
profileVisibility, |
|
headline, |
|
username, |
|
roadmapVisibility, |
|
customRoadmapVisibility, |
|
roadmaps, |
|
customRoadmaps, |
|
github, |
|
twitter, |
|
linkedin, |
|
website, |
|
name, |
|
email, |
|
dailydev, |
|
}, |
|
); |
|
|
|
if (error || !response) { |
|
setIsLoading(false); |
|
toast.error(error?.message || 'Something went wrong'); |
|
|
|
return; |
|
} |
|
|
|
await loadProfileSettings(); |
|
toast.success('Profile updated successfully'); |
|
}; |
|
|
|
const loadProfileSettings = async () => { |
|
setIsLoading(true); |
|
|
|
const { error, response } = await httpGet<UserDocument>( |
|
`${import.meta.env.PUBLIC_API_URL}/v1-get-profile-settings`, |
|
); |
|
|
|
if (error || !response) { |
|
setIsLoading(false); |
|
toast.error(error?.message || 'Something went wrong'); |
|
|
|
return; |
|
} |
|
|
|
const { |
|
name, |
|
email, |
|
links, |
|
username, |
|
profileVisibility: defaultProfileVisibility, |
|
publicConfig, |
|
avatar, |
|
} = response; |
|
|
|
setAvatar(avatar || ''); |
|
setPublicProfileUrl(username ? `/u/${username}` : ''); |
|
setUsername(username || ''); |
|
setCurrentUsername(username || ''); |
|
setName(name || ''); |
|
setEmail(email || ''); |
|
setGithub(links?.github || ''); |
|
setTwitter(links?.twitter || ''); |
|
setLinkedin(links?.linkedin || ''); |
|
setDailydev(links?.dailydev || ''); |
|
setWebsite(links?.website || ''); |
|
setProfileVisibility(defaultProfileVisibility || 'public'); |
|
setHeadline(publicConfig?.headline || ''); |
|
setRoadmapVisibility(publicConfig?.roadmapVisibility || 'all'); |
|
setCustomRoadmapVisibility(publicConfig?.customRoadmapVisibility || 'all'); |
|
setCustomRoadmaps(publicConfig?.customRoadmaps || []); |
|
setRoadmaps(publicConfig?.roadmaps || []); |
|
setIsAvailableForHire(publicConfig?.isAvailableForHire || false); |
|
setIsEmailVisible(publicConfig?.isEmailVisible ?? true); |
|
|
|
setIsLoading(false); |
|
}; |
|
|
|
const loadProfileRoadmaps = async () => { |
|
setIsLoading(true); |
|
|
|
const { error, response } = await httpGet<{ |
|
roadmaps: RoadmapType[]; |
|
}>(`${import.meta.env.PUBLIC_API_URL}/v1-get-profile-roadmaps`); |
|
|
|
if (error || !response) { |
|
setIsLoading(false); |
|
toast.error(error?.message || 'Something went wrong'); |
|
|
|
return; |
|
} |
|
|
|
setProfileRoadmaps(response?.roadmaps || []); |
|
setIsLoading(false); |
|
}; |
|
|
|
// Make a request to the backend to fill in the form with the current values |
|
useEffect(() => { |
|
Promise.all([loadProfileSettings(), loadProfileRoadmaps()]).finally(() => { |
|
pageProgressMessage.set(''); |
|
}); |
|
}, []); |
|
|
|
const publicCustomRoadmaps = profileRoadmaps.filter( |
|
(r) => r.isCustomResource, |
|
); |
|
const publicRoadmaps = profileRoadmaps.filter((r) => !r.isCustomResource); |
|
|
|
return ( |
|
<div> |
|
{isCreatingRoadmap && ( |
|
<CreateRoadmapModal onClose={() => setIsCreatingRoadmap(false)} /> |
|
)} |
|
|
|
<SkillProfileAlert /> |
|
|
|
<div className="mb-8 flex flex-col justify-between gap-2 sm:mb-1 sm:flex-row"> |
|
<div className="flex flex-grow flex-row items-center gap-2 sm:items-center"> |
|
<h3 className="mr-1 text-xl font-bold sm:text-3xl">Skill Profile</h3> |
|
{publicProfileUrl && ( |
|
<> |
|
<a |
|
href={publicProfileUrl} |
|
target="_blank" |
|
className="flex shrink-0 flex-row items-center gap-1 rounded-lg border border-black py-0.5 pl-1.5 pr-2.5 text-xs uppercase transition-colors hover:bg-black hover:text-white" |
|
> |
|
<ArrowUpRight className="h-3 w-3 stroke-[3]" /> |
|
Visit |
|
</a> |
|
<button |
|
onClick={() => { |
|
copyText(`${window.location.origin}${publicProfileUrl}`); |
|
}} |
|
className={cn( |
|
'flex shrink-0 flex-row items-center gap-1 rounded-lg border border-black py-0.5 pl-1.5 pr-2.5 text-xs uppercase transition-colors hover:bg-black hover:text-white', |
|
{ |
|
'bg-black text-white': isCopied, |
|
}, |
|
)} |
|
> |
|
{!isCopied && <Copy className="h-3 w-3 stroke-[2.5]" />} |
|
{isCopied && <Check className="h-3 w-3 stroke-[2.5]" />} |
|
{!isCopied ? 'Copy URL' : 'Copied!'} |
|
</button> |
|
</> |
|
)} |
|
</div> |
|
<VisibilityDropdown |
|
visibility={profileVisibility} |
|
setVisibility={setProfileVisibility} |
|
/> |
|
</div> |
|
<p className="mb-8 mt-2 hidden text-sm text-gray-400 sm:mt-0 sm:block sm:text-base"> |
|
Create your skill profile to showcase your skills. |
|
</p> |
|
|
|
<UploadProfilePicture |
|
type="avatar" |
|
label="Profile picture" |
|
avatarUrl={ |
|
avatar |
|
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}` |
|
: '/images/default-avatar.png' |
|
} |
|
/> |
|
|
|
<form className="mt-6 space-y-4 pb-10" onSubmit={handleSubmit}> |
|
<div className="flex w-full flex-col"> |
|
<label |
|
htmlFor="name" |
|
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]' |
|
> |
|
Name |
|
</label> |
|
<input |
|
type="text" |
|
name="name" |
|
id="name" |
|
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
|
required |
|
placeholder="John Doe" |
|
value={name} |
|
onInput={(e) => setName((e.target as HTMLInputElement).value)} |
|
/> |
|
</div> |
|
|
|
<div className="flex w-full flex-col"> |
|
<div className="flex items-center justify-between"> |
|
<label |
|
htmlFor="email" |
|
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]' |
|
> |
|
Email |
|
</label> |
|
<a |
|
href="/account/settings" |
|
className="text-xs text-purple-700 underline hover:text-purple-800" |
|
> |
|
Visit settings page to change email |
|
</a> |
|
</div> |
|
<input |
|
type="email" |
|
name="email" |
|
id="email" |
|
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
|
required |
|
disabled |
|
placeholder="john@example.com" |
|
value={email} |
|
/> |
|
<div className="flex items-center justify-end gap-2 rounded-md text-xs text-gray-400"> |
|
<div className="flex select-none items-center justify-end gap-2 rounded-md text-xs text-gray-400"> |
|
<input |
|
type="checkbox" |
|
name="isEmailVisible" |
|
id="isEmailVisible" |
|
checked={isEmailVisible} |
|
onChange={(e) => setIsEmailVisible(e.target.checked)} |
|
/> |
|
<label |
|
htmlFor="isEmailVisible" |
|
className="flex-grow cursor-pointer py-1.5" |
|
> |
|
Show my email on profile |
|
</label> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div className="flex w-full flex-col"> |
|
<label |
|
htmlFor="headline" |
|
className="text-sm leading-none text-slate-500" |
|
> |
|
Headline |
|
</label> |
|
<input |
|
type="text" |
|
name="headline" |
|
id="headline" |
|
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
|
placeholder="Full Stack Developer" |
|
value={headline} |
|
onChange={(e) => setHeadline((e.target as HTMLInputElement).value)} |
|
required={profileVisibility === 'public'} |
|
/> |
|
</div> |
|
|
|
<ProfileUsername |
|
username={username} |
|
setUsername={setUsername} |
|
profileVisibility={profileVisibility} |
|
currentUsername={currentUsername} |
|
/> |
|
|
|
<div className="rounded-md border p-4"> |
|
<h3 className="text-sm font-medium"> |
|
Which roadmap progresses do you want to show on your profile? |
|
</h3> |
|
<div className="mt-3 flex flex-wrap items-center gap-2"> |
|
<SelectionButton |
|
type="button" |
|
text="All Progress" |
|
icon={Eye} |
|
isDisabled={false} |
|
isSelected={roadmapVisibility === 'all'} |
|
onClick={() => { |
|
setRoadmapVisibility('all'); |
|
setRoadmaps([]); |
|
}} |
|
/> |
|
<SelectionButton |
|
type="button" |
|
icon={EyeOff} |
|
text="Hide my Progress" |
|
isDisabled={false} |
|
isSelected={roadmapVisibility === 'none'} |
|
onClick={() => { |
|
setRoadmapVisibility('none'); |
|
setRoadmaps([]); |
|
}} |
|
/> |
|
</div> |
|
|
|
<h3 className="mt-4 text-sm text-gray-400"> |
|
Or select the roadmaps you want to show |
|
</h3> |
|
{publicRoadmaps.length > 0 ? ( |
|
<div className="mt-3 flex flex-wrap items-center gap-2"> |
|
{publicRoadmaps.map((r) => ( |
|
<SelectionButton |
|
type="button" |
|
key={r.id} |
|
text={r.title} |
|
isDisabled={false} |
|
isSelected={roadmaps.includes(r.id)} |
|
onClick={() => { |
|
if (roadmapVisibility !== 'selected') { |
|
setRoadmapVisibility('selected'); |
|
} |
|
|
|
if (roadmaps.includes(r.id)) { |
|
setRoadmaps(roadmaps.filter((id) => id !== r.id)); |
|
} else { |
|
setRoadmaps([...roadmaps, r.id]); |
|
} |
|
}} |
|
/> |
|
))} |
|
</div> |
|
) : ( |
|
<p className="mt-2 rounded-lg bg-yellow-100 p-2 text-sm text-yellow-700"> |
|
Update{' '} |
|
<a |
|
target="_blank" |
|
className="font-medium underline underline-offset-2 hover:text-yellow-800" |
|
href="/roadmaps" |
|
> |
|
your progress on roadmaps |
|
</a>{' '} |
|
to show your learning activity. |
|
</p> |
|
)} |
|
</div> |
|
|
|
<div className="rounded-md border p-4"> |
|
<h3 className="text-sm font-medium"> |
|
Pick your custom roadmaps to show on your profile |
|
</h3> |
|
<div className="mt-3 flex flex-wrap items-center gap-2"> |
|
<SelectionButton |
|
type="button" |
|
text="All Roadmaps" |
|
icon={Eye} |
|
isDisabled={false} |
|
isSelected={customRoadmapVisibility === 'all'} |
|
onClick={() => { |
|
setCustomRoadmapVisibility('all'); |
|
setCustomRoadmaps([]); |
|
}} |
|
/> |
|
<SelectionButton |
|
type="button" |
|
text="Hide my Roadmaps" |
|
icon={EyeOff} |
|
isDisabled={false} |
|
isSelected={customRoadmapVisibility === 'none'} |
|
onClick={() => { |
|
setCustomRoadmapVisibility('none'); |
|
setCustomRoadmaps([]); |
|
}} |
|
/> |
|
</div> |
|
|
|
<h3 className="mt-4 text-sm text-gray-400"> |
|
Or select the custom roadmaps you want to show |
|
</h3> |
|
{publicCustomRoadmaps.length > 0 ? ( |
|
<div className="mt-3 flex flex-wrap items-center gap-2"> |
|
{publicCustomRoadmaps.map((r) => ( |
|
<SelectionButton |
|
type="button" |
|
key={r.id} |
|
text={r.title} |
|
isDisabled={false} |
|
isSelected={customRoadmaps.includes(r.id)} |
|
onClick={() => { |
|
if (customRoadmapVisibility !== 'selected') { |
|
setCustomRoadmapVisibility('selected'); |
|
} |
|
|
|
if (customRoadmaps.includes(r.id)) { |
|
setCustomRoadmaps( |
|
customRoadmaps.filter((id) => id !== r.id), |
|
); |
|
} else { |
|
setCustomRoadmaps([...customRoadmaps, r.id]); |
|
} |
|
}} |
|
/> |
|
))} |
|
</div> |
|
) : ( |
|
<p className="mt-2 rounded-lg bg-yellow-100 p-2 text-sm text-yellow-700"> |
|
You do not have any custom roadmaps.{' '} |
|
<button |
|
type={'button'} |
|
className="font-medium underline underline-offset-2 hover:text-yellow-800" |
|
onClick={() => { |
|
setIsCreatingRoadmap(true); |
|
}} |
|
> |
|
Create one now |
|
</button> |
|
. |
|
</p> |
|
)} |
|
</div> |
|
|
|
<div className="flex w-full flex-col"> |
|
<label |
|
htmlFor="github" |
|
className="text-sm leading-none text-slate-500" |
|
> |
|
Github |
|
</label> |
|
<input |
|
type="text" |
|
name="github" |
|
id="github" |
|
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
|
placeholder="https://github.com/username" |
|
value={github} |
|
onChange={(e) => setGithub((e.target as HTMLInputElement).value)} |
|
/> |
|
</div> |
|
<div className="flex w-full flex-col"> |
|
<label |
|
htmlFor="twitter" |
|
className="text-sm leading-none text-slate-500" |
|
> |
|
Twitter |
|
</label> |
|
<input |
|
type="text" |
|
name="twitter" |
|
id="twitter" |
|
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
|
placeholder="https://twitter.com/username" |
|
value={twitter} |
|
onChange={(e) => setTwitter((e.target as HTMLInputElement).value)} |
|
/> |
|
</div> |
|
|
|
<div className="flex w-full flex-col"> |
|
<label |
|
htmlFor="linkedin" |
|
className="text-sm leading-none text-slate-500" |
|
> |
|
LinkedIn |
|
</label> |
|
<input |
|
type="text" |
|
name="linkedin" |
|
id="linkedin" |
|
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
|
placeholder="https://www.linkedin.com/in/username/" |
|
value={linkedin} |
|
onChange={(e) => setLinkedin((e.target as HTMLInputElement).value)} |
|
/> |
|
</div> |
|
<div className="flex w-full flex-col"> |
|
<label |
|
htmlFor="dailydev" |
|
className="text-sm leading-none text-slate-500" |
|
> |
|
daily.dev |
|
</label> |
|
<input |
|
type="text" |
|
name="dailydev" |
|
id="dailydev" |
|
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
|
placeholder="https://app.daily.dev/username" |
|
value={dailydev} |
|
onChange={(e) => setDailydev((e.target as HTMLInputElement).value)} |
|
/> |
|
</div> |
|
|
|
<div className="flex w-full flex-col"> |
|
<label |
|
htmlFor="website" |
|
className="text-sm leading-none text-slate-500" |
|
> |
|
Website |
|
</label> |
|
<input |
|
type="text" |
|
name="website" |
|
id="website" |
|
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" |
|
placeholder="https://example.com" |
|
value={website} |
|
onChange={(e) => setWebsite((e.target as HTMLInputElement).value)} |
|
/> |
|
</div> |
|
|
|
<div className="flex flex-col gap-2"> |
|
<div className="flex select-none items-center gap-2 rounded-md border px-3 hover:bg-gray-100"> |
|
<input |
|
type="checkbox" |
|
name="isAvailableForHire" |
|
id="isAvailableForHire" |
|
checked={isAvailableForHire} |
|
onChange={(e) => setIsAvailableForHire(e.target.checked)} |
|
/> |
|
<label |
|
htmlFor="isAvailableForHire" |
|
className="flex-grow cursor-pointer py-1.5" |
|
> |
|
Available for Hire |
|
</label> |
|
</div> |
|
</div> |
|
|
|
<button |
|
type="submit" |
|
disabled={isLoading} |
|
className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400" |
|
> |
|
{isLoading ? 'Please wait..' : 'Save Profile'} |
|
</button> |
|
</form> |
|
</div> |
|
); |
|
}
|
|
|