Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

916 header quick search #998

Merged
merged 14 commits into from
Feb 4, 2025
6 changes: 4 additions & 2 deletions frontend/src/components/AuthenticatedAppHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import AvatarAndUserProfile from "./header/AvatarAndUserProfile";
import Menu from "./header/Menu";
import FooterVisibilityContext from "../FooterVisibilityContext";
import Logo from "./svg/Logo";
import HeaderQuickSearch from "./header/HeaderQuickSearch";

export default function AuthenticatedAppHeader({
username,
Expand Down Expand Up @@ -37,7 +38,7 @@ export default function AuthenticatedAppHeader({
className="container"
style={{
height: "50px",
paddingLeft: "5%",
paddingLeft: 0,
paddingRight: "5%",
}}
>
Expand All @@ -60,7 +61,7 @@ export default function AuthenticatedAppHeader({
href="/"
>
<Logo />
{process.env.SITE_NAME}
<span className="site-name">{process.env.SITE_NAME}</span>
</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
Expand All @@ -78,6 +79,7 @@ export default function AuthenticatedAppHeader({
showClassicEncounterSearch={showClassicEncounterSearch}
/>
</Nav>
<HeaderQuickSearch />
<NotificationButton
collaborationTitle={collaborationTitle}
collaborationData={collaborationData}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/header/AvatarAndUserProfile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function AvatarAndUserProfile({ avatar }) {
};

return (
<Nav style={{ alignItems: "center", marginLeft: "20px", width: 50 }}>
<Nav style={{ alignItems: "center", marginLeft: "5px", width: 50 }}>
<NavDropdown
title={<Avatar avatar={avatar} />}
id="basic-nav-dropdown"
Expand Down
150 changes: 150 additions & 0 deletions frontend/src/components/header/HeaderQuickSearch.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React, { useState, useContext } from "react";
import { Dropdown, FormControl, Spinner } from "react-bootstrap";
import MainButton from "../MainButton";
import { FormattedMessage } from "react-intl";
import ThemeColorContext from "../../ThemeColorProvider";
import usePostHeaderQuickSearch from "../../models/usePostHeaderQuickSearch";

export default function HeaderQuickSearch() {
const [search, setSearch] = useState("");
const [showDropdown, setShowDropdown] = useState(false);
const theme = useContext(ThemeColorContext);

const { searchResults, loading } = usePostHeaderQuickSearch(search);

const handleInputChange = (e) => {
setSearch(e.target.value);
};

const handleClearSearch = () => {
setSearch(""); //
setShowDropdown(false);
};

return (
<div className="header-quick-search">
<Dropdown show={showDropdown} onBlur={() => setShowDropdown(false)}>
<div className="d-flex">
<FormControl
type="text"
placeholder="Search Individuals"
value={search}
onChange={handleInputChange}
onFocus={() => setShowDropdown(true)}
style={{
height: "35px",
width: "150px",
paddingLeft: "10px",
backgroundColor: "transparent",
border: "1px solid white",
borderRadius: "20px 0px 0px 20px",
color: "white",
borderRight: "none",
}}
className="header-quick-search-input"
/>
<button
className="header-quick-search-button"
style={{
height: "35px",
backgroundColor: "transparent",
border: "1px solid white",
borderRadius: "0px 20px 20px 0px",
color: "white",
borderLeft: "none",
}}
onClick={handleClearSearch}
>
<i className="bi bi-x"></i>
</button>
</div>

<Dropdown.Menu
style={{
width: "300px",
marginTop: "10px",
overflow: "auto",
maxHeight: "400px",
minHeight: "100px",
}}
>
{loading && (
<Dropdown.Item className="text-center">
<Spinner animation="border" size="sm" />
<span className="ms-2">
<FormattedMessage id="LOADING" />
</span>
</Dropdown.Item>
)}
{!loading && searchResults.length === 0 && search.trim() === "" && (
<Dropdown.Item>
<FormattedMessage id="SEARCH_RESULT_DISPLAY" />
</Dropdown.Item>
)}
{!loading && searchResults.length === 0 && search.trim() !== "" && (
<Dropdown.Item>
<FormattedMessage id="NO_MATCHING_RESULTS" />
</Dropdown.Item>
)}
{!loading &&
searchResults.map((result, index) => (
<React.Fragment key={index}>
<Dropdown.Item
key={index}
as="button"
target="_blank"
rel="noopener noreferrer"
onMouseDown={(e) => {
e.preventDefault();
window.open(`/individuals.jsp?id=${result.id}`);
}}
>
<div className="d-flex flex-row justify-content-between">
<div
className="individual-name"
style={{
width: "180px",
fontSize: "0.8rem",
overflow: "hidden",
}}
>
<div>{search}</div>
<div>{result.taxonomy}</div>
</div>
<MainButton
noArrow={true}
style={{
width: "80px",
height: "30px",
color: "white",
fontSize: "0.8rem",
marginRight: 0,
}}
backgroundColor={theme.primaryColors.primary500}
>
<FormattedMessage
id={
result?.id
?.toLowerCase()
.includes(search.toLowerCase())
? "SYSTEM_ID"
: result?.names?.some((name) =>
name
.toLowerCase()
.includes(search.toLowerCase()),
)
? "FILTER_NAME"
: "UNKNOWN"
}
/>
</MainButton>
</div>
{index < searchResults.length - 1 && <Dropdown.Divider />}
</Dropdown.Item>
</React.Fragment>
))}
</Dropdown.Menu>
</Dropdown>
</div>
);
}
4 changes: 2 additions & 2 deletions frontend/src/components/header/Menu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export default function Menu({
style={{
color: "white",
boxSizing: "border-box",
paddingLeft: 5,
paddingRight: 5,
paddingLeft: 2,
paddingRight: 2,
borderBottom:
dropdownBorder[`dropdown${idx + 1}`] || "2px solid transparent",
}}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/navBar/NotificationButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const NotificationButton = ({
style={{
width: "35px",
height: "35px",
marginLeft: "20px",
marginLeft: "5px",
position: "relative",
}}
tabIndex={0}
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/css/dropdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,16 @@

.nav-item>a {
padding: 0.5rem 1rem;
}

@media (max-width: 1100px) {
.site-name {
display: none !important;
}
}

.header-quick-search-input::placeholder {
color: gray;
opacity: 0.6;
font-size: 0.9rem;
}
9 changes: 7 additions & 2 deletions frontend/src/locale/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -359,5 +359,10 @@
"BEARING": "Peilung",
"DISTANCE": "Entfernung",
"FILTER_BIRTH" : "Geburt",
"FILTER_DEATH" : "Tod"
}
"FILTER_DEATH" : "Tod",
"NO_MATCHING_RESULTS": "Keine passenden Ergebnisse",
"SYSTEM_ID": "System-ID",
"SEARCH_INDIVIDUALS": "Individuen suchen",
"SEARCH_RESULT_DISPLAY" : "Ihre Suchergebnisse werden hier angezeigt.",
"UNKNOWN" : "Unbekannt"
}
8 changes: 7 additions & 1 deletion frontend/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -359,5 +359,11 @@
"BEARING" : "Bearing",
"DISTANCE" : "Distance",
"FILTER_BIRTH" : "Birth",
"FILTER_DEATH" : "Death"
"FILTER_DEATH" : "Death",
"NO_MATCHING_RESULTS": "No matching results",
"SYSTEM_ID": "System ID",
"SEARCH_INDIVIDUALS": "Search Individuals",
"SEARCH_RESULT_DISPLAY" : "Your search results will appear here",
"UNKNOWN" : "Unknown"

}
7 changes: 6 additions & 1 deletion frontend/src/locale/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -358,5 +358,10 @@
"BEARING": "Rumbo",
"DISTANCE": "Distancia",
"FILTER_BIRTH" : "Nacimiento",
"FILTER_DEATH" : "Muerte"
"FILTER_DEATH" : "Muerte",
"NO_MATCHING_RESULTS": "No hay resultados coincidentes",
"SYSTEM_ID": "ID del Sistema",
"SEARCH_INDIVIDUALS": "Buscar Individuos",
"SEARCH_RESULT_DISPLAY": "Tus resultados de búsqueda aparecerán aquí.",
"UNKNOWN" : "Desconocido"
}
7 changes: 6 additions & 1 deletion frontend/src/locale/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -358,5 +358,10 @@
"BEARING": "Cap",
"DISTANCE": "Distance",
"FILTER_BIRTH" : "Naissance",
"FILTER_DEATH" : "Décès"
"FILTER_DEATH" : "Décès",
"NO_MATCHING_RESULTS": "Aucun résultat correspondant",
"SYSTEM_ID": "ID du Système",
"SEARCH_INDIVIDUALS": "Rechercher des Individus",
"SEARCH_RESULT_DISPLAY": "Vos résultats de recherche apparaîtront ici",
"UNKNOWN" : "Inconnu"
}
7 changes: 6 additions & 1 deletion frontend/src/locale/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -358,5 +358,10 @@
"BEARING": "Direzione",
"DISTANCE": "Distanza",
"FILTER_BIRTH" : "Nascita",
"FILTER_DEATH" : "Morte"
"FILTER_DEATH" : "Morte",
"NO_MATCHING_RESULTS": "Nessun risultato corrispondente",
"SYSTEM_ID": "ID di sistema",
"SEARCH_INDIVIDUALS": "Cerca individui",
"SEARCH_RESULT_DISPLAY": "I tuoi risultati di ricerca appariranno qui",
"UNKNOWN" : "Sconosciuto"
}
56 changes: 56 additions & 0 deletions frontend/src/models/usePostHeaderQuickSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import axios from "axios";
import { useState, useEffect } from "react";

export default function usePostHeaderQuickSearch(value) {
const [searchResults, setSearchResults] = useState([]);
const [loading, setLoading] = useState(false);

useEffect(() => {
if (!value.trim()) {
setSearchResults([]);
setLoading(false);
return;
}

const delayDebounce = setTimeout(() => {
setLoading(true);
axios
.post("/api/v3/search/individual?size=10", {
query: {
bool: {
should: [
{
wildcard: {
names: {
value: `*${value}*`,
case_insensitive: true,
},
},
},
{
wildcard: {
id: {
value: `*${value}*`,
case_insensitive: true,
},
},
},
],
},
},
})
.then((response) => {
setSearchResults(response?.data?.hits || []);
setLoading(false);
})
.catch((error) => {
console.error("Error fetching search results:", error);
setLoading(false);
});
}, 300);

return () => clearTimeout(delayDebounce);
}, [value]);

return { searchResults, loading };
}
9 changes: 9 additions & 0 deletions src/main/resources/bundles/de/header.properties
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,12 @@ sessionHeaderWarning = Warnung vor Sitzungszeit\u00FCberschreitung
sessionModalContent = Ihre Sitzung l\u00E4uft in K\u00FCrze ab.
sessionLoginButton = Anmelden
sessionLoginModalContent = Sie wurden vom System abgemeldet.

systemId = System-ID
Name = Name
unknown = Unbekannt
loading = Lade...
noMatchResults = Keine \u00DCbereinstimmungsergebnisse
errorOccurred = Fehler aufgetreten
searchResultDisplay = Suchergebnisanzeige
searchIndividuals = Individuen suchen
11 changes: 11 additions & 0 deletions src/main/resources/bundles/en/header.properties
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,14 @@ sessionHeaderWarning = Session Timeout Warning
sessionModalContent = Your session is about to expire.
sessionLoginButton = Log in
sessionLoginModalContent = You've been logged out of the system.


systemId = System-ID
Name = Name
unknown = Unknown
loading = Loading...
noMatchResults = No matching results
errorOccurred = An error occurred
searchResultDisplay = Search Result Display
searchIndividuals = Search Individuals

9 changes: 9 additions & 0 deletions src/main/resources/bundles/es/header.properties
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,12 @@ sessionHeaderWarning = Advertencia de Tiempo de Sesi\u00f3n Agotado
sessionModalContent = Tu sesi\u00f3n est\u00e1 a punto de expirar.
sessionLoginButton = Acceso
sessionLoginModalContent = Has cerrado tu sesi\u00f3n en el sistema.

systemId = ID del sistema
Name = Nombre
unknown = Desconocido
loading = Cargando...
noMatchResults = No hay resultados coincidentes
errorOccurred = Se ha producido un error
searchResultDisplay = Mostrar resultados de b\u00fasqueda
searchIndividuals = Buscar individuos
Loading