diff --git a/src/main.css b/src/main.css index 3067c8c..c8b96dc 100644 --- a/src/main.css +++ b/src/main.css @@ -21,10 +21,13 @@ body { height: 100%; } -a:link, a:visited, a:active { +a:link, a:visited, a:active, a:hover { color: #fff; text-decoration: none; } +a.active { + background: #444; +} /* App */ #app { @@ -64,6 +67,7 @@ a:link, a:visited, a:active { .queue { height: 100%; overflow: auto; + -webkit-overflow-scrolling: touch; font-size: 2rem; } .queue-item { @@ -206,14 +210,20 @@ header > h1 { display: flex; flex-direction: row; align-items: center; + background: #fff; + border-radius: 2em; + padding-right: .5em; + font-size: 1.5rem; +} +.searchForm .throb { + color: #000; } .query { flex: 1; font: inherit; - font-size: 1.5rem; + background: transparent; + border: 0 none; padding: .5rem 2rem; - border-radius: 2em; - border: 0; } .results-container { overflow: hidden; @@ -222,15 +232,24 @@ header > h1 { } .results { overflow: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; height: 100%; font-size: 2rem; } .results li { padding: .5rem 0; + display: flex; + flex-direction: row; + overflow: hidden; } .results h2 { font-style: italic; } +.results a { + display: block; + flex: 1; +} h1 { font-size: 2.5rem; } @@ -238,6 +257,28 @@ h2 { font-size: 2.25rem; color: #ddd; } +.results .throb { + color: #fff; +} + +/* loading */ +.throb { + width: 1.5em; + height: 1.5em; + background-color: red; + flex-shrink: 0; + background: transparent; + border: .25em solid currentColor; + border-radius: 2em; + border-color: currentColor transparent currentColor transparent; + animation: 500ms spin linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} @media (max-device-width: 480px) { html { diff --git a/src/main.js b/src/main.js index e7cc947..a353ded 100644 --- a/src/main.js +++ b/src/main.js @@ -5,6 +5,16 @@ function extend(o, n) { return o; } +function classSet(o) { + var c = []; + for (var k in o) { + if (!!o[k]) { + c.push(k); + } + } + return c.join(' '); +} + var App = React.createClass({ setMode: function (mode) { this.setState(extend(this.state, { @@ -196,7 +206,8 @@ var SearchPanel = React.createClass({ return { artist: [], song: [], - query: '' + query: '', + pending: false }; }, searchArtist: function (artistId) { @@ -211,17 +222,25 @@ var SearchPanel = React.createClass({ venue: this.props.zone.id }); }, - onChange: function (e) { - var q = e.target.value; - this.setState(extend(this.state, { - query: q - })); - if (q.length > 2) { + updateQuery: function (q) { + if (q.length > 0) { + this.setState(extend(this.state, { + query: q + })); this.performSearch(); } }, + onChange: function (e) { + console.log(e); + var q = e.target.value; + clearTimeout(this.to); + this.to = setTimeout(this.updateQuery.bind(this, q), 200); + }, performSearch: function () { console.log('search', this.state.query); + this.setState(extend(this.state, { + pending: true + })); socket.emit('search', { query: this.state.query, venue: this.props.zone.id @@ -230,25 +249,37 @@ var SearchPanel = React.createClass({ componentDidMount: function() { var self = this; socket.on('results', function (data) { - console.log(data.query, self.state.query); if (data.query === self.state.query) { - console.log(data); - self.setState(extend(self.state, data)); + console.log('results', data); + self.setState(extend(self.state, { + artist: data.artist, + song: data.song, + pending: false + })); } }); }, render: function () { + if (!this.props.zone) { + return
; + } + var pending = ''; + if (this.state.pending) { + pending = 'pending'; + } return (
Back

Search

-
- +
+ + {this.state.pending ? : ''}
@@ -258,15 +289,22 @@ var SearchPanel = React.createClass({ }); var SearchResults = React.createClass({ - pickSong: function (songId, e) { + pickSong: function (songId) { this.props.pickSong(songId); }, listArtist: function (artistId, e) { + e.preventDefault(); this.props.searchArtist(artistId); }, render: function () { var artists = this.props.results.artist; var songs = this.props.results.song; + if (!artists) { + artists = []; + } + if (!songs) { + songs = []; + } return (

Artists ({artists.length})

@@ -274,7 +312,14 @@ var SearchResults = React.createClass({ {artists.filter(function (r) { return r.bEnabled !== false; }).map(function (r) { - return
  • {r.sName}
  • ; + return ( +
  • + + {r.sName} + +
  • + ); }, this)}

    Songs ({songs.length})

    @@ -282,7 +327,15 @@ var SearchResults = React.createClass({ {songs.filter(function (r) { return r.bEnabled !== false; }).map(function (r) { - return
  • {r.sArtist} - {r.sName}
  • ; + var picked = false; + if (this.props.queue) { + this.props.queue.forEach(function (q) { + if (q.sSong === r.sName && q.sArtist === r.sArtist) { + picked = true; + } + }); + } + return ; }, this)}
    @@ -290,6 +343,44 @@ var SearchResults = React.createClass({ } }); +var SongResult = React.createClass({ + getInitialState: function () { + return { + pending: false + }; + }, + pick: function (e) { + e.preventDefault(); + this.props.pick(this.props.song.idSong); + this.setState({ + pending: true + }); + }, + render: function () { + var song = this.props.song; + var pending = this.state.pending && !this.props.picked; + var classes = classSet({ + 'picked': this.props.picked, + 'pending': pending + }); + return ( +
  • + + {song.sArtist} - {song.sName} + + {pending ? : ''} + {this.props.picked ?
    : ''} +
  • + ); + } +}); + +var Throbber = React.createClass({ + render: function () { + return
    ; + } +}); var socket = io.connect('/'); var o = React.createElement(Overview); diff --git a/static/main.css b/static/main.css index 1407c38..01c8eba 100644 --- a/static/main.css +++ b/static/main.css @@ -21,10 +21,13 @@ body { height: 100%; } -a:link, a:visited, a:active { +a:link, a:visited, a:active, a:hover { color: #fff; text-decoration: none; } +a.active { + background: #444; +} /* App */ #app { @@ -84,6 +87,7 @@ a:link, a:visited, a:active { .queue { height: 100%; overflow: auto; + -webkit-overflow-scrolling: touch; font-size: 2rem; } .queue-item { @@ -320,6 +324,13 @@ header > h1 { -webkit-align-items: center; -ms-flex-align: center; align-items: center; + background: #fff; + border-radius: 2em; + padding-right: .5em; + font-size: 1.5rem; +} +.searchForm .throb { + color: #000; } .query { -webkit-box-flex: 1; @@ -327,10 +338,9 @@ header > h1 { -ms-flex: 1; flex: 1; font: inherit; - font-size: 1.5rem; + background: transparent; + border: 0 none; padding: .5rem 2rem; - border-radius: 2em; - border: 0; } .results-container { overflow: hidden; @@ -342,15 +352,34 @@ header > h1 { } .results { overflow: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; height: 100%; font-size: 2rem; } .results li { padding: .5rem 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + overflow: hidden; } .results h2 { font-style: italic; } +.results a { + display: block; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} h1 { font-size: 2.5rem; } @@ -358,6 +387,40 @@ h2 { font-size: 2.25rem; color: #ddd; } +.results .throb { + color: #fff; +} + +/* loading */ +.throb { + width: 1.5em; + height: 1.5em; + background-color: red; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + background: transparent; + border: .25em solid currentColor; + border-radius: 2em; + border-color: currentColor transparent currentColor transparent; + -webkit-animation: 500ms spin linear infinite; + animation: 500ms spin linear infinite; +} + +@-webkit-keyframes spin { + + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes spin { + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} @media (max-device-width: 480px) { html { diff --git a/static/main.js b/static/main.js index 4db3dac..3081bc4 100644 --- a/static/main.js +++ b/static/main.js @@ -5,6 +5,16 @@ function extend(o, n) { return o; } +function classSet(o) { + var c = []; + for (var k in o) { + if (!!o[k]) { + c.push(k); + } + } + return c.join(' '); +} + var App = React.createClass({displayName: "App", setMode: function (mode) { this.setState(extend(this.state, { @@ -196,7 +206,8 @@ var SearchPanel = React.createClass({displayName: "SearchPanel", return { artist: [], song: [], - query: '' + query: '', + pending: false }; }, searchArtist: function (artistId) { @@ -211,17 +222,25 @@ var SearchPanel = React.createClass({displayName: "SearchPanel", venue: this.props.zone.id }); }, - onChange: function (e) { - var q = e.target.value; - this.setState(extend(this.state, { - query: q - })); - if (q.length > 2) { + updateQuery: function (q) { + if (q.length > 0) { + this.setState(extend(this.state, { + query: q + })); this.performSearch(); } }, + onChange: function (e) { + console.log(e); + var q = e.target.value; + clearTimeout(this.to); + this.to = setTimeout(this.updateQuery.bind(this, q), 200); + }, performSearch: function () { console.log('search', this.state.query); + this.setState(extend(this.state, { + pending: true + })); socket.emit('search', { query: this.state.query, venue: this.props.zone.id @@ -230,25 +249,37 @@ var SearchPanel = React.createClass({displayName: "SearchPanel", componentDidMount: function() { var self = this; socket.on('results', function (data) { - console.log(data.query, self.state.query); if (data.query === self.state.query) { - console.log(data); - self.setState(extend(self.state, data)); + console.log('results', data); + self.setState(extend(self.state, { + artist: data.artist, + song: data.song, + pending: false + })); } }); }, render: function () { + if (!this.props.zone) { + return React.createElement("div", null); + } + var pending = ''; + if (this.state.pending) { + pending = 'pending'; + } return ( React.createElement("section", {className: "search"}, React.createElement("header", null, React.createElement("a", {href: "#", onClick: this.go.bind(this, 'detail')}, "Back"), React.createElement("h1", null, "Search") ), - React.createElement("div", {className: "searchForm"}, - React.createElement("input", {className: "query", onChange: this.onChange, value: this.state.query}) + React.createElement("div", {className: "searchForm " + pending}, + React.createElement("input", {className: "query", onChange: this.onChange}), + this.state.pending ? React.createElement(Throbber, null) : '' ), React.createElement("div", {className: "results-container"}, React.createElement(SearchResults, {results: this.state, + queue: this.props.zone.aData.aQueue, searchArtist: this.searchArtist, pickSong: this.pickSong}) ) @@ -258,15 +289,22 @@ var SearchPanel = React.createClass({displayName: "SearchPanel", }); var SearchResults = React.createClass({displayName: "SearchResults", - pickSong: function (songId, e) { + pickSong: function (songId) { this.props.pickSong(songId); }, listArtist: function (artistId, e) { + e.preventDefault(); this.props.searchArtist(artistId); }, render: function () { var artists = this.props.results.artist; var songs = this.props.results.song; + if (!artists) { + artists = []; + } + if (!songs) { + songs = []; + } return ( React.createElement("div", {className: "results"}, React.createElement("h2", null, "Artists (", artists.length, ")"), @@ -274,7 +312,14 @@ var SearchResults = React.createClass({displayName: "SearchResults", artists.filter(function (r) { return r.bEnabled !== false; }).map(function (r) { - return React.createElement("li", {onClick: this.listArtist.bind(this, r.idArtist)}, r.sName); + return ( + React.createElement("li", {key: r.idArtist}, + React.createElement("a", {href: "#", + onClick: this.listArtist.bind(this, r.idArtist)}, + r.sName + ) + ) + ); }, this) ), React.createElement("h2", null, "Songs (", songs.length, ")"), @@ -282,7 +327,15 @@ var SearchResults = React.createClass({displayName: "SearchResults", songs.filter(function (r) { return r.bEnabled !== false; }).map(function (r) { - return React.createElement("li", {onClick: this.pickSong.bind(this, r.idSong)}, r.sArtist, " - ", r.sName); + var picked = false; + if (this.props.queue) { + this.props.queue.forEach(function (q) { + if (q.sSong === r.sName && q.sArtist === r.sArtist) { + picked = true; + } + }); + } + return React.createElement(SongResult, {key: r.idSong, picked: picked, song: r, pick: this.pickSong}); }, this) ) ) @@ -290,6 +343,44 @@ var SearchResults = React.createClass({displayName: "SearchResults", } }); +var SongResult = React.createClass({displayName: "SongResult", + getInitialState: function () { + return { + pending: false + }; + }, + pick: function (e) { + e.preventDefault(); + this.props.pick(this.props.song.idSong); + this.setState({ + pending: true + }); + }, + render: function () { + var song = this.props.song; + var pending = this.state.pending && !this.props.picked; + var classes = classSet({ + 'picked': this.props.picked, + 'pending': pending + }); + return ( + React.createElement("li", {key: song.idSong, className: classes}, + React.createElement("a", {href: "#", + onClick: this.pick}, + song.sArtist, " - ", song.sName + ), + pending ? React.createElement(Throbber, null) : '', + this.props.picked ? React.createElement("div", null, "✓") : '' + ) + ); + } +}); + +var Throbber = React.createClass({displayName: "Throbber", + render: function () { + return React.createElement("div", {className: "throb"}); + } +}); var socket = io.connect('/'); var o = React.createElement(Overview);