From cdf2ce6b11e83d95be1fcfceaaa18134d375f969 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Mon, 28 Jul 2025 15:17:58 +0100 Subject: [PATCH] Remove unused comments --- .../AccountStreak/AccountStreakHeatmap.tsx | 189 ----- src/components/Activity/ActivityCounters.tsx | 56 -- src/components/Activity/ActivityPage.tsx | 258 ------- src/components/Activity/EmptyActivity.tsx | 24 - src/components/CreateTeam/NotDropdown.tsx | 43 -- .../CreateVersion/CreateVersion.tsx | 145 ---- .../DeleteAccount/DeleteAccount.astro | 16 - .../DeleteAccount/DeleteAccountForm.tsx | 89 --- .../DeleteAccount/DeleteAccountPopup.astro | 17 - src/components/Friends/EmptyFriends.tsx | 52 -- src/components/Friends/FriendProgressItem.tsx | 337 --------- src/components/Friends/FriendsPage.tsx | 238 ------ src/components/Friends/InviteFriendPopup.tsx | 65 -- .../Notification/NotificationPage.tsx | 125 ---- .../PageSponsors/BottomRightSponsor.tsx | 104 --- src/components/PageSponsors/PageSponsors.tsx | 195 ----- .../PageSponsors/StickyTopSponsor.tsx | 91 --- .../ProfileSettings/ProfileSettingsPage.tsx | 57 -- src/components/ReactIcons/AcceptIcon.tsx | 24 - src/components/ReactIcons/AddUserIcon.tsx | 27 - src/components/ReactIcons/BookEmoji.tsx | 39 - src/components/ReactIcons/BuildEmoji.tsx | 36 - src/components/ReactIcons/BulbEmoji.tsx | 37 - src/components/ReactIcons/CheckEmoji.tsx | 6 - .../ReactIcons/ConstructionEmoji.tsx | 24 - src/components/ReactIcons/MailIcon.tsx | 23 - src/components/ReactIcons/RankBadgeIcon.tsx | 19 - src/components/ReactIcons/YouTubeIcon.tsx | 19 - .../RoadCard/GitHubReadmeBanner.tsx | 15 - src/components/RoadCard/RoadCardPage.tsx | 197 ----- src/components/RoadCard/RoadmapSelect.tsx | 78 -- src/components/RoadCard/SelectionButton.tsx | 40 - src/components/RoadCard/StepCounter.tsx | 17 - .../TeamMemberDetailsPage.tsx | 215 ------ .../TeamMemberDetails/TeamMemberEmptyPage.tsx | 22 - .../TeamMembers/LeaveTeamButton.tsx | 30 - src/components/TeamMembers/LeaveTeamPopup.tsx | 124 ---- .../TeamMembers/MemberActionDropdown.tsx | 109 --- src/components/TeamMembers/RoleBadge.tsx | 25 - src/components/TeamMembers/TeamMemberItem.tsx | 137 ---- .../TeamMembers/TeamMembersPage.tsx | 338 --------- .../TeamMembers/UpdateMemberPopup.tsx | 111 --- .../TeamRoadmap/CustomTeamRoadmap.tsx | 3 - .../TeamRoadmap/DefaultTeamRoadmap.tsx | 3 - .../RoadmapActionDropdown.tsx | 93 --- .../TeamRoadmapsList/TeamRoadmaps.tsx | 690 ------------------ .../TeamSettings/UpdateTeamForm.tsx | 321 -------- src/components/TeamVersions/TeamVersions.tsx | 230 ------ src/components/TopicSearch/TopicSearch.astro | 19 - src/components/TopicSearch/topics.js | 46 -- .../UpdateEmail/UpdateEmailForm.tsx | 245 ------- .../UpdatePassword/UpdatePasswordForm.tsx | 149 ---- .../UpdateProfile/ProfileUsername.tsx | 155 ---- .../UpdateProfile/SkillProfileAlert.tsx | 48 -- .../UpdateProfile/UpdateProfileForm.tsx | 154 ---- .../UpdateProfile/UpdatePublicProfileForm.tsx | 639 ---------------- .../UpdateProfile/UploadProfilePicture.tsx | 225 ------ .../UpdateProfile/VisibilityDropdown.tsx | 99 --- src/pages/account/billing.astro | 16 - src/pages/account/friends.astro | 16 - src/pages/account/index.astro | 16 - src/pages/account/notification.astro | 16 - src/pages/account/road-card.astro | 16 - src/pages/account/roadmaps.astro | 16 - src/pages/account/settings.astro | 20 - src/pages/account/update-profile.astro | 16 - src/pages/team/activity.astro | 15 - src/pages/team/index.astro | 69 -- src/pages/team/member.astro | 15 - src/pages/team/members.astro | 15 - src/pages/team/new.astro | 11 - src/pages/team/progress.astro | 15 - src/pages/team/roadmaps.astro | 11 - src/pages/team/settings.astro | 15 - 74 files changed, 7230 deletions(-) delete mode 100644 src/components/AccountStreak/AccountStreakHeatmap.tsx delete mode 100644 src/components/Activity/ActivityCounters.tsx delete mode 100644 src/components/Activity/ActivityPage.tsx delete mode 100644 src/components/Activity/EmptyActivity.tsx delete mode 100644 src/components/CreateTeam/NotDropdown.tsx delete mode 100644 src/components/CreateVersion/CreateVersion.tsx delete mode 100644 src/components/DeleteAccount/DeleteAccount.astro delete mode 100644 src/components/DeleteAccount/DeleteAccountForm.tsx delete mode 100644 src/components/DeleteAccount/DeleteAccountPopup.astro delete mode 100644 src/components/Friends/EmptyFriends.tsx delete mode 100644 src/components/Friends/FriendProgressItem.tsx delete mode 100644 src/components/Friends/FriendsPage.tsx delete mode 100644 src/components/Friends/InviteFriendPopup.tsx delete mode 100644 src/components/Notification/NotificationPage.tsx delete mode 100644 src/components/PageSponsors/BottomRightSponsor.tsx delete mode 100644 src/components/PageSponsors/PageSponsors.tsx delete mode 100644 src/components/PageSponsors/StickyTopSponsor.tsx delete mode 100644 src/components/ProfileSettings/ProfileSettingsPage.tsx delete mode 100644 src/components/ReactIcons/AcceptIcon.tsx delete mode 100644 src/components/ReactIcons/AddUserIcon.tsx delete mode 100644 src/components/ReactIcons/BookEmoji.tsx delete mode 100644 src/components/ReactIcons/BuildEmoji.tsx delete mode 100644 src/components/ReactIcons/BulbEmoji.tsx delete mode 100644 src/components/ReactIcons/CheckEmoji.tsx delete mode 100644 src/components/ReactIcons/ConstructionEmoji.tsx delete mode 100644 src/components/ReactIcons/MailIcon.tsx delete mode 100644 src/components/ReactIcons/RankBadgeIcon.tsx delete mode 100644 src/components/ReactIcons/YouTubeIcon.tsx delete mode 100644 src/components/RoadCard/GitHubReadmeBanner.tsx delete mode 100644 src/components/RoadCard/RoadCardPage.tsx delete mode 100644 src/components/RoadCard/RoadmapSelect.tsx delete mode 100644 src/components/RoadCard/SelectionButton.tsx delete mode 100644 src/components/RoadCard/StepCounter.tsx delete mode 100644 src/components/TeamMemberDetails/TeamMemberDetailsPage.tsx delete mode 100644 src/components/TeamMemberDetails/TeamMemberEmptyPage.tsx delete mode 100644 src/components/TeamMembers/LeaveTeamButton.tsx delete mode 100644 src/components/TeamMembers/LeaveTeamPopup.tsx delete mode 100644 src/components/TeamMembers/MemberActionDropdown.tsx delete mode 100644 src/components/TeamMembers/RoleBadge.tsx delete mode 100644 src/components/TeamMembers/TeamMemberItem.tsx delete mode 100644 src/components/TeamMembers/TeamMembersPage.tsx delete mode 100644 src/components/TeamMembers/UpdateMemberPopup.tsx delete mode 100644 src/components/TeamRoadmap/CustomTeamRoadmap.tsx delete mode 100644 src/components/TeamRoadmap/DefaultTeamRoadmap.tsx delete mode 100644 src/components/TeamRoadmapsList/RoadmapActionDropdown.tsx delete mode 100644 src/components/TeamRoadmapsList/TeamRoadmaps.tsx delete mode 100644 src/components/TeamSettings/UpdateTeamForm.tsx delete mode 100644 src/components/TeamVersions/TeamVersions.tsx delete mode 100644 src/components/TopicSearch/TopicSearch.astro delete mode 100644 src/components/TopicSearch/topics.js delete mode 100644 src/components/UpdateEmail/UpdateEmailForm.tsx delete mode 100644 src/components/UpdatePassword/UpdatePasswordForm.tsx delete mode 100644 src/components/UpdateProfile/ProfileUsername.tsx delete mode 100644 src/components/UpdateProfile/SkillProfileAlert.tsx delete mode 100644 src/components/UpdateProfile/UpdateProfileForm.tsx delete mode 100644 src/components/UpdateProfile/UpdatePublicProfileForm.tsx delete mode 100644 src/components/UpdateProfile/UploadProfilePicture.tsx delete mode 100644 src/components/UpdateProfile/VisibilityDropdown.tsx delete mode 100644 src/pages/account/billing.astro delete mode 100644 src/pages/account/friends.astro delete mode 100644 src/pages/account/index.astro delete mode 100644 src/pages/account/notification.astro delete mode 100644 src/pages/account/road-card.astro delete mode 100644 src/pages/account/roadmaps.astro delete mode 100644 src/pages/account/settings.astro delete mode 100644 src/pages/account/update-profile.astro delete mode 100644 src/pages/team/activity.astro delete mode 100644 src/pages/team/index.astro delete mode 100644 src/pages/team/member.astro delete mode 100644 src/pages/team/members.astro delete mode 100644 src/pages/team/new.astro delete mode 100644 src/pages/team/progress.astro delete mode 100644 src/pages/team/roadmaps.astro delete mode 100644 src/pages/team/settings.astro diff --git a/src/components/AccountStreak/AccountStreakHeatmap.tsx b/src/components/AccountStreak/AccountStreakHeatmap.tsx deleted file mode 100644 index 5d05e4df6..000000000 --- a/src/components/AccountStreak/AccountStreakHeatmap.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import CalendarHeatmap from 'react-calendar-heatmap'; -import dayjs from 'dayjs'; -import { formatActivityDate } from '../../lib/date'; -import { Tooltip as ReactTooltip } from 'react-tooltip'; -import 'react-calendar-heatmap/dist/styles.css'; -import './AccountStreakHeatmap.css'; - -const legends = [ - { count: 1, color: 'bg-slate-600' }, - { count: 3, color: 'bg-slate-500' }, - { count: 5, color: 'bg-slate-400' }, - { count: 10, color: 'bg-slate-300' }, - { count: 20, color: 'bg-slate-200' }, -]; - -type AccountStreakHeatmapProps = {}; - -export function AccountStreakHeatmap(props: AccountStreakHeatmapProps) { - const startDate = dayjs().subtract(6, 'months').toDate(); - const endDate = dayjs().toDate(); - - return ( -
- { - if (!value) { - return 'fill-slate-700 rounded-md [rx:2px] focus:outline-hidden'; - } - - const { count } = value; - if (count >= 20) { - return 'fill-slate-200 rounded-md [rx:2px] focus:outline-hidden'; - } else if (count >= 10) { - return 'fill-slate-300 rounded-md [rx:2px] focus:outline-hidden'; - } else if (count >= 5) { - return 'fill-slate-400 rounded-md [rx:2px] focus:outline-hidden'; - } else if (count >= 3) { - return 'fill-slate-500 rounded-md [rx:2px] focus:outline-hidden'; - } else { - return 'fill-slate-600 rounded-md [rx:2px] focus:outline-hidden'; - } - }} - tooltipDataAttrs={(value: any) => { - if (!value || !value.date) { - return null; - } - - const formattedDate = formatActivityDate(value.date); - return { - 'data-tooltip-id': 'user-activity-tip', - 'data-tooltip-content': `${value.count} Updates - ${formattedDate}`, - }; - }} - /> - - - -
-
- Less - {legends.map((legend) => ( -
-
-
- ))} - More - -
-
-
- ); -} diff --git a/src/components/Activity/ActivityCounters.tsx b/src/components/Activity/ActivityCounters.tsx deleted file mode 100644 index b7c11166d..000000000 --- a/src/components/Activity/ActivityCounters.tsx +++ /dev/null @@ -1,56 +0,0 @@ -type ActivityCountersType = { - done: { - today: number; - total: number; - }; - learning: { - today: number; - total: number; - }; - streak: { - count: number; - }; -}; - -type ActivityCounterType = { - text: string; - count: string; -}; - -function ActivityCounter(props: ActivityCounterType) { - const { text, count } = props; - - return ( -
-

- {count} -

-

{text}

-
- ); -} - -export function ActivityCounters(props: ActivityCountersType) { - const { done, learning, streak } = props; - - return ( -
-
- - - - - -
-
- ); -} diff --git a/src/components/Activity/ActivityPage.tsx b/src/components/Activity/ActivityPage.tsx deleted file mode 100644 index b6a95e87c..000000000 --- a/src/components/Activity/ActivityPage.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { useEffect, useState } from 'react'; -import { httpGet } from '../../lib/http'; -import { ActivityCounters } from './ActivityCounters'; -import { ResourceProgress } from './ResourceProgress'; -import { pageProgressMessage } from '../../stores/page'; -import { EmptyActivity } from './EmptyActivity'; -import { ActivityStream, type UserStreamActivity } from './ActivityStream'; -import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions'; -import type { PageType } from '../CommandMenu/CommandMenu'; -import { useToast } from '../../hooks/use-toast'; -import { ProjectProgress } from './ProjectProgress'; - -type ProgressResponse = { - updatedAt: string; - title: string; - id: string; - learning: number; - skipped: number; - done: number; - total: number; - isCustomResource: boolean; - roadmapSlug?: string; -}; - -export type ActivityResponse = { - done: { - today: number; - total: number; - }; - learning: { - today: number; - total: number; - roadmaps: ProgressResponse[]; - bestPractices: ProgressResponse[]; - customs: ProgressResponse[]; - }; - streak: { - count: number; - firstVisitAt: Date | null; - lastVisitAt: Date | null; - }; - activity: { - type: 'done' | 'learning' | 'pending' | 'skipped'; - createdAt: Date; - metadata: { - resourceId?: string; - resourceType?: 'roadmap' | 'best-practice'; - topicId?: string; - topicLabel?: string; - resourceTitle?: string; - }; - }[]; - activities: UserStreamActivity[]; - projects: ProjectStatusDocument[]; -}; - -export function ActivityPage() { - const toast = useToast(); - const [activity, setActivity] = useState(); - const [isLoading, setIsLoading] = useState(true); - const [projectDetails, setProjectDetails] = useState([]); - - async function loadActivity() { - const { error, response } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-user-stats`, - ); - - if (!response || error) { - console.error('Error loading activity'); - console.error(error); - - return; - } - - setActivity(response); - } - - async function loadAllProjectDetails() { - const { error, response } = await httpGet(`/pages.json`); - - if (error) { - toast.error(error.message || 'Something went wrong'); - return; - } - - if (!response) { - return []; - } - - const allProjects = response.filter((page) => page.group === 'Projects'); - setProjectDetails(allProjects); - } - - useEffect(() => { - Promise.allSettled([loadActivity(), loadAllProjectDetails()]).finally( - () => { - pageProgressMessage.set(''); - setIsLoading(false); - }, - ); - }, []); - - const learningRoadmaps = activity?.learning.roadmaps || []; - const learningBestPractices = activity?.learning.bestPractices || []; - - if (isLoading) { - return null; - } - - const learningRoadmapsToShow = learningRoadmaps - .sort((a, b) => { - const updatedAtA = new Date(a.updatedAt); - const updatedAtB = new Date(b.updatedAt); - - return updatedAtB.getTime() - updatedAtA.getTime(); - }) - .filter((roadmap) => roadmap.learning > 0 || roadmap.done > 0); - - const learningBestPracticesToShow = learningBestPractices - .sort((a, b) => { - const updatedAtA = new Date(a.updatedAt); - const updatedAtB = new Date(b.updatedAt); - - return updatedAtB.getTime() - updatedAtA.getTime(); - }) - .filter( - (bestPractice) => bestPractice.learning > 0 || bestPractice.done > 0, - ); - - const hasProgress = - learningRoadmapsToShow.length !== 0 || - learningBestPracticesToShow.length !== 0; - - const enrichedProjects = activity?.projects.map((project) => { - const projectDetail = projectDetails.find( - (page) => page.id === project.projectId, - ); - - return { - ...project, - title: projectDetail?.title || 'N/A', - }; - }); - - return ( - <> - - -
- {learningRoadmapsToShow.length === 0 && - learningBestPracticesToShow.length === 0 && } - - {(learningRoadmapsToShow.length > 0 || - learningBestPracticesToShow.length > 0) && ( - <> -

- Continue Following -

-
- {learningRoadmaps - .sort((a, b) => { - const updatedAtA = new Date(a.updatedAt); - const updatedAtB = new Date(b.updatedAt); - - return updatedAtB.getTime() - updatedAtA.getTime(); - }) - .filter((roadmap) => roadmap.learning > 0 || roadmap.done > 0) - .map((roadmap) => { - const learningCount = roadmap.learning || 0; - const doneCount = roadmap.done || 0; - const totalCount = roadmap.total || 0; - const skippedCount = roadmap.skipped || 0; - - return ( - totalCount ? totalCount : doneCount - } - learningCount={ - learningCount > totalCount ? totalCount : learningCount - } - totalCount={totalCount} - skippedCount={skippedCount} - resourceId={roadmap.id} - resourceType={'roadmap'} - updatedAt={roadmap.updatedAt} - title={roadmap.title} - onCleared={() => { - pageProgressMessage.set('Updating activity'); - loadActivity().finally(() => { - pageProgressMessage.set(''); - }); - }} - /> - ); - })} - - {learningBestPractices - .sort((a, b) => { - const updatedAtA = new Date(a.updatedAt); - const updatedAtB = new Date(b.updatedAt); - - return updatedAtB.getTime() - updatedAtA.getTime(); - }) - .filter( - (bestPractice) => - bestPractice.learning > 0 || bestPractice.done > 0, - ) - .map((bestPractice) => ( - { - pageProgressMessage.set('Updating activity'); - loadActivity().finally(() => { - pageProgressMessage.set(''); - }); - }} - /> - ))} -
- - )} -
- - {enrichedProjects && enrichedProjects?.length > 0 && ( -
-

- Your Projects -

-
- {enrichedProjects.map((project) => ( - - ))} -
-
- )} - - {hasProgress && ( - - )} - - ); -} diff --git a/src/components/Activity/EmptyActivity.tsx b/src/components/Activity/EmptyActivity.tsx deleted file mode 100644 index 78daf8b25..000000000 --- a/src/components/Activity/EmptyActivity.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { RoadmapIcon } from "../ReactIcons/RoadmapIcon"; - -export function EmptyActivity() { - return ( -
-
- - -

No Progress

-

- Progress will appear here as you start tracking your{' '} - - Roadmaps - {' '} - or{' '} - - Best Practices - {' '} - progress. -

-
-
- ); -} diff --git a/src/components/CreateTeam/NotDropdown.tsx b/src/components/CreateTeam/NotDropdown.tsx deleted file mode 100644 index 66259d567..000000000 --- a/src/components/CreateTeam/NotDropdown.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { ChevronDownIcon } from '../ReactIcons/ChevronDownIcon'; - -type NotDropdownProps = { - onClick: () => void; - selectedCount: number; - singularName: string; - pluralName: string; -}; - -export function NotDropdown(props: NotDropdownProps) { - const { onClick, selectedCount, singularName, pluralName } = props; - - const singularOrPlural = selectedCount === 1 ? singularName : pluralName; - - return ( -
- {selectedCount > 0 && ( -
-

- {selectedCount} {singularOrPlural} selected -

-

- Click to add or change selection -

-
- )} - - {selectedCount === 0 && ( -
-

- Click to select {pluralName} -

-
- )} - - -
- ); -} diff --git a/src/components/CreateVersion/CreateVersion.tsx b/src/components/CreateVersion/CreateVersion.tsx deleted file mode 100644 index 075886182..000000000 --- a/src/components/CreateVersion/CreateVersion.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useEffect, useState } from 'react'; -import { httpGet, httpPost } from '../../lib/http'; -import { useToast } from '../../hooks/use-toast'; -import { isLoggedIn } from '../../lib/jwt'; -import { GitFork, Loader2, Map } from 'lucide-react'; -import { showLoginPopup } from '../../lib/popup'; -import type { RoadmapDocument } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx'; - -type CreateVersionProps = { - roadmapId: string; -}; - -export function CreateVersion(props: CreateVersionProps) { - const { roadmapId } = props; - - const toast = useToast(); - const [isLoading, setIsLoading] = useState(true); - const [isCreating, setIsCreating] = useState(false); - const [isConfirming, setIsConfirming] = useState(false); - const [userVersion, setUserVersion] = useState(); - - async function loadMyVersion() { - if (!isLoggedIn()) { - return; - } - - setIsLoading(true); - const { response, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-my-version/${roadmapId}`, - {}, - ); - - if (error || !response) { - setIsLoading(false); - return; - } - - setIsLoading(false); - setUserVersion(response); - } - - useEffect(() => { - loadMyVersion().finally(() => { - setIsLoading(false); - }); - }, []); - - async function createVersion() { - if (isCreating || !roadmapId) { - return; - } - - if (!isLoggedIn()) { - showLoginPopup(); - return; - } - - setIsCreating(true); - const { response, error } = await httpPost<{ roadmapId: string }>( - `${import.meta.env.PUBLIC_API_URL}/v1-create-version/${roadmapId}`, - {}, - ); - - if (error || !response) { - setIsCreating(false); - toast.error(error?.message || 'Failed to create version'); - return; - } - - window.location.href = `${ - import.meta.env.PUBLIC_EDITOR_APP_URL - }/${response?.roadmapId}`; - } - - if (isLoading) { - return ( -
- ); - } - - if (!isLoading && userVersion?._id) { - return ( - - ); - } - - if (isConfirming) { - return ( -

- Create and edit a custom roadmap from this roadmap? - -  /  - -

- ); - } - - return ( - - ); -} diff --git a/src/components/DeleteAccount/DeleteAccount.astro b/src/components/DeleteAccount/DeleteAccount.astro deleted file mode 100644 index ffe8e2b7e..000000000 --- a/src/components/DeleteAccount/DeleteAccount.astro +++ /dev/null @@ -1,16 +0,0 @@ ---- -import DeleteAccountPopup from "./DeleteAccountPopup.astro"; ---- - - -

