diff --git a/__tests__/users/App.test.js b/__tests__/users/App.test.js index b612408f..a5c4f33a 100644 --- a/__tests__/users/App.test.js +++ b/__tests__/users/App.test.js @@ -89,4 +89,62 @@ describe('App Component', () => { const url = await page.url(); expect(url).toContain('?tab=verified'); }); + + it('should update the URL query string on search', async () => { + const initialUrl = await page.url(); + + await page.waitForSelector('.search_field'); + + await page.type('.search_field', 'John Doe'); + await page.click('.search_button'); + + const updatedUrl = await page.url(); + + expect(updatedUrl).toContain('search=John+Doe'); + }); + + it('should display user details when a user card is clicked', async () => { + await page.waitForSelector('.active_tab'); + + await page.click('.active_tab'); + + await page.waitForSelector('.user_details_section'); + + const userDetailsDisplayed = + (await page.$('.user_details_section')) !== null; + + expect(userDetailsDisplayed).toBeTruthy(); + }); + + it('should display search results matching the search term', async () => { + await page.type('.search_field', 'shu'); + await page.click('.search_button'); + + await page.waitForSelector('.user_card'); + + const userCards = await page.$$('.user_card'); + let searchTermFound = false; + + for (const card of userCards) { + const cardContent = await card.evaluate((node) => node.innerText); + if (cardContent.toLowerCase().includes('shubham')) { + searchTermFound = true; + break; + } + } + + expect(searchTermFound).toBeTruthy(); + }); + + it('should handle empty search results gracefully', async () => { + await page.type('.search_field', 'bdhsbhj'); //represents a string which won't yeild any search result + await page.click('.search_button'); + + await page.waitForSelector('.no_user_found'); + + const emptyResultsMessageDisplayed = + (await page.$('.no_user_found')) !== null; + + expect(emptyResultsMessageDisplayed).toBeTruthy(); + }); }); diff --git a/users/discord/App.js b/users/discord/App.js index 87fa873d..a1a72138 100644 --- a/users/discord/App.js +++ b/users/discord/App.js @@ -1,8 +1,9 @@ import { TabsSection } from './components/TabsSection.js'; import { UsersSection } from './components/UsersSection.js'; import { UserDetailsSection } from './components/UserDetailsSection.js'; -import { getUsers } from './utils/util.js'; +import { getUsers, searchUser } from './utils/util.js'; import { NoUserFound } from './components/NoUserFound.js'; +import { SearchField } from './components/SearchField.js'; const { createElement, rerender } = react; @@ -37,35 +38,61 @@ const handleTabNavigation = async (e) => { } }; +let users = usersData[activeTab] ?? []; + +let searchTerm = urlParams.get('search') ?? ''; + +if (searchTerm) { + users = await searchUser(searchTerm); +} + const handleUserSelected = (e) => { const selectedUserId = e.target?.getAttribute('data_key') || e.target.parentElement?.getAttribute('data_key'); if (selectedUserId) { - showUser = usersData[activeTab]?.findIndex( - (user) => user.id === selectedUserId, - ); + showUser = users?.findIndex((user) => user.id === selectedUserId); rerender(App(), window['root']); } }; -export const App = () => { - const users = usersData[activeTab] ?? []; +const handleSearchChange = (newSearchTerm) => { + if (newSearchTerm) { + searchTerm = newSearchTerm; + const searchParams = new URLSearchParams(window.location.search); + searchParams.set('search', searchTerm); + document.location.search = searchParams.toString(); + } +}; - if (users.length) +export const App = () => { + if (users.length) { return createElement('main', {}, [ + SearchField({ + onSearchChange: handleSearchChange, + initialValue: searchTerm, + }), TabsSection({ tabs, activeTab, handleTabNavigation }), UsersSection({ - users, + users: users, showUser, handleUserSelected, }), - UserDetailsSection({ user: users[showUser] ?? {} }), + users.length > 0 + ? UserDetailsSection({ user: users[showUser] ?? {} }) + : null, ]); + } return createElement('main', {}, [ TabsSection({ tabs, activeTab, handleTabNavigation }), - NoUserFound(), + createElement('div', { style: { display: 'flex' } }, [ + SearchField({ + onSearchChange: handleSearchChange, + initialValue: searchTerm, + }), + NoUserFound(), + ]), ]); }; diff --git a/users/discord/components/SearchField.js b/users/discord/components/SearchField.js new file mode 100644 index 00000000..6af01b95 --- /dev/null +++ b/users/discord/components/SearchField.js @@ -0,0 +1,21 @@ +const { createElement } = react; + +export const SearchField = ({ onSearchChange, initialValue }) => + createElement('section', { class: 'search_section' }, [ + createElement('input', { + class: 'search_field', + type: 'text', + placeholder: 'Search users...', + value: initialValue, + }), + createElement( + 'button', + { + class: 'search_button', + onclick: () => { + onSearchChange(document.querySelector('.search_field').value); + }, + }, + ['Search'], + ), + ]); diff --git a/users/discord/components/UsersSection.js b/users/discord/components/UsersSection.js index bc07cdaa..ac999fd2 100644 --- a/users/discord/components/UsersSection.js +++ b/users/discord/components/UsersSection.js @@ -1,6 +1,12 @@ const { createElement } = react; export const UsersSection = ({ users, showUser, handleUserSelected }) => { + if (users.length === 0) { + return createElement('div', { class: 'users_section no_users' }, [ + createElement('div', {}, ['No users found']), + ]); + } + return createElement( 'aside', { class: 'users_section', onclick: handleUserSelected }, diff --git a/users/discord/style.css b/users/discord/style.css index 6b402f6d..91a53299 100644 --- a/users/discord/style.css +++ b/users/discord/style.css @@ -11,9 +11,10 @@ main { display: grid; grid-template-columns: minmax(10rem, 20rem) 1fr; - grid-template-rows: auto 1fr; + grid-template-rows: auto auto 1fr; grid-template-areas: 'tab tab ' + 'search search' 'aside details'; height: 100vh; } @@ -74,3 +75,23 @@ main { font-size: larger; padding: 2rem; } + +.users_section.no_users { + padding: 1rem; + margin: 1rem 2rem; +} + +.search_section { + display: flex; +} +.search_field { + width: 14rem; + margin: 0.5rem 1rem; + padding: 0.5rem; + font-size: 1rem; +} + +.search_button { + margin: 0.5rem 0rem; + width: 4rem; +} diff --git a/users/discord/utils/util.js b/users/discord/utils/util.js index 83cd40e5..d6d6e07d 100644 --- a/users/discord/utils/util.js +++ b/users/discord/utils/util.js @@ -19,3 +19,23 @@ export const getUsers = async (tab) => { console.error(err); } }; + +export const searchUser = async (searchTerm) => { + let URL = `${API_BASE_URL}/users?search=${searchTerm}&dev=true`; // dev=true is a temporary query param + + try { + const response = await fetch(URL, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-type': 'application/json', + }, + }); + + const data = await response.json(); + return data.users ?? []; + } catch (err) { + console.error(err); + return []; + } +};