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) => (
) : (
- {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 + +

+
+ + + + + + + + + +
+ + ); }; 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]) => ( + + )); + }; + + 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 && ( - - )} - - + 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)}`)}" + + )} + +
+
{ + e.preventDefault(); + const q = new URL(window.location.href); + const searchParams = q.searchParams; + searchParams.set("q", searchTerm); + window.location.href = `/training/search?${searchParams.toString()}`; + }} + > + setSearchTerm(e.target.value)} + placeholder="Searching for training courses" + /> + + + +
+
+ +
+ - {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 ( +
+
+ ); +}; 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 ( +
+

Filter by Language

+
+ {allLanguages.map((lang, index) => ( + 3 ? "hide" : undefined} + inputId={lang} + label={lang} + defaultChecked={extractParam(camelify(lang), results) === "true"} + onChange={(e) => { + updateSearchParamsNavigate( + [ + { + key: camelify(lang), + value: e.target.checked.toString(), + }, + { key: "p", value: "1" }, + ], + getSearchData, + setResults + ); + }} + /> + ))} +
+ { + e.preventDefault(); + setShowMore(!showMore); + }} + > + {showMore ? : } + Show {showMore ? "less" : "more"} + +
+ ); +}; 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 && ( + )} {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 ( ); }; 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; + } + } }