From 0f818e42dd4bed4a8acc1fb3903a50160abce29f Mon Sep 17 00:00:00 2001 From: Sigrid <55406589+sigtheidiot@users.noreply.github.com> Date: Fri, 20 Oct 2023 10:12:15 +0200 Subject: [PATCH] Show and remove filter, change design (#156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mathilde Haukø Haugum --- .../bemanning/api/departments/route.ts | 15 +++ .../app/[organisation]/bemanning/layout.tsx | 15 +-- .../src/app/[organisation]/bemanning/page.tsx | 11 ++- frontend/src/components/ActiveFilters.tsx | 31 ++++++ frontend/src/components/DepartmentFilter.tsx | 40 ++++++-- frontend/src/components/FilterButton.tsx | 98 ++++++++++++++++--- .../components/FilteredConsultantsList.tsx | 47 +++++---- .../src/components/SearchBarComponent.tsx | 18 ++-- frontend/src/components/StaffingSidebar.tsx | 39 ++++++++ frontend/tailwind.config.js | 1 + 10 files changed, 250 insertions(+), 65 deletions(-) create mode 100644 frontend/src/app/[organisation]/bemanning/api/departments/route.ts create mode 100644 frontend/src/components/ActiveFilters.tsx create mode 100644 frontend/src/components/StaffingSidebar.tsx diff --git a/frontend/src/app/[organisation]/bemanning/api/departments/route.ts b/frontend/src/app/[organisation]/bemanning/api/departments/route.ts new file mode 100644 index 00000000..06c5bfc7 --- /dev/null +++ b/frontend/src/app/[organisation]/bemanning/api/departments/route.ts @@ -0,0 +1,15 @@ +import { fetchWithToken } from "@/data/fetchWithToken"; +import { Department } from "@/types"; +import { NextResponse } from "next/server"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const organisationName = searchParams.get("organisationName") || ""; + + const departments = + (await fetchWithToken( + `organisations/${organisationName}/departments`, + )) ?? []; + + return NextResponse.json(departments); +} diff --git a/frontend/src/app/[organisation]/bemanning/layout.tsx b/frontend/src/app/[organisation]/bemanning/layout.tsx index e84dc83b..ecebf567 100644 --- a/frontend/src/app/[organisation]/bemanning/layout.tsx +++ b/frontend/src/app/[organisation]/bemanning/layout.tsx @@ -1,20 +1,7 @@ -import DepartmentFilter from "@/components/DepartmentFilter"; -import SearchBarComponent from "@/components/SearchBarComponent"; - export default function BemanningLayout({ children, }: { children: React.ReactNode; }) { - return ( -
-
-

Filter

- - -
- -
{children}
-
- ); + return children; } diff --git a/frontend/src/app/[organisation]/bemanning/page.tsx b/frontend/src/app/[organisation]/bemanning/page.tsx index 9939cc9b..3ac00157 100644 --- a/frontend/src/app/[organisation]/bemanning/page.tsx +++ b/frontend/src/app/[organisation]/bemanning/page.tsx @@ -1,3 +1,4 @@ +import StaffingSidebar from "@/components/StaffingSidebar"; import FilteredConsultantsList from "@/components/FilteredConsultantsList"; import { fetchWithToken } from "@/data/fetchWithToken"; import { Consultant } from "@/types"; @@ -13,9 +14,13 @@ export default async function Bemanning({ )) ?? []; return ( -
-

Konsulenter

- +
+ + +
+

Konsulenter

+ {" "} +
); } diff --git a/frontend/src/components/ActiveFilters.tsx b/frontend/src/components/ActiveFilters.tsx new file mode 100644 index 00000000..9da82fcc --- /dev/null +++ b/frontend/src/components/ActiveFilters.tsx @@ -0,0 +1,31 @@ +"use client"; +import { useSearchParams } from "next/navigation"; +import { Filter } from "react-feather"; + +export default function ActiveFilters() { + const searchParams = useSearchParams(); + + const currentNameSearch = + searchParams.get("search") != "" + ? `"` + searchParams.get("search") + `"` + : ""; + const filteredDepartments = + searchParams.get("filter") != "" + ? searchParams.get("filter")?.split(",").join(", ") + : ""; + const filterSummaryText = + filteredDepartments != "" && currentNameSearch != "" + ? [filteredDepartments, currentNameSearch].join(", ").replace(/,^/, "") + : filteredDepartments + currentNameSearch; + + return ( + <> + {filterSummaryText != "" && ( +
+ {" "} +

{filterSummaryText}

+
+ )} + + ); +} diff --git a/frontend/src/components/DepartmentFilter.tsx b/frontend/src/components/DepartmentFilter.tsx index c2d2916f..d35c70ca 100644 --- a/frontend/src/components/DepartmentFilter.tsx +++ b/frontend/src/components/DepartmentFilter.tsx @@ -1,21 +1,45 @@ +"use client"; import FilterButton from "./FilterButton"; -import { fetchWithToken } from "@/data/fetchWithToken"; import { Department } from "@/types"; +import { useEffect, useState } from "react"; +import { usePathname } from "next/navigation"; -export default async function DepartmentFilter() { - const departments = - (await fetchWithToken( - "organisations/variant-norge/departments", - )) ?? []; +async function getDepartments(setDepartments: Function, pathName: string) { + try { + const data = await fetch( + `${pathName}/api/departments?organisationName=${pathName.split("/")[1]}`, + { + method: "get", + }, + ); + + const departments = await data.json(); + setDepartments(departments); + } catch (e) { + console.error("Error fetching departments:", e); + } +} + +export default function DepartmentFilter() { + const [departments, setDepartments] = useState([]); + const pathName = usePathname(); + + useEffect(() => { + getDepartments(setDepartments, pathName); + }, [pathName]); if (departments.length > 0) { return (

Avdelinger

-
+
{departments?.map((department, index) => ( - + ))}
diff --git a/frontend/src/components/FilterButton.tsx b/frontend/src/components/FilterButton.tsx index a6b9ab63..e1e5b2a0 100644 --- a/frontend/src/components/FilterButton.tsx +++ b/frontend/src/components/FilterButton.tsx @@ -1,14 +1,21 @@ "use client"; +import { useCallback, useEffect, useState } from "react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useState } from "react"; -export default function FilterButton({ filterName }: { filterName: string }) { +export default function FilterButton({ + filterName, + hotKey, +}: { + filterName: string; + hotKey?: number; +}) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const [isButtonActive, setIsButtonActive] = useState(checkFilterInUrl); + const [checkboxIsDisabled, setCheckboxIsDisabled] = useState(false); - function handleFilterClick() { + const handleFilterClick = useCallback(() => { setIsButtonActive((prevState) => !prevState); const currentSearch = searchParams.get("search"); @@ -22,26 +29,91 @@ export default function FilterButton({ filterName }: { filterName: string }) { newFilters.splice(filterIndex, 1); } const newFilterString = newFilters.join(",").replace(/^,/, ""); + router.push( `${pathname}?search=${currentSearch}&filter=${newFilterString}`, ); - } + }, [filterName, pathname, router, searchParams]); function checkFilterInUrl() { const currentFilter = searchParams.get("filter") || ""; return currentFilter.includes(filterName); } + const clearFilter = useCallback(() => { + setIsButtonActive(false); + const currentSearch = searchParams.get("search"); + router.push(`${pathname}?search=${currentSearch}&filter=`); + }, [pathname, router, searchParams]); + + useEffect(() => { + function keyDownHandler(e: { code: string }) { + if ( + hotKey && + e.code.startsWith("Digit") && + e.code.includes(hotKey.toString()) && + (document.activeElement?.tagName.toLowerCase() !== "input" || + document.activeElement?.id === "checkbox") + ) { + handleFilterClick(); + } + if (e.code.includes("0")) { + clearFilter(); + } + } + document.addEventListener("keydown", keyDownHandler); + + // clean up + return () => { + document.removeEventListener("keydown", keyDownHandler); + }; + }, [clearFilter, handleFilterClick, hotKey]); + + function setCheckboxTimeout() { + setTimeout(() => { + setCheckboxIsDisabled(false); + }, 200); + } + return ( - + { + setCheckboxIsDisabled(true); + handleFilterClick(); + setCheckboxTimeout(); + }} + disabled={checkboxIsDisabled} + /> + + + + +
); } diff --git a/frontend/src/components/FilteredConsultantsList.tsx b/frontend/src/components/FilteredConsultantsList.tsx index 7232d0a5..20fa36ff 100644 --- a/frontend/src/components/FilteredConsultantsList.tsx +++ b/frontend/src/components/FilteredConsultantsList.tsx @@ -1,8 +1,27 @@ "use client"; -import { useEffect, useState } from "react"; import ConsultantListElement from "./ConsultantListElement"; import { Consultant } from "@/types"; import { useSearchParams } from "next/navigation"; +import ActiveFilters from "./ActiveFilters"; + +function filterConsultants( + search: string, + filter: string, + consultants: Consultant[], +) { + var newFilteredConsultants = consultants; + if (search && search.length > 0) { + newFilteredConsultants = newFilteredConsultants?.filter((consultant) => + consultant.name.match(new RegExp(`\\b${search}.*\\b`, "gi")), + ); + } + if (filter && filter.length > 0) { + newFilteredConsultants = newFilteredConsultants?.filter((consultant) => + filter.toLowerCase().includes(consultant.department.toLowerCase()), + ); + } + return newFilteredConsultants; +} export default function FilteredConsultantList({ consultants, @@ -10,28 +29,16 @@ export default function FilteredConsultantList({ consultants: Consultant[]; }) { const searchParams = useSearchParams(); - const search = searchParams.get("search"); - const filter = searchParams.get("filter"); - const [filteredConsultants, setFilteredConsultants] = useState(consultants); - - useEffect(() => { - var newFilteredConsultants = consultants; - if (search && search.length > 0) { - newFilteredConsultants = newFilteredConsultants?.filter((consultant) => - consultant.name.match(new RegExp(`\\b${search}.*\\b`, "gi")), - ); - } - if (filter && filter.length > 0) { - newFilteredConsultants = newFilteredConsultants?.filter((consultant) => - filter.toLowerCase().includes(consultant.department.toLowerCase()), - ); - } - setFilteredConsultants(newFilteredConsultants); - }, [consultants, filter, search]); + const search = searchParams.get("search") || ""; + const filter = searchParams.get("filter") || ""; + const filteredConsultants = filterConsultants(search, filter, consultants); return (
-
+
+ +
+

Konsulentliste

diff --git a/frontend/src/components/SearchBarComponent.tsx b/frontend/src/components/SearchBarComponent.tsx index 80c485a8..fab1d0b9 100644 --- a/frontend/src/components/SearchBarComponent.tsx +++ b/frontend/src/components/SearchBarComponent.tsx @@ -4,23 +4,25 @@ import { useEffect, useRef, useState } from "react"; import { Search } from "react-feather"; export default function SearchBarComponent() { - const router = useRouter(); + const inputRef = useRef(null); + const searchParams = useSearchParams(); + const currentFilter = searchParams.get("filter") || ""; const pathname = usePathname(); const [searchText, setSearchText] = useState( searchParams.get("search") || "", ); - const inputRef = useRef(null); + const router = useRouter(); useEffect(() => { - const currentFilter = searchParams.get("filter") || ""; router.push(`${pathname}?search=${searchText}&filter=${currentFilter}`); - }, [searchText, searchParams, router, pathname]); + }, [searchText, router, pathname, currentFilter]); useEffect(() => { function keyDownHandler(e: { code: string }) { if ( (e.code.startsWith("Key") || e.code.includes("Backspace")) && + inputRef && inputRef.current ) { inputRef.current.focus(); @@ -28,6 +30,9 @@ export default function SearchBarComponent() { if (e.code.includes("Escape")) { setSearchText(""); } + if (e.code.startsWith("Digit")) { + inputRef.current?.blur(); + } } document.addEventListener("keydown", keyDownHandler); @@ -41,12 +46,11 @@ export default function SearchBarComponent() {

Søk

- + setSearchText(e.target.value)} - autoFocus ref={inputRef} value={searchText} > diff --git a/frontend/src/components/StaffingSidebar.tsx b/frontend/src/components/StaffingSidebar.tsx new file mode 100644 index 00000000..2e8bdb70 --- /dev/null +++ b/frontend/src/components/StaffingSidebar.tsx @@ -0,0 +1,39 @@ +"use client"; +import { useState } from "react"; +import DepartmentFilter from "./DepartmentFilter"; +import SearchBarComponent from "./SearchBarComponent"; +import { ArrowLeft, Filter } from "react-feather"; + +export default function StaffingSidebar() { + const [isSidebarHidden, setIsSidebarHidden] = useState(false); + + return ( +
+
+
+

Filter

+ +
+ + +
+ +
+ ); +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index d78adfef..13083152 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -7,6 +7,7 @@ module.exports = { white: "#FFFFFF", primary_default: "#423D89", primary_l1: "#A09EC4", + primary_l2: "#C6C5DC", primary_l3: "#ECECF3", primary_l4: "#F6F5F9", secondary_default: "#F076A6",