This commit is contained in:
Matt Claypotch 2015-01-16 17:06:06 -08:00
Родитель cb4e0d3789
Коммит b7cdff67c9
6 изменённых файлов: 388 добавлений и 34 удалений

Просмотреть файл

@ -52,17 +52,19 @@ function api(v, m, p, cb) {
qs: p
}, function(err, res, body) {
if (err) {
cb(err);
console.log('api error:', err);
return cb(err);
}
if (body) {
try {
cb(null, JSON.parse(body));
body = JSON.parse(body);
return cb(null, body);
} catch (e) {
cb(e);
return cb(e);
}
} else {
cb(null);
}
throw "whuck";
return cb(null);
});
}
@ -87,21 +89,82 @@ function checkCurrent() {
setTimeout(checkCurrent, 10000);
}
io.on('connect', function(socket) {
io.on('connect', function (socket) {
io.emit('currently', currently);
socket.on('upvote', function(o) {
socket.on('upvote', function (o) {
console.log('upvote', o);
var v = venues[o.venue];
api(o.venue, 'kiosk:add_up_vote', {pick: o.pick}, function (err, res) {
console.log(err, res);
for (var i = 0; i < currently.length; i++) {
if (currently[i].id === o.venue) {
res.name = currently[i].name;
res.id = o.venue;
currently[i] = res;
io.emit('currently', currently);
}
}
});
});
socket.on('downvote', function(o) {
socket.on('downvote', function (o) {
console.log('downvote', o);
var v = venues[o.venue];
api(o.venue, 'kiosk:add_down_vote', {pick: o.pick}, function (err, res) {
console.log(err, res);
for (var i = 0; i < currently.length; i++) {
if (currently[i].id === o.venue) {
res.name = currently[i].name;
res.id = o.venue;
currently[i] = res;
io.emit('currently', currently);
}
}
});
});
socket.on('search', function (o) {
console.log('search', o);
var results = {
query: o.query,
artist: []
};
var match = o.query.match(/^artist:(\d+)/);
if (match) {
api(o.venue, 'kiosk:get_artist', {artist: match[1]}, function (err, res) {
if (err) {
results.song = [];
} else {
results.song = res.aData;
}
socket.emit('results', results);
});
} else {
api(o.venue, 'kiosk:search_artists', {query: o.query}, function (err, res) {
if (err) {
results.artist = [];
} else {
results.artist = res.aData;
}
api(o.venue, 'kiosk:search_songs', {query: o.query}, function (err, res) {
if (err) {
results.song = [];
} else {
results.song = res.aData;
}
socket.emit('results', results);
});
});
}
});
socket.on('pick', function (o) {
api(o.venue, 'kiosk:add_song', {song: o.song}, function (err, res) {
console.log(err, res);
for (var i = 0; i < currently.length; i++) {
if (currently[i].id === o.venue) {
res.name = currently[i].name;
res.id = o.venue;
currently[i] = res;
io.emit('currently', currently);
}
}
});
});
});

Просмотреть файл

@ -8,7 +8,7 @@ function extend(o, n) {
var App = React.createClass({
setMode: function (mode) {
this.setState(extend(this.state, {
mode: 'mode'
mode: mode
}));
},
selectZone: function (zone) {
@ -37,10 +37,12 @@ var App = React.createClass({
});
},
render: function () {
var zone = this.state.zones[this.state.selected];
return (
<div className="app" data-mode={this.state.mode}>
<Overview zones={this.state.zones} selectZone={this.selectZone} />
<ZoneDetail zone={this.state.zones[this.state.selected]} go={this.setMode} />
<ZoneDetail zone={zone} go={this.setMode} />
<SearchPanel zone={zone} go={this.setMode} />
</div>
);
}
@ -102,18 +104,18 @@ var QueueItem = React.createClass({
<div className="info">{a.sArtist} - {a.sSong}</div>
<User avatar={a.sUserImage} name={a.sUser} />
<a className="up" href="#"
onClick={this.upVote.bind(this, a.idPick)}>👍 {a.iLikes}</a>
onClick={this.upVote.bind(this, a.idPick)}>{a.iLikes}</a>
<a className="down" href="#"
onClick={this.downVote.bind(this, a.idPick)}>👎 {a.iDislikes}</a>
onClick={this.downVote.bind(this, a.idPick)}>{a.iDislikes}</a>
</li>
);
}
});
var ZoneDetail = React.createClass({
back: function (e) {
go: function (mode, e) {
e.preventDefault();
this.props.go('overview');
this.props.go(mode);
},
upvote: function (pick) {
console.log('upvoting pick ' + pick);
@ -136,8 +138,9 @@ var ZoneDetail = React.createClass({
return (
<section className="detail">
<header>
<a href="#" onClick={this.back}>Back</a>
<a href="#" onClick={this.go.bind(this, 'overview')}>Back</a>
<h1>{z.name}</h1>
<a href="#" onClick={this.go.bind(this, 'search')}>Search</a>
</header>
<div className="nowplaying">
<AlbumArt url={now.sArtwork} big={true} />
@ -146,7 +149,7 @@ var ZoneDetail = React.createClass({
<User className="user" avatar={now.sUserImage} name={now.sUser} />
</div>
</div>
<h1 className="upnext">Up Next</h1>
<h2 className="upnext">Up Next</h2>
<div className="queue-container">
<ol className="queue">
{queue.map(function (item) {
@ -184,6 +187,110 @@ var User = React.createClass({
}
});
var SearchPanel = React.createClass({
go: function (mode, e) {
e.preventDefault();
this.props.go(mode);
},
getInitialState: function () {
return {
artist: [],
song: [],
query: ''
};
},
searchArtist: function (artistId) {
this.setState(extend(this.state, {
query: 'artist:' + artistId
}));
this.performSearch();
},
pickSong: function (songId) {
socket.emit('pick', {
song: songId,
venue: this.props.zone.id
});
},
onChange: function (e) {
var q = e.target.value;
this.setState(extend(this.state, {
query: q
}));
if (q.length > 2) {
this.performSearch();
}
},
performSearch: function () {
console.log('search', this.state.query);
socket.emit('search', {
query: this.state.query,
venue: this.props.zone.id
});
},
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));
}
});
},
render: function () {
return (
<section className="search">
<header>
<a href="#" onClick={this.go.bind(this, 'detail')}>Back</a>
<h1>Search</h1>
</header>
<form className="searchForm">
<input className="query" onChange={this.onChange} value={this.state.query} />
<button className="submit">submit</button>
</form>
<div className="results-container">
<SearchResults results={this.state}
searchArtist={this.searchArtist}
pickSong={this.pickSong} />
</div>
</section>
);
}
});
var SearchResults = React.createClass({
pickSong: function (songId, e) {
this.props.pickSong(songId);
},
listArtist: function (artistId, e) {
this.props.searchArtist(artistId);
},
render: function () {
var artists = this.props.results.artist;
var songs = this.props.results.song;
return (
<div className="results">
<h2>Artists ({artists.length})</h2>
<ul>
{artists.filter(function (r) {
return r.bEnabled !== false;
}).map(function (r) {
return <li onClick={this.listArtist.bind(this, r.idArtist)}>{r.sName}</li>;
}, this)}
</ul>
<h2>Songs ({songs.length})</h2>
<ul>
{songs.filter(function (r) {
return r.bEnabled !== false;
}).map(function (r) {
return <li onClick={this.pickSong.bind(this, r.idSong)}>{r.sArtist} - {r.sName}</li>;
}, this)}
</ul>
</div>
);
}
});
var socket = io.connect('/');
var o = React.createElement(Overview);

Просмотреть файл

@ -4,6 +4,10 @@
box-sizing: border-box;
}
html {
font-size: 2vmin;
}
html, body {
height: 100%;
margin: 0;
@ -14,7 +18,6 @@ body {
color: #fff;
font-family: 'Fira Sans', sans-serif;
overflow: hidden;
font-size: 4vmin;
height: 100%;
}
@ -47,6 +50,10 @@ a:link, a:visited, a:active {
.app[data-mode="detail"] {
transform: translate(-100vw, 0);
}
.app[data-mode="search"] {
transform: translate(-200vw, 0);
}
/* Queue */
.queue-container {
@ -57,7 +64,7 @@ a:link, a:visited, a:active {
.queue {
height: 100%;
overflow: auto;
font-size: 4vmin;
font-size: 2rem;
}
.queue-item {
display: flex;
@ -78,6 +85,16 @@ a:link, a:visited, a:active {
white-space: nowrap;
width: 10vmin;
text-align: center;
padding-left: 1.2em;
background-position: left center;
background-size: auto 2rem;
background-repeat: no-repeat;
}
.queue .up {
background-image: url(/thumbs-up.png);
}
.queue .down {
background-image: url(/thumbs-down.png);
}
/* Detail */
@ -93,7 +110,6 @@ header {
display: flex;
flex-direction: row;
align-items: center;
font-size: 3vmin;
}
header > * + * {
margin-left: 1em;
@ -101,6 +117,7 @@ header > * + * {
header a {
padding: 1rem;
margin: -1rem;
font-size: 1.5rem;
}
header > h1 {
flex: 1;
@ -112,7 +129,7 @@ header > h1 {
}
.upnext {
font-style: italic;
font-size: 5vmin;
font-size: 2.5rem;
margin-top: 2rem;
}
.nowplaying {
@ -121,8 +138,8 @@ header > h1 {
}
.nowplaying .info {
display: flex;
font-size: 4vmin;
line-height: 6vmin;
font-size: 2rem;
line-height: 3rem;
margin-left: 1em;
flex-direction: column;
justify-content: flex-end;
@ -151,12 +168,13 @@ header > h1 {
}
.overview .zone-item {
flex: 1;
font-size: 4vmin;
font-size: 2rem;
}
.zone-item > div {
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
}
.zone-item .art {
height: 20vh;
@ -164,3 +182,62 @@ header > h1 {
flex-shrink: 0;
margin-right: 1em;
}
/* Search */
.search {
padding: 1rem;
display: flex;
flex-direction: column;
}
.search > * + * {
margin-top: 1rem;
}
.submit {
background: #000;
border: 0;
font: inherit;
font-size: 1.5rem;
color: inherit;
cursor: pointer;
padding: 1rem;
margin-left: 1rem;
}
.searchForm {
display: flex;
flex-direction: row;
align-items: center;
}
.query {
flex: 1;
font: inherit;
font-size: 1.5rem;
padding: .5rem 2rem;
border-radius: 2em;
border: 0;
}
.results-container {
overflow: hidden;
flex: 1;
position: relative;
}
.results {
overflow: auto;
height: 100%;
font-size: 2rem;
}
.results h2 {
font-style: italic;
}
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 2.25rem;
color: #ddd;
}
@media (max-device-width: 480px) {
.queue {
font-size: 2.5rem;
}
}

Просмотреть файл

@ -8,7 +8,7 @@ function extend(o, n) {
var App = React.createClass({displayName: "App",
setMode: function (mode) {
this.setState(extend(this.state, {
mode: 'mode'
mode: mode
}));
},
selectZone: function (zone) {
@ -37,10 +37,12 @@ var App = React.createClass({displayName: "App",
});
},
render: function () {
var zone = this.state.zones[this.state.selected];
return (
React.createElement("div", {className: "app", "data-mode": this.state.mode},
React.createElement(Overview, {zones: this.state.zones, selectZone: this.selectZone}),
React.createElement(ZoneDetail, {zone: this.state.zones[this.state.selected], go: this.setMode})
React.createElement(ZoneDetail, {zone: zone, go: this.setMode}),
React.createElement(SearchPanel, {zone: zone, go: this.setMode})
)
);
}
@ -102,18 +104,18 @@ var QueueItem = React.createClass({displayName: "QueueItem",
React.createElement("div", {className: "info"}, a.sArtist, " - ", a.sSong),
React.createElement(User, {avatar: a.sUserImage, name: a.sUser}),
React.createElement("a", {className: "up", href: "#",
onClick: this.upVote.bind(this, a.idPick)}, "👍 ", a.iLikes),
onClick: this.upVote.bind(this, a.idPick)}, a.iLikes),
React.createElement("a", {className: "down", href: "#",
onClick: this.downVote.bind(this, a.idPick)}, "👎 ", a.iDislikes)
onClick: this.downVote.bind(this, a.idPick)}, a.iDislikes)
)
);
}
});
var ZoneDetail = React.createClass({displayName: "ZoneDetail",
back: function (e) {
go: function (mode, e) {
e.preventDefault();
this.props.go('overview');
this.props.go(mode);
},
upvote: function (pick) {
console.log('upvoting pick ' + pick);
@ -136,8 +138,9 @@ var ZoneDetail = React.createClass({displayName: "ZoneDetail",
return (
React.createElement("section", {className: "detail"},
React.createElement("header", null,
React.createElement("a", {href: "#", onClick: this.back}, "Back"),
React.createElement("h1", null, z.name)
React.createElement("a", {href: "#", onClick: this.go.bind(this, 'overview')}, "Back"),
React.createElement("h1", null, z.name),
React.createElement("a", {href: "#", onClick: this.go.bind(this, 'search')}, "Search")
),
React.createElement("div", {className: "nowplaying"},
React.createElement(AlbumArt, {url: now.sArtwork, big: true}),
@ -146,7 +149,7 @@ var ZoneDetail = React.createClass({displayName: "ZoneDetail",
React.createElement(User, {className: "user", avatar: now.sUserImage, name: now.sUser})
)
),
React.createElement("h1", {className: "upnext"}, "Up Next"),
React.createElement("h2", {className: "upnext"}, "Up Next"),
React.createElement("div", {className: "queue-container"},
React.createElement("ol", {className: "queue"},
queue.map(function (item) {
@ -184,6 +187,110 @@ var User = React.createClass({displayName: "User",
}
});
var SearchPanel = React.createClass({displayName: "SearchPanel",
go: function (mode, e) {
e.preventDefault();
this.props.go(mode);
},
getInitialState: function () {
return {
artist: [],
song: [],
query: ''
};
},
searchArtist: function (artistId) {
this.setState(extend(this.state, {
query: 'artist:' + artistId
}));
this.performSearch();
},
pickSong: function (songId) {
socket.emit('pick', {
song: songId,
venue: this.props.zone.id
});
},
onChange: function (e) {
var q = e.target.value;
this.setState(extend(this.state, {
query: q
}));
if (q.length > 2) {
this.performSearch();
}
},
performSearch: function () {
console.log('search', this.state.query);
socket.emit('search', {
query: this.state.query,
venue: this.props.zone.id
});
},
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));
}
});
},
render: function () {
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("form", {className: "searchForm"},
React.createElement("input", {className: "query", onChange: this.onChange, value: this.state.query}),
React.createElement("button", {className: "submit"}, "submit")
),
React.createElement("div", {className: "results-container"},
React.createElement(SearchResults, {results: this.state,
searchArtist: this.searchArtist,
pickSong: this.pickSong})
)
)
);
}
});
var SearchResults = React.createClass({displayName: "SearchResults",
pickSong: function (songId, e) {
this.props.pickSong(songId);
},
listArtist: function (artistId, e) {
this.props.searchArtist(artistId);
},
render: function () {
var artists = this.props.results.artist;
var songs = this.props.results.song;
return (
React.createElement("div", {className: "results"},
React.createElement("h2", null, "Artists (", artists.length, ")"),
React.createElement("ul", null,
artists.filter(function (r) {
return r.bEnabled !== false;
}).map(function (r) {
return React.createElement("li", {onClick: this.listArtist.bind(this, r.idArtist)}, r.sName);
}, this)
),
React.createElement("h2", null, "Songs (", songs.length, ")"),
React.createElement("ul", null,
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);
}, this)
)
)
);
}
});
var socket = io.connect('/');
var o = React.createElement(Overview);

Двоичные данные
static/thumbs-down.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.2 KiB

Двоичные данные
static/thumbs-up.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.1 KiB