Merge branch 'master' into chore/update-progress

chore/update-progress
Kamran Ahmed 2 years ago committed by GitHub
commit 7f5f96a6b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      src/components/CommandMenu/CommandMenu.tsx
  2. 47
      src/components/CreateTeam/NotDropdown.tsx
  3. 93
      src/components/CreateTeam/RoadmapSelector.tsx
  4. 152
      src/components/CreateTeam/SelectRoadmapModal.tsx
  5. 34
      src/components/CreateTeam/SelectRoadmapModalItem.tsx
  6. 4
      src/components/CreateTeam/Step0.tsx
  7. 22
      src/components/CreateTeam/Step1.tsx
  8. 7
      src/components/CreateTeam/Step2.tsx
  9. 19
      src/components/TeamDropdown/TeamDropdown.tsx
  10. 4
      src/components/TeamMembers/TeamMembersPage.tsx
  11. 19
      src/components/TeamProgress/GroupRoadmapItem.tsx
  12. 21
      src/components/TeamProgress/MemberProgressItem.tsx
  13. 23
      src/components/TeamProgress/TeamProgressPage.tsx
  14. 127
      src/components/TeamRoadmaps.tsx
  15. 28
      src/components/TeamSidebar.tsx
  16. 2
      src/components/Toast.tsx
  17. 3
      src/pages/pages.json.ts

