Skip to content

Commit 806f8c8

Browse files
lpatmoLinda Pengangelocordon
authored
[Issue 143] Refactor /resources page to use React Query (#162)
Refactor search and get resources page to use React Query. Remove avatar which is not needed for design. Add test for getResources function. Support edge case in case API breaks -- if data.results is not returned from the API for some reason h/t Gaurav and Phil. Fix linting errors and remove results null check, since we check it later on with results `&&`. Pull search phrase out of queries and into the search function. Make sure when we hit the search button again and there is an empty value, full results display again. Co-authored-by: Linda Peng <[email protected]> Co-authored-by: Angelo Cordon <[email protected]>
1 parent 359693d commit 806f8c8

File tree

7 files changed

+180
-96
lines changed

7 files changed

+180
-96
lines changed

src/components/Resources/ResourceCard.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import Card from '@material-ui/core/Card';
88
import CardHeader from '@material-ui/core/CardHeader';
99
import CardContent from '@material-ui/core/CardContent';
1010
import CardActions from '@material-ui/core/CardActions';
11-
import Avatar from '@material-ui/core/Avatar';
1211
import IconButton from '@material-ui/core/IconButton';
1312
import Typography from '@material-ui/core/Typography';
1413
import Collapse from '@material-ui/core/Collapse';
@@ -54,11 +53,6 @@ export const ResourceCard = ({ guid, title, created, description, url }) => {
5453
<Card className={classes.card}>
5554
<Link to={`/resources/${guid}`}>
5655
<CardHeader
57-
avatar={
58-
<Avatar aria-label="recipe" className={classes.avatar}>
59-
R
60-
</Avatar>
61-
}
6256
action={
6357
<IconButton aria-label="settings">
6458
<MoreVertIcon />

src/components/Resources/Resources.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React, { useState } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { useQuery } from 'react-query';
4+
import PersonalMenu from '../PersonalMenu';
5+
import Search from '../Search';
6+
import { Grid, Typography } from '@material-ui/core';
7+
import { ResourceCard } from './ResourceCard';
8+
import { getResources } from '../../utils/queries';
9+
10+
function Resources() {
11+
const [searchValue, setSearchValue] = useState('');
12+
const { isLoading, data, error } = useQuery(
13+
[`?search=${searchValue}`],
14+
getResources
15+
);
16+
17+
if (isLoading) {
18+
return <p>Loading...</p>;
19+
}
20+
21+
const search = searchValue => {
22+
if (searchValue.length === 0) {
23+
setSearchValue();
24+
}
25+
setSearchValue(searchValue);
26+
};
27+
28+
const { results, count } = data;
29+
30+
const renderResults = () => {
31+
return (
32+
<Grid container spacing={1}>
33+
{results.length === 0 ? (
34+
<Grid item lg={9}>
35+
<Typography>No resources found</Typography>
36+
</Grid>
37+
) : (
38+
results.map(resource => (
39+
<Grid item lg={3} key={resource.guid}>
40+
<ResourceCard {...resource} />
41+
</Grid>
42+
))
43+
)}
44+
</Grid>
45+
);
46+
};
47+
48+
return (
49+
<Grid container spacing={1}>
50+
<Grid item lg={3}>
51+
<PersonalMenu />
52+
</Grid>
53+
<Grid item lg={9}>
54+
<h2>Resources</h2>
55+
<Search label="Search resources" search={search} />
56+
{searchValue && (
57+
<Typography>
58+
You have searched for "<strong>{searchValue}</strong>" and gotten
59+
<strong> {count}</strong> results.
60+
</Typography>
61+
)}
62+
<br />
63+
{error && <div className="errorMessage">{error}</div>}
64+
{results && renderResults()}
65+
</Grid>
66+
</Grid>
67+
);
68+
}
69+
70+
Resources.propTypes = {
71+
getResourcesUrl: PropTypes.string,
72+
};
73+
74+
export default Resources;

src/components/Resources/index.js

Lines changed: 2 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,5 @@
1-
import React, { useState, useEffect } from 'react';
2-
import PropTypes from 'prop-types';
3-
import axios from 'axios';
4-
import PersonalMenu from '../PersonalMenu';
5-
import Search from '../Search';
6-
import { Grid, Typography } from '@material-ui/core';
1+
import Resources from './Resources';
72
import { ResourceCard } from './ResourceCard';
8-
import { buildQueryString } from '../../helpers';
9-
10-
function Resources({ getResourcesUrl }) {
11-
const [resources, setResources] = useState([]);
12-
const [searchValue, setSearchValue] = useState('');
13-
const [loading, setLoading] = useState(true);
14-
const [errorMessage, setErrorMessage] = useState(null);
15-
16-
useEffect(() => {
17-
axios
18-
.get(getResourcesUrl)
19-
.then(function(response) {
20-
// handle success
21-
setResources(response.data);
22-
setLoading(false);
23-
})
24-
.catch(function(error) {
25-
// handle error
26-
console.log(error);
27-
});
28-
}, [getResourcesUrl]);
29-
30-
// TODO: Refactor search function into its own file
31-
const search = searchValue => {
32-
setSearchValue(searchValue);
33-
setLoading(true);
34-
setErrorMessage(null);
35-
axios
36-
.get(buildQueryString(getResourcesUrl, searchValue))
37-
.then(function(response) {
38-
setResources(response.data);
39-
setLoading(false);
40-
})
41-
.catch(function(error) {
42-
console.log(error);
43-
});
44-
};
45-
46-
const { count = 0, results } = resources;
47-
48-
return (
49-
<Grid container spacing={1}>
50-
<Grid item lg={3}>
51-
<PersonalMenu />
52-
</Grid>
53-
<Grid item lg={9}>
54-
<h2>Resources</h2>
55-
<Search label="Search resources" search={search} />
56-
{searchValue && (
57-
<Typography>
58-
You have searched for "<strong>{searchValue}</strong>" and gotten
59-
<strong> {count}</strong> results.
60-
</Typography>
61-
)}
62-
<br />
63-
{loading && !errorMessage ? (
64-
<span>loading...</span>
65-
) : errorMessage ? (
66-
<div className="errorMessage">{errorMessage}</div>
67-
) : (
68-
<Grid container spacing={1}>
69-
{resources.length === 0 ? (
70-
<Typography>No resources found</Typography>
71-
) : (
72-
results.map(resource => (
73-
<Grid item lg={3} key={resource.guid}>
74-
<ResourceCard {...resource} />
75-
</Grid>
76-
))
77-
)}
78-
</Grid>
79-
)}
80-
</Grid>
81-
</Grid>
82-
);
83-
}
84-
85-
Resources.propTypes = {
86-
getResourcesUrl: PropTypes.string,
87-
};
883

894
export default Resources;
5+
export { ResourceCard };

src/components/Resources/index.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ window.MutationObserver = MutationObserver;
1111
jest.mock('axios');
1212

1313
// TODO: move mock data into its own file, test for failures, and test for successful
14-
// search after clicking on search button
14+
// search after clicking on search button (https://github.com/codebuddies/frontend/issues/159)
1515
describe('Resources', () => {
1616
test('renders correctly with resources', async () => {
17-
const url = '/api/v1/resources';
17+
const url = '/api/v1/resources/?search=';
1818

1919
axiosMock.get.mockResolvedValueOnce({
2020
data: {

src/components/Resources/submitResource.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const SubmitResource = () => {
8484
const classes = useStyles();
8585
const inputLabel = useRef(null);
8686
const [labelWidth, setLabelWidth] = useState(0);
87+
8788
useEffect(() => {
8889
setLabelWidth(inputLabel.current.offsetWidth);
8990
}, []);

src/utils/queries.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,9 @@ const getResource = async (_key, id) => {
77
return data;
88
};
99

10-
export { getResource };
10+
const getResources = async searchTerm => {
11+
const { data } = await axios.get(`${API_URL}/resources/${searchTerm}`);
12+
return data;
13+
};
14+
15+
export { getResource, getResources };

src/utils/queries.test.js

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getResource } from './queries';
1+
import { getResource, getResources } from './queries';
22
import axios from 'axios';
33

44
jest.mock('axios');
@@ -32,3 +32,97 @@ describe('getResource', () => {
3232
).rejects.toThrow(errorMessage);
3333
});
3434
});
35+
36+
describe('getResources', () => {
37+
it('returns `data` object when successfully fetched', async () => {
38+
const data = {
39+
count: 2,
40+
next: 'http://localhost:8000/api/v1/resources/?page=2',
41+
previous: null,
42+
results: [
43+
{
44+
guid: '4e85dbb6-d519-11ea-a622-0242ac130002',
45+
author: '',
46+
title: 'gitduck',
47+
description: '',
48+
url: 'https://gitduck.com',
49+
referring_url: '',
50+
other_referring_source: '',
51+
user: {
52+
id: 229,
53+
username: 'aug2nd',
54+
first_name: 'Linda',
55+
last_name: 'Peng',
56+
is_superuser: false,
57+
},
58+
date_published: '2020-08-02T16:38:45.651676-07:00',
59+
created: '2020-08-02T16:38:45.652112-07:00',
60+
modified: '2020-08-02T16:38:45.651710-07:00',
61+
media_type: 'Podcast',
62+
paid: true,
63+
tags: [],
64+
},
65+
{
66+
guid: '12f02bb2-e2aa-11ea-a58a-0242ac130006',
67+
author: '',
68+
title: 'google',
69+
description: '',
70+
url: 'https://google.com',
71+
referring_url: '',
72+
other_referring_source: '',
73+
user: {
74+
id: 233,
75+
username: 'Austyn99',
76+
first_name: 'Jarret',
77+
last_name: 'Mitchell',
78+
is_superuser: false,
79+
},
80+
date_published: '2020-08-19T22:57:47.828418-07:00',
81+
created: '2020-08-19T22:57:47.831689-07:00',
82+
modified: '2020-08-19T22:57:47.828464-07:00',
83+
media_type: 'Video',
84+
paid: true,
85+
tags: [],
86+
},
87+
],
88+
};
89+
90+
axios.get.mockImplementationOnce(() => Promise.resolve({ data }));
91+
92+
await expect(getResources('')).resolves.toEqual(data);
93+
});
94+
});
95+
it('returns the appropriate result when gitduck is searched', async () => {
96+
const data = {
97+
count: 1,
98+
next: 'http://localhost:8000/api/v1/resources/?page=2',
99+
previous: null,
100+
results: [
101+
{
102+
guid: '4e85dbb6-d519-11ea-a622-0242ac130002',
103+
author: '',
104+
title: 'gitduck',
105+
description: '',
106+
url: 'https://gitduck.com',
107+
referring_url: '',
108+
other_referring_source: '',
109+
user: {
110+
id: 229,
111+
username: 'aug2nd',
112+
first_name: 'Linda',
113+
last_name: 'Peng',
114+
is_superuser: false,
115+
},
116+
date_published: '2020-08-02T16:38:45.651676-07:00',
117+
created: '2020-08-02T16:38:45.652112-07:00',
118+
modified: '2020-08-02T16:38:45.651710-07:00',
119+
media_type: 'Podcast',
120+
paid: true,
121+
tags: [],
122+
},
123+
],
124+
};
125+
126+
axios.get.mockImplementationOnce(() => Promise.resolve({ data }));
127+
await expect(getResources('gitduck')).resolves.toEqual(data);
128+
});

0 commit comments

Comments
 (0)