diff --git a/frontend/src/AuthenticatedSwitch.jsx b/frontend/src/AuthenticatedSwitch.jsx index 027c7958c7..eb864a797c 100644 --- a/frontend/src/AuthenticatedSwitch.jsx +++ b/frontend/src/AuthenticatedSwitch.jsx @@ -13,6 +13,7 @@ import Citation from "./pages/Citation"; import AdminLogs from "./pages/AdminLogs"; import ReportEncounter from "./pages/ReportsAndManagamentPages/ReportEncounter"; import ReportConfirm from "./pages/ReportsAndManagamentPages/ReportConfirm"; +import ProjectList from "./pages/ProjectList"; export default function AuthenticatedSwitch({ showAlert, @@ -58,6 +59,7 @@ export default function AuthenticatedSwitch({ } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/constants/navMenu.js b/frontend/src/constants/navMenu.js index 8c4613d371..062edf77c4 100644 --- a/frontend/src/constants/navMenu.js +++ b/frontend/src/constants/navMenu.js @@ -154,7 +154,7 @@ const authenticatedMenu = (username, showclassicsubmit) => [ defaultMessage="My Projects" /> ), - href: "/projects/projectList.jsp", + href: "/react/projects/overview", }, ], }, diff --git a/frontend/src/css/projectList.css b/frontend/src/css/projectList.css new file mode 100644 index 0000000000..64f8bf3df0 --- /dev/null +++ b/frontend/src/css/projectList.css @@ -0,0 +1,130 @@ +.projectListDiv { + margin-top: 20px; + margin-bottom: 40px; +} + +.headerContainer { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.newProject { + box-shadow: 3px 3px black; + background-color: #007DA3; + color: white; + padding: 8px 16px; /* Add padding for a nicer look */ + border: none; + border-radius: 4px; + cursor: pointer; +} + +table { + cursor: pointer; + border-spacing: 0; + width: 100%; + margin-bottom: 20px; +} + +table thead th { + background-color: white; + padding: 8px; + text-align: left; +} + +table tbody tr:nth-child(odd) { + background-color: #f2f2f3; +} + +table tbody tr:nth-child(even) { + background-color: white; +} + +table tbody td { + max-width: 350px; + word-wrap: break-word; + padding: 8px; +} + +table thead tr th { + background-image: url('../../../src/main/webapp/javascript/tablesorter/themes/blue/bg.gif'); + background-repeat: no-repeat; + background-position: center right; + padding-right: 30px; +} + +table thead tr .headerSortUp { + background-image: url('../../../src/main/webapp/javascript/tablesorter/themes/blue/asc.gif'); +} +table thead tr .headerSortDown { + background-image: url('../../../src/main/webapp/javascript/tablesorter/themes/blue/desc.gif'); +} + +.pagination { + display: flex; + justify-content: center; +} + +.pagination .itemCounter { + margin-right: 10px; + display: flex; + align-items: center; +} + +.pagination button { + cursor: pointer; + background-color: white; + background-repeat: no-repeat; + background-position: center; + background-size: 16px 16px; + outline: none; + border: 1px solid #DEE2E6; + color: #007DA3; + padding: 6px 8px; +} + +.pagination .previous-button { + background-image: url('../../../src/main/webapp/javascript/tablesorter/themes/pagination/previous.jpg'); +} + +.pagination .next-button { + background-image: url('../../../src/main/webapp/javascript/tablesorter/themes/pagination/next.jpg'); + margin-right: 10px; +} + +.pagination-options { + align-items: center; + margin-right: 10px; +} + +.pagination-options select { + background-color: white; + border: 1px solid #ddd; + outline: none; + cursor: pointer; + padding: 8px 8px; + -webkit-appearance: none; + appearance: none; +} + +.goto-box { + display: flex; + align-items: center; +} + +.goto-box input { + margin-left: 10px; + width: 30px; + border: 1px solid #ddd; + text-align: center; + outline: none; + padding: 6px; +} + +.pagination button.active { + background-color: #007DA3; + color: white; + font-weight: bold; + border: 1px solid #007DA3; +} \ No newline at end of file diff --git a/frontend/src/locale/de.json b/frontend/src/locale/de.json index 2bd943d364..a88f236384 100644 --- a/frontend/src/locale/de.json +++ b/frontend/src/locale/de.json @@ -317,5 +317,14 @@ "BEERROR_REQUIRED" : "Konnte nicht übermittelt werden; erforderliches Feld fehlt: ", "BEERROR_INVALID" : "Konnte nicht übermittelt werden; Feldformat war ungültig: ", "BEERROR_UNKNOWN" : "Konnte aufgrund eines unbekannten Fehlers nicht abgeschickt werden.", - "ANON_UPLOAD_IMAGE_WARNING": "Bilder können erst hochgeladen werden, wenn das Captcha abgeschlossen ist." -} \ No newline at end of file + "ANON_UPLOAD_IMAGE_WARNING": "Bilder können erst hochgeladen werden, wenn das Captcha abgeschlossen ist.", + "PROJECTS_FOR" : "Projekte für", + "NEW_PROJECT" : "Neues Projekt", + "PROJECTS_ID" : "Projekte ID", + "PERCENTAGE_IDENTIFIED" : "Prozentsatz identifiziert", + "TOTAL" : "Gesamt", + "ITEMS" : "Artikel", + "INPUT_PAGE_ALERT" : "Bitte geben Sie eine gültige Seitenzahl zwischen 1 und {totalPages}.", + "NO_PROJECTS" : "Keine Projekte", + "PROJECT_LIST_TITLE": "Wildbook – Meine Projekte" +} diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 663a65a2f0..8278c718e7 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -317,5 +317,14 @@ "BEERROR_REQUIRED" : "Could not submit; missing required field: ", "BEERROR_INVALID" : "Could not submit; field format was invalid: ", "BEERROR_UNKNOWN" : "Could not submit due to unknown error.", - "ANON_UPLOAD_IMAGE_WARNING": "Images cannot be uploaded until captcha is complete." + "ANON_UPLOAD_IMAGE_WARNING": "Images cannot be uploaded until captcha is complete.", + "PROJECTS_FOR" : "Projects for", + "NEW_PROJECT" : "New Project", + "PROJECTS_ID" : "Projects ID", + "PERCENTAGE_IDENTIFIED" : "Percentage Identified", + "TOTAL" : "Total", + "ITEMS" : "items", + "INPUT_PAGE_ALERT" : "Please enter a valid page number between 1 and {totalPages}.", + "NO_PROJECTS" : "No Projects", + "PROJECT_LIST_TITLE": "Wildbook - My Projects" } \ No newline at end of file diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index ab6f09e7ab..a6685a690f 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -316,5 +316,14 @@ "BEERROR_REQUIRED" : "No se ha podido enviar; falta el campo obligatorio: ", "BEERROR_INVALID" : "No se pudo enviar; el formato del campo no era válido: ", "BEERROR_UNKNOWN" : "No se pudo enviar debido a un error desconocido.", - "ANON_UPLOAD_IMAGE_WARNING": "No se pueden subir imágenes hasta que se complete el captcha." + "ANON_UPLOAD_IMAGE_WARNING": "No se pueden subir imágenes hasta que se complete el captcha.", + "PROJECTS_FOR" : "Proyectos para", + "NEW_PROJECT" : "Nuevo Proyecto", + "PROJECTS_ID" : "Identificación de proyectos", + "PERCENTAGE_IDENTIFIED" : "Porcentaje identificado", + "TOTAL" : "Total", + "ITEMS" : "elementos", + "INPUT_PAGE_ALERT" : "Por favor, introduzca un número de página válido entre 1 y {totalPages}.", + "NO_PROJECTS" : "Sin proyectos", + "PROJECT_LIST_TITLE": "Wildbook - Mis Proyectos" } \ No newline at end of file diff --git a/frontend/src/locale/fr.json b/frontend/src/locale/fr.json index 6aadc9ff1a..4f3b72ff64 100644 --- a/frontend/src/locale/fr.json +++ b/frontend/src/locale/fr.json @@ -316,5 +316,14 @@ "BEERROR_REQUIRED" : "Impossible de soumettre ; champ requis manquant : ", "BEERROR_INVALID" : "Impossible de soumettre ; le format du champ n'est pas valide : ", "BEERROR_UNKNOWN" : "Impossible de soumettre en raison d'une erreur inconnue.", - "ANON_UPLOAD_IMAGE_WARNING": "Les images ne peuvent pas être téléchargées tant que le captcha n'est pas complété." + "ANON_UPLOAD_IMAGE_WARNING": "Les images ne peuvent pas être téléchargées tant que le captcha n'est pas complété.", + "PROJECTS_FOR" : "Projets pour", + "NEW_PROJECT" : "Nouveau projet", + "PROJECTS_ID" : "ID de projets", + "PERCENTAGE_IDENTIFIED" : "Pourcentage identifié", + "TOTAL" : "Total", + "ITEMS" : "articles", + "INPUT_PAGE_ALERT" : "Veuillez saisir un numéro de page valide entre 1 et {totalPages}.", + "NO_PROJECTS" : "Aucun projet", + "PROJECT_LIST_TITLE": "Wildbook - Mes projets" } \ No newline at end of file diff --git a/frontend/src/locale/it.json b/frontend/src/locale/it.json index f0dc73ac18..3cbfa33cd8 100644 --- a/frontend/src/locale/it.json +++ b/frontend/src/locale/it.json @@ -316,5 +316,14 @@ "BEERROR_REQUIRED" : "Impossibile inviare; manca un campo obbligatorio: ", "BEERROR_INVALID" : "Impossibile inviare; il formato del campo non era valido: ", "BEERROR_UNKNOWN" : "Impossibile inviare a causa di un errore sconosciuto.", - "ANON_UPLOAD_IMAGE_WARNING": "Non è possibile caricare immagini finché il captcha non è completato." + "ANON_UPLOAD_IMAGE_WARNING": "Non è possibile caricare immagini finché il captcha non è completato.", + "PROJECTS_FOR" : "Progetti per", + "NEW_PROJECT" : "Nuovo progetto", + "PROJECTS_ID" : "ID progetti", + "PERCENTAGE_IDENTIFIED" : "Percentuale identificata", + "TOTAL" : "Totale", + "ITEMS" : "elementi", + "INPUT_PAGE_ALERT" : "Inserisci un numero di pagina valido compreso tra 1 e {totalPages}.", + "NO_PROJECTS" : "Nessun progetto", + "PROJECT_LIST_TITLE": "Wildbook - I miei progetti" } \ No newline at end of file diff --git a/frontend/src/pages/ProjectList.jsx b/frontend/src/pages/ProjectList.jsx new file mode 100644 index 0000000000..dd8e81519b --- /dev/null +++ b/frontend/src/pages/ProjectList.jsx @@ -0,0 +1,208 @@ +import React, { useState, useEffect } from "react"; +import "../css/projectList.css"; +import axios from "axios"; +import { FormattedMessage } from "react-intl"; +import { useIntl } from "react-intl"; + +export default function ProjectList() { + const intl = useIntl(); + const [currentUser, setCurrentUser] = useState(null); + const [projects, setProjects] = useState([]); + const [sortConfig, setSortConfig] = useState({ key: null, direction: "asc" }); + const [currentPage, setCurrentPage] = useState(1); + const [projectsPerPage, setProjectsPerPage] = useState(10); + const [gotoPage, setGotoPage] = useState(1); + + const fetchData = async () => { + const response = await axios.get("/api/v3/user"); + setCurrentUser(response.data.displayName); + const projectsResponse = await axios.get("/api/v3/projects"); + setProjects(projectsResponse.data.projects); + }; + + useEffect(() => { + document.title = intl.formatMessage({ id: "PROJECT_LIST_TITLE" }); + fetchData(); + }, []); + + const sortProjects = (key) => { + let direction = "asc"; + if (sortConfig.key === key && sortConfig.direction === "asc") { + direction = "desc"; + } + setSortConfig({ key, direction }); + + const sortedProjects = [...projects].sort((a, b) => { + if (a[key] < b[key]) { + return direction === "asc" ? -1 : 1; + } + if (a[key] > b[key]) { + return direction === "asc" ? 1 : -1; + } + return 0; + }); + setProjects(sortedProjects); + }; + + // Determines if table headers have up or down arrow: + const getHeaderClass = (key) => { + if (sortConfig.key !== key) return ""; + return sortConfig.direction === "asc" ? "headerSortUp" : "headerSortDown"; + }; + + const totalPages = Math.ceil(projects.length / projectsPerPage); + + const paginatedProjects = projects.slice( + (currentPage - 1) * projectsPerPage, + currentPage * projectsPerPage, + ); + + const handlePageChange = (page) => { + if (page < 1 || page > totalPages) { + alert(intl.formatMessage({ id: "INPUT_PAGE_ALERT" }, { totalPages })); + return; + } + setCurrentPage(page); + }; + + return ( +
+
+

+ {currentUser} +

+ +
+ {projects.length === 0 ? ( +

+ +

+ ) : ( + + + + + + + + + + + {paginatedProjects.map((project) => ( + + (window.location.href = `/projects/project.jsp?id=${project.id}`) + } + > + + + + + + ))} + +
sortProjects("name")} + className={getHeaderClass("name")} + > + + sortProjects("id")} + className={getHeaderClass("id")} + > + + sortProjects("percentComplete")} + className={getHeaderClass("percentComplete")} + > + + sortProjects("numberEncounters")} + className={getHeaderClass("numberEncounters")} + > + +
{project.name}{project.id}{project.percentComplete}%{project.numberEncounters}
+ )} +
+
+ {projects.length}{" "} + +
+ + {Array.from({ length: totalPages }, (_, index) => ( + + ))} + +
+ + +
+
+ + { + const value = e.target.value; + // Only Numeric Input + if (!value || /^[0-9]+$/.test(value)) { + setGotoPage(Number(value)); + } + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + handlePageChange(gotoPage); + } + }} + /> +
+
+
+ ); +} diff --git a/src/main/resources/servletResponseTemplate.htm b/src/main/resources/servletResponseTemplate.htm index 1c4a6f9cab..cf53b85680 100755 --- a/src/main/resources/servletResponseTemplate.htm +++ b/src/main/resources/servletResponseTemplate.htm @@ -110,7 +110,7 @@
  • My Individuals
  • My Sightings
  • My BulkImports
  • -
  • My Projects
  • +
  • My Projects
  • diff --git a/src/main/webapp/header.jsp b/src/main/webapp/header.jsp index 0ccad495e4..8923af297e 100755 --- a/src/main/webapp/header.jsp +++ b/src/main/webapp/header.jsp @@ -481,7 +481,7 @@ if(request.getUserPrincipal()!=null){
  • <%=props.getProperty("myIndividuals")%>
  • <%=props.getProperty("mySightings")%>
  • <%=props.getProperty("myBulkImports")%>
  • -
  • <%=props.getProperty("myProjects")%>
  • +
  • <%=props.getProperty("myProjects")%>
  • diff --git a/src/main/webapp/javascript/tablesorter/themes/pagination/next.jpg b/src/main/webapp/javascript/tablesorter/themes/pagination/next.jpg new file mode 100644 index 0000000000..532cda6168 Binary files /dev/null and b/src/main/webapp/javascript/tablesorter/themes/pagination/next.jpg differ diff --git a/src/main/webapp/javascript/tablesorter/themes/pagination/previous.jpg b/src/main/webapp/javascript/tablesorter/themes/pagination/previous.jpg new file mode 100644 index 0000000000..cc71adcbdd Binary files /dev/null and b/src/main/webapp/javascript/tablesorter/themes/pagination/previous.jpg differ