Skip to content

Commit d2f7efe

Browse files
committed
Initial commit of Case Study material
1 parent f99de20 commit d2f7efe

25 files changed

+21637
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*.log
2+
npm-debug.log*
3+
node_modules/
4+
jspm_packages/
5+
.npm
6+
.DS_Store
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import React, {useState, useRef, useEffect} from 'react';
2+
import PlantSelectionInputSuggestion from './PlantSelectionInputSuggestion.jsx';
3+
import usePlantsLike from './usePlantsLike';
4+
5+
const MIN_QUERY_LENGTH = 3;
6+
7+
const KEYCODES = {
8+
ENTER: 13,
9+
ARROW_DOWN: 40,
10+
ARROW_UP: 38
11+
};
12+
13+
export default ({ isInitiallityOpen, value, onSelect }) => {
14+
15+
const inputRef = useRef();
16+
17+
const [isOpen, setIsOpen] = useState(isInitiallityOpen || false);
18+
const [query, setQuery] = useState(value);
19+
20+
const [selectedIndex, setSelectedIndex] = useState(1);
21+
const {loading, plants} = usePlantsLike(query);
22+
23+
function onKeyUp(e) {
24+
switch (e.keyCode) {
25+
case KEYCODES.ENTER:
26+
if (selectedIndex > -1) {
27+
onSelect(plants[selectedIndex]);
28+
}
29+
break;
30+
case KEYCODES.ARROW_UP:
31+
setSelectedIndex(selectedIndex === 0 ? 0 : selectedIndex - 1);
32+
break;
33+
case KEYCODES.ARROW_DOWN:
34+
setSelectedIndex(
35+
selectedIndex === plants.length - 1 ? selectedIndex : selectedIndex + 1
36+
);
37+
break;
38+
}
39+
}
40+
41+
useEffect(() => {
42+
setSelectedIndex(-1);
43+
}, [loading, plants]);
44+
45+
return <div className="PlantSelectionInput">
46+
<input
47+
onFocus={() => {
48+
setQuery(inputRef.current.value);
49+
setIsOpen(true);
50+
}}
51+
onBlur={() => setIsOpen(false)}
52+
onChange={() => setQuery(inputRef.current.value)}
53+
ref={inputRef}
54+
autoComplete="off"
55+
aria-autocomplete="true"
56+
spellCheck="false"
57+
aria-expanded={String(isOpen)}
58+
role="combobox"
59+
onKeyUp={onKeyUp}
60+
defaultValue={value} />
61+
{
62+
isOpen &&
63+
<ol>
64+
{
65+
!plants.length && (!query || query.length < MIN_QUERY_LENGTH) &&
66+
<li className="PlantSelectionInput_notice">
67+
Please begin typing a plant name...
68+
</li>
69+
}
70+
{
71+
!plants.length && query && query.length >= MIN_QUERY_LENGTH && loading &&
72+
<li className="PlantSelectionInput_notice">
73+
Loading...
74+
</li>
75+
}
76+
{
77+
!plants.length && query && query.length >= MIN_QUERY_LENGTH && !loading &&
78+
<li className="PlantSelectionInput_notice">
79+
No plants with that name found...
80+
</li>
81+
}
82+
{
83+
plants.map(
84+
(plant, index) =>
85+
<PlantSelectionInputSuggestion
86+
key={plant.id}
87+
plant={plant}
88+
isSelected={selectedIndex === index}
89+
onMouseEnter={() => setSelectedIndex(index)}
90+
onClick={() => {
91+
onSelect(plant);
92+
}}
93+
/>
94+
)
95+
}
96+
</ol>
97+
}
98+
</div>;
99+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from 'react';
2+
import renderer from 'react-test-renderer';
3+
import PlantSelectionInput from './';
4+
5+
describe('PlantSelectionInput', () => {
6+
7+
it('Should render deterministically to its snapshot', () => {
8+
expect(
9+
renderer
10+
.create(
11+
<PlantSelectionInput
12+
onSelect={() => {}}
13+
/>
14+
)
15+
.toJSON()
16+
).toMatchSnapshot();
17+
});
18+
19+
describe('With configured isInitiallyOpen & value properties', () => {
20+
it('Should render deterministically to its snapshot', () => {
21+
expect(
22+
renderer
23+
.create(
24+
<PlantSelectionInput
25+
isInitiallyOpen={true}
26+
value="Example..."
27+
onSelect={() => {}}
28+
/>
29+
)
30+
.toJSON()
31+
).toMatchSnapshot();
32+
});
33+
});
34+
35+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React, {useState, useRef, useEffect} from 'react';
2+
3+
export default ({ plant, isSelected, onClick, onMouseEnter }) => {
4+
5+
const ref = useRef();
6+
const [didSelectByMouseEnter, setDidSelectByMouseEnter] = useState(false);
7+
8+
useEffect(() => {
9+
if (isSelected && !didSelectByMouseEnter) {
10+
ref.current.scrollIntoView(false);
11+
}
12+
if (!isSelected) {
13+
setDidSelectByMouseEnter(false);
14+
}
15+
}, [isSelected]);
16+
17+
return (
18+
<li
19+
ref={ref}
20+
onMouseDown={onClick}
21+
className={
22+
'PlantSelectionInputSuggestion' + (isSelected ? ' isSelected' : '')
23+
}
24+
onMouseEnter={e => {
25+
setDidSelectByMouseEnter(true);
26+
onMouseEnter(e);
27+
}}
28+
>
29+
<span className="PlantSelectionInputSuggestion_genus_species">
30+
{plant.genus}
31+
{' '}
32+
{plant.species}
33+
</span>
34+
<span className="PlantSelectionInputSuggestion_family">
35+
{plant.family}
36+
</span>
37+
</li>
38+
);
39+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`PlantSelectionInput Should render deterministically to its snapshot 1`] = `
4+
<div
5+
className="PlantSelectionInput"
6+
>
7+
<input
8+
aria-autocomplete="true"
9+
aria-expanded="false"
10+
autoComplete="off"
11+
onBlur={[Function]}
12+
onChange={[Function]}
13+
onFocus={[Function]}
14+
onKeyUp={[Function]}
15+
role="combobox"
16+
spellCheck="false"
17+
/>
18+
</div>
19+
`;
20+
21+
exports[`PlantSelectionInput With configured isInitiallyOpen & value properties Should render deterministically to its snapshot 1`] = `
22+
<div
23+
className="PlantSelectionInput"
24+
>
25+
<input
26+
aria-autocomplete="true"
27+
aria-expanded="false"
28+
autoComplete="off"
29+
defaultValue="Example..."
30+
onBlur={[Function]}
31+
onChange={[Function]}
32+
onFocus={[Function]}
33+
onKeyUp={[Function]}
34+
role="combobox"
35+
spellCheck="false"
36+
/>
37+
</div>
38+
`;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"main": "PlantSelectionInput.jsx"
3+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {useState, useEffect} from 'react';
2+
import debounce from 'debounce-promise';
3+
import fetch from 'node-fetch';
4+
5+
const makeRequest = debounce(query => {
6+
return (
7+
fetch(`http://localhost:3000/plants/${query}`)
8+
.then(response => response.json())
9+
);
10+
}, 300, { leading: true });
11+
12+
export default (query, generateDataURL) => {
13+
14+
const [loading, setLoading] = useState(false);
15+
const [plants, setPlants] = useState([]);
16+
17+
useEffect(() => {
18+
19+
if (!query) {
20+
setPlants([]);
21+
return;
22+
}
23+
24+
setLoading(true);
25+
26+
makeRequest(query).then(data => {
27+
setLoading(false);
28+
setPlants(data);
29+
});
30+
31+
}, [query]);
32+
33+
return {
34+
loading,
35+
plants
36+
}
37+
38+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import ReactDOM from 'react-dom';
2+
import React from 'react';
3+
import PlantSelectionInput from './components/PlantSelectionInput';
4+
5+
ReactDOM.render(
6+
<PlantSelectionInput
7+
onSelect={
8+
plant => alert(`You selected ${plant.fullyQualifiedName}`)
9+
}
10+
/>,
11+
document.getElementById('root')
12+
);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = {
2+
presets: [
3+
[
4+
'@babel/preset-env',
5+
{
6+
targets: {
7+
node: 'current',
8+
},
9+
}
10+
],
11+
'@babel/preset-react'
12+
]
13+
};
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Plant Selection Application</title>
5+
6+
<style>
7+
body {
8+
font: 1em helvetica, sans-serif;
9+
}
10+
h1 {
11+
text-align: center;
12+
}
13+
#root {
14+
width: 400px;
15+
margin: 2em auto;
16+
}
17+
.PlantSelectionInput {
18+
width: 100%;
19+
display: flex;
20+
position: relative;
21+
}
22+
.PlantSelectionInput input {
23+
background: #fff;
24+
font-size: 1em;
25+
flex: 1 1;
26+
padding: .5em;
27+
outline: none;
28+
border: 3px solid #DDD;
29+
}
30+
.PlantSelectionInput input:focus {
31+
border: 3px solid #3aa2d5;
32+
border-top-left-radius: 2px;
33+
border-top-right-radius: 2px;
34+
}
35+
.PlantSelectionInput ol {
36+
z-index: 9;
37+
background: #fff;
38+
position: absolute;
39+
top: 100%;
40+
left: 0;
41+
width: 100%;
42+
box-sizing: border-box;
43+
padding: 0 3px;
44+
border-bottom: 3px solid #3aa2d5;
45+
border-bottom-left-radius: 2px;
46+
border-bottom-right-radius: 2px;
47+
margin: 0;
48+
overflow-y: scroll;
49+
max-height: 200px;
50+
background: #3aa2d5;
51+
box-shadow: 0 0 5px rgba(0,0,0,0.4);
52+
}
53+
.PlantSelectionInput .PlantSelectionInputSuggestion {
54+
overflow: hidden;
55+
background: white;
56+
list-style: none;
57+
margin-bottom: 2px;
58+
cursor: pointer;
59+
padding: .5em;
60+
line-height: 1.4em;
61+
}
62+
.PlantSelectionInput .PlantSelectionInputSuggestion_genus_species {
63+
padding: calc(.3em + 1px);
64+
display: inline-block;
65+
}
66+
.PlantSelectionInput .PlantSelectionInputSuggestion_family {
67+
float: right;
68+
background: #D5E8D5;
69+
border: 1px solid #ABCD9F;
70+
padding: .3em;
71+
color: #000;
72+
}
73+
.PlantSelectionInput .PlantSelectionInput_notice {
74+
color: #FFF;
75+
padding: .4em;
76+
}
77+
.PlantSelectionInput .PlantSelectionInputSuggestion.isSelected {
78+
background: #7bcaf1;
79+
color: white;
80+
}
81+
.PlantSelectionInput .PlantSelectionInputSuggestion:last-child {
82+
margin: 0;
83+
}
84+
</style>
85+
</head>
86+
<body>
87+
88+
<h1><em>EveryPlant</em> Selection App</h1>
89+
90+
<div id="root"></div>
91+
92+
<script src="./main.js"></script>
93+
94+
</body>
95+
</html>

0 commit comments

Comments
 (0)