@ -18,6 +18,7 @@ export type PageType = {
group: string; group: string;
icon?: string; icon?: string;
isProtected?: boolean; isProtected?: boolean;
metadata?: Record<string, any>;
}; };
const defaultPages: PageType[] = [ const defaultPages: PageType[] = [

@ -0,0 +1,47 @@
import ChevronDownIcon from '../../icons/chevron-down.svg';
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 (
<div
className="flex cursor-text items-center justify-between rounded-md border border-gray-300 px-3 py-2.5 hover:border-gray-400/50 hover:bg-gray-50"
role="button"
onClick={onClick}
>
{selectedCount > 0 && (
<div className="flex flex-col">
<p className="mb-1.5 text-base font-medium text-gray-800">
{selectedCount} {singularOrPlural} selected
</p>
<p className="text-sm text-gray-400">
Click to add or change selection
</p>
</div>
)}
{selectedCount === 0 && (
<div className="flex flex-col">
<p className="text-base text-gray-400">
Click to select {pluralName}
</p>
</div>
)}
<img
alt={singularName}
src={ChevronDownIcon}
className={'relative top-[1px] h-[17px] w-[17px] opacity-40'}
/>
</div>
);
}

@ -1,11 +1,12 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'preact/hooks';
import { SearchSelector } from '../SearchSelector';
import { httpGet, httpPut } from '../../lib/http'; import { httpGet, httpPut } from '../../lib/http';
import type { PageType } from '../CommandMenu/CommandMenu'; import type { PageType } from '../CommandMenu/CommandMenu';
import SearchIcon from '../../icons/search.svg'; import ChevronDownIcon from '../../icons/chevron-down.svg';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
import type { TeamDocument } from './CreateTeamForm'; import type { TeamDocument } from './CreateTeamForm';
import { UpdateTeamResourceModal } from './UpdateTeamResourceModal'; import { UpdateTeamResourceModal } from './UpdateTeamResourceModal';
import { SelectRoadmapModal } from './SelectRoadmapModal';
import { NotDropdown } from './NotDropdown';
export type TeamResourceConfig = { export type TeamResourceConfig = {
resourceId: string; resourceId: string;
@ -14,14 +15,15 @@ export type TeamResourceConfig = {
}[]; }[];
type RoadmapSelectorProps = { type RoadmapSelectorProps = {
team: TeamDocument; teamId: string;
teamResourceConfig: TeamResourceConfig; teamResourceConfig: TeamResourceConfig;
setTeamResourceConfig: (config: TeamResourceConfig) => void; setTeamResourceConfig: (config: TeamResourceConfig) => void;
}; };
export function RoadmapSelector(props: RoadmapSelectorProps) { export function RoadmapSelector(props: RoadmapSelectorProps) {
const { team, teamResourceConfig = [], setTeamResourceConfig } = props; const { teamId, teamResourceConfig = [], setTeamResourceConfig } = props;
const [showSelectRoadmapModal, setShowSelectRoadmapModal] = useState(false);
const [allRoadmaps, setAllRoadmaps] = useState<PageType[]>([]); const [allRoadmaps, setAllRoadmaps] = useState<PageType[]>([]);
const [changingRoadmapId, setChangingRoadmapId] = useState<string>(''); const [changingRoadmapId, setChangingRoadmapId] = useState<string>('');
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
@ -50,15 +52,15 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
} }
async function deleteResource(roadmapId: string) { async function deleteResource(roadmapId: string) {
if (!team?._id) { if (!teamId) {
return; return;
} }
pageProgressMessage.set(`Deleting resource`); pageProgressMessage.set(`Deleting resource`);
const { error, response } = await httpPut<TeamResourceConfig>( const { error, response } = await httpPut<TeamResourceConfig>(
`${import.meta.env.PUBLIC_API_URL}/v1-delete-team-resource-config/${ `${
team._id import.meta.env.PUBLIC_API_URL
}`, }/v1-delete-team-resource-config/${teamId}`,
{ {
resourceId: roadmapId, resourceId: roadmapId,
resourceType: 'roadmap', resourceType: 'roadmap',
@ -82,17 +84,17 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
} }
async function addTeamResource(roadmapId: string) { async function addTeamResource(roadmapId: string) {
if (!team?._id) { if (!teamId) {
return; return;
} }
pageProgressMessage.set(`Adding roadmap to team`); pageProgressMessage.set(`Adding roadmap to team`);
const { error, response } = await httpPut<TeamResourceConfig>( const { error, response } = await httpPut<TeamResourceConfig>(
`${import.meta.env.PUBLIC_API_URL}/v1-update-team-resource-config/${ `${
team._id import.meta.env.PUBLIC_API_URL
}`, }/v1-update-team-resource-config/${teamId}`,
{ {
teamId: team._id, teamId: teamId,
resourceId: roadmapId, resourceId: roadmapId,
resourceType: 'roadmap', resourceType: 'roadmap',
removed: [], removed: [],
@ -118,7 +120,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
onClose={() => setChangingRoadmapId('')} onClose={() => setChangingRoadmapId('')}
resourceId={changingRoadmapId} resourceId={changingRoadmapId}
resourceType={'roadmap'} resourceType={'roadmap'}
teamId={team?._id!} teamId={teamId}
setTeamResourceConfig={setTeamResourceConfig} setTeamResourceConfig={setTeamResourceConfig}
defaultRemovedItems={ defaultRemovedItems={
teamResourceConfig.find((c) => c.resourceId === changingRoadmapId) teamResourceConfig.find((c) => c.resourceId === changingRoadmapId)
@ -126,43 +128,38 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
} }
/> />
)} )}
{showSelectRoadmapModal && (
<SelectRoadmapModal
onClose={() => setShowSelectRoadmapModal(false)}
teamResourceConfig={teamResourceConfig}
allRoadmaps={allRoadmaps}
teamId={teamId}
onRoadmapAdd={(roadmapId) => {
addTeamResource(roadmapId).finally(() => {
pageProgressMessage.set('');
});
}}
onRoadmapRemove={(roadmapId) => {
onRemove(roadmapId).finally(() => {});
}}
/>
)}
<SearchSelector <div className="mt-3">
placeholder={`Search Roadmaps ..`} <NotDropdown
onSelect={(option) => { onClick={() => {
const roadmapId = option.value; setShowSelectRoadmapModal(true);
addTeamResource(roadmapId).finally(() => { }}
pageProgressMessage.set(''); selectedCount={teamResourceConfig.length}
}); singularName={'roadmap'}
}} pluralName={'roadmaps'}
options={allRoadmaps />
.filter((roadmap) => { </div>
return !teamResourceConfig
.map((c) => c.resourceId)
.includes(roadmap.id);
})
.map((roadmap) => ({
value: roadmap.id,
label: roadmap.title,
}))}
searchInputId={'roadmap-input'}
inputClassName="mt-2 block w-full rounded-md border px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
/>
{!teamResourceConfig.length && ( {!teamResourceConfig.length && (
<div className="mt-4 rounded-md border px-4 py-12 text-center text-sm text-gray-700"> <p className={'mb-3 mt-2 text-base text-gray-400'}>
<img No roadmaps selected.
alt={'search'} </p>
src={SearchIcon}
className={'mx-auto mb-5 h-[42px] w-[42px] opacity-10'}
/>
<span className="block text-lg font-semibold text-black">
No roadmaps selected.
</span>
<p className={'text-sm text-gray-400'}>
Please search and add roadmaps from above
</p>
</div>
)} )}
{teamResourceConfig.length > 0 && ( {teamResourceConfig.length > 0 && (

@ -0,0 +1,152 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click';
import type { PageType } from '../CommandMenu/CommandMenu';
import type { TeamResourceConfig } from './RoadmapSelector';
import CloseIcon from '../../icons/close.svg';
import { SelectRoadmapModalItem } from './SelectRoadmapModalItem';
export type SelectRoadmapModalProps = {
teamId: string;
allRoadmaps: PageType[];
onClose: () => void;
teamResourceConfig: TeamResourceConfig;
onRoadmapAdd: (roadmapId: string) => void;
onRoadmapRemove: (roadmapId: string) => void;
};
export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
const {
onClose,
allRoadmaps,
onRoadmapAdd,
onRoadmapRemove,
teamResourceConfig,
} = props;
const popupBodyEl = useRef<HTMLDivElement>(null);
const searchInputEl = useRef<HTMLInputElement>(null);
const [searchResults, setSearchResults] = useState<PageType[]>(allRoadmaps);
const [searchText, setSearchText] = useState('');
useKeydown('Escape', () => {
onClose();
});
useOutsideClick(popupBodyEl, () => {
onClose();
});
useEffect(() => {
if (!searchInputEl.current) {
return;
}
searchInputEl.current.focus();
}, [searchInputEl]);
useEffect(() => {
if (searchText.length === 0) {
setSearchResults(allRoadmaps);
return;
}
const searchResults = allRoadmaps.filter((roadmap) => {
return (
roadmap.title.toLowerCase().includes(searchText.toLowerCase()) ||
roadmap.id.toLowerCase().includes(searchText.toLowerCase())
);
});
setSearchResults(searchResults);
}, [searchText, allRoadmaps]);
const roleBasedRoadmaps = searchResults.filter((roadmap) =>
roadmap?.metadata?.tags?.includes('role-roadmap')
);
const skillBasedRoadmaps = searchResults.filter((roadmap) =>
roadmap?.metadata?.tags?.includes('skill-roadmap')
);
return (
<div class="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div class="relative mx-auto h-full w-full max-w-2xl p-4 md:h-auto">
<div
ref={popupBodyEl}
class="popup-body relative mt-4 overflow-hidden rounded-lg bg-white shadow"
>
<button
type="button"
className="popup-close absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-100 hover:text-gray-900"
onClick={onClose}
>
<img alt={'close'} src={CloseIcon} className="h-4 w-4" />
<span class="sr-only">Close modal</span>
</button>
<input
ref={searchInputEl}
type="text"
placeholder="Search roadmaps"
className="block w-full border-b px-5 pb-3.5 pt-4 outline-none placeholder:text-gray-400"
value={searchText}
onInput={(e) => setSearchText((e.target as HTMLInputElement).value)}
/>
<div className="min-h-[200px] p-4">
<span className="block pb-3 text-xs uppercase text-gray-400">
Role Based Roadmaps
</span>
{roleBasedRoadmaps.length === 0 && (
<p className="mb-1 flex h-full items-start text-sm italic text-gray-400"></p>
)}
{roleBasedRoadmaps.length > 0 && (
<div className="mb-5 flex flex-wrap items-center gap-2">
{roleBasedRoadmaps.map((roadmap) => {
const isSelected = !!teamResourceConfig.find(
(r) => r.resourceId === roadmap.id
);
return (
<SelectRoadmapModalItem
title={roadmap.title}
isSelected={isSelected}
onClick={() => {
if (isSelected) {
onRoadmapRemove(roadmap.id);
} else {
onRoadmapAdd(roadmap.id);
}
}}
/>
);
})}
</div>
)}
<span className="block pb-3 text-xs uppercase text-gray-400">
Skill Based Roadmaps
</span>
<div className="flex flex-wrap items-center gap-2">
{skillBasedRoadmaps.map((roadmap) => {
const isSelected = !!teamResourceConfig.find(
(r) => r.resourceId === roadmap.id
);
return (
<SelectRoadmapModalItem
title={roadmap.title}
isSelected={isSelected}
onClick={() => {
if (isSelected) {
onRoadmapRemove(roadmap.id);
} else {
onRoadmapAdd(roadmap.id);
}
}}
/>
);
})}
</div>
</div>
</div>
</div>
</div>
);
}

@ -0,0 +1,34 @@
import type { SelectRoadmapModalProps } from './SelectRoadmapModal';
type SelectRoadmapModalItemProps = {
title: string;
isSelected: boolean;
onClick: () => void;
};
export function SelectRoadmapModalItem(props: SelectRoadmapModalItemProps) {
const { isSelected, onClick, title } = props;
return (
<button
className={`group flex min-h-[35px] items-stretch overflow-hidden rounded-md text-sm ${
!isSelected
? 'border border-gray-300 hover:bg-gray-100'
: 'bg-black text-white transition-colors hover:bg-gray-700'
}`}
onClick={onClick}
>
<span className="flex items-center px-3">{title}</span>
{isSelected && (
<span className="flex items-center bg-gray-700 px-3 text-xs text-white transition-colors">
&times;
</span>
)}
{!isSelected && (
<span className="flex items-center bg-gray-100 px-2.5 text-xs text-gray-500">
+
</span>
)}
</button>
);
}

@ -87,10 +87,10 @@ export function Step0(props: Step0Props) {
validTeamType.value === selectedTeamType ? 'opacity-100' : '' validTeamType.value === selectedTeamType ? 'opacity-100' : ''
}`} }`}
/> />
<span className="mb-1.5 block text-2xl font-bold"> <span className="mb-2 block text-2xl font-bold">
{validTeamType.label} {validTeamType.label}
</span> </span>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500 leading-[21px]">
{validTeamType.description} {validTeamType.description}
</span> </span>
</button> </button>

@ -5,10 +5,12 @@ import type { TeamDocument } from './CreateTeamForm';
import { NextButton } from './NextButton'; import { NextButton } from './NextButton';
export const validTeamSizes = [ export const validTeamSizes = [
'0-1', '1-5',
'2-10', '6-10',
'11-50', '11-25',
'51-200', '26-50',
'51-100',
'101-200',
'201-500', '201-500',
'501-1000', '501-1000',
'1000+', '1000+',
@ -134,7 +136,7 @@ export function Step1(props: Step1Props) {
autofocus={true} autofocus={true}
id="name" id="name"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="roadmap.sh" placeholder="Roadmap Inc."
disabled={isLoading} disabled={isLoading}
required required
value={name} value={name}
@ -167,7 +169,7 @@ export function Step1(props: Step1Props) {
{selectedTeamType === 'company' && ( {selectedTeamType === 'company' && (
<div className="mt-4 flex w-full flex-col"> <div className="mt-4 flex w-full flex-col">
<label for="website" className="text-sm leading-none text-slate-500"> <label for="website" className="text-sm leading-none text-slate-500">
LinkedIn URL Company LinkedIn URL
</label> </label>
<input <input
type="url" type="url"
@ -206,7 +208,7 @@ export function Step1(props: Step1Props) {
for="team-size" for="team-size"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]' className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
> >
Company Size Tech Team Size
</label> </label>
<select <select
name="team-size" name="team-size"
@ -229,6 +231,12 @@ export function Step1(props: Step1Props) {
</div> </div>
)} )}
{error && (
<div className="mt-4 flex w-full flex-col">
<span className="text-sm text-red-500">{error}</span>
</div>
)}
<div className="mt-4 flex flex-row items-center justify-between gap-2"> <div className="mt-4 flex flex-row items-center justify-between gap-2">
<button <button
type="button" type="button"

@ -17,15 +17,14 @@ export function Step2(props: Step2Props) {
<> <>
<div className="mt-4 flex w-full flex-col"> <div className="mt-4 flex w-full flex-col">
<div className="mb-1 mt-2"> <div className="mb-1 mt-2">
<h2 className="mb-2 text-2xl font-bold">Select Roadmaps</h2> <h2 className="mb-1.5 text-2xl font-bold">Select Roadmaps</h2>
<p className="text-sm text-gray-700"> <p className="text-sm text-gray-700">
Picks the roadmaps to be made available to your team for tracking. You can always add and customize your roadmaps later.
You can always add more later.
</p> </p>
</div> </div>
<RoadmapSelector <RoadmapSelector
team={team} teamId={team._id!}
teamResourceConfig={teamResourceConfig} teamResourceConfig={teamResourceConfig}
setTeamResourceConfig={setTeamResourceConfig} setTeamResourceConfig={setTeamResourceConfig}
/> />

@ -95,15 +95,14 @@ export function TeamDropdown() {
{pendingTeamIds.length} {pendingTeamIds.length}
</span> </span>
)} )}
<div className="flex items-center gap-2"> <div className="inline-grid grid-cols-[16px_auto] items-center gap-1.5 mr-1.5">
{isLoading && <Spinner className="h-4 w-4" isDualRing={false} />} {isLoading && <Spinner className="h-4 w-4" isDualRing={false} />}
{!isLoading && ( {!isLoading && (
<img <img
src={ src={
selectedAvatar selectedAvatar
? `${ ? `${import.meta.env.PUBLIC_AVATAR_BASE_URL
import.meta.env.PUBLIC_AVATAR_BASE_URL }/${selectedAvatar}`
}/${selectedAvatar}`
: '/images/default-avatar.png' : '/images/default-avatar.png'
} }
alt="" alt=""
@ -140,28 +139,18 @@ export function TeamDropdown() {
pageLink = `/team/progress?t=${team._id}`; pageLink = `/team/progress?t=${team._id}`;
} }
if (team.roadmaps.length === 0) {
pageLink = `/team/new?t=${team._id}&s=2`;
}
return ( return (
<li> <li>
<a <a
className="flex w-full cursor-pointer items-center gap-2 rounded p-2 text-sm font-medium text-slate-100 hover:bg-slate-700" className="flex w-full cursor-pointer items-center gap-2 rounded p-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
href={`${pageLink}`} href={`${pageLink}`}
> >
<span className="flex-grow truncate">{team.name}</span> <span className="flex-grow min-w-0 truncate">{team.name}</span>
{pendingTeamIds.includes(team._id) && ( {pendingTeamIds.includes(team._id) && (
<span className="flex rounded-md bg-red-500 px-2 text-xs text-white"> <span className="flex rounded-md bg-red-500 px-2 text-xs text-white">
Invite Invite
</span> </span>
)} )}
{team.roadmaps.length === 0 && (
<span className="flex rounded-md bg-gray-500 px-2 text-xs text-white">
Draft
</span>
)}
</a> </a>
</li> </li>
); );

@ -163,8 +163,8 @@ export function TeamMembersPage() {
<MemberRoleBadge role={member.role} /> <MemberRoleBadge role={member.role} />
</span> </span>
<div className="flex items-center"> <div className="flex items-center">
<h3 className="flex items-center font-medium"> <h3 className="inline-grid grid-cols-[auto_auto] items-center font-medium">
{member.name} <span className="truncate">{member.name}</span>
{member.userId === user?.id && ( {member.userId === user?.id && (
<span className="ml-2 hidden text-xs font-normal text-blue-500 sm:inline"> <span className="ml-2 hidden text-xs font-normal text-blue-500 sm:inline">
You You

@ -3,6 +3,7 @@ import type { GroupByRoadmap, TeamMember } from './TeamProgressPage';
import { MemberProgressModal } from './MemberProgressModal'; import { MemberProgressModal } from './MemberProgressModal';
import { getUrlParams } from '../../lib/browser'; import { getUrlParams } from '../../lib/browser';
import ExternalLinkIcon from '../../icons/external-link.svg'; import ExternalLinkIcon from '../../icons/external-link.svg';
import { useAuth } from '../../hooks/use-auth';
type GroupRoadmapItemProps = { type GroupRoadmapItemProps = {
roadmap: GroupByRoadmap; roadmap: GroupByRoadmap;
@ -11,6 +12,7 @@ type GroupRoadmapItemProps = {
export function GroupRoadmapItem(props: GroupRoadmapItemProps) { export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
const { members, resourceTitle, resourceId } = props.roadmap; const { members, resourceTitle, resourceId } = props.roadmap;
const { t: teamId } = getUrlParams(); const { t: teamId } = getUrlParams();
const user = useAuth();
const [showAll, setShowAll] = useState(false); const [showAll, setShowAll] = useState(false);
const [detailResourceId, setDetailResourceId] = useState<string | null>(null); const [detailResourceId, setDetailResourceId] = useState<string | null>(null);
@ -49,10 +51,15 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
</div> </div>
<div className="relative flex grow flex-col space-y-2 p-3"> <div className="relative flex grow flex-col space-y-2 p-3">
{(showAll ? members : members.slice(0, 4)).map((member) => { {(showAll ? members : members.slice(0, 4)).map((member) => {
if (!member.progress) return null; const isMyProgress = user?.email === member?.member?.email;
if (!member.progress) {
return null;
}
return ( return (
<button <button
className="group relative w-full overflow-hidden rounded-md border p-2 hover:border-gray-300 hover:text-black focus:outline-none" className={`group relative w-full overflow-hidden rounded-md border p-2 hover:border-gray-300 hover:text-black focus:outline-none ${isMyProgress ? 'border-green-500 hover:border-green-600' : ''}`}
key={member?.member._id} key={member?.member._id}
onClick={() => { onClick={() => {
setDetailResourceId(member?.progress?.resourceId!); setDetailResourceId(member?.progress?.resourceId!);
@ -60,7 +67,7 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
}} }}
> >
<span className="relative z-10 flex items-center justify-between gap-1 text-sm"> <span className="relative z-10 flex items-center justify-between gap-1 text-sm">
<span className="inline-grid grid-cols-[20px_auto] gap-2"> <span className="inline-grid grid-cols-[20px_auto] gap-3">
<img <img
src={ src={
member.member.avatar member.member.avatar
@ -72,14 +79,16 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
alt={member.member.name || ''} alt={member.member.name || ''}
className="h-5 w-5 shrink-0 rounded-full" className="h-5 w-5 shrink-0 rounded-full"
/> />
<span className="truncate">{member?.member?.name}</span> <span className="inline-grid grid-cols-[auto,32px] items-center">
<span className="truncate mr-[5px]">{member?.member?.name}</span>
</span>
</span> </span>
<span className="shrink-0 text-xs text-gray-400"> <span className="shrink-0 text-xs text-gray-400">
{member?.progress?.done} / {member?.progress?.total} {member?.progress?.done} / {member?.progress?.total}
</span> </span>
</span> </span>
<span <span
className="absolute inset-0 bg-gray-100 group-hover:bg-gray-200" className={`absolute inset-0 ${isMyProgress ? 'bg-green-100 group-hover:bg-green-200' : 'bg-gray-100 group-hover:bg-gray-200'}`}
style={{ style={{
width: `${ width: `${
(member?.progress?.done / member?.progress?.total) * 100 (member?.progress?.done / member?.progress?.total) * 100

@ -5,9 +5,10 @@ import { MemberProgressModal } from './MemberProgressModal';
type MemberProgressItemProps = { type MemberProgressItemProps = {
teamId: string; teamId: string;
member: TeamMember; member: TeamMember;
isMyProgress?: boolean;
}; };
export function MemberProgressItem(props: MemberProgressItemProps) { export function MemberProgressItem(props: MemberProgressItemProps) {
const { member, teamId } = props; const { member, teamId, isMyProgress = false } = props;
const memberProgress = member?.progress?.sort((a, b) => { const memberProgress = member?.progress?.sort((a, b) => {
return b.done - a.done; return b.done - a.done;
@ -31,10 +32,10 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
)} )}
<div <div
className="flex h-full min-h-[270px] flex-col rounded-md border" className={`flex h-full min-h-[270px] flex-col overflow-hidden rounded-md border`}
key={member._id} key={member._id}
> >
<div className="flex items-center gap-3 border-b p-3"> <div className={`relative flex items-center gap-3 border-b p-3`}>
<img <img
src={ src={
member.avatar member.avatar
@ -44,8 +45,18 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
alt={member.name || ''} alt={member.name || ''}
className="h-8 w-8 rounded-full" className="h-8 w-8 rounded-full"
/> />
<div className="inline-grid"> <div className="inline-grid w-full">
<h3 className="truncate font-medium">{member.name}</h3> {!isMyProgress && (
<h3 className="truncate font-medium">{member.name}</h3>
)}
{isMyProgress && (
<div className="inline-grid grid-cols-[auto,32px] items-center gap-1.5">
<h3 className="truncate font-medium">{member.name}</h3>
<span className="rounded-md bg-red-500 py-0.5 px-1 text-xs text-white">
You
</span>
</div>
)}
<p className="truncate text-sm text-gray-500">{member.email}</p> <p className="truncate text-sm text-gray-500">{member.email}</p>
</div> </div>
</div> </div>

@ -7,8 +7,8 @@ import { useToast } from '../../hooks/use-toast';
import { useStore } from '@nanostores/preact'; import { useStore } from '@nanostores/preact';
import { $currentTeam } from '../../stores/team'; import { $currentTeam } from '../../stores/team';
import { GroupRoadmapItem } from './GroupRoadmapItem'; import { GroupRoadmapItem } from './GroupRoadmapItem';
import { setUrlParams } from '../../lib/browser'; import { getUrlParams, setUrlParams } from '../../lib/browser';
import { getUrlParams } from '../../lib/browser'; import { useAuth } from '../../hooks/use-auth';
export type UserProgress = { export type UserProgress = {
resourceTitle: string; resourceTitle: string;
@ -54,6 +54,7 @@ export function TeamProgressPage() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const toast = useToast(); const toast = useToast();
const currentTeam = useStore($currentTeam); const currentTeam = useStore($currentTeam);
const user = useAuth();
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]); const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
const [selectedGrouping, setSelectedGrouping] = useState< const [selectedGrouping, setSelectedGrouping] = useState<
@ -69,7 +70,17 @@ export function TeamProgressPage() {
return; return;
} }
setTeamMembers(response); setTeamMembers(
response.sort((a, b) => {
if (a.email === user?.email) {
return -1;
}
if (b.email === user?.email) {
return 1;
}
return 0;
})
);
} }
useEffect(() => { useEffect(() => {
@ -159,7 +170,11 @@ export function TeamProgressPage() {
{selectedGrouping === 'member' && ( {selectedGrouping === 'member' && (
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
{teamMembers.map((member) => ( {teamMembers.map((member) => (
<MemberProgressItem teamId={teamId} member={member} /> <MemberProgressItem
teamId={teamId}
member={member}
isMyProgress={member?.email === user?.email}
/>
))} ))}
</div> </div>
)} )}

@ -5,13 +5,14 @@ import type { TeamResourceConfig } from './CreateTeam/RoadmapSelector';
import { httpGet, httpPut } from '../lib/http'; import { httpGet, httpPut } from '../lib/http';
import { pageProgressMessage } from '../stores/page'; import { pageProgressMessage } from '../stores/page';
import ExternalLinkIcon from '../icons/external-link.svg'; import ExternalLinkIcon from '../icons/external-link.svg';
import RoadmapIcon from '../icons/roadmap.svg';
import PlusIcon from '../icons/plus.svg'; import PlusIcon from '../icons/plus.svg';
import type { PageType } from './CommandMenu/CommandMenu'; import type { PageType } from './CommandMenu/CommandMenu';
import { UpdateTeamResourceModal } from './CreateTeam/UpdateTeamResourceModal'; import { UpdateTeamResourceModal } from './CreateTeam/UpdateTeamResourceModal';
import { AddTeamRoadmap } from './AddTeamRoadmap';
import { useStore } from '@nanostores/preact'; import { useStore } from '@nanostores/preact';
import { $canManageCurrentTeam } from '../stores/team'; import { $canManageCurrentTeam } from '../stores/team';
import {useToast} from "../hooks/use-toast"; import { useToast } from '../hooks/use-toast';
import { SelectRoadmapModal } from './CreateTeam/SelectRoadmapModal';
export function TeamRoadmaps() { export function TeamRoadmaps() {
const { t: teamId } = getUrlParams(); const { t: teamId } = getUrlParams();
@ -20,6 +21,7 @@ export function TeamRoadmaps() {
const toast = useToast(); const toast = useToast();
const [isLoading, setIsLoading] = useState(true);
const [removingRoadmapId, setRemovingRoadmapId] = useState<string>(''); const [removingRoadmapId, setRemovingRoadmapId] = useState<string>('');
const [isAddingRoadmap, setIsAddingRoadmap] = useState(false); const [isAddingRoadmap, setIsAddingRoadmap] = useState(false);
const [changingRoadmapId, setChangingRoadmapId] = useState<string>(''); const [changingRoadmapId, setChangingRoadmapId] = useState<string>('');
@ -83,12 +85,14 @@ export function TeamRoadmaps() {
return; return;
} }
setIsLoading(true);
Promise.all([ Promise.all([
loadTeam(teamId), loadTeam(teamId),
loadTeamResourceConfig(teamId), loadTeamResourceConfig(teamId),
loadAllRoadmaps(), loadAllRoadmaps(),
]).finally(() => { ]).finally(() => {
pageProgressMessage.set(''); pageProgressMessage.set('');
setIsLoading(false);
}); });
}, [teamId]); }, [teamId]);
@ -97,6 +101,7 @@ export function TeamRoadmaps() {
return; return;
} }
toast.loading('Deleting roadmap');
pageProgressMessage.set(`Deleting roadmap from team`); pageProgressMessage.set(`Deleting roadmap from team`);
const { error, response } = await httpPut<TeamResourceConfig>( const { error, response } = await httpPut<TeamResourceConfig>(
`${import.meta.env.PUBLIC_API_URL}/v1-delete-team-resource-config/${ `${import.meta.env.PUBLIC_API_URL}/v1-delete-team-resource-config/${
@ -117,6 +122,35 @@ export function TeamRoadmaps() {
setResourceConfigs(response); setResourceConfigs(response);
} }
async function onAdd(roadmapId: string) {
if (!teamId) {
return;
}
toast.loading('Adding roadmap');
pageProgressMessage.set('Adding roadmap');
setIsLoading(true);
const { error, response } = await httpPut<TeamResourceConfig>(
`${
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;
}
setResourceConfigs(response);
toast.success('Roadmap added');
}
async function onRemove(resourceId: string) { async function onRemove(resourceId: string) {
pageProgressMessage.set('Removing roadmap'); pageProgressMessage.set('Removing roadmap');
@ -129,26 +163,69 @@ export function TeamRoadmaps() {
return null; return null;
} }
const addRoadmapModal = isAddingRoadmap && (
<SelectRoadmapModal
onClose={() => setIsAddingRoadmap(false)}
teamResourceConfig={resourceConfigs}
allRoadmaps={allRoadmaps}
teamId={teamId}
onRoadmapAdd={(roadmapId) => {
onAdd(roadmapId).finally(() => {
pageProgressMessage.set('');
});
}}
onRoadmapRemove={(roadmapId) => {
if (confirm('Are you sure you want to remove this roadmap?')) {
onRemove(roadmapId).finally(() => {});
}
}}
/>
);
if (resourceConfigs.length === 0 && !isLoading) {
return (
<div className="flex flex-col items-center p-4 py-20">
{addRoadmapModal}
<img
alt="roadmap"
src={RoadmapIcon}
className="mb-4 h-24 w-24 opacity-10"
/>
<h3 className="mb-1 text-2xl font-bold text-gray-900">No roadmaps</h3>
<p className="text-base text-gray-500">
{canManageCurrentTeam
? 'Add a roadmap to start tracking your team'
: 'Ask your team admin to add some roadmaps'}
</p>
{canManageCurrentTeam && (
<button
className="mt-4 rounded-lg bg-black px-4 py-2 font-medium text-white hover:bg-gray-900"
onClick={() => setIsAddingRoadmap(true)}
>
Add roadmap
</button>
)}
</div>
);
}
return ( return (
<div> <div>
{isAddingRoadmap && ( {addRoadmapModal}
<AddTeamRoadmap <div className="mb-3 flex items-center justify-between">
onMakeChanges={(roadmapId) => { <span className={'text-gray-400'}>
setChangingRoadmapId(roadmapId); {resourceConfigs.length} roadmap(s) selected
setIsAddingRoadmap(false); </span>
}} {canManageCurrentTeam && (
teamId={team?._id!} <button
setResourceConfigs={setResourceConfigs} className="flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm font-medium text-gray-500 underline hover:bg-gray-100 hover:text-gray-900"
allRoadmaps={allRoadmaps} onClick={() => setIsAddingRoadmap(true)}
availableRoadmaps={allRoadmaps.filter((r) => { >
const isAlreadyAdded = resourceConfigs.find( Add / Remove Roadmaps
(c) => c.resourceId === r.id </button>
); )}
return !isAlreadyAdded; </div>
})}
onClose={() => setIsAddingRoadmap(false)}
/>
)}
<div className={'grid grid-cols-1 gap-3 sm:grid-cols-2'}> <div className={'grid grid-cols-1 gap-3 sm:grid-cols-2'}>
{changingRoadmapId && ( {changingRoadmapId && (
<UpdateTeamResourceModal <UpdateTeamResourceModal
@ -198,8 +275,8 @@ export function TeamRoadmaps() {
)} )}
</div> </div>
{ canManageCurrentTeam && ( {canManageCurrentTeam && (
<div className={'flex w-full justify-between pt-2 pb-3 px-3'}> <div className={'flex w-full justify-between px-3 pb-3 pt-2'}>
<button <button
type="button" type="button"
className={ className={
@ -219,13 +296,7 @@ export function TeamRoadmaps() {
className={ className={
'text-xs text-red-500 underline hover:text-black focus:outline-none disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-red-500' 'text-xs text-red-500 underline hover:text-black focus:outline-none disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-red-500'
} }
disabled={resourceConfigs.length === 1}
onClick={() => setRemovingRoadmapId(resourceId)} onClick={() => setRemovingRoadmapId(resourceId)}
title={
resourceConfigs.length === 1
? 'You must have at least one roadmap.'
: 'Delete roadmap from team'
}
> >
Remove Remove
</button> </button>

@ -8,13 +8,14 @@ import MapIcon from '../icons/map.svg';
import GroupIcon from '../icons/group.svg'; import GroupIcon from '../icons/group.svg';
import { useState } from 'preact/hooks'; import { useState } from 'preact/hooks';
import { useStore } from '@nanostores/preact'; import { useStore } from '@nanostores/preact';
import { $canManageCurrentTeam } from '../stores/team'; import { $canManageCurrentTeam, $currentTeam } from '../stores/team';
import { WarningIcon } from './ReactIcons/WarningIcon';
export const TeamSidebar: FunctionalComponent<{ export const TeamSidebar: FunctionalComponent<{
activePageId: string; activePageId: string;
}> = ({ activePageId, children }) => { }> = ({ activePageId, children }) => {
const [menuShown, setMenuShown] = useState(false); const [menuShown, setMenuShown] = useState(false);
const canManageCurrentTeam = useStore($canManageCurrentTeam); const currentTeam = useStore($currentTeam);
const { teamId } = useTeamId(); const { teamId } = useTeamId();
@ -30,6 +31,7 @@ export const TeamSidebar: FunctionalComponent<{
href: `/team/roadmaps?t=${teamId}`, href: `/team/roadmaps?t=${teamId}`,
id: 'roadmaps', id: 'roadmaps',
icon: MapIcon, icon: MapIcon,
hasWarning: currentTeam?.roadmaps?.length === 0,
}, },
{ {
title: 'Members', title: 'Members',
@ -120,13 +122,21 @@ export const TeamSidebar: FunctionalComponent<{
: 'border-r-transparent text-gray-500 hover:border-r-gray-300' : 'border-r-transparent text-gray-500 hover:border-r-gray-300'
}`} }`}
> >
<span class="flex flex-grow items-center"> <span class="flex flex-grow items-center justify-between">
<img <span className="flex">
alt="menu icon" <img
src={sidebarLink.icon} alt="menu icon"
className="mr-2 h-4 w-4" src={sidebarLink.icon}
/> className="mr-2 h-4 w-4"
{sidebarLink.title} />
{sidebarLink.title}
</span>
{sidebarLink.hasWarning && (
<span class="relative mr-1 flex items-center">
<span class="relative rounded-full bg-red-200 p-1 text-xs" />
<span class="absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-red-400 p-1 text-xs" />
</span>
)}
</span> </span>
</a> </a>
</li> </li>

@ -37,7 +37,7 @@ export function Toaster(props: Props) {
onClick={() => { onClick={() => {
$toastMessage.set(undefined); $toastMessage.set(undefined);
}} }}
className={`fixed bottom-5 left-1/2 max-w-[300px] animate-fade-slide-up min-w-[300px] sm:min-w-[auto] z-50`} className={`fixed bottom-5 left-1/2 z-50 min-w-[300px] max-w-[300px] animate-fade-slide-up sm:min-w-[auto]`}
> >
<div <div
className={`flex -translate-x-1/2 transform cursor-pointer items-center gap-2 rounded-md border border-gray-200 bg-white py-3 pl-4 pr-5 text-black shadow-md hover:bg-gray-50`} className={`flex -translate-x-1/2 transform cursor-pointer items-center gap-2 rounded-md border border-gray-200 bg-white py-3 pl-4 pr-5 text-black shadow-md hover:bg-gray-50`}

@ -16,6 +16,9 @@ export async function get() {
url: `/${roadmap.id}`, url: `/${roadmap.id}`,
title: roadmap.frontmatter.briefTitle, title: roadmap.frontmatter.briefTitle,
group: 'Roadmaps', group: 'Roadmaps',
metadata: {
tags: roadmap.frontmatter.tags,
},
})), })),
...bestPractices.map((bestPractice) => ({ ...bestPractices.map((bestPractice) => ({
id: bestPractice.id, id: bestPractice.id,

Loading…
Cancel
Save