Skip to content

Commit 3f8e199

Browse files
authored
feat(Search): add toggle separation for supported filters (#102)
* feat(FilmGrid): add initial loading state and improve loading indicators * feat(FilmDirector): update to support multiple directors and improve rendering * feat(auth): add option to disable retry on error for useAuth hook * feat(Search): add toggle separation for supported filters
1 parent 6228a3d commit 3f8e199

File tree

14 files changed

+727
-251
lines changed

14 files changed

+727
-251
lines changed

src/components/Search/Filter/Cast.jsx

Lines changed: 82 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,87 @@
1-
import { useEffect, useState, useCallback, useRef } from "react";
1+
import { useEffect, useState } from "react";
22
import AsyncSelect from "react-select/async";
33
import { usePathname, useRouter, useSearchParams } from "next/navigation";
44
import axios from "axios";
5+
import { AND_SEPARATION, OR_SEPARATION } from "@/lib/constants";
6+
import debounce from "debounce";
7+
8+
const WITH_CAST = "with_cast";
59

610
export default function Cast({ inputStyles }) {
711
const router = useRouter();
812
const pathname = usePathname();
913
const searchParams = useSearchParams();
1014
const current = new URLSearchParams(Array.from(searchParams.entries()));
15+
1116
const isQueryParams = searchParams.get("query");
17+
const defaultToggleSeparation = searchParams.get(WITH_CAST)?.includes("|")
18+
? OR_SEPARATION
19+
: AND_SEPARATION;
1220

1321
const [cast, setCast] = useState([]);
22+
const [toggleSeparation, setToggleSeparation] = useState(
23+
defaultToggleSeparation,
24+
);
1425

15-
const timerRef = useRef(null);
16-
const castsLoadOptions = useCallback((inputValue, callback) => {
17-
const fetchDataWithDelay = async () => {
18-
// Delay pengambilan data selama 500ms setelah pengguna berhenti mengetik
19-
await new Promise((resolve) => setTimeout(resolve, 1000));
20-
21-
// Lakukan pengambilan data setelah delay
22-
axios
23-
.get(`/api/search/person`, { params: { query: inputValue } })
24-
.then(({ data }) => {
25-
const options = data.results.map((person) => ({
26-
value: person.id,
27-
label: person.name,
28-
}));
29-
const filteredOptions = options.filter((option) =>
30-
option.label.toLowerCase().includes(inputValue.toLowerCase()),
31-
);
32-
callback(filteredOptions);
33-
});
34-
};
26+
const separation = toggleSeparation === AND_SEPARATION ? "," : "|";
27+
28+
const castsLoadOptions = debounce(async (inputValue, callback) => {
29+
const { data } = await axios.get(`/api/search/person`, {
30+
params: { query: inputValue },
31+
});
32+
33+
const options = data.results.map((person) => ({
34+
value: person.id,
35+
label: person.name,
36+
}));
3537

36-
// Hapus pemanggilan sebelumnya jika ada
37-
clearTimeout(timerRef.current);
38+
const filteredOptions = options.filter((option) =>
39+
option.label.toLowerCase().includes(inputValue.toLowerCase()),
40+
);
3841

39-
// Set timer untuk memanggil fetchDataWithDelay setelah delay
40-
timerRef.current = setTimeout(() => {
41-
fetchDataWithDelay();
42-
}, 1000);
43-
}, []);
42+
callback(filteredOptions);
43+
}, 1000);
4444

4545
const handleCastChange = (selectedOption) => {
4646
const value = selectedOption.map((option) => option.value);
4747

4848
if (value.length === 0) {
49-
current.delete("with_cast");
49+
current.delete(WITH_CAST);
5050
} else {
51-
current.set("with_cast", value);
51+
current.set(WITH_CAST, value.join(separation));
5252
}
5353

54-
const search = current.toString();
54+
router.push(`${pathname}?${current.toString()}`);
55+
};
56+
57+
const handleSeparator = (separator) => {
58+
setToggleSeparation(separator);
59+
60+
if (searchParams.get(WITH_CAST)) {
61+
const params = searchParams.get(WITH_CAST);
62+
63+
const separation = separator === AND_SEPARATION ? "," : "|";
64+
const newSeparator = params.includes("|") ? "," : "|";
65+
if (newSeparator !== separation) return;
5566

56-
const query = search ? `?${search}` : "";
67+
const updatedParams = params.replace(/[\|,]/g, newSeparator);
5768

58-
router.push(`${pathname}${query}`);
69+
current.set(WITH_CAST, updatedParams);
70+
router.push(`${pathname}?${current.toString()}`);
71+
}
5972
};
6073

6174
useEffect(() => {
6275
// Cast
63-
if (searchParams.get("with_cast")) {
64-
const castParams = searchParams.get("with_cast").split(",");
65-
const fetchPromises = castParams.map((castId) => {
66-
return axios.get(`/api/person/${castId}`).then(({ data }) => data);
67-
});
76+
if (searchParams.get(WITH_CAST)) {
77+
const params = searchParams.get(WITH_CAST);
78+
const splitted = params.split(separation);
6879

69-
Promise.all(fetchPromises)
80+
Promise.all(
81+
splitted.map((castId) =>
82+
axios.get(`/api/person/${castId}`).then(({ data }) => data),
83+
),
84+
)
7085
.then((responses) => {
7186
const uniqueCast = [...new Set(responses)]; // Remove duplicates if any
7287
const searchCast = uniqueCast.map((cast) => ({
@@ -81,11 +96,37 @@ export default function Cast({ inputStyles }) {
8196
} else {
8297
setCast(null);
8398
}
84-
}, [searchParams]);
99+
}, [searchParams, separation]);
85100

86101
return (
87102
<section className={`flex flex-col gap-1`}>
88-
<span className={`font-medium`}>Actor</span>
103+
<div className={`flex items-center justify-between`}>
104+
<span className={`font-medium`}>Actor</span>
105+
106+
<div className={`flex rounded-full bg-base-100 p-1`}>
107+
<button
108+
onClick={() => handleSeparator(AND_SEPARATION)}
109+
className={`btn btn-ghost btn-xs rounded-full ${
110+
toggleSeparation === AND_SEPARATION
111+
? "bg-white text-base-100 hover:bg-white hover:bg-opacity-50"
112+
: ""
113+
}`}
114+
>
115+
AND
116+
</button>
117+
<button
118+
onClick={() => handleSeparator(OR_SEPARATION)}
119+
className={`btn btn-ghost btn-xs rounded-full ${
120+
toggleSeparation === OR_SEPARATION
121+
? "bg-white text-base-100 hover:bg-white hover:bg-opacity-50"
122+
: ""
123+
}`}
124+
>
125+
OR
126+
</button>
127+
</div>
128+
</div>
129+
89130
<AsyncSelect
90131
noOptionsMessage={() => "Type to search"}
91132
loadingMessage={() => "Searching..."}

src/components/Search/Filter/Company.jsx

Lines changed: 85 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,89 @@
1-
import { useEffect, useState, useMemo, useCallback, useRef } from "react";
1+
import { useEffect, useState } from "react";
22
import AsyncSelect from "react-select/async";
33
import { usePathname, useRouter, useSearchParams } from "next/navigation";
44
import axios from "axios";
5+
import { AND_SEPARATION, OR_SEPARATION } from "@/lib/constants";
6+
import debounce from "debounce";
7+
8+
const WITH_COMPANIES = "with_companies";
59

610
export default function Company({ inputStyles }) {
711
const router = useRouter();
812
const pathname = usePathname();
913
const searchParams = useSearchParams();
10-
const current = useMemo(
11-
() => new URLSearchParams(Array.from(searchParams.entries())),
12-
[searchParams],
13-
);
14+
const current = new URLSearchParams(Array.from(searchParams.entries()));
15+
1416
const isQueryParams = searchParams.get("query");
17+
const defaultToggleSeparation = searchParams
18+
.get(WITH_COMPANIES)
19+
?.includes("|")
20+
? OR_SEPARATION
21+
: AND_SEPARATION;
1522

1623
const [company, setCompany] = useState([]);
24+
const [toggleSeparation, setToggleSeparation] = useState(
25+
defaultToggleSeparation,
26+
);
1727

18-
const timerRef = useRef(null);
19-
const companiesLoadOptions = useCallback((inputValue, callback) => {
20-
const fetchDataWithDelay = async () => {
21-
// Delay pengambilan data selama 500ms setelah pengguna berhenti mengetik
22-
await new Promise((resolve) => setTimeout(resolve, 1000));
23-
24-
// Lakukan pengambilan data setelah delay
25-
axios
26-
.get(`/api/search/company`, { params: { query: inputValue } })
27-
.then(({ data }) => {
28-
const options = data.results.map((company) => ({
29-
value: company.id,
30-
label: company.name,
31-
}));
32-
const filteredOptions = options.filter((option) =>
33-
option.label.toLowerCase().includes(inputValue.toLowerCase()),
34-
);
35-
callback(filteredOptions);
36-
});
37-
};
28+
const separation = toggleSeparation === AND_SEPARATION ? "," : "|";
29+
30+
const companiesLoadOptions = debounce(async (inputValue, callback) => {
31+
const { data } = await axios.get(`/api/search/company`, {
32+
params: { query: inputValue },
33+
});
3834

39-
// Hapus pemanggilan sebelumnya jika ada
40-
clearTimeout(timerRef.current);
35+
const options = data.results.map((company) => ({
36+
value: company.id,
37+
label: company.name,
38+
}));
4139

42-
// Set timer untuk memanggil fetchDataWithDelay setelah delay
43-
timerRef.current = setTimeout(() => {
44-
fetchDataWithDelay();
45-
}, 1000);
46-
}, []);
40+
const filteredOptions = options.filter((option) =>
41+
option.label.toLowerCase().includes(inputValue.toLowerCase()),
42+
);
43+
44+
callback(filteredOptions);
45+
}, 1000);
4746

4847
const handleCompanyChange = (selectedOption) => {
4948
const value = selectedOption.map((option) => option.value);
5049

5150
if (value.length === 0) {
52-
current.delete("with_companies");
51+
current.delete(WITH_COMPANIES);
5352
} else {
54-
current.set("with_companies", value);
53+
current.set(WITH_COMPANIES, value.join(separation));
5554
}
5655

57-
const search = current.toString();
56+
router.push(`${pathname}?${current.toString()}`);
57+
};
58+
59+
const handleSeparator = (separator) => {
60+
setToggleSeparation(separator);
61+
62+
if (searchParams.get(WITH_COMPANIES)) {
63+
const params = searchParams.get(WITH_COMPANIES);
5864

59-
const query = search ? `?${search}` : "";
65+
const separation = separator === AND_SEPARATION ? "," : "|";
66+
const newSeparator = params.includes("|") ? "," : "|";
67+
if (newSeparator !== separation) return;
6068

61-
router.push(`${pathname}${query}`);
69+
const updatedParams = params.replace(/[\|,]/g, newSeparator);
70+
71+
current.set(WITH_COMPANIES, updatedParams);
72+
router.push(`${pathname}?${current.toString()}`);
73+
}
6274
};
6375

6476
useEffect(() => {
6577
// Company
66-
if (searchParams.get("with_companies")) {
67-
const companyParams = searchParams.get("with_companies").split(",");
68-
const fetchPromises = companyParams.map((companyId) => {
69-
return axios.get(`/api/company/${companyId}`).then(({ data }) => data);
70-
});
78+
if (searchParams.get(WITH_COMPANIES)) {
79+
const params = searchParams.get(WITH_COMPANIES);
80+
const splitted = params.split(separation);
7181

72-
Promise.all(fetchPromises)
82+
Promise.all(
83+
splitted.map((companyId) =>
84+
axios.get(`/api/company/${companyId}`).then(({ data }) => data),
85+
),
86+
)
7387
.then((responses) => {
7488
const uniqueCompany = [...new Set(responses)]; // Remove duplicates if any
7589
const searchCompany = uniqueCompany.map((company) => ({
@@ -84,11 +98,37 @@ export default function Company({ inputStyles }) {
8498
} else {
8599
setCompany(null);
86100
}
87-
}, [searchParams]);
101+
}, [searchParams, separation]);
88102

89103
return (
90104
<section className={`flex flex-col gap-1`}>
91-
<span className={`font-medium`}>Company</span>
105+
<div className={`flex items-center justify-between`}>
106+
<span className={`font-medium`}>Company</span>
107+
108+
<div className={`flex rounded-full bg-base-100 p-1`}>
109+
<button
110+
onClick={() => handleSeparator(AND_SEPARATION)}
111+
className={`btn btn-ghost btn-xs rounded-full ${
112+
toggleSeparation === AND_SEPARATION
113+
? "bg-white text-base-100 hover:bg-white hover:bg-opacity-50"
114+
: ""
115+
}`}
116+
>
117+
AND
118+
</button>
119+
<button
120+
onClick={() => handleSeparator(OR_SEPARATION)}
121+
className={`btn btn-ghost btn-xs rounded-full ${
122+
toggleSeparation === OR_SEPARATION
123+
? "bg-white text-base-100 hover:bg-white hover:bg-opacity-50"
124+
: ""
125+
}`}
126+
>
127+
OR
128+
</button>
129+
</div>
130+
</div>
131+
92132
<AsyncSelect
93133
noOptionsMessage={() => "Type to search"}
94134
loadingMessage={() => "Searching..."}

0 commit comments

Comments
 (0)