Migrate all the things

* Migrates from Mongo to Postgres.
* Migrates from JSPM to Webpack.
* Migrates from React to Vuejs.
* Migrates from Bootstrap to Bulma.

Also:
* Fixes rendering of meta data in the document head tag.
This commit is contained in:
Jonathan Cremin 2016-10-03 13:31:29 +01:00
parent 09706778d9
commit 7bb0497ff4
76 changed files with 6741 additions and 1760 deletions

View file

@ -1,12 +0,0 @@
{
"ecmaFeatures": {
"jsx": true
},
"plugins": [
"react"
],
"env": {
"browser": true,
"es6": true
}
}

View file

@ -1,41 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, IndexRoute } from 'react-router';
import createBrowserHistory from 'history/lib/createBrowserHistory';
import ga, { Initializer as GAInitiailizer } from 'react-google-analytics';
import Home from './home';
import Share from './share';
import Head from './head';
import ErrorView from './error';
import NotFound from './notfound';
const App = React.createClass({
render: function () {
return (
<html>
<Head {...this.props} />
<body className='home'>
{this.props.children}
<GAInitiailizer />
</body>
</html>
);
}
});
const routes = (
<Route path='/' component={App}>
<IndexRoute component={Home} />
<Route path=':service/:type/:id' component={Share}/>
<Route path='*' component={NotFound}/>
</Route>
);
if (typeof window !== 'undefined') {
console.info('Time since page started rendering: ' + (Date.now() - timerStart) + 'ms'); // eslint-disable-line no-undef
ReactDOM.render(<Router history={createBrowserHistory()}>{routes}</Router>, document);
ga('create', 'UA-66209-8', 'auto');
ga('send', 'pageview');
}
export { routes };

View file

@ -1,37 +0,0 @@
import React from 'react';
import Head from './head';
import Foot from './foot';
export default React.createClass({
render: function() {
return (
<html>
<Head {...this.props} />
<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 || ''}</pre>
</div>
</div>
</div>
<Foot page='error' />
</div>
</body>
</html>
);
}
});

View file

@ -1,36 +0,0 @@
import React from 'react';
export default React.createClass({
render: function() {
return (
<div className='row faq'>
<div className='col-md-6 col-md-offset-3'>
<h2>Questions?</h2>
<ul>
<li>
<h3>Why would I want to use this?</h3>
<p>Sometimes when people want to share music they don't know what service their friends are using. Match Audio let's you take a link from one service and expand it into a link that supports all services.</p>
</li>
<li>
<h3>I still don't get it.</h3>
<p>That's not actually a question, but that's ok. Here's an example: I'm listening to a cool new album I found on Google Play Music. So I go to the address bar (the box that sometimes says https://www.google.com in it) and copy the link to share with my friend. But my friend uses Spotify. So first I go to Match Audio and paste the link there, then grab the Match Audio link from the address bar and send them that link instead.</p>
</li>
<li>
<h3>Where do I find a link to paste in the box?</h3>
<p>Most music services have a 'share' dialog for albums and tracks in their interface. If you have them open in a web browser instead of an app, you can simply copy and paste the address bar and we'll work out the rest.</p>
</li>
<li>
<h3>Can I share playlists?</h3>
<p>Unfortunately not. Playlists would add a huge amount of complexity and would almost certainly cause the site to break the API limits imposed by some of the services we support.</p>
</li>
<li>
<h3>Why don't you guys support Bandcamp, Amazon Music, Sony Music Unlimited&hellip; ?</h3>
<p>Let me stop you there. <a href='https://github.com/kudos/match.audio'>Match Audio is open source</a>, that means any capable programmer who wants to add other music services can look at our code and submit changes. If you're not a programmer, you can always <a href='https://github.com/kudos/match.audio/issues'>submit a request</a> and maybe we'll do it for you.</p>
</li>
</ul>
</div>
</div>
);
}
});

View file

@ -1,18 +0,0 @@
import React from 'react';
export default 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>
);
}
});

View file

