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

feat: add filter toggler component #13

Merged
merged 6 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/filters/components/filter-toggler/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use client';

import { Button } from '@nextui-org/button';

import { FilterIcon } from '@/shared/components/icons/filter-icon';
import { PrimaryButton } from '@/shared/components/primary-button';

import { useFilterToggler } from './use-filter-toggler';

interface Props {
children: React.ReactNode;
countSection: React.ReactNode;
}

export const FilterToggler = ({ children, countSection }: Props) => {
const {
initializedFilters,
isPendingFilters,
toggleStyle,
buttonText,
toggleOpen,
isOpen,
applyFilters,
clearFilters,
isDisabledApply,
isDisabledClear,
} = useFilterToggler();

const PRIMARY_BUTTON_CLASS = 'cursor-pointer';

return (
<>
<div className="flex items-center justify-between">
<Button
startContent={
initializedFilters && !isPendingFilters && <FilterIcon />
}
style={toggleStyle}
onClick={toggleOpen}
isLoading={!initializedFilters || isPendingFilters}
>
{buttonText}
</Button>
{countSection}
</div>

{isOpen && (
<>
{children}

<div className="flex gap-4">
<PrimaryButton
text="Apply Filters"
className={PRIMARY_BUTTON_CLASS}
onClick={applyFilters}
isDisabled={isDisabledApply}
/>
<Button onClick={clearFilters} isDisabled={isDisabledClear}>
Clear Filters
</Button>
</div>
</>
)}
</>
);
};
78 changes: 78 additions & 0 deletions src/filters/components/filter-toggler/use-filter-toggler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useReducer } from 'react';

import { useAtom } from 'jotai';

import { getGradientBorderStyle } from '@/shared/utils/get-gradient-border-style';

import { useFiltersContext } from '@/filters/providers/filters-provider/context';

const TOGGLE_TEXT = 'Filters & Sorting';
const ACTIVE_BUTTON_STYLE = getGradientBorderStyle();
const EXCLUDED_KEY = 'query';
const RANGE_KEYS_REGEX = /min|max/g;

export const useFilterToggler = () => {
const {
initializedFilters,
isPendingFilters,
atom,
startFiltersTransition,
routeSection,
} = useFiltersContext();

const [atomValue, setAtom] = useAtom(atom);

const filterCount = useMemo(() => {
const keys = new Set();
atomValue.forEach((_, k) => {
if (k !== EXCLUDED_KEY) keys.add(k.replace(RANGE_KEYS_REGEX, ''));
});
return keys.size;
}, [atomValue]);

const [isOpen, toggleOpen] = useReducer((prev) => !prev, false);

useEffect(() => {
if (isOpen && isPendingFilters) {
toggleOpen();
}
}, [isOpen, isPendingFilters]);

const toggleStyle =
(isOpen || filterCount > 0) && !isPendingFilters
? ACTIVE_BUTTON_STYLE
: undefined;
const buttonText = `${TOGGLE_TEXT}${filterCount > 0 ? ` (${filterCount})` : ''}`;

const { push } = useRouter();

const applyFilters = () => {
startFiltersTransition(() => {
push(`/${routeSection}?${atomValue.toString()}`);
});
};

const clearFilters = () => {
startFiltersTransition(() => {
setAtom(new URLSearchParams());
push(`/${routeSection}`);
});
};

const isDisabledApply = filterCount === 0;
const isDisabledClear = atomValue.size === 0;

return {
initializedFilters,
isPendingFilters,
toggleOpen,
toggleStyle,
buttonText,
isOpen,
applyFilters,
clearFilters,
isDisabledApply,
isDisabledClear,
};
};
20 changes: 20 additions & 0 deletions src/filters/components/filter-toggler/use-init-filter-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';

import { PrimitiveAtom, useAtom } from 'jotai';

