Compare commits

...

17 Commits

Author SHA1 Message Date
Kamran Ahmed
7f5f96a6b7 Merge branch 'master' into chore/update-progress 2023-07-26 15:43:34 +01:00
Kamran Ahmed
14f9ad9530 Highlight user personal progress 2023-07-26 15:41:39 +01:00
Kamran Ahmed
076b866430 Personal progress indicator in teams 2023-07-26 15:24:29 +01:00
Kamran Ahmed
7aca57c3e4 Team roadmaps listing page 2023-07-25 21:34:00 +01:00
Kamran Ahmed
36cd03f14f Use the same add roadmap modal 2023-07-25 20:50:40 +01:00
Kamran Ahmed
5bc33cb527 Member progress item sorting 2023-07-25 20:05:47 +01:00
Kamran Ahmed
5d3202e065 Add skip button for teams 2023-07-25 18:56:40 +01:00
Kamran Ahmed
5cf286a753 Update team sizes and copy 2023-07-25 18:32:43 +01:00
Kamran Ahmed
0addc56123 Update the select roadmaps modal 2023-07-25 18:24:32 +01:00
Arik Chakma
3182e2a599 Show current user progress first (#4255)
* wip: progress sorting

* chore: show current user progress first

* fix: team guard

* fix: user progress sort
2023-07-25 17:36:49 +01:00
Arik Chakma
ff96644751 chore: current user header 2023-07-25 22:21:56 +06:00
Kamran Ahmed
8c7fb8cab5 Copy change 2023-07-25 16:51:34 +01:00
Arik Chakma
f61d360ee7 Add select roadmap modal (#4253)
* wip: roadmap selector modal

* wip

* fix: typo

* fix: prettier

* chore: close icon
2023-07-25 16:49:21 +01:00
Arik Chakma
081e16eb9b chore: show tracking for current user 2023-07-25 21:44:02 +06:00
Arik Chakma
eefce5c6a5 chore: add update progress in modal 2023-07-25 21:27:18 +06:00
Kamran Ahmed
29d91be094 Add cursors 2023-07-25 13:21:57 +01:00
Kamran Ahmed
8ee56576ea Update copy for team creation 2023-07-25 13:21:56 +01:00
24 changed files with 799 additions and 179 deletions

View File

@@ -0,0 +1,8 @@
<svg width="63" height="24" viewBox="0 0 63 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="63" height="24" rx="7" fill="#563AFF"/>
<path d="M27.2629 16.7273H25.2856L28.2984 8H30.6763L33.6848 16.7273H31.7075L29.5214 9.99432H29.4533L27.2629 16.7273ZM27.1393 13.2969H31.8098V14.7372H27.1393V13.2969Z" fill="white"/>
<path d="M37.829 16.7273H34.7352V8H37.8545C38.7324 8 39.4881 8.17472 40.1216 8.52415C40.7551 8.87074 41.2423 9.36932 41.5832 10.0199C41.927 10.6705 42.0989 11.4489 42.0989 12.3551C42.0989 13.2642 41.927 14.0455 41.5832 14.6989C41.2423 15.3523 40.7523 15.8537 40.1131 16.2031C39.4767 16.5526 38.7153 16.7273 37.829 16.7273ZM36.5804 15.1463H37.7523C38.2977 15.1463 38.7565 15.0497 39.1287 14.8565C39.5037 14.6605 39.7849 14.358 39.9724 13.9489C40.1628 13.5369 40.2579 13.0057 40.2579 12.3551C40.2579 11.7102 40.1628 11.1832 39.9724 10.7741C39.7849 10.3651 39.5051 10.0639 39.1329 9.87074C38.7608 9.67756 38.302 9.58097 37.7565 9.58097H36.5804V15.1463Z" fill="white"/>
<path d="M46.5594 16.7273H43.4657V8H46.585C47.4628 8 48.2185 8.17472 48.8521 8.52415C49.4856 8.87074 49.9728 9.36932 50.3137 10.0199C50.6574 10.6705 50.8293 11.4489 50.8293 12.3551C50.8293 13.2642 50.6574 14.0455 50.3137 14.6989C49.9728 15.3523 49.4827 15.8537 48.8435 16.2031C48.2072 16.5526 47.4458 16.7273 46.5594 16.7273ZM45.3109 15.1463H46.4827C47.0282 15.1463 47.487 15.0497 47.8592 14.8565C48.2342 14.6605 48.5154 14.358 48.7029 13.9489C48.8932 13.5369 48.9884 13.0057 48.9884 12.3551C48.9884 11.7102 48.8932 11.1832 48.7029 10.7741C48.5154 10.3651 48.2356 10.0639 47.8634 9.87074C47.4913 9.67756 47.0324 9.58097 46.487 9.58097H45.3109V15.1463Z" fill="white"/>
<path d="M10 12H18" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 8V16" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,5 @@
<svg width="89" height="24" viewBox="0 0 89 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="89" height="24" rx="7" fill="black"/>
<path d="M23.8217 17V7.54545H27.5518C28.2659 7.54545 28.8752 7.67318 29.38 7.92862C29.8878 8.18099 30.274 8.53954 30.5387 9.00426C30.8065 9.46591 30.9403 10.0091 30.9403 10.6339C30.9403 11.2617 30.8049 11.8018 30.5341 12.2543C30.2633 12.7036 29.8709 13.0483 29.3569 13.2884C28.846 13.5284 28.2274 13.6484 27.5011 13.6484H25.0036V12.0419H27.1779C27.5595 12.0419 27.8765 11.9896 28.1289 11.8849C28.3813 11.7803 28.569 11.6233 28.6921 11.4141C28.8183 11.2048 28.8814 10.9447 28.8814 10.6339C28.8814 10.32 28.8183 10.0553 28.6921 9.83984C28.569 9.62441 28.3797 9.46129 28.1243 9.3505C27.8719 9.23662 27.5534 9.17969 27.1687 9.17969H25.8207V17H23.8217ZM28.9276 12.6974L31.2773 17H29.0707L26.7717 12.6974H28.9276ZM32.353 17V7.54545H38.7237V9.19354H34.3519V11.4464H38.396V13.0945H34.3519V15.3519H38.7422V17H32.353ZM40.3129 7.54545H42.7781L45.3818 13.8977H45.4926L48.0963 7.54545H50.5615V17H48.6226V10.8462H48.5441L46.0974 16.9538H44.7771L42.3303 10.8232H42.2519V17H40.3129V7.54545ZM60.8967 12.2727C60.8967 13.3037 60.7012 14.1809 60.3104 14.9041C59.9226 15.6274 59.3932 16.1798 58.7223 16.5614C58.0545 16.94 57.3035 17.1293 56.4695 17.1293C55.6293 17.1293 54.8752 16.9384 54.2074 16.5568C53.5395 16.1752 53.0117 15.6228 52.6239 14.8995C52.2362 14.1763 52.0423 13.3007 52.0423 12.2727C52.0423 11.2417 52.2362 10.3646 52.6239 9.64134C53.0117 8.91809 53.5395 8.36719 54.2074 7.98864C54.8752 7.60701 55.6293 7.41619 56.4695 7.41619C57.3035 7.41619 58.0545 7.60701 58.7223 7.98864C59.3932 8.36719 59.9226 8.91809 60.3104 9.64134C60.7012 10.3646 60.8967 11.2417 60.8967 12.2727ZM58.87 12.2727C58.87 11.6049 58.77 11.0417 58.57 10.5831C58.373 10.1245 58.0945 9.77675 57.7344 9.53977C57.3743 9.30279 56.9527 9.1843 56.4695 9.1843C55.9863 9.1843 55.5646 9.30279 55.2045 9.53977C54.8445 9.77675 54.5644 10.1245 54.3643 10.5831C54.1674 11.0417 54.0689 11.6049 54.0689 12.2727C54.0689 12.9406 54.1674 13.5038 54.3643 13.9624C54.5644 14.4209 54.8445 14.7687 55.2045 15.0057C55.5646 15.2427 55.9863 15.3612 56.4695 15.3612C56.9527 15.3612 57.3743 15.2427 57.7344 15.0057C58.0945 14.7687 58.373 14.4209 58.57 13.9624C58.77 13.5038 58.87 12.9406 58.87 12.2727ZM63.5523 7.54545L65.8374 14.7287H65.9252L68.2149 7.54545H70.4308L67.1716 17H64.5956L61.3318 7.54545H63.5523ZM71.5688 17V7.54545H77.9395V9.19354H73.5677V11.4464H77.6118V13.0945H73.5677V15.3519H77.958V17H71.5688Z" fill="white"/>
<path d="M8 12L17 12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

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

View File

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

View File

@@ -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 && (

View File

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

View File

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

View File

@@ -10,13 +10,13 @@ export const validTeamTypes = [
value: 'company',
label: 'Company',
icon: BuildingIcon,
description: 'Use roadmap.sh for your company',
description: 'Track the skills and learning progress of the tech team at your company',
},
{
value: 'study_group',
label: 'Study Group',
icon: UsersIcon,
description: 'Invite your friends and learn together',
description: 'Invite your friends or course-mates and track your learning progress together',
},
] as const;
@@ -87,10 +87,10 @@ export function Step0(props: Step0Props) {
validTeamType.value === selectedTeamType ? 'opacity-100' : ''
}`}
/>
<span className="mb-1 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>

View File

@@ -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"

View File

@@ -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}
/>

View File

@@ -122,3 +122,7 @@ svg .removed g, svg .removed circle, svg .removed path {
height: 100%;
}
}
/*.clickable-group:hover {*/
/* cursor: url(/images/cursors/add.svg) 5 5, move;*/
/*}*/

View File

@@ -0,0 +1,22 @@
type CloseIconProps = {
className?: string;
};
export function CloseIcon(props: CloseIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
className={className}
>
<path d="M18 6 6 18" /><path d="m6 6 12 12" />
</svg>
);
}

View File

@@ -1,5 +1,4 @@
---
import { ClearProgress } from './Activity/ClearProgress';
import AstroIcon from './AstroIcon.astro';
import Icon from './AstroIcon.astro';
import ResourceProgressStats from './ResourceProgressStats.astro';

View File

@@ -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>
);

View File

@@ -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

View File

@@ -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

View File

@@ -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>
@@ -60,9 +71,11 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
>
<span className="relative z-10 flex items-center justify-between text-sm">
<span className="inline-grid">
<span className={'truncate'}>{progress.resourceTitle}</span>
<span className={'truncate'}>
{progress.resourceTitle}
</span>
</span>
<span className="text-xs text-gray-400 shrink-0 ml-1.5">
<span className="ml-1.5 shrink-0 text-xs text-gray-400">
{progress.done} / {progress.total}
</span>
</span>

View File

@@ -6,9 +6,19 @@ import { useOutsideClick } from '../../hooks/use-outside-click';
import { useKeydown } from '../../hooks/use-keydown';
import type { TeamMember } from './TeamProgressPage';
import { httpGet } from '../../lib/http';
import { renderTopicProgress } from '../../lib/resource-progress';
import {
ResourceProgressType,
ResourceType,
renderTopicProgress,
updateResourceProgress,
} from '../../lib/resource-progress';
import CloseIcon from '../../icons/close.svg';
import { useToast } from '../../hooks/use-toast';
import { useAuth } from '../../hooks/use-auth';
import { pageProgressMessage } from '../../stores/page';
import { ProgressHint } from './ProgressHint';
import QuestionIcon from '../../icons/question.svg';
import { InfoIcon } from '../ReactIcons/InfoIcon';
export type ProgressMapProps = {
member: TeamMember;
@@ -27,10 +37,13 @@ type MemberProgressResponse = {
export function MemberProgressModal(props: ProgressMapProps) {
const { resourceId, member, resourceType, teamId, onClose } = props;
const user = useAuth();
const isCurrentUser = user?.email === member.email;
const containerEl = useRef<HTMLDivElement>(null);
const popupBodyEl = useRef<HTMLDivElement>(null);
const [showProgressHint, setShowProgressHint] = useState(false);
const [memberProgress, setMemberProgress] =
useState<MemberProgressResponse>();
const [isLoading, setIsLoading] = useState(true);
@@ -75,10 +88,16 @@ export function MemberProgressModal(props: ProgressMapProps) {
}
useKeydown('Escape', () => {
if (showProgressHint) {
return;
}
onClose();
});
useOutsideClick(popupBodyEl, () => {
if (showProgressHint) {
return;
}
onClose();
});
@@ -119,10 +138,128 @@ export function MemberProgressModal(props: ProgressMapProps) {
});
}, []);
function updateTopicStatus(topicId: string, newStatus: ResourceProgressType) {
if (!resourceId || !resourceType || !isCurrentUser) {
return;
}
pageProgressMessage.set('Updating progress');
updateResourceProgress(
{
resourceId: resourceId,
resourceType: resourceType as ResourceType,
topicId,
},
newStatus
)
.then(() => {
renderTopicProgress(topicId, newStatus);
getMemberProgress(teamId, member._id, resourceType, resourceId).then(
(data) => {
setMemberProgress(data);
}
);
})
.catch((err) => {
alert('Something went wrong, please try again.');
console.error(err);
})
.finally(() => {
pageProgressMessage.set('');
});
return;
}
async function handleRightClick(e: MouseEvent) {
const targetGroup = (e.target as HTMLElement)?.closest('g');
if (!targetGroup) {
return;
}
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
if (!groupId) {
return;
}
if (targetGroup.classList.contains('removed')) {
return;
}
e.preventDefault();
const isCurrentStatusDone = targetGroup.classList.contains('done');
const normalizedGroupId = groupId.replace(/^\d+-/, '');
updateTopicStatus(
normalizedGroupId,
!isCurrentStatusDone ? 'done' : 'pending'
);
}
async function handleClick(e: MouseEvent) {
const targetGroup = (e.target as HTMLElement)?.closest('g');
if (!targetGroup) {
return;
}
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
if (!groupId) {
return;
}
if (targetGroup.classList.contains('removed')) {
return;
}
e.preventDefault();
const normalizedGroupId = groupId.replace(/^\d+-/, '');
const isCurrentStatusLearning = targetGroup.classList.contains('learning');
const isCurrentStatusSkipped = targetGroup.classList.contains('skipped');
if (e.shiftKey) {
e.preventDefault();
updateTopicStatus(
normalizedGroupId,
!isCurrentStatusLearning ? 'learning' : 'pending'
);
return;
}
if (e.altKey) {
e.preventDefault();
updateTopicStatus(
normalizedGroupId,
!isCurrentStatusSkipped ? 'skipped' : 'pending'
);
return;
}
}
useEffect(() => {
if (!isCurrentUser || !containerEl.current) {
return;
}
containerEl.current?.addEventListener('contextmenu', handleRightClick);
containerEl.current?.addEventListener('click', handleClick);
return () => {
containerEl.current?.removeEventListener('contextmenu', handleRightClick);
containerEl.current?.removeEventListener('click', handleClick);
};
}, []);
const removedTopics = memberProgress?.removed || [];
const memberDone =
memberProgress?.done.filter((id) => !removedTopics.includes(id)).length ||
0;
const memberLearning =
memberProgress?.learning.filter((id) => !removedTopics.includes(id))
.length || 0;
const memberSkipped =
memberProgress?.skipped.filter((id) => !removedTopics.includes(id))
.length || 0;
const currProgress = member.progress.find((p) => p.resourceId === resourceId);
const memberDone = currProgress?.done || 0;
const memberLearning = currProgress?.learning || 0;
const memberSkipped = currProgress?.skipped || 0;
const memberTotal = currProgress?.total || 0;
const progressPercentage = Math.round((memberDone / memberTotal) * 100);
@@ -134,38 +271,65 @@ export function MemberProgressModal(props: ProgressMapProps) {
ref={popupBodyEl}
class="popup-body relative rounded-lg bg-white shadow"
>
{showProgressHint && (
<ProgressHint
onClose={() => {
setShowProgressHint(false);
}}
/>
)}
<div className="p-4">
<div className="mb-5 mt-0 text-left md:mt-4 md:text-center">
<h2 className={'mb-1 text-lg font-bold md:text-2xl'}>
{member.name}'s Progress
</h2>
<p
className={
'hidden text-xs text-gray-500 sm:text-sm md:block md:text-base'
}
>
You are looking at {member.name}'s progress.{' '}
<a
target={'_blank'}
href={`/${resourceId}?t=${teamId}`}
className="text-blue-600 underline"
{isCurrentUser ? (
<div className="mb-5 mt-0 text-left md:mt-4 md:text-center">
<h2 className={'mb-1 text-lg font-bold md:text-2xl'}>
Your Progress
</h2>
<p className={'text-gray-500'}>
You can{' '}
<button
className="inline-flex items-center text-blue-600 underline"
onClick={() => {
setShowProgressHint(true);
}}
>
follow these instructions
</button>{' '}
to update your progress below.
</p>
</div>
) : (
<div className="mb-5 mt-0 text-left md:mt-4 md:text-center">
<h2 className={'mb-1 text-lg font-bold md:text-2xl'}>
{member.name}'s Progress
</h2>
<p
className={
'hidden text-xs text-gray-500 sm:text-sm md:block md:text-base'
}
>
View your progress
</a>
.
</p>
<p className={'block text-gray-500 md:hidden'}>
View your progress&nbsp;
<a
target={'_blank'}
href={`/${resourceId}?t=${teamId}`}
className="text-blue-600 underline"
>
on the roadmap page.
</a>
</p>
</div>
<p class="-mx-4 mb-3 flex items-center justify-start border-b border-t py-2 text-sm sm:hidden px-4">
You are looking at {member.name}'s progress.{' '}
<a
target={'_blank'}
href={`/${resourceId}?t=${teamId}`}
className="text-blue-600 underline"
>
View your progress
</a>
.
</p>
<p className={'block text-gray-500 md:hidden'}>
View your progress&nbsp;
<a
target={'_blank'}
href={`/${resourceId}?t=${teamId}`}
className="text-blue-600 underline"
>
on the roadmap page.
</a>
</p>
</div>
)}
<p class="-mx-4 mb-3 flex items-center justify-start border-b border-t px-4 py-2 text-sm sm:hidden">
<span class="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
<span>{progressPercentage}</span>% Done
</span>

View File

@@ -0,0 +1,70 @@
import { useRef } from 'preact/hooks';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { useKeydown } from '../../hooks/use-keydown';
import { CloseIcon } from '../ReactIcons/CloseIcon';
type ProgressHintProps = {
onClose: () => void;
};
export function ProgressHint(props: ProgressHintProps) {
const { onClose } = props;
const containerEl = useRef<HTMLDivElement>(null);
useOutsideClick(containerEl, onClose);
useKeydown('Escape', () => {
onClose();
});
return (
<div className="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 className="relative flex h-full w-full items-center justify-center">
<div
className="relative w-full max-w-lg rounded-md border border-yellow-300 bg-yellow-50 px-3 py-3 text-gray-500"
ref={containerEl}
>
<span className="mb-1.5 block text-xs font-medium uppercase text-green-600">
Update Progress
</span>
<p className="text-sm">Use the keyboard shortcuts listed below.</p>
<ul className="mb-1.5 mt-3 flex flex-col gap-1">
<li className="text-sm leading-loose">
<kbd className="rounded-md bg-gray-900 px-2 py-1.5 text-xs text-white">
Right Mouse Click
</kbd>{' '}
to mark as Done.
</li>
<li className="text-sm leading-loose">
<kbd className="rounded-md bg-gray-900 px-2 py-1.5 text-xs text-white">
Shift
</kbd>{' '}
+{' '}
<kbd className="rounded-md bg-gray-900 px-2 py-1.5 text-xs text-white">
Click
</kbd>{' '}
to mark as in progress.
</li>
<li className="text-sm leading-loose">
<kbd className="rounded-md bg-gray-900 px-2 py-1.5 text-xs text-white">
Option / Alt
</kbd>{' '}
+{' '}
<kbd className="rounded-md bg-gray-900 px-2 py-1.5 text-xs text-white">
Click
</kbd>{' '}
to mark as skipped.
</li>
</ul>
<button
type="button"
className="absolute right-1.5 top-1.5 ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-yellow-200 hover:text-yellow-900"
onClick={onClose}
>
<CloseIcon />
<span class="sr-only">Close modal</span>
</button>
</div>
</div>
</div>
);
}

View File

@@ -7,9 +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 { $toastMessage } from '../../stores/toast';
import { getUrlParams, setUrlParams } from '../../lib/browser';
import { useAuth } from '../../hooks/use-auth';
export type UserProgress = {
resourceTitle: string;
@@ -55,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<
@@ -70,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(() => {
@@ -78,10 +88,12 @@ export function TeamProgressPage() {
return;
}
getTeamProgress().finally(() => {
pageProgressMessage.set('');
setIsLoading(false);
});
getTeamProgress().then(
() => {
pageProgressMessage.set('');
setIsLoading(false);
}
);
}, [teamId]);
if (isLoading) {
@@ -134,11 +146,10 @@ export function TeamProgressPage() {
<div className="flex items-center gap-2">
{groupingTypes.map((grouping) => (
<button
className={`rounded-md border p-1 px-2 text-sm ${
selectedGrouping === grouping.value
? ' border-gray-400 bg-gray-200 '
: ''
}`}
className={`rounded-md border p-1 px-2 text-sm ${selectedGrouping === grouping.value
? ' border-gray-400 bg-gray-200 '
: ''
}`}
onClick={() => setSelectedGrouping(grouping.value)}
>
{grouping.label}
@@ -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>
)}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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`}

View File

@@ -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,