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,7 +1,11 @@
{
"ignore": [
"build-server.js",
"build-client.js",
],
"plugins": [
"syntax-jsx",
"transform-react-jsx",
"syntax-object-rest-spread",
"transform-object-rest-spread",
"transform-es2015-arrow-functions",
"transform-es2015-block-scoped-functions",
"transform-es2015-block-scoping",
@ -20,6 +24,7 @@
"transform-es2015-sticky-regex",
"transform-es2015-template-literals",
"transform-es2015-typeof-symbol",
"transform-es2015-unicode-regex"
"transform-es2015-unicode-regex",
"transform-object-rest-spread",
]
}

View file

@ -1,3 +0,0 @@
node_modules
.git
.env

View file

@ -1,12 +1,14 @@
{
"ecmaFeatures": {
"modules": true
},
"rules": {
"quotes": [2, "single", "avoid-escape"]
"extends": "airbnb/base",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true
},
},
"env": {
"node": true,
"es6": true
}
}
}

3
.gitignore vendored
View file

@ -1,7 +1,8 @@
chrome/build
public/jspm_packages
public/dist
public/views
node_modules
public/**/*.gz
.DS_Store
.env
npm-debug.log

22
LICENSE
View file

@ -1,22 +0,0 @@
(The MIT License)
Copyright (c) 2014 Jonathan Cremin <jonathan@crem.in>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1 +0,0 @@
web: npm start

View file

