Skip to content

Commit

Permalink
44 filter variants (#127)
Browse files Browse the repository at this point in the history
* Add api call for departments

* Add new font

* Avoid search overwriting filter

* Add transparent color to tailwind

* Add department filter to list of consultants

* Use ReactQuery to fetch departments

* Use db data, remove comma at beginning of string

* fix useDepartmentsApi and add mockDepartments

---------

Co-authored-by: Mathilde Haukø Haugum <[email protected]>
  • Loading branch information
sigridge and mathildehaugum authored Oct 12, 2023
1 parent def83ef commit fa49f79
Show file tree
Hide file tree
Showing 14 changed files with 199 additions and 45 deletions.
16 changes: 16 additions & 0 deletions backend/Api/Departments/DeparmentController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Database.DatabaseContext;
using Microsoft.AspNetCore.Mvc;

[Route("/departments")]
[ApiController]
public class DepartmentController : ControllerBase {

[HttpGet]
public ActionResult<List<DepartmentReadModel>> Get(ApplicationContext applicationContext){

return applicationContext.Department.Select(d => new DepartmentReadModel(d.Id, d.Name)).ToList();

}
}

public record DepartmentReadModel(string Id, string Name);
8 changes: 8 additions & 0 deletions frontend/mockdata/mockDepartments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Department, Variant } from "@/types";

export const MockDepartments: Department[] = [
{
id: "myDepartment",
name: "My Department",
},
];
2 changes: 2 additions & 0 deletions frontend/src/app/bemanning/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import DepartmentFilter from "@/components/DepartmentFilter";
import SearchBarComponent from "@/components/SearchBarComponent";

export default function BemanningLayout({
Expand All @@ -10,6 +11,7 @@ export default function BemanningLayout({
<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>
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,10 @@
flex: 1;
background-color: transparent;
}

.interaction-chip {
font-size: 0.75rem;
font-family: "Graphik-SemiBold";
line-height: 0.875rem;
}
}
4 changes: 4 additions & 0 deletions frontend/src/auth/fetchWithToken.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { msalInstance } from "@/auth/msalInstance";
import { loginRequest } from "@/authConfig";
import { MockConsultants } from "../../mockdata/mockConsultants";
import { MockDepartments } from "../../mockdata/mockDepartments";

export async function fetchWithToken(path: string) {
if (process.env.NEXT_PUBLIC_NO_AUTH) {
Expand Down Expand Up @@ -49,4 +50,7 @@ function mockedCall(path: string) {
if (path.includes("/variants")) {
return MockConsultants;
}
if (path.includes("/departments")) {
return MockDepartments;
}
}
61 changes: 31 additions & 30 deletions frontend/src/components/AppProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"use client"
"use client";
import { EventType } from "@azure/msal-browser";
import { MsalProvider } from "@azure/msal-react";
import { CssBaseline } from "@mui/material";
Expand All @@ -7,38 +7,39 @@ import { msalInstance } from "@/auth/msalInstance";
import PageLayout from "./PageLayout";
import ThemeRegistry from "./ThemeRegistry/ThemeRegistry";


msalInstance.initialize().then(() => {
// Account selection logic is app dependent. Adjust as needed for different use cases.
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
msalInstance.setActiveAccount(accounts[0]);
}
// Account selection logic is app dependent. Adjust as needed for different use cases.
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
msalInstance.setActiveAccount(accounts[0]);
}

msalInstance.addEventCallback((event) => {
// Types are outdated: Event payload is an object with account, token, etcw
//@ts-ignore
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload.account) {
//@ts-ignore
const account = event.payload.account;
msalInstance.setActiveAccount(account);
}
});
msalInstance.addEventCallback((event) => {
// Types are outdated: Event payload is an object with account, token, etcw
//@ts-ignore
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload.account) {
//@ts-ignore
const account = event.payload.account;
msalInstance.setActiveAccount(account);
}
});
});

const queryClient = new QueryClient()

