Skip to content

Commit 61c1615

Browse files
committed
Refactor filters to use ToolsProvider and update data fetching for tools
page Benefits 1. No more API calls for filtering/pagination on the tools page 2. Faster page loads (all data is pre-rendered at build time) 3. Better SEO since page is fully static with ISR 4. Simpler architecture because filtering happens client-side with static data 5. Can now remove old API routes and React Query for tool
1 parent e3a1bc7 commit 61c1615

9 files changed

Lines changed: 184 additions & 149 deletions

File tree

components/elements/Dropdown/Dropdown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Card } from '@components/layout';
22
import classNames from 'classnames';
3-
import { useSearchState } from 'context/SearchProvider';
3+
import { useSearchState } from 'context/ToolsProvider';
44
import { FC, useEffect, useState } from 'react';
55
import styles from './Dropdown.module.css';
66

components/elements/TagList/TagList.tsx

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { FC, useState } from 'react';
22
import Image from 'next/image';
33
import cn from 'classnames';
44
import styles from './TagList.module.css';
5-
import { TagTypes, useSearchState } from 'context/SearchProvider';
6-
import { objectToQueryString } from 'utils/query';
7-
import { useRouterPush } from 'hooks';
5+
import { useTools } from 'context/ToolsProvider';
86
import classNames from 'classnames';
97
import { tagIconPath } from 'utils/icons';
108

9+
// Tag type for filtering
10+
type TagTypes = 'languages' | 'others';
11+
1112
const NUM_TAGS = 8;
1213

