diff --git a/src/api/user.ts b/src/api/user.ts index 8a677c744..08a4aa38a 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,11 +1,26 @@ import { type APIContext } from 'astro'; import { api } from './api.ts'; +export const allowedRoadmapVisibility = ['all', 'none', 'selected'] as const; +export type AllowedRoadmapVisibility = + (typeof allowedRoadmapVisibility)[number]; + +export const allowedCustomRoadmapVisibility = [ + 'all', + 'none', + 'selected', +] as const; +export type AllowedCustomRoadmapVisibility = + (typeof allowedCustomRoadmapVisibility)[number]; + +export const allowedProfileVisibility = ['public', 'private'] as const; +export type AllowedProfileVisibility = + (typeof allowedProfileVisibility)[number]; + export interface UserDocument { _id?: string; name: string; email: string; - username: string; avatar?: string; password: string; isEnabled: boolean; @@ -27,6 +42,15 @@ export interface UserDocument { twitter?: string; website?: string; }; + username?: string; + profileVisibility: AllowedProfileVisibility; + publicConfig?: { + headline: string; + roadmaps: string[]; + customRoadmaps: string[]; + roadmapVisibility: AllowedRoadmapVisibility; + customRoadmapVisibility: AllowedCustomRoadmapVisibility; + }; resetPasswordCodeAt: Date; verifiedAt: Date; createdAt: Date; diff --git a/src/components/RoadCard/SelectionButton.tsx b/src/components/RoadCard/SelectionButton.tsx index eb5c8e643..786e09b13 100644 --- a/src/components/RoadCard/SelectionButton.tsx +++ b/src/components/RoadCard/SelectionButton.tsx @@ -1,20 +1,25 @@ +import type { ButtonHTMLAttributes } from 'react'; +import { cn } from '../../lib/classname'; + type SelectionButtonProps = { text: string; isDisabled: boolean; isSelected: boolean; onClick: () => void; -}; +} & ButtonHTMLAttributes; export function SelectionButton(props: SelectionButtonProps) { - const { text, isDisabled, isSelected, onClick } = props; + const { text, isDisabled, isSelected, onClick, className, ...rest } = props; return ( diff --git a/src/components/UpdateProfile/UpdatePublicProfileForm.tsx b/src/components/UpdateProfile/UpdatePublicProfileForm.tsx new file mode 100644 index 000000000..1e7f78e98 --- /dev/null +++ b/src/components/UpdateProfile/UpdatePublicProfileForm.tsx @@ -0,0 +1,426 @@ +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 } from 'lucide-react'; +import { useToast } from '../../hooks/use-toast'; + +type RoadmapType = { + id: string; + title: string; + isCustomResource: boolean; +}; + +type GetProfileSettingsResponse = Pick< + UserDocument, + 'username' | 'profileVisibility' | 'publicConfig' | 'links' +>; + +export function UpdatePublicProfileForm() { + const [profileVisibility, setProfileVisibility] = + useState('private'); + + const toast = useToast(); + + const [headline, setHeadline] = useState(''); + const [username, setUsername] = useState(''); + const [roadmapVisibility, setRoadmapVisibility] = + useState('none'); + const [customRoadmapVisibility, setCustomRoadmapVisibility] = + useState('none'); + const [roadmaps, setRoadmaps] = useState([]); + const [customRoadmaps, setCustomRoadmaps] = useState([]); + + const [github, setGithub] = useState(''); + const [twitter, setTwitter] = useState(''); + const [linkedin, setLinkedin] = useState(''); + const [website, setWebsite] = useState(''); + + const [profileRoadmaps, setProfileRoadmaps] = useState([]); + + const [isLoading, setIsLoading] = useState(false); + const [success, setSuccess] = useState(''); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setSuccess(''); + + const { response, error } = await httpPatch( + `${import.meta.env.PUBLIC_API_URL}/v1-update-public-profile-config`, + { + profileVisibility, + headline, + username, + roadmapVisibility, + customRoadmapVisibility, + roadmaps, + customRoadmaps, + github, + twitter, + linkedin, + website, + }, + ); + + if (error || !response) { + setIsLoading(false); + toast.error(error?.message || 'Something went wrong'); + + return; + } + + await loadProfileSettings(); + setSuccess('Profile updated successfully'); + }; + + const loadProfileSettings = async () => { + setIsLoading(true); + + const { error, response } = await httpGet( + `${import.meta.env.PUBLIC_API_URL}/v1-get-profile-settings`, + ); + + if (error || !response) { + setIsLoading(false); + toast.error(error?.message || 'Something went wrong'); + + return; + } + + const { + links, + username, + profileVisibility: defaultProfileVisibility, + publicConfig, + } = response; + setUsername(username || ''); + setGithub(links?.github || ''); + setTwitter(links?.twitter || ''); + setLinkedin(links?.linkedin || ''); + setWebsite(links?.website || ''); + setProfileVisibility(defaultProfileVisibility || 'private'); + setHeadline(publicConfig?.headline || ''); + setRoadmapVisibility(publicConfig?.roadmapVisibility || 'none'); + setCustomRoadmapVisibility(publicConfig?.customRoadmapVisibility || 'none'); + setCustomRoadmaps(publicConfig?.customRoadmaps || []); + setRoadmaps(publicConfig?.roadmaps || []); + setCustomRoadmapVisibility(publicConfig?.customRoadmapVisibility || 'none'); + + 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); + + const publicProfileUrl = `/u/${username}`; + + return ( +
+
+
+

Public Profile

+ {profileVisibility === 'public' && username && ( + + + + )} +
+ +
+ setProfileVisibility('public')} + /> + setProfileVisibility('private')} + /> +
+
+ + {profileVisibility === 'public' && ( + <> +
+ + + setHeadline((e.target as HTMLInputElement).value) + } + required={profileVisibility === 'public'} + /> +
+
+ + + setUsername((e.target as HTMLInputElement).value) + } + required={profileVisibility === 'public'} + /> +
+ +
+

