-
Notifications
You must be signed in to change notification settings - Fork 13
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
[Issue #1458]: Setup query param management - writing to the URL part 1 #1462
Changes from 20 commits
069d171
5fbbf66
6d8c024
3b14e4c
603b366
41f6a9b
37679a2
65454f8
9c4878c
abe700f
7c50e46
b52175c
f20a7f2
078590c
774bcd4
0ddaf43
e59c116
0fee347
bfcde67
91e3613
693ddd8
07ef4b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,49 +1,88 @@ | ||
import React, { useEffect, useState } from "react"; | ||
|
||
import { Checkbox } from "@trussworks/react-uswds"; | ||
import React from "react"; | ||
import { useDebouncedCallback } from "use-debounce"; | ||
import { useSearchParamUpdater } from "../../hooks/useSearchParamUpdater"; | ||
|
||
interface StatusOption { | ||
id: string; | ||
label: string; | ||
value: string; | ||
} | ||
|
||
interface SearchOpportunityStatusProps { | ||
formRef: React.RefObject<HTMLFormElement>; | ||
} | ||
|
||
const statusOptions: StatusOption[] = [ | ||
{ id: "status-forecasted", label: "Forecasted", value: "forecasted" }, | ||
{ id: "status-posted", label: "Posted", value: "posted" }, | ||
{ id: "status-closed", label: "Closed", value: "closed" }, | ||
{ id: "status-archived", label: "Archived", value: "archived" }, | ||
]; | ||
|
||
// Wait a half-second before updating query params | ||
// and submitting the form | ||
const SEARCH_OPPORTUNITY_STATUS_DEBOUNCE_TIME = 500; | ||
|
||
const SearchOpportunityStatus: React.FC<SearchOpportunityStatusProps> = ({ | ||
formRef, | ||
}) => { | ||
const [mounted, setMounted] = useState(false); | ||
const { updateQueryParams } = useSearchParamUpdater(); | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we get to remove the eslint disable statement in the part 2 pr |
||
const [selectedStatuses, setSelectedStatuses] = useState<Set<string>>( | ||
new Set(), | ||
); | ||
|
||
const debouncedUpdate = useDebouncedCallback( | ||
(selectedStatuses: Set<string>) => { | ||
const key = "status"; | ||
updateQueryParams(selectedStatuses, key); | ||
formRef?.current?.requestSubmit(); | ||
}, | ||
SEARCH_OPPORTUNITY_STATUS_DEBOUNCE_TIME, | ||
); | ||
|
||
const handleCheck = (statusValue: string, isChecked: boolean) => { | ||
setSelectedStatuses((prevSelectedStatuses) => { | ||
const updatedStatuses = new Set(prevSelectedStatuses); | ||
isChecked | ||
? updatedStatuses.add(statusValue) | ||
: updatedStatuses.delete(statusValue); | ||
|
||
debouncedUpdate(updatedStatuses); | ||
return updatedStatuses; | ||
}); | ||
}; | ||
|
||
useEffect(() => { | ||
setMounted(true); | ||
return () => { | ||
setMounted(false); | ||
}; | ||
}, []); | ||
|
||
export default function SearchOpportunityStatus() { | ||
return ( | ||
<> | ||
<h4 className="margin-bottom-1">Opportunity status</h4> | ||
|
||
<div className="grid-row flex-wrap"> | ||
<div className="grid-col-6 padding-right-1"> | ||
<Checkbox | ||
id="status-forecasted" | ||
name="status-forecasted" | ||
label="Forecasted" | ||
tile={true} | ||
className="" | ||
/> | ||
</div> | ||
<div className="grid-col-6 padding-right-1"> | ||
<Checkbox | ||
id="status-posted" | ||
name="status-posted" | ||
label="Posted" | ||
tile={true} | ||
className="" | ||
/> | ||
</div> | ||
<div className="grid-col-6 padding-right-1"> | ||
<Checkbox | ||
id="status-closed" | ||
name="status-closed" | ||
label="Closed" | ||
tile={true} | ||
className="" | ||
/> | ||
</div> | ||
<div className="grid-col-6 padding-right-1"> | ||
<Checkbox | ||
id="status-archived" | ||
name="status-archived" | ||
label="Archived" | ||
tile={true} | ||
className="" | ||
/> | ||
</div> | ||
{statusOptions.map((option) => ( | ||
<div key={option.id} className="grid-col-6 padding-right-1"> | ||
<Checkbox | ||
id={option.id} | ||
name={option.id} | ||
label={option.label} | ||
tile={true} | ||
onChange={(e) => handleCheck(option.value, e.target.checked)} | ||
disabled={!mounted} // Required to be disabled until hydrated so query params are updated properly | ||
rylew1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/> | ||
</div> | ||
))} | ||
</div> | ||
</> | ||
); | ||
} | ||
}; | ||
|
||
export default SearchOpportunityStatus; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,10 @@ const SearchResultsList: React.FC<SearchResultsListProps> = ({ | |
<div className="grid-row flex-column"> | ||
<div className="grid-col tablet:order-2"> | ||
<h2 className="margin-y-105 line-height-serif-2"> | ||
{/* TODO: href here needs to be set to: | ||
dev/staging: https://grants.gov/search-results-detail/<opportunity_id> | ||
local/prod: https://grants.gov/search-results-detail/<opportunity_id> | ||
*/} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a note to set the anchor link to the actual grant/opportunity - it differs for each environment There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, this is the same URL twice. eg. |
||
<a href="#" className="usa-link usa-link--external"> | ||
{opportunity.opportunity_title} | ||
</a> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { usePathname, useSearchParams } from "next/navigation"; | ||
|
||
export function useSearchParamUpdater() { | ||
const searchParams = useSearchParams(); | ||
const pathname = usePathname() || ""; | ||
|
||
// Singular string param updates include: search input, dropdown, and page numbers | ||
// Multiple param updates include filters: Opportunity Status, Funding Instrument, Eligibility, Agency, Category | ||
const updateQueryParams = ( | ||
queryParamValue: string | Set<string>, | ||
key: string, | ||
) => { | ||
const params = new URLSearchParams(searchParams || {}); | ||
|
||
const finalQueryParamValue = | ||
queryParamValue instanceof Set | ||
? Array.from(queryParamValue).join(",") | ||
: queryParamValue; | ||
|
||
if (finalQueryParamValue) { | ||
params.set(key, finalQueryParamValue); | ||
} else { | ||
params.delete(key); | ||
} | ||
|
||
let newPath = `${pathname}?${params.toString()}`; | ||
newPath = newPath.replaceAll("%2C", ","); | ||
|
||
window.history.pushState({}, "", newPath); | ||
}; | ||
|
||
return { | ||
updateQueryParams, | ||
rylew1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ref
here allows us to submit from non button inputs (which are basically most our inputs since we just have one submit/search button in theSearchBar
component