Skip to content

Commit

Permalink
Add filters
Browse files Browse the repository at this point in the history
  • Loading branch information
jkettmann committed Dec 8, 2022
1 parent 8ae036c commit 93ae42f
Show file tree
Hide file tree
Showing 10 changed files with 425 additions and 0 deletions.
6 changes: 6 additions & 0 deletions api/issues.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export enum IssueStatus {
open = "open",
resolved = "resolved",
}

export enum IssueLevel {
info = "info",
warning = "warning",
Expand All @@ -11,5 +16,6 @@ export type Issue = {
message: string;
stack: string;
level: IssueLevel;
status: IssueStatus;
numEvents: number;
};
30 changes: 30 additions & 0 deletions features/issues/components/filters/filters.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import styled from "styled-components";
import { breakpoint } from "@styles/theme";

export const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
margin-block: 1rem;
width: 100%;
@media (min-width: ${breakpoint("desktop")}) {
flex-direction: row;
justify-content: flex-end;
order: initial;
gap: 1rem;
flex-wrap: wrap;
}
`;

export const RightContainer = styled.div`
margin-bottom: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
order: -1;
@media (min-width: ${breakpoint("desktop")}) {
flex-direction: row;
gap: 3rem;
order: initial;
}
`;
162 changes: 162 additions & 0 deletions features/issues/components/filters/filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React, {
useState,
useEffect,
useCallback,
useRef,
useContext,
} from "react";
import { useRouter } from "next/router";
import { useWindowSize } from "react-use";
import { Select, Option, Input } from "@features/ui";
import { NavigationContext } from "@features/layout";
import { useFilters } from "../../hooks/use-filters";
import { IssueLevel, IssueStatus } from "@api/issues.types";
import { useProjects } from "@features/projects";
import * as S from "./filters.styled";

export function Filters() {
const { handleFilters, filters } = useFilters();
const { data: projects } = useProjects();
const router = useRouter();
const routerQueryProjectName =
(router.query.projectName as string)?.toLowerCase() || undefined;
const [inputValue, setInputValue] = useState<string>("");
const projectNames = projects?.map((project) => project.name.toLowerCase());
const isFirst = useRef(true);
const { width } = useWindowSize();
const isMobileScreen = width <= 1023;
const { isMobileMenuOpen } = useContext(NavigationContext);
const handleChange = (input: string) => {
setInputValue(input);

if (inputValue?.length < 2) {
handleProjectName(undefined);
return;
}

const name = projectNames?.find((name) =>
name?.toLowerCase().includes(inputValue.toLowerCase())
);

if (name) {
handleProjectName(name);
}
};

const handleLevel = (level?: string) => {
if (level) {
level = level.toLowerCase();
}
handleFilters({ level: level as IssueLevel });
};

const handleStatus = (status?: string) => {
if (status === "Unresolved") {
status = "open";
}
if (status) {
status = status.toLowerCase();
}
handleFilters({ status: status as IssueStatus });
};

const handleProjectName = useCallback(
(projectName?: string) =>
handleFilters({ project: projectName?.toLowerCase() }),
[handleFilters]
);

useEffect(() => {
const newObj: { [key: string]: string } = {
...filters,
};

Object.keys(newObj).forEach((key) => {
if (newObj[key] === undefined) {
delete newObj[key];
}
});

const url = {
pathname: router.pathname,
query: {
page: router.query.page || 1,
...newObj,
},
};

if (routerQueryProjectName && isFirst) {
handleProjectName(routerQueryProjectName);
setInputValue(routerQueryProjectName || "");
isFirst.current = false;
}

router.push(url, undefined, { shallow: false });
}, [filters.level, filters.status, filters.project, router.query.page]);

return (
<S.Container>
<Select
placeholder="Status"
defaultValue="Status"
width={isMobileScreen ? "97%" : "8rem"}
data-cy="filter-by-status"
style={{
...(isMobileMenuOpen && {
opacity: 0,
}),
}}
>
<Option value={undefined} handleCallback={handleStatus}>
--None--
</Option>
<Option value="Unresolved" handleCallback={handleStatus}>
Unresolved
</Option>
<Option value="Resolved" handleCallback={handleStatus}>
Resolved
</Option>
</Select>

<Select
placeholder="Level"
defaultValue="Level"
width={isMobileScreen ? "97%" : "8rem"}
data-cy="filter-by-level"
style={{
...(isMobileMenuOpen && {
opacity: 0,
}),
}}
>
<Option value={undefined} handleCallback={handleLevel}>
--None--
</Option>
<Option value="Error" handleCallback={handleLevel}>
Error
</Option>
<Option value="Warning" handleCallback={handleLevel}>
Warning
</Option>
<Option value="Info" handleCallback={handleLevel}>
Info
</Option>
</Select>

<Input
handleChange={handleChange}
value={inputValue}
label="project name"
placeholder="Project Name"
iconSrc="/icons/search-icon.svg"
data-cy="filter-by-project"
style={{
...(isMobileScreen && { width: "94%", marginRight: "3rem" }),
...(isMobileMenuOpen && {
opacity: 0,
}),
}}
/>
</S.Container>
);
}
52 changes: 52 additions & 0 deletions features/issues/context/filters-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { IssueLevel, IssueStatus } from "@api/issues.types";
import React, {
useState,
useMemo,
useCallback,
createContext,
ReactNode,
} from "react";

type IssueFilters = {
level?: IssueLevel;
status?: IssueStatus;
project?: string;
};

export const FiltersContext = createContext<{
filters: IssueFilters;
handleFilters: (filter: IssueFilters) => unknown;
}>({
filters: { status: undefined, level: undefined, project: undefined },
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
handleFilters: (_filter: IssueFilters) => {},
});

type FiltersProviderProps = {
children: ReactNode | ReactNode[];
};

export function FiltersProvider({ children }: FiltersProviderProps) {
const [filters, setFilters] = useState<IssueFilters>({
status: undefined,
level: undefined,
project: undefined,
});

const handleFilters = useCallback(
(filter: any) =>
setFilters((prevFilters) => ({ ...prevFilters, ...filter })),
[]
);

const memoizedValue = useMemo(
() => ({ filters, handleFilters }),
[filters, handleFilters]
);

return (
<FiltersContext.Provider value={memoizedValue}>
{children}
</FiltersContext.Provider>
);
}
4 changes: 4 additions & 0 deletions features/issues/hooks/use-filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { useContext } from "react";
import { FiltersContext } from "../context/filters-context";

export const useFilters = () => useContext(FiltersContext);
8 changes: 8 additions & 0 deletions features/layout/sidebar-navigation/navigation-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ type NavigationContextProviderProps = {

const defaultContext = {
isSidebarCollapsed: false,
isMobileMenuOpen: false,
// eslint-disable-next-line @typescript-eslint/no-empty-function
setMobileMenuOpen: () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
toggleSidebar: () => {},
};
Expand All @@ -18,11 +21,16 @@ export function NavigationProvider({
const [isSidebarCollapsed, setSidebarCollapsed] = useState(
defaultContext.isSidebarCollapsed
);
const [isMobileMenuOpen, setMobileMenuOpen] = useState(
defaultContext.isMobileMenuOpen
);

return (
<NavigationContext.Provider
value={{
isSidebarCollapsed,
isMobileMenuOpen,
setMobileMenuOpen: () => setMobileMenuOpen((isOpen) => !isOpen),
toggleSidebar: () => setSidebarCollapsed((isCollapsed) => !isCollapsed),
}}
>
Expand Down
1 change: 1 addition & 0 deletions features/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./badge";
export * from "./button";
export * from "./select";
export * from "./input";
1 change: 1 addition & 0 deletions features/ui/input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./input";
92 changes: 92 additions & 0 deletions features/ui/input/input.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import styled, { css } from "styled-components";
import { color, textFont, space } from "@styles/theme";

export const Container = styled.div`
position: relative;
`;

export const InputContainer = styled.input<{
errorMessage: string;
error: boolean;
isIconPresent: boolean;
}>`
display: block;
border: 1px solid;
border-color: ${({ errorMessage, error }) =>
errorMessage || error ? color("error", 300) : color("gray", 300)};
border-radius: 7px;
width: calc(${space(20)} * 4 - ${space(6)});
padding: ${space(2, 3)};
letter-spacing: 0.05rem;
color: ${color("gray", 900)};
${textFont("md", "regular")};
${({ isIconPresent }) =>
isIconPresent &&
css`
padding-left: ${space(10)};
width: calc(${space(20)} * 4 - ${space(12)} - ${space(2)});
`}
::placeholder {
color: ${color("gray", 400)};
}
&:focus {
outline: 3px solid;
outline-color: ${({ errorMessage, error }) =>
errorMessage || error ? color("error", 100) : color("primary", 200)};
}
&:disable {
color: ${color("gray", 500)};
background-color: ${color("gray", 50)};
}
`;

export const InputIcon = styled.img<{
iconSrc?: string;
displayLabel?: boolean;
}>`
display: block;
height: ${space(5)};
width: ${space(5)};
margin-inline: ${space(2)};
position: absolute;
box-sizing: border-box;
top: 50%;
left: 5px;
transform: translateY(-50%);
transform: ${({ displayLabel }) => displayLabel && "translateY(12%)"};
`;

export const Label = styled.label<{
displayLabel?: boolean;
}>`
display: ${({ displayLabel }) => (displayLabel ? "block" : "none")};
margin: 0;
margin-bottom: ${space(1)};
color: ${color("gray", 700)};
${textFont("md", "regular")};
`;

export const Hint = styled.p`
margin: 0;
margin-top: ${space(1)};
color: ${color("gray", 500)};
${textFont("sm", "regular")};
letter-spacing: 0.045rem;
`;

export const ErrorMessage = styled.p`
margin: 0;
margin-top: ${space(1)};
color: ${color("error", 500)};
${textFont("sm", "regular")};
letter-spacing: 0.05rem;
`;

export const ErrorIcon = styled(InputIcon)<{
displayLabel: boolean;
}>`
height: ${space(4)};
width: ${space(4)};
left: 280px;
transform: ${({ displayLabel }) => displayLabel && "translateY(40%)"};
`;
Loading

0 comments on commit 93ae42f

Please sign in to comment.