Show my Learning Activity

+
+ setRoadmapVisibility('all')} + /> + setRoadmapVisibility('none')} + /> +
+ +

+ Only Following Roadmaps +

+ {publicRoadmaps.length > 0 ? ( +
+ {publicRoadmaps.map((r) => ( + { + if (roadmapVisibility !== 'selected') { + setRoadmapVisibility('selected'); + } + + if (roadmaps.includes(r.id)) { + setRoadmaps(roadmaps.filter((id) => id !== r.id)); + } else { + setRoadmaps([...roadmaps, r.id]); + } + }} + /> + ))} +
+ ) : ( +

+ You are not following any roadmaps yet.{' '} + + Start following roadmaps + +

+ )} +
+ +
+

Show my Custom Roadmaps

+
+ setCustomRoadmapVisibility('all')} + /> + setCustomRoadmapVisibility('none')} + /> +
+ +

+ Only Following Roadmaps +

+ {publicCustomRoadmaps.length > 0 ? ( +
+ {publicCustomRoadmaps.map((r) => ( + { + if (customRoadmapVisibility !== 'selected') { + setCustomRoadmapVisibility('selected'); + } + + if (customRoadmaps.includes(r.id)) { + setCustomRoadmaps( + customRoadmaps.filter((id) => id !== r.id), + ); + } else { + setCustomRoadmaps([...customRoadmaps, r.id]); + } + }} + /> + ))} +
+ ) : ( +

+ You have not created any custom roadmaps yet.{' '} + + Create a custom roadmap + +

+ )} +
+ +
+ + setGithub((e.target as HTMLInputElement).value)} + /> +
+
+ + setTwitter((e.target as HTMLInputElement).value)} + /> +
+ +
+ + + setLinkedin((e.target as HTMLInputElement).value) + } + /> +
+ +
+ + setWebsite((e.target as HTMLInputElement).value)} + /> +
+ + )} + + {success && ( +

+ {success} +

+ )} + + +
+ ); +} diff --git a/src/pages/account/update-profile.astro b/src/pages/account/update-profile.astro index 3ad19b0dd..2db248e60 100644 --- a/src/pages/account/update-profile.astro +++ b/src/pages/account/update-profile.astro @@ -1,6 +1,7 @@ --- import AccountSidebar from '../../components/AccountSidebar.astro'; import { UpdateProfileForm } from '../../components/UpdateProfile/UpdateProfileForm'; +import { UpdatePublicProfileForm } from '../../components/UpdateProfile/UpdatePublicProfileForm'; import AccountLayout from '../../layouts/AccountLayout.astro'; --- @@ -11,5 +12,6 @@ import AccountLayout from '../../layouts/AccountLayout.astro'; > +