@ -1,34 +0,0 @@
import React from 'react';
import { State } from 'react-router';
export default React.createClass({
mixins: [ State ],
render: function() {
const image = this.props.shares ? this.props.shares[0].artwork.large : 'https://match.audio/images/logo-512.png';
const title = this.props.shares ? this.props.shares[0].name + ' by ' + this.props.shares[0].artist.name : 'Match Audio';
const shareUrl = 'https://match.audio/' + this.props.params.service + '/' + this.props.params.type + '/' + this.props.params.id;
return (
<head>
<script dangerouslySetInnerHTML={{__html: 'var timerStart = Date.now();'}}></script>
<meta charSet='utf-8' />
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
<title>{this.props.shares ? 'Listen to ' + this.props.shares[0].name + ' by ' + this.props.shares[0].artist.name + ' on Match Audio' : 'Match Audio'}</title>
<meta name='description' content='Match Audio matches album and track links from Youtube, Rdio, Spotify, Deezer, Google Music, Xbox Music, Beats Music, and iTunes and give you back one link with matches we find on all of them.' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta name='theme-color' content='#FE4365' />
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:site' content='@MatchAudio' />
<meta name='twitter:title' property='og:title' content={title} />
<meta name='twitter:description' property='og:description' content='Match Audio matches album and track links from Youtube, Rdio, Spotify, Deezer, Google Music, Xbox Music, Beats Music, and iTunes and give you back one link with matches we find on all of them.' />
<meta name='twitter:image:src' property='og:image' content={image} />
<meta property='og:url' content={shareUrl} />
<link rel='shortcut icon' href='/images/favicon.png' />
<link rel='icon' sizes='512x512' href='/images/logo-128.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>
);
}
});

View file

@ -1,167 +0,0 @@
import React from 'react';
import request from 'superagent';
import { History, State, Link } from 'react-router';
import Faq from './faq';
import Foot from './foot';
const 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>);
}
});
const RecentItem = React.createClass({
render: function() {
if (!this.props.item.artwork) {
return false;
}
return (
<div className='col-sm-4 col-xs-6'>
<Link to={`/${this.props.item.service}/${this.props.item.type}/${this.props.item.id}`}>
<div className={this.props.item.service === 'youtube' ? 'artwork-youtube artwork' : 'artwork'} style={{backgroundImage: 'url(' + this.props.item.artwork.small + ')'}}></div>
</Link>
</div>
);
}
});
const SearchForm = React.createClass({
mixins: [ History, State ],
getInitialState: function () {
return {
submitting: true,
error: false
};
},
handleSubmit: function(e) {
e.preventDefault();
this.setState({
submitting: true
});
const url = this.refs.url.value.trim();
if (!url) {
this.setState({
submitting: false
});
return;
}
request.post('/search').send({url: url}).end((req, res) => {
this.setState({
submitting: false
});
if (res.body.error) {
this.setState({error: res.body.error.message});
}
const item = res.body;
this.history.pushState(null, `/${item.service}/${item.type}/${item.id}`);
});
},
componentDidMount: function () {
this.setState({
submitting: false,
error: false
});
},
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' disabled={this.state.submitting} />
</span>
</div>
<div className={this.state.error ? 'alert alert-warning' : ''} role='alert'>
{this.state.error}
</div>
</form>
);
}
});
export default React.createClass({
getInitialState: function () {
// Use this only on first page load, refresh whenever we navigate back.
if (this.props.recents) {
const recents = this.props.recents;
delete this.props.recents;
return {
recents: recents
};
}
return {
recents: []
};
},
componentDidMount: function () {
if (!this.props.recents) {
request.get('/recent').set('Accept', 'application/json').end((err, res) => {
this.setState({
recents: res.body.recents
});
});
}
},
render: function() {
return (
<div>
<div className='page-wrap'>
<header>
<h1><Link to='/'>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>Match Audio makes sharing from music services better.
What happens when you share your favourite song on Spotify with a friend, but they don't use Spotify?
</p><p>We match album and track links from Youtube, Rdio, Spotify, Deezer, Google Music, Xbox Music, Beats Music, and iTunes and give you back one link with matches we find on all of them.
</p>
</div>
</div>
<Recent recents={this.state.recents} />
<Faq />
<div className='row'>
<div className='col-md-6 col-md-offset-3'>
<h2>Tools</h2>
<div className='row'>
<div className='col-md-6'>
<p>Download the Chrome Extension and get Match Audio links right from your address bar.</p>
</div>
<div className='col-md-6'>
<p><a href='https://chrome.google.com/webstore/detail/kjfpkmfgcflggjaldcfnoppjlpnidolk'><img src='/images/chrome-web-store.png' alt='Download the Chrome Extension' height='75' /></a></p>
</div>
</div>
</div>
</div>
</div>
</div>
<Foot page='home' />
</div>
);
}
});