export const useInitFilterParams = (
searchParams: URLSearchParams,
atom: PrimitiveAtom<URLSearchParams>,
) => {
const [initialized, setInitialized] = useState(false);
const [filterParams, setFilterParams] = useAtom(atom);

useEffect(() => {
if (!initialized) {
setFilterParams(new URLSearchParams(searchParams));
setInitialized(true);
}
}, [initialized, setFilterParams, searchParams]);

return { initialized, filterParams, setFilterParams };
};
26 changes: 26 additions & 0 deletions src/shared/components/icons/filter-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { memo } from 'react';

export const FilterIcon = memo(() => (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.78571 10.9286H14.2143C14.4707 10.9286 14.6786 10.5128 14.6786 10C14.6786 9.48716 14.4707 9.07143 14.2143 9.07143H6.78571C6.5293 9.07143 6.32143 9.48716 6.32143 10C6.32143 10.5128 6.5293 10.9286 6.78571 10.9286Z"
fill="white"
/>
<path
d="M4.46429 5.35714H16.5357C16.7921 5.35714 17 4.94141 17 4.42857C17 3.91574 16.7921 3.5 16.5357 3.5H4.46429C4.20787 3.5 4 3.91574 4 4.42857C4 4.94141 4.20787 5.35714 4.46429 5.35714Z"
fill="white"
/>
<path
d="M9.10714 16.5H11.8929C12.1493 16.5 12.3571 16.0843 12.3571 15.5714C12.3571 15.0586 12.1493 14.6429 11.8929 14.6429H9.10714C8.85072 14.6429 8.64286 15.0586 8.64286 15.5714C8.64286 16.0843 8.85072 16.5 9.10714 16.5Z"
fill="white"
/>
</svg>
));

FilterIcon.displayName = 'FilterIcon';
27 changes: 27 additions & 0 deletions src/shared/components/primary-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Button, ButtonProps } from '@nextui-org/button';
import { ClassValue } from 'clsx';

import { cn } from '@/shared/utils/cn';

interface Props extends ButtonProps {
text: string;
classNames?: {
button?: ClassValue;
text?: ClassValue;
};
}

const BUTTON_CLASS_NAME =
'flex items-center rounded-lg bg-gradient-to-l from-[#8743FF] to-[#4136F1] p-2 px-4';
const TEXT_CLASS_NAME = 'shrink-0 text-xs font-semibold sm:text-sm';

export const PrimaryButton = ({ text, classNames, ...props }: Props) => {
const buttonClassName = cn(BUTTON_CLASS_NAME, classNames?.button);
const textClassName = cn(TEXT_CLASS_NAME, classNames?.text);

return (
<Button className={buttonClassName} {...props}>
<span className={textClassName}>{text}</span>
</Button>
);
};
20 changes: 20 additions & 0 deletions src/shared/utils/get-gradient-border-style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const PRIMARY_START_COLOR = '#8743FF';
const PRIMARY_END_COLOR = '#4136F1';
const SECONDARY_START_COLOR = '#363638';
const SECONDARY_END_COLOR = '#27272A';
const BASE_BACKGROUND_COLOR = '#1e1e1e';
const TRANSPARENT_BORDER = '2px solid transparent';
const BASE_BACKGROUND_GRADIENT_DIRECTION = '90deg';
const BORDER_GRADIENT_DIRECTION = '270deg';

export const getGradientBorderStyle = (
isPrimary = true,
): React.CSSProperties => {
const startColor = isPrimary ? PRIMARY_START_COLOR : SECONDARY_START_COLOR;
const endColor = isPrimary ? PRIMARY_END_COLOR : SECONDARY_END_COLOR;

return {
background: `linear-gradient(${BASE_BACKGROUND_GRADIENT_DIRECTION}, ${BASE_BACKGROUND_COLOR}, ${BASE_BACKGROUND_COLOR}) padding-box, linear-gradient(${BORDER_GRADIENT_DIRECTION}, ${startColor}, ${endColor}) border-box`,
border: TRANSPARENT_BORDER,
};
};
Loading