Delete Account

-

- Permanently remove your account from the roadmap.sh. This cannot be undone and all your progress and data will be lost. -

- - diff --git a/src/components/DeleteAccount/DeleteAccountForm.tsx b/src/components/DeleteAccount/DeleteAccountForm.tsx deleted file mode 100644 index c448c7aa5..000000000 --- a/src/components/DeleteAccount/DeleteAccountForm.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { type FormEvent, useEffect, useState } from 'react'; -import { httpDelete } from '../../lib/http'; -import { logout } from '../../lib/auth'; - -export function DeleteAccountForm() { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const [confirmationText, setConfirmationText] = useState(''); - - useEffect(() => { - setError(''); - setConfirmationText(''); - }, []); - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setError(''); - - if (confirmationText.toUpperCase() !== 'DELETE') { - setError('Verification text does not match'); - setIsLoading(false); - return; - } - - const { response, error } = await httpDelete( - `${import.meta.env.PUBLIC_API_URL}/v1-delete-account`, - ); - - if (error || !response) { - setIsLoading(false); - setError(error?.message || 'Something went wrong'); - return; - } - - logout(); - }; - - const handleClosePopup = () => { - setIsLoading(false); - setError(''); - setConfirmationText(''); - - const deleteAccountPopup = document.getElementById('delete-account-popup'); - deleteAccountPopup?.classList.add('hidden'); - deleteAccountPopup?.classList.remove('flex'); - }; - - return ( -
-
- - setConfirmationText((e.target as HTMLInputElement).value) - } - /> - {error && ( -

{error}

- )} -
- -
- - -
-
- ); -} diff --git a/src/components/DeleteAccount/DeleteAccountPopup.astro b/src/components/DeleteAccount/DeleteAccountPopup.astro deleted file mode 100644 index 004896cac..000000000 --- a/src/components/DeleteAccount/DeleteAccountPopup.astro +++ /dev/null @@ -1,17 +0,0 @@ ---- -import Popup from '../Popup/Popup.astro'; -import { DeleteAccountForm } from './DeleteAccountForm'; ---- - - -
-

- This will permanently delete your account and all your associated data - including your progress. -

- -

Please type "delete" to confirm.

