diff --git a/app.js b/app.js index bf5eb6d..58d69ec 100644 --- a/app.js +++ b/app.js @@ -17,6 +17,10 @@ var search = require('./routes/search'); var share = require('./routes/share'); var itunesProxy = require('./routes/itunes-proxy'); +var React = require('react'); +var nodejsx = require('node-jsx').install({extension: '.jsx'}); +var ErrorView = React.createFactory(require('./views/error.jsx')); + var browserify = require('connect-browserify'); var app = express(); @@ -50,7 +54,7 @@ app.use(function(req, res, next) { if (development) { app.get('/javascript/bundle.js', - browserify('./client/index.jsx', { + browserify('./views/app.jsx', { debug: true, watch: true })); @@ -75,9 +79,17 @@ app.get('*', function(req,res,next) { app.get('/', index); app.post('/search', search); -app.get('/:service/:type/:id.json', share.json); -app.get('/:service/:type/:id', share.html); +app.get('/:service/:type/:id', share); app.get('/itunes/*', itunesProxy); +app.get('/recent', function(req, res, next) { + req.db.matches.find().sort({created_at:-1}).limit(6).toArray().then(function(docs){ + var recents = []; + docs.forEach(function(doc) { + recents.push(doc.services[doc._id.split("$$")[0]]); + }); + res.json({recents:recents}); + }); +}); // catch 404 and forward to error handler app.use(function(req, res, next) { @@ -93,11 +105,9 @@ app.use(function(req, res, next) { if (app.get('env') === 'development') { app.use(function(err, req, res, next) { res.status(err.status || 500); - res.render('error', { - page: "error", - message: err.message, - error: err - }); + + var content = React.renderToString(ErrorView({status: err.status || 500, message: err.message, error: err})); + res.send('<!doctype html>\n' + content); }); } @@ -105,11 +115,9 @@ if (app.get('env') === 'development') { // no stacktraces leaked to user app.use(function(err, req, res, next) { res.status(err.status || 500); - res.render('error', { - page: "error", - message: err.message, - error: {status: err.status || 500} - }); + + var content = React.renderToString(ErrorView({status: err.status || 500, message: err.message, error: {status: err.status || 500}})); + res.send('<!doctype html>\n' + content); }); diff --git a/client/index.jsx b/client/index.jsx deleted file mode 100644 index d40318b..0000000 --- a/client/index.jsx +++ /dev/null @@ -1,155 +0,0 @@ -/** - * @jsx React.DOM - */ -'use strict'; - -var React = require('react'); -var Router = require('react-router'); -var request = require('superagent'); -var ga = require('react-google-analytics'); -var GAInitiailizer = ga.Initializer; - -var Head = React.createClass({ - - render: function() { - return ( - <head> - <meta charSet="utf-8" /> - <meta httpEquiv="X-UA-Compatible" content="IE=edge" /> - <title>Match Audio</title> - <meta name="description" content="" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <meta name="twitter:card" content="summary_large_image" /> - <meta name="twitter:site" content="@MatchAudio" /> - <meta name="twitter:title" property="og:title" content="" /> - <meta name="twitter:description" property="og:description" content="We've matched this music on Rdio, Spotify, Deezer, Beats Music, Google Music and iTunes so you can open it in the service you use." /> - <meta name="twitter:image:src" property="og:image" content="" /> - <meta property="og:url" content="" /> - <link rel="shortcut icon" href="/images/favicon.png" /> - <link href='//fonts.googleapis.com/css?family=Open+Sans:400,300,700' rel='stylesheet' type='text/css' /> - <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" /> - <link rel="stylesheet" href="/stylesheets/style.css" /> - </head> - ); - } -}); - -var Foot = React.createClass({ - - render: function() { - return ( - <footer> - <div className="container"> - <div className="row"> - <div className={this.props.page == "home" ? "col-md-6 col-md-offset-3" : "col-md-12"}> - <a href="https://twitter.com/MatchAudio">Tweet</a> or <a href="https://github.com/kudos/match.audio">Fork</a>. A work in progress by <a href="http://crem.in">this guy</a>. - </div> - </div> - </div> - </footer> - ); - } -}); - - -var Recent = React.createClass({ - - render: function() { - return (<div className="row"> - <div className="col-md-6 col-md-offset-3"> - <h2>Recently Shared</h2> - <div className="row recent"> - {this.props.items.map(function(item, i){ - return (<RecentItem item={item} key={i} />); - })} - </div> - </div> - </div>); - } - -}); - -var RecentItem = React.createClass({ - - render: function() { - return ( - <div className="col-sm-4 col-xs-6"> - <a href={"/" + this.props.item.service + "/" + this.props.item.type + "/" + this.props.item.id}><img src={this.props.item.artwork.small} width="100%" /></a> - </div> - ); - } - -}); - -var SearchForm = React.createClass({ - - handleSubmit: function(e) { - e.preventDefault(); - var url = this.refs.url.getDOMNode().value.trim(); - if (!url) { - return; - } - request.post('/search').send({url:url}).end(function(res) { - window.location = "/" + res.body.service + "/" + res.body.type + "/" + res.body.id - }); - }, - - render: function() { - return ( - <form role="form" method="post" action="/search" onSubmit={this.handleSubmit}> - <div className="input-group input-group-lg"> - <input type="text" name="url" placeholder="Paste link here" className="form-control" autofocus ref="url" /> - <span className="input-group-btn"> - <input type="submit" className="btn btn-lg btn-custom" value="Share Music" /> - </span> - </div> - </form> - ); - } -}); - -var Home = React.createClass({ - - render: function() { - return ( - <html> - <Head /> - <body className="home"> - <div className="page-wrap"> - <header> - <h1><a href="/">match<span className="audio-lighten">.audio</span></a></h1> - </header> - <div className="container"> - <div className="row share-form"> - <div className="col-md-6 col-md-offset-3"> - <SearchForm /> - </div> - </div> - <div className="row blurb"> - <div className="col-md-6 col-md-offset-3"> - <p>Make sharing from music services better. - We match album and track links from Rdio, Spotify, Deezer, Beats Music, Google Music and iTunes and give you back a link with all of them. - </p> - </div> - </div> - <Recent items={this.props.recent} /> - </div> - </div> - <Foot page="home" /> - <GAInitiailizer /> - <script src="/javascript/bundle.js" /> - </body> - </html> - ); - } -}); - -module.exports.Home = Home; - -if (typeof window !== 'undefined') { - window.onload = function() { - React.render(<Home recent={recent} />, document); - ga('create', 'UA-66209-8', 'auto'); - ga('send', 'pageview'); - } -} \ No newline at end of file diff --git a/client/share.jsx b/client/share.jsx deleted file mode 100644 index e26a460..0000000 --- a/client/share.jsx +++ /dev/null @@ -1,153 +0,0 @@ -/** - * @jsx React.DOM - */ -'use strict'; - -var React = require('react'); -var Router = require('react-router'); -var request = require('superagent'); -var ga = require('react-google-analytics'); -var GAInitiailizer = ga.Initializer; - -var Head = React.createClass({ - - render: function() { - return ( - <head> - <meta charSet="utf-8" /> - <meta httpEquiv="X-UA-Compatible" content="IE=edge" /> - <title>Match Audio</title> - <meta name="description" content="" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <meta name="twitter:card" content="summary_large_image" /> - <meta name="twitter:site" content="@MatchAudio" /> - <meta name="twitter:title" property="og:title" content="" /> - <meta name="twitter:description" property="og:description" content="We've matched this music on Rdio, Spotify, Deezer, Beats Music, Google Music and iTunes so you can open it in the service you use." /> - <meta name="twitter:image:src" property="og:image" content="" /> - <meta property="og:url" content="" /> - <link rel="shortcut icon" href="/images/favicon.png" /> - <link href='//fonts.googleapis.com/css?family=Open+Sans:400,300,700' rel='stylesheet' type='text/css' /> - <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" /> - <link rel="stylesheet" href="/stylesheets/style.css" /> - </head> - ); - } -}); - -var Foot = React.createClass({ - - render: function() { - return ( - <footer> - <div className="container"> - <div className="row"> - <div className={this.props.page == "home" ? "col-md-6 col-md-offset-3" : "col-md-12"}> - <a href="https://twitter.com/MatchAudio">Tweet</a> or <a href="https://github.com/kudos/match.audio">Fork</a>. A work in progress by <a href="http://crem.in">this guy</a>. - </div> - </div> - </div> - </footer> - ); - } -}); - - -var Recent = React.createClass({ - - render: function() { - return (<div className="row"> - <div className="col-md-6 col-md-offset-3"> - <h2>Recently Shared</h2> - <div className="row recent"> - - </div> - </div> - </div>); - } - -}); - -var RecentItem = React.createClass({ - - render: function() { - return ( - <div className="col-sm-4 col-xs-6"> - <a href={"/" + this.props.item.service + "/" + this.props.item.type + "/" + this.props.item.id}><img src={this.props.item.artwork.small} width="100%" /></a> - </div> - ); - } - -}); - -var SearchForm = React.createClass({ - - handleSubmit: function(e) { - e.preventDefault(); - var url = this.refs.url.getDOMNode().value.trim(); - if (!url) { - return; - } - request.post('/search').send({url:url}).end(function(res) { - window.location = "/" + res.body.service + "/" + res.body.type + "/" + res.body.id - }); - }, - - render: function() { - return ( - <form role="form" method="post" action="/search" onSubmit={this.handleSubmit}> - <div className="input-group input-group-lg"> - <input type="text" name="url" placeholder="Paste link here" className="form-control" autofocus ref="url" /> - <span className="input-group-btn"> - <input type="submit" className="btn btn-lg btn-custom" value="Share Music" /> - </span> - </div> - </form> - ); - } -}); - -var Share = React.createClass({ - - render: function() { - return ( - <html> - <Head /> - <body className="home"> - <div className="page-wrap"> - <header> - <h1><a href="/">match<span className="audio-lighten">.audio</span></a></h1> - </header> - <div className="container"> - <div className="row share-form"> - <div className="col-md-6 col-md-offset-3"> - <h1>share</h1> - </div> - </div> - <div className="row blurb"> - <div className="col-md-6 col-md-offset-3"> - <p>Make sharing from music services better. - We match album and track links from Rdio, Spotify, Deezer, Beats Music, Google Music and iTunes and give you back a link with all of them. - </p> - </div> - </div> - <Recent items={this.props.recent} /> - </div> - </div> - <Foot page="home" /> - <GAInitiailizer /> - <script src="/javascript/bundle.js" /> - </body> - </html> - ); - } -}); - -module.exports.Share = Share; - -if (typeof window !== 'undefined') { - window.onload = function() { - React.render(<Share items={items} />, document); - ga('create', 'UA-66209-8', 'auto'); - ga('send', 'pageview'); - } -} \ No newline at end of file diff --git a/lib/services/xbox/index.js b/lib/services/xbox/index.js index cfefc8e..cad53fa 100644 --- a/lib/services/xbox/index.js +++ b/lib/services/xbox/index.js @@ -43,8 +43,8 @@ var formatResponse = function(res) { streamUrl: result.Link, purchaseUrl: null, artwork: { - small: result.ImageUrl.replace("http://", "https://") + "?w=250", - large: result.ImageUrl.replace("http://", "https://") + "?w=500" + small: result.ImageUrl.replace("http://", "https://") + "&w=250&h=250", + large: result.ImageUrl.replace("http://", "https://") + "&w=500&h=250" }, artist: { name: result.Artists[0].Artist.Name diff --git a/package.json b/package.json index 79bfd27..d4f8c1e 100644 --- a/package.json +++ b/package.json @@ -37,11 +37,10 @@ "node-jsx": "^0.12.4", "node-uuid": "^1.4.2", "promised-mongo": "^0.11.1", - "querystring": "^0.2.0", "rdio": "^1.5.2", "react": "^0.12.1", "react-google-analytics": "^0.2.0", - "react-router": "^0.11.4", + "react-router": "^0.11.6", "serve-favicon": "~2.1.3", "spotify": "^0.3.0", "superagent": "^0.21.0", diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index 3d7dd64..24821f7 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -60,7 +60,7 @@ header h1 a:hover, header h1 a:focus{ outline: 0 none; } -.album header h1, .track header h1 { +.share header h1, .share header h1 { text-align: left; font-size: 1.5em; line-height: 36px; diff --git a/routes/index.js b/routes/index.js index f955d23..949d926 100644 --- a/routes/index.js +++ b/routes/index.js @@ -2,18 +2,23 @@ var React = require('react'); var nodejsx = require('node-jsx').install({extension: '.jsx'}); -var Home = React.createFactory(require('../client/index.jsx').Home); +var Router = require('react-router'); +var routes = require('../views/app.jsx').routes; module.exports = function(req, res, next) { req.db.matches.find().sort({created_at:-1}).limit(6).toArray().then(function(docs){ - var recent = []; + var recents = []; docs.forEach(function(doc) { - recent.push(doc.services[doc._id.split("$$")[0]]); - }) + recents.push(doc.services[doc._id.split("$$")[0]]); + }); - var home = Home({recent: recent}); - res.send('<!doctype html>\n' + React.renderToString(home).replace("</body></html>", "<script>var recent = " + JSON.stringify(recent) + "</script></body></html>")); - //res.render('index', { page: "home", recent: docs, error: req.flash('search-error') }); + Router.run(routes, req.url, function (Handler) { + var App = React.createFactory(Handler); + var content = React.renderToString(App({recents: recents})); + res.send('<!doctype html>\n' + content.replace("</body></html>", "<script>var recents = " + JSON.stringify(recents) + "</script></body></html>")); + }); + }).catch(function(err) { + console.log(err) }); } \ No newline at end of file diff --git a/routes/search.js b/routes/search.js index 381a0a6..da19e5e 100644 --- a/routes/search.js +++ b/routes/search.js @@ -29,14 +29,14 @@ module.exports = function(req, res, next) { searching = true; services[id].parseUrl(req.body.url).timeout(10000).then(function(result) { if (!result.id) { - req.flash('search-error', 'No match found for this link'); - res.redirect('/'); + res.json({error:{message:"No match found for url"}}); } else { services[id].lookupId(result.id, result.type).then(function(item) { items[id] = item; req.db.matches.save({_id:id + "$$" + result.id, created_at: new Date(), services:items}).then(function() { - res.json(item); - //res.redirect("/" + id + "/" + result.type + "/" + result.id); + setTimeout(function() { + res.json(item); + }, 1000) }); }); } @@ -50,8 +50,7 @@ module.exports = function(req, res, next) { error.status = 500; next(error); } else if (error.status == 404){ - req.flash('search-error', 'No match found for this link'); - res.redirect('/'); + res.json({error:{message:"No match found for url"}}); } }); break; @@ -59,7 +58,6 @@ module.exports = function(req, res, next) { } } if (url.host && !searching) { - req.flash('search-error', 'No match found for this link'); - res.redirect('/'); + res.json({error:{message:"No match found for url"}}); } }; diff --git a/routes/share.js b/routes/share.js index 815b2a4..8b4ac6b 100644 --- a/routes/share.js +++ b/routes/share.js @@ -5,8 +5,9 @@ var util = require('util'); var browserify = require('connect-browserify'); var React = require('react'); +var Router = require('react-router'); var nodejsx = require('node-jsx').install(); -var Share = React.createFactory(require('../client/share').Share); +var routes = require('../views/app.jsx').routes; var services = {}; @@ -18,7 +19,7 @@ require("fs").readdirSync(path.join(__dirname, "..", "lib", "services")).forEach }); -module.exports.html = function(req, res, next) { +module.exports = function(req, res, next) { var serviceId = req.params.service; var type = req.params.type; var itemId = req.params.id; @@ -32,11 +33,13 @@ module.exports.html = function(req, res, next) { req.db.matches.findOne({_id:serviceId + "$$" + itemId}, function(err, doc) { if (err) { return next(new Error()); + } else if (!doc) { + return next(); } - var items = []; + var shares = []; for (var docService in Object.keys(services)) { var loopServiceId = Object.keys(services)[docService]; - items.push(doc.services[loopServiceId]); + shares.push(doc.services[loopServiceId]); if (doc.services[loopServiceId].id === undefined) { services[loopServiceId].search(doc.services[serviceId]).timeout(15000).then(function(item) { if (!item.id) { @@ -52,11 +55,11 @@ module.exports.html = function(req, res, next) { } } - var items = items.filter(function(item) { + var shares = shares.filter(function(item) { return item.service != serviceId; }); - items.sort(function(a, b) { + shares.sort(function(a, b) { return !a.id || !b.id; }).sort(function(a, b) { return !a.streamUrl || b.streamUrl; @@ -64,33 +67,18 @@ module.exports.html = function(req, res, next) { return a.type == "video" && b.type != "video"; }); - items.unshift(doc.services[serviceId]); - - var share = Share({items: items}); - res.send('<!doctype html>\n' + React.renderToString(share).replace("</body></html>", "<script>var items = " + JSON.stringify(items) + "</script></body></html>")); - - // res.render(type, { - // page: type, - // title: doc.services[serviceId].name + " by " + doc.services[serviceId].artist.name, - // matching: doc.services[serviceId], - // matches: items, - // thisUrl: req.userProtocol + '://' + req.get('host') + req.originalUrl - // }); + shares.unshift(doc.services[serviceId]); + if (req.accepts(['html', 'json']) === 'json') { + req.db.matches.findOne({_id:serviceId + "$$" + itemId}, function(err, doc) { + res.json({shares:shares}); + }); + } else { + Router.run(routes, req.url, function (Handler) { + var App = React.createFactory(Handler); + var content = React.renderToString(App({shares: shares})); + res.send('<!doctype html>\n' + content.replace("</body></html>", "<script>var shares = " + JSON.stringify(shares) + "</script></body></html>")); + }); + } + }); }; - -module.exports.json = function(req, res, next) { - var serviceId = req.params.service; - var type = req.params.type; - var itemId = req.params.id; - var promises = []; - - if (!services[serviceId] || (type != "album" && type != "track")) { - next(); - return; - } - - req.db.matches.findOne({_id:serviceId + "$$" + itemId}, function(err, doc) { - res.json(doc); - }); -}; \ No newline at end of file diff --git a/views/album.ejs b/views/album.ejs deleted file mode 100644 index a887df4..0000000 --- a/views/album.ejs +++ /dev/null @@ -1,88 +0,0 @@ -<% include header.ejs %> - <header> - <div class="container"> - <div class="row"> - <div class="col-md-12"> - <h1><a href="/">match<span class="audio-lighten">.audio</span></a></h1> - </div> - </div> - </div> - </header> - <div class="container"> - <div class="row"> - <div class="col-md-9 col-sm-8 col-xs-12"> - <h3>Matched albums for</h3> - <h2><%= matching.name %> <span class="artist-lighten">- <%= matching.artist.name %></span></h2> - </div> - <div class="col-md-3 col-sm-4 hidden-xs"> - <ul class="list-inline share-tools"> - <li>Share this</li> - <li><a href="http://twitter.com/intent/tweet/?text=Check out <%= matching.name + " by " + matching.artist.name %>&url=<%= thisUrl %>&via=MatchAudio" class="share-dialog"><img src="/images/twitter.png" alt="Twitter" /></a></li> - <li><a href="http://www.facebook.com/sharer/sharer.php?p[url]=<%= thisUrl %>" class="share-dialog"><img src="/images/facebook.png" alt="Facebook" /></a></li> - <li><a href="https://plus.google.com/share?url=<%= thisUrl %>" class="share-dialog"><img src="/images/googleplus.png" alt="Google+" /></a></li> - </ul> - </div> - </div> - <div class="row"> - <% for (var i=0;i < matches.length;i++) { var album = matches[i]; %> - <% if (album.type != "video") { %><div class="col-md-3 col-xs-6"><% } else { %><div class="col-md-6 col-xs-12"><% } %> - <div class="service <%= i==0 ? "source-service" : "" %>"> - <div class="matching-from hidden-xs"><%= i==0 ? "Found matches using this link" : "" %></div> - <% if (album.type == "video") { %> - <div class="js-video widescreen"> - <iframe width="100%" src="//www.youtube.com/embed/<%= album.id %>" frameborder="0" allowfullscreen></iframe> - </div> - <a href="https://www.youtube.com/results?search_query=<%= matching.name %> <%= matching.artist.name %>">More Youtube matches</a> - <% } else if (album.streamUrl) { %> - <a href="<%= album.streamUrl %>"><img src="<%= album.artwork.small %>" class="img-rounded album-artwork" width="100%"></a> - <div class="service-link"> - <a href="<%= album.streamUrl %>"><img src="/images/<%= album.service %>.png" class="img-rounded"></a> - </div> - <% } else if (album.purchaseUrl) { %> - <a href="<%= album.purchaseUrl %>"><img src="<%= album.artwork.small %>" class="img-rounded album-artwork" width="100%"></a> - <div class="service-link"> - <a href="<%= album.purchaseUrl %>"><img src="/images/<%= album.service %>.png" class="img-rounded"></a> - </div> - <% } else { %> - <img src="<%= matching.artwork.small %>" class="img-rounded album-artwork not-found" width="100%"></a> - <div class="service-link"> - <img src="/images/<%= album.service %>.png" class="img-rounded not-found"> - </div> - <% } %> - </div> - </div><% if((i+1)%4 == 0) { %></div><div class="row"><% } %> - <% } %> - </div> - <div class="row visible-xs-block"> - <div class="col-md-12"> - <ul class="list-inline share-tools"> - <li>Share this</li> - <li><a href="http://twitter.com/intent/tweet/?text=Check out <%= matching.name + " by " + matching.artist.name %>&url=<%= thisUrl %>&via=MatchAudio" class="share-dialog"><img src="/images/twitter.png" alt="Twitter" /></a></li> - <li><a href="http://www.facebook.com/sharer/sharer.php?p[url]=<%= thisUrl %>" class="share-dialog"><img src="/images/facebook.png" alt="Facebook" /></a></li> - <li><a href="https://plus.google.com/share?url=<%= thisUrl %>" class="share-dialog"><img src="/images/googleplus.png" alt="Google+" /></a></li> - </ul> - </div> - </div> - </div> - <script> - Array.prototype.forEach.call(document.querySelectorAll(".share-dialog"), function(dialog){ - dialog.addEventListener("click", function(event) { - event.preventDefault(); - var w = 845; - var h = 670; - var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : screen.left; - var dualScreenTop = window.screenTop != undefined ? window.screenTop : screen.top; - - width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width; - height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height; - - var left = ((width / 2) - (w / 2)) + dualScreenLeft; - var top = ((height / 2) - (h / 2)) + dualScreenTop; - var newWindow = window.open(dialog.href, "Share Music", 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left); - if (window.focus) { - newwindow.focus(); - } - }); - }); - </script> -<% include footer.ejs %> diff --git a/views/app.jsx b/views/app.jsx new file mode 100644 index 0000000..eaf34b9 --- /dev/null +++ b/views/app.jsx @@ -0,0 +1,62 @@ +'use strict'; + +var request = require('superagent'); +var React = require('react'); +var Router = require('react-router'); +var Route = require('react-router').Route; +var DefaultRoute = require('react-router').DefaultRoute; +var NotFoundRoute = require('react-router').NotFoundRoute; +var RouteHandler = require('react-router').RouteHandler; +var Home = require('./home.jsx'); +var Share = require('./share.jsx'); +var Error = require('./error.jsx'); +var Head = require('./head.jsx'); +var ga = require('react-google-analytics'); +var GAInitiailizer = ga.Initializer; + +var App = React.createClass({ + + render: function () { + + return ( + <html> + <Head /> + <body className="home"> + <RouteHandler {...this.props} /> + <GAInitiailizer /> + <script src="/javascript/bundle.js" /> + </body> + </html> + ); + } +}); + +var NotFound = React.createClass({ + render: function () { + return <h2>Not found</h2>; + } +}); + +var routes = ( + <Route name="home" handler={App} path="/"> + <DefaultRoute handler={Home} /> + <Route name="share" path=":service/:type/:id" handler={Share}/> + <NotFoundRoute handler={NotFound}/> + </Route> +); + +module.exports.routes = routes; + +if (typeof window !== 'undefined') { + window.onload = function() { + Router.run(routes, Router.HistoryLocation, function (Handler) { + if (typeof recents !== "undefined") { + React.render(<Handler recents={recents} />, document); + } else if (typeof shares !== "undefined") { + React.render(<Handler shares={shares} />, document); + } + }); + ga('create', 'UA-66209-8', 'auto'); + ga('send', 'pageview'); + } +} \ No newline at end of file diff --git a/views/error.ejs b/views/error.ejs deleted file mode 100644 index f7ea6f0..0000000 --- a/views/error.ejs +++ /dev/null @@ -1,20 +0,0 @@ -<% include header.ejs %> -<header> - <div class="container"> - <div class="row"> - <div class="col-md-12"> - <h1><a href="/">match<span class="audio-lighten">.audio</span></a></h1> - </div> - </div> - </div> -</header> -<div class="container main"> - <div class="row"> - <div class="col-md-6 col-md-offset-3"> - <h2><%= error.status %></h2> - <h1><%= message %></h1> - <% if (error.stack) { %><pre><%= error.stack %></pre><% } %> - </div> - </div> -</div> -<% include footer.ejs %> diff --git a/views/error.jsx b/views/error.jsx new file mode 100644 index 0000000..7fef054 --- /dev/null +++ b/views/error.jsx @@ -0,0 +1,39 @@ +'use strict'; + +var React = require('react'); +var Head = require('./head.jsx'); +var Foot = require('./foot.jsx'); + +module.exports = React.createClass({ + + render: function() { + return ( + <html> + <Head /> + <body> + <div className="error"> + <header> + <div className="container"> + <div className="row"> + <div className="col-md-12"> + <h1><a href="/">match<span className="audio-lighten">.audio</span></a></h1> + </div> + </div> + </div> + </header> + <div className="container main"> + <div className="row"> + <div className="col-md-6 col-md-offset-3"> + <h2>{this.props.status}</h2> + <h1>{this.props.message}</h1> + <pre>{this.props.error.stack || ""}</pre> + </div> + </div> + </div> + <Foot page="error" /> + </div> + </body> + </html> + ); + } +}); diff --git a/views/foot.jsx b/views/foot.jsx new file mode 100644 index 0000000..3bc832e --- /dev/null +++ b/views/foot.jsx @@ -0,0 +1,20 @@ +'use strict'; + +var React = require('react'); + +module.exports = React.createClass({ + + render: function() { + return ( + <footer> + <div className="container"> + <div className="row"> + <div className={this.props.page == "home" || this.props.page == "error" ? "col-md-6 col-md-offset-3" : "col-md-12"}> + <a href="https://twitter.com/MatchAudio">Tweet</a> or <a href="https://github.com/kudos/match.audio">Fork</a>. A work in progress by <a href="http://crem.in">this guy</a>. + </div> + </div> + </div> + </footer> + ); + } +}); diff --git a/views/footer.ejs b/views/footer.ejs deleted file mode 100644 index 01ad8ae..0000000 --- a/views/footer.ejs +++ /dev/null @@ -1,21 +0,0 @@ - </div> - <footer> - <div class="container"> - <div class="row"> - <% if (page == "home") { %><div class="col-md-6 col-md-offset-3"><% } else { %><div class="col-md-12"><% } %> - <a href="https://twitter.com/MatchAudio">Tweet</a> or <a href="https://github.com/kudos/match.audio">Fork</a>. A work in progress by <a href="http://crem.in">this guy</a>. - </div> - </div> - </div> - </footer> - <script> - (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), - m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); - ga('create', 'UA-66209-8', 'match.audio'); - ga('require', 'displayfeatures'); - ga('send', 'pageview'); - </script> -</body> -</html> diff --git a/views/head.jsx b/views/head.jsx new file mode 100644 index 0000000..9b37e6c --- /dev/null +++ b/views/head.jsx @@ -0,0 +1,28 @@ +'use strict'; + +var React = require('react'); + +module.exports = React.createClass({ + + render: function() { + return ( + <head> + <meta charSet="utf-8" /> + <meta httpEquiv="X-UA-Compatible" content="IE=edge" /> + <title>Match Audio</title> + <meta name="description" content="" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="twitter:card" content="summary_large_image" /> + <meta name="twitter:site" content="@MatchAudio" /> + <meta name="twitter:title" property="og:title" content="" /> + <meta name="twitter:description" property="og:description" content="We've matched this music on Rdio, Spotify, Deezer, Beats Music, Google Music and iTunes so you can open it in the service you use." /> + <meta name="twitter:image:src" property="og:image" content="" /> + <meta property="og:url" content="" /> + <link rel="shortcut icon" href="/images/favicon.png" /> + <link href='//fonts.googleapis.com/css?family=Open+Sans:400,300,700' rel='stylesheet' type='text/css' /> + <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" /> + <link rel="stylesheet" href="/stylesheets/style.css" /> + </head> + ); + } +}); \ No newline at end of file diff --git a/views/header.ejs b/views/header.ejs deleted file mode 100644 index 7729b4c..0000000 --- a/views/header.ejs +++ /dev/null @@ -1,23 +0,0 @@ -<!doctype html> -<html lang=""> -<head> - <meta charset="utf-8"> - <meta http-equiv="X-UA-Compatible" content="IE=edge"> - <title><% if (locals.title) { %><%= title %> - <% } %>Match Audio</title> - <meta name="description" content=""> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <% if (locals.items) { var item = items[0] %> - <meta name="twitter:card" content="summary_large_image"> - <meta name="twitter:site" content="@MatchAudio"> - <meta name="twitter:title" property="og:title" content="<%= item.name + " by " + item.artist.name %>"> - <meta name="twitter:description" property="og:description" content="We've matched this music on Rdio, Spotify, Deezer, Beats Music, Google Music and iTunes so you can open it in the service you use." /> - <meta name="twitter:image:src" property="og:image" content="<%= item.artwork.large %>" /> - <meta property="og:url" content="<%= thisUrl %>" /> - <% } %> - <link rel="shortcut icon" href="/images/favicon.png"> - <link href='//fonts.googleapis.com/css?family=Open+Sans:400,300,700' rel='stylesheet' type='text/css'> - <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css"> - <link rel="stylesheet" href="/stylesheets/style.css"> -</head> -<body class="<%= page %>"> -<div class="page-wrap"> diff --git a/views/home.jsx b/views/home.jsx new file mode 100644 index 0000000..e91bef9 --- /dev/null +++ b/views/home.jsx @@ -0,0 +1,114 @@ +'use strict'; + +var React = require('react'); +var request = require('superagent'); +var Router = require('react-router'); +var Link = require('react-router').Link; +var Foot = require('./foot.jsx'); + +var Recent = React.createClass({ + + render: function() { + return (<div className="row"> + <div className="col-md-6 col-md-offset-3"> + <h2>Recently Shared</h2> + <div className="row recent"> + {this.props.recents.map(function(item, i){ + return (<RecentItem item={item} key={i} />); + })} + </div> + </div> + </div>); + } + +}); + +var RecentItem = React.createClass({ + + render: function() { + return ( + <div className="col-sm-4 col-xs-6"> + <Link to="share" params={this.props.item}><img src={this.props.item.artwork.small} width="100%" /></Link> + </div> + ); + } + +}); + +var SearchForm = React.createClass({ + + mixins: [ Router.Navigation, Router.State ], + + handleSubmit: function(e) { + var that = this; + e.preventDefault(); + var url = this.refs.url.getDOMNode().value.trim(); + if (!url) { + return; + } + request.post('/search').send({url:url}).end(function(res) { + console.log(res) + that.transitionTo("/" + res.body.service + "/" + res.body.type + "/" + res.body.id); + }); + }, + + render: function() { + return ( + <form role="form" method="post" action="/search" onSubmit={this.handleSubmit}> + <div className="input-group input-group-lg"> + <input type="text" name="url" placeholder="Paste link here" className="form-control" autofocus ref="url" /> + <span className="input-group-btn"> + <input type="submit" className="btn btn-lg btn-custom" value="Share Music" /> + </span> + </div> + </form> + ); + } +}); + +module.exports = React.createClass({ + + getInitialState: function () { + return { + recents: this.props.recents + }; + }, + + componentDidMount: function () { + if (!this.props.recents) { + request.get('/recent').set('Accept', 'application/json').end(function(res) { + this.setState({ + recents: res.body.recents + }); + }.bind(this)); + } + }, + + render: function() { + return ( + <div> + <div className="page-wrap"> + <header> + <h1><Link to="home">match<span className="audio-lighten">.audio</span></Link></h1> + </header> + <div className="container"> + <div className="row share-form"> + <div className="col-md-6 col-md-offset-3"> + <SearchForm /> + </div> + </div> + <div className="row blurb"> + <div className="col-md-6 col-md-offset-3"> + <p>Make sharing from music services better. + We match album and track links from Rdio, Spotify, Deezer, Beats Music, Google Music and iTunes and give you back a link with all of them. + </p> + </div> + </div> + <Recent recents={this.props.recents} /> + </div> + </div> + <Foot page="home" /> + </div> + ); + } +}); diff --git a/views/index.ejs b/views/index.ejs deleted file mode 100644 index e94f9d1..0000000 --- a/views/index.ejs +++ /dev/null @@ -1,43 +0,0 @@ -<% include header.ejs %> - <header> - <h1><a href="/">match<span class="audio-lighten">.audio</span></a></h1> - </header> - <div class="container"> - - <div class="row share-form"> - <div class="col-md-6 col-md-offset-3"> - <form role="form" method="post" action="/search"> - <div class="input-group input-group-lg"> - <input type="text" name="url" placeholder="Paste link here" class="form-control" autofocus> - <span class="input-group-btn"> - <input type="submit" class="btn btn-lg btn-custom" value="Share Music"> - </span> - </div> - </form> - <% if (error.length > 0) { %> - <p class="alert alert-danger"><%= error %></p> - <% } %> - </div> - </div> - - <div class="row blurb"> - <div class="col-md-6 col-md-offset-3"> - <p>Make sharing from music services better. - We match album and track links from Rdio, Spotify, Deezer, Beats Music, Google Music and iTunes and give you back a link with all of them.</p> - </div> - </div> - - <div class="row"> - <div class="col-md-6 col-md-offset-3"> - <% if (recent.length) { %><h2>Recently Shared</h2><% } %> - <div class="row recent"> - <% for (var i=0;i < recent.length;i++) { var item = recent[i].services[recent[i]._id.split('$$')[0]]; %> - <div class="col-sm-4 col-xs-6"> - <a href="/<%= item.service.replace("playmusic", "") %>/<%= item.type %>/<%= item.id %>"><img src="<%= item.artwork.small ? item.artwork.small : item.artwork %>" width="100%"></a> - </div> - <% } %> - </div> - </div> - </div> - </div> -<% include footer.ejs %> diff --git a/views/share.jsx b/views/share.jsx new file mode 100644 index 0000000..11a5cc9 --- /dev/null +++ b/views/share.jsx @@ -0,0 +1,154 @@ +'use strict'; + +var React = require('react'); +var request = require('superagent'); +var Router = require('react-router'); +var Link = require('react-router').Link; +var Foot = require('./foot.jsx'); + +var MusicItem = React.createClass({ + + render: function() { + if (!this.props.item.name) { + return ( + <div className="col-md-3 col-xs-6"> + <div className="service"> + <div className="matching-from hidden-xs"></div> + <img src={this.props.items[0].artwork.small} className="img-rounded album-artwork not-found" width="100%" /> + <div className="service-link"> + <img src={"/images/" + this.props.item.service + ".png"} className="img-rounded" /> + </div> + </div> + </div> + ); + } else { + return ( + <div className="col-md-3 col-xs-6"> + <div className="service"> + <div className="matching-from hidden-xs"></div> + <a href={this.props.item.streamUrl || this.props.item.purchaseUrl}> + <img src={this.props.item.artwork.small} className="img-rounded album-artwork" width="100%" /></a> + <div className="service-link"> + <a href={this.props.item.streamUrl || this.props.item.purchaseUrl}> + <img src={"/images/" + this.props.item.service + ".png"} className="img-rounded" /> + </a> + </div> + </div> + </div> + ); + } + } + +}); + +var VideoItem = React.createClass({ + + render: function() { + if (this.props.item.id) { + return ( + <div className="col-md-6 col-xs-12"> + <div className="service"> + <div className="js-video widescreen"> + <iframe width="100%" src={"//www.youtube.com/embed/" + this.props.item.id} frameBorder="0" allowFullScreen></iframe> + </div> + <a href={"https://www.youtube.com/results?search_query=" + this.props.items[0].name + " " + this.props.items[0].artist.name}>More Youtube matches</a> + </div> + </div> + ); + } else { + return (<div />); + } + } + +}); + +module.exports = React.createClass({ + + mixins: [ Router.State ], + + getInitialState: function () { + return { + name: this.props.shares[0].name || "", + artist: this.props.shares[0].artist.name || "", + shares: this.props.shares || [] + }; + }, + + componentDidMount: function () { + if (!this.props.shares) { + request.get(this.getPathname()).set('Accept', 'application/json').end(function(res) { + var shares = res.body.shares; + this.setState({ + name: shares[0].name, + artist: shares[0].artist.name, + shares: shares + }); + }.bind(this)) + } + + // Some hacks to pop open the Twitter/Facebook/Google Plus sharing dialogs without using their code. + Array.prototype.forEach.call(document.querySelectorAll(".share-dialog"), function(dialog){ + dialog.addEventListener("click", function(event) { + event.preventDefault(); + var w = 845; + var h = 670; + var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : screen.left; + var dualScreenTop = window.screenTop != undefined ? window.screenTop : screen.top; + + var width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width; + var height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height; + + var left = ((width / 2) - (w / 2)) + dualScreenLeft; + var top = ((height / 2) - (h / 2)) + dualScreenTop; + var newWindow = window.open(dialog.href, "Share Music", 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left); + if (window.focus) { + newWindow.focus(); + } + }); + }); + }, + + render: function() { + return ( + <div> + <div className="page-wrap share"> + <header> + <div className="container"> + <div className="row"> + <div className="col-md-12"> + <h1><Link to="home">match<span className="audio-lighten">.audio</span></Link></h1> + </div> + </div> + </div> + </header> + <div className="container"> + <div className="row"> + <div className="col-md-9 col-sm-8 col-xs-12"> + <h3>Matched {this.state.shares[0] ? this.state.shares[0].type + "s" : ""} for</h3> + <h2>{this.state.name} <span className="artist-lighten">- {this.state.artist}</span></h2> + </div> + <div className="col-md-3 col-sm-4 hidden-xs"> + <ul className="list-inline share-tools"> + <li>Share this</li> + <li><a href={"http://twitter.com/intent/tweet/?text=Check out " + this.state.name + " by " + this.state.artist + "&via=MatchAudio"} className="share-dialog"><img src="/images/twitter.png" alt="Twitter" /></a></li> + <li><a href="http://www.facebook.com/sharer/sharer.php" className="share-dialog"><img src="/images/facebook.png" alt="Facebook" /></a></li> + <li><a href="https://plus.google.com/share" className="share-dialog"><img src="/images/googleplus.png" alt="Google+" /></a></li> + </ul> + </div> + </div> + <div className="row"> + {this.state.shares.map(function(item, i){ + if (item.service == "youtube") { + return (<VideoItem items={this.state.shares} item={item} inc={i} key={i} />); + } else { + return (<MusicItem items={this.state.shares} item={item} inc={i} key={i} />); + } + }.bind(this))} + </div> + </div> + </div> + <Foot page="share" /> + </div> + ); + } +}); diff --git a/views/track.ejs b/views/track.ejs deleted file mode 100644 index eb04790..0000000 --- a/views/track.ejs +++ /dev/null @@ -1,88 +0,0 @@ -<% include header.ejs %> - <header> - <div class="container"> - <div class="row"> - <div class="col-md-12"> - <h1><a href="/">match<span class="audio-lighten">.audio</span></a></h1> - </div> - </div> - </div> - </header> - <div class="container"> - <div class="row"> - <div class="col-md-9 col-sm-8 col-xs-12"> - <h3>Matched tracks for</h3> - <h2><%= items[0].name %> <span class="artist-lighten">- <%= items[0].artist.name %></span></h2> - </div> - <div class="col-md-3 col-sm-4 hidden-xs"> - <ul class="list-inline share-tools"> - <li>Share this</li> - <li><a href="http://twitter.com/intent/tweet/?text=Check out <%= items[0].name + " by " + items[0].artist.name %>&url=<%= thisUrl %>&via=MatchAudio" class="share-dialog"><img src="/images/twitter.png" alt="Twitter" /></a></li> - <li><a href="http://www.facebook.com/sharer/sharer.php?p[url]=<%= thisUrl %>" class="share-dialog"><img src="/images/facebook.png" alt="Facebook" /></a></li> - <li><a href="https://plus.google.com/share?url=<%= thisUrl %>" class="share-dialog"><img src="/images/googleplus.png" alt="Google+" /></a></li> - </ul> - </div> - </div> - <div class="row"> - <% for (var i=0;i < items.length;i++) { var track = items[i]; %> - <% if (track.type != "video") { %><div class="col-md-3 col-xs-6"><% } else { %><div class="col-md-6 col-xs-12"><% } %> - <div class="service <%= i==0 ? "source-service" : "" %>"> - <div class="matching-from hidden-xs"><%= i==0 ? "Found matches using this link" : "" %></div> - <% if (track.type == "video") { %> - <div class="js-video widescreen"> - <iframe width="100%" src="//www.youtube.com/embed/<%= track.id %>" frameborder="0" allowfullscreen></iframe> - </div> - <a href="https://www.youtube.com/results?search_query=<%= items[0].name %> <%= items[0].artist.name %>">More Youtube matches</a> - <% } else if (track.streamUrl) { %> - <a href="<%= track.streamUrl %>"><img src="<%= track.artwork.small %>" class="img-rounded album-artwork" width="100%"></a> - <div class="service-link"> - <a href="<%= track.streamUrl %>"><img src="/images/<%= track.service %>.png" class="img-rounded"></a> - </div> - <% } else if (track.purchaseUrl) { %> - <a href="<%= track.purchaseUrl %>"><img src="<%= track.artwork.small %>" class="img-rounded album-artwork" width="100%"></a> - <div class="service-link"> - <a href="<%= track.purchaseUrl %>"><img src="/images/<%= track.service %>.png" class="img-rounded"></a> - </div> - <% } else { %> - <img src="<%= items[0].artwork.small %>" class="img-rounded album-artwork not-found" width="100%"></a> - <div class="service-link"> - <img src="/images/<%= track.service %>.png" class="img-rounded not-found"> - </div> - <% } %> - </div> - </div><% if((i+1)%4 == 0) { %></div><div class="row"><% } %> - <% } %> - </div> - <div class="row visible-xs-block"> - <div class="col-md-12"> - <ul class="list-inline share-tools"> - <li>Share this</li> - <li><a href="http://twitter.com/intent/tweet/?text=Check out <%= items[0].name + " by " + items[0].artist.name %>&url=<%= thisUrl %>&via=MatchAudio" class="share-dialog"><img src="/images/twitter.png" alt="Twitter" /></a></li> - <li><a href="http://www.facebook.com/sharer/sharer.php?p[url]=<%= thisUrl %>" class="share-dialog"><img src="/images/facebook.png" alt="Facebook" /></a></li> - <li><a href="https://plus.google.com/share?url=<%= thisUrl %>" class="share-dialog"><img src="/images/googleplus.png" alt="Google+" /></a></li> - </ul> - </div> - </div> - </div> - <script> - Array.prototype.forEach.call(document.querySelectorAll(".share-dialog"), function(dialog){ - dialog.addEventListener("click", function(event) { - event.preventDefault(); - var w = 845; - var h = 670; - var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : screen.left; - var dualScreenTop = window.screenTop != undefined ? window.screenTop : screen.top; - - width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width; - height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height; - - var left = ((width / 2) - (w / 2)) + dualScreenLeft; - var top = ((height / 2) - (h / 2)) + dualScreenTop; - var newWindow = window.open(dialog.href, "Share Music", 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left); - if (window.focus) { - newwindow.focus(); - } - }); - }); - </script> -<% include footer.ejs %>