diff --git a/components/Application/Guilds/Guild/Guild.tsx b/components/Application/Guilds/Guild/Guild.tsx index 321704b..58166d4 100644 --- a/components/Application/Guilds/Guild/Guild.tsx +++ b/components/Application/Guilds/Guild/Guild.tsx @@ -13,6 +13,7 @@ export const Guild: React.FC = (props) => { return ( = (props) => { const { path } = props - - const [guilds, setGuilds] = useState([]) - const [hasMore, setHasMore] = useState(true) - const [fetchState, setFetchState] = useFetchState('idle') - const afterId = useRef(null) - const { authentication } = useAuthentication() - const fetchGuilds = useCallback(async (): Promise => { - if (fetchState !== 'idle') { - return + const { items, hasMore, nextPage } = usePagination( + { + api: authentication.api, + url: '/guilds' } - setFetchState('loading') - const { data } = await authentication.api.get( - `/guilds?limit=20${ - afterId.current != null ? `&after=${afterId.current}` : '' - }` - ) - afterId.current = data.length > 0 ? data[data.length - 1].id : null - setGuilds((oldGuilds) => { - return [...oldGuilds, ...data] - }) - setHasMore(data.length > 0) - setFetchState('idle') - }, [authentication, fetchState, setFetchState]) + ) useEffect(() => { - fetchGuilds().catch((error) => { - console.error(error) - }) - }, []) // eslint-disable-line react-hooks/exhaustive-deps + nextPage() + }, [nextPage]) return (
= (props) => { > } > - {guilds.map((guild) => { + {items.map((guild) => { return ( { - const [guilds, setGuilds] = useState([]) - const [hasMore, setHasMore] = useState(true) - const [inputSearch, setInputSearch] = useState('') - const [fetchState, setFetchState] = useFetchState('idle') - const afterId = useRef(null) - + const [search, setSearch] = useState('') const { authentication } = useAuthentication() - const fetchGuilds = useCallback(async (): Promise => { - if (fetchState !== 'idle') { - return - } - setFetchState('loading') - const { data } = await authentication.api.get( - `/guilds/public?limit=20&search=${inputSearch}${ - afterId.current != null ? `&after=${afterId.current}` : '' - }` - ) - afterId.current = data.length > 0 ? data[data.length - 1].id : null - setGuilds((oldGuilds) => { - return [...oldGuilds, ...data] + const { items, hasMore, nextPage, resetPagination } = + usePagination({ + api: authentication.api, + url: '/guilds/public' }) - setHasMore(data.length > 0) - setFetchState('idle') - }, [authentication, fetchState, setFetchState, inputSearch]) useEffect(() => { - afterId.current = null - setGuilds([]) - fetchGuilds().catch((error) => { - console.error(error) - }) - }, [inputSearch]) // eslint-disable-line react-hooks/exhaustive-deps + resetPagination() + nextPage({ search }) + }, [resetPagination, nextPage, search]) const handleChange = (event: React.ChangeEvent): void => { - setInputSearch(event.target.value) + setSearch(event.target.value) } return ( @@ -59,13 +39,13 @@ export const JoinGuildsPublic: React.FC = () => {
} > - {guilds.map((guild) => { + {items.map((guild) => { return })} diff --git a/components/design/IconLink/IconLink.tsx b/components/design/IconLink/IconLink.tsx index a825a80..bbab736 100644 --- a/components/design/IconLink/IconLink.tsx +++ b/components/design/IconLink/IconLink.tsx @@ -5,13 +5,14 @@ export interface IconLinkProps { selected?: boolean href: string title?: string + className?: string } export const IconLink: React.FC = (props) => { - const { children, selected, href, title } = props + const { children, selected, href, title, className } = props return ( -
+
{children} diff --git a/cypress/integration/common/application/authentication.spec.ts b/cypress/integration/common/application/authentication.spec.ts new file mode 100644 index 0000000..3673c17 --- /dev/null +++ b/cypress/integration/common/application/authentication.spec.ts @@ -0,0 +1,36 @@ +import { getGuildsHandler } from '../../../fixtures/guilds/get' +import { authenticationHandlers } from '../../../fixtures/handler' + +const applicationPaths = [ + '/application', + '/application/users/0', + '/application/guilds/create', + '/application/guilds/join', + '/application/0/0' +] + +describe('Common > application/authentication', () => { + beforeEach(() => { + cy.task('stopMockServer') + }) + + it('should redirect the user to `/authentication/signin` if not signed in', () => { + for (const applicationPath of applicationPaths) { + cy.visit(applicationPath) + .location('pathname') + .should('eq', '/authentication/signin') + } + }) + + it('should not redirect the user if signed in', () => { + cy.task('startMockServer', [ + ...authenticationHandlers, + getGuildsHandler + ]).setCookie('refreshToken', 'refresh-token') + for (const applicationPath of applicationPaths) { + cy.visit(applicationPath) + .location('pathname') + .should('eq', applicationPath) + } + }) +}) diff --git a/cypress/integration/common/application/left-sidebar.spec.ts b/cypress/integration/common/application/left-sidebar.spec.ts new file mode 100644 index 0000000..55caf91 --- /dev/null +++ b/cypress/integration/common/application/left-sidebar.spec.ts @@ -0,0 +1,17 @@ +import { getGuildsHandler } from '../../../fixtures/guilds/get' +import { authenticationHandlers } from '../../../fixtures/handler' + +describe('Common > application/left-sidebar', () => { + beforeEach(() => { + cy.task('stopMockServer') + }) + + it('should shows all the guilds of the current user in left sidebar', () => { + cy.task('startMockServer', [ + ...authenticationHandlers, + getGuildsHandler + ]).setCookie('refreshToken', 'refresh-token') + cy.visit('/application') + cy.get('.guilds-list').children().should('have.length', 2) + }) +}) diff --git a/cypress/integration/pages/application/index.spec.ts b/cypress/integration/pages/application/index.spec.ts index 983ba81..a4b345b 100644 --- a/cypress/integration/pages/application/index.spec.ts +++ b/cypress/integration/pages/application/index.spec.ts @@ -1,45 +1,31 @@ -import { getGuildsHandler } from '../../../fixtures/guilds/get' import { authenticationHandlers } from '../../../fixtures/handler' -const applicationPaths = [ - '/application', - '/application/users/0', - '/application/guilds/create', - '/application/guilds/join', - '/application/0/0' -] - describe('Pages > /application', () => { beforeEach(() => { cy.task('stopMockServer') }) - it('should redirect the user to `/authentication/signin` if not signed in', () => { - for (const applicationPath of applicationPaths) { - cy.visit(applicationPath) - .location('pathname') - .should('eq', '/authentication/signin') - } - }) - - it('should not redirect the user if signed in', () => { - cy.task('startMockServer', [ - ...authenticationHandlers, - getGuildsHandler - ]).setCookie('refreshToken', 'refresh-token') - for (const applicationPath of applicationPaths) { - cy.visit(applicationPath) - .location('pathname') - .should('eq', applicationPath) - } - }) - - it('should shows all the guilds of the current user in left sidebar', () => { - cy.task('startMockServer', [ - ...authenticationHandlers, - getGuildsHandler - ]).setCookie('refreshToken', 'refresh-token') + it('should redirect user to `/application/guilds/create` on click on "Create a Guild"', () => { + cy.task('startMockServer', [...authenticationHandlers]).setCookie( + 'refreshToken', + 'refresh-token' + ) cy.visit('/application') - cy.get('.guilds-list').children().should('have.length', 2) + cy.get('a[href="/application/guilds/create"]') + .click() + .location('pathname') + .should('eq', '/application/guilds/create') + }) + + it('should redirect user to `/application/guilds/join` on click on "Join a Guild"', () => { + cy.task('startMockServer', [...authenticationHandlers]).setCookie( + 'refreshToken', + 'refresh-token' + ) + cy.visit('/application') + cy.get('a[href="/application/guilds/join"]') + .click() + .location('pathname') + .should('eq', '/application/guilds/join') }) }) diff --git a/hooks/usePagination.ts b/hooks/usePagination.ts new file mode 100644 index 0000000..ea9c178 --- /dev/null +++ b/hooks/usePagination.ts @@ -0,0 +1,73 @@ +import { useState, useRef, useCallback } from 'react' +import { AxiosInstance } from 'axios' +import { FetchState } from './useFetchState' + +export interface Query { + [key: string]: string +} +export type NextPageAsync = (query?: Query) => Promise +export type NextPage = (query?: Query) => void + +export interface UsePaginationOptions { + api: AxiosInstance + url: string +} + +export interface UsePaginationResult { + items: T[] + nextPage: NextPage + resetPagination: () => void + hasMore: boolean +} + +export const usePagination = ( + options: UsePaginationOptions +): UsePaginationResult => { + const { api, url } = options + + const [items, setItems] = useState([]) + const [hasMore, setHasMore] = useState(true) + const fetchState = useRef('idle') + const afterId = useRef(null) + + const nextPageAsync: NextPageAsync = useCallback( + async (query) => { + if (fetchState.current !== 'idle') { + return + } + fetchState.current = 'loading' + const searchParameters = new URLSearchParams(query) + searchParameters.append('limit', '20') + if (afterId.current != null) { + searchParameters.append('after', afterId.current.toString()) + } + const { data: newItems } = await api.get( + `${url}?${searchParameters.toString()}` + ) + afterId.current = + newItems.length > 0 ? newItems[newItems.length - 1].id : null + setItems((oldItems) => { + return [...oldItems, ...newItems] + }) + setHasMore(newItems.length > 0) + fetchState.current = 'idle' + }, + [api, url] + ) + + const nextPage: NextPage = useCallback( + (query) => { + nextPageAsync(query).catch((error) => { + console.error(error) + }) + }, + [nextPageAsync] + ) + + const resetPagination = useCallback((): void => { + afterId.current = null + setItems([]) + }, []) + + return { items, hasMore, nextPage, resetPagination } +} diff --git a/pages/application/index.tsx b/pages/application/index.tsx index 3f6c52c..019245c 100644 --- a/pages/application/index.tsx +++ b/pages/application/index.tsx @@ -1,6 +1,6 @@ import { Head } from 'components/Head' import { Application } from 'components/Application' -import { PopupGuild } from 'components/Application/PopupGuild/PopupGuild.stories' +import { PopupGuild } from 'components/Application/PopupGuild' import { authenticationFromServerSide, AuthenticationProvider,