mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-09 02:36:40 +02:00
Update profile form
This commit is contained in:
@@ -18,7 +18,6 @@ type AccountDropdownListProps = {
|
|||||||
|
|
||||||
export function AccountDropdownList(props: AccountDropdownListProps) {
|
export function AccountDropdownList(props: AccountDropdownListProps) {
|
||||||
const { setIsTeamsOpen, onCreateRoadmap } = props;
|
const { setIsTeamsOpen, onCreateRoadmap } = props;
|
||||||
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul>
|
<ul>
|
||||||
|
@@ -25,7 +25,12 @@ export function ProfileUsername(props: ProfileUsernameProps) {
|
|||||||
}, [debouncedUsername]);
|
}, [debouncedUsername]);
|
||||||
|
|
||||||
const checkIsUnique = async (username: string) => {
|
const checkIsUnique = async (username: string) => {
|
||||||
if (isLoading || username.length < 3) {
|
if (isLoading || !username) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username.length < 3) {
|
||||||
|
setIsUnique(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,8 +59,44 @@ export function ProfileUsername(props: ProfileUsernameProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<label htmlFor="username" className="text-sm leading-none text-slate-500">
|
<label
|
||||||
Username
|
htmlFor="username"
|
||||||
|
className="flex min-h-[16.5px] items-center justify-between text-sm leading-none text-slate-500"
|
||||||
|
>
|
||||||
|
<span>Profile URL</span>
|
||||||
|
{!isLoading && (
|
||||||
|
<span className="flex items-center">
|
||||||
|
{currentUsername &&
|
||||||
|
(currentUsername === username || !username || !isUnique) && (
|
||||||
|
<span className="text-xs">
|
||||||
|
Current URL{' '}
|
||||||
|
<a
|
||||||
|
href={`${import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh'}/u/${currentUsername}`}
|
||||||
|
target="_blank"
|
||||||
|
className={
|
||||||
|
'ml-0.5 rounded-md border border-purple-500 px-1.5 py-0.5 font-mono text-xs font-medium text-purple-700 transition-colors hover:bg-purple-500 hover:text-white'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
roadmap.sh/u/{currentUsername}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{currentUsername !== username && username && isUnique && (
|
||||||
|
<span className="text-xs text-green-600">
|
||||||
|
URL after update{' '}
|
||||||
|
<a
|
||||||
|
href={`${import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh'}/u/${username}`}
|
||||||
|
target="_blank"
|
||||||
|
className={
|
||||||
|
'ml-0.5 rounded-md border border-purple-500 px-1.5 py-0.5 text-xs font-medium text-purple-700 transition-colors hover:bg-purple-500 hover:text-purple-800 hover:text-white'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
roadmap.sh/u/{username}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-2 flex items-center overflow-hidden rounded-lg border border-gray-300">
|
<div className="mt-2 flex items-center overflow-hidden rounded-lg border border-gray-300">
|
||||||
<span className="border-r border-gray-300 bg-gray-100 p-2">
|
<span className="border-r border-gray-300 bg-gray-100 p-2">
|
||||||
@@ -75,7 +116,8 @@ export function ProfileUsername(props: ProfileUsernameProps) {
|
|||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
// only allow letters, numbers
|
// only allow letters, numbers
|
||||||
const keyCode = e.key;
|
const keyCode = e.key;
|
||||||
const validKey = /^[a-zA-Z0-9]*$/.test(keyCode);
|
const validKey =
|
||||||
|
/^[a-zA-Z0-9]*$/.test(keyCode) && username.length < 10;
|
||||||
if (
|
if (
|
||||||
!validKey &&
|
!validKey &&
|
||||||
!['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight'].includes(
|
!['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight'].includes(
|
||||||
@@ -88,19 +130,20 @@ export function ProfileUsername(props: ProfileUsernameProps) {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setUsername((e.target as HTMLInputElement).value.toLowerCase());
|
setUsername((e.target as HTMLInputElement).value.toLowerCase());
|
||||||
}}
|
}}
|
||||||
onBlur={(e) => checkIsUnique((e.target as HTMLInputElement).value)}
|
|
||||||
required={profileVisibility === 'public'}
|
required={profileVisibility === 'public'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className="absolute bottom-0 right-0 top-0 flex items-center px-2">
|
{username && (
|
||||||
{isLoading ? (
|
<span className="absolute bottom-0 right-0 top-0 flex items-center px-2">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
{isLoading ? (
|
||||||
) : isUnique === false ? (
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
<X className="h-4 w-4 text-red-500" />
|
) : isUnique === false ? (
|
||||||
) : isUnique === true ? (
|
<X className="h-4 w-4 text-red-500" />
|
||||||
<CheckIcon className="h-4 w-4 text-green-500" />
|
) : isUnique === true ? (
|
||||||
) : null}
|
<CheckIcon className="h-4 w-4 text-green-500" />
|
||||||
</span>
|
) : null}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,7 +2,6 @@ import { type FormEvent, useEffect, useState } from 'react';
|
|||||||
import { httpGet, httpPost } from '../../lib/http';
|
import { httpGet, httpPost } from '../../lib/http';
|
||||||
import { pageProgressMessage } from '../../stores/page';
|
import { pageProgressMessage } from '../../stores/page';
|
||||||
import UploadProfilePicture from './UploadProfilePicture';
|
import UploadProfilePicture from './UploadProfilePicture';
|
||||||
import {ArrowDown, ChevronDown} from "lucide-react";
|
|
||||||
|
|
||||||
export function UpdateProfileForm() {
|
export function UpdateProfileForm() {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
@@ -113,7 +112,10 @@ export function UpdateProfileForm() {
|
|||||||
>
|
>
|
||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
<a href='/account/settings' className="text-purple-700 text-xs underline hover:text-purple-800">
|
<a
|
||||||
|
href="/account/settings"
|
||||||
|
className="text-xs text-purple-700 underline hover:text-purple-800"
|
||||||
|
>
|
||||||
Visit settings page to change email
|
Visit settings page to change email
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -13,6 +13,7 @@ import { useToast } from '../../hooks/use-toast';
|
|||||||
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
|
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
|
||||||
import { VisibilityDropdown } from './VisibilityDropdown.tsx';
|
import { VisibilityDropdown } from './VisibilityDropdown.tsx';
|
||||||
import { ProfileUsername } from './ProfileUsername.tsx';
|
import { ProfileUsername } from './ProfileUsername.tsx';
|
||||||
|
import UploadProfilePicture from './UploadProfilePicture.tsx';
|
||||||
|
|
||||||
type RoadmapType = {
|
type RoadmapType = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -37,6 +38,7 @@ export function UpdatePublicProfileForm() {
|
|||||||
const [isEmailVisible, setIsEmailVisible] = useState(true);
|
const [isEmailVisible, setIsEmailVisible] = useState(true);
|
||||||
const [headline, setHeadline] = useState('');
|
const [headline, setHeadline] = useState('');
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
const [roadmapVisibility, setRoadmapVisibility] =
|
const [roadmapVisibility, setRoadmapVisibility] =
|
||||||
useState<AllowedRoadmapVisibility>('all');
|
useState<AllowedRoadmapVisibility>('all');
|
||||||
const [customRoadmapVisibility, setCustomRoadmapVisibility] =
|
const [customRoadmapVisibility, setCustomRoadmapVisibility] =
|
||||||
@@ -45,7 +47,9 @@ export function UpdatePublicProfileForm() {
|
|||||||
const [customRoadmaps, setCustomRoadmaps] = useState<string[]>([]);
|
const [customRoadmaps, setCustomRoadmaps] = useState<string[]>([]);
|
||||||
|
|
||||||
const [currentUsername, setCurrentUsername] = useState('');
|
const [currentUsername, setCurrentUsername] = useState('');
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
|
||||||
|
const [avatar, setAvatar] = useState('');
|
||||||
const [github, setGithub] = useState('');
|
const [github, setGithub] = useState('');
|
||||||
const [twitter, setTwitter] = useState('');
|
const [twitter, setTwitter] = useState('');
|
||||||
const [linkedin, setLinkedin] = useState('');
|
const [linkedin, setLinkedin] = useState('');
|
||||||
@@ -75,6 +79,8 @@ export function UpdatePublicProfileForm() {
|
|||||||
twitter,
|
twitter,
|
||||||
linkedin,
|
linkedin,
|
||||||
website,
|
website,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -104,26 +110,31 @@ export function UpdatePublicProfileForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
links,
|
links,
|
||||||
username,
|
username,
|
||||||
profileVisibility: defaultProfileVisibility,
|
profileVisibility: defaultProfileVisibility,
|
||||||
publicConfig,
|
publicConfig,
|
||||||
|
avatar,
|
||||||
} = response;
|
} = response;
|
||||||
|
|
||||||
|
setAvatar(avatar || '');
|
||||||
setPublicProfileUrl(username ? `/u/${username}` : '');
|
setPublicProfileUrl(username ? `/u/${username}` : '');
|
||||||
setUsername(username || '');
|
setUsername(username || '');
|
||||||
setCurrentUsername(username || '');
|
setCurrentUsername(username || '');
|
||||||
|
setName(name || '');
|
||||||
|
setEmail(email || '');
|
||||||
setGithub(links?.github || '');
|
setGithub(links?.github || '');
|
||||||
setTwitter(links?.twitter || '');
|
setTwitter(links?.twitter || '');
|
||||||
setLinkedin(links?.linkedin || '');
|
setLinkedin(links?.linkedin || '');
|
||||||
setWebsite(links?.website || '');
|
setWebsite(links?.website || '');
|
||||||
setProfileVisibility(defaultProfileVisibility || 'public');
|
setProfileVisibility(defaultProfileVisibility || 'public');
|
||||||
setHeadline(publicConfig?.headline || '');
|
setHeadline(publicConfig?.headline || '');
|
||||||
setRoadmapVisibility(publicConfig?.roadmapVisibility || 'none');
|
setRoadmapVisibility(publicConfig?.roadmapVisibility || 'all');
|
||||||
setCustomRoadmapVisibility(publicConfig?.customRoadmapVisibility || 'none');
|
setCustomRoadmapVisibility(publicConfig?.customRoadmapVisibility || 'all');
|
||||||
setCustomRoadmaps(publicConfig?.customRoadmaps || []);
|
setCustomRoadmaps(publicConfig?.customRoadmaps || []);
|
||||||
setRoadmaps(publicConfig?.roadmaps || []);
|
setRoadmaps(publicConfig?.roadmaps || []);
|
||||||
setCustomRoadmapVisibility(publicConfig?.customRoadmapVisibility || 'none');
|
|
||||||
setIsAvailableForHire(publicConfig?.isAvailableForHire || false);
|
setIsAvailableForHire(publicConfig?.isAvailableForHire || false);
|
||||||
setIsEmailVisible(publicConfig?.isEmailVisible ?? true);
|
setIsEmailVisible(publicConfig?.isEmailVisible ?? true);
|
||||||
|
|
||||||
@@ -186,13 +197,13 @@ export function UpdatePublicProfileForm() {
|
|||||||
const publicRoadmaps = profileRoadmaps.filter((r) => !r.isCustomResource);
|
const publicRoadmaps = profileRoadmaps.filter((r) => !r.isCustomResource);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="-mx-10 mt-10 border-t px-10 pt-10">
|
<div>
|
||||||
{isCreatingRoadmap && (
|
{isCreatingRoadmap && (
|
||||||
<CreateRoadmapModal onClose={() => setIsCreatingRoadmap(false)} />
|
<CreateRoadmapModal onClose={() => setIsCreatingRoadmap(false)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mb-1 flex flex-col justify-between gap-2 sm:flex-row">
|
<div className="mb-8 flex flex-col justify-between gap-2 sm:mb-1 sm:flex-row">
|
||||||
<div className="flex flex-grow flex-col items-start gap-2 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">
|
<h3 className="mr-1 text-xl font-bold sm:text-3xl">
|
||||||
Personal Profile
|
Personal Profile
|
||||||
</h3>
|
</h3>
|
||||||
@@ -200,7 +211,7 @@ export function UpdatePublicProfileForm() {
|
|||||||
<a
|
<a
|
||||||
href={publicProfileUrl}
|
href={publicProfileUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="flex h-[30px] shrink-0 flex-row items-center gap-1 rounded-lg border border-black pl-1.5 pr-2.5 text-sm transition-colors hover:bg-black hover:text-white"
|
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]" />
|
<ArrowUpRight className="h-3 w-3 stroke-[3]" />
|
||||||
Visit
|
Visit
|
||||||
@@ -212,11 +223,84 @@ export function UpdatePublicProfileForm() {
|
|||||||
setVisibility={setProfileVisibility}
|
setVisibility={setProfileVisibility}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 hidden text-sm text-gray-400 sm:mt-0 sm:block sm:text-base">
|
<p className="mb-8 mt-2 hidden text-sm text-gray-400 sm:mt-0 sm:block sm:text-base">
|
||||||
Set up your public profile to showcase your learning progress.
|
Set up your public profile to showcase your learning progress.
|
||||||
</p>
|
</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}>
|
<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">
|
<div className="flex w-full flex-col">
|
||||||
<label
|
<label
|
||||||
htmlFor="headline"
|
htmlFor="headline"
|
||||||
@@ -459,22 +543,6 @@ export function UpdatePublicProfileForm() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex select-none items-center gap-2 rounded-md border px-2 hover:bg-gray-100">
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
Make my email public
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex select-none items-center gap-2 rounded-md border px-2 hover:bg-gray-100">
|
<div className="flex select-none items-center gap-2 rounded-md border px-2 hover:bg-gray-100">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
import AccountSidebar from '../../components/AccountSidebar.astro';
|
import AccountSidebar from '../../components/AccountSidebar.astro';
|
||||||
import { UpdateProfileForm } from '../../components/UpdateProfile/UpdateProfileForm';
|
|
||||||
import { UpdatePublicProfileForm } from '../../components/UpdateProfile/UpdatePublicProfileForm';
|
import { UpdatePublicProfileForm } from '../../components/UpdateProfile/UpdatePublicProfileForm';
|
||||||
import AccountLayout from '../../layouts/AccountLayout.astro';
|
import AccountLayout from '../../layouts/AccountLayout.astro';
|
||||||
---
|
---
|
||||||
@@ -11,7 +10,6 @@ import AccountLayout from '../../layouts/AccountLayout.astro';
|
|||||||
initialLoadingMessage={'Loading profile'}
|
initialLoadingMessage={'Loading profile'}
|
||||||
>
|
>
|
||||||
<AccountSidebar activePageId='profile' activePageTitle='Profile'>
|
<AccountSidebar activePageId='profile' activePageTitle='Profile'>
|
||||||
<UpdateProfileForm client:load />
|
|
||||||
<UpdatePublicProfileForm client:load />
|
<UpdatePublicProfileForm client:load />
|
||||||
</AccountSidebar>
|
</AccountSidebar>
|
||||||
</AccountLayout>
|
</AccountLayout>
|
||||||
|
Reference in New Issue
Block a user