App.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import React, { Component } from 'react';
  2. import logo from './logo.svg';
  3. import './App.css';
  4. function CategoryFlatList(props) {
  5. const { error, isLoaded, items } = props.state;
  6. var content;
  7. if (error) {
  8. content = <div>Error: {error.message}</div>
  9. } else if (!isLoaded) {
  10. content = <div>Loading</div>
  11. } else {
  12. content = (
  13. <select size="10" defaultValue="" onChange={props.onChange}>
  14. <option value="">[ANY]</option>
  15. {items.map(item => (
  16. <option key={item.item} value={item.item}>{item.item} - {item.count}</option>
  17. ))}
  18. </select>
  19. );
  20. }
  21. return (
  22. <div className="category-list">
  23. <h2>{props.title}</h2>
  24. {content}
  25. </div>
  26. );
  27. }
  28. function formatDuration(duration) {
  29. var seconds = duration % 60;
  30. const minutes = Math.floor(duration / 60);
  31. if (seconds < 10) {
  32. seconds = '0' + seconds;
  33. }
  34. return minutes + ':' + seconds;
  35. }
  36. function mapAlbums(items) {
  37. var artists = [], curArtist = {}, curType = {};
  38. for (const album of items) {
  39. if (album.artist !== curArtist.name) {
  40. curArtist = {name: album.artist, types: []}
  41. curType = {}
  42. artists.push(curArtist)
  43. }
  44. if (album.type !== curType.type) {
  45. curType = {type: album.type, albums: []}
  46. curArtist.types.push(curType)
  47. }
  48. curType.albums.push(album);
  49. }
  50. return artists;
  51. }
  52. function AlbumList(props) {
  53. const { error, isLoaded, items } = props.state;
  54. var content;
  55. if (error) {
  56. content = <div>Error: {error.message}</div>
  57. } else if (!isLoaded) {
  58. content = <div>Loading</div>
  59. } else {
  60. const artists = mapAlbums(items);
  61. content = artists.map(artist => (
  62. <div key={artist.name}>
  63. <h3>{artist.name}</h3>
  64. {artist.types.map(type => (
  65. <div key={type.type}>
  66. <h4>{type.type}</h4>
  67. <table><tbody>
  68. {type.albums.map(album => (
  69. <tr key={album.id}>
  70. <td><img src={album.cover} alt={album.album} /></td>
  71. <td>{album.year}</td>
  72. <td>{album.album}</td>
  73. <td>{album.publisher}</td>
  74. <td>{album.country}</td>
  75. <td>{album.genre}</td>
  76. <td>{album.track_count}</td>
  77. <td>{formatDuration(album.total_duration)}</td>
  78. </tr>
  79. ))}
  80. </tbody></table>
  81. </div>
  82. ))}
  83. </div>
  84. ))
  85. }
  86. return (
  87. <div className="AlbumList">
  88. <h2>{props.title}</h2>
  89. {content}
  90. </div>
  91. );
  92. }
  93. class Player extends Component {
  94. stateLoading = {
  95. error: null,
  96. isLoaded: false,
  97. items: []
  98. };
  99. constructor(props) {
  100. super(props);
  101. this.categories = [{type: 'artist', title: 'Artists'},
  102. {type: 'year', title: 'Years'},
  103. {type: 'genre', title: 'Genres'},
  104. {type: 'publisher', title: 'Labels'},
  105. {type: 'country', title: 'Countries'},
  106. {type: 'type', title: 'Album types'},
  107. {type: 'status', title: 'Album statuses'}];
  108. this.state = {};
  109. for (var {type} of this.categories) {
  110. this.state[type] = Object.assign({}, this.stateLoading);
  111. }
  112. this.state.album = Object.assign({}, this.stateLoading);
  113. this.filterTimeout = null;
  114. }
  115. fetchCategory(cat) {
  116. const {restrictions, filter} = this.state;
  117. function getQueryString(params) {
  118. var esc = encodeURIComponent;
  119. return Object.keys(params)
  120. .map(k => esc(k) + '=' + esc(params[k]))
  121. .join('&');
  122. }
  123. var params = Object.assign({}, restrictions, {offset:0, limit:100});
  124. if (cat === 'album' && filter) {
  125. params.filter = filter;
  126. }
  127. const qs = getQueryString(params);
  128. fetch('/cat/' + cat + (qs ? ('?' + qs) : ''))
  129. .then(res => (res.ok ? res.json() : Promise.reject({message:res.statusText})))
  130. .then(result => {
  131. this.setState({[cat]: {
  132. isLoaded: true,
  133. items: result
  134. }});
  135. })
  136. .catch(error => {
  137. this.setState({[cat]: {
  138. isLoaded: true,
  139. error: error
  140. }});
  141. });
  142. this.setState({[cat]: Object.assign({}, this.stateLoading)});
  143. }
  144. fetchAll() {
  145. const {restrictions} = this.state;
  146. console.log(restrictions);
  147. this.categories.map(({type}) => !restrictions[type] && this.fetchCategory(type))
  148. this.fetchCategory('album');
  149. }
  150. componentDidMount() {
  151. this.setState({restrictions: {}});
  152. }
  153. componentDidUpdate(prevProps, prevState) {
  154. console.log('stateChange');
  155. if (this.state.restrictions !== prevState.restrictions) {
  156. // Reload category filters on restriction changes
  157. this.fetchAll();
  158. } else if (this.state.filter !== prevState.filter) {
  159. this.fetchCategory('album');
  160. }
  161. }
  162. handleCategoryChange(type, value) {
  163. console.log(type, value);
  164. var restrictions = Object.assign({}, this.state.restrictions);
  165. if (value) {
  166. restrictions[type] = value;
  167. } else {
  168. delete restrictions[type];
  169. }
  170. this.setState({restrictions: restrictions});
  171. }
  172. handleFilterChange = (e) => {
  173. if (this.filterTimeout) {
  174. clearInterval(this.filterTimeout);
  175. }
  176. const value = e.target.value;
  177. this.filterTimeout = setTimeout(() => this.setState({filter:value}), 500);
  178. }
  179. render() {
  180. return (
  181. <div className="Player">
  182. <div className="PlayerFilters">
  183. {this.categories.map(({type, title}) => (
  184. <CategoryFlatList key={type} title={title} state={this.state[type]} onChange={e => this.handleCategoryChange(type, e.target.value)}/>
  185. ))}
  186. </div><div style={{clear:'both', paddingBottom:'20px'}}/>
  187. <input type="text" placeholder="Search albums" onChange={this.handleFilterChange} size={150} />
  188. <AlbumList title="Albums" state={this.state.album} />
  189. </div>
  190. );
  191. }
  192. }
  193. class App extends Component {
  194. render() {
  195. return (
  196. <div className="App">
  197. <header className="App-header">
  198. <img src={logo} className="App-logo" alt="logo" />
  199. <h1 className="App-title">Chad Music</h1>
  200. </header>
  201. <Player />
  202. </div>
  203. );
  204. }
  205. }
  206. export default App;