@ -8,22 +8,16 @@ Make sharing from music services better. We match links from Rdio, Spotify, Deez
* iTunes
* Deezer
* Google Play Music (requires `GOOGLE_EMAIL` and `GOOGLE_PASSWORD`)
* Rdio (requires `RDIO_CLIENT_ID` and `RDIO_CLIENT_SECRET`)
* Xbox Music (requires `XBOX_CLIENT_ID` and `XBOX_CLIENT_SECRET`)
* Youtube (requires `YOUTUBE_KEY`)
Google doesn't provide a public API for Play Music, hence this `GOOGLE_PASSWORD` awfulness. The account also needs to be a Google Play Music All Access subscriber and to have played at least one track on a mobile device. Yeah.
It doesn't parse Youtube urls yet, only finds matches on Youtube. The metadata we get from Youtube around music videos is poor and will require more work than the others.
Support for Amazon Music is [in a branch](https://github.com/kudos/match.audio/tree/amazon) but the library I used is poor and needs either rewriting or replacing to support Amazon urls and not just finding matches on Amazon.
If you don't provide credentials, it will simply disable support for that service. Spotify, Deezer and iTunes don't need any auth. The test suite will fail if you don't provide credentials for all services, but individual tests will pass otherwise.
## Getting Started
Install `node` and `mongodb` if you don't already have them. Then `npm install` and run the app with `npm start` or tests with `npm test`.
Install `node` and `postgres` if you don't already have them. Then `npm install` and run the app with `npm start` or tests with `npm test`.
## Contributing

73
app.js
View file

@ -6,77 +6,42 @@ import route from 'koa-route';
import logger from 'koa-logger';
import favicon from 'koa-favicon';
import compress from 'koa-compress';
import staticHandler from 'koa-file-server';
import serve from 'koa-static';
import views from 'koa-views';
import bodyparser from 'koa-bodyparser';
import co from 'co';
import db from './config/db';
import debuglog from 'debug';
import index from './routes/index';
import recent from './routes/recent';
import search from './routes/search';
import share from './routes/share';
import itunesProxy from './routes/itunes-proxy';
import React from 'react';
import { routes } from './views/app';
import errorHandler from './lib/error-handler';
import debuglog from 'debug';
const debug = debuglog('match.audio');
process.env.VUE_ENV = 'server';
const app = koa();
app.use(errorHandler(routes));
app.use(bodyparser());
app.use(cors());
app.use(compress({flush: zlib.Z_SYNC_FLUSH }));
app.use(compress({ flush: zlib.Z_SYNC_FLUSH }));
app.use(favicon(path.join(__dirname, '/public/images/favicon.png')));
app.use(logger());
app.use(staticHandler({root: 'public', maxage: 31536000000}));
app.use(serve('public', { maxage: 31536000000 }));
let mongo = {};
co(function*() {
mongo = yield db();
});
app.use(function* (next){
this.db = mongo;
yield next;
});
app.use(function* (next) {
// force SSL
if (this.headers['cf-visitor'] && this.headers['cf-visitor'] !== '{"scheme":"https"}') {
return this.redirect('https://' + this.headers.host + this.url);
} else if (this.headers['cf-visitor']) {
this.userProtocol = 'https';
} else {
this.userProtocol = 'http';
}
// redirect www
if (this.headers.host.match(/^www/) !== null ) {
return this.redirect(this.userProtocol + '://' + this.headers.host.replace(/^www\./, '') + this.url);
} else {
yield next;
}
});
app.use(route.get('/', index));
app.use(route.post('/search', search));
app.use(route.get('/itunes/(.*)', itunesProxy));
app.use(route.get('/:service/:type/:id.:format?', share));
app.use(route.get('/recent', function* () {
let recents = [];
let docs = yield this.db.matches.find().sort({'created_at': -1}).limit(6).toArray();
docs.forEach(function(doc) {
recents.push(doc.services[doc._id.split('$$')[0]]); // eslint-disable-line no-underscore-dangle
});
this.body = {recents: recents};
app.use(views(path.resolve(__dirname, './views'), {
map: {
html: 'ejs',
},
}));
app.use(route.get('/', index));
app.use(route.get('/recent', recent));
app.use(route.post('/search', search));
app.use(route.get('/:service/:type/:id.:format?', share));
if (!module.parent) {
app.listen(process.env.PORT || 3000, function() {
debug('Koa HTTP server listening on port ' + (process.env.PORT || 3000));
app.listen(process.env.PORT || 3000, () => {
debug(`Koa HTTP server listening on port ${(process.env.PORT || 3000)}`);
});
}

41
chrome/src/spotify.js Normal file
View file

@ -0,0 +1,41 @@
const apiUrl = 'https://match.audio';
function contextClick(e) {
console.log(e);
}
console.log('attach');
window.addEventListener('click', contextClick);
// document.addSelectorListener('iframe', (outerEvent) => {
// document.addEventListener('click', contextClick);
// outerEvent.target.contentDocument.addEventListener('click', contextClick);
// document.removeSelectorListener('.front iframe');
// iframes[outerEvent.target.src] = true;
// });
// document.addSelectorListener('#context-actions-area iframe', (outerEvent) => {
// outerEvent.target.addEventListener('load', () => {
// const ul = outerEvent.target.contentDocument.querySelector('.dropdown-interior-menu');
//
// for (let node of ul.childNodes) {
// console.log(node);
// }
//
//
// const li = document.createElement('li');
// ul.appendChild(li);
// const a = document.createElement('a');
// a.innerText = 'Open in Match Audio'
// a.href = apiUrl;
// a.target = '_blank';
// a.addEventListener('click', (e) => {
// e.preventDefault();
// window.open(apiUrl + '/spotify/' + match[1] + '/' + match[2], '_blank');
// });
// li.appendChild(a);
// });
// });

View file

@ -1,3 +0,0 @@
machine:
node:
version: 5.1.0

View file

@ -1,13 +0,0 @@
import mongodb from 'mongodb-promisified';
const MongoClient = mongodb().MongoClient;
import debuglog from 'debug';
const debug = debuglog('match.audio');
const uristring = process.env.MONGOLAB_URI || process.env.MONGOHQ_URL || 'mongodb://localhost:27017/match-audio';
export default function* () {
const client = yield MongoClient.connect(uristring);
debug('Successfully connected to Mongodb');
client.matches = client.collection('matches');
return client;
};

View file

@ -1,38 +0,0 @@
import React from 'react';
import { renderPage } from './react-handler';
import debuglog from 'debug';
const debug = debuglog('match.audio');
export default function (routes) {
return function* (next) {
this.set('Server', 'Nintendo 64');
try {
yield next;
} catch (err) {
if (err.status === 404) {
this.body = yield renderPage(routes, this.request.url, {});
} else {
debug('Error: %o', err);
throw err;
}
}
if (404 != this.status) return;
switch (this.accepts('html', 'json')) {
case 'html':
this.type = 'html';
this.body = yield renderPage(routes, this.request.url, {});
break;
case 'json':
this.body = {
message: 'Page Not Found'
};
break;
default:
this.type = 'text';
this.body = 'Page Not Found';
}
}
}

34
lib/react-handler.js vendored
View file

@ -1,34 +0,0 @@
import React from 'react';
import { renderToString } from 'react-dom/server';
import { RoutingContext, match } from 'react-router';
import createLocation from 'history/lib/createLocation';
export function matchRoute(routes, url) {
const location = createLocation(url);
return new Promise((resolve, reject) => {
match({ routes, location }, (error, redirectLocation, renderProps) => {
resolve({error, redirectLocation, renderProps});
});
});
}
export function* renderPage(routes, url, state) {
const { error, redirectLocation, renderProps } = yield matchRoute(routes, url);
if (error) {
throw new Error(error.message);
} else if (redirectLocation) {
return redirectLocation.pathname + redirectLocation.search;
} else if (renderProps === null) {
return false;
}
const content = renderToString(<RoutingContext {...renderProps} />);
return '<!doctype html>\n' + content.replace('</body></html>', `<script>window.STATE = ${JSON.stringify(state)}</script>
<script src='/jspm_packages/system.js'></script>
<script src='/config.js'></script>
<script>System.import('babel/external-helpers')</script>
<script>System.import('views/app')</script>
</body></html>`);
}

16
lib/render.js Normal file
View file

@ -0,0 +1,16 @@
import fs from 'fs';
import { createBundleRenderer } from 'vue-server-renderer';
const app = fs.readFileSync('./public/dist/js/build-server.js', 'utf8');
export default function(url, initialState) {
const renderer = createBundleRenderer(app);
return new Promise((resolve, reject) => {
renderer.renderToString({ url, initialState }, (error, html) => {
if(error) {
return reject(error);
}
resolve(html);
});
});
}

View file

@ -51,7 +51,7 @@ function formatResponse(res) {
purchaseUrl: null,
artwork: {
small: result.ImageUrl.replace('http://', 'https://') + '&w=250&h=250',
large: result.ImageUrl.replace('http://', 'https://') + '&w=500&h=250'
large: result.ImageUrl.replace('http://', 'https://') + '&w=500&h=500'
},
artist: {
name: result.Artists[0].Artist.Name

91
lib/share.js Normal file
View file

@ -0,0 +1,91 @@
import co from 'co';
import debuglog from 'debug';
import models from '../models';
import services from '../lib/services';
const debug = debuglog('match.audio:share');
export function find(music) {
return models[music.type].findOne({
where: {
externalId: music.id,
},
include: [
{ model: models.artist },
{ model: models.match },
],
});
}
export function create(music) {
return models[music.type].create({
externalId: music.id,
service: music.service,
name: music.name,
albumName: music.type === 'track' ? music.album.name : null,
artist: {
name: music.artist.name,
artworkSmall: null,
artworkLarge: null,
},
matches: [
{
externalId: music.id,
service: music.service,
name: music.name,
streamUrl: music.streamUrl,
purchaseUrl: music.purchaseUrl,
artworkSmall: music.artwork.small,
artworkLarge: music.artwork.large,
},
],
}, {
include: [
{ model: models.artist },
{ model: models.match },
],
});
}
export function findMatchesAsync(share) {
process.nextTick(() => {
for (const service of services) {
if (service.id === share.service) {
continue; // eslint-disable-line no-continue
}
co(function* gen() { // eslint-disable-line no-loop-func
const match = yield service.search(share);
console.log(service.id)
console.log(match)
if (match.id) {
models.match.create({
trackId: share.albumName ? share.id : null,
albumId: share.albumName ? null : share.id,
externalId: match.id,
service: match.service,
name: match.name,
streamUrl: match.streamUrl,
purchaseUrl: match.purchaseUrl,
artworkSmall: match.artwork.small,
artworkLarge: match.artwork.large,
});
} else {
models.match.create({
trackId: share.trackId ? share.id : null,
albumId: share.albumId ? share.id : null,
externalId: null,
service: match.service,
name: null,
streamUrl: null,
purchaseUrl: null,
artworkSmall: null,
artworkLarge: null,
});
}
}).catch((err) => {
debug(err);
});
}
});
}

36
models/album.js Normal file
View file

@ -0,0 +1,36 @@
export default function (sequelize, DataTypes) {
const Album = sequelize.define('album', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
externalId: { type: DataTypes.STRING(50), index: true }, // eslint-disable-line new-cap
service: DataTypes.ENUM( // eslint-disable-line new-cap
'deezer',
'google',
'itunes',
'spotify',
'xbox',
'youtube'
),
name: DataTypes.TEXT,
artistId: DataTypes.INTEGER,
}, {
paranoid: true,
classMethods: {
associate: (models) => {
Album.hasMany(models.match);
Album.belongsTo(models.artist);
},
},
indexes: [
{
fields: ['externalId', 'service'],
},
],
getterMethods: {
type() {
return this.getDataValue('trackId') ? 'track' : 'album';
},
},
});
return Album;
}

25
models/artist.js Normal file
View file

@ -0,0 +1,25 @@
export default function (sequelize, DataTypes) {
const Artist = sequelize.define('artist', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
name: DataTypes.TEXT,
streamUrl: DataTypes.TEXT,
purchaseUrl: DataTypes.TEXT,
artworkSmall: DataTypes.TEXT,
artworkLarge: DataTypes.TEXT,
}, {
paranoid: true,
classMethods: {
associate: (models) => {
Artist.hasMany(models.track);
Artist.hasMany(models.album);
},
},
indexes: [
{
fields: ['name'],
},
],
});
return Artist;
}

35
models/index.js Normal file
View file

@ -0,0 +1,35 @@
import fs from 'fs';
import path from 'path';
import Sequelize from 'sequelize';
import debugname from 'debug';
const debug = debugname('match.audio:models');
const config = {
dialect: 'postgres',
protocol: 'postgres',
logging: debug,
};
const sequelize = new Sequelize(process.env.DATABASE_URL, config);
const db = {};
fs
.readdirSync(__dirname)
.filter(file => (file.indexOf('.') !== 0) && (file !== 'index.js'))
.forEach((file) => {
const model = sequelize.import(path.join(__dirname, file));
db[model.name] = model;
});
Object.keys(db).forEach((modelName) => {
if ('associate' in db[modelName]) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
export default db;

35
models/match.js Normal file
View file

@ -0,0 +1,35 @@
export default function (sequelize, DataTypes) {
const Match = sequelize.define('match', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
trackId: DataTypes.INTEGER,
albumId: DataTypes.INTEGER,
externalId: { type: DataTypes.STRING(50), index: true }, // eslint-disable-line new-cap
service: DataTypes.ENUM( // eslint-disable-line new-cap
'deezer',
'google',
'itunes',
'spotify',
'xbox',
'youtube'
),
name: DataTypes.TEXT,
streamUrl: DataTypes.TEXT,
purchaseUrl: DataTypes.TEXT,
artworkSmall: DataTypes.TEXT,
artworkLarge: DataTypes.TEXT,
}, {
paranoid: true,
indexes: [
{
fields: ['externalId', 'service'],
},
],
getterMethods: {
type() {
return this.getDataValue('trackId') ? 'track' : 'album';
},
},
});
return Match;
}

38
models/track.js Normal file
View file

@ -0,0 +1,38 @@
export default function (sequelize, DataTypes) {
const Track = sequelize.define('track', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
externalId: { type: DataTypes.STRING(50), index: true }, // eslint-disable-line new-cap
service: DataTypes.ENUM( // eslint-disable-line new-cap
'deezer',
'google',
'itunes',
'spotify',
'xbox',
'youtube'
),
name: DataTypes.TEXT,
artistId: DataTypes.INTEGER,
albumId: DataTypes.INTEGER,
albumName: DataTypes.TEXT,
}, {
paranoid: true,
classMethods: {
associate: (models) => {
Track.hasMany(models.match);
Track.belongsTo(models.artist);
},
},
indexes: [
{
fields: ['externalId', 'service'],
},
],
getterMethods: {
type() {
return this.getDataValue('trackId') ? 'track' : 'album';
},
},
});
return Track;
}

View file

@ -4,24 +4,25 @@
"repository": "https://github.com/kudos/match.audio",
"license": "MIT",
"scripts": {
"build": "babel -d public/views views",
"cover": "istanbul cover _mocha -- --require babel-core/register --require co-mocha test/**/*.js",
"start": "node -r babel-core/register app.js",
"test": "mocha --require co-mocha --compilers js:babel-core/register test/**/*.js --timeout=15000",
"build": "webpack -p --config webpack.config.js && webpack -p --config webpack.config.server.js",
"start": "node -r babel-register app.js",
"test": "mocha -r co-mocha --compilers js:babel-register test/**/*.js --timeout=15000",
"watch": "parallelshell \"npm run watch-js\" \"npm run watch-server\"",
"watch-js": "babel -wd public/views views",
"watch-server": "nodemon -x \"node -r babel-core/register\" -e js,jsx -i public/ -i chrome/ app.js",
"heroku-postbuild": "jspm install && npm run build"
"watch-js": "parallelshell \"webpack -w -d --config webpack.config.js\" \"webpack -w -d --config webpack.config.server.js\"",
"watch-server": "nodemon -x \"node -r babel-register\" -e js,vue -i node_modules -i chrome/ app.js",
"heroku-postbuild": "npm run build"
},
"engines": {
"node": "^5.0.0",
"npm": "^3.3.0"
"node": "^6.7.0",
"npm": "^3.10.0"
},
"dependencies": {
"babel": "^6.1.18",
"babel-cli": "^6.3.13",
"babel-core": "^6.3.13",
"babel-loader": "^6.0.0",
"babel-plugin-syntax-jsx": "^6.3.13",
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
"babel-plugin-transform-es2015-arrow-functions": "^6.3.13",
"babel-plugin-transform-es2015-block-scoped-functions": "^6.1.18",
"babel-plugin-transform-es2015-block-scoping": "^6.1.18",
@ -41,64 +42,60 @@
"babel-plugin-transform-es2015-template-literals": "^6.1.18",
"babel-plugin-transform-es2015-typeof-symbol": "^6.1.18",
"babel-plugin-transform-es2015-unicode-regex": "^6.1.18",
"babel-plugin-transform-react-jsx": "^6.2.0",
"bluebird": "~2.10.2",
"babel-plugin-transform-object-assign": "^6.8.0",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-plugin-transform-remove-strict-mode": "0.0.2",
"babel-preset-es2015": "^6.0.0",
"babel-preset-latest-minimal": "^1.1.2",
"babel-register": "^6.16.3",
"bluebird": "^3.4.1",
"bulma": "^0.2.3",
"co": "~4.6.0",
"copy-webpack-plugin": "^3.0.1",
"css-loader": "^0.25.0",
"debug": "~2.2.0",
"history": "^1.12.3",
"jspm": "~0.16.20",
"ejs": "^2.5.2",
"file-loader": "^0.9.0",
"json-loader": "^0.5.4",
"kcors": "^1.0.1",
"koa": "~1.1.2",
"koa-bodyparser": "~2.0.1",
"koa": "^1.2.1",
"koa-bodyparser": "^2.2.0",
"koa-compress": "~1.0.8",
"koa-favicon": "~1.2.0",
"koa-file-server": "~2.3.1",
"koa-logger": "~1.3.0",
"koa-route": "~2.4.2",
"moment": "~2.11.1",
"mongodb-promisified": "~1.0.2",
"koa-static": "^2.0.0",
"koa-views": "^4.0.1",
"koa-websocket": "^2.1.0",
"moment": "^2.14.1",
"node-uuid": "~1.4.2",
"playmusic": "~2.1.0",
"react": "~0.14.5",
"react-dom": "~0.14.5",
"react-google-analytics": "~0.2.0",
"react-helmet": "^3.0.1",
"react-router": "~1.0.0",
"pg": "^6.1.0",
"playmusic": "~2.2.0",
"sequelize": "^3.24.3",
"spotify": "~0.3.0",
"superagent": "~1.6.1",
"superagent-bluebird-promise": "~2.1.1"
"style-loader": "^0.13.1",
"superagent": "^2.1.0",
"superagent-bluebird-promise": "^3.0.2",
"vue": "^2.0.3",
"vue-loader": "^9.7.0",
"vue-router": "^2.0.1",
"vue-server-renderer": "^2.0.3",
"vuex": "^2.0.0",
"vuex-router-sync": "^3.0.0",
"webpack": "2.1.0-beta.25"
},
"devDependencies": {
"babel-plugin-transform-runtime": "^6.15.0",
"babel-runtime": "^6.11.6",
"co-mocha": "~1.1.0",
"eslint": "~1.10.1",
"eslint-plugin-react": "~3.14.0",
"eslint": "^3.8.0",
"eslint-config-airbnb": "^12.0.0",
"eslint-plugin-import": "^2.0.1",
"istanbul": "^0.4.0",
"mocha": "~2.3.0",
"nodemon": "~1.8.0",
"mocha": "^3.0.2",
"nodemon": "^1.10.2",
"parallelshell": "~2.0.0",
"should": "~8.0.2"
},
"jspm": {
"directories": {
"baseURL": "public"
},
"dependencies": {
"history": "npm:history@^1.12.3",
"react": "npm:react@~0.14.5",
"react-dom": "npm:react-dom@~0.14.5",
"react-google-analytics": "npm:react-google-analytics@~0.2.0",
"react-helmet": "npm:react-helmet@^3.0.1",
"react-router": "npm:react-router@~1.0.0",
"superagent": "npm:superagent@~1.2.0"
},
"devDependencies": {
"babel": "npm:babel-core@^5.8.24",
"babel-runtime": "npm:babel-runtime@^5.8.24",
"core-js": "npm:core-js@^1.1.4"
},
"buildConfig": {
"minify": true,
"transpileES6": true
}
"should": "^11.1.0"
}
}

View file

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 979 B

After

Width:  |  Height:  |  Size: 979 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 644 B

After

Width:  |  Height:  |  Size: 644 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 770 B

After

Width:  |  Height:  |  Size: 770 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 951 B

After

Width:  |  Height:  |  Size: 951 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Before After
Before After

View file

@ -1,568 +0,0 @@
System.config({
baseURL: "/",
defaultJSExtensions: true,
transpiler: "babel",
babelOptions: {
"optional": [
"runtime",
"optimisation.modules.system"
]
},
paths: {
"github:*": "jspm_packages/github/*",
"npm:*": "jspm_packages/npm/*"
},
map: {
"babel": "npm:babel-core@5.8.34",
"babel-runtime": "npm:babel-runtime@5.8.34",
"core-js": "npm:core-js@1.2.6",
"history": "npm:history@1.17.0",
"react": "npm:react@0.14.6",
"react-dom": "npm:react-dom@0.14.6",
"react-google-analytics": "npm:react-google-analytics@0.2.0",
"react-router": "npm:react-router@1.0.3",
"superagent": "npm:superagent@1.2.0",
"github:jspm/nodelibs-assert@0.1.0": {
"assert": "npm:assert@1.3.0"
},
"github:jspm/nodelibs-buffer@0.1.0": {
"buffer": "npm:buffer@3.6.0"
},
"github:jspm/nodelibs-constants@0.1.0": {
"constants-browserify": "npm:constants-browserify@0.0.1"
},
"github:jspm/nodelibs-crypto@0.1.0": {
"crypto-browserify": "npm:crypto-browserify@3.11.0"
},
"github:jspm/nodelibs-domain@0.1.0": {
"domain-browser": "npm:domain-browser@1.1.7"
},
"github:jspm/nodelibs-events@0.1.1": {
"events": "npm:events@1.0.2"
},
"github:jspm/nodelibs-http@1.7.1": {
"Base64": "npm:Base64@0.2.1",
"events": "github:jspm/nodelibs-events@0.1.1",
"inherits": "npm:inherits@2.0.1",
"stream": "github:jspm/nodelibs-stream@0.1.0",
"url": "github:jspm/nodelibs-url@0.1.0",
"util": "github:jspm/nodelibs-util@0.1.0"
},
"github:jspm/nodelibs-https@0.1.0": {
"https-browserify": "npm:https-browserify@0.0.0"
},
"github:jspm/nodelibs-net@0.1.2": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"http": "github:jspm/nodelibs-http@1.7.1",
"net": "github:jspm/nodelibs-net@0.1.2",
"process": "github:jspm/nodelibs-process@0.1.2",
"stream": "github:jspm/nodelibs-stream@0.1.0",
"timers": "github:jspm/nodelibs-timers@0.1.0",
"util": "github:jspm/nodelibs-util@0.1.0"
},
"github:jspm/nodelibs-os@0.1.0": {
"os-browserify": "npm:os-browserify@0.1.2"
},
"github:jspm/nodelibs-path@0.1.0": {
"path-browserify": "npm:path-browserify@0.0.0"
},
"github:jspm/nodelibs-process@0.1.2": {
"process": "npm:process@0.11.2"
},
"github:jspm/nodelibs-querystring@0.1.0": {
"querystring": "npm:querystring@0.2.0"
},
"github:jspm/nodelibs-stream@0.1.0": {
"stream-browserify": "npm:stream-browserify@1.0.0"
},
"github:jspm/nodelibs-string_decoder@0.1.0": {
"string_decoder": "npm:string_decoder@0.10.31"
},
"github:jspm/nodelibs-timers@0.1.0": {
"timers-browserify": "npm:timers-browserify@1.4.2"
},
"github:jspm/nodelibs-tty@0.1.0": {
"tty-browserify": "npm:tty-browserify@0.0.0"
},
"github:jspm/nodelibs-url@0.1.0": {
"url": "npm:url@0.10.3"
},
"github:jspm/nodelibs-util@0.1.0": {
"util": "npm:util@0.10.3"
},
"github:jspm/nodelibs-vm@0.1.0": {
"vm-browserify": "npm:vm-browserify@0.0.4"
},
"github:jspm/nodelibs-zlib@0.1.0": {
"browserify-zlib": "npm:browserify-zlib@0.1.4"
},
"npm:amdefine@1.0.0": {
"fs": "github:jspm/nodelibs-fs@0.1.2",
"module": "github:jspm/nodelibs-module@0.1.0",
"path": "github:jspm/nodelibs-path@0.1.0",
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:asap@2.0.3": {
"domain": "github:jspm/nodelibs-domain@0.1.0",
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:asn1.js@4.3.0": {
"assert": "github:jspm/nodelibs-assert@0.1.0",
"bn.js": "npm:bn.js@4.6.2",
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"inherits": "npm:inherits@2.0.1",
"minimalistic-assert": "npm:minimalistic-assert@1.0.0",
"vm": "github:jspm/nodelibs-vm@0.1.0"
},
"npm:assert@1.3.0": {
"util": "npm:util@0.10.3"
},
"npm:async@0.9.2": {
"process": "github:jspm/nodelibs-process@0.1.2",
"systemjs-json": "github:systemjs/plugin-json@0.1.0"
},
"npm:babel-runtime@5.8.34": {
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:browserify-aes@1.0.5": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"buffer-xor": "npm:buffer-xor@1.0.3",
"cipher-base": "npm:cipher-base@1.0.2",
"create-hash": "npm:create-hash@1.1.2",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"evp_bytestokey": "npm:evp_bytestokey@1.0.0",
"fs": "github:jspm/nodelibs-fs@0.1.2",
"inherits": "npm:inherits@2.0.1",
"systemjs-json": "github:systemjs/plugin-json@0.1.0"
},
"npm:browserify-cipher@1.0.0": {
"browserify-aes": "npm:browserify-aes@1.0.5",
"browserify-des": "npm:browserify-des@1.0.0",
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"evp_bytestokey": "npm:evp_bytestokey@1.0.0"
},
"npm:browserify-des@1.0.0": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"cipher-base": "npm:cipher-base@1.0.2",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"des.js": "npm:des.js@1.0.0",
"inherits": "npm:inherits@2.0.1"
},
"npm:browserify-rsa@4.0.0": {
"bn.js": "npm:bn.js@4.6.2",
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"constants": "github:jspm/nodelibs-constants@0.1.0",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"randombytes": "npm:randombytes@2.0.1"
},
"npm:browserify-sign@4.0.0": {
"bn.js": "npm:bn.js@4.6.2",
"browserify-rsa": "npm:browserify-rsa@4.0.0",
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"create-hash": "npm:create-hash@1.1.2",
"create-hmac": "npm:create-hmac@1.1.4",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"elliptic": "npm:elliptic@6.0.2",
"inherits": "npm:inherits@2.0.1",
"parse-asn1": "npm:parse-asn1@5.0.0",
"stream": "github:jspm/nodelibs-stream@0.1.0"
},
"npm:browserify-zlib@0.1.4": {
"assert": "github:jspm/nodelibs-assert@0.1.0",
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"pako": "npm:pako@0.2.8",
"process": "github:jspm/nodelibs-process@0.1.2",
"readable-stream": "npm:readable-stream@2.0.5",
"util": "github:jspm/nodelibs-util@0.1.0"
},
"npm:buffer-xor@1.0.3": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"systemjs-json": "github:systemjs/plugin-json@0.1.0"
},
"npm:buffer@3.6.0": {
"base64-js": "npm:base64-js@0.0.8",
"child_process": "github:jspm/nodelibs-child_process@0.1.0",
"fs": "github:jspm/nodelibs-fs@0.1.2",
"ieee754": "npm:ieee754@1.1.6",
"isarray": "npm:isarray@1.0.0",
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:cipher-base@1.0.2": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"inherits": "npm:inherits@2.0.1",
"stream": "github:jspm/nodelibs-stream@0.1.0",
"string_decoder": "github:jspm/nodelibs-string_decoder@0.1.0"
},
"npm:combined-stream@0.0.7": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"delayed-stream": "npm:delayed-stream@0.0.5",
"stream": "github:jspm/nodelibs-stream@0.1.0",
"util": "github:jspm/nodelibs-util@0.1.0"
},
"npm:constants-browserify@0.0.1": {
"systemjs-json": "github:systemjs/plugin-json@0.1.0"
},
"npm:core-js@1.2.6": {
"fs": "github:jspm/nodelibs-fs@0.1.2",
"path": "github:jspm/nodelibs-path@0.1.0",
"process": "github:jspm/nodelibs-process@0.1.2",
"systemjs-json": "github:systemjs/plugin-json@0.1.0"
},
"npm:core-util-is@1.0.2": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0"
},
"npm:create-ecdh@4.0.0": {
"bn.js": "npm:bn.js@4.6.2",
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"elliptic": "npm:elliptic@6.0.2"
},
"npm:create-hash@1.1.2": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"cipher-base": "npm:cipher-base@1.0.2",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"fs": "github:jspm/nodelibs-fs@0.1.2",
"inherits": "npm:inherits@2.0.1",
"ripemd160": "npm:ripemd160@1.0.1",
"sha.js": "npm:sha.js@2.4.4"
},
"npm:create-hmac@1.1.4": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"create-hash": "npm:create-hash@1.1.2",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"inherits": "npm:inherits@2.0.1",
"stream": "github:jspm/nodelibs-stream@0.1.0"
},
"npm:crypto-browserify@3.11.0": {
"browserify-cipher": "npm:browserify-cipher@1.0.0",
"browserify-sign": "npm:browserify-sign@4.0.0",
"create-ecdh": "npm:create-ecdh@4.0.0",
"create-hash": "npm:create-hash@1.1.2",
"create-hmac": "npm:create-hmac@1.1.4",
"diffie-hellman": "npm:diffie-hellman@5.0.0",
"inherits": "npm:inherits@2.0.1",
"pbkdf2": "npm:pbkdf2@3.0.4",
"public-encrypt": "npm:public-encrypt@4.0.0",
"randombytes": "npm:randombytes@2.0.1"
},
"npm:debug@2.2.0": {
"fs": "github:jspm/nodelibs-fs@0.1.2",
"ms": "npm:ms@0.7.1",
"net": "github:jspm/nodelibs-net@0.1.2",
"process": "github:jspm/nodelibs-process@0.1.2",
"tty": "github:jspm/nodelibs-tty@0.1.0",
"util": "github:jspm/nodelibs-util@0.1.0"
},
"npm:delayed-stream@0.0.5": {
"stream": "github:jspm/nodelibs-stream@0.1.0",
"util": "github:jspm/nodelibs-util@0.1.0"
},
"npm:des.js@1.0.0": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"inherits": "npm:inherits@2.0.1",
"minimalistic-assert": "npm:minimalistic-assert@1.0.0"
},
"npm:diffie-hellman@5.0.0": {
"bn.js": "npm:bn.js@4.6.2",
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"miller-rabin": "npm:miller-rabin@4.0.0",
"randombytes": "npm:randombytes@2.0.1",
"systemjs-json": "github:systemjs/plugin-json@0.1.0"
},
"npm:domain-browser@1.1.7": {
"events": "github:jspm/nodelibs-events@0.1.1"
},
"npm:elliptic@6.0.2": {
"bn.js": "npm:bn.js@4.6.2",
"brorand": "npm:brorand@1.0.5",
"hash.js": "npm:hash.js@1.0.3",
"inherits": "npm:inherits@2.0.1",
"systemjs-json": "github:systemjs/plugin-json@0.1.0"
},
"npm:envify@3.4.0": {
"jstransform": "npm:jstransform@10.1.0",
"process": "github:jspm/nodelibs-process@0.1.2",
"through": "npm:through@2.3.8"
},
"npm:esprima-fb@13001.1001.0-dev-harmony-fb": {
"fs": "github:jspm/nodelibs-fs@0.1.2",
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:evp_bytestokey@1.0.0": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"create-hash": "npm:create-hash@1.1.2",
"crypto": "github:jspm/nodelibs-crypto@0.1.0"
},
"npm:fbjs@0.6.1": {
"core-js": "npm:core-js@1.2.6",
"loose-envify": "npm:loose-envify@1.1.0",
"process": "github:jspm/nodelibs-process@0.1.2",
"promise": "npm:promise@7.1.1",
"ua-parser-js": "npm:ua-parser-js@0.7.10",
"whatwg-fetch": "npm:whatwg-fetch@0.9.0"
},
"npm:form-data@0.2.0": {
"async": "npm:async@0.9.2",
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"combined-stream": "npm:combined-stream@0.0.7",
"fs": "github:jspm/nodelibs-fs@0.1.2",
"http": "github:jspm/nodelibs-http@1.7.1",
"https": "github:jspm/nodelibs-https@0.1.0",
"mime-types": "npm:mime-types@2.0.14",
"path": "github:jspm/nodelibs-path@0.1.0",
"process": "github:jspm/nodelibs-process@0.1.2",
"url": "github:jspm/nodelibs-url@0.1.0",
"util": "github:jspm/nodelibs-util@0.1.0"
},
"npm:formidable@1.0.14": {
"assert": "github:jspm/nodelibs-assert@0.1.0",
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"events": "github:jspm/nodelibs-events@0.1.1",
"fs": "github:jspm/nodelibs-fs@0.1.2",
"http": "github:jspm/nodelibs-http@1.7.1",
"os": "github:jspm/nodelibs-os@0.1.0",
"path": "github:jspm/nodelibs-path@0.1.0",
"process": "github:jspm/nodelibs-process@0.1.2",
"querystring": "github:jspm/nodelibs-querystring@0.1.0",
"stream": "github:jspm/nodelibs-stream@0.1.0",
"string_decoder": "github:jspm/nodelibs-string_decoder@0.1.0",
"util": "github:jspm/nodelibs-util@0.1.0"
},
"npm:hash.js@1.0.3": {
"inherits": "npm:inherits@2.0.1"
},
"npm:history@1.17.0": {
"child_process": "github:jspm/nodelibs-child_process@0.1.0",
"deep-equal": "npm:deep-equal@1.0.1",
"fs": "github:jspm/nodelibs-fs@0.1.2",
"invariant": "npm:invariant@2.2.0",
"process": "github:jspm/nodelibs-process@0.1.2",
"query-string": "npm:query-string@3.0.0",
"warning": "npm:warning@2.1.0"
},
"npm:https-browserify@0.0.0": {
"http": "github:jspm/nodelibs-http@1.7.1"
},
"npm:inherits@2.0.1": {
"util": "github:jspm/nodelibs-util@0.1.0"
},
"npm:invariant@2.2.0": {
"loose-envify": "npm:loose-envify@1.1.0",
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:jstransform@10.1.0": {
"base62": "npm:base62@0.1.1",
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"esprima-fb": "npm:esprima-fb@13001.1001.0-dev-harmony-fb",
"fs": "github:jspm/nodelibs-fs@0.1.2",
"process": "github:jspm/nodelibs-process@0.1.2",
"source-map": "npm:source-map@0.1.31"
},
"npm:loose-envify@1.1.0": {
"js-tokens": "npm:js-tokens@1.0.2",
"process": "github:jspm/nodelibs-process@0.1.2",
"stream": "github:jspm/nodelibs-stream@0.1.0",
"util": "github:jspm/nodelibs-util@0.1.0"
},
"npm:methods@1.0.1": {
"http": "github:jspm/nodelibs-http@1.7.1"
},
"npm:miller-rabin@4.0.0": {
"bn.js": "npm:bn.js@4.6.2",
"brorand": "npm:brorand@1.0.5"
},
"npm:mime-db@1.12.0": {
"systemjs-json": "github:systemjs/plugin-json@0.1.0"
},
"npm:mime-types@2.0.14": {
"mime-db": "npm:mime-db@1.12.0"
},
"npm:mime@1.3.4": {
"assert": "github:jspm/nodelibs-assert@0.1.0",
"fs": "github:jspm/nodelibs-fs@0.1.2",
"path": "github:jspm/nodelibs-path@0.1.0",
"process": "github:jspm/nodelibs-process@0.1.2",
"systemjs-json": "github:systemjs/plugin-json@0.1.0"
},
"npm:os-browserify@0.1.2": {
"os": "github:jspm/nodelibs-os@0.1.0"
},
"npm:pako@0.2.8": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:parse-asn1@5.0.0": {
"asn1.js": "npm:asn1.js@4.3.0",
"browserify-aes": "npm:browserify-aes@1.0.5",
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"create-hash": "npm:create-hash@1.1.2",
"evp_bytestokey": "npm:evp_bytestokey@1.0.0",
"pbkdf2": "npm:pbkdf2@3.0.4",
"systemjs-json": "github:systemjs/plugin-json@0.1.0"
},
"npm:path-browserify@0.0.0": {
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:pbkdf2@3.0.4": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"child_process": "github:jspm/nodelibs-child_process@0.1.0",
"create-hmac": "npm:create-hmac@1.1.4",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"path": "github:jspm/nodelibs-path@0.1.0",
"process": "github:jspm/nodelibs-process@0.1.2",
"systemjs-json": "github:systemjs/plugin-json@0.1.0"
},
"npm:process-nextick-args@1.0.6": {
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:process@0.11.2": {
"assert": "github:jspm/nodelibs-assert@0.1.0"
},
"npm:promise@7.1.1": {
"asap": "npm:asap@2.0.3",
"fs": "github:jspm/nodelibs-fs@0.1.2"
},
"npm:public-encrypt@4.0.0": {
"bn.js": "npm:bn.js@4.6.2",
"browserify-rsa": "npm:browserify-rsa@4.0.0",
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"create-hash": "npm:create-hash@1.1.2",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"parse-asn1": "npm:parse-asn1@5.0.0",
"randombytes": "npm:randombytes@2.0.1"
},
"npm:punycode@1.3.2": {
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:query-string@3.0.0": {
"strict-uri-encode": "npm:strict-uri-encode@1.1.0"
},
"npm:randombytes@2.0.1": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"crypto": "github:jspm/nodelibs-crypto@0.1.0",
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:react-dom@0.14.6": {
"react": "npm:react@0.14.6"
},
"npm:react-google-analytics@0.2.0": {
"react": "npm:react@0.14.6"
},
"npm:react-router@1.0.3": {
"child_process": "github:jspm/nodelibs-child_process@0.1.0",
"fs": "github:jspm/nodelibs-fs@0.1.2",
"history": "npm:history@1.17.0",
"invariant": "npm:invariant@2.2.0",
"process": "github:jspm/nodelibs-process@0.1.2",
"warning": "npm:warning@2.1.0"
},
"npm:react@0.14.6": {
"envify": "npm:envify@3.4.0",
"fbjs": "npm:fbjs@0.6.1",
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:readable-stream@1.0.27-1": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"core-util-is": "npm:core-util-is@1.0.2",
"events": "github:jspm/nodelibs-events@0.1.1",
"inherits": "npm:inherits@2.0.1",
"isarray": "npm:isarray@0.0.1",
"process": "github:jspm/nodelibs-process@0.1.2",
"stream": "github:jspm/nodelibs-stream@0.1.0",
"string_decoder": "npm:string_decoder@0.10.31"
},
"npm:readable-stream@2.0.5": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"core-util-is": "npm:core-util-is@1.0.2",
"events": "github:jspm/nodelibs-events@0.1.1",
"inherits": "npm:inherits@2.0.1",
"isarray": "npm:isarray@0.0.1",
"process": "github:jspm/nodelibs-process@0.1.2",
"process-nextick-args": "npm:process-nextick-args@1.0.6",
"string_decoder": "npm:string_decoder@0.10.31",
"util-deprecate": "npm:util-deprecate@1.0.2"
},
"npm:ripemd160@1.0.1": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:sha.js@2.4.4": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"fs": "github:jspm/nodelibs-fs@0.1.2",
"inherits": "npm:inherits@2.0.1",
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:source-map@0.1.31": {
"amdefine": "npm:amdefine@1.0.0",
"fs": "github:jspm/nodelibs-fs@0.1.2",
"path": "github:jspm/nodelibs-path@0.1.0",
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:stream-browserify@1.0.0": {
"events": "github:jspm/nodelibs-events@0.1.1",
"inherits": "npm:inherits@2.0.1",
"readable-stream": "npm:readable-stream@1.0.27-1"
},
"npm:string_decoder@0.10.31": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0"
},
"npm:superagent@1.2.0": {
"buffer": "github:jspm/nodelibs-buffer@0.1.0",
"component-emitter": "npm:component-emitter@1.1.2",
"cookiejar": "npm:cookiejar@2.0.1",
"debug": "npm:debug@2.2.0",
"extend": "npm:extend@1.2.1",
"form-data": "npm:form-data@0.2.0",
"formidable": "npm:formidable@1.0.14",
"fs": "github:jspm/nodelibs-fs@0.1.2",
"http": "github:jspm/nodelibs-http@1.7.1",
"https": "github:jspm/nodelibs-https@0.1.0",
"methods": "npm:methods@1.0.1",
"mime": "npm:mime@1.3.4",
"qs": "npm:qs@2.3.3",
"readable-stream": "npm:readable-stream@1.0.27-1",
"reduce-component": "npm:reduce-component@1.0.1",
"stream": "github:jspm/nodelibs-stream@0.1.0",
"string_decoder": "github:jspm/nodelibs-string_decoder@0.1.0",
"systemjs-json": "github:systemjs/plugin-json@0.1.0",
"url": "github:jspm/nodelibs-url@0.1.0",
"util": "github:jspm/nodelibs-util@0.1.0",
"zlib": "github:jspm/nodelibs-zlib@0.1.0"
},
"npm:through@2.3.8": {
"process": "github:jspm/nodelibs-process@0.1.2",
"stream": "github:jspm/nodelibs-stream@0.1.0"
},
"npm:timers-browserify@1.4.2": {
"process": "npm:process@0.11.2"
},
"npm:ua-parser-js@0.7.10": {
"systemjs-json": "github:systemjs/plugin-json@0.1.0"
},
"npm:url@0.10.3": {
"assert": "github:jspm/nodelibs-assert@0.1.0",
"punycode": "npm:punycode@1.3.2",
"querystring": "npm:querystring@0.2.0",
"util": "github:jspm/nodelibs-util@0.1.0"
},
"npm:util-deprecate@1.0.2": {
"util": "github:jspm/nodelibs-util@0.1.0"
},
"npm:util@0.10.3": {
"inherits": "npm:inherits@2.0.1",
"process": "github:jspm/nodelibs-process@0.1.2"
},
"npm:vm-browserify@0.0.4": {
"indexof": "npm:indexof@0.0.1"
},
"npm:warning@2.1.0": {
"loose-envify": "npm:loose-envify@1.1.0",
"process": "github:jspm/nodelibs-process@0.1.2"
}
}
});

