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'; 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); 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) { if (!teamId) { return; } toast.loading('Adding roadmap'); pageProgressMessage.set('Adding roadmap'); setIsLoading(true); const { error, response } = await httpPut( `${ import.meta.env.PUBLIC_API_URL }/v1-update-team-resource-config/${teamId}`, { teamId: teamId, resourceId: roadmapId, resourceType: 'roadmap', removed: [], }, ); if (error || !response) { toast.error(error?.message || 'Error adding roadmap'); return; } setTeamResources(response); toast.success('Roadmap added'); } 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 addRoadmapModal = isAddingRoadmap && ( setIsAddingRoadmap(false)} teamResourceConfig={teamResources} allRoadmaps={allRoadmaps} teamId={teamId} onRoadmapAdd={(roadmapId: string) => { onAdd(roadmapId).finally(() => { pageProgressMessage.set(''); }); }} onRoadmapRemove={(roadmapId: string) => { if (confirm('Are you sure you want to remove this roadmap?')) { onRemove(roadmapId).finally(() => {}); } }} /> ); 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}

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} {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}
); }