Skip to content

Commit

Permalink
Show and remove filter, change design (#156)
Browse files Browse the repository at this point in the history
Co-authored-by: Mathilde Haukø Haugum <[email protected]>
  • Loading branch information
sigridge and mathildehaugum authored Oct 20, 2023
1 parent 230cad1 commit 0f818e4
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 65 deletions.
15 changes: 15 additions & 0 deletions frontend/src/app/[organisation]/bemanning/api/departments/route.ts
Original file line number Diff line number Diff line change
@@ -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<Department[]>(
`organisations/${organisationName}/departments`,
)) ?? [];

return NextResponse.json(departments);
}
15 changes: 1 addition & 14 deletions frontend/src/app/[organisation]/bemanning/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,7 @@
import DepartmentFilter from "@/components/DepartmentFilter";
import SearchBarComponent from "@/components/SearchBarComponent";

export default function BemanningLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex flex-row">
<div className="bg-primary_l4 py-6 px-4 flex flex-col gap-6 min-h-screen">
<h3 className="">Filter</h3>
<SearchBarComponent />
<DepartmentFilter />
</div>

<div className="p-6 w-full">{children}</div>
</div>
);
return children;
}
11 changes: 8 additions & 3 deletions frontend/src/app/[organisation]/bemanning/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import StaffingSidebar from "@/components/StaffingSidebar";
import FilteredConsultantsList from "@/components/FilteredConsultantsList";
import { fetchWithToken } from "@/data/fetchWithToken";
import { Consultant } from "@/types";
Expand All @@ -13,9 +14,13 @@ export default async function Bemanning({
)) ?? [];

return (
<div>
<h1>Konsulenter</h1>
<FilteredConsultantsList consultants={consultants} />
<div className="flex flex-row">
<StaffingSidebar />

<div className="p-6 w-full">
<h1>Konsulenter</h1>
<FilteredConsultantsList consultants={consultants} />{" "}
</div>
</div>
);
}
31 changes: 31 additions & 0 deletions frontend/src/components/ActiveFilters.tsx
Original file line number Diff line number Diff line change
@@ -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 != "" && (
<div className="flex flex-row gap-[5px] text-primary_default items-center">
<Filter size="12" />{" "}
<p className="body-small-bold"> {filterSummaryText} </p>
</div>
)}
</>
);
}
40 changes: 32 additions & 8 deletions frontend/src/components/DepartmentFilter.tsx
Original file line number Diff line number Diff line change
@@ -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<Department[]>(
"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<Department[]>([]);
const pathName = usePathname();

useEffect(() => {
getDepartments(setDepartments, pathName);
}, [pathName]);

if (departments.length > 0) {
return (
<div>
<div className="flex flex-col gap-2">
<p className="body-small">Avdelinger</p>
<div className="flex flew-row flex-wrap gap-2 w-52">
<div className="flex flex-col gap-2 w-52">
{departments?.map((department, index) => (
<FilterButton key={index} filterName={department.name} />
<FilterButton
key={index}
filterName={department.name}
hotKey={index + 1}
/>
))}
</div>
</div>
Expand Down
98 changes: 85 additions & 13 deletions frontend/src/components/FilterButton.tsx
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -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 (
<button
onClick={() => handleFilterClick()}
className={`px-3 py-2 border-2 rounded-full ${
isButtonActive
? "bg-primary_default text-white border-transparent "
: "border-primary_default border-opacity-50 text-primary_default hover:bg-primary_default hover:bg-opacity-10"
}`}
<div
className="flex items-center"
onClick={() => {
setCheckboxIsDisabled(true);
handleFilterClick();
setCheckboxTimeout();
}}
>
<p className="interaction-chip">{filterName}</p>
</button>
<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}
onChange={() => {
setCheckboxIsDisabled(true);
handleFilterClick();
setCheckboxTimeout();
}}
disabled={checkboxIsDisabled}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
className={`absolute ml-[3px] pointer-events-none ${
!isButtonActive && "hidden"
}`}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.3536 2.64645C10.5488 2.84171 10.5488 3.15829 10.3536 3.35355L4.85355 8.85355C4.65829 9.04882 4.34171 9.04882 4.14645 8.85355L1.64645 6.35355C1.45118 6.15829 1.45118 5.84171 1.64645 5.64645C1.84171 5.45118 2.15829 5.45118 2.35355 5.64645L4.5 7.79289L9.64645 2.64645C9.84171 2.45118 10.1583 2.45118 10.3536 2.64645Z"
fill="white"
/>
</svg>
<label className="body">{filterName}</label>
</div>
);
}
47 changes: 27 additions & 20 deletions frontend/src/components/FilteredConsultantsList.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,44 @@
"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,
}: {
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 (
<div>
<div className="flex flex-row gap-3 pt-16 pb-4 items-center">
<div className="my-6 min-h-[56px]">
<ActiveFilters />
</div>
<div className="flex flex-row gap-3 pb-4 items-center">
<p className="body-large-bold ">Konsulentliste</p>

<div className="rounded-full bg-black bg-opacity-5 px-2 py-1">
Expand Down
18 changes: 11 additions & 7 deletions frontend/src/components/SearchBarComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,35 @@ import { useEffect, useRef, useState } from "react";
import { Search } from "react-feather";

export default function SearchBarComponent() {
const router = useRouter();
const inputRef = useRef<HTMLInputElement>(null);

const searchParams = useSearchParams();
const currentFilter = searchParams.get("filter") || "";
const pathname = usePathname();
const [searchText, setSearchText] = useState(
searchParams.get("search") || "",
);
const inputRef = useRef<HTMLInputElement>(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();
}
if (e.code.includes("Escape")) {
setSearchText("");
}
if (e.code.startsWith("Digit")) {
inputRef.current?.blur();
}
}
document.addEventListener("keydown", keyDownHandler);

Expand All @@ -41,12 +46,11 @@ export default function SearchBarComponent() {
<div className="flex flex-col gap-2">
<p className="body-small">Søk</p>
<div className="flex flex-row gap-2 rounded-lg border border-primary_l1 px-3 py-2 w-max">
<Search className="text-primary_default" />
<Search className="text-primary_default h-4 w-4" />
<input
placeholder="Søk etter konsulent"
className="input w-36 focus:outline-none body-small "
className="input w-[131px] focus:outline-none body-small "
onChange={(e) => setSearchText(e.target.value)}
autoFocus
ref={inputRef}
value={searchText}
></input>
Expand Down
Loading

0 comments on commit 0f818e4

Please sign in to comment.