- - -
-
diff --git a/src/components/Friends/EmptyFriends.tsx b/src/components/Friends/EmptyFriends.tsx deleted file mode 100644 index 601a05d77..000000000 --- a/src/components/Friends/EmptyFriends.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useCopyText } from '../../hooks/use-copy-text'; -import { CopyIcon, UserPlus2 } from 'lucide-react'; - -type EmptyFriendsProps = { - befriendUrl: string; -}; - -export function EmptyFriends(props: EmptyFriendsProps) { - const { befriendUrl } = props; - const { isCopied, copyText } = useCopyText(); - - return ( -
-
- - -

Invite your Friends

-

- Share the unique link below with your friends to track their skills - and progress. -

- -
- { - e.currentTarget.select(); - copyText(befriendUrl); - }} - type="text" - value={befriendUrl} - className="w-full border-none bg-transparent px-1.5 outline-hidden" - readOnly - /> - -
-
-
- ); -} diff --git a/src/components/Friends/FriendProgressItem.tsx b/src/components/Friends/FriendProgressItem.tsx deleted file mode 100644 index a19e9dff5..000000000 --- a/src/components/Friends/FriendProgressItem.tsx +++ /dev/null @@ -1,337 +0,0 @@ -import { useState } from 'react'; -import type { ListFriendsResponse } from './FriendsPage'; -import { DeleteUserIcon } from '../ReactIcons/DeleteUserIcon'; -import { pageProgressMessage } from '../../stores/page'; -import { httpDelete, httpPost } from '../../lib/http'; -import { useToast } from '../../hooks/use-toast'; -import { TrashIcon } from '../ReactIcons/TrashIcon'; -import { AddedUserIcon } from '../ReactIcons/AddedUserIcon'; -import { AddUserIcon } from '../ReactIcons/AddUserIcon'; -import type { AllowedRoadmapRenderer } from '../../lib/roadmap'; - -type FriendProgressItemProps = { - friend: ListFriendsResponse[0]; - onShowResourceProgress: ( - resourceId: string, - isCustomResource?: boolean, - renderer?: AllowedRoadmapRenderer, - ) => void; - onReload: () => void; -}; - -export function FriendProgressItem(props: FriendProgressItemProps) { - const { friend, onShowResourceProgress, onReload } = props; - const toast = useToast(); - const [isConfirming, setIsConfirming] = - useState(); - - async function deleteFriend(userId: string, successMessage: string) { - pageProgressMessage.set('Please wait...'); - const { response, error } = await httpDelete( - `${import.meta.env.PUBLIC_API_URL}/v1-delete-friend/${userId}`, - {}, - ); - - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - return; - } - - toast.success(successMessage); - onReload(); - } - - async function addFriend(userId: string, successMessage: string) { - pageProgressMessage.set('Please wait...'); - const { response, error } = await httpPost( - `${import.meta.env.PUBLIC_API_URL}/v1-add-friend/${userId}`, - {}, - ); - - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - return; - } - - toast.success(successMessage); - onReload(); - } - - const roadmaps = (friend?.roadmaps || []).sort((a, b) => { - return b.done - a.done; - }); - - const [showAll, setShowAll] = useState(false); - const status = friend.status; - - return ( - <> -
-
- {friend.name -
-

{friend.name}

-

{friend.email}

-
-
- {friend.status === 'accepted' && ( - <> -
- {(showAll ? roadmaps : roadmaps.slice(0, 4)).map((progress) => { - return ( - - ); - })} - - {roadmaps.length > 4 && !showAll && ( - - )} - - {showAll && ( - - )} - - {roadmaps.length === 0 && ( -
No progress
- )} -
- <> - {isConfirming !== 'accepted' && ( - - )} - - {isConfirming === 'accepted' && ( - - Are you sure?{' '} - {' '} - - - )} - - - )} - - {friend.status === 'rejected' && ( - <> -
- - - Request Rejected - -
- - Changed your mind?{' '} - - - - )} - - {friend.status === 'got_rejected' && ( - <> -
- - - Request Rejected - -
- - - - - )} - - {friend.status === 'sent' && ( - <> -
- - - Request Sent - -
- <> - {isConfirming !== 'sent' && ( - - )} - - {isConfirming === 'sent' && ( - - Are you sure?{' '} - {' '} - - - )} - - - )} - - {friend.status === 'received' && ( - <> -
- - Request Received - - - - -
- - )} -
- - ); -} diff --git a/src/components/Friends/FriendsPage.tsx b/src/components/Friends/FriendsPage.tsx deleted file mode 100644 index 00c939feb..000000000 --- a/src/components/Friends/FriendsPage.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { useEffect, useState } from 'react'; -import { pageProgressMessage } from '../../stores/page'; -import { useAuth } from '../../hooks/use-auth'; -import { AddUserIcon } from '../ReactIcons/AddUserIcon'; -import { httpGet } from '../../lib/http'; -import type { FriendshipStatus } from '../Befriend'; -import { useToast } from '../../hooks/use-toast'; -import { EmptyFriends } from './EmptyFriends'; -import { FriendProgressItem } from './FriendProgressItem'; -import { UserProgressModal } from '../UserProgress/UserProgressModal'; -import { InviteFriendPopup } from './InviteFriendPopup'; -import { UserCustomProgressModal } from '../UserProgress/UserCustomProgressModal'; -import { UserIcon } from 'lucide-react'; -import type { AllowedRoadmapRenderer } from '../../lib/roadmap'; - -type FriendResourceProgress = { - updatedAt: string; - title: string; - resourceId: string; - resourceType: string; - isCustomResource: boolean; - learning: number; - skipped: number; - done: number; - total: number; - renderer?: AllowedRoadmapRenderer; -}; - -export type ListFriendsResponse = { - userId: string; - name: string; - email: string; - avatar: string; - status: FriendshipStatus; - roadmaps: FriendResourceProgress[]; - bestPractices: FriendResourceProgress[]; -}[]; - -type GroupingType = { - label: string; - value: 'active' | 'requests' | 'sent'; - statuses: FriendshipStatus[]; -}; - -const groupingTypes: GroupingType[] = [ - { label: 'Active', value: 'active', statuses: ['accepted'] }, - { label: 'Requests', value: 'requests', statuses: ['received', 'rejected'] }, - { label: 'Sent', value: 'sent', statuses: ['sent', 'got_rejected'] }, -]; - -export function FriendsPage() { - const toast = useToast(); - - const [showInviteFriendPopup, setShowInviteFriendPopup] = useState(false); - - const [showFriendProgress, setShowFriendProgress] = useState<{ - resourceId: string; - friend: ListFriendsResponse[0]; - isCustomResource?: boolean; - renderer?: AllowedRoadmapRenderer; - }>(); - - const [isLoading, setIsLoading] = useState(true); - const [friends, setFriends] = useState([]); - const [selectedGrouping, setSelectedGrouping] = - useState('active'); - - async function loadFriends() { - const { response, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-list-friends`, - ); - - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - return; - } - - setFriends(response); - } - - useEffect(() => { - loadFriends().finally(() => { - pageProgressMessage.set(''); - setIsLoading(false); - }); - }, []); - - const user = useAuth(); - const baseUrl = import.meta.env.DEV - ? 'http://localhost:3000' - : 'https://roadmap.sh'; - const befriendUrl = `${baseUrl}/befriend?u=${user?.id}`; - - const selectedGroupingType = groupingTypes.find( - (grouping) => grouping.value === selectedGrouping, - ); - - const filteredFriends = friends.filter((friend) => - selectedGroupingType?.statuses.includes(friend.status), - ); - - const receivedRequests = friends.filter( - (friend) => friend.status === 'received', - ); - - if (isLoading) { - return null; - } - - if (!friends?.length) { - return ; - } - - const progressModal = - showFriendProgress && showFriendProgress?.isCustomResource ? ( - setShowFriendProgress(undefined)} - /> - ) : ( - setShowFriendProgress(undefined)} - isCustomResource={showFriendProgress?.isCustomResource} - renderer={showFriendProgress?.renderer} - /> - ); - - return ( -
- {showInviteFriendPopup && ( - setShowInviteFriendPopup(false)} - /> - )} - - {showFriendProgress && progressModal} - -
-
- {groupingTypes.map((grouping) => { - let requestCount = 0; - if (grouping.value === 'requests') { - requestCount = receivedRequests.length; - } - - return ( - - ); - })} -
- -
- - {filteredFriends.length > 0 && ( -
- {filteredFriends.map((friend) => ( - { - setShowFriendProgress({ - resourceId, - friend, - isCustomResource, - renderer, - }); - }} - key={friend.userId} - onReload={() => { - pageProgressMessage.set('Reloading friends ..'); - loadFriends().finally(() => { - pageProgressMessage.set(''); - }); - }} - /> - ))} -
- )} - - {filteredFriends.length === 0 && ( -
- - -

- {selectedGrouping === 'active' && 'No friends yet'} - {selectedGrouping === 'sent' && 'No requests sent'} - {selectedGrouping === 'requests' && 'No requests received'} -

-

- Invite your friends to join you on Roadmap -

- -
- )} -
- ); -} diff --git a/src/components/Friends/InviteFriendPopup.tsx b/src/components/Friends/InviteFriendPopup.tsx deleted file mode 100644 index 8104c30f2..000000000 --- a/src/components/Friends/InviteFriendPopup.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import type { MouseEvent } from 'react'; -import { useRef } from 'react'; -import { useOutsideClick } from '../../hooks/use-outside-click'; -import { useCopyText } from '../../hooks/use-copy-text'; -import { CopyIcon } from 'lucide-react'; - -type InviteFriendPopupProps = { - befriendUrl: string; - onClose: () => void; -}; - -export function InviteFriendPopup(props: InviteFriendPopupProps) { - const { onClose, befriendUrl } = props; - - const { isCopied, copyText } = useCopyText(); - - const popupBodyRef = useRef(null); - - const handleClosePopup = () => { - onClose(); - }; - - useOutsideClick(popupBodyRef, handleClosePopup); - - return ( -
-
-
-

Invite URL

-

- Share the link below with your friends to invite them. -

- -
- ) => { - (e?.target as HTMLInputElement)?.select(); - copyText(befriendUrl); - }} - /> - -
-
-
-
- ); -} diff --git a/src/components/Notification/NotificationPage.tsx b/src/components/Notification/NotificationPage.tsx deleted file mode 100644 index ea6b071e7..000000000 --- a/src/components/Notification/NotificationPage.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useEffect, useState } from 'react'; -import { httpGet, httpPatch } from '../../lib/http'; -import { pageProgressMessage } from '../../stores/page'; -import type { TeamMemberDocument } from '../TeamMembers/TeamMembersPage'; -import { useToast } from '../../hooks/use-toast'; -import { AcceptIcon } from '../ReactIcons/AcceptIcon.tsx'; -import { XIcon } from 'lucide-react'; - -interface NotificationList extends TeamMemberDocument { - name: string; -} - -export function NotificationPage() { - const toast = useToast(); - const [isLoading, setIsLoading] = useState(false); - const [notifications, setNotifications] = useState([]); - const [error, setError] = useState(''); - - const lostNotifications = async () => { - const { error, response } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-invitation-list`, - ); - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - return; - } - - setNotifications(response); - }; - - async function respondInvitation( - status: 'accept' | 'reject', - inviteId: string, - ) { - setIsLoading(true); - setError(''); - const { response, error } = await httpPatch<{ teamId: string }>( - `${import.meta.env.PUBLIC_API_URL}/v1-respond-invite/${inviteId}`, - { - status, - }, - ); - if (error || !response) { - setError(error?.message || 'Something went wrong'); - setIsLoading(false); - return; - } - - if (status === 'accept') { - window.location.href = `/team/activity?t=${response.teamId}`; - } else { - window.dispatchEvent( - new CustomEvent('refresh-notification', { - detail: { - count: notifications.length - 1, - }, - }), - ); - setNotifications( - notifications.filter((notification) => notification._id !== inviteId), - ); - setIsLoading(false); - } - } - - useEffect(() => { - lostNotifications().finally(() => { - pageProgressMessage.set(''); - }); - }, []); - - return ( -
-
-

Notification

-

Manage your notifications

-
- {notifications.length === 0 && ( -
-

- No notifications, you can{' '} - - create a team - {' '} - and invite your friends to join. -

-
- )} -
- {notifications.map((notification) => ( -
-
-
-

- {notification.name} -

-
-
-
- - -
-
- ))} -
-
- ); -} diff --git a/src/components/PageSponsors/BottomRightSponsor.tsx b/src/components/PageSponsors/BottomRightSponsor.tsx deleted file mode 100644 index 00560c973..000000000 --- a/src/components/PageSponsors/BottomRightSponsor.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useEffect, useState } from 'react'; -import { httpGet, httpPatch, httpPost } from '../../lib/http'; -import { sponsorHidden } from '../../stores/page'; -import { useStore } from '@nanostores/react'; -import { X } from 'lucide-react'; -import { setViewSponsorCookie } from '../../lib/jwt'; -import { isMobile } from '../../lib/is-mobile'; -import Cookies from 'js-cookie'; -import { getUrlUtmParams } from '../../lib/browser.ts'; - -export type BottomRightSponsorType = { - id: string; - company: string; - description: string; - gaLabel: string; - imageUrl: string; - pageUrl: string; - title: string; - url: string; -}; - -type V1GetSponsorResponse = { - id?: string; - href?: string; - sponsor?: BottomRightSponsorType; -}; - -type BottomRightSponsorProps = { - sponsor: BottomRightSponsorType; - - onSponsorClick: () => void; - onSponsorImpression: () => void; - onSponsorHidden: () => void; -}; - -export function BottomRightSponsor(props: BottomRightSponsorProps) { - const { sponsor, onSponsorImpression, onSponsorClick, onSponsorHidden } = - props; - - const [isHidden, setIsHidden] = useState(false); - - useEffect(() => { - if (!sponsor) { - return; - } - - onSponsorImpression(); - }, []); - - const { url, title, imageUrl, description, company, gaLabel } = sponsor; - - const isRoadmapAd = title.toLowerCase() === 'advertise with us!'; - - if (isHidden) { - return null; - } - - return ( - - { - e.preventDefault(); - e.stopPropagation(); - - setIsHidden(true); - onSponsorHidden(); - }} - > - - - - Sponsor Banner - - - - {title} - {description} - - {!isRoadmapAd && ( - <> - - Partner Content - - - Partner Content - - - )} - - - ); -} diff --git a/src/components/PageSponsors/PageSponsors.tsx b/src/components/PageSponsors/PageSponsors.tsx deleted file mode 100644 index cee656e07..000000000 --- a/src/components/PageSponsors/PageSponsors.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { useEffect, useState } from 'react'; -import { httpGet, httpPatch } from '../../lib/http'; -import { sponsorHidden } from '../../stores/page'; -import { useStore } from '@nanostores/react'; -import { setViewSponsorCookie } from '../../lib/jwt'; -import { isMobile } from '../../lib/is-mobile'; -import Cookies from 'js-cookie'; -import { getUrlUtmParams } from '../../lib/browser.ts'; -import { StickyTopSponsor } from './StickyTopSponsor.tsx'; -import { BottomRightSponsor } from './BottomRightSponsor.tsx'; - -type PageSponsorType = { - company: string; - description: string; - gaLabel: string; - imageUrl: string; - pageUrl: string; - title: string; - url: string; - id: string; -}; - -export type StickyTopSponsorType = PageSponsorType & { - buttonText: string; - style?: { - fromColor?: string; - toColor?: string; - textColor?: string; - buttonBackgroundColor?: string; - buttonTextColor?: string; - }; -}; -export type BottomRightSponsorType = PageSponsorType; - -type V1GetSponsorResponse = { - bottomRightAd?: BottomRightSponsorType; - stickyTopAd?: StickyTopSponsorType; -}; - -type PageSponsorsProps = { - gaPageIdentifier?: string; -}; - -const CLOSE_SPONSOR_KEY = 'sponsorClosed'; - -function markSponsorHidden(sponsorId: string) { - Cookies.set(`${CLOSE_SPONSOR_KEY}-${sponsorId}`, '1', { - path: '/', - expires: 1, - sameSite: 'lax', - secure: true, - domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', - }); -} - -function isSponsorMarkedHidden(sponsorId: string) { - return Cookies.get(`${CLOSE_SPONSOR_KEY}-${sponsorId}`) === '1'; -} - -export function PageSponsors(props: PageSponsorsProps) { - const { gaPageIdentifier } = props; - - const $isSponsorHidden = useStore(sponsorHidden); - - const [stickyTopSponsor, setStickyTopSponsor] = - useState(); - const [bottomRightSponsor, setBottomRightSponsor] = - useState(); - - useEffect(() => { - const foundUtmParams = getUrlUtmParams(); - - if (!foundUtmParams.utmSource) { - return; - } - - localStorage.setItem('utm_params', JSON.stringify(foundUtmParams)); - }, []); - - async function loadSponsor() { - const currentPath = window.location.pathname; - if ( - currentPath === '/' || - currentPath === '/best-practices' || - currentPath === '/roadmaps' || - currentPath.startsWith('/guides') || - currentPath.startsWith('/videos') || - currentPath.startsWith('/account') || - currentPath.startsWith('/team/') - ) { - return; - } - - const { response, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-sponsor`, - { - href: window.location.pathname, - mobile: isMobile() ? 'true' : 'false', - }, - ); - - if (error) { - console.error(error); - return; - } - - setStickyTopSponsor(response?.stickyTopAd); - setBottomRightSponsor(response?.bottomRightAd); - } - - async function logSponsorImpression( - sponsor: BottomRightSponsorType | StickyTopSponsorType, - ) { - window.fireEvent({ - category: 'SponsorImpression', - action: `${sponsor?.company} Impression`, - label: - sponsor?.gaLabel || `${gaPageIdentifier} / ${sponsor?.company} Link`, - }); - } - - async function clickSponsor( - sponsor: BottomRightSponsorType | StickyTopSponsorType, - ) { - const { id: sponsorId, company, gaLabel } = sponsor; - - const labelValue = gaLabel || `${gaPageIdentifier} / ${company} Link`; - - window.fireEvent({ - category: 'SponsorClick', - action: `${company} Redirect`, - label: labelValue, - value: labelValue, - }); - - const clickUrl = new URL( - `${import.meta.env.PUBLIC_API_URL}/v1-view-sponsor/${sponsorId}`, - ); - - const { response, error } = await httpPatch<{ status: 'ok' }>( - clickUrl.toString(), - { - mobile: isMobile(), - }, - ); - - if (error || !response) { - console.error(error); - return; - } - - setViewSponsorCookie(sponsorId); - } - - useEffect(() => { - window.setTimeout(loadSponsor); - }, []); - - if ($isSponsorHidden) { - return null; - } - - return ( -
- {stickyTopSponsor && !isSponsorMarkedHidden(stickyTopSponsor.id) && ( - { - logSponsorImpression(stickyTopSponsor).catch(console.error); - }} - onSponsorClick={() => { - clickSponsor(stickyTopSponsor).catch(console.error); - }} - onSponsorHidden={() => { - markSponsorHidden(stickyTopSponsor.id); - }} - /> - )} - {bottomRightSponsor && !isSponsorMarkedHidden(bottomRightSponsor.id) && ( - { - clickSponsor(bottomRightSponsor).catch(console.error); - }} - onSponsorHidden={() => { - markSponsorHidden(bottomRightSponsor.id); - }} - onSponsorImpression={() => { - logSponsorImpression(bottomRightSponsor).catch(console.error); - }} - /> - )} -
- ); -} diff --git a/src/components/PageSponsors/StickyTopSponsor.tsx b/src/components/PageSponsors/StickyTopSponsor.tsx deleted file mode 100644 index 3f22404cb..000000000 --- a/src/components/PageSponsors/StickyTopSponsor.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { cn } from '../../lib/classname.ts'; -import { useScrollPosition } from '../../hooks/use-scroll-position.ts'; -import { X } from 'lucide-react'; -import type { StickyTopSponsorType } from './PageSponsors.tsx'; -import { useEffect, useState } from 'react'; -import { isOnboardingStripHidden } from '../../stores/page.ts'; - -type StickyTopSponsorProps = { - sponsor: StickyTopSponsorType; - - onSponsorImpression: () => void; - onSponsorClick: () => void; - onSponsorHidden: () => void; -}; - -const SCROLL_DISTANCE = 100; - -export function StickyTopSponsor(props: StickyTopSponsorProps) { - const { sponsor, onSponsorHidden, onSponsorImpression, onSponsorClick } = - props; - - const { y: scrollY } = useScrollPosition(); - const [isImpressionLogged, setIsImpressionLogged] = useState(false); - const [isHidden, setIsHidden] = useState(false); - - useEffect(() => { - if (!sponsor) { - return; - } - - // preload the image so that we don't see a flicker - const img = new Image(); - img.src = sponsor.imageUrl; - - // hide the onboarding strip when the sponsor is visible - isOnboardingStripHidden.set(true); - }, [sponsor]); - - useEffect(() => { - if (scrollY < SCROLL_DISTANCE || isImpressionLogged) { - return; - } - - setIsImpressionLogged(true); - onSponsorImpression(); - }, [scrollY]); - - if (scrollY < SCROLL_DISTANCE || isHidden) { - return null; - } - - return ( - - {'ad'} - {sponsor.description} - - - - ); -} diff --git a/src/components/ProfileSettings/ProfileSettingsPage.tsx b/src/components/ProfileSettings/ProfileSettingsPage.tsx deleted file mode 100644 index d4203bd00..000000000 --- a/src/components/ProfileSettings/ProfileSettingsPage.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useEffect, useState } from 'react'; -import { UpdateEmailForm } from '../UpdateEmail/UpdateEmailForm'; -import UpdatePasswordForm from '../UpdatePassword/UpdatePasswordForm'; -import { pageProgressMessage } from '../../stores/page'; -import { httpGet } from '../../lib/http'; -import { useToast } from '../../hooks/use-toast'; - -export function ProfileSettingsPage() { - const toast = useToast(); - - const [authProvider, setAuthProvider] = useState(''); - const [currentEmail, setCurrentEmail] = useState(''); - const [newEmail, setNewEmail] = useState(''); - - const loadProfile = async () => { - const { error, response } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-me`, - ); - - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - - return; - } - - const { authProvider, email, newEmail } = response; - setAuthProvider(authProvider); - setCurrentEmail(email); - setNewEmail(newEmail || ''); - }; - - useEffect(() => { - loadProfile().finally(() => { - pageProgressMessage.set(''); - }); - }, []); - - return ( - <> - -
- { - setNewEmail(newEmail); - loadProfile().finally(() => {}); - }} - onVerificationCancel={() => { - loadProfile().finally(() => {}); - }} - /> - - ); -} diff --git a/src/components/ReactIcons/AcceptIcon.tsx b/src/components/ReactIcons/AcceptIcon.tsx deleted file mode 100644 index f26c231e9..000000000 --- a/src/components/ReactIcons/AcceptIcon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -type AcceptIconProps = { - className?: string; -}; - -export function AcceptIcon(props: AcceptIconProps) { - const { className } = props; - - return ( - - - - ); -} diff --git a/src/components/ReactIcons/AddUserIcon.tsx b/src/components/ReactIcons/AddUserIcon.tsx deleted file mode 100644 index eef326ba0..000000000 --- a/src/components/ReactIcons/AddUserIcon.tsx +++ /dev/null @@ -1,27 +0,0 @@ -type CheckIconProps = { - additionalClasses?: string; -}; - -export function AddUserIcon(props: CheckIconProps) { - const { additionalClasses = 'mr-2 w-[20px] h-[20px]' } = props; - - return ( - - - - - - - ); -} diff --git a/src/components/ReactIcons/BookEmoji.tsx b/src/components/ReactIcons/BookEmoji.tsx deleted file mode 100644 index b4565b253..000000000 --- a/src/components/ReactIcons/BookEmoji.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { SVGProps } from 'react'; -import React from 'react'; - -export function BookEmoji(props: SVGProps) { - return ( - - - - - - - - - ); -} diff --git a/src/components/ReactIcons/BuildEmoji.tsx b/src/components/ReactIcons/BuildEmoji.tsx deleted file mode 100644 index a6d2f8a49..000000000 --- a/src/components/ReactIcons/BuildEmoji.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import type { SVGProps } from 'react'; - -export function BuildEmoji(props: SVGProps) { - return ( - - - - - - - - - - - - ); -} diff --git a/src/components/ReactIcons/BulbEmoji.tsx b/src/components/ReactIcons/BulbEmoji.tsx deleted file mode 100644 index f5e344f7a..000000000 --- a/src/components/ReactIcons/BulbEmoji.tsx +++ /dev/null @@ -1,37 +0,0 @@ -// twitter bulb emoji -import type { SVGProps } from 'react'; - -type BulbEmojiProps = SVGProps; - -export function BulbEmoji(props: BulbEmojiProps) { - return ( - - - - - - - - ); -} diff --git a/src/components/ReactIcons/CheckEmoji.tsx b/src/components/ReactIcons/CheckEmoji.tsx deleted file mode 100644 index 629237be3..000000000 --- a/src/components/ReactIcons/CheckEmoji.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import type { SVGProps } from 'react'; - -export function CheckEmoji(props: SVGProps) { - return (); -} \ No newline at end of file diff --git a/src/components/ReactIcons/ConstructionEmoji.tsx b/src/components/ReactIcons/ConstructionEmoji.tsx deleted file mode 100644 index df9d0e9f8..000000000 --- a/src/components/ReactIcons/ConstructionEmoji.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { SVGProps } from 'react'; -import React from 'react'; - -export function ConstructionEmoji(props: SVGProps) { - return ( - - - - - - ); -} diff --git a/src/components/ReactIcons/MailIcon.tsx b/src/components/ReactIcons/MailIcon.tsx deleted file mode 100644 index 4708f4318..000000000 --- a/src/components/ReactIcons/MailIcon.tsx +++ /dev/null @@ -1,23 +0,0 @@ -interface MailIconProps { - className?: string; -} -export function MailIcon(props: MailIconProps) { - const { className } = props; - return ( - - - - - ); -} diff --git a/src/components/ReactIcons/RankBadgeIcon.tsx b/src/components/ReactIcons/RankBadgeIcon.tsx deleted file mode 100644 index 541671754..000000000 --- a/src/components/ReactIcons/RankBadgeIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { SVGProps } from 'react'; - -export function RankBadgeIcon(props: SVGProps) { - return ( - - - - ); -} diff --git a/src/components/ReactIcons/YouTubeIcon.tsx b/src/components/ReactIcons/YouTubeIcon.tsx deleted file mode 100644 index 2ccbe6684..000000000 --- a/src/components/ReactIcons/YouTubeIcon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -type YouTubeIconProps = { - className?: string; -}; -export function YouTubeIcon(props: YouTubeIconProps) { - const { className } = props; - - return ( - - - - ); -} diff --git a/src/components/RoadCard/GitHubReadmeBanner.tsx b/src/components/RoadCard/GitHubReadmeBanner.tsx deleted file mode 100644 index a31f86ffb..000000000 --- a/src/components/RoadCard/GitHubReadmeBanner.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export function GitHubReadmeBanner() { - return ( -

