Update respotify tutorial

This commit is contained in:
Timothy Warren 2016-08-24 13:48:35 -04:00
parent a2c108af80
commit 95efdb7303
12 changed files with 292 additions and 20 deletions

View File

@ -0,0 +1,7 @@
{
"extends": "airbnb",
"rules": {
"arrow-body-style": 0,
"no-param-reassign": 0
}
}

View File

@ -1,27 +1,39 @@
{
"name": "respotify",
"version": "1.0.0",
"description": "",
"description": "The webapp accompanying the React tutorial on patternhatch.com",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --hot --inline",
"build": "webpack",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"keywords": [
"react",
"redux",
"webpack",
"babel"
],
"author": "Nitin Punjabi",
"license": "MIT",
"devDependencies": {
"babel-core": "^6.10.4",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.11.1",
"eslint": "^2.13.1",
"eslint-config-airbnb": "^9.0.1",
"eslint-loader": "^1.4.0",
"eslint-plugin-import": "^1.10.2",
"eslint-plugin-jsx-a11y": "^1.5.5",
"eslint-plugin-react": "^5.2.2",
"file-loader": "^0.9.0",
"react-hot-loader": "^1.3.0",
"webpack": "^1.13.1",
"webpack-dev-server": "^1.14.1"
},
"dependencies": {
"axios": "^0.13.1",
"react": "^15.2.0",
"react-dom": "^15.2.0"
}

View File

@ -0,0 +1,17 @@
import axios from 'axios';
function fetch(request, callback) {
axios.get(request).then(response => {
callback(response.data);
});
}
export function getAlbums(artist, callback) {
const request = `https://api.spotify.com/v1/search?q=${artist}&type=album`;
fetch(request, callback);
}
export function getTracks(albumId, callback) {
const request = `https://api.spotify.com/v1/albums/${albumId}`;
fetch(request, callback);
}

View File

@ -0,0 +1,27 @@
import React from 'react';
const Album = (props) => {
return (
<li>
<img
src={props.album.images[1].url}
alt={props.album.name}
style={Album.styles.img}
onClick={() => props.getTracks(props.album.id)}
/>
</li>
);
};
Album.propTypes = {
album: React.PropTypes.object.isRequired,
getTracks: React.PropTypes.func.isRequired,
};
Album.styles = {
img: {
marginBottom: '1em',
},
};
export default Album;

View File

@ -0,0 +1,36 @@
import React from 'react';
import Album from './Album';
const AlbumList = (props) => {
const albums = props.albums.map((album) =>
<Album key={album.id} album={album} getTracks={props.getTracks} />);
return (
<div className="col-md-3" style={AlbumList.styles.div}>
<ul style={AlbumList.styles.ul}>
{albums}
</ul>
</div>
);
};
AlbumList.propTypes = {
albums: React.PropTypes.array.isRequired,
getTracks: React.PropTypes.func.isRequired,
};
AlbumList.styles = {
div: {
width: 370,
marginLeft: 30,
textAlign: 'right',
maxHeight: '85vh',
overflowY: 'auto',
},
ul: {
listStyle: 'none',
},
};
export default AlbumList;

View File

@ -0,0 +1,53 @@
import React from 'react';
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
searchTerm: '',
};
this.handleInputChange = this.handleInputChange.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
}
handleInputChange(event) {
this.setState({
searchTerm: event.target.value,
});
}
handleKeyPress(event) {
if (event.key === 'Enter') {
this.props.getAlbums(this.state.searchTerm);
}
}
render() {
return (
<div style={SearchBar.styles.div}>
<h3>Search for an Artist</h3>
<input
onChange={this.handleInputChange}
onKeyPress={this.handleKeyPress}
style={SearchBar.styles.input}
/>
</div>
);
}
}
SearchBar.propTypes = {
getAlbums: React.PropTypes.func.isRequired,
};
SearchBar.styles = {
div: {
margin: 30,
textAlign: 'center',
},
input: {
width: '60%',
},
};
export default SearchBar;

