Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add hook to deal with filtering logic #169

Merged
merged 6 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions frontend/src/app/[organisation]/bemanning/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import StaffingSidebar from "@/components/StaffingSidebar";
import FilteredConsultantsList from "@/components/FilteredConsultantsList";
import { fetchWithToken } from "@/data/fetchWithToken";
import { Consultant } from "@/types";
import { Consultant, Department } from "@/types";
import { ConsultantFilterProvider } from "@/components/FilteredConsultantProvider";

export default async function Bemanning({
params,
Expand All @@ -13,14 +14,24 @@ export default async function Bemanning({
`${params.organisation}/consultants`,
)) ?? [];

const departments =
(await fetchWithToken<Department[]>(
`organisations/${params.organisation}/departments`,
)) ?? [];

return (
<div className="flex flex-row">
<StaffingSidebar />
<ConsultantFilterProvider
consultants={consultants}
departments={departments}
>
<div className="flex flex-row">
<StaffingSidebar />

<div className="p-6 w-full">
<h1>Konsulenter</h1>
<FilteredConsultantsList consultants={consultants} />{" "}
<div className="p-6 w-full">
<h1>Konsulenter</h1>
<FilteredConsultantsList />{" "}
</div>
</div>
</div>
</ConsultantFilterProvider>
);
}
25 changes: 11 additions & 14 deletions frontend/src/components/ActiveFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
"use client";
import { useSearchParams } from "next/navigation";
import { Filter } from "react-feather";
import { useFilteredConsultants } from "@/hooks/useFilteredConsultants";

export default function ActiveFilters() {
const searchParams = useSearchParams();
const { filteredDepartments, currentNameSearch } = useFilteredConsultants();
const filterTextComponents: string[] = [];

const currentNameSearch =
searchParams.get("search") != ""
? `"` + searchParams.get("search") + `"`
: "";
const filteredDepartments =
searchParams.get("filter") != ""
? searchParams.get("filter")?.replace(",", ", ")
: "";
const filterSummaryText =
filteredDepartments != "" && currentNameSearch != ""
? [filteredDepartments, currentNameSearch].join(", ").replace(/,^/, "")
: filteredDepartments + currentNameSearch;
if (filteredDepartments.length > 0)
filterTextComponents.push(
filteredDepartments.map((d) => d.name).join(", "),
);
if (currentNameSearch != "")
filterTextComponents.push(`"${currentNameSearch}"`);

const filterSummaryText = filterTextComponents.join(" ");

return (
<>
Expand Down
37 changes: 9 additions & 28 deletions frontend/src/components/DepartmentFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,10 @@
"use client";
import FilterButton from "./FilterButton";
import { Department } from "@/types";
import { useEffect, useState } from "react";
import { usePathname } from "next/navigation";

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);
}
}
import { useFilteredConsultants } from "@/hooks/useFilteredConsultants";