1314
export interface TagListProps {
@@ -17,8 +18,7 @@ export interface TagListProps {
1718
}
1819

1920
const TagList: FC<TagListProps> = ({ languageTags, otherTags, className }) => {
20-
const routerPush = useRouterPush();
21-
const { search, setSearch } = useSearchState();
21+
const { toggleFilter, isSelected } = useTools();
2222
const [showAllTags, setShowAllTags] = useState(false);
2323

2424
const toggleShowAllTags = () => {
@@ -31,28 +31,11 @@ const TagList: FC<TagListProps> = ({ languageTags, otherTags, className }) => {
3131
) => {
3232
const target = event?.target as HTMLElement;
3333
const tag = target.innerText;
34+
toggleFilter(tagType, tag);
35+
};
3436

35-
let currValue = search[tagType] || [];
36-
if (!Array.isArray(currValue)) {
37-
currValue = [currValue];
38-
}
39-
if (currValue.length) {
40-
const index = currValue.indexOf(tag);
41-
if (index > -1) {
42-
currValue.splice(index, 1);
43-
} else {
44-
currValue.push(tag);
45-
}
46-
} else {
47-
currValue.push(tag);
48-
}
49-
50-
const newState = { ...search, [tagType]: currValue };
51-
setSearch(newState);
52-
// Update query params in router
53-
routerPush(`/tools?${objectToQueryString(newState)}`, undefined, {
54-
shallow: true,
55-
});
37+
const isTagSelected = (tag: string, type: TagTypes): boolean => {
38+
return isSelected(type, tag);
5639
};
5740

5841
const combinedTags = [
@@ -75,7 +58,7 @@ const TagList: FC<TagListProps> = ({ languageTags, otherTags, className }) => {
7558
{tagsToRender.map(({ tag, type }, index) => (
7659
<li
7760
className={classNames(styles.tag, {
78-
[`${styles.highlight}`]: search[type]?.includes(tag),
61+
[`${styles.highlight}`]: isTagSelected(tag, type),
7962
})}
8063
key={`${tag}-${index}`}>
8164
<a onClick={(e) => handleClick(e, type)}>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
11
.clearButton {
22
width: 140px;
33
}
4+
5+
.resultsCount {
6+
font-size: 0.875rem;
7+
color: var(--color-text-secondary, #666);
8+
margin-right: auto;
9+
}
10+
11+
.loadMoreContainer {
12+
display: flex;
13+
justify-content: center;
14+
padding: 2rem 0;
15+
}

components/tools/listPage/MobileFilters/MobileFilter/MobileFilter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import styles from './MobileFilter.module.css';
33
import Select, { MultiValue } from 'react-select';
44
import { Button, PanelHeader } from '@components/elements';
55
import classNames from 'classnames';
6-
import { SearchState, SearchFilter } from 'context/SearchProvider';
6+
import { SearchState, SearchFilter } from 'context/ToolsProvider';
77

88
interface Option {
99
value: string;

components/tools/listPage/MobileFilters/MobileFilters.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { FC, useState } from 'react';
22
import classNames from 'classnames';
33
import { Button, PanelHeader } from '@components/elements';
44
import styles from './MobileFilters.module.css';
5-
import { useLanguagesQuery } from '@components/tools/queries';
6-
import { useSearchState } from 'context/SearchProvider';
5+
import { useTools, type SearchState } from 'context/ToolsProvider';
76
import MobileFilter from './MobileFilter/MobileFilter';
87

98
import {
@@ -13,20 +12,22 @@ import {
1312
LICENSE_OPTIONS,
1413
PRICING_OPTIONS,
1514
} from '@appdata/filters';
16-
import { useOthersQuery } from '@components/tools/queries/others';
15+
import { LanguageFilterOption } from '../ToolsSidebar/FilterCard/LanguageFilterCard';
16+
import { FilterOption } from '../ToolsSidebar/FilterCard/FilterCard';
1717

18-
const MobileFilters: FC = () => {
19-
const { search, setSearch } = useSearchState();
18+
export interface MobileFiltersProps {
19+
languages?: LanguageFilterOption[];
20+
others?: FilterOption[];
21+
}
2022

21-
const [modelOpen, setModelOpen] = useState(false);
22-
const [state, setState] = useState(search);
23-
24-
const { data: otherResult } = useOthersQuery();
25-
const { data: languageResult } = useLanguagesQuery();
23+
const MobileFilters: FC<MobileFiltersProps> = ({
24+
languages = [],
25+
others = [],
26+
}) => {
27+
const { search, setSearch } = useTools();
2628

27-
const others = otherResult?.data || [];
28-
// Filter duplicates on tag
29-
const languages = languageResult?.data || [];
29+
const [modelOpen, setModelOpen] = useState(false);
30+
const [state, setState] = useState<SearchState>(search);
3031

3132
const submit = () => {
3233
setSearch(state);
@@ -39,6 +40,8 @@ const MobileFilters: FC = () => {
3940
};
4041

4142
const openModal = () => {
43+
// Sync state with current search when opening
44+
setState(search);
4245
// Add modal-open class to body
4346
document.body.classList.add('modal-open');
4447
setModelOpen(true);
@@ -87,7 +90,7 @@ const MobileFilters: FC = () => {
8790
<MobileFilter
8891
id="languages"
8992
label="Languages"
90-
options={languages || []}
93+
options={languages}
9194
placeholder="Choose Language"
9295
state={state}
9396
setState={setState}
@@ -132,7 +135,7 @@ const MobileFilters: FC = () => {
132135
<MobileFilter
133136
id="others"
134137
label="Other Tags"
135-
options={others || []}
138+
options={others}
136139
placeholder="Other Tags"
137140
state={state}
138141
setState={setState}

components/tools/listPage/ToolsSidebar/FilterCard/FilterCard.tsx

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import { Card } from '@components/layout';
44
import { Heading } from '@components/typography';
55

66
import styles from './FilterCard.module.css';
7-
import { SearchFilter, useSearchState } from 'context/SearchProvider';
8-
import { isChecked, isSelectedFilter, sortByChecked } from './utils';
9-
import { changeQuery } from 'utils/query';
7+
import { useTools, type SearchFilter } from 'context/ToolsProvider';
108
import classNames from 'classnames';
119

1210
export interface FilterOption {
@@ -25,8 +23,6 @@ export interface FilterCardProps {
2523
className?: string;
2624
}
2725

28-
// TODO: Add Toggle Deprecated (default off)
29-
// TODO: Add click functionality and debounce
3026
const FilterCard: FC<FilterCardProps> = ({
3127
showAllCheckbox = true,
3228
heading,
@@ -35,10 +31,11 @@ const FilterCard: FC<FilterCardProps> = ({
3531
limit = 10,
3632
className,
3733
}) => {
38-
const { search, setSearch } = useSearchState();
34+
const { search, toggleFilter, updateFilter, isSelected } = useTools();
3935

4036
const shouldShowToggle = options.length > limit;
4137
const [listLimit, setLimit] = useState(limit);
38+
4239
const toggleAll = () => {
4340
if (listLimit === 999) {
4441
setLimit(limit);
@@ -49,15 +46,33 @@ const FilterCard: FC<FilterCardProps> = ({
4946

5047
const resetFilter = () => {
5148
const searchFilter = filter as SearchFilter;
52-
if (search[searchFilter]) {
53-
delete search[searchFilter];
54-
}
55-
setSearch({ ...search });
49+
updateFilter(searchFilter, []);
50+
};
51+
52+
const handleCheckboxChange = (value: string) => {
53+
const searchFilter = filter as SearchFilter;
54+
toggleFilter(searchFilter, value);
55+
};
56+
57+
const isChecked = (value: string): boolean => {
58+
const searchFilter = filter as SearchFilter;
59+
return isSelected(searchFilter, value);
60+
};
61+
62+
const isFilterActive = (): boolean => {
63+
const searchFilter = filter as SearchFilter;
64+
const values = search[searchFilter];
65+
return values !== undefined && values.length > 0;
5666
};
5767

58-
if (options.length > limit) {
59-
options.sort(sortByChecked(filter, search));
60-
}
68+
// Sort options: checked items first
69+
const sortedOptions = [...options].sort((a, b) => {
70+
const aChecked = isChecked(a.value);
71+
const bChecked = isChecked(b.value);
72+
if (aChecked && !bChecked) return -1;
73+
if (!aChecked && bChecked) return 1;
74+
return 0;
75+
});
6176

6277
return (
6378
<Card className={classNames(className)}>
@@ -70,36 +85,32 @@ const FilterCard: FC<FilterCardProps> = ({
7085
<li>
7186
<Input
7287
type="checkbox"
73-
id="checkbox_all"
88+
id={`checkbox_all_${filter}`}
7489
data-filter={filter}
75-
checked={!isSelectedFilter(filter, search)}
90+
checked={!isFilterActive()}
7691
onChange={resetFilter}
7792
/>
7893
<label
7994
className={styles.checkboxLabel}
80-
htmlFor="checkbox_all"
95+
htmlFor={`checkbox_all_${filter}`}
8196
onClick={resetFilter}>
8297
All
8398
</label>
8499
</li>
85100
)}
86-
{options.slice(0, listLimit).map((option, index) => (
101+
{sortedOptions.slice(0, listLimit).map((option, index) => (
87102
<li key={index}>
88103
<Input
89104
type="checkbox"
90-
id={`checkbox_${option.value}`}
105+
id={`checkbox_${filter}_${option.value}`}
91106
value={option.value}
92107
data-filter={filter}
93-
checked={isChecked(filter, option.value, search)}
94-
onChange={changeQuery(
95-
option.value,
96-
search,
97-
setSearch,
98-
)}
108+
checked={isChecked(option.value)}
109+
onChange={() => handleCheckboxChange(option.value)}
99110
/>
100111
<label
101112
className={styles.checkboxLabel}
102-
htmlFor={`checkbox_${option.value}`}>
113+
htmlFor={`checkbox_${filter}_${option.value}`}>
103114
{option.name}
104115
</label>
105116

0 commit comments

Comments
 (0)