View file

12
public/src/app.js Normal file
View file

@ -0,0 +1,12 @@
import Vue from 'vue';
import App from './app.vue';
import store from './store';
import router from './router';
const app = new Vue({
router,
store,
...App,
});
export { app, router, store };

42
public/src/app.vue Normal file
View file

@ -0,0 +1,42 @@
<template>
<div id="app">
<h1 class="title has-text-centered header"><router-link to="/" exact>
match<span class="lighter">.audio</span>
</router-link></h1>
<div>
<router-view></router-view>
</div>
<footer class="footer">
<div class="container has-text-right">
<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>
</footer>
</div>
</template>
<style>
body {
color: #445470;
background: #fff;
}
h1 {
background: #FE4365;
padding: 25px 0;
}
h1 a {
color: #fff;
}
h1 a:hover {
color: #ffacc5;
}
.title a:hover {
border-bottom: none;
}
h1 .lighter {
color: #ffacc5;
}
.footer {
margin-top: 50px;
padding-bottom: 40px;
}
</style>

View file

@ -0,0 +1,54 @@
<template>
<form role="form" method="post" action="/search" v-on:submit="submit">
<p class="control has-addons">
<input class="input is-expanded is-large" type="text" placeholder="Paste your link here" v-model="url">
<button type="submit" class="button is-primary is-large">
Share Music
</button>
</p>
</form>
</template>
<script>
import { musicSearch } from '../store/api';
export default {
name: 'search-view',
data() {
return {
url: '',
};
},
methods: {
submit (event) {
event.preventDefault();
musicSearch(this.url).end((req, res) => {
const item = res.body;
this.$router.push(`/${item.service}/${item.albumName ? 'track' : 'album'}/${item.externalId}`);
});
},
},
}
</script>
<style>
.button.is-primary {
background-color: #FE4365;
}
.button.is-primary:hover {
background-color: #E52A4C;
}
.button.is-primary:focus {
background-color: #E52A4C;
}
.input:active {
border-color: #FE4365;
}
.input:focus {
border-color: #FE4365;
}
form {
margin-bottom: 50px;
margin-top: 200px;
}
</style>