export default function AppProviders({ children }: { children: React.ReactNode }) {

return (<ThemeRegistry>
<CssBaseline />
<MsalProvider instance={msalInstance}>
<QueryClientProvider client={queryClient}>
<PageLayout>
{children}
</PageLayout>
</QueryClientProvider>
</MsalProvider>
</ThemeRegistry>)
const queryClient = new QueryClient();

export default function AppProviders({
children,
}: {
children: React.ReactNode;
}) {
return (
<ThemeRegistry>
<CssBaseline />
<MsalProvider instance={msalInstance}>
<QueryClientProvider client={queryClient}>
<PageLayout>{children}</PageLayout>
</QueryClientProvider>
</MsalProvider>
</ThemeRegistry>
);
}
27 changes: 27 additions & 0 deletions frontend/src/components/DepartmentFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";
import FilterButton from "./FilterButton";
import useDepartmentsApi from "@/hooks/useDepartmentsApi";
import { CircularProgress } from "@mui/material";

export default function DepartmentFilter() {
const { data, isLoading } = useDepartmentsApi();

if (isLoading) {
<CircularProgress />;
}

if (data) {
return (
<div>
<div className="flex flex-col gap-2">
<p className="body-small">Avdelinger</p>
<div className="flex flew-row flex-wrap gap-2">
{data?.map((department, index) => (
<FilterButton key={index} filterName={department.name} />
))}
</div>
</div>
</div>
);
}
}
44 changes: 44 additions & 0 deletions frontend/src/components/FilterButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";

export default function FilterButton({ filterName }: { filterName: string }) {
const router = useRouter();
const searchParams = useSearchParams();
const [isButtonActive, setIsButtonActive] = useState(checkFilterInUrl);

function handleFilterClick() {
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(`/bemanning?search=${currentSearch}&filter=${newFilterString}`);
}

function checkFilterInUrl() {
const currentFilter = searchParams.get("filter") || "";
return currentFilter.includes(filterName);
}

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"
}`}
>
<p className="interaction-chip">{filterName}</p>
</button>
);
}
15 changes: 10 additions & 5 deletions frontend/src/components/FilteredConsultantsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,23 @@ export default function FilteredConsultantList({
}) {
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) {
const filtered = consultants?.filter((consultant) =>
newFilteredConsultants = newFilteredConsultants?.filter((consultant) =>
consultant.name.toLowerCase().includes(search.toLowerCase()),
);
setFilteredConsultants(filtered);
} else {
setFilteredConsultants(consultants);
}
}, [search, consultants]);
if (filter && filter.length > 0) {
newFilteredConsultants = newFilteredConsultants?.filter((consultant) =>
filter.toLowerCase().includes(consultant.department.toLowerCase()),
);
}
setFilteredConsultants(newFilteredConsultants);
}, [consultants, filter, search]);

return (
<div>
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/components/SearchBarComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
"use client";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { Search } from "react-feather";

export default function SearchBarComponent() {
const router = useRouter();
const searchParams = useSearchParams();
const [searchText, setSearchText] = useState("");
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
router.push(`/bemanning?search=${searchText}`);
}, [searchText, router]);
const currentFilter = searchParams.get("filter") || "";
router.push(`/bemanning?search=${searchText}&filter=${currentFilter}`);
}, [searchText, searchParams, router]);

useEffect(() => {
function keyDownHandler(e: { code: string }) {
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/hooks/useDepartmentsApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

import { useIsAuthenticated } from "@azure/msal-react";
import { useQuery } from "react-query";
import { Department } from "@/types";
import { fetchWithToken } from "@/auth/fetchWithToken";

function useDepartmentsApi() {
const isAuthenticated =
useIsAuthenticated() || process.env.NEXT_PUBLIC_NO_AUTH;

return useQuery({
queryKey: "departments",
queryFn: async () => {
if (isAuthenticated) {
try {
const response: Department[] =
await fetchWithToken(`/api/departments`);
return response;
} catch (err) {
console.error(err);
return [];
}
}
// If not authenticated, return an empty array
return [];
},
refetchOnWindowFocus: false,
});
}

export default useDepartmentsApi;
7 changes: 1 addition & 6 deletions frontend/src/hooks/useVibesApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@
import { Variant } from "@/types";
import { fetchWithToken } from "@/auth/fetchWithToken";
import { useIsAuthenticated } from "@azure/msal-react";
import { useQuery, useQueryClient } from "react-query";
import { useEffect } from "react";
import { useQuery } from "react-query";

function useVibesApi(includeOccupied: boolean) {
const isAuthenticated =
useIsAuthenticated() || process.env.NEXT_PUBLIC_NO_AUTH;
const client = useQueryClient();

//TODO: We need a better way of handling state/cache. This works for now though, but it's a bit hacky ngl
useEffect(() => client.clear(), [includeOccupied, client]);

return useQuery({
queryKey: "vibes",
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,15 @@ export type Variant = {
];
};

export type AnchorProp = Element | (() => Element) | PopoverVirtualElement | (() => PopoverVirtualElement) | null | undefined;
export type Department = {
id: string;
name: string;
};

export type AnchorProp =
| Element
| (() => Element)
| PopoverVirtualElement
| (() => PopoverVirtualElement)
| null
| undefined;
1 change: 1 addition & 0 deletions frontend/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = {
primary_l4: "#F6F5F9",
secondary_default: "#F076A6",
neutral_l1: "#858585",
transparent: "transparent",
},
extend: {},
screens: {
Expand Down

0 comments on commit fa49f79

Please sign in to comment.