Skip to content

Commit

Permalink
Merge pull request #14 from andrecego/feat/add-candidate-details-view
Browse files Browse the repository at this point in the history
Add CandidateCard component and candidate details page
  • Loading branch information
andrecego authored Feb 11, 2024
2 parents aeb84a5 + 36166a2 commit 126b1e8
Show file tree
Hide file tree
Showing 14 changed files with 910 additions and 56 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"object-curly-spacing": ["error", "always"],
"semi": ["error", "never"],
"max-len": ["error", { "ignoreComments": true, "code": 120 }],
"react/prop-types": "off"
"react/prop-types": "off",
"react/display-name": "off"
}
}
33 changes: 26 additions & 7 deletions app/javascript/src/components/CandidateCard.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import React from 'react'
import { Card, Avatar, Flex, Typography } from 'antd'
import { GiftOutlined, MailOutlined, UserOutlined } from '@ant-design/icons'

const CandidateCard = ({ candidate }) => {
const { Title } = Typography

const CandidateCard = ({ candidate, loading }) => {
return (
<div className="card">
<div className="card-body">
<h5 className="card-title">{candidate.name}</h5>
<p className="card-text">{candidate.email}</p>
</div>
</div>
<Card loading={loading}>
<Flex gap={16}>
<Avatar size='large' icon={<UserOutlined />} />

<Flex gap={16} vertical>
<Title level={2}>{candidate.name}</Title>

<Flex gap={16}>
<MailOutlined style={{ fontSize: '20px' }} />
<a href={`mailto:${candidate.email}`} data-testid='email-link'>
{candidate.email}
</a>
</Flex>

<Flex gap={16}>
<GiftOutlined style={{ fontSize: '20px' }} />
<p>{candidate.birthdate?.format('YYYY/MM/DD')}</p>
</Flex>
</Flex>
</Flex>
</Card>
)
}

Expand Down
5 changes: 4 additions & 1 deletion app/javascript/src/i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"welcomeToHaistack": "Welcome to Haistack",
"candidates": "Candidates"
"candidates": "Candidates",
"candidate": {
"candidateDetails": "Candidate Details"
}
}
49 changes: 47 additions & 2 deletions app/javascript/src/main.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,48 @@
.main {
padding: 20px;
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/

html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
10 changes: 10 additions & 0 deletions app/javascript/src/repositories/candidates.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,13 @@ export const fetchCandidates = async () => {
meta: response.data?.meta,
}
}

export const getCandidate = async (id) => {
const response = await get(`${BASE_URL}/api/v1/candidates/${id}`)
const candidate = createCandidate(response.data)

return {
candidate,
status: response.status,
}
}
11 changes: 8 additions & 3 deletions app/javascript/src/routes.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import React from 'react'
import { createBrowserRouter } from 'react-router-dom'
import { Home, Candidates } from '@/views/index.js'
import { Home, Candidate } from '@/views/index.js'

export const router = createBrowserRouter([
{
path: '/',
element: <Home />,
},
{
path: '/candidates',
element: <Candidates />,
path: 'candidates',
children: [
{
path: ':id',
element: <Candidate />,
},
],
},
])
38 changes: 38 additions & 0 deletions app/javascript/src/tests/components/CandidateCard.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { expect, describe, it, vi, afterEach } from 'vitest'
import { render, cleanup } from '@testing-library/react'
import CandidateCard from '@/components/CandidateCard.jsx'
import createCandidate from '@/models/candidate'

vi.mock('antd', () => ({
Card: ({ children }) => <div>{children}</div>,
Avatar: ({ children }) => <div>{children}</div>,
Flex: ({ children }) => <div>{children}</div>,
Typography: {
Title: ({ children }) => <div>{children}</div>,
},
}))

describe('CandidateCard', () => {
afterEach(cleanup)

const candidate = createCandidate({
id: 1,
name: 'John Doe',
email: 'john@doe.com',
birthdate: '1990-01-01',
})

it('renders a card with the candidate information', () => {
const { getByText } = render(<CandidateCard candidate={candidate} loading={false} />)

expect(getByText('John Doe')).toBeTruthy()
expect(getByText('john@doe.com')).toBeTruthy()
expect(getByText('1990/01/01')).toBeTruthy()
})

it('renders a link to send an email to the candidate', () => {
const { getByTestId } = render(<CandidateCard candidate={candidate} loading={false} />)

expect(getByTestId('email-link').getAttribute('href')).toBe('mailto:john@doe.com')
})
})
102 changes: 70 additions & 32 deletions app/javascript/src/tests/repositories/candidates.spec.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,97 @@
import { describe, it, expect, vi } from 'vitest'
import { get } from '@/lib/request.js'
import { fetchCandidates } from '@/repositories/candidates.js'
import { fetchCandidates, getCandidate } from '@/repositories/candidates.js'

