mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-10-02 20:06:44 +02:00
Add sorting to project solution listing
This commit is contained in:
@@ -16,6 +16,7 @@ import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
|||||||
import { SelectLanguages } from './SelectLanguages.tsx';
|
import { SelectLanguages } from './SelectLanguages.tsx';
|
||||||
import type { ProjectFrontmatter } from '../../lib/project.ts';
|
import type { ProjectFrontmatter } from '../../lib/project.ts';
|
||||||
import { ProjectSolutionModal } from './ProjectSolutionModal.tsx';
|
import { ProjectSolutionModal } from './ProjectSolutionModal.tsx';
|
||||||
|
import { SortProjects } from './SortProjects.tsx';
|
||||||
|
|
||||||
export interface ProjectStatusDocument {
|
export interface ProjectStatusDocument {
|
||||||
_id?: string;
|
_id?: string;
|
||||||
@@ -57,11 +58,13 @@ type ListProjectSolutionsResponse = {
|
|||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
p?: string;
|
p?: string;
|
||||||
l?: string;
|
l?: string;
|
||||||
|
s?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PageState = {
|
type PageState = {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
language: string;
|
language: string;
|
||||||
|
sort: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ListProjectSolutionsProps = {
|
type ListProjectSolutionsProps = {
|
||||||
@@ -100,6 +103,7 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
|||||||
const [pageState, setPageState] = useState<PageState>({
|
const [pageState, setPageState] = useState<PageState>({
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
language: '',
|
language: '',
|
||||||
|
sort: 'rating',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -108,12 +112,17 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
|||||||
ListProjectSolutionsResponse['data'][number] | null
|
ListProjectSolutionsResponse['data'][number] | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
const loadSolutions = async (page = 1, language: string = '') => {
|
const loadSolutions = async (
|
||||||
|
page = 1,
|
||||||
|
language: string = '',
|
||||||
|
sort: string = 'rating',
|
||||||
|
) => {
|
||||||
const { response, error } = await httpGet<ListProjectSolutionsResponse>(
|
const { response, error } = await httpGet<ListProjectSolutionsResponse>(
|
||||||
`${import.meta.env.PUBLIC_API_URL}/v1-list-project-solutions/${projectId}`,
|
`${import.meta.env.PUBLIC_API_URL}/v1-list-project-solutions/${projectId}`,
|
||||||
{
|
{
|
||||||
currPage: page,
|
currPage: page,
|
||||||
...(language ? { languages: language } : {}),
|
...(language ? { languages: language } : {}),
|
||||||
|
sort,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -178,6 +187,7 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
|||||||
setPageState({
|
setPageState({
|
||||||
currentPage: +(queryParams.p || '1'),
|
currentPage: +(queryParams.p || '1'),
|
||||||
language: queryParams.l || '',
|
language: queryParams.l || '',
|
||||||
|
sort: queryParams.s || 'rating',
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -187,17 +197,27 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageState.currentPage !== 1 || pageState.language !== '') {
|
if (
|
||||||
|
pageState.currentPage !== 1 ||
|
||||||
|
pageState.language !== '' ||
|
||||||
|
pageState.sort !== 'rating'
|
||||||
|
) {
|
||||||
setUrlParams({
|
setUrlParams({
|
||||||
p: String(pageState.currentPage),
|
p: String(pageState.currentPage),
|
||||||
l: pageState.language,
|
l: pageState.language,
|
||||||
|
s: pageState.sort,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
deleteUrlParam('p');
|
deleteUrlParam('p');
|
||||||
deleteUrlParam('l');
|
deleteUrlParam('l');
|
||||||
|
deleteUrlParam('s');
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSolutions(pageState.currentPage, pageState.language).finally(() => {
|
loadSolutions(
|
||||||
|
pageState.currentPage,
|
||||||
|
pageState.language,
|
||||||
|
pageState.sort,
|
||||||
|
).finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}, [pageState]);
|
}, [pageState]);
|
||||||
@@ -227,6 +247,16 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
|||||||
<p className="text-sm text-gray-500">{projectData.description}</p>
|
<p className="text-sm text-gray-500">{projectData.description}</p>
|
||||||
</div>
|
</div>
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
|
<div className="flex flex-shrink-0 items-center gap-2">
|
||||||
|
<SortProjects
|
||||||
|
selectedSort={pageState.sort}
|
||||||
|
onSelectSort={(sort) => {
|
||||||
|
setPageState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
sort,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<SelectLanguages
|
<SelectLanguages
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
selectedLanguage={selectedLanguage}
|
selectedLanguage={selectedLanguage}
|
||||||
@@ -237,6 +267,7 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
|
|||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -114,7 +114,7 @@ export function SelectLanguages(props: SelectLanguagesProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex">
|
<div className="relative flex flex-shrink-0">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1 rounded-md border border-gray-300 py-1.5 pl-3 pr-2 text-xs font-medium text-gray-900"
|
className="flex items-center gap-1 rounded-md border border-gray-300 py-1.5 pl-3 pr-2 text-xs font-medium text-gray-900"
|
||||||
|
66
src/components/Projects/SortProjects.tsx
Normal file
66
src/components/Projects/SortProjects.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
|
type SortOption = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortOptions: SortOption[] = [
|
||||||
|
{ label: 'Latest First', value: 'latest' },
|
||||||
|
{ label: 'Oldest First', value: 'oldest' },
|
||||||
|
{ label: 'Highest Rating', value: 'rating' },
|
||||||
|
];
|
||||||
|
|
||||||
|
type SortProjectsProps = {
|
||||||
|
selectedSort: string;
|
||||||
|
onSelectSort: (sort: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SortProjects(props: SortProjectsProps) {
|
||||||
|
const { selectedSort, onSelectSort } = props;
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useOutsideClick(dropdownRef, () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedOption =
|
||||||
|
sortOptions.find((option) => option.value === selectedSort) ||
|
||||||
|
sortOptions[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex-shrink-0" ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-1 rounded-md border border-gray-300 py-1.5 pl-3 pr-2 text-xs font-medium text-gray-900"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
>
|
||||||
|
{selectedOption.label}
|
||||||
|
<ChevronDown className="ml-1 h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute right-0 top-full z-10 mt-1.5 min-w-[150px] overflow-hidden rounded-md border border-gray-300 bg-white shadow-lg">
|
||||||
|
<div className="py-1">
|
||||||
|
{sortOptions.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
className={`flex w-full items-center px-4 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-100 ${
|
||||||
|
selectedSort === option.value ? 'bg-gray-100' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
onSelectSort(option.value);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Reference in New Issue
Block a user