Spaces:
Runtime error
Runtime error
Commit ·
485ac5f
1
Parent(s): 1a1c017
frontend/src/components/ProjectExplorer.tsx
CHANGED
|
@@ -29,6 +29,12 @@ interface FilterOptions {
|
|
| 29 |
fundingSchemes: string[];
|
| 30 |
ids: string[];
|
| 31 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
|
| 34 |
projects,
|
|
@@ -46,6 +52,10 @@ const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
|
|
| 46 |
setFundingSchemeFilter,
|
| 47 |
idFilter,
|
| 48 |
setIdFilter,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
page,
|
| 50 |
setPage,
|
| 51 |
setSelectedProject,
|
|
@@ -74,8 +84,10 @@ const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
|
|
| 74 |
if (orgFilter) params.set("organization", orgFilter);
|
| 75 |
if (countryFilter) params.set("country", countryFilter);
|
| 76 |
if (search) params.set("search", search);
|
| 77 |
-
if (idFilter)
|
| 78 |
if (fundingSchemeFilter) params.set("fundingScheme", fundingSchemeFilter);
|
|
|
|
|
|
|
| 79 |
|
| 80 |
fetch(`/api/filters?${params.toString()}`)
|
| 81 |
.then((res) => res.json())
|
|
@@ -88,6 +100,22 @@ const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
|
|
| 88 |
num != null
|
| 89 |
? num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
| 90 |
: '-';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
|
| 93 |
return (
|
|
@@ -101,15 +129,13 @@ const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
|
|
| 101 |
onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
| 102 |
width={{ base: "100%", md: "200px" }}
|
| 103 |
/>
|
| 104 |
-
<
|
| 105 |
-
placeholder={
|
| 106 |
value={idFilter}
|
| 107 |
-
onChange={(e) =>
|
|
|
|
| 108 |
isDisabled={loadingFilters}
|
| 109 |
-
|
| 110 |
-
>
|
| 111 |
-
{filterOpts.fundingSchemes.map((c) => <option key={c} value={c}>{c}</option>)}
|
| 112 |
-
</ChakraSelect>
|
| 113 |
<ChakraSelect
|
| 114 |
placeholder={loadingFilters ? "Loading..." : "Status"}
|
| 115 |
value={statusFilter}
|
|
@@ -176,12 +202,28 @@ const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
|
|
| 176 |
>
|
| 177 |
<Thead>
|
| 178 |
<Tr>
|
| 179 |
-
|
| 180 |
-
<
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
</Tr>
|
| 186 |
</Thead>
|
| 187 |
<Tbody>
|
|
|
|
| 29 |
fundingSchemes: string[];
|
| 30 |
ids: string[];
|
| 31 |
}
|
| 32 |
+
const MIN_SEARCH_LEN = 5;
|
| 33 |
+
|
| 34 |
+
type SortField = keyof Pick<Project, 'title' | 'status' | 'id' | 'startDate' | 'fundingScheme' | 'ecMaxContribution'>;
|
| 35 |
+
|
| 36 |
+
type SortOrder = 'asc' | 'desc';
|
| 37 |
+
|
| 38 |
|
| 39 |
const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
|
| 40 |
projects,
|
|
|
|
| 52 |
setFundingSchemeFilter,
|
| 53 |
idFilter,
|
| 54 |
setIdFilter,
|
| 55 |
+
setSortField,
|
| 56 |
+
sortField,
|
| 57 |
+
setSortOrder,
|
| 58 |
+
sortOrder,
|
| 59 |
page,
|
| 60 |
setPage,
|
| 61 |
setSelectedProject,
|
|
|
|
| 84 |
if (orgFilter) params.set("organization", orgFilter);
|
| 85 |
if (countryFilter) params.set("country", countryFilter);
|
| 86 |
if (search) params.set("search", search);
|
| 87 |
+
if (idFilter.length >= MIN_SEARCH_LEN) params.set("id", idFilter);
|
| 88 |
if (fundingSchemeFilter) params.set("fundingScheme", fundingSchemeFilter);
|
| 89 |
+
params.set("sortField", sortField);
|
| 90 |
+
params.set("sortOrder", sortOrder);
|
| 91 |
|
| 92 |
fetch(`/api/filters?${params.toString()}`)
|
| 93 |
.then((res) => res.json())
|
|
|
|
| 100 |
num != null
|
| 101 |
? num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
| 102 |
: '-';
|
| 103 |
+
|
| 104 |
+
const handleSort = (field: SortField) => {
|
| 105 |
+
if (sortField === field) {
|
| 106 |
+
// toggle using current sortOrder value
|
| 107 |
+
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
| 108 |
+
} else {
|
| 109 |
+
setSortField(field);
|
| 110 |
+
setSortOrder('asc');
|
| 111 |
+
}
|
| 112 |
+
setPage(0);
|
| 113 |
+
};
|
| 114 |
+
|
| 115 |
+
const handleMinInput = (value: string, setter: (v: string) => void) => {
|
| 116 |
+
setter(value);
|
| 117 |
+
setPage(0);
|
| 118 |
+
};
|
| 119 |
|
| 120 |
|
| 121 |
return (
|
|
|
|
| 129 |
onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
| 130 |
width={{ base: "100%", md: "200px" }}
|
| 131 |
/>
|
| 132 |
+
<Input
|
| 133 |
+
placeholder={`ID (min ${MIN_SEARCH_LEN})`}
|
| 134 |
value={idFilter}
|
| 135 |
+
onChange={(e) => handleMinInput(e.target.value, setIdFilter)}
|
| 136 |
+
w="100px"
|
| 137 |
isDisabled={loadingFilters}
|
| 138 |
+
/>
|
|
|
|
|
|
|
|
|
|
| 139 |
<ChakraSelect
|
| 140 |
placeholder={loadingFilters ? "Loading..." : "Status"}
|
| 141 |
value={statusFilter}
|
|
|
|
| 202 |
>
|
| 203 |
<Thead>
|
| 204 |
<Tr>
|
| 205 |
+
<Thead>
|
| 206 |
+
<Tr>
|
| 207 |
+
<Th w="50%" whiteSpace="nowrap" onClick={() => handleSort('title')} cursor="pointer">
|
| 208 |
+
Title{sortField==='title'? (sortOrder==='asc'?' ↑':' ↓'):''}
|
| 209 |
+
</Th>
|
| 210 |
+
<Th w="10%" whiteSpace="nowrap" onClick={() => handleSort('status')} cursor="pointer">
|
| 211 |
+
Status{sortField==='status'? (sortOrder==='asc'?' ↑':' ↓'):''}
|
| 212 |
+
</Th>
|
| 213 |
+
<Th w="10%" whiteSpace="nowrap" onClick={() => handleSort('id')} cursor="pointer">
|
| 214 |
+
ID{sortField==='id'? (sortOrder==='asc'?' ↑':' ↓'):''}
|
| 215 |
+
</Th>
|
| 216 |
+
<Th w="10%" whiteSpace="nowrap" onClick={() => handleSort('startDate')} cursor="pointer">
|
| 217 |
+
Start Date{sortField==='startDate'? (sortOrder==='asc'?' ↑':' ↓'):''}
|
| 218 |
+
</Th>
|
| 219 |
+
<Th w="10%" whiteSpace="nowrap" onClick={() => handleSort('fundingScheme')} cursor="pointer">
|
| 220 |
+
Funding Scheme{sortField==='fundingScheme'? (sortOrder==='asc'?' ↑':' ↓'):''}
|
| 221 |
+
</Th>
|
| 222 |
+
<Th w="10%" whiteSpace="nowrap" onClick={() => handleSort('ecMaxContribution')} cursor="pointer">
|
| 223 |
+
Funding (€){sortField==='ecMaxContribution'? (sortOrder==='asc'?' ↑':' ↓'):''}
|
| 224 |
+
</Th>
|
| 225 |
+
</Tr>
|
| 226 |
+
</Thead>
|
| 227 |
</Tr>
|
| 228 |
</Thead>
|
| 229 |
<Tbody>
|
frontend/src/hooks/types.ts
CHANGED
|
@@ -93,6 +93,10 @@ export interface ProjectExplorerProps {
|
|
| 93 |
setFundingSchemeFilter: (value: string) => void;
|
| 94 |
idFilter: string;
|
| 95 |
setIdFilter: (value: string) => void;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
page: number;
|
| 97 |
setPage: React.Dispatch<React.SetStateAction<number>>;
|
| 98 |
setSelectedProject: (project: Project) => void;
|
|
|
|
| 93 |
setFundingSchemeFilter: (value: string) => void;
|
| 94 |
idFilter: string;
|
| 95 |
setIdFilter: (value: string) => void;
|
| 96 |
+
setSortField: (field: string) => void;
|
| 97 |
+
sortField: string;
|
| 98 |
+
setSortOrder : (order: "asc" | "desc") => void;
|
| 99 |
+
sortOrder : "asc" | "desc";
|
| 100 |
page: number;
|
| 101 |
setPage: React.Dispatch<React.SetStateAction<number>>;
|
| 102 |
setSelectedProject: (project: Project) => void;
|
frontend/src/hooks/useAppState.ts
CHANGED
|
@@ -22,6 +22,8 @@ export const useAppState = () => {
|
|
| 22 |
const [countryFilter, setCountryFilter] = useState('');
|
| 23 |
const [fundingSchemeFilter, setFundingSchemeFilter ] = useState('');
|
| 24 |
const [idFilter, setIdFilter] = useState('');
|
|
|
|
|
|
|
| 25 |
const [filters, setFilters] = useState<FilterState>({
|
| 26 |
status: "",
|
| 27 |
organization: "",
|
|
@@ -45,7 +47,7 @@ export const useAppState = () => {
|
|
| 45 |
const messagesEndRef = useRef<HTMLDivElement | null>(null);
|
| 46 |
|
| 47 |
const fetchProjects = () => {
|
| 48 |
-
fetch(`/api/projects?page=${page}&search=${encodeURIComponent(search)}&status=${statusFilter}&legalBasis=${legalFilter}&organization=${orgFilter}&country=${countryFilter}&fundingScheme=${fundingSchemeFilter}&id=${idFilter}`)
|
| 49 |
.then(res => res.json())
|
| 50 |
.then((data: Project[]) => setProjects(data))
|
| 51 |
.catch(console.error);
|
|
@@ -95,7 +97,7 @@ export const useAppState = () => {
|
|
| 95 |
}
|
| 96 |
};
|
| 97 |
|
| 98 |
-
useEffect(fetchProjects, [page, search, statusFilter,legalFilter, orgFilter, countryFilter, fundingSchemeFilter, idFilter]);
|
| 99 |
useEffect(() => {
|
| 100 |
console.log("Updated filters:", filters);
|
| 101 |
fetchStats(filters);
|
|
@@ -126,6 +128,10 @@ export const useAppState = () => {
|
|
| 126 |
setFundingSchemeFilter,
|
| 127 |
idFilter,
|
| 128 |
setIdFilter,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
page,
|
| 130 |
setPage,
|
| 131 |
setSelectedProject,
|
|
|
|
| 22 |
const [countryFilter, setCountryFilter] = useState('');
|
| 23 |
const [fundingSchemeFilter, setFundingSchemeFilter ] = useState('');
|
| 24 |
const [idFilter, setIdFilter] = useState('');
|
| 25 |
+
const [sortField, setSortField] = useState('');
|
| 26 |
+
const [sortOrder, setSortOrder] = useState('');
|
| 27 |
const [filters, setFilters] = useState<FilterState>({
|
| 28 |
status: "",
|
| 29 |
organization: "",
|
|
|
|
| 47 |
const messagesEndRef = useRef<HTMLDivElement | null>(null);
|
| 48 |
|
| 49 |
const fetchProjects = () => {
|
| 50 |
+
fetch(`/api/projects?page=${page}&search=${encodeURIComponent(search)}&status=${statusFilter}&legalBasis=${legalFilter}&organization=${orgFilter}&country=${countryFilter}&fundingScheme=${fundingSchemeFilter}&id=${idFilter}&sortField=${sortField}&sortOrder=${sortOrder}`)
|
| 51 |
.then(res => res.json())
|
| 52 |
.then((data: Project[]) => setProjects(data))
|
| 53 |
.catch(console.error);
|
|
|
|
| 97 |
}
|
| 98 |
};
|
| 99 |
|
| 100 |
+
useEffect(fetchProjects, [page, search, statusFilter,legalFilter, orgFilter, countryFilter, fundingSchemeFilter, idFilter, sortField, sortOrder]);
|
| 101 |
useEffect(() => {
|
| 102 |
console.log("Updated filters:", filters);
|
| 103 |
fetchStats(filters);
|
|
|
|
| 128 |
setFundingSchemeFilter,
|
| 129 |
idFilter,
|
| 130 |
setIdFilter,
|
| 131 |
+
setSortField,
|
| 132 |
+
sortField,
|
| 133 |
+
setSortOrder,
|
| 134 |
+
sortOrder,
|
| 135 |
page,
|
| 136 |
setPage,
|
| 137 |
setSelectedProject,
|