View File

@ -0,0 +1,33 @@
import React from 'react';
const mouseOverColor = '#add8e6';
const mouseOutColor = 'white';
const Track = (props) => {
return (
<li
style={Track.styles.li}
onMouseOver={(e) => {e.target.style.backgroundColor = mouseOverColor}}
onMouseOut={(e) => {e.target.style.backgroundColor = mouseOutColor}}
onClick={() => props.playPreview(props.track.preview_url)}
>
{props.track.name}
</li>
);
};
Track.propTypes = {
track: React.PropTypes.object.isRequired,
playPreview: React.PropTypes.func.isRequired,
};
Track.styles = {
li: {
fontSize: '1.5em',
padding: '0.2em',
listStyleType: 'none',
backgroundColor: mouseOutColor,
},
};
export default Track;

View File

@ -0,0 +1,23 @@
import React from 'react';
import Track from './Track';
const TrackList = (props) => {
const tracks = props.tracks.map((track) =>
<Track key={track.key} track={track} playPreview={props.playPreview} />);
return (
<div className="col-md-3">
<ul style={{ listStyle: 'none' }}>
{tracks}
</ul>
</div>
);
};
TrackList.propTypes = {
tracks: React.PropTypes.array.isRequired,
playPreview: React.PropTypes.func.isRequired,
};
export default TrackList;

View File

@ -1,11 +0,0 @@
import React from "react";
export default React.createClass({
render: function() {
return (
<div className="greeting">
Hello, {this.props.name}!
</div>
)
}
})

View File

@ -3,9 +3,9 @@
<head>
<title>Respotify</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" />
</head>
<body>
<h1>Respotify</h1>
<div id="container"></div>
<script src="/bundle.js"></script>
</body>

View File

@ -1,8 +1,73 @@
import React from "react";
import ReactDOM from "react-dom";
import Greeting from "./greeting";
import React from 'react';
import ReactDOM from 'react-dom';
import SearchBar from './components/SearchBar';
import AlbumList from './components/AlbumList';
import TrackList from './components/TrackList';
import * as musicApi from './api/musicApi';
class App extends React.Component {
constructor() {
super();
this.state = ({
albums: [],
tracks: [],
currentPreview: null,
});
this.getAlbums = this.getAlbums.bind(this);
this.processAlbums = this.processAlbums.bind(this);
this.getTracks = this.getTracks.bind(this);
this.processTracks = this.processTracks.bind(this);
this.playPreview = this.playPreview.bind(this);
}
getAlbums(artist) {
musicApi.getAlbums(artist, this.processAlbums);
}
getTracks(albumId) {
musicApi.getTracks(albumId, this.processTracks);
}
processAlbums(payload) {
this.setState({
albums: payload.albums.items,
tracks: [],
});
}
processTracks(payload) {
this.setState({
tracks: payload.tracks.items,
});
}
playPreview(previewUrl) {
if (this.state.currentPreview) {
const curAudioObject = this.state.currentPreview;
curAudioObject.pause();
}
const newAudioObject = new Audio(previewUrl);
this.setState({
currentPreview: newAudioObject
});
newAudioObject.play();
}
render() {
return (
<div>
<SearchBar getAlbums={this.getAlbums} />
<AlbumList albums={this.state.albums} getTracks={this.getTracks} />
<TrackList tracks={this.state.tracks} playPreview={this.playPreview} />
</div>
);
}
}
ReactDOM.render(
<Greeting name="World" />,
<App />,
document.getElementById('container')
);

View File

@ -20,7 +20,17 @@ module.exports = {
devServer: {
contentBase: PATHS.dist
},
eslint: {
emitWarning: true
},
module: {
preloaders: [
{
test: /\.js$/,
loaders: ['eslint-loader'],
exclude: /node_modules/
}
],
loaders: [
{
test: /\.html$/,