View file

@ -0,0 +1,8 @@
import { sync } from 'vuex-router-sync';
import { app, store, router } from './app';
document.addEventListener('DOMContentLoaded', () => { // eslint-disable-line no-undef
store.replaceState(window.__INITIAL_STATE__); // eslint-disable-line
sync(store, router);
app.$mount('#app');
});

View file

@ -0,0 +1,15 @@
import { app, router, store } from './app';
// This exported function will be called by `bundleRenderer`.
// This is where we perform data-prefetching to determine the
// state of our application before actually rendering it.
// Since data fetching is async, this function is expected to
// return a Promise that resolves to the app instance.
export default (context) => {
// set router's location
router.push(context.url);
store.replaceState(context.initialState);
return app;
};

View file

@ -0,0 +1,25 @@
import Vue from 'vue';
import Router from 'vue-router';
import index from '../views/index.vue';
import share from '../views/share.vue';
Vue.use(Router);
const router = new Router({
mode: 'history',
routes: [
{ path: '/', component: index },
{ path: '/:service/:type/:id', name: 'share', component: share },
],
});
router.afterEach((to) => {
if (typeof window !== 'undefined') {
ga('send', { // eslint-disable-line no-undef
hitType: 'pageview',
page: to.fullPath,
});
}
});
export default router;

