Skip to content
Snippets Groups Projects
Commit 8cfeeede authored by Bjarke Madsen's avatar Bjarke Madsen
Browse files

Add ECProjects page

parent 2398f199
No related branches found
No related tags found
1 merge request!11merge feature/COMP-152-EC-PROJECTS-TABLE into develop
......@@ -12,6 +12,7 @@ import StaffGraph from "./pages/StaffGraph";
import { FilterSelection } from "./Schema";
import SubOrganisation from "./pages/SubOrganisation";
import ParentOrganisation from "./pages/ParentOrganisation";
import ECProjects from "./pages/ECProjects";
function App(): ReactElement {
......@@ -35,6 +36,7 @@ function App(): ReactElement {
<Route path="/charging" element={<ChargingStructurePage />} />
<Route path="/suborganisations" element={<SubOrganisation filterSelection={filterSelection} setFilterSelection={setFilterSelection} />} />
<Route path="/parentorganisation" element={<ParentOrganisation filterSelection={filterSelection} setFilterSelection={setFilterSelection} />} />
<Route path="/ec-projects" element={<ECProjects filterSelection={filterSelection} setFilterSelection={setFilterSelection} />} />
<Route path="*" element={<Landing />} />
</Routes>
<GeantFooter />
......
......@@ -130,3 +130,9 @@ export interface Organisation {
name: string,
role?: string
}
export interface ECProject {
nren: string,
year: number,
project: string,
}
......@@ -66,6 +66,11 @@ function CompendiumData(): ReactElement {
<span>NREN Parent Organisations</span>
</Link>
</Row>
<Row>
<Link to="/ec-projects" className="link-text-underline">
<span>NREN Involvement in European Commission Projects</span>
</Link>
</Row>
</div>
</CollapsibleBox>
</div>
......
import React, { useEffect, useMemo, useState } from 'react';
import { Col, Container, Row, Table } from "react-bootstrap";
import Filter from "../components/graphing/Filter"
import {
ECProject,
FilterSelection
} from "../Schema";
async function getData(): Promise<ECProject[]> {
try {
const response = await fetch('/api/ec-project');
return response.json();
} catch (error) {
console.error(`Failed to load data: ${error}`);
throw error;
}
}
function getYearsAndNrens(sourceData: ECProject[]) {
const years = new Set<number>();
const nrens = new Set<string>();
sourceData.forEach(datapoint => {
years.add(datapoint.year);
nrens.add(datapoint.nren);
});
return { years: years, nrens: nrens };
}
interface inputProps {
filterSelection: FilterSelection
setFilterSelection: React.Dispatch<React.SetStateAction<FilterSelection>>
}
function ECProjects({ filterSelection, setFilterSelection }: inputProps) {
const [projectData, setProjectData] = useState<ECProject[]>();
const { years, nrens } = useMemo(
() => getYearsAndNrens(projectData || []),
[projectData]
);
const selectedData = (projectData || []).filter(project =>
filterSelection.selectedYears.includes(project.year) && filterSelection.selectedNrens.includes(project.nren)
);
const projectDataByYear = new Map<number, Map<string, ECProject[]>>();
// group ECProjects by year and nren
selectedData.forEach(project => {
if (!projectDataByYear.has(project.year)) {
projectDataByYear.set(project.year, new Map<string, ECProject[]>());
}
const ecProjectsForYear = projectDataByYear.get(project.year)!;
const nrenData = ecProjectsForYear.get(project.nren) || [];
nrenData.push(project);
ecProjectsForYear.set(project.nren, nrenData);
});
function nrensort(a: ECProject, b: ECProject) {
const byNREN = a.nren < b.nren ? -1 : a.nren > b.nren ? 1 : 0;
const byYear = a.year < b.year ? -1 : a.year > b.year ? 1 : 0;
return byYear || byNREN;
}
// sort projectDataByYear by year and nren, maps keep insertion order
projectDataByYear.forEach((nrenData, year) => {
const sortedNrenData = new Map<string, ECProject[]>();
[...nrenData.entries()].sort((a, b) => nrensort(a[1][0], b[1][0])).forEach(([nren, projects]) => {
sortedNrenData.set(nren, projects);
});
projectDataByYear.set(year, sortedNrenData);
});
useEffect(() => {
const loadData = async () => {
const data = await getData();
setProjectData(data);
// filter fallback for when nothing is selected (only last year for all nrens)
const { years, nrens } = getYearsAndNrens(data);
setFilterSelection(previous => {
const visibleYears = previous.selectedYears.filter(year => years.has(year));
const newSelectedYears = visibleYears.length ? previous.selectedYears : [Math.max(...years)];
const visibleNrens = previous.selectedNrens.filter(nren => nrens.has(nren));
const newSelectedNrens = visibleNrens.length ? previous.selectedNrens : [...nrens];
return { selectedYears: newSelectedYears, selectedNrens: newSelectedNrens };
});
}
loadData()
}, [setFilterSelection]);
return (
<Container>
<Row>
<Col xs={9}>
<Row>
<h3>NREN Involvement in European Commission Projects</h3>
</Row>
<Row>
<Table>
<thead>
<tr>
<th>NREN</th>
<th>Year</th>
<th>EC Project Membership</th>
</tr>
</thead>
<tbody className='table-bg-highlighted'>
{Array.from(projectDataByYear.keys()).sort().map((year) => {
return projectDataByYear.get(year) && Array.from(projectDataByYear.get(year)!.entries()).map(([nren, projects]) => (
<tr key={nren + year + projects.length}>
<td style={{ whiteSpace: 'nowrap' }}>{nren}</td>
<td>{year}</td>
<td>
<ul>
{projects.map(project => (
<li key={project.project}>{project.project}</li>
))}
</ul>
</td>
</tr>
))
})}
</tbody>
</Table>
</Row>
</Col>
<Col xs={3}>
<div className="sticky-top" style={{ top: '1rem' }}>
{/* the sticky-top class makes the filter follow the user as they scroll down the page */}
<Filter
filterOptions={{ availableYears: [...years], availableNrens: [...nrens] }}
filterSelection={filterSelection}
setFilterSelection={setFilterSelection}
/>
</div>
</Col>
</Row>
</Container>
);
}
export default ECProjects;
......@@ -86,12 +86,12 @@
}
@mixin linkHover {
&:hover{
&:hover {
color: #003753
}
}
.link-text{
.link-text {
text-decoration: none;
color: #003753;
@include linkHover;
......@@ -108,7 +108,7 @@
.page-footer {
min-height: 350px;
width: 100%;
bottom:0;
bottom: 0;
justify-content: center;
align-items: center;
padding-top: 20px;
......@@ -125,22 +125,45 @@
}
.btn-compendium {
--bs-btn-color:#fff;
--bs-btn-bg:#003753;
--bs-btn-border-color:#003753;
--bs-btn-hover-color:#fff;
--bs-btn-hover-bg:#3b536b;
--bs-btn-hover-border-color:#3b536b;
--bs-btn-focus-shadow-rgb:49,132,253;
--bs-btn-active-color:#f5f5f5;
--bs-btn-active-bg:#3b536b;
--bs-btn-active-border-color:#003753;
--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color:#fff;
--bs-btn-disabled-bg:#0d6efd;
--bs-btn-disabled-border-color:#0d6efd
--bs-btn-color: #fff;
--bs-btn-bg: #003753;
--bs-btn-border-color: #003753;
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: #3b536b;
--bs-btn-hover-border-color: #3b536b;
--bs-btn-focus-shadow-rgb: 49, 132, 253;
--bs-btn-active-color: #f5f5f5;
--bs-btn-active-bg: #3b536b;
--bs-btn-active-border-color: #003753;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #fff;
--bs-btn-disabled-bg: #0d6efd;
--bs-btn-disabled-border-color: #0d6efd
}
.fit-max-content {
min-width: max-content;
}
.table-bg-highlighted {
tr:nth-child(even) {
background-color: rgba(102, 121, 139, 0.178);
}
tr:hover {
background-color: rgba(102, 121, 139, 0.521);
}
// set LI icons to be a minus sign
li {
list-style-type: square;
list-style-position: inside;
}
}
.sticky-top {
position: sticky;
top: 0;
z-index: 1;
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment