feat: on blur check username

pull/5494/head
Arik Chakma 8 months ago
parent d4a563fe49
commit fe651b21bc
  1. 87
      src/components/UpdateProfile/ProfileUsername.tsx
  2. 81
      src/components/UpdateProfile/UpdatePublicProfileForm.tsx

@ -0,0 +1,87 @@
import { useState } from 'react';
import type { AllowedProfileVisibility } from '../../api/user';
import { httpGet, httpPost } from '../../lib/http';
import { useToast } from '../../hooks/use-toast';
import { CheckIcon, Loader2, X, XCircle } from 'lucide-react';
type ProfileUsernameProps = {
username: string;
setUsername: (username: string) => void;
profileVisibility: AllowedProfileVisibility;
currentUsername?: string;
};
export function ProfileUsername(props: ProfileUsernameProps) {
const { username, setUsername, profileVisibility, currentUsername } = props;
const toast = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isUnique, setIsUnique] = useState<boolean | null>(null);
const checkIsUnique = async (username: string) => {
if (isLoading || username.length < 3) {
return;
}
if (currentUsername && username === currentUsername && isUnique !== false) {
setIsUnique(true);
return;
}
setIsLoading(true);
const { response, error } = await httpPost<{
isUnique: boolean;
}>(`${import.meta.env.PUBLIC_API_URL}/v1-check-is-unique-username`, {
username,
});
if (error || !response) {
setIsUnique(null);
setIsLoading(false);
toast.error(error?.message || 'Something went wrong. Please try again.');
return;
}
setIsUnique(response.isUnique);
setIsLoading(false);
};
return (
<div className="flex w-full flex-col">
<label htmlFor="username" className="text-sm leading-none text-slate-500">
Username
</label>
<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">
roadmap.sh/u/
</span>
<div className="relative grow">
<input
type="text"
name="username"
id="username"
className="w-full px-3 py-2 outline-none placeholder:text-gray-400"
placeholder="johndoe"
spellCheck={false}
value={username}
title="Username must be at least 3 characters long and can only contain letters, numbers, and underscores"
onChange={(e) => setUsername((e.target as HTMLInputElement).value)}
onBlur={(e) => checkIsUnique((e.target as HTMLInputElement).value)}
required={profileVisibility === 'public'}
/>
<span className="absolute bottom-0 right-0 top-0 flex items-center px-2">
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : isUnique === false ? (
<X className="h-4 w-4 text-red-500" />
) : isUnique === true ? (
<CheckIcon className="h-4 w-4 text-green-500" />
) : null}
</span>
</div>
</div>
</div>
);
}

@ -12,6 +12,7 @@ import { ArrowUpRight, Eye, EyeOff } from 'lucide-react';
import { useToast } from '../../hooks/use-toast'; 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';
type RoadmapType = { type RoadmapType = {
id: string; id: string;
@ -43,6 +44,8 @@ export function UpdatePublicProfileForm() {
const [roadmaps, setRoadmaps] = useState<string[]>([]); const [roadmaps, setRoadmaps] = useState<string[]>([]);
const [customRoadmaps, setCustomRoadmaps] = useState<string[]>([]); const [customRoadmaps, setCustomRoadmaps] = useState<string[]>([]);
const [currentUsername, setCurrentUsername] = 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('');
@ -109,6 +112,7 @@ export function UpdatePublicProfileForm() {
setPublicProfileUrl(username ? `/u/${username}` : ''); setPublicProfileUrl(username ? `/u/${username}` : '');
setUsername(username || ''); setUsername(username || '');
setCurrentUsername(username || '');
setGithub(links?.github || ''); setGithub(links?.github || '');
setTwitter(links?.twitter || ''); setTwitter(links?.twitter || '');
setLinkedin(links?.linkedin || ''); setLinkedin(links?.linkedin || '');
@ -187,26 +191,28 @@ export function UpdatePublicProfileForm() {
<CreateRoadmapModal onClose={() => setIsCreatingRoadmap(false)} /> <CreateRoadmapModal onClose={() => setIsCreatingRoadmap(false)} />
)} )}
<div className="flex gap-2 flex-col sm:flex-row justify-between mb-1"> <div className="mb-1 flex flex-col justify-between gap-2 sm:flex-row">
<div className="flex gap-2 flex-grow flex-col sm:flex-row items-start"> <div className="flex flex-grow flex-col items-start gap-2 sm:flex-row">
<h3 className="mr-1 text-xl sm:text-3xl font-bold">Personal Profile</h3> <h3 className="mr-1 text-xl font-bold sm:text-3xl">
{publicProfileUrl && ( Personal Profile
<a </h3>
href={publicProfileUrl} {publicProfileUrl && (
target="_blank" <a
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" href={publicProfileUrl}
> target="_blank"
<ArrowUpRight className="h-3 w-3 stroke-[3]" /> 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"
Visit >
</a> <ArrowUpRight className="h-3 w-3 stroke-[3]" />
)} Visit
</div> </a>
<VisibilityDropdown )}
visibility={profileVisibility}
setVisibility={setProfileVisibility}
/>
</div> </div>
<p className="text-gray-400 text-sm sm:text-base mt-2 sm:mt-0 hidden sm:block"> <VisibilityDropdown
visibility={profileVisibility}
setVisibility={setProfileVisibility}
/>
</div>
<p className="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>
@ -229,40 +235,19 @@ export function UpdatePublicProfileForm() {
required={profileVisibility === 'public'} required={profileVisibility === 'public'}
/> />
</div> </div>
<div className="flex w-full flex-col">
<label
htmlFor="username"
className="text-sm leading-none text-slate-500"
>
Username
</label>
<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">
roadmap.sh/u/
</span>
<input <ProfileUsername
type="text" username={username}
name="username" setUsername={setUsername}
id="username" profileVisibility={profileVisibility}
className="w-full px-3 py-2 outline-none placeholder:text-gray-400" currentUsername={currentUsername}
placeholder="johndoe" />
spellCheck={false}
value={username}
title="Username must be at least 3 characters long and can only contain letters, numbers, and underscores"
onChange={(e) =>
setUsername((e.target as HTMLInputElement).value)
}
required={profileVisibility === 'public'}
/>
</div>
</div>
<div className="rounded-md border p-4"> <div className="rounded-md border p-4">
<h3 className="text-sm font-medium"> <h3 className="text-sm font-medium">
Which roadmap progresses do you want to show on your profile? Which roadmap progresses do you want to show on your profile?
</h3> </h3>
<div className="mt-3 flex items-center flex-wrap gap-2"> <div className="mt-3 flex flex-wrap items-center gap-2">
<SelectionButton <SelectionButton
type="button" type="button"
text="All Progress" text="All Progress"
@ -332,7 +317,7 @@ export function UpdatePublicProfileForm() {
<h3 className="text-sm font-medium"> <h3 className="text-sm font-medium">
Pick your custom roadmaps to show on your profile Pick your custom roadmaps to show on your profile
</h3> </h3>
<div className="mt-3 flex items-center gap-2 flex-wrap"> <div className="mt-3 flex flex-wrap items-center gap-2">
<SelectionButton <SelectionButton
type="button" type="button"
text="All Roadmaps" text="All Roadmaps"

Loading…
Cancel
Save