14
public/src/store/api.js Normal file
View file

@ -0,0 +1,14 @@
import request from 'superagent';
import 'superagent-bluebird-promise';
export function fetchItem(service, type, id) {
return request.get(`/${service}/${type}/${id}.json`);
}
export function fetchRecents() {
return request.get('/recent');
}
export function musicSearch(url) {
return request.post('/search').send({ url });
}

37
public/src/store/index.js Normal file
View file

@ -0,0 +1,37 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { fetchItem, fetchRecents } from './api';
Vue.use(Vuex);
const store = new Vuex.Store({
debug: true,
state: {
recents: [],
item: {},
services: [],
},
actions: {
// ensure data for rendering given list type
FETCH_RECENTS: ({ commit }) => fetchRecents()
.then(res => commit('SET_RECENTS', { recents: res.body.recents })),
FETCH_ITEM: ({ commit, state }, { service, type, id }) => fetchItem(service, type, id)
.then(item => {
return commit('SET_ITEM', { item })
}),
},
mutations: {
SET_RECENTS: (state, { recents }) => {
state.recents = recents; // eslint-disable-line no-param-reassign
},
SET_ITEM: (state, { item }) => {
state.item = item.body;
},
},
});
export default store;

149
public/src/style/style.css Normal file
View file

@ -0,0 +1,149 @@
/* app.vue */
body {
color: #445470;
background: #fff;
}
h1 {
background: #FE4365;
padding: 25px 0;
}
h1 a {
color: #fff;
}
h1 a:hover {
color: #ffacc5;
}
.title a:hover {
border-bottom: none;
}
h1 .lighter {
color: #ffacc5;
}
.footer {
margin-top: 50px;
padding-bottom: 40px;
}
/* index.vue */
.blurb {
margin-bottom: 50px;
}
.recently-shared {
margin-bottom: 50px;
}
.faq {
margin-bottom: 50px;
}
.faq p {
margin-bottom: 30px;
}
.home {
width: 600px;
margin-top: 40px;
}
p {
margin-bottom: 10px;
}
.recent .artwork {
margin-bottom: 30px;
}
.artwork {
position: relative;
width: 100%;
height: 0;
padding-bottom: 100%;
background-repeat: no-repeat;
background-size: cover;
border-radius: 5px;
}
/* search.vue */
.button.is-primary {
background-color: #FE4365;
}
.button.is-primary:hover {
background-color: #E52A4C;
}
.button.is-primary:focus {
background-color: #E52A4C;
}
.input:active {
border-color: #FE4365;
}
.input:focus {
border-color: #FE4365;
}
form {
margin-bottom: 50px;
margin-top: 200px;
}
/* share.vue */
.share-heading {
margin-bottom: 50px
}
.share-heading .title {
color: #8396b0;
}
.share-heading .title strong {
color: #445470;
font-weight: 700;
}
.artwork {
position: relative;
width: 100%;
height: 0;
padding-bottom: 100%;
background-repeat: no-repeat;
background-size: cover;
border-radius: 5px;
}
.artwork-youtube {
background-position: 50% 0%;
}
.service {
position: relative;
margin-bottom: 10px;
}
.service-link img {
margin-top: 20px;
margin-bottom: 20px;
height: 40px;
}
img {
vertical-align: middle;
}
.not-found {
opacity: 0.2;
}
.match {
position: relative;
}
.no-match {
position: absolute;
top: 10px;
right: 10px;
background: #fff;
color: #FE4365;
padding: 3px 6px;
border-radius: 3px;
opacity: 0.7;
font-weight: bold;
}
.loading-wrap {
position: absolute;
top: 0;left: 0;
background: #fff;
height: 100%;
width: 100%;
opacity: 0.8;
}
.loading {
position: absolute;
top: 35%;
left: 40%;
width: 20%;
}