- Add this badge to your{' '} - - GitHub profile readme. - -

- ); -} diff --git a/src/components/RoadCard/RoadCardPage.tsx b/src/components/RoadCard/RoadCardPage.tsx deleted file mode 100644 index 6ada8c408..000000000 --- a/src/components/RoadCard/RoadCardPage.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { useState } from 'react'; - -import { useCopyText } from '../../hooks/use-copy-text'; -import { useAuth } from '../../hooks/use-auth'; -import { RoadmapSelect } from './RoadmapSelect'; -import { GitHubReadmeBanner } from './GitHubReadmeBanner'; -import { downloadImage } from '../../helper/download-image'; -import { SelectionButton } from './SelectionButton'; -import { StepCounter } from './StepCounter'; -import { Editor } from './Editor'; -import { CopyIcon } from 'lucide-react'; -import { httpPatch } from '../../lib/http'; -import { useToast } from '../../hooks/use-toast'; - -type StepLabelProps = { - label: string; -}; -function StepLabel(props: StepLabelProps) { - const { label } = props; - - return ( - - {label} - - ); -} - -export function RoadCardPage() { - const user = useAuth(); - const toast = useToast(); - - const { isCopied, copyText } = useCopyText(); - const [roadmaps, setRoadmaps] = useState([]); - const [version, setVersion] = useState<'tall' | 'wide'>('tall'); - const [variant, setVariant] = useState<'dark' | 'light'>('dark'); - - const markRoadCardDone = async () => { - const { error } = await httpPatch( - `${import.meta.env.PUBLIC_API_URL}/v1-update-onboarding-config`, - { - id: 'roadCard', - status: 'done', - }, - ); - - if (error) { - toast.error(error?.message || 'Something went wrong'); - } - }; - - if (!user) { - return null; - } - - const badgeUrl = new URL( - `${import.meta.env.PUBLIC_APP_URL}/card/${version}/${user?.id}`, - ); - - badgeUrl.searchParams.set('variant', variant); - if (roadmaps.length > 0) { - badgeUrl.searchParams.set('roadmaps', roadmaps.join(',')); - } - - return ( - <> -
- -
- - -
- -
-
-
- -
- -
- - -
- { - setVariant('dark'); - }} - /> - - { - setVariant('light'); - }} - /> -
-
-
- -
- -
- - -
- { - setVersion('tall'); - }} - /> - { - setVersion('wide'); - }} - /> -
-
-
- -
- -
- -
- - RoadCard - -
- -
- - -
- -
- roadmap.sh`.trim()} - onCopy={() => markRoadCardDone()} - /> - - markRoadCardDone()} - /> -
- - -
-
- - ); -} diff --git a/src/components/RoadCard/RoadmapSelect.tsx b/src/components/RoadCard/RoadmapSelect.tsx deleted file mode 100644 index baa7fae22..000000000 --- a/src/components/RoadCard/RoadmapSelect.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { httpGet } from '../../lib/http'; -import { useEffect, useState } from 'react'; -import { pageProgressMessage } from '../../stores/page'; -import { SelectionButton } from './SelectionButton'; -import type { UserProgressResponse } from '../Roadmaps/RoadmapsPage'; - -type RoadmapSelectProps = { - selectedRoadmaps: string[]; - setSelectedRoadmaps: (updatedRoadmaps: string[]) => void; -}; - -export function RoadmapSelect(props: RoadmapSelectProps) { - const { selectedRoadmaps, setSelectedRoadmaps } = props; - - const [progressList, setProgressList] = useState(); - - const fetchProgress = async () => { - const { response, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-user-all-progress`, - ); - - if (error || !response) { - return; - } - - setProgressList(response); - }; - - useEffect(() => { - fetchProgress().finally(() => { - pageProgressMessage.set(''); - }); - }, []); - - const canSelectMore = selectedRoadmaps.length < 4; - const allProgress = - progressList?.filter( - (progress) => - progress.resourceType === 'roadmap' && - progress.resourceId && - progress.resourceTitle, - ) || []; - - return ( -
- {allProgress?.length === 0 && ( -

- No progress tracked so far. -

- )} - - {allProgress?.map((progress) => { - const isSelected = selectedRoadmaps.includes(progress.resourceId); - const canSelect = isSelected || canSelectMore; - - return ( - { - if (isSelected) { - setSelectedRoadmaps( - selectedRoadmaps.filter( - (roadmap) => roadmap !== progress.resourceId, - ), - ); - } else if (selectedRoadmaps.length < 4) { - setSelectedRoadmaps([...selectedRoadmaps, progress.resourceId]); - } - }} - /> - ); - })} -
- ); -} diff --git a/src/components/RoadCard/SelectionButton.tsx b/src/components/RoadCard/SelectionButton.tsx deleted file mode 100644 index 2126a72d1..000000000 --- a/src/components/RoadCard/SelectionButton.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { ButtonHTMLAttributes } from 'react'; -import { cn } from '../../lib/classname'; -import type { LucideIcon } from 'lucide-react'; - -type SelectionButtonProps = { - icon?: LucideIcon; - text: string; - isDisabled: boolean; - isSelected: boolean; - onClick: () => void; -} & ButtonHTMLAttributes; - -export function SelectionButton(props: SelectionButtonProps) { - const { - icon: Icon, - text, - isDisabled, - isSelected, - onClick, - className, - ...rest - } = props; - - return ( - - ); -} diff --git a/src/components/RoadCard/StepCounter.tsx b/src/components/RoadCard/StepCounter.tsx deleted file mode 100644 index c4a809329..000000000 --- a/src/components/RoadCard/StepCounter.tsx +++ /dev/null @@ -1,17 +0,0 @@ -type StepCounterProps = { - step: number; -}; - -export function StepCounter(props: StepCounterProps) { - const { step } = props; - - return ( - - {step} - - ); -} diff --git a/src/components/TeamMemberDetails/TeamMemberDetailsPage.tsx b/src/components/TeamMemberDetails/TeamMemberDetailsPage.tsx deleted file mode 100644 index ad594af93..000000000 --- a/src/components/TeamMemberDetails/TeamMemberDetailsPage.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { useEffect, useState } from 'react'; -import { httpGet } from '../../lib/http'; -import { pageProgressMessage } from '../../stores/page'; -import { getUrlParams } from '../../lib/browser'; -import { useToast } from '../../hooks/use-toast'; -import type { TeamMemberDocument } from '../TeamMembers/TeamMembersPage'; -import type { UserProgress } from '../TeamProgress/TeamProgressPage'; -import type { TeamActivityStreamDocument } from '../TeamActivity/TeamActivityPage'; -import { ResourceProgress } from '../Activity/ResourceProgress'; -import { ActivityStream } from '../Activity/ActivityStream'; -import { MemberRoleBadge } from '../TeamMembers/RoleBadge'; -import { TeamMemberEmptyPage } from './TeamMemberEmptyPage'; -import { Pagination } from '../Pagination/Pagination'; -import type { ResourceType } from '../../lib/resource-progress'; -import { MemberProgressModal } from '../TeamProgress/MemberProgressModal'; -import { useStore } from '@nanostores/react'; -import { $currentTeam } from '../../stores/team'; -import { MemberCustomProgressModal } from '../TeamProgress/MemberCustomProgressModal'; - -type GetTeamMemberProgressesResponse = TeamMemberDocument & { - name: string; - avatar: string; - email: string; - progresses: UserProgress[]; -}; - -type GetTeamMemberActivityResponse = { - data: TeamActivityStreamDocument[]; - totalCount: number; - totalPages: number; - currPage: number; - perPage: number; -}; - -export function TeamMemberDetailsPage() { - const { t: teamId, m: memberId } = getUrlParams() as { t: string; m: string }; - - const toast = useToast(); - const currentTeam = useStore($currentTeam); - - const [memberProgress, setMemberProgress] = - useState(null); - const [memberActivity, setMemberActivity] = - useState(null); - const [currPage, setCurrPage] = useState(1); - - const [selectedResource, setSelectedResource] = useState<{ - resourceId: string; - resourceType: ResourceType; - isCustomResource?: boolean; - } | null>(null); - - const loadMemberProgress = async () => { - const { response, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-team-member-progresses/${teamId}/${memberId}`, - ); - if (error || !response) { - pageProgressMessage.set(''); - toast.error(error?.message || 'Failed to load team member'); - return; - } - - setMemberProgress(response); - }; - - const loadMemberActivity = async (currPage: number = 1) => { - const { response, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-team-member-activity/${teamId}/${memberId}`, - { - currPage, - }, - ); - if (error || !response) { - pageProgressMessage.set(''); - toast.error(error?.message || 'Failed to load team member activity'); - return; - } - - setMemberActivity(response); - setCurrPage(response?.currPage || 1); - }; - - useEffect(() => { - if (!teamId) { - return; - } - - Promise.allSettled([loadMemberProgress(), loadMemberActivity()]).finally( - () => { - pageProgressMessage.set(''); - }, - ); - }, [teamId]); - - if (!teamId || !memberId || !memberProgress || !memberActivity) { - return null; - } - - const avatarUrl = memberProgress?.avatar - ? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${memberProgress?.avatar}` - : '/img/default-avatar.png'; - - const ProgressModal = - selectedResource && !selectedResource.isCustomResource - ? MemberProgressModal - : MemberCustomProgressModal; - - return ( - <> - {selectedResource && ( - setSelectedResource(null)} - onShowMyProgress={() => { - window.location.href = `/team/member?t=${teamId}&m=${currentTeam?.memberId}`; - }} - /> - )} -
- {memberProgress?.name} -
-

{memberProgress?.name}

-

{memberProgress?.email}

-
-
- - {memberProgress?.progresses && memberProgress?.progresses?.length > 0 ? ( - <> -

- Progress Overview -

-
- {memberProgress?.progresses?.map((progress) => { - const learningCount = progress.learning || 0; - const doneCount = progress.done || 0; - const totalCount = progress.total || 0; - const skippedCount = progress.skipped || 0; - - return ( - totalCount ? totalCount : doneCount} - learningCount={ - learningCount > totalCount ? totalCount : learningCount - } - totalCount={totalCount} - skippedCount={skippedCount} - resourceId={progress.resourceId} - resourceType={'roadmap'} - updatedAt={progress.updatedAt} - title={progress.resourceTitle} - roadmapSlug={progress.roadmapSlug} - showActions={false} - onResourceClick={() => { - setSelectedResource({ - resourceId: progress.resourceId, - resourceType: progress.resourceType, - isCustomResource: progress.isCustomResource, - }); - }} - /> - ); - })} -
- - ) : ( - - )} - - {memberActivity?.data && memberActivity?.data?.length > 0 ? ( - <> - act.activity) || [] - } - onResourceClick={(resourceId, resourceType, isCustomResource) => { - setSelectedResource({ - resourceId, - resourceType, - isCustomResource, - }); - }} - /> - { - pageProgressMessage.set('Loading Activity'); - loadMemberActivity(page).finally(() => { - pageProgressMessage.set(''); - }); - }} - /> - - ) : null} - - ); -} diff --git a/src/components/TeamMemberDetails/TeamMemberEmptyPage.tsx b/src/components/TeamMemberDetails/TeamMemberEmptyPage.tsx deleted file mode 100644 index 4de37f834..000000000 --- a/src/components/TeamMemberDetails/TeamMemberEmptyPage.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { RoadmapIcon } from '../ReactIcons/RoadmapIcon'; - -type TeamMemberEmptyPageProps = { - teamId: string; -}; - -export function TeamMemberEmptyPage(props: TeamMemberEmptyPageProps) { - const { teamId } = props; - - return ( -
-
- - -

No Progress

-

- Progress will appear here as they start tracking their roadmaps. -

-
-
- ); -} diff --git a/src/components/TeamMembers/LeaveTeamButton.tsx b/src/components/TeamMembers/LeaveTeamButton.tsx deleted file mode 100644 index 13f98f932..000000000 --- a/src/components/TeamMembers/LeaveTeamButton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useState } from 'react'; -import { LeaveTeamPopup } from './LeaveTeamPopup'; - -type LeaveTeamButtonProps = { - teamId: string; -}; - -export function LeaveTeamButton(props: LeaveTeamButtonProps) { - const [showLeaveTeamPopup, setShowLeaveTeamPopup] = useState(false); - - return ( - <> - {showLeaveTeamPopup && ( - { - setShowLeaveTeamPopup(false); - }} - /> - )} - - - ); -} diff --git a/src/components/TeamMembers/LeaveTeamPopup.tsx b/src/components/TeamMembers/LeaveTeamPopup.tsx deleted file mode 100644 index 3ef71c932..000000000 --- a/src/components/TeamMembers/LeaveTeamPopup.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { type FormEvent, useEffect, useRef, useState } from 'react'; -import { httpDelete } from '../../lib/http'; -import { useTeamId } from '../../hooks/use-team-id'; -import { useOutsideClick } from '../../hooks/use-outside-click'; - -type LeaveTeamPopupProps = { - onClose: () => void; -}; - -export function LeaveTeamPopup(props: LeaveTeamPopupProps) { - const { onClose } = props; - - const popupBodyRef = useRef(null); - const confirmationEl = useRef(null); - const [confirmationText, setConfirmationText] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const { teamId } = useTeamId(); - - useEffect(() => { - setError(''); - setConfirmationText(''); - confirmationEl?.current?.focus(); - }, []); - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setError(''); - - if (confirmationText.toUpperCase() !== 'LEAVE') { - setError('Verification text does not match'); - setIsLoading(false); - return; - } - - const { response, error } = await httpDelete( - `${import.meta.env.PUBLIC_API_URL}/v1-leave-team/${teamId}`, - {} - ); - - if (error || !response) { - setIsLoading(false); - setError(error?.message || 'Something went wrong'); - return; - } - - window.location.href = '/account?c=tl'; - }; - - const handleClosePopup = () => { - setIsLoading(false); - setError(''); - setConfirmationText(''); - - onClose(); - }; - - useOutsideClick(popupBodyRef, handleClosePopup); - - return ( -
-
-
-

- Leave Team -

-

- You will lose access to the team, the roadmaps and progress of other team members. -

-

- Please type "leave" to confirm. -

-
-
- - setConfirmationText((e.target as HTMLInputElement).value) - } - /> - {error && ( -

- {error} -

- )} -
- -
- - -
-
-
-
-
- ); -} diff --git a/src/components/TeamMembers/MemberActionDropdown.tsx b/src/components/TeamMembers/MemberActionDropdown.tsx deleted file mode 100644 index b2ee89177..000000000 --- a/src/components/TeamMembers/MemberActionDropdown.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { useRef, useState } from 'react'; -import type { TeamMemberDocument } from './TeamMembersPage'; -import { useOutsideClick } from '../../hooks/use-outside-click'; -import { MoreVerticalIcon } from '../ReactIcons/MoreVerticalIcon.tsx'; - -export function MemberActionDropdown({ - member, - onUpdateMember, - onDeleteMember, - onResendInvite, - isDisabled = false, - onSendProgressReminder, - allowProgressReminder = false, - allowUpdateRole = true, -}: { - onDeleteMember: () => void; - onUpdateMember: () => void; - onResendInvite: () => void; - onSendProgressReminder: () => void; - isDisabled: boolean; - allowProgressReminder: boolean; - allowUpdateRole: boolean; - member: TeamMemberDocument; -}) { - const menuRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - useOutsideClick(menuRef, () => { - setIsOpen(false); - }); - - const actions = [ - ...(allowUpdateRole - ? [ - { - name: 'Update Role', - handleClick: () => { - onUpdateMember(); - setIsOpen(false); - }, - }, - ] - : []), - ...(allowProgressReminder - ? [ - { - name: 'Send Progress Reminder', - handleClick: () => { - onSendProgressReminder(); - setIsOpen(false); - }, - }, - ] - : []), - ...(['invited'].includes(member.status) - ? [ - { - name: 'Resend Invite', - handleClick: () => { - onResendInvite(); - setIsOpen(false); - }, - }, - ] - : []), - { - name: 'Delete', - handleClick: () => { - onDeleteMember(); - setIsOpen(false); - }, - }, - ]; - return ( -
- - - {isOpen && ( -
-
    - {actions.map((action, index) => { - return ( -
  • - -
  • - ); - })} -
-
- )} -
- ); -} diff --git a/src/components/TeamMembers/RoleBadge.tsx b/src/components/TeamMembers/RoleBadge.tsx deleted file mode 100644 index f42c81e79..000000000 --- a/src/components/TeamMembers/RoleBadge.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { cn } from '../../lib/classname'; -import type { AllowedRoles } from '../CreateTeam/RoleDropdown'; - -type RoleBadgeProps = { - role: AllowedRoles; - className?: string; -}; -export function MemberRoleBadge(props: RoleBadgeProps) { - const { role, className } = props; - - return ( - - {role} - - ); -} diff --git a/src/components/TeamMembers/TeamMemberItem.tsx b/src/components/TeamMembers/TeamMemberItem.tsx deleted file mode 100644 index b2917f117..000000000 --- a/src/components/TeamMembers/TeamMemberItem.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { MailIcon } from '../ReactIcons/MailIcon'; -import { MemberActionDropdown } from './MemberActionDropdown'; -import { MemberRoleBadge } from './RoleBadge'; -import type { TeamMemberItem } from './TeamMembersPage'; -import { $canManageCurrentTeam, $currentTeam } from '../../stores/team'; -import { useStore } from '@nanostores/react'; -import { useAuth } from '../../hooks/use-auth'; -import { cn } from '../../lib/classname'; - -type TeamMemberProps = { - member: TeamMemberItem; - userId: string; - index: number; - teamId: string; - canViewProgress: boolean; - canManageCurrentTeam: boolean; - onDeleteMember: () => void; - onUpdateMember: () => void; - onSendProgressReminder: () => void; - onResendInvite: () => void; -}; - -export function TeamMemberItem(props: TeamMemberProps) { - const { - member, - index, - onResendInvite, - onUpdateMember, - canManageCurrentTeam, - userId, - onDeleteMember, - onSendProgressReminder, - canViewProgress = true, - } = props; - - const currentTeam = useStore($currentTeam); - const canManageTeam = useStore($canManageCurrentTeam); - const showNoProgressBadge = canViewProgress && !member.hasProgress && member.status === 'joined'; - const allowProgressReminder = - canManageTeam && - !member.hasProgress && - member.status === 'joined' && - member.userId !== userId; - const isPersonalProgressOnly = - currentTeam?.personalProgressOnly && - currentTeam.role === 'member' && - String(member._id) !== currentTeam.memberId; - - return ( -
-
- {member.name -
-
- -
-
-

- { - if (isPersonalProgressOnly) { - e.preventDefault(); - } - }} - aria-disabled={isPersonalProgressOnly} - > - {member.name} - - {showNoProgressBadge && ( - - No Progress - - )} - {member.userId === userId && ( - - You - - )} -

-
- {member.status === 'invited' && ( - - Invited - - )} - {member.status === 'rejected' && ( - - Rejected - - )} -
-
-

- {member.invitedEmail} -

-
-
- -
- - - - {canManageCurrentTeam && ( - - )} -
-
- ); -} diff --git a/src/components/TeamMembers/TeamMembersPage.tsx b/src/components/TeamMembers/TeamMembersPage.tsx deleted file mode 100644 index a568170bd..000000000 --- a/src/components/TeamMembers/TeamMembersPage.tsx +++ /dev/null @@ -1,338 +0,0 @@ -import { useEffect, useState } from 'react'; -import { httpDelete, httpGet, httpPatch } from '../../lib/http'; -import { useAuth } from '../../hooks/use-auth'; -import { pageProgressMessage } from '../../stores/page'; -import type { TeamDocument } from '../CreateTeam/CreateTeamForm'; -import { LeaveTeamButton } from './LeaveTeamButton'; -import type { AllowedRoles } from '../CreateTeam/RoleDropdown'; -import type { AllowedMemberStatus } from '../TeamDropdown/TeamDropdown'; -import { InviteMemberPopup } from './InviteMemberPopup'; -import { getUrlParams } from '../../lib/browser'; -import { UpdateMemberPopup } from './UpdateMemberPopup'; -import { useStore } from '@nanostores/react'; -import { $canManageCurrentTeam } from '../../stores/team'; -import { useToast } from '../../hooks/use-toast'; -import { TeamMemberItem } from './TeamMemberItem'; - -export interface TeamMemberDocument { - _id?: string; - userId?: string; - invitedEmail?: string; - teamId: string; - role: AllowedRoles; - status: AllowedMemberStatus; - createdAt: Date; - updatedAt: Date; -} - -export interface UserResourceProgressDocument { - _id?: string; - userId: string; - resourceId: string; - resourceType: 'roadmap' | 'best-practice'; - isFavorite?: boolean; - done: string[]; - learning: string[]; - skipped: string[]; - createdAt: Date; - updatedAt: Date; -} - -export interface TeamMemberItem extends TeamMemberDocument { - name: string; - avatar: string; - hasProgress: boolean; -} - -const MAX_MEMBER_COUNT = 200; - -export function TeamMembersPage() { - const { t: teamId } = getUrlParams(); - - const toast = useToast(); - const canManageCurrentTeam = useStore($canManageCurrentTeam); - - const [memberToUpdate, setMemberToUpdate] = useState(); - const [isInvitingMember, setIsInvitingMember] = useState(false); - const [teamMembers, setTeamMembers] = useState([]); - const [team, setTeam] = useState(); - - const user = useAuth(); - - async function loadTeam() { - const { response, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamId}`, - ); - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - return; - } - - if (response) { - setTeam(response); - } - } - - async function getTeamMemberList() { - const { response, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-team-member-list/${teamId}`, - ); - if (error || !response) { - toast.error(error?.message || 'Failed to load team member list'); - return; - } - - setTeamMembers(response); - } - - useEffect(() => { - if (!teamId) { - return; - } - - Promise.all([loadTeam(), getTeamMemberList()]).finally(() => { - pageProgressMessage.set(''); - }); - }, [teamId]); - async function deleteMember(teamId: string, memberId: string) { - pageProgressMessage.set('Deleting member'); - const { response, error } = await httpDelete( - `${ - import.meta.env.PUBLIC_API_URL - }/v1-delete-member/${teamId}/${memberId}`, - {}, - ); - - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - return; - } - - toast.success('Member has been deleted'); - await getTeamMemberList(); - } - - async function resendInvite(teamId: string, memberId: string) { - pageProgressMessage.set('Resending Invite'); - const { response, error } = await httpPatch( - `${ - import.meta.env.PUBLIC_API_URL - }/v1-resend-invite/${teamId}/${memberId}`, - {}, - ); - - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - return; - } - - toast.success('Invite has been sent'); - } - - async function handleSendReminder(teamId: string, memberId: string) { - pageProgressMessage.set('Sending Reminder'); - const { response, error } = await httpPatch( - `${ - import.meta.env.PUBLIC_API_URL - }/v1-send-progress-reminder/${teamId}/${memberId}`, - {}, - ); - - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - return; - } - - toast.success('Reminder has been sent'); - } - - const joinedMembers = teamMembers.filter( - (member) => member.status === 'joined', - ); - const invitedMembers = teamMembers.filter( - (member) => member.status === 'invited', - ); - const rejectedMembers = teamMembers.filter( - (member) => member.status === 'rejected', - ); - - return ( -
- {memberToUpdate && ( - { - pageProgressMessage.set('Refreshing members'); - getTeamMemberList().finally(() => { - pageProgressMessage.set(''); - }); - setMemberToUpdate(undefined); - toast.success('Member has been updated'); - }} - onClose={() => { - setMemberToUpdate(undefined); - }} - /> - )} - {isInvitingMember && ( - { - toast.success('Invite sent'); - getTeamMemberList().then(() => null); - setIsInvitingMember(false); - }} - onClose={() => { - setIsInvitingMember(false); - }} - /> - )} -
-
-
-

- {teamMembers.length} people in the team. -

-

- {teamMembers.length} members -

- -
- {joinedMembers.map((member, index) => { - return ( - { - resendInvite(teamId, member._id!).finally(() => { - pageProgressMessage.set(''); - }); - }} - canManageCurrentTeam={canManageCurrentTeam} - onDeleteMember={() => { - deleteMember(teamId, member._id!).finally(() => { - pageProgressMessage.set(''); - }); - }} - onUpdateMember={() => { - setMemberToUpdate(member); - }} - onSendProgressReminder={() => { - handleSendReminder(teamId, member._id!).finally(() => { - pageProgressMessage.set(''); - }); - }} - /> - ); - })} -
- - {invitedMembers.length > 0 && ( -
-

Invited Members

-
- {invitedMembers.map((member, index) => { - return ( - { - resendInvite(teamId, member._id!).finally(() => { - pageProgressMessage.set(''); - }); - }} - canManageCurrentTeam={canManageCurrentTeam} - onDeleteMember={() => { - deleteMember(teamId, member._id!).finally(() => { - pageProgressMessage.set(''); - }); - }} - onUpdateMember={() => { - setMemberToUpdate(member); - }} - onSendProgressReminder={() => { - handleSendReminder(teamId, member._id!).finally(() => { - pageProgressMessage.set(''); - }); - }} - /> - ); - })} -
-
- )} - - {rejectedMembers.length > 0 && ( -
-

- Rejected Invites -

-
- {rejectedMembers.map((member, index) => { - return ( - { - resendInvite(teamId, member._id!).finally(() => { - pageProgressMessage.set(''); - }); - }} - canManageCurrentTeam={canManageCurrentTeam} - onDeleteMember={() => { - deleteMember(teamId, member._id!).finally(() => { - pageProgressMessage.set(''); - }); - }} - onUpdateMember={() => { - setMemberToUpdate(member); - }} - onSendProgressReminder={() => { - handleSendReminder(teamId, member._id!).finally(() => { - pageProgressMessage.set(''); - }); - }} - /> - ); - })} -
-
- )} -
- - {canManageCurrentTeam && ( -
- -
- )} - - {teamMembers.length >= MAX_MEMBER_COUNT && canManageCurrentTeam && ( -

- You have reached the maximum number of members in a team. Please reach - out to us if you need more. -

- )} -
- ); -} diff --git a/src/components/TeamMembers/UpdateMemberPopup.tsx b/src/components/TeamMembers/UpdateMemberPopup.tsx deleted file mode 100644 index 5118f5087..000000000 --- a/src/components/TeamMembers/UpdateMemberPopup.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { type FormEvent, useRef, useState } from 'react'; -import { httpPut } from '../../lib/http'; -import { useTeamId } from '../../hooks/use-team-id'; -import { useOutsideClick } from '../../hooks/use-outside-click'; -import { type AllowedRoles, RoleDropdown } from '../CreateTeam/RoleDropdown'; -import type { TeamMemberDocument } from './TeamMembersPage'; - -type InviteMemberPopupProps = { - member: TeamMemberDocument; - onUpdated: () => void; - onClose: () => void; -}; - -export function UpdateMemberPopup(props: InviteMemberPopupProps) { - const { onClose, onUpdated, member } = props; - - const popupBodyRef = useRef(null); - const [selectedRole, setSelectedRole] = useState(member.role); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const { teamId } = useTeamId(); - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setError(''); - - const { response, error } = await httpPut( - `${import.meta.env.PUBLIC_API_URL}/v1-update-member-role/${teamId}/${ - member._id - }`, - { role: selectedRole } - ); - - if (error || !response) { - setIsLoading(false); - setError(error?.message || 'Something went wrong'); - return; - } - - setIsLoading(false); - onUpdated(); - }; - - const handleClosePopup = () => { - setIsLoading(false); - setError(''); - - onClose(); - }; - - useOutsideClick(popupBodyRef, handleClosePopup); - - return ( -
-
-
-

- Update Role -

-

- Select the role to update for this member -

- -
-
- - {member.invitedEmail} - - -
- -
- - {error && ( -

- {error} -

- )} -
- -
- - -
-
-
-
-
- ); -} diff --git a/src/components/TeamRoadmap/CustomTeamRoadmap.tsx b/src/components/TeamRoadmap/CustomTeamRoadmap.tsx deleted file mode 100644 index 289ec4de3..000000000 --- a/src/components/TeamRoadmap/CustomTeamRoadmap.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function CustomTeamRoadmap() { - return null; -} \ No newline at end of file diff --git a/src/components/TeamRoadmap/DefaultTeamRoadmap.tsx b/src/components/TeamRoadmap/DefaultTeamRoadmap.tsx deleted file mode 100644 index 995e63d16..000000000 --- a/src/components/TeamRoadmap/DefaultTeamRoadmap.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function DefaultTeamRoadmap() { - return null; -} diff --git a/src/components/TeamRoadmapsList/RoadmapActionDropdown.tsx b/src/components/TeamRoadmapsList/RoadmapActionDropdown.tsx deleted file mode 100644 index 7a5ccc956..000000000 --- a/src/components/TeamRoadmapsList/RoadmapActionDropdown.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useRef, useState } from 'react'; -import { useOutsideClick } from '../../hooks/use-outside-click'; -import { Lock, MoreVertical, Shapes, Trash2 } from 'lucide-react'; -import { MoreVerticalIcon } from '../ReactIcons/MoreVerticalIcon.tsx'; - -type RoadmapActionDropdownProps = { - onDelete?: () => void; - onCustomize?: () => void; - onUpdateSharing?: () => void; -}; - -export function RoadmapActionDropdown(props: RoadmapActionDropdownProps) { - const { onDelete, onUpdateSharing, onCustomize } = props; - - const menuRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); - - useOutsideClick(menuRef, () => { - setIsOpen(false); - }); - - return ( -
- - - - {isOpen && ( -
-
    - {onUpdateSharing && ( -
  • - -
  • - )} - {onCustomize && ( -
  • - -
  • - )} - {onDelete && ( -
  • - -
  • - )} -
-
- )} -
- ); -} diff --git a/src/components/TeamRoadmapsList/TeamRoadmaps.tsx b/src/components/TeamRoadmapsList/TeamRoadmaps.tsx deleted file mode 100644 index 9058f96c7..000000000 --- a/src/components/TeamRoadmapsList/TeamRoadmaps.tsx +++ /dev/null @@ -1,690 +0,0 @@ -import { getUrlParams } from '../../lib/browser'; -import { useEffect, useState } from 'react'; -import type { TeamDocument } from '../CreateTeam/CreateTeamForm'; -import type { TeamResourceConfig } from '../CreateTeam/RoadmapSelector'; -import { httpGet, httpPut } from '../../lib/http'; -import { pageProgressMessage } from '../../stores/page'; -import type { PageType } from '../CommandMenu/CommandMenu'; -import { useStore } from '@nanostores/react'; -import { $canManageCurrentTeam } from '../../stores/team'; -import { useToast } from '../../hooks/use-toast'; -import { SelectRoadmapModal } from '../CreateTeam/SelectRoadmapModal'; -import { PickRoadmapOptionModal } from '../TeamRoadmaps/PickRoadmapOptionModal'; -import type { AllowedRoadmapVisibility } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal'; -import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal'; -import { - ExternalLink, - Globe, - LockIcon, - type LucideIcon, - Package, - PackageMinus, - PenSquare, - Shapes, - Users, -} from 'lucide-react'; -import { RoadmapActionDropdown } from './RoadmapActionDropdown'; -import { UpdateTeamResourceModal } from '../CreateTeam/UpdateTeamResourceModal'; -import { ShareOptionsModal } from '../ShareOptions/ShareOptionsModal'; -import { cn } from '../../lib/classname'; -import { RoadmapIcon } from '../ReactIcons/RoadmapIcon.tsx'; -import { ContentConfirmationModal } from '../CreateTeam/ContentConfirmationModal.tsx'; - -export function TeamRoadmaps() { - const { t: teamId } = getUrlParams(); - - const canManageCurrentTeam = useStore($canManageCurrentTeam); - - const toast = useToast(); - - const [isLoading, setIsLoading] = useState(true); - const [isPickingOptions, setIsPickingOptions] = useState(false); - const [isAddingRoadmap, setIsAddingRoadmap] = useState(false); - const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false); - const [changingRoadmapId, setChangingRoadmapId] = useState(''); - const [team, setTeam] = useState(); - const [teamResources, setTeamResources] = useState([]); - const [allRoadmaps, setAllRoadmaps] = useState([]); - const [selectedResource, setSelectedResource] = useState< - TeamResourceConfig[0] | null - >(null); - const [confirmationContentId, setConfirmationContentId] = useState(''); - - async function loadAllRoadmaps() { - const { error, response } = await httpGet(`/pages.json`); - - if (error) { - toast.error(error.message || 'Something went wrong'); - return; - } - - if (!response) { - return []; - } - - const allRoadmaps = response - .filter((page) => page.group === 'Roadmaps') - .sort((a, b) => { - if (a.title === 'Android') return 1; - return a.title.localeCompare(b.title); - }); - - setAllRoadmaps(allRoadmaps); - return response; - } - - async function loadTeam(teamIdToFetch: string) { - const { response, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamIdToFetch}`, - ); - - if (error || !response) { - toast.error('Error loading team'); - window.location.href = '/account'; - return; - } - - setTeam(response); - } - - async function loadTeamResourceConfig(teamId: string) { - const { error, response } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-team-resource-config/${teamId}`, - ); - if (error || !Array.isArray(response)) { - console.error(error); - return; - } - - setTeamResources(response); - } - - useEffect(() => { - if (!teamId) { - return; - } - - setIsLoading(true); - Promise.all([ - loadTeam(teamId), - loadTeamResourceConfig(teamId), - loadAllRoadmaps(), - ]).finally(() => { - pageProgressMessage.set(''); - setIsLoading(false); - }); - }, [teamId]); - - async function deleteResource(roadmapId: string) { - if (!team?._id) { - return; - } - - toast.loading('Deleting roadmap'); - pageProgressMessage.set(`Deleting roadmap from team`); - const { error, response } = await httpPut( - `${import.meta.env.PUBLIC_API_URL}/v1-delete-team-resource-config/${ - team._id - }`, - { - resourceId: roadmapId, - resourceType: 'roadmap', - }, - ); - - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - return; - } - - toast.success('Roadmap removed'); - setTeamResources(response); - } - - async function onAdd(roadmapId: string, shouldCopyContent = false) { - if (!teamId) { - return; - } - - toast.loading('Adding roadmap'); - pageProgressMessage.set('Adding roadmap'); - setIsLoading(true); - const roadmap = allRoadmaps.find((r) => r.id === roadmapId); - const { error, response } = await httpPut( - `${ - import.meta.env.PUBLIC_API_URL - }/v1-update-team-resource-config/${teamId}`, - { - teamId: teamId, - resourceId: roadmapId, - resourceType: 'roadmap', - removed: [], - renderer: roadmap?.renderer || 'balsamiq', - shouldCopyContent, - }, - ); - - if (error || !response) { - toast.error(error?.message || 'Error adding roadmap'); - return; - } - - setTeamResources(response); - toast.success('Roadmap added'); - if (roadmap?.renderer === 'editor') { - setIsAddingRoadmap(false); - } - } - - async function onRemove(resourceId: string) { - pageProgressMessage.set('Removing roadmap'); - - deleteResource(resourceId).finally(() => { - pageProgressMessage.set(''); - }); - } - - useEffect(() => { - function handleCustomRoadmapCreated(event: Event) { - const { roadmapId } = (event as CustomEvent)?.detail; - if (!roadmapId) { - return; - } - - loadAllRoadmaps().finally(() => {}); - onAdd(roadmapId).finally(() => { - pageProgressMessage.set(''); - }); - } - window.addEventListener( - 'custom-roadmap-created', - handleCustomRoadmapCreated, - ); - - return () => { - window.removeEventListener( - 'custom-roadmap-created', - handleCustomRoadmapCreated, - ); - }; - }, []); - - if (!team) { - return null; - } - - const pickRoadmapOptionModal = isPickingOptions && ( - setIsPickingOptions(false)} - showDefaultRoadmapsModal={() => { - setIsAddingRoadmap(true); - setIsPickingOptions(false); - }} - showCreateCustomRoadmapModal={() => { - setIsCreatingRoadmap(true); - setIsPickingOptions(false); - }} - /> - ); - - const filteredAllRoadmaps = allRoadmaps.filter( - (r) => !teamResources.find((c) => c?.defaultRoadmapId === r.id), - ); - const addRoadmapModal = isAddingRoadmap && ( - setIsAddingRoadmap(false)} - teamResourceConfig={teamResources.map((c) => c.resourceId)} - allRoadmaps={filteredAllRoadmaps.filter((r) => r.renderer === 'editor')} - teamId={teamId} - onRoadmapAdd={(roadmapId: string) => { - const isEditorRoadmap = allRoadmaps.find( - (r) => r.id === roadmapId && r.renderer === 'editor', - ); - - if (!isEditorRoadmap) { - onAdd(roadmapId).finally(() => { - pageProgressMessage.set(''); - }); - - return; - } - - setIsAddingRoadmap(false); - setConfirmationContentId(roadmapId); - }} - onRoadmapRemove={(roadmapId: string) => { - if (confirm('Are you sure you want to remove this roadmap?')) { - onRemove(roadmapId).finally(() => {}); - } - }} - /> - ); - - const confirmationContentIdModal = confirmationContentId && ( - { - setConfirmationContentId(''); - }} - onClick={(shouldCopy) => { - onAdd(confirmationContentId, shouldCopy).finally(() => { - pageProgressMessage.set(''); - setConfirmationContentId(''); - }); - }} - /> - ); - - const createRoadmapModal = isCreatingRoadmap && ( - { - setIsCreatingRoadmap(false); - }} - onCreated={() => { - loadTeamResourceConfig(teamId).finally(() => null); - setIsCreatingRoadmap(false); - }} - /> - ); - - const placeholderRoadmaps = teamResources.filter( - (c: TeamResourceConfig[0]) => c.isCustomResource && !c.topics, - ); - const customRoadmaps = teamResources.filter( - (c: TeamResourceConfig[0]) => c.isCustomResource && c.topics, - ); - const defaultRoadmaps = teamResources.filter( - (c: TeamResourceConfig[0]) => !c.isCustomResource, - ); - - const hasRoadmaps = - customRoadmaps.length > 0 || - defaultRoadmaps.length > 0 || - (placeholderRoadmaps.length > 0 && canManageCurrentTeam); - if (!hasRoadmaps && !isLoading) { - return ( -
- {pickRoadmapOptionModal} - {addRoadmapModal} - {createRoadmapModal} - {confirmationContentIdModal} - - - -

No roadmaps

-

- {canManageCurrentTeam - ? 'Add a roadmap to start tracking your team' - : 'Ask your team admin to add some roadmaps'} -

- - {canManageCurrentTeam && ( - - )} -
- ); - } - - const customizeRoadmapModal = changingRoadmapId && ( - setChangingRoadmapId('')} - resourceId={changingRoadmapId} - resourceType={'roadmap'} - teamId={team?._id!} - setTeamResourceConfig={setTeamResources} - defaultRemovedItems={ - defaultRoadmaps.find((c) => c.resourceId === changingRoadmapId) - ?.removed || [] - } - /> - ); - - const shareSettingsModal = selectedResource && ( - { - setTeamResources((prev) => { - return prev.map((c) => { - if (c.resourceId !== selectedResource.resourceId) { - return c; - } - - return { - ...c, - ...shareSettings, - }; - }); - }); - }} - onClose={() => setSelectedResource(null)} - /> - ); - - return ( -
- {pickRoadmapOptionModal} - {addRoadmapModal} - {createRoadmapModal} - {customizeRoadmapModal} - {shareSettingsModal} - {confirmationContentIdModal} - - {canManageCurrentTeam && placeholderRoadmaps.length > 0 && ( -
-
-

- Placeholder Roadmaps - - Total {placeholderRoadmaps.length} roadmap(s) - -

-
-
- {placeholderRoadmaps.map( - (resourceConfig: TeamResourceConfig[0]) => { - return ( -
-
-

- {resourceConfig.title} -

- - Placeholder roadmap - -
- - {canManageCurrentTeam && ( -
- { - setSelectedResource(resourceConfig); - }} - onDelete={() => { - if ( - confirm( - 'Are you sure you want to remove this roadmap?', - ) - ) { - onRemove(resourceConfig.resourceId).finally( - () => {}, - ); - } - }} - /> - - - Create Roadmap - -
- )} -
- ); - }, - )} -
-
- )} - - {customRoadmaps.length > 0 && ( -
-
-

- Custom Roadmaps - - Total {customRoadmaps.length} roadmap(s) - -

-
-
- {customRoadmaps.map((resourceConfig: TeamResourceConfig[0]) => { - const editorLink = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${ - resourceConfig.resourceId - }`; - - return ( -
-
-

- {resourceConfig.title} -

- - - · - - {resourceConfig.topics} topic - -
-
- {canManageCurrentTeam && ( - { - setSelectedResource(resourceConfig); - }} - onCustomize={() => { - window.open(editorLink, '_blank'); - }} - onDelete={() => { - if ( - confirm( - 'Are you sure you want to remove this roadmap?', - ) - ) { - onRemove(resourceConfig.resourceId).finally( - () => {}, - ); - } - }} - /> - )} - - - - Visit - - {canManageCurrentTeam && ( - - - Edit - - )} -
-
- ); - })} -
-
- )} - - {defaultRoadmaps.length > 0 && ( -
-
-

- Default Roadmaps - - Total {defaultRoadmaps.length} roadmap(s) - -

-
-
- {defaultRoadmaps.map((resourceConfig: TeamResourceConfig[0]) => { - return ( -
-
-

- {resourceConfig.title} -

- - {resourceConfig?.removed?.length > 0 && ( - <> - - {resourceConfig.removed.length} topics removed - - )} - - {!resourceConfig?.removed?.length && ( - <> - - No changes made - - )} - -
-
- {canManageCurrentTeam && ( - { - setChangingRoadmapId(resourceConfig.resourceId); - }} - onDelete={() => { - if ( - confirm( - 'Are you sure you want to remove this roadmap?', - ) - ) { - onRemove(resourceConfig.resourceId).finally( - () => {}, - ); - } - }} - /> - )} - - - - Visit - -
-
- ); - })} -
-
- )} - - {canManageCurrentTeam && ( -
- -
- )} -
- ); -} - -type VisibilityLabelProps = { - visibility: AllowedRoadmapVisibility; - sharedTeamMemberIds?: string[]; - sharedFriendIds?: string[]; -}; - -const visibilityDetails: Record< - AllowedRoadmapVisibility, - { - icon: LucideIcon; - label: string; - } -> = { - public: { - icon: Globe, - label: 'Public', - }, - me: { - icon: LockIcon, - label: 'Only me', - }, - team: { - icon: Users, - label: 'Team Member(s)', - }, - friends: { - icon: Users, - label: 'Friend(s)', - }, -} as const; - -export function VisibilityBadge(props: VisibilityLabelProps) { - const { visibility, sharedTeamMemberIds = [], sharedFriendIds = [] } = props; - - const { label, icon: Icon } = visibilityDetails[visibility]; - - return ( - - -
- {visibility === 'team' && sharedTeamMemberIds?.length > 0 && ( - {sharedTeamMemberIds.length} - )} - {visibility === 'friends' && sharedFriendIds?.length > 0 && ( - {sharedFriendIds.length} - )} - {label} -
-
- ); -} diff --git a/src/components/TeamSettings/UpdateTeamForm.tsx b/src/components/TeamSettings/UpdateTeamForm.tsx deleted file mode 100644 index bf92571ea..000000000 --- a/src/components/TeamSettings/UpdateTeamForm.tsx +++ /dev/null @@ -1,321 +0,0 @@ -import { type FormEvent, useEffect, useState } from 'react'; -import { httpGet, httpPut } from '../../lib/http'; -import { Spinner } from '../ReactIcons/Spinner'; -import UploadProfilePicture from '../UpdateProfile/UploadProfilePicture'; -import type { TeamDocument } from '../CreateTeam/CreateTeamForm'; -import { pageProgressMessage } from '../../stores/page'; -import { useTeamId } from '../../hooks/use-team-id'; -import { DeleteTeamPopup } from '../DeleteTeamPopup'; -import { $isCurrentTeamAdmin } from '../../stores/team'; -import { useStore } from '@nanostores/react'; -import { useToast } from '../../hooks/use-toast'; -export function UpdateTeamForm() { - const [isLoading, setIsLoading] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - - const isCurrentTeamAdmin = useStore($isCurrentTeamAdmin); - - const toast = useToast(); - - const [name, setName] = useState(''); - const [avatar, setAvatar] = useState(''); - const [website, setWebsite] = useState(''); - const [linkedIn, setLinkedIn] = useState(''); - const [gitHub, setGitHub] = useState(''); - const [teamType, setTeamType] = useState(''); - const [teamSize, setTeamSize] = useState(''); - const [personalProgressOnly, setPersonalProgressOnly] = useState(false); - const validTeamSizes = [ - '0-1', - '2-10', - '11-50', - '51-200', - '201-500', - '501-1000', - '1000+', - ]; - const [isDisabled, setIsDisabled] = useState(false); - const { teamId } = useTeamId(); - - useEffect(() => { - setIsDisabled(!isCurrentTeamAdmin); - }, [isCurrentTeamAdmin]); - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setIsLoading(true); - if (!name || !teamType) { - setIsLoading(false); - return; - } - - const { response, error } = await httpPut( - `${import.meta.env.PUBLIC_API_URL}/v1-update-team/${teamId}`, - { - name, - website, - type: teamType, - gitHubUrl: gitHub || undefined, - personalProgressOnly, - ...(teamType === 'company' && { - teamSize, - linkedInUrl: linkedIn || undefined, - }), - }, - ); - - if (error) { - setIsLoading(false); - toast.error(error.message || 'Something went wrong'); - return; - } - - if (response) { - await loadTeam(); - setIsLoading(false); - toast.success('Team updated successfully'); - } - }; - - async function loadTeam() { - const { response, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamId}`, - ); - if (error || !response) { - console.log(error); - return; - } - - setName(response.name); - setAvatar(response.avatar || ''); - setWebsite(response?.links?.website || ''); - setLinkedIn(response?.links?.linkedIn || ''); - setGitHub(response?.links?.github || ''); - setTeamType(response.type); - setPersonalProgressOnly(response.personalProgressOnly ?? false); - if (response.teamSize) { - setTeamSize(response.teamSize); - } - } - - useEffect(() => { - if (!teamId) { - return; - } - loadTeam().finally(() => { - pageProgressMessage.set(''); - }); - }, [teamId]); - - return ( -
- -
-
- - setName((e.target as HTMLInputElement).value)} - /> -
-
- - setWebsite((e.target as HTMLInputElement).value)} - /> -
- {teamType === 'company' && ( -
- - setLinkedIn((e.target as HTMLInputElement).value)} - /> -
- )} -
- - setGitHub((e.target as HTMLInputElement).value)} - /> -
-
- - -
- - {teamType === 'company' && ( -
- - -
- )} - -
- -
- - {personalProgressOnly && ( -

- Only admins and managers will be able to see the progress of members -

- )} - -
- -
-
- - {!isCurrentTeamAdmin && ( -

- Only team admins can update team information. -

- )} - - {isCurrentTeamAdmin && ( - <> -
- {isDeleting && ( - { - setIsDeleting(false); - }} - /> - )} -

Delete Team

-

- Permanently delete this team and all of its resources. -

- - - - )} -
- ); -} diff --git a/src/components/TeamVersions/TeamVersions.tsx b/src/components/TeamVersions/TeamVersions.tsx deleted file mode 100644 index 059364516..000000000 --- a/src/components/TeamVersions/TeamVersions.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { useState, useEffect, useRef } from 'react'; -import type { TeamDocument } from '../CreateTeam/CreateTeamForm'; -import type { TeamResourceConfig } from '../CreateTeam/RoadmapSelector'; -import { httpGet } from '../../lib/http'; -// import DropdownIcon from '../../icons/dropdown.svg'; -import { - clearResourceProgress, - refreshProgressCounters, - renderTopicProgress, -} from '../../lib/resource-progress'; -import { renderResourceProgress } from '../../lib/resource-progress'; -import { deleteUrlParam, getUrlParams, setUrlParams } from '../../lib/browser'; -import { useOutsideClick } from '../../hooks/use-outside-click'; -import { useKeydown } from '../../hooks/use-keydown'; -import { isLoggedIn } from '../../lib/jwt'; -import { useAuth } from '../../hooks/use-auth'; -import { useToast } from '../../hooks/use-toast'; -import { DropdownIcon } from '../ReactIcons/DropdownIcon'; - -type TeamVersionsProps = { - resourceId: string; - resourceType: 'roadmap' | 'best-practice'; -}; - -type TeamVersionsResponse = { - team: TeamDocument; - config: TeamResourceConfig[0]; -}[]; - -export function TeamVersions(props: TeamVersionsProps) { - const { t: teamId } = getUrlParams(); - if (!isLoggedIn()) { - return; - } - - const { resourceId, resourceType } = props; - const user = useAuth(); - const toast = useToast(); - const teamDropdownRef = useRef(null); - - const [isPreparing, setIsPreparing] = useState(true); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [containerOpacity, setContainerOpacity] = useState(0); - const [teamVersions, setTeamVersions] = useState([]); - const [selectedTeamVersion, setSelectedTeamVersion] = useState< - TeamVersionsResponse[0] | null - >(null); - - let shouldShowAvatar: boolean; - const selectedAvatar = selectedTeamVersion - ? selectedTeamVersion.team.avatar - : user?.avatar; - const selectedLabel = selectedTeamVersion - ? selectedTeamVersion.team.name - : user?.name; - - // Show avatar if team has one, or if user has one otherwise use first letter of name - if (selectedTeamVersion?.team.avatar) { - shouldShowAvatar = true; - } else { - shouldShowAvatar = !!(!selectedTeamVersion && user?.avatar); - } - - useOutsideClick(teamDropdownRef, () => { - setIsDropdownOpen(false); - }); - - useKeydown('Escape', () => { - setIsDropdownOpen(false); - }); - - async function loadTeamVersions() { - const { response, error } = await httpGet( - `${ - import.meta.env.PUBLIC_API_URL - }/v1-get-team-versions?${new URLSearchParams({ - resourceId, - resourceType, - })}`, - ); - - if (error || !response) { - toast.error(error?.message || 'Failed to load team versions.'); - return; - } - - setTeamVersions(response); - if (teamId) { - const foundVersion = response.find((v) => v.team._id === teamId) || null; - setSelectedTeamVersion(foundVersion); - } - - setTimeout(() => { - setIsPreparing(false); - - setTimeout(() => { - setContainerOpacity(100); - }, 50); - }, 0); - } - - useEffect(() => { - loadTeamVersions().finally(() => { - // - }); - }, []); - - useEffect(() => { - if (!selectedTeamVersion) { - return; - } - - clearResourceProgress(); - - // teams have customizations. Assigning #customized-roadmap to roadmapSvgWrap - // makes those customizations visible and removes extra boxes - const roadmapSvgWrap: HTMLElement = - document.getElementById('resource-svg-wrap')?.parentElement || - document.createElement('div'); - - if (!selectedTeamVersion) { - deleteUrlParam('t'); - renderResourceProgress(resourceType, resourceId).then(); - - roadmapSvgWrap.id = ''; - } else { - setUrlParams({ t: selectedTeamVersion.team._id! }); - - renderResourceProgress(resourceType, resourceId).then(() => { - selectedTeamVersion.config?.removed?.forEach((topic) => { - renderTopicProgress(topic, 'removed'); - }); - refreshProgressCounters(); - roadmapSvgWrap.id = 'customized-roadmap'; - }); - } - }, [selectedTeamVersion]); - - if (isPreparing) { - return null; - } - - if (!teamVersions.length) { - return null; - } - - return ( -
- - {isDropdownOpen && ( - <> - - ); -} diff --git a/src/components/TopicSearch/TopicSearch.astro b/src/components/TopicSearch/TopicSearch.astro deleted file mode 100644 index 37d879989..000000000 --- a/src/components/TopicSearch/TopicSearch.astro +++ /dev/null @@ -1,19 +0,0 @@ ---- -import Icon from '../AstroIcon.astro'; ---- - - - -
- - - - - -
diff --git a/src/components/TopicSearch/topics.js b/src/components/TopicSearch/topics.js deleted file mode 100644 index 26b31edee..000000000 --- a/src/components/TopicSearch/topics.js +++ /dev/null @@ -1,46 +0,0 @@ -class Topics { - constructor() { - this.topicSearchId = 'search-topic-input'; - - this.onDOMLoaded = this.onDOMLoaded.bind(this); - this.init = this.init.bind(this); - this.filterTopicNodes = this.filterTopicNodes.bind(this); - } - - get topicSearchEl() { - return document.getElementById(this.topicSearchId); - } - - filterTopicNodes(e) { - const value = e.target.value.trim().toLowerCase(); - if (!value) { - document - .querySelectorAll(`[data-topic]`) - .forEach((item) => item.classList.remove('hidden')); - return; - } - - document - .querySelectorAll(`[data-topic]`) - .forEach((item) => item.classList.add('hidden')); - - document - .querySelectorAll(`[data-topic*="${value}"]`) - .forEach((item) => item.classList.remove('hidden')); - } - - onDOMLoaded() { - if (!this.topicSearchEl) { - return; - } - - this.topicSearchEl.addEventListener('keyup', this.filterTopicNodes); - } - - init() { - window.addEventListener('DOMContentLoaded', this.onDOMLoaded); - } -} - -const topicRef = new Topics(); -topicRef.init(); diff --git a/src/components/UpdateEmail/UpdateEmailForm.tsx b/src/components/UpdateEmail/UpdateEmailForm.tsx deleted file mode 100644 index 23a2d394e..000000000 --- a/src/components/UpdateEmail/UpdateEmailForm.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { type FormEvent, useState } from 'react'; -import { httpPatch } from '../../lib/http'; -import { pageProgressMessage } from '../../stores/page'; -import { useToast } from '../../hooks/use-toast'; -import { cn } from '../../lib/classname'; -import { ArrowUpRight, X } from 'lucide-react'; - -type UpdateEmailFormProps = { - authProvider: string; - currentEmail: string; - newEmail?: string; - onSendVerificationCode?: (newEmail: string) => void; - onVerificationCancel?: () => void; -}; - -export function UpdateEmailForm(props: UpdateEmailFormProps) { - const { - authProvider, - currentEmail, - newEmail: defaultNewEmail = '', - onSendVerificationCode, - onVerificationCancel, - } = props; - const toast = useToast(); - - const [isLoading, setIsLoading] = useState(false); - const [isSubmitted, setIsSubmitted] = useState(defaultNewEmail !== ''); - const [newEmail, setNewEmail] = useState(defaultNewEmail); - const [isResendDone, setIsResendDone] = useState(false); - - const handleSentVerificationCode = async (e: FormEvent) => { - e.preventDefault(); - if (!newEmail || !newEmail.includes('@') || isSubmitted) { - return; - } - - setIsLoading(true); - pageProgressMessage.set('Sending verification code'); - const { response, error } = await httpPatch( - `${import.meta.env.PUBLIC_API_URL}/v1-update-user-email`, - { email: newEmail }, - ); - - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - setIsLoading(false); - pageProgressMessage.set(''); - - return; - } - - pageProgressMessage.set(''); - setIsLoading(false); - setIsSubmitted(true); - onSendVerificationCode?.(newEmail); - }; - - const handleResendVerificationCode = async () => { - if (isResendDone) { - toast.error('You have already resent the verification code'); - return; - } - - setIsLoading(true); - pageProgressMessage.set('Resending verification code'); - const { response, error } = await httpPatch( - `${import.meta.env.PUBLIC_API_URL}/v1-resend-email-verification-code`, - { email: newEmail }, - ); - - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - setIsLoading(false); - pageProgressMessage.set(''); - - return; - } - - toast.success('Verification code has been resent'); - pageProgressMessage.set(''); - setIsResendDone(true); - setIsLoading(false); - }; - - const handleCancelEmailVerification = async () => { - setIsLoading(true); - pageProgressMessage.set('Cancelling email verification'); - const { response, error } = await httpPatch( - `${import.meta.env.PUBLIC_API_URL}/v1-cancel-email-verification`, - {}, - ); - - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - setIsLoading(false); - pageProgressMessage.set(''); - - return; - } - - pageProgressMessage.set(''); - onVerificationCancel?.(); - setIsSubmitted(false); - setNewEmail(''); - setIsLoading(false); - }; - - if (authProvider && authProvider !== 'email') { - return ( -
-

Update Email

-

- You have used {authProvider} when signing up. Please set your password - first. -

- -
- - -
-

- Please set your password first to update your email. -

-
- ); - } - - return ( - <> -
-

Update Email

-

- Use the form below to update your email. -

-
- -
-
- - -
-
-
- - - {isSubmitted && ( -
- -
- )} -
- setNewEmail(e.target.value)} - disabled={isSubmitted} - /> - {!isSubmitted && ( - - )} - {isSubmitted && ( - <> - -
- - A verification link has been sent to your{' '} - new email address. Please follow the instructions - in email to verify and update your email. - -
- - )} -
-
- - ); -} diff --git a/src/components/UpdatePassword/UpdatePasswordForm.tsx b/src/components/UpdatePassword/UpdatePasswordForm.tsx deleted file mode 100644 index 94e99bcc5..000000000 --- a/src/components/UpdatePassword/UpdatePasswordForm.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { type FormEvent, useState } from 'react'; -import { httpPost } from '../../lib/http'; -import { useToast } from '../../hooks/use-toast'; - -type UpdatePasswordFormProps = { - authProvider: string; -}; - -export default function UpdatePasswordForm(props: UpdatePasswordFormProps) { - const { authProvider } = props; - - const toast = useToast(); - - const [currentPassword, setCurrentPassword] = useState(''); - const [newPassword, setNewPassword] = useState(''); - const [newPasswordConfirmation, setNewPasswordConfirmation] = useState(''); - - const [isLoading, setIsLoading] = useState(false); - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setIsLoading(true); - - if (newPassword !== newPasswordConfirmation) { - toast.error('Passwords do not match'); - setIsLoading(false); - - return; - } - - const { response, error } = await httpPost( - `${import.meta.env.PUBLIC_API_URL}/v1-update-password`, - { - oldPassword: authProvider === 'email' ? currentPassword : 'social-auth', - password: newPassword, - confirmPassword: newPasswordConfirmation, - }, - ); - - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - setIsLoading(false); - - return; - } - - setCurrentPassword(''); - setNewPassword(''); - setNewPasswordConfirmation(''); - toast.success('Password updated successfully'); - setIsLoading(false); - setTimeout(() => { - window.location.reload(); - }, 1000); - }; - - return ( -
-
-

Password

-

- Use the form below to update your password. -

-
-
- {authProvider === 'email' && ( -
- - - setCurrentPassword((e.target as HTMLInputElement).value) - } - /> -
- )} - -
- - - setNewPassword((e.target as HTMLInputElement).value) - } - /> -
-
- - - setNewPasswordConfirmation((e.target as HTMLInputElement).value) - } - /> -
- - -
-
- ); -} diff --git a/src/components/UpdateProfile/ProfileUsername.tsx b/src/components/UpdateProfile/ProfileUsername.tsx deleted file mode 100644 index aeb9b9bc4..000000000 --- a/src/components/UpdateProfile/ProfileUsername.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { useEffect, useState } from 'react'; -import type { AllowedProfileVisibility } from '../../api/user'; -import { httpPost } from '../../lib/http'; -import { useToast } from '../../hooks/use-toast'; -import { CheckIcon, Loader2, X } from 'lucide-react'; -import { useDebounceValue } from '../../hooks/use-debounce.ts'; - -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(null); - const debouncedUsername = useDebounceValue(username, 500); - - useEffect(() => { - checkIsUnique(debouncedUsername).then(); - }, [debouncedUsername]); - - const checkIsUnique = async (username: string) => { - if (isLoading || !username) { - return; - } - - if (username.length < 3) { - setIsUnique(false); - 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); - }; - const USERNAME_REGEX = /^[a-zA-Z0-9]*$/; - const isUserNameValid = (value: string) => - USERNAME_REGEX.test(value) && value.length <= 20; - - return ( -
- -
- - roadmap.sh/u/ - - -
- { - // only allow letters, numbers - const keyCode = e.key; - if ( - !isUserNameValid(keyCode) && - !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight'].includes( - keyCode, - ) - ) { - e.preventDefault(); - } - }} - onInput={(e) => { - const value = (e.target as HTMLInputElement).value?.trim(); - if (!isUserNameValid(value)) { - return; - } - - setUsername((e.target as HTMLInputElement).value.toLowerCase()); - }} - required={profileVisibility === 'public'} - /> - - {username && ( - - {isLoading ? ( - - ) : isUnique === false ? ( - - ) : isUnique === true ? ( - - ) : null} - - )} -
-
-
- ); -} diff --git a/src/components/UpdateProfile/SkillProfileAlert.tsx b/src/components/UpdateProfile/SkillProfileAlert.tsx deleted file mode 100644 index a5fd1293d..000000000 --- a/src/components/UpdateProfile/SkillProfileAlert.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { CheckCircle, FileBadge } from 'lucide-react'; - -const ideas = [ - 'Add a link to your profile in your social media bio', - 'Include your profile link in your resume to showcase your skills', - 'Add a link to your profile in your email signature', - 'Showcase your skills in your GitHub profile', - 'Share your profile with potential employers', -]; - -export function SkillProfileAlert() { - return ( -
- - -

- Announcing Skill Profiles!{' '} -

-

- Create your skill profile to showcase your skills or learning progress. - Here are some of the ways you can use your skill profile: -

- -
- {ideas.map((idea) => ( -

- - {idea} -

- ))} -
- -

- Make sure to mark your expertise{' '} - - in the roadmaps. - -

-
- ); -} diff --git a/src/components/UpdateProfile/UpdateProfileForm.tsx b/src/components/UpdateProfile/UpdateProfileForm.tsx deleted file mode 100644 index bbf36599c..000000000 --- a/src/components/UpdateProfile/UpdateProfileForm.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { type FormEvent, useEffect, useState } from 'react'; -import { httpGet, httpPost } from '../../lib/http'; -import { pageProgressMessage } from '../../stores/page'; -import UploadProfilePicture from './UploadProfilePicture'; - -export function UpdateProfileForm() { - const [name, setName] = useState(''); - const [avatar, setAvatar] = useState(''); - const [email, setEmail] = useState(''); - const [username, setUsername] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setError(''); - setSuccess(''); - - const { response, error } = await httpPost( - `${import.meta.env.PUBLIC_API_URL}/v1-update-profile`, - { - name, - }, - ); - - if (error || !response) { - setIsLoading(false); - setError(error?.message || 'Something went wrong'); - - return; - } - - await loadProfile(); - setSuccess('Profile updated successfully'); - }; - - const loadProfile = async () => { - setIsLoading(true); - - const { error, response } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-me`, - ); - - if (error || !response) { - setIsLoading(false); - setError(error?.message || 'Something went wrong'); - - return; - } - - const { name, email, avatar, username } = response; - - setName(name); - setEmail(email); - setUsername(username); - setAvatar(avatar || ''); - - setIsLoading(false); - }; - - // Make a request to the backend to fill in the form with the current values - useEffect(() => { - loadProfile().finally(() => { - pageProgressMessage.set(''); - }); - }, []); - - return ( -
-
-

