diff --git a/src/app/career-pathways/[industry]/Content/InDemandDetails.tsx b/src/app/career-pathways/[industry]/Content/InDemandDetails.tsx
index 564f069a8..28336c5a8 100644
--- a/src/app/career-pathways/[industry]/Content/InDemandDetails.tsx
+++ b/src/app/career-pathways/[industry]/Content/InDemandDetails.tsx
@@ -3,15 +3,6 @@ import { InfoBox } from "@components/modules/InfoBox";
import { LabelBox } from "@components/modules/LabelBox";
import { SeeMoreList } from "@components/modules/SeeMoreList";
import { Tag } from "@components/modules/Tag";
-import {
- ArrowSquareOut,
- Briefcase,
- GraduationCap,
- Hourglass,
- MapPinLine,
-} from "@phosphor-icons/react";
-import { calendarLength } from "@utils/calendarLength";
-import { toUsCurrency } from "@utils/toUsCurrency";
import {
InDemandItemProps,
OccupationDetail,
@@ -38,7 +29,7 @@ export const InDemandDetails = (props: {
const getJobNumbers = async () => {
const jobNumbers = await fetch(
- `${process.env.REACT_APP_API_URL}/api/jobcount/${props.content.title}`,
+ `${process.env.REACT_APP_API_URL}/api/jobcount/${props.content.title}`
);
const jobNumbersArray = await jobNumbers.json();
@@ -70,7 +61,7 @@ export const InDemandDetails = (props: {
const uniqueTrainings = sortedCourses?.filter(
(training, index, self) =>
- index === self.findIndex((t) => t.name === training.name),
+ index === self.findIndex((t) => t.name === training.name)
);
setSortedTraining(uniqueTrainings);
diff --git a/src/app/career-pathways/[industry]/page.tsx b/src/app/career-pathways/[industry]/page.tsx
index 93f1c37d3..6c68a6236 100644
--- a/src/app/career-pathways/[industry]/page.tsx
+++ b/src/app/career-pathways/[industry]/page.tsx
@@ -11,10 +11,6 @@ async function getData() {
query: CAREER_PATHWAYS_PAGE_QUERY,
});
- if (process.env.REACT_APP_FEATURE_CAREER_PATHWAYS === "false" || !page) {
- return notFound();
- }
-
return {
page,
};
diff --git a/src/app/support-resources/[slug]/Filter.tsx b/src/app/support-resources/[slug]/Filter.tsx
index 442eb3d93..bdf76b49b 100644
--- a/src/app/support-resources/[slug]/Filter.tsx
+++ b/src/app/support-resources/[slug]/Filter.tsx
@@ -28,18 +28,18 @@ export const Filter = ({
page.items[0].title === "Career Support"
? "purple"
: page.items[0].title === "Tuition Assistance"
- ? "green"
- : "navy";
+ ? "green"
+ : "navy";
useEffect(() => {
if (selectedTags.length > 0) {
const filtered = listingItems.resources.items.filter(
(resource: ResourceCardProps) => {
const resourceTags = resource.tagsCollection.items.map(
- (tag) => tag.title,
+ (tag) => tag.title
);
return selectedTags.some((tag) => resourceTags.includes(tag.title));
- },
+ }
);
setFilteredResources(filtered);
} else {
diff --git a/src/app/training/[code]/SocDrawer.tsx b/src/app/training/[code]/SocDrawer.tsx
index 85d297ad3..a0db3717b 100644
--- a/src/app/training/[code]/SocDrawer.tsx
+++ b/src/app/training/[code]/SocDrawer.tsx
@@ -14,10 +14,10 @@ export const SocDrawer = ({
Standard Occupational Classification (SOC) codes
- "The 2018 Standard Occupational Classification (SOC) system is a federal
- statistical standard used by federal agencies to classify workers into
- occupational categories for the purpose of collecting, calculating, or
- disseminating data." 1.
+ "The 2018 Standard Occupational Classification (SOC) system is a
+ federal statistical standard used by federal agencies to classify
+ workers into occupational categories for the purpose of collecting,
+ calculating, or disseminating data." 1.
You can find a list of SOC codes{" "}
diff --git a/src/app/training/search/components/CompareTable.tsx b/src/app/training/search/components/CompareTable.tsx
index b6b9e68b7..36b3e92a0 100644
--- a/src/app/training/search/components/CompareTable.tsx
+++ b/src/app/training/search/components/CompareTable.tsx
@@ -5,17 +5,14 @@ import { X } from "@phosphor-icons/react";
import { calendarLength } from "@utils/calendarLength";
import { toUsCurrency } from "@utils/toUsCurrency";
import { ResultProps } from "@utils/types";
-import { useState } from "react";
+import { useContext, useState } from "react";
+import { ResultsContext } from "./Results";
+
+export const CompareTable = () => {
+ let { compare, setCompare } = useContext(ResultsContext);
-export const CompareTable = ({
- items,
- setCompare,
-}: {
- items: ResultProps[];
- setCompare: (items: ResultProps[]) => void;
-}) => {
const [expanded, setExpanded] = useState(false);
- const remainingBoxes = 3 - items.length;
+ const remainingBoxes = 3 - compare.length;
const remainingBoxesArray = Array.from(Array(remainingBoxes).keys());
@@ -28,7 +25,7 @@ export const CompareTable = ({
- {items.map((item) => (
+ {compare.map((item) => (
{item.name}
@@ -53,7 +50,7 @@ export const CompareTable = ({
Cost
- {items.map((item) => (
+ {compare.map((item) => (
{item.totalCost ? toUsCurrency(item.totalCost) : "--"}
@@ -64,7 +61,7 @@ export const CompareTable = ({
Employment Rate %
- {items.map((item) => (
+ {compare.map((item) => (
{" "}
{item.percentEmployed
@@ -78,7 +75,7 @@ export const CompareTable = ({
Time to Complete
- {items.map((item) => (
+ {compare.map((item) => (
{item.calendarLength
? calendarLength(item.calendarLength)
@@ -91,7 +88,7 @@ export const CompareTable = ({
- {items.map((item) => (
+ {compare.map((item) => (
See Details
@@ -107,7 +104,7 @@ export const CompareTable = ({
) : (
- {items.map((item) => (
+ {compare.map((item) => (
{item.name}
{item.providerName}
@@ -116,12 +113,14 @@ export const CompareTable = ({
onClick={() => {
// find the div with id of item.id and uncheck the checkbox
const checkbox: HTMLInputElement = document.getElementById(
- `checkbox_${item.id}`,
+ `checkbox_${item.id}`
) as HTMLInputElement;
checkbox.checked = false;
setCompare(
- items.filter((compareItem) => compareItem.id !== item.id),
+ compare.filter(
+ (compareItem) => compareItem.id !== item.id
+ )
);
}}
>
diff --git a/src/app/training/search/components/Filter.tsx b/src/app/training/search/components/Filter.tsx
index 6a15e9d87..4aac2d0cc 100644
--- a/src/app/training/search/components/Filter.tsx
+++ b/src/app/training/search/components/Filter.tsx
@@ -1,514 +1,51 @@
"use client";
-import { Button } from "@components/modules/Button";
-import { FormInput } from "@components/modules/FormInput";
-import { Switch } from "@components/modules/Switch";
-import { Flex } from "@components/utility/Flex";
-import { CaretDown, CaretUp, WarningCircle } from "@phosphor-icons/react";
-import { counties } from "@utils/counties";
-import { allLanguages } from "@utils/languages";
-import { camelify } from "@utils/slugify";
-import { FetchResultsProps, ResultProps } from "@utils/types";
-import { zipCodes } from "@utils/zipCodeCoordinates";
-import { useEffect, useState } from "react";
-import { getSearchData } from "../utils/getSearchData";
-
-interface FilterProps {
- allItems?: ResultProps[];
- className?: string;
- searchParams?: string;
- setResults?: (results: FetchResultsProps) => void;
-}
-
-const Filter = ({ className, searchParams = "", setResults }: FilterProps) => {
- const extractQuery = () => {
- return searchParams.split("q=")[1]?.split("&")[0];
- };
-
- const extractParam = (param: string) => {
- const q = new URLSearchParams(searchParams);
- return q.get(param);
- };
- const isInitialZipValid =
- zipCodes.filter((zip) => zip === extractParam("zip")).length > 0
- ? true
- : false;
-
- const [searchValue, setSearchValue] = useState
(extractQuery() || "");
- const [searchQuery, setSearchQuery] = useState(searchParams);
- const [loading, setLoading] = useState(true);
- const [zipError, setZipError] = useState(!isInitialZipValid);
- const [zipCode, setZipCode] = useState(extractParam("zip") || "");
- const [attempted, setAttempted] = useState(
- extractParam("zip") ? !isInitialZipValid || false : false
- );
- const [showMore, setShowMore] = useState(false);
-
- const updateSearchParams = (key: string, value: string) => {
- const q = new URL(window.location.href);
- const searchParams = q.searchParams;
-
- if (!value || value === "" || value === "false") {
- searchParams.delete(key);
- } else {
- searchParams.set(key, value);
- }
-
- setSearchQuery(`${searchParams}`);
- };
-
- const updateSearchParamsNavigate = async (
- keyValueArray: { key: string; value: string }[]
- ) => {
- const q = new URL(window.location.href);
- const searchParams = q.searchParams;
-
- keyValueArray.forEach((keyValue) => {
- if (
- !keyValue.value ||
- keyValue.value === "" ||
- keyValue.value === "false"
- ) {
- searchParams.delete(keyValue.key);
- } else {
- searchParams.set(keyValue.key, keyValue.value);
- }
- });
-
- setSearchQuery(`${searchParams || ""}`);
-
- window.history.pushState(
- {},
- "",
- `${window.location.pathname}?${searchParams}`
- );
-
- const searchParamObject = {
- searchParams: Object.fromEntries(searchParams.entries()),
- };
-
- const searchProps = await getSearchData(searchParamObject as any);
-
- if (setResults) {
- setResults(searchProps);
- }
- };
-
- const removeSearchParams = (keyArray: { key: string }[]) => {
- const q = new URL(window.location.href);
- const searchParams = q.searchParams;
-
- keyArray.forEach((key) => {
- searchParams.delete(key.key);
- setSearchQuery(`${searchParams || ""}`);
- });
-
- window.history.pushState(
- {},
- "",
- `${window.location.pathname}?${searchParams}`
- );
- };
-
- useEffect(() => {
- setLoading(false);
- }, []);
+import { useContext } from "react";
+import {
+ CipSoc,
+ ClearAll,
+ CompletionTime,
+ Cost,
+ County,
+ Distance,
+ FilterForm,
+ Format,
+ InDemand,
+ Language,
+ Services,
+} from "./controls";
+import { ResultsContext } from "./Results";
+import { colors } from "@utils/settings";
+import { X } from "@phosphor-icons/react";
+
+const Filter = () => {
+ const { setToggle, toggle } = useContext(ResultsContext);
return (
-
+
+
+
+ Filters
+ {
+ setToggle(!toggle);
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/src/app/training/search/components/ParamTags.tsx b/src/app/training/search/components/ParamTags.tsx
new file mode 100644
index 000000000..0ec861d5b
--- /dev/null
+++ b/src/app/training/search/components/ParamTags.tsx
@@ -0,0 +1,106 @@
+"use client";
+import { Tag } from "@components/modules/Tag";
+import { Flex } from "@components/utility/Flex";
+import { allLanguages } from "@utils/languages";
+import React, { useContext } from "react";
+import {
+ extractParam,
+ updateSearchParamsNavigate,
+} from "../utils/filterFunctions";
+import { getSearchData } from "../utils/getSearchData";
+import { ResultsContext } from "./Results";
+
+interface ParamTagProps {
+ queryString: string;
+}
+
+const formatKey = (key: string) => {
+ return key
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
+ .replace(/^./, (str) => str.toUpperCase());
+};
+
+const categorizeTag = (key: string): string => {
+ if (key === "online" || key === "inPerson") return "Format";
+ if (["days", "weeks", "months", "years"].includes(key))
+ return "Time to complete";
+ if (allLanguages.includes(formatKey(key))) return "Language";
+ if (key === "maxCost") return "Cost";
+ if (
+ key === "isWheelchairAccessible" ||
+ key === "hasChildcareAssistance" ||
+ key === "hasEveningCourses" ||
+ key === "hasJobPlacementAssistance"
+ )
+ return "Services";
+ if (key === "zipCode") return "ZIP code";
+ return formatKey(key);
+};
+
+const formatValue = (key: string, value: string): string => {
+ if (value === "true") return formatKey(key);
+ return value;
+};
+
+export const ParamTags = () => {
+ const { results, setResults } = useContext(ResultsContext);
+
+ const params = new URLSearchParams(results.searchParams);
+
+ const paramArray = Array.from(params.entries()).filter(
+ ([key]) =>
+ key !== "q" &&
+ key !== "toggle" &&
+ key !== "p" &&
+ key !== "limit" &&
+ key !== "sort"
+ );
+
+ const renderTags = () => {
+ return Array.from(params.entries())
+ .filter(
+ ([key]) =>
+ key !== "q" &&
+ key !== "toggle" &&
+ key !== "p" &&
+ key !== "limit" &&
+ key !== "sort"
+ )
+ .map(([key, value]) => (
+ {
+ updateSearchParamsNavigate(
+ [
+ { key, value: "" },
+ { key: "p", value: "1" },
+ ],
+ getSearchData,
+ setResults
+ );
+ window.location.reload();
+ }}
+ >
+
+
+ ));
+ };
+
+ return (
+
+ {renderTags()}
+ {paramArray.length > 0 && (
+
+ Clear filters
+
+ )}
+
+ );
+};
diff --git a/src/app/training/search/components/Results.tsx b/src/app/training/search/components/Results.tsx
index 78b844709..e16a1303d 100644
--- a/src/app/training/search/components/Results.tsx
+++ b/src/app/training/search/components/Results.tsx
@@ -1,15 +1,15 @@
"use client";
import { ResultCard } from "@components/modules/ResultCard";
import { Filter } from "./Filter";
-import { CipDefinition, FetchResultsProps, ResultProps } from "@utils/types";
-import { useState } from "react";
-import { Button } from "@components/modules/Button";
+import { FetchResultsProps, ResultProps } from "@utils/types";
+import { createContext, useEffect, useState } from "react";
import { ResultsHeader } from "./ResultsHeader";
import { CompareTable } from "./CompareTable";
-import { Breadcrumbs } from "@components/modules/Breadcrumbs";
-import { StarterText } from "./StarterText";
+import { SEARCH_RESULTS_PAGE_DATA as pageContent } from "@data/pages/training/search";
import { HelpText } from "./HelpText";
import { Pagination } from "@components/modules/Pagination";
+import { Alert } from "@components/modules/Alert";
+import { ParamTags } from "./ParamTags";
export interface FilterProps {
searchQuery?: string;
@@ -29,6 +29,24 @@ export interface FilterProps {
socCode?: string;
}
+export interface ContextProps {
+ results: FetchResultsProps;
+ setResults: (results: FetchResultsProps) => void;
+ compare: ResultProps[];
+ setCompare: (compare: ResultProps[]) => void;
+ toggle: boolean;
+ setToggle: (toggle: boolean) => void;
+ searchTerm: string;
+ setSearchTerm: (searchTerm: string) => void;
+ extractParam: (param: string) => string | null;
+ sortValue: string;
+ setSortValue: (sortValue: string) => void;
+ itemsPerPage: string;
+ setItemsPerPage: (itemsPerPage: string) => void;
+}
+
+export const ResultsContext = createContext({} as ContextProps);
+
const Results = ({
items,
query,
@@ -51,6 +69,9 @@ const Results = ({
const [compare, setCompare] = useState([]);
const [toggle, setToggle] = useState(extractParam("toggle") === "true");
+ const [searchTerm, setSearchTerm] = useState(extractParam("q") || "");
+ const [sortValue, setSortValue] = useState(extractParam("sort") || "");
+ const [itemsPerPage, setItemsPerPage] = useState(extractParam("limit") || "");
const [results, setResults] = useState({
itemCount: count,
pageData: items,
@@ -60,60 +81,45 @@ const Results = ({
totalPages: totalPages,
});
- return (
- <>
-
- {compare.length > 0 && (
-
- )}
-
- {
- setToggle(!toggle);
- }}
- >
- Edit Search or Filter
-
+ useEffect(() => {
+ if (toggle && window.innerWidth < 768) {
+ document.body.style.overflow = "hidden";
+ } else {
+ document.body.style.overflow = "auto";
+ }
+ }, [toggle]);
-
+ return (
+
+ {compare.length > 0 && }
+
+ {results.searchParams && }
-
+
-
+
<>
{results.pageData.length === 0 && !query && page === 1 ? (
-
+
) : results.pageData.length <= 3 && page === 1 ? (
-
+
) : (
<>>
)}
@@ -153,17 +159,10 @@ const Results = ({
))}
>
- {results.totalPages > 1 && (
-
10}
- hasNextPage={results.pageData.length > 10}
- />
- )}
+ {results.totalPages > 1 && }
- >
+
);
};
diff --git a/src/app/training/search/components/ResultsHeader.tsx b/src/app/training/search/components/ResultsHeader.tsx
index 4725e3469..4b94d838e 100644
--- a/src/app/training/search/components/ResultsHeader.tsx
+++ b/src/app/training/search/components/ResultsHeader.tsx
@@ -1,56 +1,140 @@
+"use client";
+import { Button } from "@components/modules/Button";
import { FormInput } from "@components/modules/FormInput";
import { Heading } from "@components/modules/Heading";
import { decodeUrlEncodedString } from "@utils/decodeUrlEncodedString";
-import { useEffect, useState } from "react";
+import { useContext, useEffect } from "react";
+import { ResultsContext } from "./Results";
+import { handleSortChange } from "../utils/handleSortChange";
+import { handleItemsPerPageChange } from "../utils/handleItemsPerPageChange";
+import { SEARCH_RESULTS_PAGE_DATA as contentData } from "@data/pages/training/search";
+import {
+ extractParam,
+ updateSearchParamsNavigate,
+} from "../utils/filterFunctions";
+import { FunnelSimple, MagnifyingGlass } from "@phosphor-icons/react";
+import { getSearchData } from "../utils/getSearchData";
-export const ResultsHeader = ({
- count,
- query,
- defaultSort,
-}: {
- count: number;
- query: string;
- defaultSort?: string;
-}) => {
- const [sortValue, setSortValue] = useState(defaultSort);
+export const ResultsHeader = () => {
+ let {
+ itemsPerPage,
+ results,
+ searchTerm,
+ setItemsPerPage,
+ setResults,
+ setSearchTerm,
+ setSortValue,
+ setToggle,
+ sortValue,
+ toggle,
+ } = useContext(ResultsContext);
+
+ const params = new URLSearchParams(results.searchParams);
+
+ const paramArray = Array.from(params.entries()).filter(
+ ([key]) =>
+ key !== "q" &&
+ key !== "toggle" &&
+ key !== "p" &&
+ key !== "limit" &&
+ key !== "sort"
+ );
useEffect(() => {
if (typeof window !== "undefined") {
const urlParams = new URLSearchParams(window.location.search);
setSortValue(urlParams.get("sort") || "");
+ setItemsPerPage(urlParams.get("limit") || "");
}
}, []);
- const handleSortChange = (e: React.ChangeEvent
) => {
- if (typeof window !== "undefined") {
- const q = new URL(window.location.href);
- const searchParams = q.searchParams;
- if (e.target.value === "") {
- searchParams.delete("sort");
- const newUrlWithSort = searchParams.toString();
- window.location.href = `/training/search?${newUrlWithSort}`;
- } else {
- const newUrlWithSort = new URLSearchParams(searchParams);
- newUrlWithSort.set("sort", e.target.value);
- window.location.href = `/training/search?${newUrlWithSort}`;
- }
- }
- };
-
return (
-
- {count === 0 && (query === "undefined" || query === "null") ? (
- <>Find Training>
- ) : (
- <>
- {count} {count === 1 ? "result" : "results"} found for "
- {decodeUrlEncodedString(query)}"
- >
- )}
-
+
+
+ {results.itemCount === 0 &&
+ (searchTerm === "undefined" || searchTerm === "null") ? (
+ <>Find Training>
+ ) : (
+ <>
+ {results.itemCount}{" "}
+ {results.itemCount === 1 ? "result" : "results"} found for "
+ {decodeUrlEncodedString(`${extractParam("q", results)}`)}"
+ >
+ )}
+
+
+
+ {
+ setToggle(!toggle);
+ }}
+ >
+
+ Filter for Search
+ {paramArray.length > 0 && (
+ {paramArray.length}
+ )}
+
+
+
+
+
+
{
+ setToggle(!toggle);
+ updateSearchParamsNavigate(
+ [{ key: "toggle", value: (!toggle).toString() }],
+ getSearchData,
+ setResults
+ );
+ }}
+ >
+ {`${toggle ? "Hide" : "Show"} Filters`}
+
- {count > 0 && (
+
+
- )}
+
);
};
diff --git a/src/app/training/search/components/controls/CipSoc.tsx b/src/app/training/search/components/controls/CipSoc.tsx
new file mode 100644
index 000000000..4dc3c68ec
--- /dev/null
+++ b/src/app/training/search/components/controls/CipSoc.tsx
@@ -0,0 +1,53 @@
+"use client";
+import { useContext } from "react";
+import {
+ extractParam,
+ updateSearchParamsNavigate,
+} from "../../utils/filterFunctions";
+import { ResultsContext } from "../Results";
+import { FormInput } from "@components/modules/FormInput";
+import { getSearchData } from "../../utils/getSearchData";
+import { Flex } from "@components/utility/Flex";
+
+export const CipSoc = () => {
+ let { results, setResults } = useContext(ResultsContext);
+
+ return (
+
+ {
+ updateSearchParamsNavigate(
+ [
+ { key: "cipCode", value: e.target.value },
+ { key: "p", value: "1" },
+ ],
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+ {
+ updateSearchParamsNavigate(
+ [
+ { key: "socCode", value: e.target.value },
+ { key: "p", value: "1" },
+ ],
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+
+ );
+};
diff --git a/src/app/training/search/components/controls/ClearAll.tsx b/src/app/training/search/components/controls/ClearAll.tsx
new file mode 100644
index 000000000..b19850b3f
--- /dev/null
+++ b/src/app/training/search/components/controls/ClearAll.tsx
@@ -0,0 +1,33 @@
+import { Button } from "@components/modules/Button";
+import { extractParam } from "../../utils/filterFunctions";
+import { useContext } from "react";
+import { ResultsContext } from "../Results";
+
+export const ClearAll = () => {
+ let { results, setToggle } = useContext(ResultsContext);
+ return (
+
+ {
+ setToggle(false);
+ }}
+ />
+ {
+ window.location.href = `/training/search?q=${extractParam(
+ "q",
+ results
+ )}`;
+ }}
+ />
+
+ );
+};
diff --git a/src/app/training/search/components/controls/CompletionTime.tsx b/src/app/training/search/components/controls/CompletionTime.tsx
new file mode 100644
index 000000000..b59f1bad9
--- /dev/null
+++ b/src/app/training/search/components/controls/CompletionTime.tsx
@@ -0,0 +1,85 @@
+"use client";
+import { useContext } from "react";
+import {
+ extractParam,
+ updateSearchParamsNavigate,
+} from "../../utils/filterFunctions";
+import { ResultsContext } from "../Results";
+import { FormInput } from "@components/modules/FormInput";
+import { getSearchData } from "../../utils/getSearchData";
+
+export const CompletionTime = () => {
+ let { results, setResults } = useContext(ResultsContext);
+
+ return (
+
+
Time to Complete
+
+ {
+ updateSearchParamsNavigate(
+ [
+ { key: "days", value: e.target.checked.toString() },
+ { key: "p", value: "1" },
+ ],
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+ {
+ updateSearchParamsNavigate(
+ [
+ { key: "weeks", value: e.target.checked.toString() },
+ { key: "p", value: "1" },
+ ],
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+ {
+ updateSearchParamsNavigate(
+ [
+ { key: "months", value: e.target.checked.toString() },
+ { key: "p", value: "1" },
+ ],
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+ {
+ updateSearchParamsNavigate(
+ [
+ { key: "years", value: e.target.checked.toString() },
+ { key: "p", value: "1" },
+ ],
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+
+
+ );
+};
diff --git a/src/app/training/search/components/controls/Cost.tsx b/src/app/training/search/components/controls/Cost.tsx
new file mode 100644
index 000000000..ed7214fe6
--- /dev/null
+++ b/src/app/training/search/components/controls/Cost.tsx
@@ -0,0 +1,35 @@
+"use client";
+import { useContext } from "react";
+import {
+ extractParam,
+ updateSearchParamsNavigate,
+} from "../../utils/filterFunctions";
+import { ResultsContext } from "../Results";
+import { FormInput } from "@components/modules/FormInput";
+import { getSearchData } from "../../utils/getSearchData";
+
+export const Cost = () => {
+ let { results, setResults } = useContext(ResultsContext);
+
+ return (
+
+ {
+ updateSearchParamsNavigate(
+ [
+ { key: "maxCost", value: e.target.value },
+ { key: "p", value: "1" },
+ ],
+
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+
+ );
+};
diff --git a/src/app/training/search/components/controls/County.tsx b/src/app/training/search/components/controls/County.tsx
new file mode 100644
index 000000000..fae3d845c
--- /dev/null
+++ b/src/app/training/search/components/controls/County.tsx
@@ -0,0 +1,39 @@
+"use client";
+import { useContext } from "react";
+import {
+ extractParam,
+ updateSearchParamsNavigate,
+} from "../../utils/filterFunctions";
+import { ResultsContext } from "../Results";
+import { counties } from "@utils/counties";
+import { getSearchData } from "../../utils/getSearchData";
+import { FormInput } from "@components/modules/FormInput";
+
+export const County = () => {
+ let { results, setResults } = useContext(ResultsContext);
+
+ return (
+
+ ({
+ key: county,
+ value: county,
+ })),
+ ]}
+ onChangeSelect={(e) => {
+ updateSearchParamsNavigate(
+ [{ key: "county", value: e.target.value }],
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+
+ );
+};
diff --git a/src/app/training/search/components/controls/Distance.tsx b/src/app/training/search/components/controls/Distance.tsx
new file mode 100644
index 000000000..14aad35c5
--- /dev/null
+++ b/src/app/training/search/components/controls/Distance.tsx
@@ -0,0 +1,100 @@
+"use client";
+import { useContext, useState } from "react";
+import {
+ extractParam,
+ removeSearchParams,
+ updateSearchParams,
+ updateSearchParamsNavigate,
+} from "../../utils/filterFunctions";
+import { ResultsContext } from "../Results";
+import { getSearchData } from "../../utils/getSearchData";
+import { FormInput } from "@components/modules/FormInput";
+import { zipCodes } from "@utils/zipCodeCoordinates";
+import { Flex } from "@components/utility/Flex";
+import { WarningCircle } from "@phosphor-icons/react";
+
+export const Distance = () => {
+ let { results, setResults } = useContext(ResultsContext);
+ const isInitialZipValid =
+ zipCodes.filter((zip) => zip === extractParam("zip", results)).length > 0
+ ? true
+ : false;
+
+ const [zipError, setZipError] = useState(!isInitialZipValid);
+ const [zipCode, setZipCode] = useState(extractParam("zip", results) || "");
+ const [attempted, setAttempted] = useState(
+ extractParam("zip", results) ? !isInitialZipValid || false : false
+ );
+
+ return (
+
+
Event a New Jersey Zip Code
+
+ {
+ if (e.target.value === "") {
+ removeSearchParams([{ key: "miles" }, { key: "zip" }]);
+ }
+ updateSearchParamsNavigate(
+ [
+ { key: "miles", value: e.target.value },
+ { key: "zip", value: zipCode || "" },
+ { key: "p", value: "1" },
+ ],
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+ from
+ {
+ updateSearchParams("zip", e.target.value);
+ setZipCode(e.target.value);
+ }}
+ onBlur={() => {
+ setAttempted(true);
+ const isValidZip =
+ zipCodes.filter((zip) => zip === zipCode).length > 0
+ ? false
+ : true;
+ setZipError(isValidZip);
+ }}
+ />
+
+ {zipError && attempted ? (
+
+
+ Please enter a 5-digit New Jersey ZIP code.
+
+ ) : undefined}
+
+ );
+};
diff --git a/src/app/training/search/components/controls/FilterForm.tsx b/src/app/training/search/components/controls/FilterForm.tsx
new file mode 100644
index 000000000..ca59e9f29
--- /dev/null
+++ b/src/app/training/search/components/controls/FilterForm.tsx
@@ -0,0 +1,27 @@
+import { useContext } from "react";
+import { updateSearchParamsNavigate } from "../../utils/filterFunctions";
+import { ResultsContext } from "../Results";
+import { getSearchData } from "../../utils/getSearchData";
+
+export const FilterForm = ({ children }: { children: React.ReactNode }) => {
+ let { setResults, searchTerm, toggle } = useContext(ResultsContext);
+ return (
+
+ );
+};
diff --git a/src/app/training/search/components/controls/Format.tsx b/src/app/training/search/components/controls/Format.tsx
new file mode 100644
index 000000000..a81946a8b
--- /dev/null
+++ b/src/app/training/search/components/controls/Format.tsx
@@ -0,0 +1,54 @@
+"use client";
+import { useContext } from "react";
+import {
+ extractParam,
+ updateSearchParamsNavigate,
+} from "../../utils/filterFunctions";
+import { ResultsContext } from "../Results";
+import { getSearchData } from "../../utils/getSearchData";
+import { FormInput } from "@components/modules/FormInput";
+
+export const Format = () => {
+ let { results, setResults } = useContext(ResultsContext);
+
+ return (
+
+
Class Format
+
{
+ updateSearchParamsNavigate(
+ [
+ { key: "inPerson", value: e.target.checked.toString() },
+ { key: "p", value: "1" },
+ ],
+
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+
+ {
+ updateSearchParamsNavigate(
+ [
+ { key: "online", value: e.target.checked.toString() },
+ { key: "p", value: "1" },
+ ],
+
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+
+ );
+};
diff --git a/src/app/training/search/components/controls/InDemand.tsx b/src/app/training/search/components/controls/InDemand.tsx
new file mode 100644
index 000000000..ce9b6e9d4
--- /dev/null
+++ b/src/app/training/search/components/controls/InDemand.tsx
@@ -0,0 +1,33 @@
+"use client";
+import { useContext } from "react";
+import {
+ extractParam,
+ updateSearchParamsNavigate,
+} from "../../utils/filterFunctions";
+import { ResultsContext } from "../Results";
+import { getSearchData } from "../../utils/getSearchData";
+import { Switch } from "@components/modules/Switch";
+
+export const InDemand = () => {
+ let { results, setResults } = useContext(ResultsContext);
+
+ return (
+
+ {
+ updateSearchParamsNavigate(
+ [
+ { key: "inDemand", value: e.target.checked.toString() },
+ { key: "p", value: "1" },
+ ],
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+
+ );
+};
diff --git a/src/app/training/search/components/controls/Language.tsx b/src/app/training/search/components/controls/Language.tsx
new file mode 100644
index 000000000..876b44854
--- /dev/null
+++ b/src/app/training/search/components/controls/Language.tsx
@@ -0,0 +1,58 @@
+"use client";
+import { useContext, useState } from "react";
+import {
+ extractParam,
+ updateSearchParamsNavigate,
+} from "../../utils/filterFunctions";
+import { ResultsContext } from "../Results";
+import { FormInput } from "@components/modules/FormInput";
+import { getSearchData } from "../../utils/getSearchData";
+import { CaretDown, CaretUp } from "@phosphor-icons/react";
+import { camelify } from "@utils/slugify";
+import { allLanguages } from "@utils/languages";
+
+export const Language = () => {
+ let { results, setResults } = useContext(ResultsContext);
+ const [showMore, setShowMore] = useState(false);
+
+ return (
+
+ );
+};
diff --git a/src/app/training/search/components/controls/Services.tsx b/src/app/training/search/components/controls/Services.tsx
new file mode 100644
index 000000000..7112b3b89
--- /dev/null
+++ b/src/app/training/search/components/controls/Services.tsx
@@ -0,0 +1,97 @@
+"use client";
+import { useContext } from "react";
+import {
+ extractParam,
+ updateSearchParamsNavigate,
+} from "../../utils/filterFunctions";
+import { ResultsContext } from "../Results";
+import { getSearchData } from "../../utils/getSearchData";
+import { Switch } from "@components/modules/Switch";
+
+export const Services = () => {
+ let { results, setResults } = useContext(ResultsContext);
+
+ return (
+
+
Filter by Provider Services
+
+
{
+ updateSearchParamsNavigate(
+ [
+ {
+ key: "isWheelchairAccessible",
+ value: e.target.checked.toString(),
+ },
+ { key: "p", value: "1" },
+ ],
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+ {
+ updateSearchParamsNavigate(
+ [
+ {
+ key: "hasChildcareAssistance",
+ value: e.target.checked.toString(),
+ },
+ ],
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+ {
+ updateSearchParamsNavigate(
+ [
+ {
+ key: "hasEveningCourses",
+ value: e.target.checked.toString(),
+ },
+ { key: "p", value: "1" },
+ ],
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+ {
+ updateSearchParamsNavigate(
+ [
+ {
+ key: "hasJobPlacementAssistance",
+ value: e.target.checked.toString(),
+ },
+ { key: "p", value: "1" },
+ ],
+ getSearchData,
+ setResults
+ );
+ }}
+ />
+
+ );
+};
diff --git a/src/app/training/search/components/controls/index.ts b/src/app/training/search/components/controls/index.ts
new file mode 100644
index 000000000..367c15733
--- /dev/null
+++ b/src/app/training/search/components/controls/index.ts
@@ -0,0 +1,11 @@
+export { CipSoc } from "./CipSoc";
+export { ClearAll } from "./ClearAll";
+export { CompletionTime } from "./CompletionTime";
+export { Cost } from "./Cost";
+export { County } from "./County";
+export { Distance } from "./Distance";
+export { FilterForm } from "./FilterForm";
+export { Format } from "./Format";
+export { InDemand } from "./InDemand";
+export { Language } from "./Language";
+export { Services } from "./Services";
diff --git a/src/app/training/search/page.tsx b/src/app/training/search/page.tsx
index 6f42aeb57..6074e9822 100644
--- a/src/app/training/search/page.tsx
+++ b/src/app/training/search/page.tsx
@@ -1,7 +1,8 @@
import globalOgImage from "@images/globalOgImage.jpeg";
import { getSearchData } from "./utils/getSearchData";
import { Results } from "./components/Results";
-
+import { SEARCH_RESULTS_PAGE_DATA as contentData } from "@data/pages/training/search";
+import { Breadcrumbs } from "@components/modules/Breadcrumbs";
export const revalidate = 0;
export const generateMetadata = async ({
@@ -47,6 +48,7 @@ export default async function SearchPage(props: {
return (
+
{
+ const q = new URL(window.location.href);
+ const sParams = q.searchParams;
+
+ keyArray.forEach((key) => {
+ sParams.delete(key.key);
+ });
+
+ window.history.pushState({}, "", `${window.location.pathname}?${sParams}`);
+};
+
+export const updateSearchParamsNavigate = async (
+ keyValueArray: { key: string; value: string }[],
+ getSearchData: (searchParams: any) => Promise,
+ setResults?: (results: any) => void
+) => {
+ const q = new URL(window.location.href);
+ const sParams = q.searchParams;
+
+ keyValueArray.forEach((keyValue) => {
+ if (
+ !keyValue.value ||
+ keyValue.value === "" ||
+ keyValue.value === "false"
+ ) {
+ sParams.delete(keyValue.key);
+ } else {
+ sParams.set(keyValue.key, keyValue.value);
+ }
+ });
+
+ window.history.pushState({}, "", `${window.location.pathname}?${sParams}`);
+
+ const searchParamObject = {
+ searchParams: Object.fromEntries(sParams.entries()),
+ };
+
+ const searchProps = await getSearchData(searchParamObject as any);
+
+ if (setResults) {
+ setResults(searchProps);
+ }
+};
+
+export const updateSearchParams = (key: string, value: string) => {
+ const q = new URL(window.location.href);
+ const sParams = q.searchParams;
+
+ if (!value || value === "" || value === "false") {
+ sParams.delete(key);
+ } else {
+ sParams.set(key, value);
+ }
+};
+
+export const extractParam = (param: string, results: any) => {
+ const q = new URLSearchParams(results.searchParams);
+ return q.get(param);
+};
diff --git a/src/app/training/search/utils/getSearchData.ts b/src/app/training/search/utils/getSearchData.ts
index f46824e02..d89c2bd6d 100644
--- a/src/app/training/search/utils/getSearchData.ts
+++ b/src/app/training/search/utils/getSearchData.ts
@@ -12,7 +12,7 @@ export async function getSearchData(props: {
const { searchParams } = props;
const searchData = await fetch(
- `${process.env.REACT_APP_API_URL}/api/trainings/search?query=${searchParams.q}`,
+ `${process.env.REACT_APP_API_URL}/api/trainings/search?query=${searchParams.q}`
);
if (!searchData.ok) {
@@ -48,8 +48,8 @@ export async function getSearchData(props: {
searchParams.mockData === "baking"
? baking
: searchParams.mockData === "digitalMarketing"
- ? digitalMarketing
- : undefined;
+ ? digitalMarketing
+ : undefined;
const pageData = mockData || searchDataItems;
@@ -65,7 +65,7 @@ export async function getSearchData(props: {
if (searchParams.miles && searchParams.zip) {
const zipCodes = getZipCodesInRadius(
searchParams.zip,
- parseInt(searchParams.miles),
+ parseInt(searchParams.miles)
);
filterObject = {
@@ -332,7 +332,9 @@ export async function getSearchData(props: {
if (searchParams.cipCode) {
const removeDecimal = searchParams.cipCode.includes(".")
- ? `${searchParams.cipCode.split(".")[0]}${searchParams.cipCode.split(".")[1]}`
+ ? `${searchParams.cipCode.split(".")[0]}${
+ searchParams.cipCode.split(".")[1]
+ }`
: searchParams.cipCode;
filterObject = {
@@ -343,7 +345,10 @@ export async function getSearchData(props: {
if (searchParams.socCode) {
if (searchParams.socCode.length === 6) {
- const socCode = `${searchParams.socCode.slice(0, 2)}-${searchParams.socCode.slice(2)}`;
+ const socCode = `${searchParams.socCode.slice(
+ 0,
+ 2
+ )}-${searchParams.socCode.slice(2)}`;
filterObject = {
...filterObject,
socCode,
@@ -402,7 +407,7 @@ export async function getSearchData(props: {
}
const pageNumber = searchParams.p ? parseInt(searchParams.p) : 1;
- const itemsPerPage = 10;
+ const itemsPerPage = searchParams.limit ? parseInt(searchParams.limit) : 10;
const start = (pageNumber - 1) * itemsPerPage;
const end = start + itemsPerPage;
diff --git a/src/app/training/search/utils/handleItemsPerPageChange.ts b/src/app/training/search/utils/handleItemsPerPageChange.ts
new file mode 100644
index 000000000..1554bfb74
--- /dev/null
+++ b/src/app/training/search/utils/handleItemsPerPageChange.ts
@@ -0,0 +1,19 @@
+export const handleItemsPerPageChange = (
+ e: React.ChangeEvent
+) => {
+ if (typeof window !== "undefined") {
+ const q = new URL(window.location.href);
+ const searchParams = q.searchParams;
+ if (e.target.value === "") {
+ searchParams.delete("limit");
+ searchParams.set("p", "1");
+ const newUrlWithItemsPerPage = searchParams.toString();
+ window.location.href = `/training/search?${newUrlWithItemsPerPage}`;
+ } else {
+ const newUrlWithItemsPerPage = new URLSearchParams(searchParams);
+ newUrlWithItemsPerPage.set("p", "1");
+ newUrlWithItemsPerPage.set("limit", e.target.value);
+ window.location.href = `/training/search?${newUrlWithItemsPerPage}`;
+ }
+ }
+};
diff --git a/src/app/training/search/utils/handleSortChange.ts b/src/app/training/search/utils/handleSortChange.ts
new file mode 100644
index 000000000..a21d08cd4
--- /dev/null
+++ b/src/app/training/search/utils/handleSortChange.ts
@@ -0,0 +1,15 @@
+export const handleSortChange = (e: React.ChangeEvent) => {
+ if (typeof window !== "undefined") {
+ const q = new URL(window.location.href);
+ const searchParams = q.searchParams;
+ if (e.target.value === "") {
+ searchParams.delete("sort");
+ const newUrlWithSort = searchParams.toString();
+ window.location.href = `/training/search?${newUrlWithSort}`;
+ } else {
+ const newUrlWithSort = new URLSearchParams(searchParams);
+ newUrlWithSort.set("sort", e.target.value);
+ window.location.href = `/training/search?${newUrlWithSort}`;
+ }
+ }
+};
diff --git a/src/components/modules/Alert.test.tsx b/src/components/modules/Alert.test.tsx
index e0dbf4a52..cfce44dcb 100644
--- a/src/components/modules/Alert.test.tsx
+++ b/src/components/modules/Alert.test.tsx
@@ -11,7 +11,7 @@ describe("Alert component", () => {
beforeEach(() => {
sessionStorage.clear();
(parseMarkdownToHTML as jest.Mock).mockReturnValue(
- "parsed markdown
",
+ "parsed markdown
"
);
});
@@ -34,13 +34,6 @@ describe("Alert component", () => {
expect(screen.getByRole("alert")).toHaveClass("custom-class");
});
- it("renders the heading if provided", () => {
- render( );
- expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
- "Test Heading",
- );
- });
-
it("renders the copy if provided", () => {
render( );
expect(screen.getByText("parsed markdown")).toBeInTheDocument();
diff --git a/src/components/modules/Alert.tsx b/src/components/modules/Alert.tsx
index 2190481e8..7270cb67a 100644
--- a/src/components/modules/Alert.tsx
+++ b/src/components/modules/Alert.tsx
@@ -4,12 +4,13 @@ import { Heading } from "./Heading";
import { parseMarkdownToHTML } from "@utils/parseMarkdownToHTML";
import { useEffect, useState } from "react";
import { IconSelector } from "./IconSelector";
+import { CaretDown, CaretUp } from "@phosphor-icons/react";
-interface AlertProps {
+export interface AlertProps {
className?: string;
copy?: string;
heading?: string;
- headingLevel?: HeadingLevel;
+ collapsable?: boolean;
noIcon?: boolean;
slim?: boolean;
alertId?: string;
@@ -21,15 +22,16 @@ export const Alert = ({
className,
copy,
heading,
- headingLevel = 3,
noIcon,
dismissible,
+ collapsable,
alertId,
slim,
type,
}: AlertProps) => {
const [remove, setRemove] = useState(false);
const [loading, setLoading] = useState(!!alertId);
+ const [show, setShow] = useState(false);
useEffect(() => {
if (!alertId) {
@@ -56,18 +58,24 @@ export const Alert = ({
slim ? " usa-alert--slim" : ""
}${loading || remove ? " hide" : ""}${
noIcon ? " usa-alert--no-icon" : ""
- }${className ? ` ${className}` : ""}`}
+ }${collapsable ? " collapsable" : ""}${className ? ` ${className}` : ""}`}
>
{heading && (
-
- {heading}
-
+
{heading}
+ )}
+ {collapsable && (
+
setShow(!show)} className="toggle">
+ {show ? : }
+ {show ? "show less" : "show more"}
+
)}
{copy && (
{
+export const Pagination = () => {
const [breakCount, setBreakCount] = useState(0);
+ const [loading, setLoading] = useState(true);
+
+ let { results } = useContext(ResultsContext);
useEffect(() => {
const breakElements = document.querySelectorAll(
@@ -80,7 +76,13 @@ export const Pagination = ({
}
}
}
- }, [currentPage, totalPages]);
+ }, [results.pageNumber, results.totalPages]);
+
+ useEffect(() => {
+ if (typeof window !== "undefined") {
+ setLoading(false);
+ }
+ }, []);
return (
- `Go to page ${page}`}
- breakLabel="..."
- forcePage={currentPage - 1}
- className="usa-pagination__list"
- nextClassName="control usa-pagination__item usa-pagination__arrow"
- nextLinkClassName="usa-pagination__link usa-pagination__next-page"
- previousClassName="control usa-pagination__item usa-pagination__arrow"
- previousLinkClassName="usa-pagination__link usa-pagination__previous-page"
- pageClassName="usa-pagination__item usa-pagination__page-no"
- pageLinkClassName="usa-pagination__button"
- breakClassName="usa-pagination__item usa-pagination__overflow"
- activeLinkClassName="usa-pagination__button usa-current"
- previousLabel={
- <>
-
- Prev
- >
- }
- nextLabel={
- <>
- Next
-
- >
- }
- pageCount={totalPages}
- renderOnZeroPageCount={null}
- pageRangeDisplayed={2}
- onPageChange={(page) => {
- const newUrl = new URL(window.location.href);
- newUrl.searchParams.set("p", `${page.selected + 1}`);
- window.location.href = newUrl.toString();
- }}
- hrefBuilder={(page) => {
- const newUrl = new URL(window.location.href);
- newUrl.searchParams.set("p", `${page}`);
- return newUrl.toString();
- }}
- />
+ {!loading && (
+ `Go to page ${page}`}
+ breakLabel="..."
+ forcePage={results.pageNumber - 1}
+ className="usa-pagination__list"
+ nextClassName="control usa-pagination__item usa-pagination__arrow"
+ nextLinkClassName="usa-pagination__link usa-pagination__next-page"
+ previousClassName="control usa-pagination__item usa-pagination__arrow"
+ previousLinkClassName="usa-pagination__link usa-pagination__previous-page"
+ pageClassName="usa-pagination__item usa-pagination__page-no"
+ pageLinkClassName="usa-pagination__button"
+ breakClassName="usa-pagination__item usa-pagination__overflow"
+ activeLinkClassName="usa-pagination__button usa-current"
+ previousLabel={
+ <>
+
+ Prev
+ >
+ }
+ nextLabel={
+ <>
+ Next
+
+ >
+ }
+ pageCount={results.totalPages}
+ renderOnZeroPageCount={null}
+ pageRangeDisplayed={2}
+ onPageChange={(page) => {
+ const newUrl = new URL(window.location.href);
+ newUrl.searchParams.set("p", `${page.selected + 1}`);
+ window.location.href = newUrl.toString();
+ }}
+ hrefBuilder={(page) => {
+ const newUrl = new URL(window.location.href);
+ newUrl.searchParams.set("p", `${page}`);
+ return newUrl.toString();
+ }}
+ />
+ )}
);
};
diff --git a/src/components/modules/Tag.tsx b/src/components/modules/Tag.tsx
index 2a9edb63f..6cdcb6813 100644
--- a/src/components/modules/Tag.tsx
+++ b/src/components/modules/Tag.tsx
@@ -1,12 +1,14 @@
import { IconNames } from "@utils/enums";
import { IconWeight, ThemeColors } from "@utils/types";
import { IconSelector } from "./IconSelector";
+import { parseMarkdownToHTML } from "@utils/parseMarkdownToHTML";
export interface TagItemProps {
chip?: boolean;
className?: string;
color: ThemeColors;
icon?: string;
+ markdown?: boolean;
iconWeight?: IconWeight;
small?: boolean;
suffixIcon?: string;
@@ -21,6 +23,7 @@ export const Tag = ({
icon,
tooltip,
iconWeight,
+ markdown,
small,
suffixIcon,
title,
@@ -40,7 +43,14 @@ export const Tag = ({
{icon && (
)}
- {title}
+ {markdown ? (
+
+ ) : (
+ <>{title}>
+ )}
+
{suffixIcon && (
div {
+ width: 100%;
+ }
+
+ &:before {
+ content: none;
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ .heading-tag {
+ font-weight: bold;
+ font-size: settings.$size-22;
+ }
}
button {
cursor: pointer;
+
+ &.toggle {
+ width: settings.$size-60;
+ height: settings.$size-60;
+ position: absolute;
+ top: 0;
+ right: 0;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+
+ &.collapsable {
+ .heading-tag {
+ width: calc(100% - settings.$size-60);
+ }
}
}
diff --git a/src/styles/blocks/_searchFilter.scss b/src/styles/blocks/_searchFilter.scss
index ec2b41de7..39393cfbb 100644
--- a/src/styles/blocks/_searchFilter.scss
+++ b/src/styles/blocks/_searchFilter.scss
@@ -1,86 +1,147 @@
-@use "../settings" as settings;
+@use "../settings" as s;
.searchFilter {
- background-color: settings.$secondary-lighter;
- display: flex;
- flex-direction: column;
- gap: settings.$size-20;
- padding: settings.$size-16 settings.$size-48;
-
- @media screen and (max-width: (settings.$tablet-md - 1)) {
- padding: settings.$size-16;
- display: none;
+ background-color: s.$secondary-lighter;
+ border-radius: s.$size-8;
+ width: 0;
+ display: block;
+ transition-duration: s.$transition-speed;
+
+ @media screen and (min-width: (s.$tablet-md)) {
+ position: sticky;
+ top: 0;
+ max-height: 100svh;
+ opacity: 0;
+ }
+
+ @media screen and (max-width: (s.$tablet-md - 1)) {
+ width: 100%;
+ display: block;
+ position: fixed;
+ left: 0;
+ max-height: 90svh;
+ z-index: 10;
+ bottom: -110svh;
}
&.open {
- @media screen and (max-width: (settings.$tablet-md - 1)) {
- display: flex;
+ bottom: 0;
+
+ @media screen and (min-width: (s.$tablet)) {
+ width: s.$size-320;
+ margin-right: s.$size-24;
+ opacity: 1;
}
- }
- form {
- display: flex;
- flex-direction: column;
- gap: settings.$size-8;
- }
+ @media screen and (max-width: (s.$tablet-md - 1)) {
+ width: 100%;
+ }
- .search {
- margin-top: 0;
- display: flex;
- flex-direction: column;
- gap: settings.$size-8;
+ .overlay {
+ @media screen and (max-width: (s.$tablet-md - 1)) {
+ pointer-events: all;
+ opacity: 1;
+ height: calc(100% - 89svh);
+ }
+ }
- &.section {
- margin-bottom: settings.$size-16;
+ .section {
+ &.search {
+ @media screen and (max-width: (s.$tablet-md - 1)) {
+ bottom: 0;
+ }
+ }
+ }
- @media screen and (max-width: (settings.$tablet-md - 1)) {
- margin-top: 0;
+ > form {
+ @media screen and (min-width: (s.$tablet-md)) {
+ opacity: 1;
+ transition-delay: s.$transition-speed / 2;
}
}
}
- input,
- select {
- border: settings.$size-1 solid settings.$base-light;
- }
+ > form {
+ white-space: nowrap;
- .usa-select {
- &:disabled,
- &[aria-disabled="true"] {
- background-color: settings.$disabled-light;
- color: settings.$disabled-dark;
- border: none;
+ @media screen and (min-width: (s.$tablet-md)) {
+ transition-duration: s.$transition-speed;
+ min-width: s.$size-320;
+ opacity: 0;
}
}
- .MuiSwitch-colorPrimary.Mui-checked {
- color: settings.$secondary;
+ .overlay {
+ transition-duration: s.$transition-speed;
+ display: block;
+ height: 100%;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ pointer-events: none;
+ opacity: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: 1;
}
- .MuiSwitch-colorPrimary.Mui-checked + .MuiSwitch-track {
- background-color: settings.$secondary;
- }
+ .controls {
+ display: flex;
+ flex-direction: column;
+ gap: s.$size-24;
+ padding: 0 s.$size-16 s.$size-16;
+ margin-top: 0;
- .MuiSwitch-root {
- margin-left: -(settings.$size-10);
- }
+ @media screen and (max-width: (s.$tablet-md - 1)) {
+ padding-bottom: s.$size-120;
+ }
- .usa-button {
- margin: 0;
+ @media screen and (min-width: (s.$tablet-md)) {
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+ height: calc(100svh - s.$size-64);
+ padding-bottom: s.$size-112;
+ }
}
- .label,
- .usa-label {
+ .heading {
font-weight: bold;
+ font-size: s.$size-20;
margin: 0;
+ background-color: s.$secondary-lighter;
+ padding: s.$size-16;
+ position: relative;
+
+ @media screen and (min-width: (s.$tablet-md)) {
+ border-radius: s.$size-8 s.$size-8 0 0;
+ }
+
+ @media screen and (max-width: (s.$tablet-md - 1)) {
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ }
+
+ button {
+ display: none;
+ cursor: pointer;
+
+ @media screen and (max-width: (s.$tablet-md - 1)) {
+ display: block;
+ position: absolute;
+ right: s.$size-16;
+ top: s.$size-16;
+ }
+ }
}
- .language {
+ .language,
+ .completion {
a {
- margin-top: settings.$size-16;
+ margin-top: s.$size-16;
display: inline-flex;
align-items: center;
- gap: settings.$size-8;
+ gap: s.$size-8;
justify-content: flex-start;
}
.items {
@@ -93,24 +154,6 @@
}
}
- .usa-checkbox__input {
- &:checked {
- + [class*="__label"] {
- &::before {
- background-color: settings.$secondary;
- box-shadow: inset 0 0 0 0.125rem settings.$secondary;
- }
- }
- }
- &:focus {
- + [class*="__label"] {
- &::before {
- outline-color: settings.$secondary;
- }
- }
- }
- }
-
.input-row {
display: flex;
align-items: center;
@@ -119,16 +162,49 @@
span:not(.formField) {
display: block;
text-align: center;
- width: settings.$size-52;
+ width: s.$size-52;
}
.formField {
- width: calc(50% - settings.$size-26);
+ width: calc(50% - s.$size-26);
}
}
-
.errorMessage {
- color: settings.$error;
- margin-top: settings.$size-8;
+ color: s.$error;
+ margin-top: s.$size-8;
+ }
+
+ .section {
+ &.search {
+ padding: s.$size-16;
+ background-color: s.$secondary-light;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition-duration: s.$transition-speed;
+ left: 0;
+ width: 100%;
+
+ @media screen and (min-width: (s.$tablet-md)) {
+ bottom: 0;
+ border-radius: 0 0 s.$size-8 s.$size-8;
+ position: absolute;
+ }
+
+ @media screen and (max-width: (s.$tablet-md - 1)) {
+ bottom: -100px;
+ position: fixed;
+ }
+
+ button {
+ width: s.$size-144;
+
+ &.apply {
+ @media screen and (min-width: (s.$tablet-md)) {
+ display: none !important;
+ }
+ }
+ }
+ }
}
}
diff --git a/src/styles/global/_footer.scss b/src/styles/global/_footer.scss
index 1e3bc0cb6..e4376d825 100644
--- a/src/styles/global/_footer.scss
+++ b/src/styles/global/_footer.scss
@@ -130,4 +130,8 @@ footer {
margin-bottom: 0;
}
}
+
+ img {
+ max-width: settings.$size-216;
+ }
}
diff --git a/src/styles/modules/_buttons.scss b/src/styles/modules/_buttons.scss
index f4703c279..1fd8cd125 100644
--- a/src/styles/modules/_buttons.scss
+++ b/src/styles/modules/_buttons.scss
@@ -25,6 +25,12 @@
text-align: left;
padding: settings.$size-16 settings.$size-24;
+ &:focus {
+ outline-offset: 0 !important;
+ outline-width: settings.$size-4 !important;
+ outline-color: settings.$primary !important;
+ }
+
&.unstyled {
&:disabled {
color: settings.$base !important;
diff --git a/src/styles/modules/_formField.scss b/src/styles/modules/_formField.scss
index e5dcc7cbc..bf85fbe86 100644
--- a/src/styles/modules/_formField.scss
+++ b/src/styles/modules/_formField.scss
@@ -1,4 +1,4 @@
-@use "../settings" as settings;
+@use "../settings" as s;
.formField {
position: relative;
@@ -8,15 +8,15 @@
),
textarea,
select {
- background-color: settings.$white;
- border: settings.$size-1 solid settings.$base-light;
- border-radius: settings.$size-4;
- min-height: settings.$size-48;
- padding: settings.$size-12 settings.$size-8;
+ background-color: s.$white;
+ border: s.$size-1 solid s.$base-light;
+ border-radius: s.$size-4;
+ min-height: s.$size-48;
+ padding: s.$size-12 s.$size-8;
text-align: left;
&::placeholder {
- color: settings.$base-light;
+ color: s.$base-light;
}
}
select {
@@ -29,7 +29,7 @@
),
textarea,
select {
- border-color: settings.$error;
+ border-color: s.$error;
}
span {
@@ -50,33 +50,33 @@
&.textarea {
textarea {
- margin-top: settings.$size-4;
- min-height: settings.$size-96;
+ margin-top: s.$size-4;
+ min-height: s.$size-96;
}
}
&.radio {
&.error {
label:before {
- box-shadow: 0 0 0 settings.$size-2 settings.$error;
+ box-shadow: 0 0 0 s.$size-2 s.$error;
}
span {
&.error {
- margin-top: settings.$size-20;
+ margin-top: s.$size-20;
}
}
}
}
.error {
- color: settings.$error;
+ color: s.$error;
display: flex;
min-height: none;
flex-direction: row;
align-items: flex-start;
- gap: settings.$size-4;
- margin-top: settings.$size-4;
+ gap: s.$size-4;
+ margin-top: s.$size-4;
svg {
position: relative;
@@ -90,14 +90,14 @@
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not(
[type="submit"]
) {
- padding-left: settings.$size-52;
+ padding-left: s.$size-52;
}
svg {
&.icon {
position: absolute;
- bottom: settings.$size-12;
- left: settings.$size-16;
+ bottom: s.$size-12;
+ left: s.$size-16;
width: auto;
}
}
@@ -105,7 +105,63 @@
&.error {
svg {
&.icon {
- bottom: settings.$size-32 + settings.$size-4;
+ bottom: s.$size-32 + s.$size-4;
+ }
+ }
+ }
+ }
+
+ .switch & {
+ &.checkbox {
+ label {
+ padding: 0;
+ font-weight: bold;
+
+ &:before {
+ content: none;
+ }
+ }
+
+ .toggle {
+ background-color: s.$disabled-dark;
+ width: s.$size-52;
+ height: s.$size-28;
+ cursor: pointer;
+ display: block;
+ border: s.$size-1 solid s.$base;
+ border-radius: s.$size-32;
+ position: relative;
+ transition-duration: s.$transition-speed;
+
+ .dot {
+ background-color: s.$base;
+ width: s.$size-20;
+ height: s.$size-20;
+ display: block;
+ border-radius: 100%;
+ position: absolute;
+ top: 50%;
+ left: s.$size-4;
+ transition-duration: s.$transition-speed;
+ transform: translateY(-50%);
+ }
+ }
+ }
+ }
+
+ .switch.checked & {
+ &.checkbox {
+ label {
+ &:before {
+ content: none;
+ }
+ }
+
+ .toggle {
+ background-color: s.$secondary;
+ .dot {
+ left: calc(100% - s.$size-24);
+ background-color: s.$white;
}
}
}
diff --git a/src/styles/modules/_resultCard.scss b/src/styles/modules/_resultCard.scss
index 5939afc12..42bcb7ff4 100644
--- a/src/styles/modules/_resultCard.scss
+++ b/src/styles/modules/_resultCard.scss
@@ -1,20 +1,21 @@
-@use "../settings" as settings;
+@use "../settings" as s;
.resultCard {
- border-radius: settings.$border-radius-5;
- border: settings.$size-1 solid settings.$base-lighter;
- padding: settings.$size-24;
+ border-radius: s.$border-radius-5;
+ border: s.$size-1 solid s.$base-lighter;
+ padding: s.$size-24;
display: flex;
- gap: settings.$size-14;
+ gap: s.$size-14;
flex-direction: column;
- color: settings.$ink;
- transition-duration: settings.$transition-speed;
+ background-color: s.$white;
+ color: s.$ink;
+ transition-duration: s.$transition-speed;
transition-timing-function: ease-in-out;
&:hover {
- @media screen and (min-width: (settings.$tablet)) {
- box-shadow: settings.$shadow-3;
- color: settings.$ink;
+ @media screen and (min-width: (s.$tablet)) {
+ box-shadow: s.$shadow-3;
+ color: s.$ink;
}
}
@@ -31,15 +32,15 @@
.footing {
display: flex;
- @media screen and (min-width: (settings.$mobile-md)) {
+ @media screen and (min-width: (s.$mobile-md)) {
align-items: center;
justify-content: space-between;
}
- @media screen and (max-width: (settings.$mobile-md - 1)) {
+ @media screen and (max-width: (s.$mobile-md - 1)) {
flex-direction: column;
align-items: flex-start;
- gap: settings.$size-16;
+ gap: s.$size-16;
}
.formField {
@@ -50,7 +51,7 @@
}
.footing {
- margin-top: settings.$size-16;
+ margin-top: s.$size-16;
&.noLabel {
justify-content: flex-end;
@@ -60,17 +61,17 @@
.heading {
flex-direction: row;
justify-content: space-between;
- gap: settings.$size-14;
+ gap: s.$size-14;
.heading-tag {
- color: settings.$primary-vivid;
+ color: s.$primary-vivid;
margin: 0;
}
}
.footing {
.usa-checkbox {
- @media screen and (max-width: (settings.$tablet - 1)) {
+ @media screen and (max-width: (s.$tablet - 1)) {
justify-content: flex-end;
display: flex;
}
@@ -79,11 +80,11 @@
.heading {
.heading-tag {
- font-size: settings.$size-18;
+ font-size: s.$size-18;
}
> span {
- font-size: settings.$size-20;
+ font-size: s.$size-20;
font-weight: bold;
}
}
@@ -91,12 +92,12 @@
.information {
display: flex;
flex-direction: column;
- gap: settings.$size-8;
+ gap: s.$size-8;
p {
display: flex;
align-items: center;
- gap: settings.$size-8;
+ gap: s.$size-8;
&.cipCode {
display: block;
@@ -104,16 +105,16 @@
b {
display: block;
width: 100%;
- padding-left: settings.$size-24;
+ padding-left: s.$size-24;
}
}
svg {
- width: settings.$size-16;
+ width: s.$size-16;
}
span {
- width: calc(100% - settings.$size-16);
+ width: calc(100% - s.$size-16);
}
&.description {
diff --git a/src/styles/modules/_tag.scss b/src/styles/modules/_tag.scss
index 188856bdf..a0bbbf6ec 100644
--- a/src/styles/modules/_tag.scss
+++ b/src/styles/modules/_tag.scss
@@ -11,9 +11,13 @@
&.small {
padding: settings.$size-2 settings.$size-8;
- font-size: settings.$size-12;
+ font-size: settings.$size-14;
line-height: 1;
+ * {
+ font-size: settings.$size-14;
+ }
+
svg {
width: settings.$size-14;
height: settings.$size-14;
diff --git a/src/styles/pages/_search.scss b/src/styles/pages/_search.scss
index a5158048a..f74c7e114 100644
--- a/src/styles/pages/_search.scss
+++ b/src/styles/pages/_search.scss
@@ -1,80 +1,146 @@
-@use "../settings" as settings;
+@use "../settings" as s;
.search {
- margin-top: settings.$size-24;
+ margin-top: s.$size-24;
.inner {
display: flex;
align-items: flex-start;
- gap: settings.$size-32;
- padding: 0 0 settings.$size-48;
+ padding: 0 0 s.$size-48;
- @media screen and (max-width: (settings.$tablet-md - 1)) {
+ @media screen and (max-width: (s.$tablet-md - 1)) {
flex-direction: column;
}
}
.resultsHeader {
display: flex;
- justify-content: space-between;
- width: 100%;
- align-items: flex-end;
- margin-bottom: settings.$size-16;
- gap: settings.$size-16;
-
- @media screen and (max-width: (settings.$mobile-lg - 1)) {
- align-items: flex-start;
- flex-direction: column;
- gap: 0;
- margin-top: settings.$size-16;
- }
-
- .heading-tag {
- @media screen and (max-width: (settings.$tablet-md - 1)) {
- font-size: settings.$size-22;
+ flex-direction: column;
+ gap: s.$size-16;
+ margin-bottom: s.$size-16;
+
+ .headingContainer {
+ display: flex;
+ justify-content: space-between;
+ @media screen and (max-width: (s.$tablet-md - 1)) {
+ flex-direction: column;
+ gap: s.$size-16;
}
- @media screen and (max-width: (settings.$mobile-lg - 1)) {
- font-size: settings.$size-16;
+ .heading-tag,
+ .searchFormContainer {
+ width: 100%;
}
- }
- .resultsCount {
- margin: settings.$size-30 0 0 0;
- }
+ .heading-tag {
+ margin: 0;
- .sortBy {
- width: 100%;
- @media screen and (min-width: (settings.$mobile-lg)) {
- width: settings.$size-200;
+ @media screen and (max-width: (s.$tablet-xl - 1)) {
+ font-size: s.$size-24;
+ }
+ }
+
+ .searchFormContainer {
+ @media screen and (min-width: (s.$tablet-md)) {
+ max-width: s.$size-411;
+ }
+
+ @media screen and (max-width: (s.$tablet-md - 1)) {
+ display: flex;
+ gap: s.$size-16;
+ align-items: center;
+ }
+
+ .searchForm {
+ flex-grow: 1;
+
+ @media screen and (max-width: (s.$tablet-md - 1)) {
+ width: calc(100% - s.$size-60);
+
+ input {
+ min-width: auto;
+ }
+ }
+ }
+
+ button {
+ margin: 0;
+ padding: 0;
+ }
}
- label {
- font-size: settings.$size-12;
- margin-left: settings.$size-8;
- margin-top: 0;
- display: inline-block;
+ .editSearchToggle {
+ padding: 0;
+ width: calc(s.$size-40 - s.$size-2);
+ height: calc(s.$size-40 - s.$size-2);
+ justify-content: center;
+ outline-color: s.$disabled-dark;
+ color: s.$disabled-dark;
position: relative;
- bottom: -(settings.$size-10);
- user-select: none;
- z-index: 1;
- pointer-events: none;
- padding: 0 settings.$size-4;
- background-color: settings.$white;
+
+ @media screen and (min-width: (s.$tablet-md)) {
+ display: none !important;
+ }
+
+ .filterCount {
+ display: none;
+
+ @media screen and (max-width: (s.$tablet-md - 1)) {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ z-index: 1;
+ background-color: s.$primary;
+ color: s.$white;
+ font-size: s.$size-12;
+ width: s.$size-20;
+ height: s.$size-20;
+ border-radius: 100%;
+ top: -(s.$size-8);
+ right: -(s.$size-8);
+ }
+ }
}
+ }
+
+ .formField {
+ margin: 0;
+ width: 100%;
+ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not(
+ [type="submit"]
+ ),
select {
- margin: 0;
+ max-width: none;
+ width: 100%;
+ padding: s.$size-8;
+ border-width: s.$size-2;
+ min-height: auto;
}
}
- }
- aside {
- width: 100%;
+ .resultsHeaderControls {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+
+ .sortBy {
+ display: flex;
+ gap: s.$size-16;
+ width: 100%;
+
+ @media screen and (min-width: (s.$tablet-md)) {
+ max-width: s.$size-411;
+ }
- @media screen and (min-width: (settings.$tablet-md)) {
- width: settings.$size-400;
+ .formField {
+ width: 100%;
+ }
+ }
}
+ }
+ aside {
input,
select {
max-width: none;
@@ -84,11 +150,16 @@
.results {
display: flex;
flex-direction: column;
- gap: settings.$size-16;
+ gap: s.$size-16;
width: 100%;
- @media screen and (min-width: (settings.$tablet-md)) {
- width: calc(100% - settings.$size-400);
+ @media screen and (min-width: (s.$tablet-md)) {
+ transition-duration: s.$transition-speed;
+ width: calc(100% - (s.$size-320));
+
+ &.wide {
+ width: 100%;
+ }
}
.spinner {
@@ -96,12 +167,25 @@
}
}
- .editSearch {
+ .usa-button.primary.usa-button--outline.editSearch {
justify-content: center;
- padding-left: settings.$size-8;
- padding-right: settings.$size-8;
- @media screen and (min-width: (settings.$tablet-md)) {
+
+ @media screen and (max-width: (s.$tablet-md - 1)) {
display: none !important;
}
}
+
+ .param-tags {
+ margin: s.$size-16 0;
+
+ @media screen and (max-width: (s.$tablet-md - 1)) {
+ display: none;
+ }
+
+ a {
+ font-weight: bold;
+ color: s.$error;
+ text-decoration: none;
+ }
+ }
}