33
views/index.html Normal file
View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<title>Match Audio &bull; <%=head.title%></title>
<link rel="stylesheet" href="/dist/css/bulma.css" />
<link rel="stylesheet" href="/src/style/style.css" />
<meta name='description' content='Match Audio matches album and track links from Youtube, Rdio, Spotify, Deezer, Google Music, Xbox Music, Beats Music, and iTunes and give you back one link with matches we find on all of them.' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta name='theme-color' content='#FE4365' />
<meta name='twitter:card' content='<%=typeof share == 'undefined' ? 'summary': 'summary_large_image'%>' />
<meta name='twitter:site' content='@MatchAudio' />
<meta name='twitter:title' property='og:title' content='Match Audio &bull; <%=head.title%>' />
<meta name='twitter:description' property='og:description' content='Match Audio matches album and track links from Youtube, Rdio, Spotify, Deezer, Google Music, Xbox Music, Beats Music, and iTunes and give you back one link with matches we find on all of them.' />
<meta name='twitter:image:src' property='og:image' content='<%=head.image%>' />
<meta property='og:url' content='<%=head.shareUrl%>' />
<link rel='shortcut icon' href='/assets/images/favicon.png' />
<link rel='icon' sizes='512x512' href='/assets/images/logo-512.png' />
</head>
<body>
<%-html%>
<script>
window.__INITIAL_STATE__=<%-JSON.stringify(initialState)%>
</script>
<script async src='https://www.google-analytics.com/analytics.js'></script>
<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', 'UA-66209-8', 'auto');
ga('send', 'pageview');
</script>
<script src="/dist/js/build-client.js"></script>
</body>
</html>

View file

@ -1,30 +0,0 @@
import React from 'react';
import { Link } from 'react-router';
import Head from './head';
import Foot from './foot';
export default React.createClass({
render: function() {
return (
<div className='error'>
<div className='container main'>
<div className='row'>
<div className='col-md-12'>
<div className='error-logo'>
<Link to='/'><img src='/images/logo-full-300.png' width='50' /></Link>
</div>
</div>
</div>
<div className='row vertical-center'>
<div className='col-md-12'>
<h2>404</h2>
<h1>Sorry, it looks like the page you asked for is gone.</h1>
<Link to='/'>Take Me Home</Link> or <a href='https://www.youtube.com/watch?v=gnnIrTLlLyA' target='_blank'>Take Me to the Wubs</a>
</div>
</div>
</div>
</div>
);
}
});

View file

