Merge branch 'master' into chore/update-progress

chore/update-progress
Kamran Ahmed 1 year 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;
icon?: string;
isProtected?: boolean;
metadata?: Record<string, any>;
};
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 { SearchSelector } from '../SearchSelector';
import { httpGet, httpPut } from '../../lib/http';
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 type { TeamDocument } from './CreateTeamForm';
import { UpdateTeamResourceModal } from './UpdateTeamResourceModal';
import { SelectRoadmapModal } from './SelectRoadmapModal';
import { NotDropdown } from './NotDropdown';
export type TeamResourceConfig = {
resourceId: string;
@ -14,14 +15,15 @@ export type TeamResourceConfig = {
}[];
type RoadmapSelectorProps = {
team: TeamDocument;
teamId: string;
teamResourceConfig: TeamResourceConfig;
setTeamResourceConfig: (config: TeamResourceConfig) => void;
};
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 [changingRoadmapId, setChangingRoadmapId] = useState<string>('');
const [error, setError] = useState<string>('');
@ -50,15 +52,15 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
}
async function deleteResource(roadmapId: string) {
if (!team?._id) {
if (!teamId) {
return;
}
pageProgressMessage.set(`Deleting resource`);
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,
resourceType: 'roadmap',
@ -82,17 +84,17 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
}
async function addTeamResource(roadmapId: string) {
if (!team?._id) {
if (!teamId) {
return;
}
pageProgressMessage.set(`Adding roadmap to team`);
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,
resourceType: 'roadmap',
removed: [],
@ -118,7 +120,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
onClose={() => setChangingRoadmapId('')}
resourceId={changingRoadmapId}
resourceType={'roadmap'}
teamId={team?._id!}
teamId={teamId}
setTeamResourceConfig={setTeamResourceConfig}
defaultRemovedItems={
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
placeholder={`Search Roadmaps ..`}
onSelect={(option) => {
const roadmapId = option.value;
addTeamResource(roadmapId).finally(() => {
pageProgressMessage.set('');
});
}}
options={allRoadmaps
.filter((roadmap) => {
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"
/>
<div className="mt-3">
<NotDropdown
onClick={() => {
setShowSelectRoadmapModal(true);
}}
selectedCount={teamResourceConfig.length}
singularName={'roadmap'}
pluralName={'roadmaps'}
/>
</div>
{!teamResourceConfig.length && (
<div className="mt-4 rounded-md border px-4 py-12 text-center text-sm text-gray-700">
<img
alt={'search'}
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>
<p className={'mb-3 mt-2 text-base text-gray-400'}>
No roadmaps selected.
</p>
)}
{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' : ''
}`}
/>
<span className="mb-1.5 block text-2xl font-bold">
<span className="mb-2 block text-2xl font-bold">
{validTeamType.label}
</span>
<span className="text-sm text-gray-500">
<span className="text-sm text-gray-500 leading-[21px]">
{validTeamType.description}
</span>
</button>

@ -5,10 +5,12 @@ import type { TeamDocument } from './CreateTeamForm';
import { NextButton } from './NextButton';
export const validTeamSizes = [
'0-1',
'2-10',
'11-50',
'51-200',
'1-5',
'6-10',
'11-25',
'26-50',
'51-100',
'101-200',
'201-500',
'501-1000',
'1000+',
@ -134,7 +136,7 @@ export function Step1(props: Step1Props) {
autofocus={true}
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"
placeholder="roadmap.sh"
placeholder="Roadmap Inc."
disabled={isLoading}
required
value={name}
@ -167,7 +169,7 @@ export function Step1(props: Step1Props) {
{selectedTeamType === 'company' && (
<div className="mt-4 flex w-full flex-col">
<label for="website" className="text-sm leading-none text-slate-500">
LinkedIn URL
Company LinkedIn URL
</label>
<input
type="url"
@ -206,7 +208,7 @@ export function Step1(props: Step1Props) {
for="team-size"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
>
Company Size
Tech Team Size
</label>
<select
name="team-size"
@ -229,6 +231,12 @@ export function Step1(props: Step1Props) {
</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">
<button
type="button"

@ -17,15 +17,14 @@ export function Step2(props: Step2Props) {
<>
<div className="mt-4 flex w-full flex-col">
<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">
Picks the roadmaps to be made available to your team for tracking.
You can always add more later.
You can always add and customize your roadmaps later.
</p>
</div>
<RoadmapSelector
team={team}
teamId={team._id!}
teamResourceConfig={teamResourceConfig}
setTeamResourceConfig={setTeamResourceConfig}
/>

@ -95,15 +95,14 @@ export function TeamDropdown() {
{pendingTeamIds.length}
</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 && (
<img
src={
selectedAvatar
? `${
import.meta.env.PUBLIC_AVATAR_BASE_URL
}/${selectedAvatar}`
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL
}/${selectedAvatar}`
: '/images/default-avatar.png'
}
alt=""
@ -140,28 +139,18 @@ export function TeamDropdown() {
pageLink = `/team/progress?t=${team._id}`;
}
if (team.roadmaps.length === 0) {
pageLink = `/team/new?t=${team._id}&s=2`;
}
return (
<li>
<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"
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) && (
<span className="flex rounded-md bg-red-500 px-2 text-xs text-white">
Invite
</span>
)}
{team.roadmaps.length === 0 && (
<span className="flex rounded-md bg-gray-500 px-2 text-xs text-white">
Draft
</span>
)}
</a>
</li>
);

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

@ -3,6 +3,7 @@ import type { GroupByRoadmap, TeamMember } from './TeamProgressPage';
import { MemberProgressModal } from './MemberProgressModal';
import { getUrlParams } from '../../lib/browser';
import ExternalLinkIcon from '../../icons/external-link.svg';
import { useAuth } from '../../hooks/use-auth';
type GroupRoadmapItemProps = {
roadmap: GroupByRoadmap;
@ -11,6 +12,7 @@ type GroupRoadmapItemProps = {
export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
const { members, resourceTitle, resourceId } = props.roadmap;
const { t: teamId } = getUrlParams();
const user = useAuth();
const [showAll, setShowAll] = useState(false);
const [detailResourceId, setDetailResourceId] = useState<string | null>(null);
@ -49,10 +51,15 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
</div>
<div className="relative flex grow flex-col space-y-2 p-3">
{(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 (
<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}
onClick={() => {
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="inline-grid grid-cols-[20px_auto] gap-2">
<span className="inline-grid grid-cols-[20px_auto] gap-3">
<img
src={
member.member.avatar
@ -72,14 +79,16 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
alt={member.member.name || ''}
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 className="shrink-0 text-xs text-gray-400">
{member?.progress?.done} / {member?.progress?.total}
</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={{
width: `${
(member?.progress?.done / member?.progress?.total) * 100

@ -5,9 +5,10 @@ import { MemberProgressModal } from './MemberProgressModal';
type MemberProgressItemProps = {
teamId: string;
member: TeamMember;
isMyProgress?: boolean;
};
export function MemberProgressItem(props: MemberProgressItemProps) {
const { member, teamId } = props;
const { member, teamId, isMyProgress = false } = props;
const memberProgress = member?.progress?.sort((a, b) => {
return b.done - a.done;
@ -31,10 +32,10 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
)}
<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}
>
<div className="flex items-center gap-3 border-b p-3">
<div className={`relative flex items-center gap-3 border-b p-3`}>
<img
src={
member.avatar
@ -44,8 +45,18 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
alt={member.name || ''}
className="h-8 w-8 rounded-full"
/>
<div className="inline-grid">
<h3 className="truncate font-medium">{member.name}</h3>
<div className="inline-grid w-full">
{!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>
</div>
</div>

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

@ -5,13 +5,14 @@ import type { TeamResourceConfig } from './CreateTeam/RoadmapSelector';
import { httpGet, httpPut } from '../lib/http';
import { pageProgressMessage } from '../stores/page';
import ExternalLinkIcon from '../icons/external-link.svg';
import RoadmapIcon from '../icons/roadmap.svg';
import PlusIcon from '../icons/plus.svg';
import type { PageType } from './CommandMenu/CommandMenu';
import { UpdateTeamResourceModal } from './CreateTeam/UpdateTeamResourceModal';
import { AddTeamRoadmap } from './AddTeamRoadmap';
import { useStore } from '@nanostores/preact';
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() {
const { t: teamId } = getUrlParams();
@ -20,6 +21,7 @@ export function TeamRoadmaps() {
const toast = useToast();
const [isLoading, setIsLoading] = useState(true);
const [removingRoadmapId, setRemovingRoadmapId] = useState<string>('');
const [isAddingRoadmap, setIsAddingRoadmap] = useState(false);
const [changingRoadmapId, setChangingRoadmapId] = useState<string>('');
@ -83,12 +85,14 @@ export function TeamRoadmaps() {
return;
}
setIsLoading(true);
Promise.all([
loadTeam(teamId),
loadTeamResourceConfig(teamId),
loadAllRoadmaps(),
]).finally(() => {
pageProgressMessage.set('');
setIsLoading(false);
});
}, [teamId]);
@ -97,6 +101,7 @@ export function TeamRoadmaps() {
return;
}
toast.loading('Deleting roadmap');
pageProgressMessage.set(`Deleting roadmap from team`);
const { error, response } = await httpPut<TeamResourceConfig>(
`${import.meta.env.PUBLIC_API_URL}/v1-delete-team-resource-config/${
@ -117,6 +122,35 @@ export function TeamRoadmaps() {
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) {
pageProgressMessage.set('Removing roadmap');
@ -129,26 +163,69 @@ export function TeamRoadmaps() {
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 (
<div>
{isAddingRoadmap && (
<AddTeamRoadmap
onMakeChanges={(roadmapId) => {
setChangingRoadmapId(roadmapId);
setIsAddingRoadmap(false);
}}
teamId={team?._id!}
setResourceConfigs={setResourceConfigs}
allRoadmaps={allRoadmaps}
availableRoadmaps={allRoadmaps.filter((r) => {
const isAlreadyAdded = resourceConfigs.find(
(c) => c.resourceId === r.id
);
return !isAlreadyAdded;
})}
onClose={() => setIsAddingRoadmap(false)}
/>
)}
{addRoadmapModal}
<div className="mb-3 flex items-center justify-between">
<span className={'text-gray-400'}>
{resourceConfigs.length} roadmap(s) selected
</span>
{canManageCurrentTeam && (
<button
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"
onClick={() => setIsAddingRoadmap(true)}
>
Add / Remove Roadmaps
</button>
)}
</div>
<div className={'grid grid-cols-1 gap-3 sm:grid-cols-2'}>
{changingRoadmapId && (
<UpdateTeamResourceModal
@ -198,8 +275,8 @@ export function TeamRoadmaps() {
)}
</div>
{ canManageCurrentTeam && (
<div className={'flex w-full justify-between pt-2 pb-3 px-3'}>
{canManageCurrentTeam && (
<div className={'flex w-full justify-between px-3 pb-3 pt-2'}>
<button
type="button"
className={
@ -219,13 +296,7 @@ export function TeamRoadmaps() {
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'
}
disabled={resourceConfigs.length === 1}
onClick={() => setRemovingRoadmapId(resourceId)}
title={
resourceConfigs.length === 1
? 'You must have at least one roadmap.'
: 'Delete roadmap from team'
}
>
Remove
</button>

@ -8,13 +8,14 @@ import MapIcon from '../icons/map.svg';
import GroupIcon from '../icons/group.svg';
import { useState } from 'preact/hooks';
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<{
activePageId: string;
}> = ({ activePageId, children }) => {
const [menuShown, setMenuShown] = useState(false);
const canManageCurrentTeam = useStore($canManageCurrentTeam);
const currentTeam = useStore($currentTeam);
const { teamId } = useTeamId();
@ -30,6 +31,7 @@ export const TeamSidebar: FunctionalComponent<{
href: `/team/roadmaps?t=${teamId}`,
id: 'roadmaps',
icon: MapIcon,
hasWarning: currentTeam?.roadmaps?.length === 0,
},
{
title: 'Members',
@ -120,13 +122,21 @@ export const TeamSidebar: FunctionalComponent<{
: 'border-r-transparent text-gray-500 hover:border-r-gray-300'
}`}
>
<span class="flex flex-grow items-center">
<img
alt="menu icon"
src={sidebarLink.icon}
className="mr-2 h-4 w-4"
/>
{sidebarLink.title}
<span class="flex flex-grow items-center justify-between">
<span className="flex">
<img
alt="menu icon"
src={sidebarLink.icon}
className="mr-2 h-4 w-4"
/>
{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>
</a>
</li>

@ -37,7 +37,7 @@ export function Toaster(props: Props) {
onClick={() => {
$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
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}`,
title: roadmap.frontmatter.briefTitle,
group: 'Roadmaps',
metadata: {
tags: roadmap.frontmatter.tags,
},
})),
...bestPractices.map((bestPractice) => ({
id: bestPractice.id,

Loading…
Cancel
Save