124
public/src/views/index.vue Normal file
View file

@ -0,0 +1,124 @@
<template>
<div class="home container">
<search></search>
<div class="blurb">
<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 class="recently-shared">
<h2 class="title is-2">Recently Shared</h2>
<ul class="columns is-multiline">
<li v-for="(item, index) in recents" class="column is-one-third ">
<router-link :to="{ name: 'share', params: { service: item.service, type: item.albumName ? 'track' : 'album', id: item.externalId }}"><div v-bind:style="{ backgroundImage: `url(${item.matches.find(function(el) { return el.service == item.service }).artworkLarge })` }" class="artwork">
</div></router-link>
</li>
</ul>
</div>
<div class="faq">
<h2 class="title is-2">Questions?</h2>
<h3 class="title is-3">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>
<h3 class="title is-3">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>
<h3 class="title is-3">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>
<h3 class="title is-3">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>
<h3 class="title is-3">Why don't you guys support Bandcamp, Amazon Music, Sony Music Unlimited ?</h3>
<p>Let me stop you there. Match Audio is open source, 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 submit a request and maybe we'll do it for you.</p>
</div>
<div>
<h2 class="title is-2">Tools</h2>
<div class="columns">
<p class="column is-half">
Download the Chrome Extension and get Match Audio links right from your address bar.
</p>
<p class="column is-half">
<a href="https://chrome.google.com/webstore/detail/kjfpkmfgcflggjaldcfnoppjlpnidolk"><img src="/assets/images/chrome-web-store.png" alt="Download the Chrome Extension" /></a>
</p>
</div>
</div>
</div>
</template>
<script>
import { fetchRecents } from '../store/api';
import search from '../components/search.vue';
export default {
name: 'index-view',
components: { search },
created () {
// fetch the data when the view is created and the data is
// already being observed
this.fetch();
},
data() {
return {
recents: {},
};
},
watch: {
'$route': 'fetch',
recents: function () {
if (typeof document !== 'undefined') {
const recents = this.$store.state.recents;
document.title = `Match Audio • Share Music`;
}
},
},
methods: {
fetch () {
if (!this.$store.state.recents) {
fetchRecents().then((res) => {
this.recents = res.body.recents;
});
} else {
this.recents = this.$store.state.recents;
}
},
},
}
</script>
<style>
.blurb {
margin-bottom: 50px;
}
.recently-shared {
margin-bottom: 50px;
}
.faq {
margin-bottom: 50px;
}
.faq p {
margin-bottom: 30px;
}
.home {
width: 600px;
margin-top: 40px;
}
p {
margin-bottom: 10px;
}
.recent .artwork {
margin-bottom: 30px;
}
.artwork {
position: relative;
width: 100%;
height: 0;
padding-bottom: 100%;
background-repeat: no-repeat;
background-size: cover;
border-radius: 5px;
}
</style>

