From 6a54acd89f1d1b6d8a68ee4e899c4c4ad40615a4 Mon Sep 17 00:00:00 2001 From: Oscar Cortes Date: Mon, 23 Oct 2023 00:01:19 +0200 Subject: [PATCH 01/18] Package.json improvements --- phone-test/package.json | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/phone-test/package.json b/phone-test/package.json index 0f844cf..34eafd2 100644 --- a/phone-test/package.json +++ b/phone-test/package.json @@ -5,6 +5,16 @@ "dependencies": { "@aircall/tractor": "2.0.0-next.13", "@apollo/client": "^3.7.1", + "date-fns": "^2.29.3", + "graphql": "^16.6.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.4.3", + "react-scripts": "5.0.1", + "styled-components": "^5.3.6", + "web-vitals": "^2.1.0" + }, + "devDependencies": { "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.2.1", @@ -13,18 +23,10 @@ "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/styled-components": "^5.1.26", - "date-fns": "^2.29.3", - "graphql": "^16.6.0", + "typescript": "^4.4.2", "husky": "^8.0.2", - "lint-staged": "^13.0.3", "prettier": "^2.7.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.4.3", - "react-scripts": "5.0.1", - "styled-components": "^5.3.6", - "typescript": "^4.4.2", - "web-vitals": "^2.1.0" + "lint-staged": "^13.0.3" }, "scripts": { "start": "react-scripts start", @@ -60,4 +62,4 @@ "prettier --write" ] } -} +} \ No newline at end of file From e669a98d3d1184ceff5499669dc8eb3ae442fc28 Mon Sep 17 00:00:00 2001 From: Oscar Cortes Date: Mon, 23 Oct 2023 00:19:43 +0200 Subject: [PATCH 02/18] Add 404 page for not existing routes --- phone-test/src/App.tsx | 3 ++- .../src/components/Error/ErrorBoundary.tsx | 19 +++++++++++++++ .../src/components/NotFoundPage/NotFound.tsx | 18 +++++++++++++++ .../components/NotFoundPage/notFound.style.ts | 23 +++++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 phone-test/src/components/Error/ErrorBoundary.tsx create mode 100644 phone-test/src/components/NotFoundPage/NotFound.tsx create mode 100644 phone-test/src/components/NotFoundPage/notFound.style.ts diff --git a/phone-test/src/App.tsx b/phone-test/src/App.tsx index 6619726..2df0059 100644 --- a/phone-test/src/App.tsx +++ b/phone-test/src/App.tsx @@ -12,6 +12,7 @@ import { GlobalAppStyle } from './style/global'; import { ApolloClient, InMemoryCache, ApolloProvider, createHttpLink } from '@apollo/client'; import { setContext } from '@apollo/client/link/context'; import { AuthProvider } from './hooks/useAuth'; +import { ErrorBoundary } from './components/Error/ErrorBoundary'; const httpLink = createHttpLink({ uri: 'https://frontend-test-api.aircall.dev/graphql' @@ -38,7 +39,7 @@ const client = new ApolloClient({ export const router = createBrowserRouter( createRoutesFromElements( - }> + } errorElement={}> } /> }> } /> diff --git a/phone-test/src/components/Error/ErrorBoundary.tsx b/phone-test/src/components/Error/ErrorBoundary.tsx new file mode 100644 index 0000000..d08069c --- /dev/null +++ b/phone-test/src/components/Error/ErrorBoundary.tsx @@ -0,0 +1,19 @@ +import { isRouteErrorResponse, useRouteError } from 'react-router-dom'; +import { NotFoundPage } from '../NotFoundPage/NotFound'; + +export const ErrorBoundary = () => { + const error = useRouteError(); + let errorMessage: string; + + if (isRouteErrorResponse(error)) { + console.log(error); + if (error.status === 404) return ; + errorMessage = error.statusText; + } else if (error instanceof Error) { + errorMessage = error.message; + } else { + errorMessage = 'Unknown error'; + } + + return
{errorMessage}
; +}; diff --git a/phone-test/src/components/NotFoundPage/NotFound.tsx b/phone-test/src/components/NotFoundPage/NotFound.tsx new file mode 100644 index 0000000..042040c --- /dev/null +++ b/phone-test/src/components/NotFoundPage/NotFound.tsx @@ -0,0 +1,18 @@ + import { Flex } from '@aircall/tractor'; + import { ErrorImage, ErrorText, ErrorTitle } from './notFound.style'; + import { Link } from 'react-router-dom'; + + export const NotFoundPage = () => { + return ( + + + Oops! Page not found + The page you are looking for is unavailable. + Go to calls page + + ); + }; + \ No newline at end of file diff --git a/phone-test/src/components/NotFoundPage/notFound.style.ts b/phone-test/src/components/NotFoundPage/notFound.style.ts new file mode 100644 index 0000000..61be661 --- /dev/null +++ b/phone-test/src/components/NotFoundPage/notFound.style.ts @@ -0,0 +1,23 @@ +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; + +export const ErrorTitle = styled.h1` + font-size: '3rem'; + margin-bottom: '1rem'; +`; + +export const ErrorText = styled.p` + font-size: '1.5rem'; + text-align: 'center' + max-width: '50ch'; +`; + +export const ErrorImage = styled.img` + max-width: '400px'; + margin-bottom: '2rem'; +`; + +export const ErrorLink = styled(Link)` +font-size: '1.5rem'; +text-decoration: 'underline'; +`; \ No newline at end of file From f0defa2bd13c35ab91e4a76811b67bcb32ca8c94 Mon Sep 17 00:00:00 2001 From: Oscar Cortes Date: Mon, 23 Oct 2023 00:51:00 +0200 Subject: [PATCH 03/18] Fix pagination when change items number UI --- phone-test/src/pages/CallsList.tsx | 32 ++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/phone-test/src/pages/CallsList.tsx b/phone-test/src/pages/CallsList.tsx index 8466c43..eefe303 100644 --- a/phone-test/src/pages/CallsList.tsx +++ b/phone-test/src/pages/CallsList.tsx @@ -23,13 +23,15 @@ export const PaginationWrapper = styled.div` } `; -const CALLS_PER_PAGE = 5; +const CALLS_PER_PAGE = 25; export const CallsListPage = () => { - const [search] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); - const pageQueryParams = search.get('page'); + const pageQueryParams = searchParams.get('page'); + const callsPerPage = parseInt(searchParams.get('cpe') || CALLS_PER_PAGE.toString()); const activePage = !!pageQueryParams ? parseInt(pageQueryParams) : 1; + const { loading, error, data } = useQuery(PAGINATED_CALLS, { variables: { offset: (activePage - 1) * CALLS_PER_PAGE, @@ -42,14 +44,30 @@ export const CallsListPage = () => { if (error) return

ERROR

; if (!data) return

Not found

; - const { totalCount, nodes: calls } = data.paginatedCalls; + const { totalCount, nodes: calls } = data?.paginatedCalls; const handleCallOnClick = (callId: string) => { navigate(`/calls/${callId}`); }; const handlePageChange = (page: number) => { - navigate(`/calls/?page=${page}`); + console.log(page, Math.ceil(totalCount / callsPerPage), totalCount, callsPerPage) + if (page === activePage || page < 1 || page > Math.ceil(totalCount / callsPerPage)) return; + setSearchParams(params => { + params.set('page', page.toString()); + return params; + }); + + navigate(`/calls/?${searchParams.toString()}`); + }; + + const handlePageSizeChange = (pageSize: number) => { + setSearchParams(params => { + params.set('page', '1'); + if (pageSize === CALLS_PER_PAGE || !pageSize) params.delete('cpe'); + else params.set('cpe', pageSize.toString()); + return params; + }); }; return ( @@ -114,8 +132,10 @@ export const CallsListPage = () => { From a218713da0f1fbd602bf9fbb4ae9c997cd327a48 Mon Sep 17 00:00:00 2001 From: Oscar Cortes Date: Mon, 23 Oct 2023 01:42:23 +0200 Subject: [PATCH 04/18] Add Protected route and layout --- phone-test/src/App.tsx | 3 ++- phone-test/src/components/routing/ProtectedLayout.tsx | 2 +- phone-test/src/components/routing/ProtectedRoute.tsx | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/phone-test/src/App.tsx b/phone-test/src/App.tsx index 2df0059..c858566 100644 --- a/phone-test/src/App.tsx +++ b/phone-test/src/App.tsx @@ -13,6 +13,7 @@ import { ApolloClient, InMemoryCache, ApolloProvider, createHttpLink } from '@ap import { setContext } from '@apollo/client/link/context'; import { AuthProvider } from './hooks/useAuth'; import { ErrorBoundary } from './components/Error/ErrorBoundary'; +import { ProtectedRoute } from './components/routing/ProtectedRoute'; const httpLink = createHttpLink({ uri: 'https://frontend-test-api.aircall.dev/graphql' @@ -41,7 +42,7 @@ export const router = createBrowserRouter( createRoutesFromElements( } errorElement={}> } /> - }> + }> } /> } /> diff --git a/phone-test/src/components/routing/ProtectedLayout.tsx b/phone-test/src/components/routing/ProtectedLayout.tsx index 55f8193..6d560ea 100644 --- a/phone-test/src/components/routing/ProtectedLayout.tsx +++ b/phone-test/src/components/routing/ProtectedLayout.tsx @@ -4,7 +4,7 @@ import logo from '../../logo.png'; export const ProtectedLayout = () => { return ( - + Aircall diff --git a/phone-test/src/components/routing/ProtectedRoute.tsx b/phone-test/src/components/routing/ProtectedRoute.tsx index 6e410c9..2beb677 100644 --- a/phone-test/src/components/routing/ProtectedRoute.tsx +++ b/phone-test/src/components/routing/ProtectedRoute.tsx @@ -1,4 +1,6 @@ + export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { // TODO check that the user is authenticated before displaying the route + return <>{children}; }; From d5e6ed6eeff57395e75df33ad782b98b8f511bcb Mon Sep 17 00:00:00 2001 From: Oscar Cortes Date: Mon, 23 Oct 2023 01:44:09 +0200 Subject: [PATCH 05/18] Create componente CallListItem --- .../calls/CallListItem/CallListItem.tsx | 81 +++++++++++++++++++ phone-test/src/pages/CallsList.tsx | 76 +++-------------- 2 files changed, 93 insertions(+), 64 deletions(-) create mode 100644 phone-test/src/components/calls/CallListItem/CallListItem.tsx diff --git a/phone-test/src/components/calls/CallListItem/CallListItem.tsx b/phone-test/src/components/calls/CallListItem/CallListItem.tsx new file mode 100644 index 0000000..6596234 --- /dev/null +++ b/phone-test/src/components/calls/CallListItem/CallListItem.tsx @@ -0,0 +1,81 @@ +import { + Box, + DiagonalDownOutlined, + DiagonalUpOutlined, + Grid, + Icon, + Typography +} from '@aircall/tractor'; +import { formatDate, formatDuration } from '../../../helpers/dates'; +import { useNavigate } from 'react-router-dom'; + +interface CallListItemProps { + call: Call; +} + +const CallListItem = ({ call }: CallListItemProps) => { + const navigate = useNavigate(); + + const icon = call.direction === 'inbound' ? DiagonalDownOutlined : DiagonalUpOutlined; + const callTypeTitles: Record = { + missed: 'Missed call', + answered: 'Call answered', + voicemail: 'Voicemail' + }; + const title = callTypeTitles[call.call_type] || 'Unknown call type'; + const callTypesColors: Record = { + missed: 'red-500', + answered: 'green-500', + voicemail: 'blue-500' + }; + const color = callTypesColors[call.call_type] || 'neutral-600'; + const subtitle = call.direction === 'inbound' ? `from ${call.from}` : `to ${call.to}`; + const duration = formatDuration(call.duration / 1000); + const date = formatDate(call.created_at); + const notes = call.notes ? `Call has ${call.notes.length} notes` : <>; + + const handleCallOnClick = (callId: string) => { + navigate(`/calls/${callId}`); + }; + + return ( + handleCallOnClick(call.id)} + > + + + + + + {title} + {subtitle} + + + + {duration} + + {date} + + + + {notes} + + + ); +}; + +export default CallListItem; diff --git a/phone-test/src/pages/CallsList.tsx b/phone-test/src/pages/CallsList.tsx index eefe303..1cf28b5 100644 --- a/phone-test/src/pages/CallsList.tsx +++ b/phone-test/src/pages/CallsList.tsx @@ -2,17 +2,13 @@ import { useQuery } from '@apollo/client'; import styled from 'styled-components'; import { PAGINATED_CALLS } from '../gql/queries'; import { - Grid, - Icon, Typography, Spacer, Box, - DiagonalDownOutlined, - DiagonalUpOutlined, Pagination } from '@aircall/tractor'; -import { formatDate, formatDuration } from '../helpers/dates'; import { useNavigate, useSearchParams } from 'react-router-dom'; +import CallListItem from '../components/calls/CallListItem/CallListItem'; export const PaginationWrapper = styled.div` > div { @@ -46,13 +42,11 @@ export const CallsListPage = () => { const { totalCount, nodes: calls } = data?.paginatedCalls; - const handleCallOnClick = (callId: string) => { - navigate(`/calls/${callId}`); - }; - const handlePageChange = (page: number) => { - console.log(page, Math.ceil(totalCount / callsPerPage), totalCount, callsPerPage) - if (page === activePage || page < 1 || page > Math.ceil(totalCount / callsPerPage)) return; + console.log(page, Math.ceil(totalCount / callsPerPage), totalCount, callsPerPage); + if (page === activePage) return; + if (page < 1) page= 1; + if (page > Math.ceil(totalCount / callsPerPage)) page= Math.ceil(totalCount / callsPerPage); setSearchParams(params => { params.set('page', page.toString()); return params; @@ -75,59 +69,13 @@ export const CallsListPage = () => { Calls History - - {calls.map((call: Call) => { - const icon = call.direction === 'inbound' ? DiagonalDownOutlined : DiagonalUpOutlined; - const title = - call.call_type === 'missed' - ? 'Missed call' - : call.call_type === 'answered' - ? 'Call answered' - : 'Voicemail'; - const subtitle = call.direction === 'inbound' ? `from ${call.from}` : `to ${call.to}`; - const duration = formatDuration(call.duration / 1000); - const date = formatDate(call.created_at); - const notes = call.notes ? `Call has ${call.notes.length} notes` : <>; - - return ( - handleCallOnClick(call.id)} - > - - - - - - {title} - {subtitle} - - - - {duration} - - {date} - - - - {notes} - - - ); - })} - - + + + {calls.map((call: Call) => ( + + ))} + + {totalCount && ( Date: Mon, 23 Oct 2023 02:03:31 +0200 Subject: [PATCH 06/18] Add calls sorting and filtering in memory --- .../calls/CallsFilterBar/CallsFilterBar.tsx | 85 ++++++++++++ .../EmptyCallListItem/EmptyCallListItem.tsx | 10 ++ phone-test/src/components/calls/index.ts | 3 + phone-test/src/pages/CallsList.tsx | 122 +++++++++++++----- 4 files changed, 190 insertions(+), 30 deletions(-) create mode 100644 phone-test/src/components/calls/CallsFilterBar/CallsFilterBar.tsx create mode 100644 phone-test/src/components/calls/EmptyCallListItem/EmptyCallListItem.tsx create mode 100644 phone-test/src/components/calls/index.ts diff --git a/phone-test/src/components/calls/CallsFilterBar/CallsFilterBar.tsx b/phone-test/src/components/calls/CallsFilterBar/CallsFilterBar.tsx new file mode 100644 index 0000000..f98577e --- /dev/null +++ b/phone-test/src/components/calls/CallsFilterBar/CallsFilterBar.tsx @@ -0,0 +1,85 @@ +import { + Box, + Button, + DiagonalDownOutlined, + DiagonalUpOutlined, + Flex, + Icon, + Select +} from '@aircall/tractor'; +import { useNavigate, useSearchParams } from 'react-router-dom'; + +const CallsFilterBar = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); + + //Used to remove page when changing filter. Fix pagination bug + const deletePageParam = () => { + setSearchParams(params => { + params.delete('page'); + return params; + }); + }; + + const handleCallDirection = (direction: string) => { + if (direction === 'all') { + setSearchParams(params => { + params.delete('dir'); + return params; + }); + } else { + setSearchParams(params => { + params.set('dir', direction); + return params; + }); + } + deletePageParam(); + navigate(`/calls/?${searchParams.toString()}`); + }; + + const handleCallType = (key: React.Key[]) => { + setSearchParams(params => { + if (!key[0]) params.delete('type'); + else params.set('type', key[0].toString()); + return params; + }); + deletePageParam(); + navigate(`/calls/?${searchParams.toString()}`); + }; + + const callTypes = [ + { value: 'missed', label: 'Missed calls' }, + { value: 'answered', label: 'Answered calls' }, + { value: 'voicemail', label: 'Voicemail calls' }, + { value: 'unknown', label: 'Unknown calls' } + ]; + + return ( + + + {/* I'm not using defaultValue because it's not working. I tried defaultValue="missed" hardcoded and + it doesn't work. I tried passing all option object and no works*/} +