Basic Information

-

- Update and set up your public profile below. -

-
- -
-
- - setName((e.target as HTMLInputElement).value)} - /> -
- - - {error && ( -

{error}

- )} - - {success && ( -

- {success} -

- )} - - -
-
- ); -} diff --git a/src/components/UpdateProfile/UpdatePublicProfileForm.tsx b/src/components/UpdateProfile/UpdatePublicProfileForm.tsx deleted file mode 100644 index 57a8884f6..000000000 --- a/src/components/UpdateProfile/UpdatePublicProfileForm.tsx +++ /dev/null @@ -1,639 +0,0 @@ -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('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('all'); - const [customRoadmapVisibility, setCustomRoadmapVisibility] = - useState('all'); - const [roadmaps, setRoadmaps] = useState([]); - const [customRoadmaps, setCustomRoadmaps] = useState([]); - - 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([]); - - const [isLoading, setIsLoading] = useState(false); - const [isProfileUpdated, setIsProfileUpdated] = useState(false); - - const { isCopied, copyText } = useCopyText(); - - const handleSubmit = async (e: FormEvent) => { - 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'); - setIsProfileUpdated(true); - }; - - 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 { - 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 && r.id && r.title, - ); - const publicRoadmaps = profileRoadmaps.filter( - (r) => !r.isCustomResource && r.id && r.title, - ); - - return ( -
- {isCreatingRoadmap && ( - setIsCreatingRoadmap(false)} /> - )} - - - -
-
-