146
public/src/views/share.vue Normal file
View file

@ -0,0 +1,146 @@
<template>
<div class="container" v-if="item.name">
<div class="share-heading">
<h3 class="title is-3">Matched {{ item.albumName ? 'tracks' : 'albums' }} for</h3>
<h2 class="title is-2"><strong>{{ item.name }}</strong> - {{ item.artist.name }}</h2>
</div>
<ul class="columns is-multiline">
<li v-for="match in item.matches" class="column is-2">
<div v-if="match.externalId && match.id != 152">
<a v-bind:href="match.streamUrl"><div v-bind:style="{ backgroundImage: `url(${match.artworkLarge})` }" class="artwork">
</div></a>
<div class='service-link has-text-centered'>
<a v-bind:href="match.streamUrl"><img v-bind:src="`/assets/images/${match.service}.png`" /></a>
</div>
</div>
<div v-if="match.matching || match.id === 152" class="service">
<div v-bind:style="{ backgroundImage: `url(${item.matches[0].artworkLarge})` }" class="artwork">
</div>
<div class='loading-wrap'>
<img src='/assets/images/eq.svg' class='loading' />
</div>
<div class='service-link has-text-centered'>
<img v-bind:src="`/assets/images/${match.service}.png`" />
</div>
</div>
<div class="service" v-if="!match.externalId && !match.matching">
<div v-bind:style="{ backgroundImage: `url(${item.matches[0].artworkLarge})` }" class="artwork not-found">
</div>
<div class='no-match'>
No Match
</div>
<div class='service-link has-text-centered not-found'>
<img v-bind:src="`/assets/images/${match.service}.png`" />
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
import { fetchItem } from '../store/api';
export default {
name: 'share-view',
data() {
return {
item: {},
};
},
created () {
// fetch the data when the view is created and the data is
// already being observed
this.fetch();
this.interval = setInterval(() => {
this.fetch();
}, 1000);
},
watch: {
// call again the method if the route changes
'$route': 'fetch',
},
methods: {
fetch () {
const item = this.$store.state.item;
const id = this.$route.params.id;
if (item && item.externalId === id && (typeof window === 'undefined' || !item.matches.some(match => match.matching))) {
this.item = this.$store.state.item;
} else {
fetchItem(this.$route.params.service, this.$route.params.type, id).then((res) => {
if(!res.body.matches.some(match => match.matching)) {
clearInterval(this.interval);
}
this.item = res.body;
document.title = `Match Audio • ${this.item.artist.name} - ${this.item.name}`;
});
}
}
}
}
</script>
<style>
.share-heading {
margin-bottom: 50px
}
.share-heading .title {
color: #8396b0;
}
.share-heading .title strong {
color: #445470;
font-weight: 700;
}
.artwork {
position: relative;
width: 100%;
height: 0;
padding-bottom: 100%;
background-repeat: no-repeat;
background-size: cover;
background-position-x: center;
border-radius: 5px;
}
.service {
margin-bottom: 10px;
}
.service-link img {
margin-top: 20px;
margin-bottom: 20px;
height: 40px;
}
img {
vertical-align: middle;
}
.not-found {
opacity: 0.2;
}
.match {
position: relative;
}
.no-match {
position: absolute;
top: 10px;
right: 10px;
background: #fff;
color: #FE4365;
padding: 3px 6px;
border-radius: 3px;
opacity: 0.7;
font-weight: bold;
}
.loading-wrap {
position: absolute;
top: 0;left: 0;
background: #fff;
height: 100%;
width: 100%;
opacity: 0.8;
}
.loading {
position: absolute;
top: 35%;
left: 40%;
width: 20%;
}
</style>

View file

@ -1,259 +0,0 @@
body {
font-family: "Open Sans";
color: #445470;
}
html, body {
height: 100%;
}
header {
background: #FE4365;
}
.page-wrap {
min-height: 100%;
margin-bottom: -180px;
}
.page-wrap:after {
content: "";
display: block;
}
footer, .page-wrap:after {
margin-top: 80px;
height: 100px;
}
footer {
line-height: 100px;
text-align: right;
}
header h1 {
margin: 0;
padding: 0;
text-align: center;
font-weight: 300;
font-size: 2em;
line-height: 80px;
}
header h1 a {
color: #fff;
}
a {
color: #C04969;
}
a:hover {
color: #824F6D;
}
header h1 a:hover, header h1 a:focus{
color: #ffacc5;
text-decoration: none;
}
.home input[type="text"]:focus {
border-color: rgba(255, 68, 109, 0.8);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 5px rgba(255, 68, 109, 0.8);
outline: 0 none;
}
.faq ul {
padding: 0;
}
.faq li {
list-style: none;
}
.share header h1, .share header h1 {
text-align: left;
font-size: 1.5em;
line-height: 36px;
}
.audio-lighten {
color: #ffacc5;
}
.artist-lighten {
color: #8396b0;
font-weight: 300;
}
h2 {
font-size: 1.8em;
font-weight: 700;
margin-bottom: 40px;
}
h3 {
font-size: 1.5em;
color: #8396b0;
font-weight: 300;
}
.share-form {
margin-top: 25vh;
margin-bottom: 20px;
text-align: center;
}
.share-form form {
margin-bottom: 30px;
}
.share-form .alert {
margin-top: 20px;
}
.btn-custom {
background-color: #FE4365;
}
.btn-custom, .btn-custom:active, .btn-custom:hover, .btn-custom:focus {
color: #fff;
}
.blurb {
margin-bottom: 50px;
}
.recent .artwork {
margin-bottom: 30px;
}
.share-tools {
text-align: center;
margin-top: 40px;
font-weight: 300;
}
.share-tools img {
width: 20px;
}
.service {
padding: 40px 10px 10px 10px;
margin-bottom: 10px;
}
.matching-from {
position: absolute;
top: 10px;
left: 25px;
}
.source-service {
border-radius: 5px;
background: #eee;
}
.artwork {
position: relative;
width: 100%;
height: 0;
padding-bottom: 100%;
background-repeat: none;
background-size: cover;
border-radius: 5px;
}
.artwork-youtube {
background-position: 50% 0%;
}
.not-found {
opacity: 0.2;
}
.no-match {
position: absolute;
bottom: 80px;
right: 37px;
background: #fff;
color: #FE4365;
padding: 3px 6px;
border-radius: 3px;
opacity: 0.7;
font-weight: bold;
}
.youtube {
position: absolute;
font-weight: bold;
bottom: 65px;
left: 25px;
right: 25px;
padding: 10px;
opacity: 0.85;
background: #fff;
color: #FE4365;
}
.loading-wrap {
position: absolute;
top: 0;left: 0;
background: #fff;
height: 100%;
width: 100%;
opacity: 0.8;
}
.loading {
position: absolute;
top: 35%;
left: 40%;
width: 20%;
}
.service-link {
text-align: center;
}
.service-link a {
font-size: 1.8em;
color: #444;
}
.service-link a:hover {
text-decoration: none;
}
.service-link img {
height: 40px;
}
.error {
background: #FE4365;
color: #febdc9;
}
.error h1, .error h2 {
font-weight: 100;
}
.error .main h1 {
font-size: 2em;
margin-bottom: 20px;
color: #fff;
}
.error h2 {
color: #ff7c94;
font-size: 4em;
margin-top: 0px;
margin-bottom: 20px;
}
.error .col-md-12 {
margin-top: -1px;
}
.error a {
color: #fff;
}
.error-logo {
position: absolute;
top: 50px;
left: 15px;
}
.vertical-center {
min-height: 100%; /* Fallback for browsers do NOT support vh unit */
min-height: 100vh; /* These two lines are counted as one :-) */
display: flex;
align-items: center;
}

View file

@ -1,19 +1,46 @@
import React from 'react';
import { renderPage } from '../lib/react-handler';
import { routes } from '../views/app';
import debuglog from 'debug';
import services from '../lib/services';
import render from '../lib/render';
import models from '../models';
const debug = debuglog('match.audio:share');
const recentQuery = {
include: [
{ model: models.artist },
{ model: models.match },
],
limit: 6,
order: [
['updatedAt', 'DESC'],
],
};
export default function* () {
const recents = [];
const docs = yield this.db.matches.find().sort({'created_at': -1}).limit(6).toArray();
docs.forEach(function(doc) {
let shares = Object.keys(doc.services).map(function (key) {return doc.services[key]; });
shares.some(function(item) {
if (item.service === doc._id.split('$$')[0]) { // eslint-disable-line no-underscore-dangle
recents.push(item);
return false;
}
});
});
const recentAlbums = yield models.album.findAll(recentQuery);
const recentTracks = yield models.track.findAll(recentQuery);
yield renderPage(routes, this.request.url, {recents: recents});
};
const initialState = {
recents: recentAlbums.map(album => album.toJSON())
.concat(recentTracks.map(track => track.toJSON()))
.sort((a, b) => a.createdAt < b.createdAt).slice(0, 6),
services: services.map(service => service.id),
};
const url = '/';
const html = yield render(url, initialState);
const head = {
title: `Share Music`,
shareUrl: `${this.request.origin}${url}`,
image: `${this.request.origin}/assets/images/logo-512.png`,
}
yield this.render('index', {
initialState,
head,
html,
});
}