@ -1,194 +0,0 @@
import React from 'react';
import request from 'superagent';
import { Link } from 'react-router';
import Foot from './foot';
const MusicItem = React.createClass({
render: function() {
if (!this.props.item.matched_at) {
return (
<div className='col-md-3 col-xs-6'>
<div className='service'>
<div className='artwork' style={{backgroundImage: 'url(' + this.props.items[0].artwork.small + ')'}}>
</div>
<div className='loading-wrap'>
<img src='/images/eq.svg' className='loading' />
</div>
</div>
<div className='service-link'>
<img src={'/images/' + this.props.item.service + '.png'} className='img-rounded' />
</div>
</div>
);
} else if (!this.props.item.id) {
return (
<div className='col-md-3 col-xs-6'>
<div className='service'>
<div className='artwork not-found' style={{backgroundImage: 'url(' + this.props.items[0].artwork.small + ')'}}></div>
<div className='no-match'>
No Match
</div>
</div>
<div className='service-link not-found'>
<img src={'/images/' + this.props.item.service + '.png'} className='img-rounded' />
</div>
</div>
);
} else {
return (
<div className='col-md-3 col-xs-6'>
<div className={'service' + (this.props.inc === 0 ? ' source-service' : '')}>
<div className='matching-from'>{this.props.inc === 0 ? 'Found matches using' : ''}</div>
<a href={this.props.item.streamUrl || this.props.item.purchaseUrl}>
<div className={this.props.item.service === 'youtube' ? 'artwork-youtube artwork' : 'artwork'} style={{backgroundImage: 'url(' + this.props.item.artwork.small + ')'}}>
</div>
<div className={this.props.item.service === 'youtube' && this.props.inc > 0 ? 'youtube' : ''}>
{this.props.item.service === 'youtube' && this.props.inc > 0 ? this.props.item.name : ''}
</div>
</a>
</div>
<div className='service-link'>
<a href={this.props.item.streamUrl || this.props.item.purchaseUrl}>
<img src={'/images/' + this.props.item.service + '.png'} />
</a>
</div>
</div>
);
}
}
});
export default React.createClass({
getInitialState: function () {
if (this.props.shares && this.props.shares[0].id === this.props.params.id) {
return {
name: this.props.shares[0].name,
artist: this.props.shares[0].artist.name,
shares: this.props.shares,
shareUrl: 'https://match.audio/' + this.props.shares[0].service + '/' + this.props.shares[0].type + '/' + this.props.shares[0].id
};
}
return {
name: '',
artist: '',
shares: [],
shareUrl: ''
};
},
componentWillUnmount: function() {
if (this.state.interval) {
clearInterval(this.state.interval);
}
},
componentDidMount: function () {
let complete = this.state.shares.length > 0;
this.state.shares.forEach(function(share) {
if (typeof share.matched_at === 'undefined') {
complete = false;
}
});
const getShares = () => {
request.get(this.props.location.pathname + '.json').end((err, res) => {
const shares = res.body.shares;
complete = true;
shares.forEach(function(share) {
if (typeof share.matched_at === 'undefined') {
complete = false;
}
});
if (complete) {
clearInterval(this.state.interval);
}
if (shares.length) {
this.setState({
name: shares[0].name,
artist: shares[0].artist.name,
shares: shares,
shareUrl: 'https://match.audio/' + shares[0].service + '/' + shares[0].type + '/' + shares[0].id
});
}
});
};
if (!this.state.shares.length) {
getShares();
}
// Temporary until websockets implementation
this.state.interval = setInterval(function() {
if (!complete) {
getShares();
}
}, 2000);
// 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(e) {
e.preventDefault();
const w = 845;
const h = 670;
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left;
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top;
const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
const left = ((width / 2) - (w / 2)) + dualScreenLeft;
const top = ((height / 2) - (h / 2)) + dualScreenTop;
const 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='/'>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=' + encodeURIComponent(this.state.name) + ' by ' + encodeURIComponent(this.state.artist) + '&via=MatchAudio&url=' + this.state.shareUrl} className='share-dialog'><img src='/images/twitter.png' alt='Twitter' /></a></li>
<li><a href={'http://www.facebook.com/sharer/sharer.php?p[url]=' + this.state.shareUrl} className='share-dialog'><img src='/images/facebook.png' alt='Facebook' /></a></li>
<li><a href={'https://plus.google.com/share?url=' + this.state.shareUrl} className='share-dialog'><img src='/images/googleplus.png' alt='Google+' /></a></li>
</ul>
</div>
</div>
<div className='row'>
{this.state.shares.map((item, i) => {
return (<MusicItem items={this.state.shares} item={item} inc={i} key={i} />);
})}
</div>
</div>
</div>
<Foot page='share' />
</div>
);
}
});