mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-12 20:24:21 +02:00
Add select roadmap modal (#4253)
* wip: roadmap selector modal * wip * fix: typo * fix: prettier * chore: close icon
This commit is contained in:
@@ -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 PlusIcon from '../../icons/plus.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';
|
||||||
|
import { SelectRoadmapModal } from './SelectRoadmapModal';
|
||||||
|
|
||||||
export type TeamResourceConfig = {
|
export type TeamResourceConfig = {
|
||||||
resourceId: string;
|
resourceId: string;
|
||||||
@@ -22,6 +23,7 @@ type RoadmapSelectorProps = {
|
|||||||
export function RoadmapSelector(props: RoadmapSelectorProps) {
|
export function RoadmapSelector(props: RoadmapSelectorProps) {
|
||||||
const { team, teamResourceConfig = [], setTeamResourceConfig } = props;
|
const { team, 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>('');
|
||||||
@@ -126,42 +128,42 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{showSelectRoadmapModal && (
|
||||||
<SearchSelector
|
<SelectRoadmapModal
|
||||||
placeholder={`Search Roadmaps ..`}
|
onClose={() => setShowSelectRoadmapModal(false)}
|
||||||
onSelect={(option) => {
|
teamResourceConfig={teamResourceConfig}
|
||||||
const roadmapId = option.value;
|
allRoadmaps={allRoadmaps}
|
||||||
addTeamResource(roadmapId).finally(() => {
|
teamId={team?._id!}
|
||||||
pageProgressMessage.set('');
|
onRoadmapAdd={(roadmapId) => {
|
||||||
});
|
addTeamResource(roadmapId).finally(() => {
|
||||||
}}
|
pageProgressMessage.set('');
|
||||||
options={allRoadmaps
|
});
|
||||||
.filter((roadmap) => {
|
}}
|
||||||
return !teamResourceConfig
|
onRoadmapRemove={(roadmapId) => {
|
||||||
.map((c) => c.resourceId)
|
onRemove(roadmapId).finally(() => {});
|
||||||
.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">
|
<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">
|
<span className="block text-lg font-semibold text-black">
|
||||||
No roadmaps selected.
|
No roadmaps selected.
|
||||||
</span>
|
</span>
|
||||||
<p className={'text-sm text-gray-400'}>
|
<p className={'text-sm text-gray-400'}>
|
||||||
Please search and add roadmaps from above
|
Please search and add roadmaps from below
|
||||||
</p>
|
</p>
|
||||||
|
<div className="mt-4 flex justify-center">
|
||||||
|
<button
|
||||||
|
className="group flex items-center justify-center gap-2 rounded-md bg-black px-4 py-2 transition-opacity hover:opacity-70"
|
||||||
|
onClick={() => setShowSelectRoadmapModal(true)}
|
||||||
|
>
|
||||||
|
<img alt="add" src={PlusWhiteIcon} className="h-5 w-5" />
|
||||||
|
<span className="text-sm text-white focus:outline-none">
|
||||||
|
Add Roadmaps
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -214,6 +216,19 @@ 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>
|
||||||
|
114
src/components/CreateTeam/SelectRoadmapModal.tsx
Normal file
114
src/components/CreateTeam/SelectRoadmapModal.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
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 { useToast } from '../../hooks/use-toast';
|
||||||
|
import type { TeamResourceConfig } from './RoadmapSelector';
|
||||||
|
import CloseIcon from '../../icons/close.svg';
|
||||||
|
|
||||||
|
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 toast = useToast();
|
||||||
|
const popupBodyEl = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [searchResults, setSearchResults] = useState<PageType[]>(allRoadmaps);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
|
||||||
|
useKeydown('Escape', () => {
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
|
||||||
|
useOutsideClick(popupBodyEl, () => {
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
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-4xl 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-200 hover:text-gray-900"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<img src={CloseIcon} className="h-4 w-4" />
|
||||||
|
<span class="sr-only">Close modal</span>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search roadmaps"
|
||||||
|
className="block w-full px-4 py-3 outline-none placeholder:text-gray-400"
|
||||||
|
value={searchText}
|
||||||
|
onInput={(e) => setSearchText((e.target as HTMLInputElement).value)}
|
||||||
|
/>
|
||||||
|
<div className="min-h-[200px]">
|
||||||
|
<div className="flex flex-wrap items-center gap-2.5 border-t p-4">
|
||||||
|
{searchResults.map((roadmap) => {
|
||||||
|
const isSelected = teamResourceConfig.find(
|
||||||
|
(r) => r.resourceId === roadmap.id
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center rounded-md border ${
|
||||||
|
isSelected && 'bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
onClick={() => {
|
||||||
|
if (isSelected) {
|
||||||
|
onRoadmapRemove(roadmap.id);
|
||||||
|
} else {
|
||||||
|
onRoadmapAdd(roadmap.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSelected ? '-' : '+'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{searchResults.length === 0 && (
|
||||||
|
<div className="text-gray-400">No roadmaps found</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
1
src/icons/plus-white.svg
Normal file
1
src/icons/plus-white.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-plus-2"><path d="M4 22h14a2 2 0 0 0 2-2V7.5L14.5 2H6a2 2 0 0 0-2 2v4"/><polyline points="14 2 14 8 20 8"/><path d="M3 15h6"/><path d="M6 12v6"/></svg>
|
After Width: | Height: | Size: 353 B |
Reference in New Issue
Block a user