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

44 filter variants #127

Merged
merged 11 commits into from
Oct 12, 2023
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