mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-09 18:57:49 +02:00
Update the select roadmaps modal
This commit is contained in:
@@ -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[] = [
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
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 PlusIcon from '../../icons/plus.svg';
|
import ChevronDownIcon from '../../icons/chevron-down.svg';
|
||||||
import PlusWhiteIcon from '../../icons/plus-white.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';
|
||||||
@@ -145,26 +144,41 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!teamResourceConfig.length && (
|
<div
|
||||||
<div className="mt-4 rounded-md border px-4 py-12 text-center text-sm text-gray-700">
|
className="mt-3 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"
|
||||||
<span className="block text-lg font-semibold text-black">
|
role="button"
|
||||||
No roadmaps selected.
|
onClick={() => {
|
||||||
</span>
|
setShowSelectRoadmapModal(true);
|
||||||
<p className={'text-sm text-gray-400'}>
|
}}
|
||||||
Please search and add roadmaps from below
|
>
|
||||||
</p>
|
{teamResourceConfig.length > 0 && (
|
||||||
<div className="mt-4 flex justify-center">
|
<div className="flex flex-col">
|
||||||
<button
|
<p className="mb-1.5 text-base font-medium text-gray-800">
|
||||||
className="group flex items-center justify-center gap-2 rounded-md bg-black px-4 py-2 transition-opacity hover:opacity-70"
|
{teamResourceConfig.length} roadmaps selected
|
||||||
onClick={() => setShowSelectRoadmapModal(true)}
|
</p>
|
||||||
>
|
<p className="text-sm text-gray-400">
|
||||||
<img alt="add" src={PlusWhiteIcon} className="h-5 w-5" />
|
Click to add or change selection
|
||||||
<span className="text-sm text-white focus:outline-none">
|
</p>
|
||||||
Add Roadmaps
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{!teamResourceConfig.length && (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p className="text-base text-gray-400">Click to select roadmaps</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<img
|
||||||
|
alt={'roadmap'}
|
||||||
|
src={ChevronDownIcon}
|
||||||
|
className={'relative top-[1px] h-[17px] w-[17px] opacity-40'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!teamResourceConfig.length && (
|
||||||
|
<p className={'mb-3 mt-2 text-base text-gray-400'}>
|
||||||
|
No roadmaps selected.
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{teamResourceConfig.length > 0 && (
|
{teamResourceConfig.length > 0 && (
|
||||||
@@ -216,19 +230,6 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<button
|
|
||||||
className="group flex min-h-[110px] flex-col items-center justify-center rounded-md border border-dashed border-gray-300 px-8 transition-colors hover:border-gray-600 hover:bg-gray-50"
|
|
||||||
onClick={() => setShowSelectRoadmapModal(true)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="add"
|
|
||||||
src={PlusIcon}
|
|
||||||
className="mb-1 h-6 w-6 opacity-20 transition-opacity group-hover:opacity-100"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-400 transition-colors focus:outline-none group-hover:text-black">
|
|
||||||
Add Roadmaps
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,9 +2,9 @@ import { useEffect, useRef, useState } from 'preact/hooks';
|
|||||||
import { useKeydown } from '../../hooks/use-keydown';
|
import { useKeydown } from '../../hooks/use-keydown';
|
||||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||||
import type { PageType } from '../CommandMenu/CommandMenu';
|
import type { PageType } from '../CommandMenu/CommandMenu';
|
||||||
import { useToast } from '../../hooks/use-toast';
|
|
||||||
import type { TeamResourceConfig } from './RoadmapSelector';
|
import type { TeamResourceConfig } from './RoadmapSelector';
|
||||||
import CloseIcon from '../../icons/close.svg';
|
import CloseIcon from '../../icons/close.svg';
|
||||||
|
import { SelectRoadmapModalItem } from './SelectRoadmapModalItem';
|
||||||
|
|
||||||
export type SelectRoadmapModalProps = {
|
export type SelectRoadmapModalProps = {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -23,8 +23,8 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
|
|||||||
onRoadmapRemove,
|
onRoadmapRemove,
|
||||||
teamResourceConfig,
|
teamResourceConfig,
|
||||||
} = props;
|
} = props;
|
||||||
const toast = useToast();
|
|
||||||
const popupBodyEl = useRef<HTMLDivElement>(null);
|
const popupBodyEl = useRef<HTMLDivElement>(null);
|
||||||
|
const searchInputEl = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [searchResults, setSearchResults] = useState<PageType[]>(allRoadmaps);
|
const [searchResults, setSearchResults] = useState<PageType[]>(allRoadmaps);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
@@ -37,6 +37,14 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
|
|||||||
onClose();
|
onClose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!searchInputEl.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchInputEl.current.focus();
|
||||||
|
}, [searchInputEl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchText.length === 0) {
|
if (searchText.length === 0) {
|
||||||
setSearchResults(allRoadmaps);
|
setSearchResults(allRoadmaps);
|
||||||
@@ -52,43 +60,54 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
|
|||||||
setSearchResults(searchResults);
|
setSearchResults(searchResults);
|
||||||
}, [searchText, allRoadmaps]);
|
}, [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 (
|
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="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-4xl p-4 md:h-auto">
|
<div class="relative mx-auto h-full w-full max-w-2xl p-4 md:h-auto">
|
||||||
<div
|
<div
|
||||||
ref={popupBodyEl}
|
ref={popupBodyEl}
|
||||||
class="popup-body relative mt-4 overflow-hidden rounded-lg bg-white shadow"
|
class="popup-body relative mt-4 overflow-hidden rounded-lg bg-white shadow"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="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-200 hover:text-gray-900"
|
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}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<img src={CloseIcon} className="h-4 w-4" />
|
<img alt={'close'} src={CloseIcon} className="h-4 w-4" />
|
||||||
<span class="sr-only">Close modal</span>
|
<span class="sr-only">Close modal</span>
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
|
ref={searchInputEl}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search roadmaps"
|
placeholder="Search roadmaps"
|
||||||
className="block w-full px-4 py-3 outline-none placeholder:text-gray-400"
|
className="block w-full border-b px-5 pb-3.5 pt-4 outline-none placeholder:text-gray-400"
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onInput={(e) => setSearchText((e.target as HTMLInputElement).value)}
|
onInput={(e) => setSearchText((e.target as HTMLInputElement).value)}
|
||||||
/>
|
/>
|
||||||
<div className="min-h-[200px]">
|
<div className="min-h-[200px] p-4">
|
||||||
<div className="flex flex-wrap items-center gap-2.5 border-t p-4">
|
<span className="block pb-3 text-xs uppercase text-gray-400">
|
||||||
{searchResults.map((roadmap) => {
|
Role Based Roadmaps
|
||||||
const isSelected = teamResourceConfig.find(
|
</span>
|
||||||
(r) => r.resourceId === roadmap.id
|
{roleBasedRoadmaps.length === 0 && (
|
||||||
);
|
<p className="mb-1 flex h-full items-start text-sm italic text-gray-400"></p>
|
||||||
return (
|
)}
|
||||||
<div
|
{roleBasedRoadmaps.length > 0 && (
|
||||||
className={`flex items-center rounded-md border ${
|
<div className="mb-5 flex flex-wrap items-center gap-2">
|
||||||
isSelected && 'bg-gray-100'
|
{roleBasedRoadmaps.map((roadmap) => {
|
||||||
}`}
|
const isSelected = !!teamResourceConfig.find(
|
||||||
>
|
(r) => r.resourceId === roadmap.id
|
||||||
<span className="px-4">{roadmap.title}</span>
|
);
|
||||||
<button
|
|
||||||
className="flex h-8 w-8 items-center justify-center border-l p-1 leading-none hover:bg-gray-100"
|
return (
|
||||||
|
<SelectRoadmapModalItem
|
||||||
|
title={roadmap.title}
|
||||||
|
isSelected={isSelected}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
onRoadmapRemove(roadmap.id);
|
onRoadmapRemove(roadmap.id);
|
||||||
@@ -96,15 +115,34 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
|
|||||||
onRoadmapAdd(roadmap.id);
|
onRoadmapAdd(roadmap.id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{isSelected ? '-' : '+'}
|
);
|
||||||
</button>
|
})}
|
||||||
</div>
|
</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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{searchResults.length === 0 && (
|
|
||||||
<div className="text-gray-400">No roadmaps found</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
34
src/components/CreateTeam/SelectRoadmapModalItem.tsx
Normal file
34
src/components/CreateTeam/SelectRoadmapModalItem.tsx
Normal 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">
|
||||||
|
×
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isSelected && (
|
||||||
|
<span className="flex items-center bg-gray-100 px-2.5 text-xs text-gray-500">
|
||||||
|
+
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
@@ -17,10 +17,9 @@ 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>
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
Reference in New Issue
Block a user