export default function DepartmentFilter() {
const [departments, setDepartments] = useState<Department[]>([]);
const pathName = usePathname();

useEffect(() => {
getDepartments(setDepartments, pathName);
}, [pathName]);
const { departments, filteredDepartments, toggleDepartmentFilter } =
useFilteredConsultants();

if (departments.length > 0) {
return (
Expand All @@ -36,9 +14,12 @@ export default function DepartmentFilter() {
<div className="flex flex-col gap-2 w-52">
{departments?.map((department, index) => (
<FilterButton
key={index}
filterName={department.name}
hotKey={index + 1}
key={department.id}
label={department.name}
onClick={() => toggleDepartmentFilter(department)}
checked={filteredDepartments
.map((d) => d.id)
.includes(department.id)}
/>
))}
</div>
Expand Down
99 changes: 14 additions & 85 deletions frontend/src/components/FilterButton.tsx
Original file line number Diff line number Diff line change
@@ -1,95 +1,24 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";

export default function FilterButton({
filterName,
hotKey,
label,
onClick,
checked,
enabled,
}: {
filterName: string;
hotKey?: number;
label: string;
onClick: () => void;
checked: boolean;
enabled?: boolean;
}) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [isButtonActive, setIsButtonActive] = useState(checkFilterInUrl);
const [checkboxIsDisabled, setCheckboxIsDisabled] = useState(false);

const handleFilterClick = useCallback(() => {
setIsButtonActive((prevState) => !prevState);

const currentSearch = searchParams.get("search");
const currentFilter = searchParams.get("filter") || "";
const filters = currentFilter.split(",");
const filterIndex = filters.indexOf(filterName);
const newFilters = [...filters];
if (filterIndex === -1) {
newFilters.push(filterName);
} else {
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);
}
enabled = enabled ?? true;

return (
<div
className="flex items-center"
onClick={() => {
setCheckboxIsDisabled(true);
handleFilterClick();
setCheckboxTimeout();
}}
>
<div className="flex items-center" onClick={onClick}>
<input
id="checkbox"
type="checkbox"
className="appearance-none border flex items-center border-primary_default m-[1px] mr-2 h-4 w-4 rounded-sm hover:bg-primary_l2 hover:border-primary_l2 checked:bg-primary_default"
checked={isButtonActive}
disabled={checkboxIsDisabled}
checked={checked}
disabled={!enabled}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -98,7 +27,7 @@ export default function FilterButton({
viewBox="0 0 12 12"
fill="none"
className={`absolute ml-[3px] pointer-events-none ${
!isButtonActive && "hidden"
!checked && "hidden"
}`}
>
<path
Expand All @@ -108,7 +37,7 @@ export default function FilterButton({
fill="white"
/>
</svg>
<label className="body">{filterName}</label>
<label className="body">{label}</label>
</div>
);
}
28 changes: 28 additions & 0 deletions frontend/src/components/FilteredConsultantProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import { Consultant, Department } from "@/types";
import React, { createContext, ReactNode } from "react";

type FilterContextType = {
consultants: Consultant[];
departments: Department[];
};

export const FilteredContext = createContext<FilterContextType>({
consultants: [],
departments: [],
});

export function ConsultantFilterProvider(props: {
consultants: Consultant[];
departments: Department[];
children: ReactNode;
}) {
return (
<FilteredContext.Provider
value={{ consultants: props.consultants, departments: props.departments }}
>
{props.children}
</FilteredContext.Provider>
);
}
38 changes: 5 additions & 33 deletions frontend/src/components/FilteredConsultantsList.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,10 @@
"use client";
import ConsultantListElement from "./ConsultantListElement";
import { Consultant } from "@/types";
import { useSearchParams } from "next/navigation";
import ActiveFilters from "./ActiveFilters";
import { useFilteredConsultants } from "@/hooks/useFilteredConsultants";

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(`(?<!\\p{L})${search}.*\\b`, "giu")),
);
}
if (filter && filter.length > 0) {
newFilteredConsultants = newFilteredConsultants?.filter((consultant) =>
filter.toLowerCase().includes(consultant.department.toLowerCase()),
);
}
return newFilteredConsultants;
}

export default function FilteredConsultantList({
consultants,
}: {
consultants: Consultant[];
}) {
const searchParams = useSearchParams();
const search = searchParams.get("search") || "";
const filter = searchParams.get("filter") || "";
const filteredConsultants = filterConsultants(search, filter, consultants);

export default function FilteredConsultantList() {
const { filteredConsultants: consultants } = useFilteredConsultants();
return (
<div>
<div className="my-6 min-h-[56px]">
Expand All @@ -42,10 +14,10 @@ export default function FilteredConsultantList({
<p className="body-large-bold ">Konsulentliste</p>

<div className="rounded-full bg-black bg-opacity-5 px-2 py-1">
<p className="text-black body-small">{filteredConsultants?.length}</p>
<p className="text-black body-small">{consultants?.length}</p>
</div>
</div>
{filteredConsultants?.map((consultant) => (
{consultants?.map((consultant) => (
<ConsultantListElement key={consultant.id} consultant={consultant} />
))}
</div>
Expand Down
23 changes: 6 additions & 17 deletions frontend/src/components/SearchBarComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { Search } from "react-feather";
import { useFilteredConsultants } from "@/hooks/useFilteredConsultants";

export default function SearchBarComponent() {
const { currentNameSearch, setNameSearch } = useFilteredConsultants();
const inputRef = useRef<HTMLInputElement>(null);

const searchParams = useSearchParams();
const currentFilter = searchParams.get("filter") || "";
const pathname = usePathname();
const [searchText, setSearchText] = useState(
searchParams.get("search") || "",
);
const router = useRouter();

useEffect(() => {
router.push(`${pathname}?search=${searchText}&filter=${currentFilter}`);
}, [searchText, router, pathname, currentFilter]);

useEffect(() => {
function keyDownHandler(e: { code: string }) {
if (
Expand All @@ -28,7 +17,7 @@ export default function SearchBarComponent() {
inputRef.current.focus();
}
if (e.code.includes("Escape")) {
setSearchText("");
setNameSearch("");
}
if (e.code.startsWith("Digit")) {
inputRef.current?.blur();
Expand All @@ -40,7 +29,7 @@ export default function SearchBarComponent() {
return () => {
document.removeEventListener("keydown", keyDownHandler);
};
}, []);
}, [setNameSearch]);

return (
<div className="flex flex-col gap-2">
Expand All @@ -50,9 +39,9 @@ export default function SearchBarComponent() {
<input
placeholder="Søk etter konsulent"
className="input w-[131px] focus:outline-none body-small "
onChange={(e) => setSearchText(e.target.value)}
onChange={(e) => setNameSearch(e.target.value)}
ref={inputRef}
value={searchText}
value={currentNameSearch}
></input>
</div>
</div>
Expand Down
Loading
Loading