Skill Profile

- {publicProfileUrl && ( - <> - - - Visit - - - - )} -
- -
-

- Create your skill profile to showcase your skills. -

- - - -
-
- - setName((e.target as HTMLInputElement).value)} - /> -
- -
- - -
-
- setIsEmailVisible(e.target.checked)} - /> - -
-
-
- -
- - setHeadline((e.target as HTMLInputElement).value)} - required={profileVisibility === 'public'} - /> -
- - - -
-

- Which roadmap progresses do you want to show on your profile? -

-
- { - setRoadmapVisibility('all'); - setRoadmaps([]); - }} - /> - { - setRoadmapVisibility('none'); - setRoadmaps([]); - }} - /> -
- -

- Or select the roadmaps you want to show -

- {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]); - } - }} - /> - ))} -
- ) : ( -

- Update{' '} - - your progress on roadmaps - {' '} - to show your learning activity. -

- )} -
- -
-

- Pick your custom roadmaps to show on your profile -

-
- { - setCustomRoadmapVisibility('all'); - setCustomRoadmaps([]); - }} - /> - { - setCustomRoadmapVisibility('none'); - setCustomRoadmaps([]); - }} - /> -
- -

- Or select the custom roadmaps you want to show -

- {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 do not have any custom roadmaps.{' '} - - . -

- )} -
- -
- - setGithub((e.target as HTMLInputElement).value)} - /> -
-
- - setTwitter((e.target as HTMLInputElement).value)} - /> -
- -
- - setLinkedin((e.target as HTMLInputElement).value)} - /> -
-
- - setDailydev((e.target as HTMLInputElement).value)} - /> -
- -
- - setWebsite((e.target as HTMLInputElement).value)} - /> -
- -
-
- setIsAvailableForHire(e.target.checked)} - /> - -
-
- - - {isProfileUpdated && publicProfileUrl && ( -
- - - - View Profile - -
- )} - -
- ); -} diff --git a/src/components/UpdateProfile/UploadProfilePicture.tsx b/src/components/UpdateProfile/UploadProfilePicture.tsx deleted file mode 100644 index 2e45febd0..000000000 --- a/src/components/UpdateProfile/UploadProfilePicture.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import { type ChangeEvent, type FormEvent, useEffect, useRef, useState } from 'react'; -import { TOKEN_COOKIE_NAME, removeAuthToken } from '../../lib/jwt'; - -interface PreviewFile extends File { - preview: string; -} - -type UploadProfilePictureProps = { - isDisabled?: boolean; - avatarUrl: string; - type: 'avatar' | 'logo'; - label?: string; - teamId?: string; -}; - -function getDimensions(file: File) { - return new Promise<{ - width: number; - height: number; - }>((resolve) => { - const img = new Image(); - - img.onload = () => { - resolve({ width: img.width, height: img.height }); - }; - - img.onerror = () => { - resolve({ width: 0, height: 0 }); - }; - - img.src = URL.createObjectURL(file); - }); -} - -async function validateImage(file: File): Promise { - const dimensions = await getDimensions(file); - - if (dimensions.width > 3000 || dimensions.height > 3000) { - return 'Image dimensions are too big. Maximum 3000x3000 pixels.'; - } - - if (dimensions.width < 100 || dimensions.height < 100) { - return 'Image dimensions are too small. Minimum 100x100 pixels.'; - } - - if (file.size > 1024 * 1024) { - return 'Image size is too big. Maximum 1MB.'; - } - - return null; -} - -export default function UploadProfilePicture(props: UploadProfilePictureProps) { - const { avatarUrl, teamId, type, isDisabled = false } = props; - - const [file, setFile] = useState(null); - const [error, setError] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const inputRef = useRef(null); - - const onImageChange = async (e: ChangeEvent) => { - setError(''); - - const file = (e.target as HTMLInputElement).files?.[0]; - if (!file) { - return; - } - - const error = await validateImage(file); - if (error) { - setError(error); - return; - } - - setFile( - Object.assign(file, { - preview: URL.createObjectURL(file), - }) - ); - }; - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setError(''); - setIsLoading(true); - - if (!file) { - return; - } - - const formData = new FormData(); - formData.append('name', 'avatar'); - formData.append('avatar', file); - - // FIXME: Use `httpCall` helper instead of fetch - let res: Response; - if (type === 'avatar') { - res = await fetch( - `${import.meta.env.PUBLIC_API_URL}/v1-upload-profile-picture`, - { - method: 'POST', - body: formData, - credentials: 'include', - } - ); - } else { - res = await fetch( - `${import.meta.env.PUBLIC_API_URL}/v1-upload-team-logo/${teamId}`, - { - method: 'POST', - body: formData, - credentials: 'include', - } - ); - } - - if (res.ok) { - window.location.reload(); - return; - } - - const data = await res.json(); - - setError(data?.message || 'Something went wrong'); - setIsLoading(false); - - // Logout user if token is invalid - if (data.status === 401) { - removeAuthToken(); - window.location.reload(); - } - }; - - useEffect(() => { - // Necessary to revoke the preview URL when the component unmounts for avoiding memory leaks - return () => { - if (file) { - URL.revokeObjectURL(file.preview); - } - }; - }, [file]); - - return ( -
- {props.label && ( - - )} -
- - - - {file && ( -
- - -
- )} -
- {error && ( -

{error}

- )} -
- ); -} diff --git a/src/components/UpdateProfile/VisibilityDropdown.tsx b/src/components/UpdateProfile/VisibilityDropdown.tsx deleted file mode 100644 index 6e3354b9f..000000000 --- a/src/components/UpdateProfile/VisibilityDropdown.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { ChevronDown, Globe, LockIcon } from 'lucide-react'; -import { type AllowedProfileVisibility } from '../../api/user.ts'; -import { pageProgressMessage } from '../../stores/page.ts'; -import { httpPatch } from '../../lib/http.ts'; -import { useToast } from '../../hooks/use-toast.ts'; -import { useRef, useState } from 'react'; -import { useOutsideClick } from '../../hooks/use-outside-click.ts'; -import { cn } from '../../lib/classname.ts'; - -type VisibilityDropdownProps = { - visibility: AllowedProfileVisibility; - setVisibility: (visibility: AllowedProfileVisibility) => void; -}; - -export function VisibilityDropdown(props: VisibilityDropdownProps) { - const { visibility, setVisibility } = props; - const toast = useToast(); - const dropdownRef = useRef(null); - - useOutsideClick(dropdownRef, () => { - setIsVisibilityDropdownOpen(false); - }); - - const [isVisibilityDropdownOpen, setIsVisibilityDropdownOpen] = - useState(false); - - async function updateProfileVisibility(visibility: AllowedProfileVisibility) { - pageProgressMessage.set('Updating profile visibility'); - setIsVisibilityDropdownOpen(false); - - const { error } = await httpPatch( - `${import.meta.env.PUBLIC_API_URL}/v1-update-public-profile-visibility`, - { - profileVisibility: visibility, - }, - ); - - if (error) { - toast.error(error.message || 'Something went wrong'); - - return; - } - - pageProgressMessage.set(''); - setVisibility(visibility); - } - - return ( -
- - {isVisibilityDropdownOpen && ( -
- - -
- )} -
- ); -} diff --git a/src/pages/account/billing.astro b/src/pages/account/billing.astro deleted file mode 100644 index 1ef9f5237..000000000 --- a/src/pages/account/billing.astro +++ /dev/null @@ -1,16 +0,0 @@ ---- -import AccountSidebar from '../../components/AccountSidebar.astro'; -import AccountLayout from '../../layouts/AccountLayout.astro'; -import { BillingPage } from '../../components/Billing/BillingPage'; ---- - - - - - - \ No newline at end of file diff --git a/src/pages/account/friends.astro b/src/pages/account/friends.astro deleted file mode 100644 index ebaef5450..000000000 --- a/src/pages/account/friends.astro +++ /dev/null @@ -1,16 +0,0 @@ ---- -import AccountSidebar from '../../components/AccountSidebar.astro'; -import AccountLayout from '../../layouts/AccountLayout.astro'; -import { FriendsPage } from '../../components/Friends/FriendsPage'; ---- - - - - - - diff --git a/src/pages/account/index.astro b/src/pages/account/index.astro deleted file mode 100644 index 823ef6a45..000000000 --- a/src/pages/account/index.astro +++ /dev/null @@ -1,16 +0,0 @@ ---- -import AccountSidebar from '../../components/AccountSidebar.astro'; -import { ActivityPage } from '../../components/Activity/ActivityPage'; -import AccountLayout from '../../layouts/AccountLayout.astro'; ---- - - - - - - diff --git a/src/pages/account/notification.astro b/src/pages/account/notification.astro deleted file mode 100644 index 2f3cc80c6..000000000 --- a/src/pages/account/notification.astro +++ /dev/null @@ -1,16 +0,0 @@ ---- -import AccountSidebar from '../../components/AccountSidebar.astro'; -import { NotificationPage } from '../../components/Notification/NotificationPage'; -import AccountLayout from '../../layouts/AccountLayout.astro'; ---- - - - - - - diff --git a/src/pages/account/road-card.astro b/src/pages/account/road-card.astro deleted file mode 100644 index 48c3e2274..000000000 --- a/src/pages/account/road-card.astro +++ /dev/null @@ -1,16 +0,0 @@ ---- -import AccountSidebar from '../../components/AccountSidebar.astro'; -import AccountLayout from '../../layouts/AccountLayout.astro'; -import { RoadCardPage } from '../../components/RoadCard/RoadCardPage'; ---- - - - - - - diff --git a/src/pages/account/roadmaps.astro b/src/pages/account/roadmaps.astro deleted file mode 100644 index bea17e7e6..000000000 --- a/src/pages/account/roadmaps.astro +++ /dev/null @@ -1,16 +0,0 @@ ---- -import AccountSidebar from '../../components/AccountSidebar.astro'; -import AccountLayout from '../../layouts/AccountLayout.astro'; -import { RoadmapListPage } from '../../components/CustomRoadmap/RoadmapListPage'; ---- - - - - - - diff --git a/src/pages/account/settings.astro b/src/pages/account/settings.astro deleted file mode 100644 index dc52edd64..000000000 --- a/src/pages/account/settings.astro +++ /dev/null @@ -1,20 +0,0 @@ ---- -import AccountSidebar from '../../components/AccountSidebar.astro'; -import AccountLayout from '../../layouts/AccountLayout.astro'; -import DeleteAccount from '../../components/DeleteAccount/DeleteAccount.astro'; -import { ProfileSettingsPage } from '../../components/ProfileSettings/ProfileSettingsPage'; ---- - - - - -
- -
-
diff --git a/src/pages/account/update-profile.astro b/src/pages/account/update-profile.astro deleted file mode 100644 index 6e84eaa4e..000000000 --- a/src/pages/account/update-profile.astro +++ /dev/null @@ -1,16 +0,0 @@ ---- -import AccountSidebar from '../../components/AccountSidebar.astro'; -import { UpdatePublicProfileForm } from '../../components/UpdateProfile/UpdatePublicProfileForm'; -import AccountLayout from '../../layouts/AccountLayout.astro'; ---- - - - - - - diff --git a/src/pages/team/activity.astro b/src/pages/team/activity.astro deleted file mode 100644 index 964f0ed4b..000000000 --- a/src/pages/team/activity.astro +++ /dev/null @@ -1,15 +0,0 @@ ---- -import { TeamSidebar } from '../../components/TeamSidebar'; -import { TeamActivityPage } from '../../components/TeamActivity/TeamActivityPage'; -import AccountLayout from '../../layouts/AccountLayout.astro'; ---- - - - - - - diff --git a/src/pages/team/index.astro b/src/pages/team/index.astro deleted file mode 100644 index 3db25ade8..000000000 --- a/src/pages/team/index.astro +++ /dev/null @@ -1,69 +0,0 @@ ---- -import { DashboardPage } from '../../components/Dashboard/DashboardPage'; -import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getAllBestPractices } from '../../lib/best-practice'; -import { getRoadmapsByTag } from '../../lib/roadmap'; - -const roleRoadmaps = await getRoadmapsByTag('role-roadmap'); -const skillRoadmaps = await getRoadmapsByTag('skill-roadmap'); -const bestPractices = await getAllBestPractices(); - -const enrichedRoleRoadmaps = roleRoadmaps - .filter((roadmapItem) => !roadmapItem.frontmatter.isHidden) - .map((roadmap) => { - const { frontmatter } = roadmap; - - return { - id: roadmap.id, - url: `/${roadmap.id}`, - title: frontmatter.briefTitle, - description: frontmatter.briefDescription, - relatedRoadmapIds: frontmatter.relatedRoadmaps, - renderer: frontmatter.renderer, - metadata: { - tags: frontmatter.tags, - }, - }; - }); -const enrichedSkillRoadmaps = skillRoadmaps - .filter((roadmapItem) => !roadmapItem.frontmatter.isHidden) - .map((roadmap) => { - const { frontmatter } = roadmap; - - return { - id: roadmap.id, - url: `/${roadmap.id}`, - title: - frontmatter.briefTitle === 'Go' ? 'Go Roadmap' : frontmatter.briefTitle, - description: frontmatter.briefDescription, - relatedRoadmapIds: frontmatter.relatedRoadmaps, - renderer: frontmatter.renderer, - metadata: { - tags: frontmatter.tags, - }, - }; - }); - -const enrichedBestPractices = bestPractices.map((bestPractice) => { - const { frontmatter } = bestPractice; - - return { - id: bestPractice.id, - url: `/best-practices/${bestPractice.id}`, - title: frontmatter.briefTitle, - description: frontmatter.briefDescription, - }; -}); ---- - - - -
-
- diff --git a/src/pages/team/member.astro b/src/pages/team/member.astro deleted file mode 100644 index 43fd9dea7..000000000 --- a/src/pages/team/member.astro +++ /dev/null @@ -1,15 +0,0 @@ ---- -import { TeamSidebar } from '../../components/TeamSidebar'; -import AccountLayout from '../../layouts/AccountLayout.astro'; -import { TeamMemberDetailsPage } from '../../components/TeamMemberDetails/TeamMemberDetailsPage'; ---- - - - - - - diff --git a/src/pages/team/members.astro b/src/pages/team/members.astro deleted file mode 100644 index 22695fbce..000000000 --- a/src/pages/team/members.astro +++ /dev/null @@ -1,15 +0,0 @@ ---- -import { TeamSidebar } from '../../components/TeamSidebar'; -import { TeamMembersPage } from '../../components/TeamMembers/TeamMembersPage'; -import AccountLayout from '../../layouts/AccountLayout.astro'; ---- - - - - - - diff --git a/src/pages/team/new.astro b/src/pages/team/new.astro deleted file mode 100644 index d03b8e854..000000000 --- a/src/pages/team/new.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- -import AccountSidebar from '../../components/AccountSidebar.astro'; -import AccountLayout from '../../layouts/AccountLayout.astro'; -import { CreateTeamForm } from '../../components/CreateTeam/CreateTeamForm'; ---- - - - - - - diff --git a/src/pages/team/progress.astro b/src/pages/team/progress.astro deleted file mode 100644 index acdf0f19f..000000000 --- a/src/pages/team/progress.astro +++ /dev/null @@ -1,15 +0,0 @@ ---- -import { TeamSidebar } from '../../components/TeamSidebar'; -import { TeamProgressPage } from '../../components/TeamProgress/TeamProgressPage'; -import AccountLayout from '../../layouts/AccountLayout.astro'; ---- - - - - - - diff --git a/src/pages/team/roadmaps.astro b/src/pages/team/roadmaps.astro deleted file mode 100644 index 29235295b..000000000 --- a/src/pages/team/roadmaps.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- -import { TeamSidebar } from '../../components/TeamSidebar'; -import { TeamRoadmaps } from '../../components/TeamRoadmapsList/TeamRoadmaps'; -import AccountLayout from '../../layouts/AccountLayout.astro'; ---- - - - - - - diff --git a/src/pages/team/settings.astro b/src/pages/team/settings.astro deleted file mode 100644 index ff31c3deb..000000000 --- a/src/pages/team/settings.astro +++ /dev/null @@ -1,15 +0,0 @@ ---- -import { TeamSidebar } from '../../components/TeamSidebar'; -import { UpdateTeamForm } from '../../components/TeamSettings/UpdateTeamForm'; -import AccountLayout from '../../layouts/AccountLayout.astro'; ---- - - - - - -