diff --git a/__tests__/users/App.test.js b/__tests__/users/App.test.js index 54cbbeeb..70fc431e 100644 --- a/__tests__/users/App.test.js +++ b/__tests__/users/App.test.js @@ -72,6 +72,106 @@ describe('App Component', () => { afterAll(async () => { await browser.close(); }); + it('should fetch and append new users on subsequent pages for discord users tab when feature flag is on', async () => { + await page.goto(`${BASE_URL}/users/discord/?tab=in_discord&dev=true`); + await page.waitForNetworkIdle(); + + const initialUserCardTestIds = await page.$$eval( + '[data-testid^="user-card-"]', + (cards) => cards.map((card) => card.getAttribute('data-testid')), + ); + expect(initialUserCardTestIds.length).toBeLessThanOrEqual(10); + expect(initialUserCardTestIds.length).toBeGreaterThan(0); + + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + await page.waitForNetworkIdle(); + + const updatedUserCardTestIds = await page.$$eval( + '[data-testid^="user-card-"]', + (cards) => cards.map((card) => card.getAttribute('data-testid')), + ); + expect(updatedUserCardTestIds.length).toBeLessThanOrEqual(20); + expect(updatedUserCardTestIds.length).toBeGreaterThanOrEqual( + initialUserCardTestIds.length, + ); + }); + it('should fetch and append new users on subsequent pages for discord users tab', async () => { + await page.goto(`${BASE_URL}/users/discord/?tab=in_discord`); + await page.waitForNetworkIdle(); + + const initialUserCardTestIds = await page.$$eval( + '[data-testid^="user-card-"]', + (cards) => cards.map((card) => card.getAttribute('data-testid')), + ); + expect(initialUserCardTestIds.length).toBeLessThanOrEqual(10); + expect(initialUserCardTestIds.length).toBeGreaterThan(0); + + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + await page.waitForNetworkIdle(); + + const updatedUserCardTestIds = await page.$$eval( + '[data-testid^="user-card-"]', + (cards) => cards.map((card) => card.getAttribute('data-testid')), + ); + expect(updatedUserCardTestIds.length).toBeLessThanOrEqual(20); + expect(updatedUserCardTestIds.length).toBeGreaterThanOrEqual( + initialUserCardTestIds.length, + ); + }); + it('should fetch and append new users on subsequent pages for verified users tab when feature flag is on', async () => { + await page.goto(`${BASE_URL}/users/discord/?tab=verified&dev=true`); + await page.waitForNetworkIdle(); + + const initialUserCardTestIds = await page.$$eval( + '[data-testid^="user-card-"]', + (cards) => cards.map((card) => card.getAttribute('data-testid')), + ); + expect(initialUserCardTestIds.length).toBeLessThanOrEqual(10); + expect(initialUserCardTestIds.length).toBeGreaterThan(0); + + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + await page.waitForNetworkIdle(); + + const updatedUserCardTestIds = await page.$$eval( + '[data-testid^="user-card-"]', + (cards) => cards.map((card) => card.getAttribute('data-testid')), + ); + expect(updatedUserCardTestIds.length).toBeLessThanOrEqual(20); + expect(updatedUserCardTestIds.length).toBeGreaterThanOrEqual( + initialUserCardTestIds.length, + ); + }); + it('should fetch and append new users on subsequent pages for verified users tab', async () => { + await page.goto(`${BASE_URL}/users/discord/?tab=verified`); + await page.waitForNetworkIdle(); + + const initialUserCardTestIds = await page.$$eval( + '[data-testid^="user-card-"]', + (cards) => cards.map((card) => card.getAttribute('data-testid')), + ); + expect(initialUserCardTestIds.length).toBeLessThanOrEqual(10); + expect(initialUserCardTestIds.length).toBeGreaterThan(0); + + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + await page.waitForNetworkIdle(); + + const updatedUserCardTestIds = await page.$$eval( + '[data-testid^="user-card-"]', + (cards) => cards.map((card) => card.getAttribute('data-testid')), + ); + expect(updatedUserCardTestIds.length).toBeLessThanOrEqual(20); + expect(updatedUserCardTestIds.length).toBeGreaterThanOrEqual( + initialUserCardTestIds.length, + ); + }); it('should render all sections', async () => { await page.waitForSelector('.tabs_section'); diff --git a/users/discord/App.js b/users/discord/App.js index d1b57e0e..61180816 100644 --- a/users/discord/App.js +++ b/users/discord/App.js @@ -15,11 +15,51 @@ export const usersData = { verified: null, }; const urlParams = new URLSearchParams(window.location.search); +const isDev = urlParams.get('dev') === 'true'; let activeTab = urlParams.get('tab') ?? 'in_discord'; - +const INITIAL_USERS = 10; +let isLoading = false; +let currentPage = 1; let showUser = 0; -usersData[activeTab] = await getUsers(activeTab); + +if (!isDev) { + usersData[activeTab] = await getUsers(activeTab); +} + +export const paginateFetchedUsers = async (tabId, page = 1) => { + if (isLoading) { + return; + } + usersData[activeTab] = await getUsers(activeTab); + + isLoading = true; + + try { + const start = (page - 1) * INITIAL_USERS; + const end = start + INITIAL_USERS; + + const newUsers = usersData[tabId].slice(start, end); + + if (newUsers.length > 0) { + if (page === 1) { + usersData[tabId] = newUsers; // Initial load + } else { + const existingIds = new Set(usersData[tabId].map((user) => user.id)); + const uniqueNewUsers = newUsers.filter( + (user) => !existingIds.has(user.id), + ); + usersData[tabId] = [...usersData[tabId], ...uniqueNewUsers]; + } + currentPage = page; + } + } catch (error) { + console.error('Error fetching users', error); + } finally { + isLoading = false; + rerender(App(), document.getElementById('root')); + } +}; const handleTabNavigation = async (e) => { const selectedTabId = e.target.getAttribute('data_key'); @@ -60,6 +100,10 @@ export const App = () => { users, showUser, handleUserSelected, + paginateFetchedUsers, + activeTab, + currentPage, + isLoading, }), UserDetailsSection({ user: users[showUser] ?? {} }), ]); @@ -69,3 +113,6 @@ export const App = () => { NoUserFound(), ]); }; +if (isDev) { + paginateFetchedUsers(activeTab, 1); +} diff --git a/users/discord/components/LoadingSpinner.js b/users/discord/components/LoadingSpinner.js new file mode 100644 index 00000000..ea8d84f5 --- /dev/null +++ b/users/discord/components/LoadingSpinner.js @@ -0,0 +1,7 @@ +const { createElement } = react; + +export const LoadingSpinner = () => { + return createElement('aside', { class: 'users_section' }, [ + createElement('div', { class: 'loading' }, ['Loading...']), + ]); +}; diff --git a/users/discord/components/UsersSection.js b/users/discord/components/UsersSection.js index 51773f79..e6462c12 100644 --- a/users/discord/components/UsersSection.js +++ b/users/discord/components/UsersSection.js @@ -1,11 +1,32 @@ const { createElement } = react; +import { LoadingSpinner } from './LoadingSpinner.js'; + +export const UsersSection = ({ + users, + showUser, + handleUserSelected, + paginateFetchedUsers, + activeTab, + currentPage, + isLoading, +}) => { + window.addEventListener( + 'scroll', + debounce(() => { + if (window.innerHeight + window.scrollY >= document.body.offsetHeight) { + paginateFetchedUsers(activeTab, currentPage + 1); + } + }, 200), + ); + + if (isLoading) { + return LoadingSpinner(); + } -export const UsersSection = ({ users, showUser, handleUserSelected }) => { return createElement( 'aside', { class: 'users_section', - 'data-testid': 'users-section', }, users?.map((user) => { diff --git a/users/discord/style.css b/users/discord/style.css index 23058547..f71d7a51 100644 --- a/users/discord/style.css +++ b/users/discord/style.css @@ -19,8 +19,9 @@ main { .users_section { grid-area: aside; - overflow: scroll; + overflow: auto; padding-inline: 1rem; + max-height: 80vh; } .tabs_section {