diff --git a/features/issues/components/filters/filters.tsx b/features/issues/components/filters/filters.tsx index 07a0f26..78640a6 100644 --- a/features/issues/components/filters/filters.tsx +++ b/features/issues/components/filters/filters.tsx @@ -1,25 +1,40 @@ -import React, { - useState, - useEffect, - useCallback, - useRef, - useContext, -} from "react"; +import React, { useState } from "react"; import styled from "styled-components"; +import { useDebouncedCallback } from "use-debounce"; import { - Select, + Select as UnstyledSelect, Option, - Input, - Button, + Button as UnstyledButton, IconOptions, - NavigationContext, + Input as UnstyledInput, } from "@features/ui"; -import { useFilters, IssueLevel, IssueStatus } from "@features/issues"; -import { useProjects } from "@features/projects"; +import { useFilters } from "../../hooks"; +import { IssueLevel, IssueStatus } from "../../types/issue.types"; import { breakpoint } from "@styles/theme"; -import { useWindowSize } from "react-use"; +import { OptionType } from "@features/ui/form/select/select"; + +const statusOptions = [ + { value: undefined, text: "--None--" }, + { value: IssueStatus.open, text: "Unresolved" }, + { value: IssueStatus.resolved, text: "Resolved" }, +] as OptionType[]; + +const levelOptions = [ + { value: undefined, text: "--None--" }, + { value: IssueLevel.error, text: "Error" }, + { value: IssueLevel.warning, text: "Warning" }, + { value: IssueLevel.info, text: "Info" }, +] as OptionType[]; + +const Button = styled(UnstyledButton)` + height: 2.5rem; + min-width: 8rem; + width: 100%; -import { useRouter } from "next/router"; + @media (min-width: ${breakpoint("desktop")}) { + width: auto; + } +`; const Container = styled.div` display: flex; @@ -49,96 +64,42 @@ const RightContainer = styled.div` } `; -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(""); - const projectNames = projects?.map((project) => project.name.toLowerCase()); - const isFirst = useRef(true); - const { width } = useWindowSize(); - const isMobileScreen = width <= 1023; - const { isMobileMenuOpen } = useContext(NavigationContext); +const Select = styled(UnstyledSelect)` + width: 100%; - const handleChange = (input: string) => { - setInputValue(input); + @media (min-width: ${breakpoint("desktop")}) { + width: 8rem; + } +`; - if (inputValue?.length < 2) { - handleProjectName(undefined); - return; - } +const Input = styled(UnstyledInput)` + width: 100%; + box-sizing: border-box; +`; - const name = projectNames?.find((name) => - name?.toLowerCase().includes(inputValue.toLowerCase()) - ); +export function Filters() { + const { updateFilters, filters } = useFilters(); + const [project, setProject] = useState(filters.project); + const debouncedUpdateFilters = useDebouncedCallback(updateFilters, 300); - if (name) { - handleProjectName(name); - } + const handleChange = (input: string) => { + setProject(input); + debouncedUpdateFilters({ project: input }); }; const handleLevel = (level?: string) => { - if (level) { - level = level.toLowerCase(); - } - handleFilters({ level: level as IssueLevel }); + updateFilters({ level: level as IssueLevel }); }; const handleStatus = (status?: string) => { - if (status === "Unresolved") { - status = "open"; - } - if (status) { - status = status.toLowerCase(); - } - handleFilters({ status: status as IssueStatus }); + updateFilters({ status: status as IssueStatus }); }; - const handleProjectName = useCallback( - (projectName) => 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 ( @@ -146,64 +107,36 @@ export function Filters() { diff --git a/features/issues/context/filters-context.tsx b/features/issues/context/filters-context.tsx deleted file mode 100644 index 0fbf1cc..0000000 --- a/features/issues/context/filters-context.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { - useState, - useMemo, - useCallback, - createContext, - ReactNode, -} from "react"; -import { IssueFilters } from "@features/issues"; - -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({ - status: undefined, - level: undefined, - project: undefined, - }); - - const handleFilters = useCallback( - (filter) => setFilters((prevFilters) => ({ ...prevFilters, ...filter })), - [] - ); - - const memoizedValue = useMemo( - () => ({ filters, handleFilters }), - [filters, handleFilters] - ); - - return ( - - {children} - - ); -} diff --git a/features/issues/context/index.ts b/features/issues/context/index.ts deleted file mode 100644 index 8c4cc1d..0000000 --- a/features/issues/context/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { FiltersContext, FiltersProvider } from "./filters-context"; diff --git a/features/issues/hooks/use-filters.tsx b/features/issues/hooks/use-filters.tsx index 8ebce02..b0406a3 100644 --- a/features/issues/hooks/use-filters.tsx +++ b/features/issues/hooks/use-filters.tsx @@ -1,4 +1,25 @@ -import { useContext } from "react"; -import { FiltersContext } from "../context/filters-context"; +import { useRouter } from "next/router"; +import { IssueLevel, IssueStatus } from "../types/issue.types"; -export const useFilters = () => useContext(FiltersContext); +type Filters = { + status: IssueStatus; + level: IssueLevel; + project: string; +}; + +export const useFilters = () => { + const router = useRouter(); + + const filters = { + status: router.query.status as IssueStatus, + level: router.query.level as IssueLevel, + project: router.query.project, + } as Filters; + + const updateFilters = (newFilters: Partial) => { + const query = { ...router.query, ...newFilters }; + router.push({ query }); + }; + + return { filters, updateFilters }; +}; diff --git a/features/issues/index.ts b/features/issues/index.ts index 3c0c107..3b832e8 100644 --- a/features/issues/index.ts +++ b/features/issues/index.ts @@ -1,5 +1,4 @@ export * from "./api/use-issues"; export * from "./components/IssueList"; export * from "./types/issue.types"; -export * from "./context"; export * from "./hooks"; diff --git a/features/ui/form/input/input.tsx b/features/ui/form/input/input.tsx index dad88cc..dc80304 100644 --- a/features/ui/form/input/input.tsx +++ b/features/ui/form/input/input.tsx @@ -29,7 +29,6 @@ const InputContainer = styled.input<{ 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)}; diff --git a/features/ui/form/select/index.ts b/features/ui/form/select/index.ts index 270d864..b1c8b7b 100644 --- a/features/ui/form/select/index.ts +++ b/features/ui/form/select/index.ts @@ -1,3 +1,4 @@ export { Option } from "./option"; export { Select } from "./select"; -export { SelectContext, useSelectContext } from "./selectContext"; +export { useSelectContext } from "./selectContext"; +export type { OptionType } from "./select"; diff --git a/features/ui/form/select/option.tsx b/features/ui/form/select/option.tsx index 01d1681..96ee44f 100644 --- a/features/ui/form/select/option.tsx +++ b/features/ui/form/select/option.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, ReactText } from "react"; +import React, { ReactNode } from "react"; import styled, { css } from "styled-components"; import { useSelectContext } from "./selectContext"; import { color, textFont, space } from "@styles/theme"; @@ -6,7 +6,6 @@ import { color, textFont, space } from "@styles/theme"; type OptionProps = { children: ReactNode | ReactNode[]; value: any; - handleCallback?: (value: any) => unknown; }; const ListItem = styled.li.attrs(() => ({ @@ -42,20 +41,26 @@ const ListItemIcon = styled.img<{ isCurrentlySelected: boolean }>` height: ${space(4)}; `; -export function Option({ children, value, handleCallback }: OptionProps) { - const { changeSelectedOption, selectedOption } = useSelectContext(); - const isCurrentlySelected = selectedOption === value; +export function Option({ children, value }: OptionProps) { + const { changeSelectedValue, selectedValue } = useSelectContext(); + const isCurrentlySelected = selectedValue === value; + + const onClick = () => { + changeSelectedValue(value); + }; + + const onKeyDown = (event: React.KeyboardEvent) => { + if (event.code === "Space") { + onClick(); + } + }; return ( { - changeSelectedOption(value); - if (handleCallback) { - handleCallback(value); - } - }} + onClick={onClick} + onKeyDown={onKeyDown} role="option" > {children} diff --git a/features/ui/form/select/select.stories.tsx b/features/ui/form/select/select.stories.tsx index 10ed054..4cc5ae4 100644 --- a/features/ui/form/select/select.stories.tsx +++ b/features/ui/form/select/select.stories.tsx @@ -41,7 +41,6 @@ Default.args = { label: "Team member", hint: "This is a hint text to help user.", errorMessage: "", - width: "", }; Default.parameters = { viewMode: "docs", diff --git a/features/ui/form/select/select.tsx b/features/ui/form/select/select.tsx index 5c23288..94a4554 100644 --- a/features/ui/form/select/select.tsx +++ b/features/ui/form/select/select.tsx @@ -1,38 +1,37 @@ -import React, { - useState, - ReactNode, - useCallback, - useMemo, - useRef, - SelectHTMLAttributes, -} from "react"; +import React, { useState, ReactNode, useRef, HTMLAttributes } from "react"; import styled, { css } from "styled-components"; import { useClickAway } from "react-use"; import { SelectContext } from "./selectContext"; import { color, textFont, space } from "@styles/theme"; -type SelectProps = SelectHTMLAttributes & { +export type OptionType = { + text: string; + value: string; +}; + +type SelectProps = Omit, "onChange"> & { children: ReactNode | ReactNode[]; errorMessage?: string; defaultValue?: string; placeholder?: string; disabled?: boolean; iconSrc?: string; - width?: string | number; label?: string; hint?: string; + value?: string; + onChange?: (value?: string) => void; + options: OptionType[]; }; -const Container = styled.div` +const Container = styled.div` position: relative; display: block; - width: ${({ width }) => width || `calc(${space(20)} * 4)`}; background-color: #fff; `; const List = styled.ul<{ showDropdown: boolean }>` display: block; - width: 100%; + min-width: 100%; margin: 0; padding: 0; position: absolute; @@ -57,15 +56,15 @@ const List = styled.ul<{ showDropdown: boolean }>` const SelectedOption = styled.div.attrs(() => ({ tabIndex: 0, ariaHasPopup: "listbox", -}))` +}))<{ disabled: boolean; hasError: boolean; isSelected: boolean }>` + width: 100%; border: 1px solid; - border-color: ${({ disabled, errorMessage }) => - !disabled && errorMessage ? color("error", 300) : color("gray", 300)}; + border-color: ${({ disabled, hasError }) => + !disabled && hasError ? color("error", 300) : color("gray", 300)}; border-radius: 7px; - width: ${({ width }) => width || `calc(${space(20)} * 4 - ${space(6)})`}; padding: ${space(2, 3)}; - color: ${({ selectedOption }) => - selectedOption ? color("gray", 900) : color("gray", 500)}; + color: ${({ isSelected }) => + isSelected ? color("gray", 900) : color("gray", 500)}; cursor: pointer; display: flex; justify-content: space-between; @@ -74,8 +73,8 @@ const SelectedOption = styled.div.attrs(() => ({ &:focus { outline: 3px solid; - outline-color: ${({ disabled, errorMessage }) => - !disabled && errorMessage ? color("error", 100) : color("primary", 200)}; + outline-color: ${({ disabled, hasError }) => + !disabled && hasError ? color("error", 100) : color("primary", 200)}; } ${({ disabled }) => @@ -129,17 +128,18 @@ const ErrorMessage = styled.p` export function Select({ placeholder = "Choose an option", - defaultValue = "", - iconSrc = "", + defaultValue, + value, + iconSrc, disabled = false, - label = "", - hint = "", - errorMessage = "", - width = "", + label, + hint, + errorMessage, children, + options, + onChange, ...props }: SelectProps) { - const [selectedOption, setSelectedOption] = useState(defaultValue || ""); const [showDropdown, setShowDropdown] = useState(false); const ref = useRef(null); @@ -148,37 +148,47 @@ export function Select({ setShowDropdown(false); }); - const showDropdownHandler = useCallback( - () => setShowDropdown((prevShowDropdown) => !prevShowDropdown), - [] - ); - - const updateSelectedOption = useCallback((option: string) => { - setSelectedOption(option); + const updateSelectedOption = (value: string) => { + if (onChange) onChange(value); setShowDropdown(false); - }, []); + }; - const value = useMemo( - () => ({ selectedOption, changeSelectedOption: updateSelectedOption }), - [selectedOption, updateSelectedOption] - ); + const selectedOption = + value === undefined + ? undefined + : options.find((option) => option.value === value); + + const toggleDropDown = () => { + setShowDropdown(!showDropdown); + }; + + const onKeyDown = (event: React.KeyboardEvent) => { + if (event.code === "Space") { + toggleDropDown(); + } + }; return ( - - + + {label && } {iconSrc && } - {selectedOption || placeholder} + {selectedOption?.text || placeholder} void; + selectedValue?: string; + changeSelectedValue: (value: string) => void; }>({ - selectedOption: "", - // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars - changeSelectedOption: (option: string) => {}, + selectedValue: undefined, + changeSelectedValue: () => null, }); export const useSelectContext = () => { - const context = useContext(SelectContext); - if (!context) { - throw new Error("Error in creating the context"); - } - return context; + return useContext(SelectContext); }; diff --git a/features/ui/page-container/page-container.tsx b/features/ui/page-container/page-container.tsx index 71c5a91..2d79014 100644 --- a/features/ui/page-container/page-container.tsx +++ b/features/ui/page-container/page-container.tsx @@ -3,7 +3,6 @@ import Head from "next/head"; import styled from "styled-components"; import { SidebarNavigation, Footer } from "@features/ui"; import { color, displayFont, space, breakpoint, textFont } from "@styles/theme"; -import { FiltersProvider } from "@features/issues"; type PageContainerProps = { children: React.ReactNode; @@ -60,23 +59,21 @@ const Info = styled.div` export function PageContainer({ children, title, info }: PageContainerProps) { return ( - - - ProLog - {title} - - - + + ProLog - {title} + + + - -
- - {title} - {info} - {children} - -
-
-
+ +
+ + {title} + {info} + {children} + +
+
); } diff --git a/features/ui/sidebar-navigation/sidebar-navigation.tsx b/features/ui/sidebar-navigation/sidebar-navigation.tsx index b7816df..1f3db95 100644 --- a/features/ui/sidebar-navigation/sidebar-navigation.tsx +++ b/features/ui/sidebar-navigation/sidebar-navigation.tsx @@ -48,6 +48,7 @@ const Container = styled.div<{ isCollapsed: boolean }>` const FixedContainer = styled.div` ${containerStyles} position: fixed; + z-index: 1; `; const Header = styled.header` diff --git a/package-lock.json b/package-lock.json index 73ccddd..6b6117a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "react-query": "^3.34.19", "react-use": "^17.4.0", "styled-components": "^5.3.3", - "styled-normalize": "^8.0.7" + "styled-normalize": "^8.0.7", + "use-debounce": "^9.0.2" }, "devDependencies": { "@babel/core": "^7.17.5", @@ -25014,6 +25015,17 @@ "react": "^16.8.0 || ^17.0.0" } }, + "node_modules/use-debounce": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.2.tgz", + "integrity": "sha512-QLyB0sxt9F5AisGDrUybCRJSLE60bTQR0yXc+IebNGUu1GCXwii1zsZl82mPGdWqDVQy7+1FKMLHQUixxf5Nbw==", + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/use-isomorphic-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz", @@ -45627,6 +45639,12 @@ "dev": true, "requires": {} }, + "use-debounce": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.2.tgz", + "integrity": "sha512-QLyB0sxt9F5AisGDrUybCRJSLE60bTQR0yXc+IebNGUu1GCXwii1zsZl82mPGdWqDVQy7+1FKMLHQUixxf5Nbw==", + "requires": {} + }, "use-isomorphic-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz", diff --git a/package.json b/package.json index 839cf7b..28f1d8d 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "react-query": "^3.34.19", "react-use": "^17.4.0", "styled-components": "^5.3.3", - "styled-normalize": "^8.0.7" + "styled-normalize": "^8.0.7", + "use-debounce": "^9.0.2" }, "devDependencies": { "@babel/core": "^7.17.5", diff --git a/pages/dashboard/issues.tsx b/pages/dashboard/issues.tsx index 0281713..73a91ce 100644 --- a/pages/dashboard/issues.tsx +++ b/pages/dashboard/issues.tsx @@ -1,18 +1,15 @@ import { PageContainer } from "@features/ui"; import { IssueList } from "@features/issues"; import type { NextPage } from "next"; -import { FiltersProvider } from "@features/issues"; const IssuesPage: NextPage = () => { return ( - - - - - + + + ); };