Skip to content

Commit

Permalink
chore(Contacts): add basic search functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
mjkeaton committed Dec 19, 2024
1 parent 85bb4a6 commit 98d5d7a
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 15 deletions.
8 changes: 7 additions & 1 deletion src/constants/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
const GET_BILLS = "/bills";
const SEARCH_BILLS = "/bill/search";
const GET_CONTACTS = "/contacts/list";
const SEARCH_CONTACTS = "/contacts/search";

export { GET_BILLS, SEARCH_BILLS, GET_CONTACTS };
export {
GET_BILLS,
SEARCH_BILLS,
GET_CONTACTS,
SEARCH_CONTACTS,
};
2 changes: 2 additions & 0 deletions src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { recentBills } from "./handlers/home/recent-bills";
import { billsList } from "./handlers/bills/list";
import { searchBills } from "./handlers/bills/search";
import { contactList } from "./handlers/contacts/list";
import { searchContacts } from "./handlers/contacts/search";

export const handlers = [
recentBills,
balances,
billsList,
searchBills,
contactList,
searchContacts,
];
6 changes: 3 additions & 3 deletions src/mocks/handlers/contacts/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const BARNEY = {
identification_number: "1234567890",
};

const data: Contact[] = [AMAZON, BOB, APPLE, TERRY, ALICE, BARNEY, ADA];
export const data: Contact[] = [AMAZON, BOB, APPLE, TERRY, ALICE, BARNEY, ADA];

type ContactsResponse = {
contacts: Contact[];
Expand All @@ -94,7 +94,7 @@ type ContactsResponse = {
export const contactList = http.get<never, never, ContactsResponse, "/contacts/list">(
"/contacts/list",
async () => {
await delay(3000);
await delay(2_000);

return HttpResponse.json({
contacts: data
Expand All @@ -105,7 +105,7 @@ export const contactList = http.get<never, never, ContactsResponse, "/contacts/l
export const emptyContactsList = http.get<never, never, ContactsResponse, "/contacts/list">(
"/contacts/list",
async () => {
await delay(1000);
await delay(1_000);

return HttpResponse.json({
contacts: []
Expand Down
62 changes: 62 additions & 0 deletions src/mocks/handlers/contacts/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { http, delay, HttpResponse } from "msw";
import { SEARCH_CONTACTS } from "@/constants/endpoints";
import type { Contact } from "@/types/contact";
import * as contacts from "@/mocks/handlers/contacts/list";

type SearchContactsResponse = {
contacts: Contact[];
}

export const data: SearchContactsResponse = {
contacts: contacts.data
};

type SearchContactsFilter = {
search_term?: string;
types?: Contact['type'][];
};

const filterContacts = (
data: SearchContactsResponse,
{ search_term, types }: SearchContactsFilter
): SearchContactsResponse => {
return {
contacts: data.contacts.filter((it) => {
const matchesSearchTerm = !search_term ? true : Object.entries(it).some(([, value]) => {
if (typeof value === "object") {
return Object.values(it).some(
(innerValue) =>
typeof innerValue === "string" &&
innerValue.toLowerCase().includes(search_term.toLowerCase())
);
}
return (
typeof value === "string" &&
value.toLowerCase().includes(search_term.toLowerCase())
);
});

const matchesTypes = types === undefined || types.includes(it.type);

return (
matchesSearchTerm && matchesTypes
);
})
};
};

export const searchContacts = http.post<
never,
never,
SearchContactsResponse,
typeof SEARCH_CONTACTS
>(SEARCH_CONTACTS, async ({ request }) => {

await delay(500);

const { filter } = await request.json();

const filteredData = filterContacts(data, filter);

return HttpResponse.json(filteredData);
});
39 changes: 30 additions & 9 deletions src/pages/contacts/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import List from "./components/List";
import EmptyList from "./components/EmptyList";
import type { Contact } from "@/types/contact";
import TypeFilter from "./components/TypeFilter";
import { getContacts } from "@/services/contact";
import { useQuery } from "@tanstack/react-query";
import { getContacts, searchContacts } from "@/services/contact";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Skeleton } from "@/components/ui/skeleton";

function NoResults() {
Expand Down Expand Up @@ -46,17 +46,30 @@ function Loader() {
export default function Overview() {
const intl = useIntl();
const navigate = useNavigate();

const queryClient = useQueryClient();
const { isPending, isSuccess, data } = useQuery({
queryKey: ["contacts"],
queryFn: getContacts,
});
}, queryClient);

const {
data: searchData,
isPending: isSearchPending,
isSuccess: isSearchSuccess,
mutate
} = useMutation({
mutationFn: () => searchContacts({
filter: {
search_term: searchTerm,
types: typeFilters.length === 0 ? undefined : typeFilters,
}
})
}, queryClient);

const [values, setValues] = useState<Contact[]>(data?.contacts || []);
const [filteredResults, setFilteredResults] = useState<Contact[]>(values);
const [typeFilters, setTypeFilters] = useState<Contact['type'][]>([]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [search, setSearch] = useState<string>();
const [searchTerm, setSearchTerm] = useState("");

const goToCreate = () => {
navigate(routes.CREATE_CONTACT);
Expand All @@ -72,6 +85,12 @@ export default function Overview() {
}
}, [isSuccess, data]);

useEffect(() => {
if (isSearchSuccess) {
setFilteredResults(searchData.contacts);
}
}, [isSearchSuccess, searchData]);

useEffect(() => {
setFilteredResults(typeFilters.length === 0 ? values : values.filter(value => typeFilters.includes(value.type)));
}, [values, typeFilters]);
Expand All @@ -98,8 +117,10 @@ export default function Overview() {
defaultMessage: "Name, address, email...",
description: "Placeholder text for contacts search input",
})}
onChange={setSearch}
onSearch={() => {}}
onChange={setSearchTerm}
onSearch={() => {
mutate();
}}
/>
<TypeFilter values={typeFilters} onChange={setTypeFilters} multiple />
</div>
Expand Down Expand Up @@ -128,7 +149,7 @@ export default function Overview() {
<Separator className="bg-divider-75" />
</div>

{isPending ? (<>
{isPending || isSearchPending ? (<>
<Loader />
</>) : (<>
{values.length === 0 ? (<>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/contacts/components/TypeFilter.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const meta = {
title: 'Element/Contacts/TypeFilter',
component: TypeFilter,
args: {
value: ContactTypes.Company,
values: [ContactTypes.Company, ContactTypes.Person],
onChange: fn(),
}
} satisfies Meta<typeof TypeFilter>;
Expand Down
31 changes: 30 additions & 1 deletion src/services/contact.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GET_CONTACTS } from "@/constants/endpoints";
import { GET_CONTACTS, SEARCH_CONTACTS } from "@/constants/endpoints";
import type { Contact } from "@/types/contact";

type ContactsResponse = {
Expand All @@ -14,3 +14,32 @@ export const getContacts = async () => {

return response.json() as Promise<ContactsResponse>;
};

type SearchContactsPayload = {
filter: {
search_term?: string;
types?: Contact['type'][];
};
};

type SearchContactsResponse = {
contacts: Contact[];
};

export const searchContacts = async (payload: SearchContactsPayload) => {
const response = await fetch(SEARCH_CONTACTS, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});

if (!response.ok) {
throw new Error(`Failed to search contacts: ${response.statusText}`);
}

const data = (await response.json()) as Promise<SearchContactsResponse>;

return data;
};

0 comments on commit 98d5d7a

Please sign in to comment.