vi.mock('@/lib/request.js', () => ({
get: vi.fn(),
}))

vi.mock('@/models/candidate.js', () => ({ default: () => 'createCandidate' }))

describe('fetchCandidates', () => {
it('returns candidates and status', async () => {
const candidates = [{ id: 1, name: 'John Doe' }]
const response = { data: { candidates }, status: 200 }
get.mockResolvedValue(response)

const result = await fetchCandidates()

expect(result.candidates).toEqual(['createCandidate'])
expect(result.status).toBe(200)
})

describe('when response has meta', () => {
it('returns meta', async () => {
describe('candidates repository', () => {
describe('fetchCandidates', () => {
it('returns candidates and status', async () => {
const candidates = [{ id: 1, name: 'John Doe' }]
const meta = { total: 1 }
const response = { data: { candidates, meta }, status: 200 }
const response = { data: { candidates }, status: 200 }
get.mockResolvedValue(response)

const result = await fetchCandidates()

expect(result.meta).toEqual(meta)
expect(result.candidates).toEqual(['createCandidate'])
expect(result.status).toBe(200)
})
})

describe('when response has no candidates', () => {
it('returns empty array', async () => {
const response = { data: {}, status: 200 }
get.mockResolvedValue(response)
describe('when response has meta', () => {
it('returns meta', async () => {
const candidates = [{ id: 1, name: 'John Doe' }]
const meta = { total: 1 }
const response = { data: { candidates, meta }, status: 200 }
get.mockResolvedValue(response)

const result = await fetchCandidates()
const result = await fetchCandidates()

expect(result.candidates).toEqual([])
expect(result.meta).toEqual(meta)
})
})

describe('when response has no candidates', () => {
it('returns empty array', async () => {
const response = { data: {}, status: 200 }
get.mockResolvedValue(response)

const result = await fetchCandidates()

expect(result.candidates).toEqual([])
})
})

describe('when response is not successful', () => {
it('returns status', async () => {
const response = { data: {}, status: 500 }
get.mockResolvedValue(response)

const result = await fetchCandidates()

expect(result.candidates).toEqual([])
expect(result.status).toBe(500)
})
})
})

describe('when response is not successful', () => {
it('returns status', async () => {
const response = { data: {}, status: 500 }
describe('getCandidate', () => {
it('returns candidate and status', async () => {
const candidate = { id: 1, name: 'John Doe' }
const response = { data: candidate, status: 200 }
get.mockResolvedValue(response)

const result = await fetchCandidates()
const result = await getCandidate(1)

expect(result.candidate).toEqual('createCandidate')
expect(result.status).toBe(200)
})

describe('when response is not successful', () => {
it('returns status', async () => {
const response = { data: {}, status: 500 }
get.mockResolvedValue(response)

expect(result.candidates).toEqual([])
expect(result.status).toBe(500)
const result = await getCandidate(1)

expect(result.candidate).toEqual('createCandidate')
expect(result.status).toBe(500)
})
})

describe('when response has no data', () => {
it('returns status', async () => {
const response = { status: 404 }
get.mockResolvedValue(response)

const result = await getCandidate(1)

expect(result.candidate).toEqual('createCandidate')
expect(result.status).toBe(404)
})
})
})
})

52 changes: 52 additions & 0 deletions app/javascript/src/tests/views/Candidate.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { expect, describe, it, vi, afterEach } from 'vitest'
import { render, cleanup } from '@testing-library/react'
import Candidate from '@/views/Candidate.jsx'
import { getCandidate } from '@/repositories/candidates.js'
import { useParams } from 'react-router-dom'

vi.mock('antd', () => ({
Typography: {
Title: ({ children }) => <div>{children}</div>,
},
}))

vi.mock('@/components/CandidateCard', () => ({
default: () => <div>CandidateCard</div>,
}))

vi.mock('react-router-dom', () => ({
useParams: vi.fn(() => ({ id: 1 })),
}))

vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key) => key }),
}))

vi.mock('@/repositories/candidates.js', () => ({
getCandidate: vi.fn(() =>({
candidate: {
id: 1,
name: 'John Doe',
email: 'john@doe.com',
birthdate: '1990-01-01',
},
})),
}))

describe('CandidateCard', () => {
afterEach(cleanup)

it('renders the title', () => {
const { getByText } = render(<Candidate />)

expect(getByText('candidate.candidateDetails')).toBeTruthy()
})

it('calls the getCandidate function with the id', () => {
useParams.mockReturnValue({ id: 2 })

render(<Candidate />)

expect(getCandidate).toHaveBeenCalledWith(2)
})
})
Loading

0 comments on commit 126b1e8

Please sign in to comment.