View file

@ -1,14 +0,0 @@
import { parse } from 'url';
import request from 'superagent';
export default function* (next) {
const url = 'http://' + this.request.url.substr(8);
const parsed = parse(url);
if (parsed.host.match(/mzstatic\.com/)) {
const proxyResponse = yield request.get(url);
this.set(proxyResponse.headers);
this.body = proxyResponse.body;
} else {
yield next;
}
};

23
routes/recent.js Normal file
View file

@ -0,0 +1,23 @@
import models from '../models';
const recentQuery = {
include: [
{ model: models.artist },
{ model: models.match },
],
limit: 6,
order: [
['updatedAt', 'DESC'],
],
};
export default function* () {
const recentAlbums = yield models.album.findAll(recentQuery);
const recentTracks = yield models.track.findAll(recentQuery);
this.body = {
recents: recentAlbums.map(album => album.toJSON())
.concat(recentTracks.map(track => track.toJSON()))
.sort((a, b) => a.createdAt < b.createdAt).slice(0, 6),
};
}

View file

@ -1,49 +1,39 @@
import { parse } from 'url';
import co from 'co';
import lookup from '../lib/lookup';
import services from '../lib/services';
import { find, create, findMatchesAsync } from '../lib/share';
import debuglog from 'debug';
const debug = debuglog('match.audio:search');
module.exports = function* () {
export default function* () {
const url = parse(this.request.body.url);
this.assert(url.host, 400, {error: {message: 'You need to submit a url.'}});
const item = yield lookup(this.request.body.url);
this.assert(url.host, 400, { error: { message: 'You need to submit a url.' } });
this.assert(item, 400, {error: {message: 'No supported music found at that link :('}});
const music = yield lookup(this.request.body.url);
item.matched_at = new Date(); // eslint-disable-line camelcase
const matches = {};
matches[item.service] = item;
this.assert(music, 400, { error: { message: 'No supported music found at that link :(' } });
let share = yield find(music);
for (let service of services) {
if (service.id === item.service) {
continue;
}
matches[service.id] = {service: service.id};
if (!share) {
share = yield create(music);
findMatchesAsync(share);
}
yield this.db.matches.save({_id: item.service + '$$' + item.id, 'created_at': new Date(), services: matches});
this.body = item;
share = share.toJSON();
process.nextTick(() => {
for (let service of services) {
if (service.id === item.service) {
continue;
}
matches[service.id] = {service: service.id};
co(function* (){
const match = yield service.search(item);
match.matched_at = new Date(); // eslint-disable-line camelcase
const update = {};
update['services.' + match.service] = match;
yield this.db.matches.updateOne({_id: item.service + '$$' + item.id}, {'$set': update});
}.bind(this)).catch((err) => {
debug(err);
});
}
});
};
const unmatched = services.filter(service =>
!share.matches.find(match => match.service === service.id));
share.matches = share.matches.concat(unmatched.map((service) => {
return {
service: service.id,
matching: true,
};
}));
share.matches = share.matches.sort(a => !!a.externalId);
this.body = share;
}

View file

@ -1,72 +1,73 @@
import React from 'react';
import { renderPage } from '../lib/react-handler';
import { routes } from '../views/app';
import services from '../lib/services';
import co from 'co';
import render from '../lib/render';
import models from '../models';
import { find, create, findMatchesAsync } from '../lib/share';
function formatAndSort(matches, serviceId) {
matches = Object.keys(matches).map(function (key) {return matches[key]; });
matches.sort(function(a, b) {
return a.id && !b.id;
}).sort(function(a) {
return a.service !== serviceId;
});
return matches;
};
export default function* (serviceId, type, itemId, format) {
this.assert(type === 'album' || type === 'track', 400, { error: 'Invalid type' });
export default function* (serviceId, type, itemId, format, next) {
let matchedService;
services.some(function(service) {
matchedService = serviceId === service.id ? service : null;
return matchedService;
let share = yield models[type].findOne({
where: {
externalId: itemId,
},
include: [
{ model: models.match },
{ model: models.artist },
],
});
if (!matchedService || (type !== 'album' && type !== 'track')) {
return yield next;
}
if (!share) {
const matchedService = services.find(service => serviceId === service.id);
const music = yield matchedService.lookupId(itemId, type);
let doc = yield this.db.matches.findOne({_id: serviceId + '$$' + itemId});
if (!doc) {
const item = yield matchedService.lookupId(itemId, type);
this.assert(item.id, 404);
item.matched_at = new Date(); // eslint-disable-line camelcase
const matches = {};
matches[item.service] = item;
this.assert(music, 400, { error: { message: 'No supported music found at that link :(' } });
for (let service of services) {
if (service.id === item.service) {
continue;
}
matches[service.id] = {service: service.id};
share = yield find(music);
if (!share) {
share = yield create(music);
findMatchesAsync(share);
}
doc = {_id: item.service + '$$' + item.id, 'created_at': new Date(), services: matches};
yield this.db.matches.save(doc);
process.nextTick(() => {
for (let service of services) {
console.log(service.id);
if (service.id === item.service) {
continue;
}
matches[service.id] = {service: service.id};
co(function* (){
const match = yield service.search(item);
console.log(match.id);
match.matched_at = new Date(); // eslint-disable-line camelcase
const update = {};
update['services.' + match.service] = match;
yield this.db.matches.updateOne({_id: item.service + '$$' + item.id}, {'$set': update});
}.bind(this)).catch((err) => {
debug(err);
});
}
});
}
const shares = formatAndSort(doc.services, serviceId);
this.assert(share, 404);
const unmatched = services.filter(service =>
!share.matches.find(match => match.service === service.id));
share = share.toJSON();
share.matches = share.matches.concat(unmatched.map((service) => {
return {
service: service.id,
matching: true,
};
}));
share.matches = share.matches.sort(a => !a.externalId);
if (format === 'json') {
return this.body = {shares: shares};
}
this.body = share;
} else {
const initialState = {
item: share,
services: services.map(service => service.id),
};
this.body = yield renderPage(routes, this.request.url, {shares: shares});
};
const url = `/${serviceId}/${type}/${itemId}`;
const html = yield render(url, initialState);
const head = {
share,
title: `${share.name} by ${share.artist.name}`,
shareUrl: `${this.request.origin}${url}`,
image: share.matches.find(el => el.service === share.service).artworkLarge,
};
yield this.render('index', {
initialState,
share,
head,
html,
});
}
}

View file

@ -1,6 +1,6 @@
{
"env": {
"node": true,
"node": true,
"mocha": true
}
}

11
test/init.js Normal file
View file

@ -0,0 +1,11 @@
import co from 'co';
import models from '../models';
import debugname from 'debug';
const debug = debugname('match.audio:db');
co(function *sync() {
debug('Syncing schema');
yield models.sequelize.sync();
debug('Schema synced');
});

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>
);
}
});

45
webpack.config.js Normal file
View file

@ -0,0 +1,45 @@
const path = require('path');
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
entry: './public/src/entry-client.js',
output: {
path: path.resolve(__dirname, './public/dist'),
publicPath: '/dist/',
filename: 'js/build-client.js',
},
resolve: {
modules: [
'node_modules',
path.resolve(__dirname, 'public/src'),
],
extensions: ['.js', '.json', '.vue', '.css'],
},
module: {
loaders: [
{
test: /\.vue$/,
loader: 'vue',
},
{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules/,
},
],
},
devtool: '#source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"',
},
}),
new webpack.optimize.OccurrenceOrderPlugin(),
new CopyWebpackPlugin([{
from: path.resolve(__dirname, './node_modules/bulma/css'),
to: path.resolve(__dirname, './public/dist/css/'),
}]),
],
};

49
webpack.config.server.js Normal file
View file

@ -0,0 +1,49 @@
const path = require('path');
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
target: 'node',
entry: './public/src/entry-server.js',
output: {
libraryTarget: 'commonjs2',
path: path.resolve(__dirname, './public/dist'),
publicPath: '/dist/',
filename: 'js/build-server.js',
},
resolve: {
modules: [
'node_modules',
path.resolve(__dirname, 'public/src'),
],
extensions: ['.js', '.json', '.vue', '.css'],
},
plugins: [
new webpack.DefinePlugin({
'global.GENTLY': false,
}),
new webpack.optimize.OccurrenceOrderPlugin(),
new CopyWebpackPlugin([{
from: path.resolve(__dirname, './node_modules/bulma/css'),
to: path.resolve(__dirname, './public/dist/css/'),
}]),
],
module: {
loaders: [
{
test: /\.vue$/,
loader: 'vue',
},
{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules/,
},
{
test: /\.json$/,
loader: 'json',
},
],
},
devtool: '#source-map',
};

5419
yarn.lock Normal file

File diff suppressed because it is too large Load diff