feat: update public profile

feat/public-profile
Arik Chakma 4 weeks ago
parent 4183871a75
commit c81fba3535
  1. 3
      src/components/Dashboard/DashboardPage.tsx
  2. 86
      src/components/Dashboard/PersonalDashboard.tsx
  3. 33
      src/components/UpdateProfile/UpdatePublicProfileForm.tsx
  4. 18
      src/components/UserPublicProfile/UserPublicProfileHeader.tsx

@ -54,13 +54,14 @@ export function DashboardPage(props: DashboardPageProps) {
return ( return (
<div className="min-h-screen bg-gray-50 pb-20 pt-8"> <div className="min-h-screen bg-gray-50 pb-20 pt-8">
<div className="container"> <div className="container">
<div className="mb-6 sm:mb-8 flex flex-wrap items-center gap-1.5"> <div className="mb-6 flex flex-wrap items-center gap-1.5 sm:mb-8">
<DashboardTab <DashboardTab
label="Personal" label="Personal"
isActive={!selectedTeamId} isActive={!selectedTeamId}
onClick={() => setSelectedTeamId(undefined)} onClick={() => setSelectedTeamId(undefined)}
avatar={userAvatar} avatar={userAvatar}
/> />
{isLoading && ( {isLoading && (
<> <>
<DashboardTabSkeleton /> <DashboardTabSkeleton />

@ -14,6 +14,9 @@ import { CheckEmoji } from '../ReactIcons/CheckEmoji.tsx';
import { ConstructionEmoji } from '../ReactIcons/ConstructionEmoji.tsx'; import { ConstructionEmoji } from '../ReactIcons/ConstructionEmoji.tsx';
import { BookEmoji } from '../ReactIcons/BookEmoji.tsx'; import { BookEmoji } from '../ReactIcons/BookEmoji.tsx';
import { DashboardAiRoadmaps } from './DashboardAiRoadmaps.tsx'; import { DashboardAiRoadmaps } from './DashboardAiRoadmaps.tsx';
import type { AllowedProfileVisibility } from '../../api/user.ts';
import { PencilIcon, type LucideIcon } from 'lucide-react';
import { cn } from '../../lib/classname.ts';
type UserDashboardResponse = { type UserDashboardResponse = {
name: string; name: string;
@ -21,6 +24,7 @@ type UserDashboardResponse = {
avatar: string; avatar: string;
headline: string; headline: string;
username: string; username: string;
profileVisibility: AllowedProfileVisibility;
progresses: UserProgress[]; progresses: UserProgress[];
projects: ProjectStatusDocument[]; projects: ProjectStatusDocument[];
aiRoadmaps: { aiRoadmaps: {
@ -222,18 +226,20 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
return 0; return 0;
}); });
const { username } = personalDashboardDetails || {};
return ( return (
<section> <section>
{isLoading ? ( {isLoading ? (
<div className="h-7 w-1/4 animate-pulse rounded-lg bg-gray-200"></div> <div className="h-7 w-1/4 animate-pulse rounded-lg bg-gray-200"></div>
) : ( ) : (
<div className="flex items-start sm:items-center justify-between flex-col sm:flex-row gap-1"> <div className="flex flex-col items-start justify-between gap-1 sm:flex-row sm:items-center">
<h2 className="text-lg font-medium"> <h2 className="text-lg font-medium">
Hi {name}, good {getCurrentPeriod()}! Hi {name}, good {getCurrentPeriod()}!
</h2> </h2>
<a <a
href="/home" href="/home"
className="text-xs font-medium bg-gray-200 hover:bg-gray-300 px-2.5 py-1 rounded-full text-gray-700 hover:text-black" className="rounded-full bg-gray-200 px-2.5 py-1 text-xs font-medium text-gray-700 hover:bg-gray-300 hover:text-black"
> >
Visit Homepage Visit Homepage
</a> </a>
@ -253,8 +259,16 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
<DashboardCard <DashboardCard
imgUrl={avatarLink} imgUrl={avatarLink}
title={name!} title={name!}
description="Setup your profile" description={
href="/account/update-profile" username ? 'View your profile' : 'Setup your profile'
}
href={username ? `/u/${username}` : '/account/update-profile'}
{...(username && {
externalLinkIcon: PencilIcon,
externalLinkHref: '/account/update-profile',
externalLinkText: 'Edit',
})}
className={username ? 'border-dashed' : ''}
/> />
<DashboardCard <DashboardCard
@ -312,33 +326,61 @@ type DashboardCardProps = {
title: string; title: string;
description: string; description: string;
href: string; href: string;
externalLinkIcon?: LucideIcon;
externalLinkText?: string;
externalLinkHref?: string;
className?: string;
}; };
function DashboardCard(props: DashboardCardProps) { function DashboardCard(props: DashboardCardProps) {
const { icon: Icon, imgUrl, title, description, href } = props; const {
icon: Icon,
imgUrl,
title,
description,
href,
externalLinkHref,
externalLinkIcon: ExternalLinkIcon,
externalLinkText,
className,
} = props;
return ( return (
<a <div
href={href} className={cn(
className="flex flex-col overflow-hidden rounded-lg border border-gray-300 bg-white hover:border-gray-400 hover:bg-gray-50" 'relative overflow-hidden rounded-lg border border-gray-300 bg-white hover:border-gray-400 hover:bg-gray-50',
> className,
{Icon && (
<div className="px-4 pb-3 pt-4">
<Icon className="size-6" />
</div>
)} )}
>
<a href={href} className="flex flex-col">
{Icon && (
<div className="px-4 pb-3 pt-4">
<Icon className="size-6" />
</div>
)}
{imgUrl && (
<div className="px-4 pb-1.5 pt-3.5">
<img src={imgUrl} alt={title} className="size-8 rounded-full" />
</div>
)}
{imgUrl && ( <div className="flex grow flex-col justify-center gap-0.5 p-4">
<div className="px-4 pb-1.5 pt-3.5"> <h3 className="truncate font-medium text-black">{title}</h3>
<img src={imgUrl} alt={title} className="size-8 rounded-full" /> <p className="text-xs text-black">{description}</p>
</div> </div>
</a>
{externalLinkHref && (
<a
href={externalLinkHref}
className="absolute right-0 top-0 flex items-center gap-1.5 rounded-bl-md bg-gray-200 p-1 px-2 text-sm text-gray-600 hover:bg-gray-300 hover:text-black"
>
{ExternalLinkIcon && <ExternalLinkIcon className="size-3" />}
{externalLinkText}
</a>
)} )}
</div>
<div className="flex grow flex-col justify-center gap-0.5 p-4">
<h3 className="truncate font-medium text-black">{title}</h3>
<p className="text-xs text-black">{description}</p>
</div>
</a>
); );
} }

@ -71,6 +71,7 @@ export function UpdatePublicProfileForm() {
const [profileRoadmaps, setProfileRoadmaps] = useState<RoadmapType[]>([]); const [profileRoadmaps, setProfileRoadmaps] = useState<RoadmapType[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isProfileUpdated, setIsProfileUpdated] = useState(false);
const { isCopied, copyText } = useCopyText(); const { isCopied, copyText } = useCopyText();
@ -109,6 +110,7 @@ export function UpdatePublicProfileForm() {
await loadProfileSettings(); await loadProfileSettings();
toast.success('Profile updated successfully'); toast.success('Profile updated successfully');
setIsProfileUpdated(true);
}; };
const loadProfileSettings = async () => { const loadProfileSettings = async () => {
@ -593,6 +595,37 @@ export function UpdatePublicProfileForm() {
> >
{isLoading ? 'Please wait..' : 'Save Profile'} {isLoading ? 'Please wait..' : 'Save Profile'}
</button> </button>
{isProfileUpdated && publicProfileUrl && (
<div className="flex items-center gap-4">
<button
type="button"
className={cn(
'flex items-center justify-center gap-2 text-gray-500 underline underline-offset-2 hover:text-black hover:no-underline',
isCopied
? 'text-green-500 hover:text-green-600'
: 'text-gray-500',
)}
onClick={() => {
copyText(`${window.location.origin}${publicProfileUrl}`);
}}
>
{isCopied ? (
<CheckCircle className="size-4" />
) : (
<Copy className="size-4" />
)}
Copy Profile URL
</button>
<a
className="flex items-center justify-center gap-2 text-gray-500 underline underline-offset-2 hover:text-black hover:no-underline"
href={publicProfileUrl}
target="_blank"
>
<ArrowUpRight className="size-4" />
View Profile
</a>
</div>
)}
</form> </form>
</div> </div>
); );

@ -3,6 +3,7 @@ import {
Globe, Globe,
LinkedinIcon, LinkedinIcon,
Mail, Mail,
Pencil,
Twitter, Twitter,
} from 'lucide-react'; } from 'lucide-react';
import type { GetPublicProfileResponse } from '../../api/user'; import type { GetPublicProfileResponse } from '../../api/user';
@ -15,11 +16,12 @@ type UserPublicProfileHeaderProps = {
export function UserPublicProfileHeader(props: UserPublicProfileHeaderProps) { export function UserPublicProfileHeader(props: UserPublicProfileHeaderProps) {
const { userDetails } = props; const { userDetails } = props;
const { name, links, publicConfig, avatar, email } = userDetails; const { name, links, publicConfig, avatar, email, isOwnProfile } =
userDetails;
const { headline, isAvailableForHire, isEmailVisible } = publicConfig!; const { headline, isAvailableForHire, isEmailVisible } = publicConfig!;
return ( return (
<div className="container flex items-center gap-6 rounded-xl border bg-white p-8"> <div className="container relative flex items-center gap-6 rounded-xl border bg-white p-8">
<img <img
src={ src={
avatar avatar
@ -27,7 +29,7 @@ export function UserPublicProfileHeader(props: UserPublicProfileHeaderProps) {
: '/images/default-avatar.png' : '/images/default-avatar.png'
} }
alt={name} alt={name}
className="h-32 w-32 object-cover rounded-full" className="h-32 w-32 rounded-full object-cover"
/> />
<div> <div>
@ -51,6 +53,16 @@ export function UserPublicProfileHeader(props: UserPublicProfileHeaderProps) {
{isEmailVisible && <UserLink href={`mailto:${email}`} icon={Mail} />} {isEmailVisible && <UserLink href={`mailto:${email}`} icon={Mail} />}
</div> </div>
</div> </div>
{isOwnProfile && (
<a
href="/account/update-profile"
className="absolute right-4 top-4 flex items-center gap-1.5 text-sm text-gray-500 hover:text-black"
>
<Pencil className="h-3 w-3 stroke-2" />
Edit Profile
</a>
)}
</div> </div>
); );
} }

Loading…
Cancel
Save