├── .babelrc
├── .jshintrc
├── client
├── public
│ ├── assets
│ │ ├── album-image.jpg
│ │ ├── artist-image.jpg
│ │ ├── venue-image.jpg
│ │ └── favicons
│ │ │ ├── favicon.ico
│ │ │ ├── favicon-16x16.png
│ │ │ ├── favicon-32x32.png
│ │ │ ├── mstile-150x150.png
│ │ │ ├── apple-touch-icon.png
│ │ │ ├── android-chrome-192x192.png
│ │ │ ├── manifest.json
│ │ │ ├── browserconfig.xml
│ │ │ └── safari-pinned-tab.svg
│ ├── AudioPlayer.css
│ ├── DrawNavigation.css
│ ├── UpcomingShows.css
│ ├── VenueDetail.css
│ ├── Venues.css
│ ├── ShowList.css
│ ├── NavLogin.css
│ ├── Artists.css
│ ├── DrawMap.css
│ ├── ArtistDetail.css
│ ├── NavBar.css
│ ├── GenArtist.css
│ ├── UpcomingShowsDetail.css
│ ├── GenVenue.css
│ ├── index.html
│ ├── styles.css
│ ├── Show.css
│ └── react-datepicker.css
├── reducers
│ ├── reducer_venues.js
│ ├── reducer_location.js
│ ├── reducer_artists.js
│ ├── reducer_selectShow.js
│ ├── reducer_fetchShows.js
│ ├── reducer_selectArtist.js
│ ├── reducer_selectVenue.js
│ ├── reducer_speaker.js
│ ├── index.js
│ └── spotify.js
├── components
│ ├── Error.js
│ ├── NavLogin.js
│ ├── VideoList.js
│ ├── UserLogin.js
│ ├── VideoDetail.js
│ ├── GenVenue.js
│ ├── VideoListItem.js
│ ├── SearchBar.js
│ ├── App.js
│ ├── DropdownArtistTitle.js
│ ├── VenueItem.js
│ ├── ArtistList.js
│ ├── AudioPlayer.js
│ ├── UpcomingShowsDetail.js
│ ├── ArtistItem.js
│ ├── DropdownArtists.js
│ ├── UpcomingShows.js
│ ├── DropdownArtist.js
│ ├── DrawNavigation.js
│ └── DrawMap.js
├── models
│ ├── spotify.js
│ ├── getDistanceFromLatLonInKm.js
│ ├── helpers.js
│ └── api.js
├── index.js
├── containers
│ ├── Speaker.js
│ ├── Home.js
│ ├── ShowList.js
│ ├── NavBar.js
│ ├── SongkickSearch.js
│ ├── Venues.js
│ ├── Artists.js
│ ├── Show.js
│ ├── VenueDetail.js
│ └── ArtistDetail.js
└── actions
│ └── actions.js
├── .eslintrc.js
├── .gitignore
├── starter.sh
├── server
├── models
│ ├── m_lastFM.js
│ ├── m_google.js
│ ├── m_artist.js
│ ├── m_spotifyApi.js
│ └── m_songkick.js
├── ARTISTS_Schema.js
├── VENUE_Schema.js
├── server.js
├── db.js
└── routes.js
├── .eslintrc.yml
├── package.json
├── README.md
└── git_workflow.md
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0", "react"]
3 | }
4 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "browser": true,
4 | "esnext": true,
5 | "newcap": false
6 | }
7 |
--------------------------------------------------------------------------------
/client/public/assets/album-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterlooATX/MelodyMap/HEAD/client/public/assets/album-image.jpg
--------------------------------------------------------------------------------
/client/public/assets/artist-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterlooATX/MelodyMap/HEAD/client/public/assets/artist-image.jpg
--------------------------------------------------------------------------------
/client/public/assets/venue-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterlooATX/MelodyMap/HEAD/client/public/assets/venue-image.jpg
--------------------------------------------------------------------------------
/client/public/assets/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterlooATX/MelodyMap/HEAD/client/public/assets/favicons/favicon.ico
--------------------------------------------------------------------------------
/client/public/assets/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterlooATX/MelodyMap/HEAD/client/public/assets/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/client/public/assets/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterlooATX/MelodyMap/HEAD/client/public/assets/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/client/public/assets/favicons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterlooATX/MelodyMap/HEAD/client/public/assets/favicons/mstile-150x150.png
--------------------------------------------------------------------------------
/client/public/assets/favicons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterlooATX/MelodyMap/HEAD/client/public/assets/favicons/apple-touch-icon.png
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "airbnb",
3 | "plugins": [
4 | "react",
5 | "jsx-a11y",
6 | "import"
7 | ]
8 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /server/models/api_keys.js
2 |
3 | /node_modules/**/*
4 | !/node_modules/songkick-api/src/songkick-api.js
5 |
6 | npm-debug.log
7 | .DS_Store
8 | dist
9 |
--------------------------------------------------------------------------------
/client/public/assets/favicons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WaterlooATX/MelodyMap/HEAD/client/public/assets/favicons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/client/reducers/reducer_venues.js:
--------------------------------------------------------------------------------
1 | import { VENUES } from '../actions/actions';
2 |
3 | export default function (state = {}, action) {
4 | switch (action.type) {
5 | case VENUES:
6 | return action.payload;
7 | }
8 | return state;
9 | }
10 |
--------------------------------------------------------------------------------
/client/reducers/reducer_location.js:
--------------------------------------------------------------------------------
1 | import { LOCATION } from '../actions/actions';
2 |
3 | export default function (state = [], action) {
4 | switch (action.type) {
5 | case LOCATION:
6 | return action.payload;
7 | }
8 | return state;
9 | }
10 |
--------------------------------------------------------------------------------
/client/reducers/reducer_artists.js:
--------------------------------------------------------------------------------
1 | import { FETCH_ARTIST } from '../actions/actions';
2 |
3 | export default function (state = {}, action) {
4 | switch (action.type) {
5 | case FETCH_ARTIST:
6 | return action.payload;
7 | }
8 | return state;
9 | }
10 |
--------------------------------------------------------------------------------
/client/reducers/reducer_selectShow.js:
--------------------------------------------------------------------------------
1 | import { SELECT_SHOW } from '../actions/actions';
2 |
3 | export default function (state = null, action) {
4 | switch (action.type) {
5 | case SELECT_SHOW:
6 | return action.payload;
7 | }
8 | return state;
9 | }
10 |
--------------------------------------------------------------------------------
/client/reducers/reducer_fetchShows.js:
--------------------------------------------------------------------------------
1 | import { FETCH_SHOWS } from '../actions/actions';
2 |
3 | export default function (state = {}, action) {
4 | switch (action.type) {
5 | case FETCH_SHOWS:
6 | return action.payload.data;
7 | }
8 | return state;
9 | }
10 |
--------------------------------------------------------------------------------
/client/reducers/reducer_selectArtist.js:
--------------------------------------------------------------------------------
1 | import { SELECT_ARTIST } from '../actions/actions';
2 |
3 | export default function (state = null, action) {
4 | switch (action.type) {
5 | case SELECT_ARTIST:
6 | return action.payload;
7 | }
8 | return state;
9 | }
10 |
--------------------------------------------------------------------------------
/client/reducers/reducer_selectVenue.js:
--------------------------------------------------------------------------------
1 | import { SELECT_VENUE } from '../actions/actions';
2 |
3 | export default function (state = null, action) {
4 | switch (action.type) {
5 | case SELECT_VENUE:
6 | return action.payload;
7 | }
8 | return state;
9 | }
10 |
--------------------------------------------------------------------------------
/client/public/assets/favicons/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MelodyMap",
3 | "icons": [
4 | {
5 | "src": "\/android-chrome-192x192.png",
6 | "sizes": "192x192",
7 | "type": "image\/png"
8 | }
9 | ],
10 | "theme_color": "#ffffff",
11 | "display": "standalone"
12 | }
13 |
--------------------------------------------------------------------------------
/starter.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ $(ps -e -o uid,cmd | grep $UID | grep node | grep -v grep | wc -l | tr -s "\n") -eq 0 ]
4 | then
5 | export PATH=/usr/local/bin:$PATH
6 | forever start --sourceDir /sites/MelodyMap/server/server.js >> /sites/MelodyMap/server/log.txt 2>&1
7 | fi
8 |
9 |
--------------------------------------------------------------------------------
/client/reducers/reducer_speaker.js:
--------------------------------------------------------------------------------
1 | import { SET_SPEAKER } from '../actions/actions';
2 |
3 | export default function (state = { songPlayed: false, songButton: null }, action) {
4 | switch (action.type) {
5 | case SET_SPEAKER:
6 | return action.payload;
7 | }
8 | return state;
9 | }
10 |
--------------------------------------------------------------------------------
/client/public/assets/favicons/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/client/components/Error.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Login = (props) => {
4 | let { errorMsg } = props.params;
5 | return (
6 |
7 |
8 | AN ERROR OCCURED
9 |
{errorMsg}
10 |
11 | );
12 | };
13 |
14 | export default Login;
15 |
--------------------------------------------------------------------------------
/client/models/spotify.js:
--------------------------------------------------------------------------------
1 | import Spotify from 'spotify-web-api-js';
2 | const spotifyApi = new Spotify();
3 |
4 | export function followArtist(token, artistid) {
5 | spotifyApi.setAccessToken(token);
6 | spotifyApi.followArtists(artistid)
7 | .then(function (data) {
8 | console.log('DATA', data);
9 | return data;
10 | }, function (err) {
11 | console.error('ERROR', err);
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/client/components/NavLogin.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NavLogin = (props) => {
4 | return (
5 |
10 | );
11 | };
12 |
13 | export default NavLogin;
14 |
--------------------------------------------------------------------------------
/server/models/m_lastFM.js:
--------------------------------------------------------------------------------
1 | // https://github.com/leemm/last.fm.api
2 | const {LAST_FM_APIKEY, LAST_FM_APISECRET} = require('./api_keys');
3 |
4 | const API = require('last.fm.api'),
5 | api = new API({
6 | apiKey: LAST_FM_APIKEY,
7 | apiSecret: LAST_FM_APISECRET
8 | });
9 |
10 |
11 | exports.getInfo = (name) => {
12 | return api.artist.getInfo({
13 | artist: name
14 | })
15 | .then(json => json)
16 | .catch(err => console.error(err));
17 | }
18 |
--------------------------------------------------------------------------------
/client/components/VideoList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import VideoListItem from './VideoListItem';
3 |
4 | const VideoList = (props) => {
5 | const videoItems = props.videos.map(video => {
6 | return ( );
11 | });
12 |
13 | return (
14 |
17 | );
18 | };
19 |
20 | export default VideoList;
21 |
--------------------------------------------------------------------------------
/client/components/UserLogin.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const UserLogin = (props) => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 | {props.spotifyData.username.split(' ')[0]}
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default UserLogin;
18 |
--------------------------------------------------------------------------------
/client/public/AudioPlayer.css:
--------------------------------------------------------------------------------
1 |
2 | .audioPlayer-container {
3 | display: inline-block;
4 | overflow: scroll;
5 | max-height: 408px;
6 | }
7 | .audioPlayer-list-item {
8 | float:left;
9 | border: 1px solid #93c54b;
10 | width: 100%;
11 | text-align: center;
12 | padding: 9px 20px;
13 | }
14 | .audioPlayer-speaker {
15 | display: inline-block;
16 | float: left;
17 | padding-right: 20px;
18 | }
19 | .audioPlayer-trackName {
20 | display: inline-block;
21 | font-weight: 800;
22 | font-size: 14px;
23 | font-family: 'Oswald', sans-serif;
24 | }
25 |
--------------------------------------------------------------------------------
/client/components/VideoDetail.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const VideoDetail = ({ video }) => {
4 | if (!video) {
5 | return (
6 |
7 | Loading....
8 |
9 | );
10 | }
11 |
12 | const url = `https://www.youtube.com/embed/${video.id.videoId}`;
13 |
14 | return (
15 |
20 | );
21 | };
22 |
23 | export default VideoDetail;
24 |
--------------------------------------------------------------------------------
/client/models/getDistanceFromLatLonInKm.js:
--------------------------------------------------------------------------------
1 | export function getDistanceFromLatLonInKm(lat1, lon1, lat2, lon2) {
2 | function deg2rad(deg) {
3 | return deg * (Math.PI / 180);
4 | }
5 |
6 | const R = 6371; // Radius of the earth in km
7 | const dLat = deg2rad(lat2 - lat1); // deg2rad below
8 | const dLon = deg2rad(lon2 - lon1);
9 | const a =
10 | Math.sin(dLat / 2) * Math.sin(dLat / 2) +
11 | Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
12 | Math.sin(dLon / 2) * Math.sin(dLon / 2);
13 | const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
14 | const d = R * c; // Distance in km
15 | return d;
16 | }
17 |
--------------------------------------------------------------------------------
/client/components/GenVenue.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import VenueItem from '../components/VenueItem';
3 |
4 | export default class GenVenue extends Component {
5 |
6 | _createVenues() {
7 | const venues = this.props.venues;
8 | const mapped = [];
9 | for (let venueId in venues) {
10 | if (venues[venueId]) {
11 | mapped.push( );
12 | }
13 | }
14 | return mapped;
15 | }
16 |
17 | render() {
18 | const Venues = this._createVenues();
19 | return (
20 |
21 | {Venues}
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/client/components/VideoListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const VideoListItem = ({
4 | video,
5 | onVideoSelect,
6 | }) => {
7 | const imageUrl = video.snippet.thumbnails.default.url;
8 | return (
9 | onVideoSelect(video)} className="list-group-item">
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
{ video.snippet.title }
18 |
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default VideoListItem;
26 |
--------------------------------------------------------------------------------
/client/components/SearchBar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class SearchBar extends Component {
4 |
5 | constructor(props) {
6 | super(props);
7 |
8 | this.state = {
9 | term: '',
10 | };
11 | }
12 |
13 | render() {
14 | return (
15 |
16 | this.onInputChange(event.target.value)}
21 | />
22 |
23 | );
24 | }
25 |
26 | onInputChange(term) {
27 | this.setState({
28 | term,
29 | });
30 | this.props.onSearchTermChange(term);
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/client/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import NavBar from '../containers/NavBar';
3 |
4 | export default class App extends Component {
5 | constructor(props) {
6 | super(props);
7 |
8 | this.state = {
9 | visibleSearch: true,
10 | };
11 | }
12 |
13 | _onLink(bool) {
14 | this.setState({
15 | visibleSearch: bool,
16 | });
17 | }
18 |
19 | render() {
20 | return (
21 |
22 |
26 |
27 | {this.props.children}
28 |
29 |
30 | );
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/client/public/DrawNavigation.css:
--------------------------------------------------------------------------------
1 | .navigation-container {
2 | position: relative;
3 | }
4 |
5 | .navigation-close {
6 | display: flex;
7 | flex-direction: row;
8 | align-items: center;
9 | justify-content: center;
10 | width: 100%;
11 | position: absolute;
12 | top: 0;
13 | left: 0;
14 | z-index: 10;
15 | font-size: 3em;
16 | cursor: pointer;
17 | border: 1px solid #dfd7ca;
18 | border-radius: 10px;
19 | background-color: rgba(248, 245, 240, 0.6);
20 | }
21 |
22 | .glyphicon-remove-sign {
23 | -webkit-text-stroke: 1px black;
24 | color: #f75a50;
25 | margin: 0 5px;
26 | }
27 |
28 | .navigation-close-text {
29 | margin: 0 10px;
30 | }
31 |
32 | @media screen and (max-width: 767px) {
33 | .navigation-close {
34 | font-size: 2em;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/client/public/UpcomingShows.css:
--------------------------------------------------------------------------------
1 | .upcoming-show {
2 | display: flex;
3 | flex-direction: row;
4 | margin: 0px;
5 | padding: 20px 0;
6 | border-bottom: 1px solid #eee;
7 | }
8 |
9 | .upcoming-show-info {
10 | display: inline-block;
11 | flex-grow: 1;
12 | padding-bottom: 18px;
13 | }
14 |
15 | .upcoming-show-artists {
16 | font-size: 28px;
17 | }
18 |
19 | .upcoming-show-artists h1 {
20 | font-size: 30px;
21 | margin-top: 0px;
22 | }
23 |
24 | .upcoming-show-date h3 {
25 | font-size: 18px;
26 | margin-top: 0px;
27 | margin-bottom: 08px;
28 | }
29 |
30 | .upcoming-show-info {
31 | margin: 0px;
32 | padding: 0px;
33 |
34 | }
35 |
36 | .upcoming-show-venue-info h3 {
37 | font-size: 18px;
38 | margin-top: 0px;
39 | margin-bottom: 08px;
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/client/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import showReducer from './reducer_fetchShows';
3 | import locationReducer from './reducer_location';
4 | import selectShowReducer from './reducer_selectShow';
5 | import selectArtistReducer from './reducer_selectArtist';
6 | import artistReducer from './reducer_artists';
7 | import venuesReducer from './reducer_venues';
8 | import spekaerReducer from './reducer_speaker';
9 | import spotifyUser from './spotify';
10 |
11 |
12 | const rootReducer = combineReducers({
13 | shows: showReducer,
14 | selectedShow: selectShowReducer,
15 | location: locationReducer,
16 | artists: artistReducer,
17 | venues: venuesReducer,
18 | speaker: spekaerReducer,
19 | spotifyUser,
20 | });
21 |
22 | export default rootReducer;
23 |
--------------------------------------------------------------------------------
/server/ARTISTS_Schema.js:
--------------------------------------------------------------------------------
1 | var mongoose = require('mongoose');
2 |
3 | // Create a Artist schema
4 | const artistSchema = new mongoose.Schema({
5 | lastFM_imgs: Array,
6 | spotifyURL: String,
7 | id: String,
8 | name: String,
9 | images: Array,
10 | img: String,
11 | popularity: Number,
12 | followers: Number,
13 | relatedArtists: Array,
14 | albumsImages: Array,
15 | topTracks: Array,
16 | summaryBio: String,
17 | fullBio: String,
18 | updated_at: { type: Date, default: Date.now },
19 | songKickID: Number,
20 | onTour: String,
21 | genre: Array
22 | });
23 |
24 | // Created a Mongoose schema which maps to a MongoDB collection and defines
25 | // the shape of the documents within that collection.
26 |
27 | // exported the Mongoose Artist model
28 | module.exports = mongoose.model("Artist", artistSchema);
29 |
--------------------------------------------------------------------------------
/server/VENUE_Schema.js:
--------------------------------------------------------------------------------
1 | var mongoose = require('mongoose');
2 |
3 | // Create a Venue schema
4 | const venueSchema = new mongoose.Schema({
5 | id: Number,
6 | ageRestriction: String,
7 | capacity: String(),
8 | street: String,
9 | geo: {lat: Number, long: Number},
10 | city: String,
11 | state: String,
12 | website: String,
13 | name: String,
14 | address: String,
15 | phone: String,
16 | upcomingShows: Array,
17 | updated_at: { type: Date, default: Date.now },
18 | google: Object,
19 | photo: String,
20 | rating: String,
21 | icon: String,
22 | googleID: String,
23 | price: String
24 | });
25 |
26 | // Created a Mongoose schema which maps to a MongoDB collection and defines
27 | // the shape of the documents within that collection.
28 |
29 | // exported the Mongoose Venue model
30 | module.exports = mongoose.model("Venue", venueSchema);
31 |
--------------------------------------------------------------------------------
/client/public/VenueDetail.css:
--------------------------------------------------------------------------------
1 | .venue-basic-info {
2 | margin: 5px;
3 | }
4 |
5 | .venue-detail-jumbotron {
6 | height: auto;
7 | }
8 |
9 | .venue-detail-row {
10 | padding: 0px 30px;
11 | }
12 |
13 | .google-embeds {
14 | padding: 30px;
15 | }
16 |
17 | .google-iframe {
18 | width: 100%;
19 | height: 60vh;
20 | pointer-events: none;
21 | /*border: none;*/
22 | }
23 |
24 | .upcoming-shows-container {
25 | overflow: scroll;
26 | height: 120vh;
27 | padding: 30px;
28 | }
29 |
30 | .upcoming-shows-header {
31 | margin: 0px;
32 | padding-bottom: 20px;
33 | }
34 |
35 | @media screen and (max-width: 767px) {
36 | .google-embeds {
37 | padding: 0;
38 | }
39 |
40 | .btn {
41 | width: 23px;
42 | padding: 0 0 0 1px;
43 | }
44 |
45 | .upcoming-show h1 {
46 | font-size: 17px;
47 | }
48 |
49 | .upcoming-show div {
50 | font-size: 13px;
51 | }
52 | }
--------------------------------------------------------------------------------
/client/public/Venues.css:
--------------------------------------------------------------------------------
1 | .page-header > h1 {
2 | display: inline;
3 | font-size: 38px;
4 | }
5 |
6 | .page-header.venues-header {
7 | margin-top: 110px;
8 | padding-bottom: 20px;
9 | }
10 |
11 | .venueList {
12 | overflow-y: scroll;
13 | height: 80vh;
14 | }
15 |
16 |
17 | #venue-search-bar{
18 | display: inline ;
19 | width: 30%;
20 | position: relative;
21 | float: right;
22 | }
23 |
24 | .venueName {
25 | padding-top: 8px;
26 |
27 | }
28 |
29 | .venue-title {
30 | display: inline-block;
31 | /*font-weight: 800;*/
32 | font-size: 38px;
33 | }
34 |
35 | .search-bar {
36 | margin: 20px;
37 | text-align: center;
38 | }
39 | .search-bar input {
40 | width: 75%;
41 | }
42 |
43 | .songkickEndorse{
44 | float: right;
45 | color: grey;
46 | font-weight: bold;
47 | }
48 |
49 | .searchError{
50 | float: right;
51 | color: #f75a50;
52 | font-weight: bold;
53 | font-size: 20px;
54 | }
--------------------------------------------------------------------------------
/client/public/ShowList.css:
--------------------------------------------------------------------------------
1 | .show-error {
2 | margin: 10px;
3 | font-size: 1.4em;
4 | }
5 | .glyphicon-cd {
6 | margin-bottom: 40px;
7 | font-size: 15em;
8 | -webkit-animation-name: rotate;
9 | -webkit-animation-duration: 1s;
10 | -webkit-animation-iteration-count: infinite;
11 | -webkit-animation-timing-function: linear;
12 | }
13 | @-webkit-keyframes rotate {
14 | from {
15 | -webkit-transform: rotate(0deg);
16 | }
17 | to {
18 | -webkit-transform: rotate(360deg);
19 | }
20 | }
21 | .spinner h1 {
22 | vertical-align: top;
23 | }
24 | .spinner {
25 | text-align: center;
26 | margin-top: 80px;
27 | }
28 | @media screen and (max-width: 767px) {
29 | .show-error {
30 | font-size: 1em;
31 | }
32 | .spinner {
33 | margin-top: 30px;
34 | }
35 | .spinner h1 {
36 | display: none;
37 | }
38 | ::-webkit-scrollbar {
39 | width: 0px;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/client/components/DropdownArtistTitle.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router';
2 | import React, { Component } from 'react';
3 |
4 | export default class DropdownArtistsTitle extends Component {
5 |
6 | render() {
7 | return this.props.venue ? this._renderVenue() : null;
8 | }
9 |
10 | _doorsOpen() {
11 | return !this.props.doorsOpen.includes('Invalid date') ? `Doors open ${this.props.doorsOpen}` : null;
12 | }
13 |
14 | _renderVenue() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 | {this.props.venue.name}
23 |
24 |
25 |
26 | {this._doorsOpen()}
27 |
28 |
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/server/models/m_google.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const querystring = require('querystring');
3 | const {GOOGLE_PLACES_API_KEY, GOOGLE_PLACES_OUTPUT_FORMAT} = require('./api_keys')
4 | const GooglePlaces = require('googleplaces')
5 | const Places = new GooglePlaces(GOOGLE_PLACES_API_KEY, GOOGLE_PLACES_OUTPUT_FORMAT)
6 |
7 | exports.placeIdAPI = (name, lat, long) => {
8 | // console.log('m_google runnning');
9 | return axios(`https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${lat},${long}&radius=500&type=establishment&name=${name}&key=${GOOGLE_PLACES_API_KEY}`)
10 | .then(data => data.data.results)
11 | .catch(err => console.error('error', err))
12 | }
13 |
14 | exports.photoAPI = (photoreference) => {
15 | const parameters = {
16 | photoreference: photoreference,
17 | sensor: false
18 | };
19 |
20 | return new Promise( function(resolve, reject) {
21 | Places.imageFetch(parameters, (error, response) => {
22 | if (error) throw error;
23 | resolve(response)
24 | })
25 | })
26 | }
27 |
--------------------------------------------------------------------------------
/client/components/VenueItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router';
3 |
4 | export default class VenueItem extends Component {
5 |
6 | render() {
7 | const venue = this.props.venue;
8 | const image = venue.photo ? venue.photo : '/assets/venue-image.jpg';
9 | return (
10 |
11 |
12 |
18 |
19 |
20 |
21 | {venue.name}
22 |
23 |
24 |
25 |
26 |
{venue.city + ', ' + venue.state}
27 |
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/server/models/m_artist.js:
--------------------------------------------------------------------------------
1 | const Songkick = require("./m_songkick")
2 | const Spotify = require("./m_spotifyApi")
3 | const LastFM = require("./m_lastFM")
4 | const db = require("../db")
5 | const ArtistModel = require("../ARTISTS_Schema")
6 | const mongoose = require('mongoose');
7 |
8 | exports.artistInfo = (name) => {
9 |
10 | return Spotify.searchArtists(name)
11 | .then(data => {
12 | const Artist = new ArtistModel();
13 |
14 | Artist.spotifyURL = data[0].external_urls.spotify
15 | Artist.id = data[0].id
16 | Artist.songKickName = name
17 | Artist.spotifyName = data[0].name
18 | Artist.artistImages = data[0].images
19 | Artist.img = data[0].images.length ? data[0].images[1].url : "http://assets.audiomack.com/default-artist-image.jpg"
20 | Artist.popularity = data[0].popularity
21 | Artist.followers = data[0].followers.total
22 | Artist.songKickID = 1337
23 |
24 | Artist.save(function(err, fluffy) {
25 | if (err) return console.log(err);
26 | });
27 |
28 | return Artist
29 | })
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var browserify = require('browserify-middleware');
3 | var path = require('path');
4 | var routes = require('./routes');
5 | var bodyParser = require('body-parser');
6 | var querystring = require('querystring');
7 | var cookieParser = require('cookie-parser');
8 | var mongoose = require('mongoose');
9 |
10 | var app = express();
11 | exports.app = app;
12 |
13 | var port = process.env.PORT || 4000;
14 |
15 | var assetFolder = path.join(__dirname, '..', 'client','public');
16 |
17 | // Serve Static Assets
18 | app.use(express.static(assetFolder))
19 | .use(bodyParser.json())
20 | .use(cookieParser())
21 | .use('/', routes);
22 |
23 | // Serve JS Assets
24 | app.get('/app-bundle.js',
25 | browserify('./client/index.js', {
26 | transform: [ [ require('babelify'), { presets: ['es2015', 'react'] } ] ]
27 | })
28 | );
29 |
30 |
31 |
32 | // Wild card route for client side routing.
33 | // do we need this?
34 | app.get('/*', function(req, res){
35 | res.sendFile( assetFolder + '/index.html' );
36 | })
37 |
38 | // Start server
39 | app.listen(port);
40 | console.log('Listening on localhost:' + port);
41 |
--------------------------------------------------------------------------------
/client/public/NavLogin.css:
--------------------------------------------------------------------------------
1 | .profInfo {
2 | float: right;
3 | font-size: 1.1em;
4 | vertical-align: center
5 | }
6 | .profPic {
7 | border-radius: 100%;
8 | vertical-align: center;
9 | margin-top: 3px;
10 | }
11 | .NavLogin {
12 | display: inline;
13 | }
14 | button.spotify {
15 | background: #81b33a;
16 | width: 180px;
17 | height: 37px;
18 | border-radius: 10px;
19 | border: 0;
20 | text-transform: uppercase;
21 | font-size: 0.9em;
22 | font-weight: bold;
23 | color: white;
24 | float: right;
25 | margin: 13px 0 13px 13px;
26 | padding-top: 3px;
27 | display: inline-block;
28 | }
29 | .spotify .aLink,
30 | .spotify .aLink:hover {
31 | display: inline-block;
32 | color: white;
33 | margin: 8px;
34 | position: relative;
35 | top: -3px;
36 | }
37 | .fa-spotify:before {
38 | display: inline-block;
39 | margin: 2px;
40 | }
41 | /* On small screens, set height to 'auto' for sidenav and grid */
42 |
43 | @media screen and (max-width: 767px) {
44 | button.spotify {
45 | display: inline-block;
46 | margin: 0 50px 15px 0;
47 | width: 210px;
48 | float: none;
49 | }
50 |
51 | .profInfo {
52 | display: flex;
53 | flex-direction: column;
54 | float: none;
55 | margin: 0 13px 13px 0;
56 | }
57 |
58 | .profPic {
59 | display: none;
60 | }
61 | }
--------------------------------------------------------------------------------
/server/db.js:
--------------------------------------------------------------------------------
1 | // https://devcenter.heroku.com/articles/nodejs-mongoose
2 | // http://scottksmith.com/blog/2014/05/05/beer-locker-building-a-restful-api-with-node-crud/
3 | // http://www.phil-hudson.com/data-driven-nodejs-tutorials-part-3-integrating-models-mongoose-and-mongodb/
4 | // http://javabeat.net/mongoose-nodejs-mongodb/
5 | // Mongoose/MongoDB Tutorials https://www.youtube.com/playlist?list=PLVBXNyNyLNq0jyDcfjOc9psQYK_leG52r
6 | var mongoose = require('mongoose');
7 |
8 | /*
9 | * Mongoose by default sets the auto_reconnect option to true.
10 | * We recommend setting socket options at both the server and replica set level.
11 | * We recommend a 30 second connection timeout because it allows for
12 | * plenty of time in most operating environments.
13 | */
14 | var options = {
15 | server: {
16 | socketOptions: {
17 | keepAlive: 300000,
18 | connectTimeoutMS: 30000
19 | }
20 | },
21 | replset: {
22 | socketOptions: {
23 | keepAlive: 300000,
24 | connectTimeoutMS: 30000
25 | }
26 | }
27 | };
28 | var mongodbUri = 'mongodb://MelodyMap:makersquare@ds147995.mlab.com:47995/melodymap'
29 |
30 | mongoose.connect(mongodbUri, options);
31 | var db = mongoose.connection;
32 |
33 | db.on('error', console.error.bind(console, 'connection error:'));
34 |
35 | db.once('open', function() {})
36 |
--------------------------------------------------------------------------------
/client/components/ArtistList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ArtistItem from '../components/ArtistItem';
3 |
4 | export default class ArtistList extends Component {
5 |
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | songPlayed: false,
10 | songButton: null,
11 | };
12 | }
13 |
14 | _songPlayToggle(songPlayed, songButton) {
15 | this.setState({
16 | songPlayed,
17 | songButton,
18 | });
19 | }
20 |
21 | _createArtists() {
22 | const artists = this.props.artists;
23 | const sorted = this._sortArtistsByPopularity(artists);
24 | return sorted.map(artist =>
25 |
33 | );
34 | }
35 |
36 | _sortArtistsByPopularity(artists) {
37 | const sorted = [];
38 | for (const artist in artists) sorted.push(artists[artist]);
39 | return sorted.sort((a, b) => b.popularity - a.popularity);
40 | }
41 |
42 | render() {
43 | const Artists = this._createArtists();
44 | return (
45 |
46 | {Artists}
47 |
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/client/public/Artists.css:
--------------------------------------------------------------------------------
1 | #artist-search-bar {
2 | /*margin-bottom: 20px;*/
3 | display: inline ;
4 | width: 30%;
5 | position: relative;
6 | float: right;
7 | }
8 |
9 | .artist-item-name {
10 | padding-top: 8px;
11 |
12 | }
13 |
14 | .searchError{
15 | float: right;
16 | color: #f75a50;
17 | font-weight: bold;
18 | font-size: 20px;
19 | }
20 |
21 | .songkickEndorse{
22 | float: right;
23 | color: grey;
24 | font-weight: bold;
25 | }
26 |
27 | .artist-title {
28 | display: inline-block;
29 | /*font-weight: 800;*/
30 | font-size: 38px;
31 | }
32 |
33 | .page-header > h1 {
34 | display: inline;
35 | font-size: 38px;
36 | }
37 |
38 | .page-header.artists-header {
39 | margin-top: 110px;
40 | padding-bottom: 20px;
41 | }
42 |
43 | .ArtistList {
44 | overflow-y: scroll;
45 | height: 80vh;
46 | }
47 |
48 | .search-bar {
49 | margin: 20px;
50 | text-align: center;
51 | }
52 |
53 | .search-bar input {
54 | width: 75%;
55 | }
56 |
57 | .video-detail img {
58 | max-width: 64px;
59 | }
60 |
61 | .video-detail .details {
62 | margin-top: 10px;
63 | padding: 10px;
64 | border: 1px solid #ddd;
65 | border-radius: 4px;
66 | }
67 |
68 | .list-group-item {
69 | cursor: pointer;
70 | }
71 |
72 | .list-group-item:hover {
73 | background-color: #eee;
74 | }
75 |
76 | @media screen and (max-width: 767px) {
77 | #artist-search-bar {
78 | display: inline ;
79 | width: 50%;
80 | position: relative;
81 | float: right;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/client/components/AudioPlayer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Speaker from '../containers/Speaker';
3 | import { topTrack } from '../models/helpers';
4 |
5 | export default class AudioPlayer extends Component {
6 | render() {
7 | return (
8 |
9 |
10 | {this._checkTracks(this.props.artist.topTracks)}
11 |
12 |
13 | );
14 | }
15 |
16 | _checkTracks(tracks) {
17 | return tracks ? this._tracks(tracks) : null;
18 | }
19 |
20 | _tracks(tracks) {
21 | return tracks.map(track => {
22 | return (
23 |
29 | );
30 | });
31 | }
32 |
33 | }
34 |
35 | class Track extends Component {
36 |
37 | _search(trackName) {
38 | this.props.youtubeSearch(this.props.artist.name + ' ' + trackName);
39 | }
40 |
41 | render() {
42 | const track = this.props.track;
43 |
44 | return (
45 |
46 | {track.name.slice(0, 30)}
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/client/reducers/spotify.js:
--------------------------------------------------------------------------------
1 | import { SPOTIFY_TOKENS, SPOTIFY_ME_BEGIN, SPOTIFY_ME_SUCCESS, SPOTIFY_ME_FAILURE } from '../actions/actions';
2 |
3 | /** The initial state; no tokens and no user info */
4 | const initialState = {
5 | accessToken: null,
6 | refreshToken: null,
7 | user: {
8 | loading: false,
9 | country: null,
10 | display_name: null,
11 | email: null,
12 | external_urls: {},
13 | followers: {},
14 | href: null,
15 | id: null,
16 | images: [],
17 | product: null,
18 | type: null,
19 | uri: null,
20 | },
21 | };
22 |
23 | /**
24 | * Our reducer
25 | */
26 | export default function reduce(state = initialState, action) {
27 | switch (action.type) {
28 | // when we get the tokens... set the tokens!
29 | case SPOTIFY_TOKENS:
30 | const { accessToken, refreshToken } = action;
31 | return Object.assign({}, state, { accessToken, refreshToken });
32 |
33 | // set our loading property when the loading begins
34 | case SPOTIFY_ME_BEGIN:
35 | return Object.assign({}, state, {
36 | user: Object.assign({}, state.user, { loading: true }),
37 | });
38 |
39 | // when we get the data merge it in
40 | case SPOTIFY_ME_SUCCESS:
41 | return Object.assign({}, state, {
42 | user: Object.assign({}, state.user, action.data, { loading: false }),
43 | });
44 |
45 | // currently no failure state :(
46 | case SPOTIFY_ME_FAILURE:
47 | return state;
48 |
49 | default:
50 | return state;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/client/public/DrawMap.css:
--------------------------------------------------------------------------------
1 | .gm-style-iw {
2 | position: relative;
3 | padding: 10px;
4 | background-color: #fff;
5 | }
6 | #iw-container .iw-title {
7 | font-family: 'Open Sans Condensed', sans-serif;
8 | font-size: 22px;
9 | font-weight: 400;
10 | padding: 5px;
11 | background-color: #81b33a;
12 | color: white;
13 | margin: 0;
14 | border-radius: 10px 10px 10px 10px;
15 | }
16 | #iw-container .iw-content {
17 | line-height: 18px;
18 | font-weight: 400;
19 | padding: 0 5px;
20 | max-height: 140px;
21 | overflow-y: auto;
22 | overflow-x: hidden;
23 | }
24 | .iw-content img {
25 | float: right;
26 | margin: 0;
27 | padding: 0;
28 | }
29 | .iw-subTitle {
30 | font-size: 16px;
31 | font-weight: 700;
32 | padding: 10px 0;
33 | }
34 | .iw-address {
35 | font-size: 11px;
36 | }
37 | .iw-content a {
38 | font-size: 11px;
39 | }
40 | .iw-bottom-gradient {
41 | position: absolute;
42 | width: 326px;
43 | height: 25px;
44 | bottom: 10px;
45 | right: 18px;
46 | background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
47 | background: -webkit-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
48 | background: -moz-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
49 | background: -ms-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
50 | }
51 | @media screen and (max-width: 767px) {
52 | #iw-container .iw-content {
53 | line-height: 12px;
54 | }#iw-container .iw-title {
55 | font-size: 18px;
56 | }
57 | .iw-subTitle {
58 | font-size: 14px;
59 | }
60 | .iw-address {
61 | font-size: 9px;
62 | }
63 | .iw-content a {
64 | font-size: 9px;
65 | }
66 | }
--------------------------------------------------------------------------------
/client/components/UpcomingShowsDetail.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import moment from 'moment';
3 |
4 |
5 | export default class UpcomingShowsDetail extends Component {
6 | render() {
7 | return (
8 |
9 | {this._checkShows(this.props.shows)}
10 |
11 |
12 | );
13 | }
14 |
15 | _checkShows(shows) {
16 | return shows ? this._shows(shows) : null;
17 | }
18 |
19 | _shows(shows) {
20 | return shows.map(show => );
21 | }
22 |
23 | }
24 |
25 | class Show extends Component {
26 |
27 | render() {
28 | const show = this.props.show;
29 | return (
30 |
31 |
32 | { moment(show.start.date).format('ll').split(',')[0] }
33 |
34 |
35 |
36 |
37 | {show.venue.displayName}
38 |
39 |
{ show.location.city }
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/client/components/ArtistItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router';
3 | import Speaker from '../containers/Speaker';
4 | import { getAlbumArt, getRandomAlbumArt, topTrack } from '../models/helpers';
5 |
6 | export default class ArtistItem extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | albumArt: null,
11 | };
12 | }
13 |
14 | componentDidMount() {
15 | this._randomAlbumArt();
16 | }
17 |
18 | render() {
19 | const artist = this.props.artist;
20 | const albumArt = this.state.albumArt ? this.state.albumArt : getAlbumArt(artist);
21 | return (
22 |
23 |
24 |
30 |
37 |
38 |
39 | {artist.name}
40 |
41 |
42 |
43 |
44 | {artist.onTour == '1' || artist.onTourUntil ?
ON TOUR
: null}
45 |
46 | );
47 | }
48 |
49 | _randomAlbumArt() {
50 | this.setState({
51 | albumArt: getRandomAlbumArt(this.props.artist),
52 | });
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends: airbnb
2 | plugins:
3 | - react
4 | ecmaFeatures:
5 | jsx: true
6 | env:
7 | es6: true
8 | browser: true
9 | node: true
10 | jest: true
11 | settings:
12 | import/resolver: webpack
13 | globals:
14 | google: false
15 | twttr: true
16 | Auth0: false
17 | rules:
18 | consistent-return: off
19 | comma-dangle:
20 | - error
21 | - never
22 | no-trailing-spaces:
23 | - error
24 | - skipBlankLines: true
25 | no-confusing-arrow:
26 | - error
27 | - allowParens: true
28 | block-spacing:
29 | - error
30 | - never
31 | arrow-spacing:
32 | - error
33 | - before: true
34 | after: true
35 | object-curly-spacing: off
36 | space-in-parens:
37 | - error
38 | - never
39 | space-before-function-paren:
40 | - error
41 | - never
42 | space-before-blocks:
43 | - error
44 | - always
45 | keyword-spacing:
46 | - error
47 | - before: true
48 | after: true
49 | jsx-quotes: off
50 | quotes:
51 | - error
52 | - single
53 | - avoidEscape: true
54 | allowTemplateLiterals: true
55 | global-require: off
56 | react/jsx-closing-bracket-location: off
57 | import/no-unresolved: off
58 | react/react-in-jsx-scope: off
59 | no-irregular-whitespace: off
60 | no-unused-expressions:
61 | - error
62 | - allowShortCircuit: true
63 | allowTernary: true
64 | no-underscore-dangle:
65 | - error
66 | - allowAfterThis: true
67 | allow:
68 | - _handleNestedListToggle
69 | - _disableDisplay
70 | - _DEPRECATION_NOTICE
71 | - __IS_SMOOTH_SCROLLING
72 | - __scroll__direction
73 | - __CA_DELAYED_MODAL
74 | - _wq
75 | - __dataID__
76 | - _fbq
77 | - _hsq
78 | no-param-reassign:
79 | - error
80 | - props: false
81 |
--------------------------------------------------------------------------------
/client/components/DropdownArtists.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import DropdownArtistTitle from './DropdownArtistTitle';
3 | import DropdownArtist from './DropdownArtist';
4 | import { isReduxLoaded } from '../models/helpers';
5 |
6 | export default class DropdownArtists extends Component {
7 |
8 | _createBand() {
9 | const bands = this.props.bands;
10 | if (isReduxLoaded(this.props.artists) && bands) {
11 | return this._mappedArtists(bands);
12 | }
13 | }
14 |
15 | _mappedArtists(bands) {
16 | const artist = bands.filter(name => this.props.artists[name]);
17 | return artist.map((name, index) => {
18 | return (
19 |
23 | );
24 | });
25 | }
26 |
27 | _venueLoading() {
28 | return (
29 | Loading
36 |
37 | );
38 | }
39 |
40 | _venue() {
41 | return (
42 | BUY TICKETS
49 |
50 | );
51 | }
52 |
53 | render() {
54 | const bands = this._createBand();
55 | return (
56 |
57 |
63 | { this.props.artists ? bands : null }
64 | {this.props.venue ? this._venue() : this._venueLoading()}
65 |
66 | );
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/client/public/ArtistDetail.css:
--------------------------------------------------------------------------------
1 | .jumbotron {
2 | min-height:380px;
3 | padding-top: 24px;
4 | margin-top: 67px;
5 | }
6 |
7 | .jumbotron h1 {
8 | display: inline-block;
9 | margin-top: 0;
10 | }
11 |
12 | .artist-video {
13 | padding: 0;
14 | }
15 |
16 | .artist-audio {
17 | padding: 0;
18 | margin: 10px 0;
19 | }
20 |
21 | .artist-upcoming {
22 | text-align: center;
23 | margin: 10px 0;
24 | padding-left: 10px;
25 | padding-right: 10px;
26 | }
27 |
28 | .container-similar {
29 | display: flex;
30 | flex-direction: column;
31 | }
32 |
33 | .artistDetail-bio {
34 | overflow-y: scroll;
35 | height: 240px;
36 | }
37 |
38 | .artistDetail-ontour {
39 | display: inline-block;
40 | font-family: 'Oswald', sans-serif;
41 | padding-left: 20px;
42 | font-size: 22px;
43 | }
44 |
45 | .container-similar div {
46 | display: flex;
47 | flex-wrap: wrap;
48 | justify-content: space-between;
49 | }
50 |
51 | .video-detail .details {
52 | /*hides the video-detail*/
53 | margin-top: -150px;
54 | }
55 |
56 | .video-detail {
57 | width: 100%;
58 | }
59 |
60 | .similar-artist {
61 | display: flex;
62 | flex-direction: column;
63 | margin: 10px;
64 | align-items: center;
65 | }
66 |
67 | .similar-artist img {
68 | height: 200px;
69 | width: 200px;
70 | -webkit-box-shadow: 7px -7px 14px 0px rgba(0,0,0,0.84);
71 | -moz-box-shadow: 7px -7px 14px 0px rgba(0,0,0,0.84);
72 | box-shadow: 7px -7px 14px 0px rgba(0,0,0,0.84);
73 |
74 | }
75 |
76 | .similar-artist a {
77 | color: #93c54b;
78 | font-size: 20px;
79 | font-weight: 800;
80 | }
81 |
82 | .genre-item{
83 | text-transform: capitalize;
84 | }
85 |
86 | .text-center {
87 | display: inline-block;
88 | cursor: pointer;
89 | }
90 |
91 | .scrollable-menu {
92 | height: auto;
93 | max-height: 400px;
94 | overflow-x: hidden;
95 | margin-bottom: 50px;
96 | }
97 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { Router, Route, IndexRoute, Link, IndexLink, browserHistory } from 'react-router';
5 | import ReduxPromise from 'redux-promise';
6 | import NavBar from './containers/NavBar';
7 | import App from './components/App';
8 | import Artists from './containers/Artists';
9 | import Venues from './containers/Venues';
10 | import ArtistDetail from './containers/ArtistDetail';
11 | import VenueDetail from './containers/VenueDetail';
12 | import NavLogin from './components/NavLogin';
13 | import Error from './components/Error';
14 | import Home from './containers/Home';
15 |
16 |
17 | import { createStore, applyMiddleware } from 'redux';
18 |
19 | import reducers from './reducers';
20 | // import configureStore from './store/configureStore';
21 |
22 | const createStoreWithMiddleware = applyMiddleware(ReduxPromise)(createStore);
23 | // const store = configureStore();
24 |
25 | // Render to DOM
26 | ReactDOM.render(
27 | (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | ),
38 | document.getElementById('mount')
39 | );
40 | // {/* */}
41 | //
42 | //
43 | // {/* On artist name click, route to corresponding ArtistDetail */}
44 | //
45 |
--------------------------------------------------------------------------------
/client/public/NavBar.css:
--------------------------------------------------------------------------------
1 | .home-navbar {
2 | padding-bottom: 3px !important;
3 | }
4 |
5 | .navbar-collapse {
6 | margin-right: 0;
7 | }
8 |
9 | .nav-container {
10 | display: flex;
11 | flex: 1;
12 | }
13 |
14 | .navbar .nav > li > a {
15 | font-size: 14px;
16 | line-height: 22px;
17 | font-weight: 800;
18 | text-transform: uppercase;
19 | }
20 |
21 | .navbar {
22 | margin-bottom: 0 !important;
23 | border-radius: 0 !important;
24 | }
25 |
26 | .navbar-brand {
27 | font-size: 33px;
28 | font-family: 'Pacifico', cursive;
29 | margin-right: 10px;
30 | }
31 |
32 | .glyphicon-search {
33 | margin-right: 5px;
34 | }
35 |
36 | .songkick-search {
37 | display: flex;
38 | flex: 1;
39 | justify-content: center;
40 | align-items: center;
41 | }
42 |
43 | .songkick-search a {
44 | cursor: pointer;
45 | font-size: 13px;
46 | }
47 |
48 | .songkick-search a:hover {
49 | text-decoration: none;
50 | }
51 |
52 | .songkick-search input {
53 | border-radius: 4px;
54 | margin: 8px;
55 | height: 25px;
56 | width: 90px;
57 | font-size: 12px;
58 | padding-left: 4px;
59 | }
60 |
61 | input.input-city {
62 | width: 120px;
63 | }
64 |
65 | .songkick-search button {
66 | background: #81b33a;
67 | width: 70px;
68 | height: 25px;
69 | border-radius: 4px;
70 | border: 0;
71 | text-transform: uppercase;
72 | font-size: 0.9em;
73 | font-weight: bold;
74 | color: white;
75 | margin: 13px 0px 13px 10px;
76 | padding-top: 3px;
77 | display: inline-block;
78 | vertical-align: center;
79 | }
80 |
81 | @media screen and (max-width: 767px) {
82 | .nav-container {
83 | display: flex;
84 | flex-direction: column;
85 | }
86 |
87 | .songkick-search {
88 | display: flex;
89 | flex-direction: column;
90 | margin-bottom: 20px;
91 | align-items: baseline;
92 | }
93 |
94 | .songkick-search input, .songkick-search button {
95 | margin: 0 50px 20px 0;
96 | width: 170px;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/client/public/GenArtist.css:
--------------------------------------------------------------------------------
1 | .gridding {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: baseline;
5 | border: 1px solid white;
6 | font-weight: bold;
7 | margin-bottom: 15px;
8 | margin-top: 15px;
9 | height: 375px;
10 | width: 375px;
11 | }
12 |
13 | .genImage{
14 | height: 250px;
15 | width: 250px;
16 | object-fit: cover;
17 | -webkit-box-shadow: 7px -2px 25px -1px rgba(0,0,0,1);
18 | -moz-box-shadow: 7px -2px 25px -1px rgba(0,0,0,1);
19 | box-shadow: 7px -2px 25px -1px rgba(0,0,0,1);
20 | }
21 |
22 | /*.genImage #selected .selArtist{
23 | height: 250px;
24 | width: 250px;
25 | margin-bottom: 5px;
26 | object-fit: cover;
27 | -webkit-box-shadow: 7px -2px 25px -1px rgba(0,0,0,1);
28 | -moz-box-shadow: 7px -2px 25px -1px rgba(0,0,0,1);
29 | box-shadow: 7px -2px 25px -1px rgba(0,0,0,1);
30 | }*/
31 |
32 | .gridding .artist-label {
33 | position: relative;
34 | display: flex;
35 | /*height: 48px;*/
36 | width: 300px;
37 | justify-content: center;
38 | /*padding-bottom: 1px;*/
39 | }
40 |
41 | .gridding .artist-label .selArtist {
42 | margin: 0;
43 | font-size: 21px;
44 | text-align: center;
45 | width: 75%;
46 | /*padding-bottom: 2px;*/
47 | }
48 |
49 | /*.gridding .artist-label .fa{
50 | margin: 5px 0;
51 |
52 | position: absolute;
53 |
54 | left: 265px;
55 | cursor: pointer;
56 | z-index: 10;
57 | }*/
58 | .gridding .artist-label .fa {
59 | position: absolute;
60 | bottom: 0;
61 | right: 0;
62 | cursor: pointer;
63 | z-index: 10;
64 | }
65 |
66 |
67 | .tour {
68 | font-size: 16px;
69 | display: inline-block;
70 | text-align: center;
71 | }
72 |
73 | @media screen and (max-width: 767px) {
74 | .gridding .artist-label {
75 | justify-content: flex-start;
76 | }
77 |
78 | .ArtistList .gridding {
79 | width: 265px;
80 | align-items: baseline;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/client/components/UpcomingShows.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router';
3 | import moment from 'moment';
4 |
5 | export default class UpcomingShows extends Component {
6 |
7 | _createArtistList() {
8 | const show = this.props.show;
9 | const artists = this.props.show.performance;
10 |
11 | const artistArr = artists.map(function (artist) {
12 | return {artist.displayName};
13 | });
14 |
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 | {artistArr[0] } { artistArr[1] ? with {artistArr[1]} : null}
23 | {artistArr[2] ? , {artistArr[2]} : null}
24 |
25 |
26 |
27 |
28 |
{show.venue.displayName}, {show.location.city}
29 |
30 |
31 |
32 |
{show.start.datetime ?
33 | {moment(show.start.datetime).format('LLLL')}
:
34 | {moment(show.start.date).format('LL')}
}
35 |
36 |
37 |
38 |
39 |
40 | {show.status !== 'ok' ?
41 |
{show.status} :
42 |
BUY TICKETS }
43 |
44 |
45 |
46 | );
47 | }
48 |
49 | render() {
50 | let artistList = this._createArtistList();
51 |
52 | return (
53 |
54 | {artistList}
55 |
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/client/public/UpcomingShowsDetail.css:
--------------------------------------------------------------------------------
1 | .UpcomingShowsDetail-container {
2 | max-height: 393px;
3 | overflow-y: scroll;
4 | }
5 |
6 | .UpcomingShowsDetail-bottom-gradient {
7 | position: absolute;
8 | width: 100%;
9 | height: 35px;
10 | bottom: 0;
11 | background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
12 | background: -webkit-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
13 | background: -moz-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
14 | background: -ms-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
15 | }
16 |
17 | .UpcomingShowsDetail-list-item {
18 | display: flex;
19 | flex-direction: row;
20 | border: 1px solid #93c54b;
21 | padding: 1px;
22 | cursor: default;
23 | }
24 |
25 | .UpcomingShowsDetail-date {
26 | display: inline-flex;
27 | position: relative;
28 | margin: 0 2px;
29 | width: 20px;
30 | }
31 |
32 | .fa-calendar-o:before {
33 | position: absolute;
34 | left: 2px;
35 | top: 16px;
36 | font-size: 21px;
37 | }
38 |
39 | .UpcomingShowsDetail-name-location {
40 | display: inline-flex;
41 | flex-direction: column;
42 | flex-grow: 1;
43 | padding-top: 6px;
44 | }
45 |
46 | .UpcomingShowsDetail-Name {
47 | font-weight: 400;
48 | font-size: 12px;
49 | font-family: 'Oswald', sans-serif;
50 | color: #98978b;
51 | }
52 |
53 | .UpcomingShowsDetail-location {
54 | font-weight: 400;
55 | font-size: 10px;
56 | font-family: 'Oswald', sans-serif;
57 | }
58 |
59 | .UpcomingShowsDetail-buy {
60 | display: inline-flex;
61 | }
62 |
63 | .UpcomingShowsDetail-buy button {
64 | background-color: #93c54b;
65 | border-top-left-radius: 0;
66 | border-bottom-left-radius: 0;
67 | padding: 5px;
68 | }
69 |
70 | .fa-usd:before {
71 | color: black;
72 | font-size: 21px
73 | }
74 |
75 | .UpcomingShowsDetail-buy button:hover, .UpcomingShowsDetail-buy button:focus, .UpcomingShowsDetail-buy button:active {
76 | background-color: #79a736;
77 | }
--------------------------------------------------------------------------------
/client/public/GenVenue.css:
--------------------------------------------------------------------------------
1 | .mapIcon {
2 | margin: 0;
3 | margin-top: -10px;
4 | margin-right: 5px;
5 | padding: 0;
6 | display: inline-block;
7 | float: left;
8 | margin-left: 10px;
9 | }
10 |
11 | .VenueItem{
12 | height: 350px;
13 | }
14 | .venueAddressLink {
15 | color: #3e3f3a !important;
16 | }
17 | .genvenue-address{
18 | margin-bottom: 25px;
19 | }
20 |
21 | #venueMarker {
22 | display: inline-block;
23 | position: absolute;
24 | }
25 | .venueName {
26 | display: inline-block;
27 | font-size: 21px;
28 | font-weight: 800;
29 | width: 250px;
30 | }
31 |
32 | @media screen and (max-width: 767px) {
33 | .VenueItem{
34 | height: 350px;
35 | margin-left: 10%;
36 | }
37 | }
38 |
39 | .gridding {
40 | display: flex;
41 | flex-direction: column;
42 | align-items: center;
43 | border: 1px solid white;
44 | font-weight: bold;
45 | margin-bottom: 15px;
46 | margin-top: 15px;
47 | height: 375px;
48 | width: 375px;
49 | }
50 |
51 | .genImage{
52 | height: 250px;
53 | width: 250px;
54 | object-fit: cover;
55 | -webkit-box-shadow: 7px -2px 25px -1px rgba(0,0,0,1);
56 | -moz-box-shadow: 7px -2px 25px -1px rgba(0,0,0,1);
57 | box-shadow: 7px -2px 25px -1px rgba(0,0,0,1);
58 | }
59 |
60 | .gridding .venue-label {
61 | position: relative;
62 | display: flex;
63 | /*height: 48px;*/
64 | width: 300px;
65 | justify-content: center;
66 | /*padding-bottom: 1px;*/
67 | }
68 |
69 | .gridding .venue-label .venueLink {
70 | margin: 0;
71 | font-size: 24px;
72 | text-align: center;
73 | }
74 |
75 |
76 | .gridding .venue-label .fa {
77 | position: absolute;
78 | bottom: 0;
79 | right: 0;
80 | cursor: pointer;
81 | z-index: 10;
82 | }
83 | .venueStreetAddress {
84 | font-size: 16px;
85 | text-align: center;
86 | }
87 |
88 | @media screen and (max-width: 767px) {
89 | .gridding .venue-label {
90 | justify-content: flex-start;
91 | }
92 |
93 | .venueList .gridding {
94 | width: 265px;
95 | align-items: baseline;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/client/containers/Speaker.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import { setSpeaker } from '../actions/actions';
5 |
6 | class Speaker extends Component {
7 |
8 | render() {
9 | if (this.props.track) {
10 | return (
11 |
18 |
19 |
20 | );
21 | } else {
22 | return null;
23 | }
24 | }
25 |
26 | _songPlayToggle(songPlayed, songButton) {
27 | this.props.setSpeaker({
28 | songPlayed,
29 | songButton,
30 | });
31 | }
32 |
33 | _toggleSound(event) {
34 | const songPlayed = this.props.speaker.songPlayed;
35 | const playButton = event.target;
36 | const parent = playButton.parentElement;
37 | const audioElem = parent.getElementsByTagName('audio')[0];
38 | if (!songPlayed) {
39 | this._songPlayToggle(audioElem, playButton);
40 | playButton.className = `fa fa-pause fa-${this.props.size}x`;
41 | audioElem.play();
42 | } else if (songPlayed === audioElem) {
43 | audioElem.pause();
44 | playButton.className = `fa fa-volume-up fa-${this.props.size}x`;
45 | this._songPlayToggle(false, null);
46 | } else if (songPlayed !== audioElem) {
47 | songPlayed.pause();
48 | this.props.speaker.songButton.className = `fa fa-volume-up fa-${this.props.size}x`;
49 | this._songPlayToggle(audioElem, playButton);
50 | playButton.className = `fa fa-pause fa-${this.props.size}x`;
51 | audioElem.play();
52 | }
53 | }
54 | }
55 |
56 | const mapStateToProps = (state) => { return { speaker: state.speaker }; };
57 | const mapDispatchToProps = (dispatch) => bindActionCreators({ setSpeaker }, dispatch);
58 | export default connect(mapStateToProps, mapDispatchToProps)(Speaker);
59 |
--------------------------------------------------------------------------------
/client/public/assets/favicons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Melody-Map",
3 | "version": "1.0.0",
4 | "author": "WaterlooATX",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/WaterlooATX/MelodyMap.git"
8 | },
9 | "description": "Easily locate concerts Designed not only to keep you informed when your favorite bands come to your area, but also to help you discover new artists that you might enjoy.",
10 | "license": "ISC",
11 | "scripts": {
12 | "start": "nodemon server/server.js",
13 | "lint": "node ./node_modules/eslint/bin/eslint.js ./client"
14 | },
15 | "devDependencies": {
16 | "babel-core": "^6.0.20",
17 | "babel-eslint": "^4.1.3",
18 | "babel-loader": "^6.2.4",
19 | "babel-preset-es2015": "^6.0.15",
20 | "babel-preset-react": "^6.0.15",
21 | "babel-preset-stage-0": "^6.0.15",
22 | "eslint": "^3.1.1",
23 | "eslint-config-airbnb": "^9.0.1",
24 | "eslint-plugin-import": "^1.11.1",
25 | "eslint-plugin-jsx-a11y": "^1.5.5",
26 | "eslint-plugin-react": "^5.2.2",
27 | "react-tap-event-plugin": "^1.0.0",
28 | "webpack": "^1.13.1"
29 | },
30 | "dependencies": {
31 | "axios": "^0.13.1",
32 | "babelify": "^7.3.0",
33 | "body-parser": "^1.15.2",
34 | "browserify-middleware": "^7.0.0",
35 | "cookie-parser": "^1.4.3",
36 | "express": "^4.14.0",
37 | "google-map-react": "^0.16.3",
38 | "googleplaces": "^0.6.0",
39 | "last.fm.api": "^0.1.3",
40 | "lodash": "^4.14.1",
41 | "middleware": "^1.0.0",
42 | "moment": "^2.14.1",
43 | "mongoose": "^4.5.8",
44 | "nodemon": "^1.10.0",
45 | "parseUri": "^1.2.3-2",
46 | "parseurl": "^1.3.1",
47 | "querystring": "^0.2.0",
48 | "react": "^15.1.0",
49 | "react-datepicker": "^0.29.0",
50 | "react-dom": "^15.0.1",
51 | "react-google-maps": "^4.11.0",
52 | "react-redux": "^4.4.5",
53 | "react-router": "^2.5.1",
54 | "redux": "^3.5.2",
55 | "redux-logger": "^2.6.1",
56 | "redux-promise": "^0.5.3",
57 | "request": "^2.74.0",
58 | "songkick-api": "0.0.2",
59 | "spotify-web-api-js": "^0.19.3",
60 | "spotify-web-api-node": "^2.3.5",
61 | "url": "^0.11.0",
62 | "youtube-api-search": "0.0.5"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/client/actions/actions.js:
--------------------------------------------------------------------------------
1 | export const FETCH_ARTIST = 'FETCH_ARTIST';
2 | export const FETCH_SHOWS = 'FETCH_SHOWS';
3 | export const LOCATION = 'LOCATION';
4 | export const SELECT_ARTIST = 'SELECT_ARTIST';
5 | export const SELECT_SHOW = 'SELECT_SHOW';
6 | export const SELECT_VENUE = 'SELECT_VENUE';
7 | export const SET_SPEAKER = 'SET_SPEAKER';
8 | export const SPOTIFY_ME_BEGIN = 'SPOTIFY_ME_BEGIN';
9 | export const SPOTIFY_ME_FAILURE = 'SPOTIFY_ME_FAILURE';
10 | export const SPOTIFY_ME_SUCCESS = 'SPOTIFY_ME_SUCCESS';
11 | export const SPOTIFY_TOKENS = 'SPOTIFY_TOKENS';
12 | export const VENUES = 'VENUES';
13 |
14 | import Spotify from 'spotify-web-api-js';
15 | import { fetchShowsAPI } from '../models/api';
16 | const spotifyApi = new Spotify();
17 |
18 | export function setSpeaker(speaker) {
19 | return {
20 | type: SET_SPEAKER,
21 | payload: speaker,
22 | };
23 | }
24 |
25 | export function redux_Artists(artist) {
26 | return {
27 | type: FETCH_ARTIST,
28 | payload: artist,
29 | };
30 | }
31 |
32 | export function setLocation(location) {
33 | return {
34 | type: LOCATION,
35 | payload: location,
36 | };
37 | }
38 |
39 | export function selectArtist(artist) {
40 | return {
41 | type: SELECT_ARTIST,
42 | payload: artist,
43 | };
44 | }
45 |
46 | export function selectShow(show) {
47 | return {
48 | type: SELECT_SHOW,
49 | payload: show,
50 | };
51 | }
52 |
53 | export function selectVenue(venue) {
54 | return {
55 | type: SELECT_VENUE,
56 | payload: venue,
57 | };
58 | }
59 |
60 | export function fetchShows(data) {
61 | return {
62 | type: FETCH_SHOWS,
63 | payload: fetchShowsAPI(data.long, data.lat, data.startDate, data.endDate),
64 | };
65 | }
66 |
67 | /** set the app's access and refresh tokens */
68 | export function setTokens(accessToken, refreshToken) {
69 | if (accessToken) {
70 | spotifyApi.setAccessToken(accessToken);
71 | }
72 | return {
73 | type: SPOTIFY_TOKENS,
74 | accessToken,
75 | refreshToken,
76 | };
77 | }
78 |
79 | export function getMyInfo() {
80 | return {
81 | type: SPOTIFY_ME_BEGIN,
82 | payload: spotifyApi.getMe().then(data => {
83 | return data;
84 | }),
85 | };
86 | }
87 |
88 |
89 | export function redux_Venues(venue) {
90 | return {
91 | type: VENUES,
92 | payload: venue,
93 | };
94 | }
95 |
--------------------------------------------------------------------------------
/client/components/DropdownArtist.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router';
2 | import React, { Component } from 'react';
3 | import { getAlbumArt, getBio, getRandomAlbumArt, topTrack } from '../models/helpers';
4 | import Speaker from '../containers/Speaker';
5 |
6 | export default class DropdownArtist extends Component {
7 |
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | albumArt: null,
12 | };
13 | }
14 |
15 | componentDidMount() {
16 | this._randomAlbumArt();
17 | }
18 |
19 | _randomAlbumArt() {
20 | this.setState({
21 | albumArt: getRandomAlbumArt(this.props.artist),
22 | });
23 | }
24 |
25 | render() {
26 | const artist = this.props.artist;
27 | const name = artist ? artist.name : null;
28 | const popularity = artist ? artist.popularity : 'none';
29 | const albumArt = this.state.albumArt ? this.state.albumArt : getAlbumArt(artist);
30 |
31 | return (
32 |
33 |
34 |
35 |
42 |
43 |
44 |
45 |
46 |
{name}
47 |
48 |
49 |
50 |
Popularity
51 |
62 |
{getBio(artist)}
63 |
64 |
65 |
66 |
67 |
68 |
69 | );
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Melody Map
2 | Discover live music in your area tonight. With Melody Map, you can easily explore artists, venues, and shows happening in your city and beyond. See when your favorite bands are playing, check out that new venue, or give a listen and discover new artists.
3 |
4 | ## Features
5 | - Listen to Spotify artist preview tracks
6 | - Use the map feature to explore shows by location (populated by default with your GPS location) and get simple visual directions or extensive Google directions at the touch of a button
7 | - Search for artists or venues in their respective pages
8 | - Use the 'advanced search' feature to populate map with shows for a given date range and location
9 | - Explore artists: see bios, pictures, album art, YouTube videos, popularity, upcoming shows, similar artists and sample top ten tracks
10 | - Explore venues: see basic venue information, pictures, street view, map of location, upcoming shows, and click to buy tickets for upcoming shows
11 |
12 | ## Built with:
13 | -React, Redux, Axios, MongoDB + Mongoose, Bootstrap, and Browserify
14 | -APIs: Songkick, Spotify, LastFM, Google Maps and Places and YouTube
15 |
16 | ## Installation
17 |
18 | Run the following commands in the command line to deploy app locally
19 | ```
20 | $ git clone https://github.com/WaterlooATXs/melodyMap
21 | $ cd MelodyMap
22 | $ npm install
23 |
24 | ```
25 |
26 | ## To start the server
27 | > npm start
28 |
29 | Open your browser to ***http://localhost:4000*** to view!
30 |
31 | ## API Reference
32 |
33 | Refer to ***http://www.songkick.com/developer*** for documentation.
34 |
35 | ***https://groups.google.com/forum/#!forum/songkick-api***
36 |
37 | ***https://developer.spotify.com/web-api/***
38 |
39 | A Node.js wrapper for Spotify's Web API.
40 | ***https://github.com/thelinmichael/spotify-web-api-node***
41 |
42 | A small javascript library (for NodeJS) to interface with the SongKick API
43 | ***https://github.com/MrJaeger/songkick-api***
44 |
45 |
46 | ## Contribution and Style Guides
47 | Please check out the [Git Workflow](https://github.com/WaterlooATX/MelodyMap/blob/master/git_workflow.md) in the MelodyMap repo to learn more about how you can contribute!
48 |
49 |
50 | ## Authors
51 | - [Amanda Fillip](https://github.com/afillip)
52 | - [Chris Ritchie](https://github.com/Buisness8)
53 | - [Chris Wiles](https://github.com/ChrisWiles)
54 | - [Ricardo D'Alessandro](https://github.com/rgdalessandro)
55 | - [Shane McQuerter](https://github.com/Shanetou)
56 |
57 | 
58 |
--------------------------------------------------------------------------------
/client/components/DrawNavigation.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { GoogleMapLoader, GoogleMap, DirectionsRenderer } from 'react-google-maps';
3 |
4 |
5 | export default class DrawNavigation extends Component {
6 |
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | origin: {
11 | lat: +this.props.location.lat,
12 | lng: +this.props.location.long,
13 | },
14 | destination: {
15 | lat: +this.props.selectedShow.venue.lat,
16 | lng: +this.props.selectedShow.venue.lng,
17 | },
18 | directions: null,
19 | };
20 | }
21 |
22 | componentDidMount() {
23 | const DirectionsService = new google.maps.DirectionsService();
24 |
25 | DirectionsService.route({
26 | origin: this.state.origin,
27 | destination: this.state.destination,
28 | travelMode: google.maps.TravelMode.DRIVING,
29 | }, (result, status) => {
30 | if (status === google.maps.DirectionsStatus.OK) {
31 | this.setState({
32 | directions: result,
33 | });
34 | } else {
35 | console.error(`error fetching directions ${result}`, result);
36 | }
37 | });
38 | }
39 |
40 | render() {
41 | return (
42 |
43 |
44 |
45 | Close Directions
46 |
47 |
}
49 | googleMapElement={
50 |
55 | { this.state.directions ? : null }
56 |
57 | }
58 | />
59 |
60 | );
61 | }
62 |
63 | }
64 |
65 | const styles = [{ 'featureType': 'road', 'elementType': 'geometry', 'stylers': [{ 'lightness': 100 }, { 'visibility': 'simplified' }] }, { 'featureType': 'water', 'elementType': 'geometry', 'stylers': [{ 'visibility': 'on' }, { 'color': '#C6E2FF' }] }, { 'featureType': 'poi', 'elementType': 'geometry.fill', 'stylers': [{ 'color': '#C5E3BF' }] }, { 'featureType': 'road', 'elementType': 'geometry.fill', 'stylers': [{ 'color': '#D1D1B8' }] }];
66 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Melody Map
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/client/public/styles.css:
--------------------------------------------------------------------------------
1 | .container-fluid.text-center {
2 | position: fixed;
3 | margin-top: 67px;
4 | }
5 |
6 | .jumbotron{
7 | background: linear-gradient(to right, #d0cac0, #fffefd);
8 | }
9 |
10 | .detailImage {
11 | float: right;
12 | top: -16px;
13 | right: -706px;
14 | margin-top: 10px;
15 | margin-left: 20px;
16 |
17 | display: inline-block;
18 | max-height: 300px;
19 | max-width: auto;
20 | }
21 | /* Remove the navbar's default margin-bottom and rounded borders */
22 |
23 | /*.navbar {
24 | margin-bottom: 0 !important;
25 | border-radius: 0 !important;
26 | }
27 | .navbar-brand {
28 | font-size: 30px;
29 | font-family: 'Pacifico', cursive;
30 | }*/
31 | /* Set height of the grid so .sidenav can be 100% (adjust as needed) */
32 |
33 | .form-control {
34 | font-size: 17px;
35 | font-weight: 800;
36 | border: 2px solid #dfd7ca;
37 | }
38 | .page-header {
39 | border-bottom: 2px solid #f8f5f0;
40 | }
41 | .row.content {
42 | min-width: 100vw;
43 | height: 94vh;
44 | }
45 | /* Set gray background color and 100% height */
46 |
47 | .sidenav {
48 | padding: 0px !important;
49 | background-color: #f1f1f1;
50 | height: 94vh;
51 | }
52 | .mapContainer {
53 | height: 95vh;
54 | }
55 | .Main {
56 | padding-right: 0px !important;
57 | padding-left: 0px !important;
58 | overflow-y: scroll;
59 | height: 94vh;
60 | }
61 |
62 | .detail-title {
63 | display: block;
64 | }
65 |
66 | .display-title-name {
67 | display: inline-block;
68 | font-family: 'Oswald', sans-serif;
69 | font-size: 50px;
70 | font-weight: 700;
71 | }
72 | /* On small screens, set height to 'auto' for sidenav and grid */
73 |
74 | @media screen and (max-width: 767px) {
75 | .row.content {
76 | height: 90vh;
77 | width: 100%;
78 | display: flex;
79 | flex-direction: column;
80 | }
81 | .sidenav {
82 | display: block;
83 | width: 100%;
84 | order: 1;
85 | height: 45vh;
86 | overflow: hidden;
87 | }
88 | .mapContainer {
89 | height: 50vh;
90 | }
91 | .Main {
92 | display: block;
93 | height: 45vh;
94 | width: 100%;
95 | order: 2;
96 | }
97 | }
98 | /*::-webkit-scrollbar-track
99 | {
100 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
101 | background-color: #F5F5F5;
102 | }*/
103 |
104 | ::-webkit-scrollbar {
105 | width: 0px;
106 | }
107 | /*::-webkit-scrollbar-thumb
108 | {
109 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
110 | background-color: #555;
111 | }
112 | */
113 |
--------------------------------------------------------------------------------
/client/models/helpers.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | export const topTrack = (Artist) => {
3 | if (Artist) {
4 | let topTracks = Artist.topTracks ? Artist.topTracks[0] : null;
5 | return topTracks = topTracks ? Artist.topTracks[0].preview_url : null;
6 | } else {
7 | return null;
8 | }
9 | };
10 | export const getArtistImg = (Artist) => Artist ? Artist.img : '/assets/artist-image.jpg';
11 | export const getAlbumArt = (Artist) => {
12 | let albumArt = Artist ? Artist.albumsImages : null;
13 | albumArt = albumArt ? albumArt[0] : null;
14 | albumArt = albumArt ? albumArt.images[1].url : '/assets/album-image.jpg';
15 | return albumArt || Artist.img;
16 | };
17 | export const getBio = (Artist) => {
18 | const randomBio = 'The music sails alive with the compelling combination of rich layers among mixed styles of rhythm that hit the soul. By melding hook-filled melody within hard and heavy beats, has the ability to compact a vast array of influence and experience into a singular song';
19 |
20 | const checkBio = (fullBio) => {
21 | if (fullBio && fullBio.length) {
22 | return fullBio.split('/').join(' /').split('%').join('% ').split(' {
31 | if (Object.keys(obj).length) {
32 | return true;
33 | } else {
34 | return false;
35 | }
36 | };
37 |
38 | export const getRandomAlbumArt = (Artist) => {
39 | let albumArt = Artist ? Artist.albumsImages : null;
40 |
41 | if (albumArt && Artist) {
42 | const albumsImages = Artist.albumsImages.map(album => {
43 | albumArt = album.images.length ? album.images[1] : null;
44 | return albumArt ? albumArt.url : null;
45 | });
46 |
47 | if (albumsImages) {
48 | const num = albumsImages.length;
49 | return albumsImages[Math.floor(Math.random() * num)];
50 | }
51 | }
52 | };
53 | export const addArtistToRedux = (shows, Artist, Spotify_searchArtistsAPI, redux_Artists) => {
54 | shows.forEach(show => addArtists(show));
55 |
56 | function addArtists(show) {
57 | let artists = [...show.performance];
58 | artists = _.uniq(artists.map(artist => {
59 | return {
60 | name: artist.artist.displayName,
61 | id: artist.artist.id,
62 | };
63 | }));
64 | artists.forEach(artist => addArtist(artist));
65 | }
66 |
67 | function addArtist(artist) {
68 | if (!Artist[artist.name]) getArtistData(artist);
69 | }
70 |
71 | function getArtistData(artist) {
72 | Spotify_searchArtistsAPI(artist).then(obj => {
73 | if (obj.data) {
74 | Artist[artist.name] = obj.data;
75 |
76 | redux_Artists(Artist);
77 | }
78 | }).catch(err => console.log(err));
79 | }
80 | };
81 |
--------------------------------------------------------------------------------
/client/containers/Home.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import { geolocationAPI, ipLocationAPI, googleapis_geolocation } from '../models/api';
5 | import { getMyInfo, setTokens, selectShow, fetchShows, setLocation } from '../actions/actions';
6 | import ShowList from './ShowList';
7 | import DrawMap from '../components/DrawMap';
8 | import DrawNavigation from '../components/DrawNavigation';
9 |
10 |
11 | class Home extends Component {
12 |
13 | constructor(props) {
14 | super(props);
15 | this.state = {
16 | navigating: false,
17 | };
18 | }
19 |
20 | componentWillMount() {
21 | googleapis_geolocation()
22 | .then(l => this._setNewCoords(l.data.location.lat, l.data.location.lng));
23 | }
24 |
25 | _setNewCoords(lat, long) {
26 | this.props.setLocation({ lat, long });
27 | this.props.fetchShows(this.props.location);
28 | }
29 |
30 | _spinner() {
31 | return (
32 |
33 |
34 |
Fetching your location...
35 |
36 | );
37 | }
38 |
39 | _displayShowList(shows) {
40 | if (shows) {
41 | return ;
42 | } else {
43 | return this._spinner();
44 | }
45 | }
46 |
47 | _displayMap(nav) {
48 | if (nav) {
49 | return (
50 |
55 | );
56 | } else {
57 | return (
58 |
66 | );
67 | }
68 | }
69 |
70 | render() {
71 | return (
72 |
73 |
74 |
75 | {this._displayShowList(this.props.shows.length)}
76 |
77 |
78 | {this._displayMap(this.state.navigating)}
79 |
80 |
81 |
82 | );
83 | }
84 |
85 | _onNavigateClick() {
86 | this.setState({
87 | navigating: true,
88 | });
89 | }
90 |
91 | _onCloseNavigate() {
92 | this.setState({
93 | navigating: false,
94 | });
95 | }
96 |
97 | }
98 |
99 | const mapStateToProps = (state) => { return { venues: state.venues, shows: state.shows, selectedShow: state.selectedShow, selectShow: state.selectShow, location: state.location }; };
100 | const mapDispatchToProps = (dispatch) => bindActionCreators({ fetchShows, setLocation, selectShow }, dispatch);
101 | export default connect(mapStateToProps, mapDispatchToProps)(Home);
102 |
--------------------------------------------------------------------------------
/client/containers/ShowList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import { selectShow, redux_Artists } from '../actions/actions';
5 | import { Spotify_searchArtistsAPI } from '../models/api';
6 | import Show from './Show';
7 | import { addArtistToRedux } from '../models/helpers';
8 |
9 | export class ShowList extends Component {
10 |
11 | render() {
12 | const shows = this.props.shows;
13 | if (shows.length && typeof shows === 'string') {
14 | return {shows}
;
15 | } else {
16 | addArtistToRedux(shows, this.props.artists, Spotify_searchArtistsAPI, redux_Artists);
17 | return (
18 |
24 | { this._createShows(shows) }
25 |
26 | );
27 | }
28 | }
29 |
30 | // This callback is sent to as props to grab show id
31 | // on click and then use it to update selectedShow on state
32 | _sendToState(arg) {
33 | const shows = this.props.shows;
34 | const showWithId = shows.filter(show => show.id === arg);
35 | this.props.selectShow(showWithId[0]);
36 | }
37 |
38 | _sortShowsPopulartyDate(shows) {
39 | const dates = {};
40 | const sortedDates = [];
41 | const sortedShows = [];
42 |
43 | // group arrays of shows in obj with key set to date
44 | shows.forEach(show => {
45 | if (!dates[show.start.date]) {
46 | sortedDates.push(show.start.date);
47 | dates[show.start.date] = [show];
48 | } else {
49 | dates[show.start.date].push(show);
50 | }
51 | });
52 | // sort the date strings
53 | sortedDates.sort();
54 | // sort each array of shows by popularty for the selected date
55 | const sortPop = (items) => items.sort((a, b) => b.popularity - a.popularity);
56 | // create new array with shows sorted by date and then subsorted by popularity
57 | sortedDates.forEach(date => sortedShows.push(...sortPop(dates[date])));
58 | return sortedShows;
59 | }
60 |
61 |
62 | _createShows(shows) {
63 | shows = this._sortShowsPopulartyDate(shows).filter(show => {
64 | if(show.performance.length >0){
65 | return show;
66 | }
67 | });
68 | return shows.map(show => {
69 | return ( );
85 | });
86 | }
87 | }
88 |
89 | const mapStateToProps = (state) => { return { shows: state.shows, selectedShow: state.selectedShow, artists: state.artists, location: state.location }; };
90 | const mapDispatchToProps = (dispatch) => bindActionCreators({ redux_Artists, selectShow }, dispatch);
91 | export default connect(mapStateToProps, mapDispatchToProps)(ShowList);
92 |
--------------------------------------------------------------------------------
/client/models/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { GOOGLE_MAP_KEY, GOOGLE_GEOLOCATION_KEY } from '../../server/models/api_keys';
3 |
4 |
5 | export function Google_placeIdAPI(name, lat, long) {
6 | return axios.post('/Google_placeIdAPI', {
7 | name,
8 | lat,
9 | long,
10 | });
11 | }
12 |
13 | export function Google_photoAPI(photoReference) {
14 | return axios('/Google_photoAPI', {
15 | params: {
16 | photoReference,
17 | },
18 | });
19 | }
20 |
21 | export function googleapis_geolocation() {
22 | return axios.post(`https://www.googleapis.com/geolocation/v1/geolocate?key=${GOOGLE_GEOLOCATION_KEY}`);
23 | }
24 |
25 | export function geolocationAPI(success, fail, options) {
26 | return navigator.geolocation.getCurrentPosition(success, fail, options);
27 | }
28 |
29 | export function ipLocationAPI() {
30 | return axios('http://ip-api.com/json');
31 | }
32 |
33 | export function Google_geocoder(city) {
34 | return axios(`https://maps.googleapis.com/maps/api/geocode/json?address=${city}&key=${GOOGLE_MAP_KEY}`);
35 | }
36 |
37 | export function Spotify_searchArtistsAPI(artist) {
38 | return axios.post('/Spotify_searchArtists', {
39 | name: artist.name,
40 | id: artist.id,
41 | });
42 | }
43 |
44 | export function Spotify_getArtistTopTracksAPI(artistID, countryCode) {
45 | return axios.post('/Spotify_getArtistTopTracks', {
46 | id: artistID,
47 | code: countryCode,
48 | });
49 | }
50 |
51 | export function LastFM_getInfoAPI(name) {
52 | return axios.post('/LastFM_getInfo', {
53 | name,
54 | });
55 | }
56 |
57 | export function fetchShowsAPI(long, lat, dateA, dateB) {
58 | return axios.post('/fetchShows', {
59 | long,
60 | lat,
61 | dateA,
62 | dateB,
63 | });
64 | }
65 |
66 | export function fetchArtistsAPI(query) {
67 | return axios.post('/fetchArtists', {
68 | query,
69 | });
70 | }
71 |
72 | export function fetchVenuesAPI(query) {
73 | return axios.post('/fetchVenues', {
74 | query,
75 | });
76 | }
77 |
78 | export function getArtistAlbumsAPI(artistID) {
79 | return axios.post('/getArtistAlbums', {
80 | id: artistID,
81 | });
82 | }
83 |
84 | export function Songkick_getShows(city, dateA, dateB) {
85 | return axios.post('/Songkick_getShows', {
86 | city,
87 | dateA,
88 | dateB,
89 | });
90 | }
91 |
92 | export function Songkick_getVenueAPI(venueID) {
93 | return axios.post('/Songkick_getVenue', {
94 | id: venueID,
95 | });
96 | }
97 |
98 | export function Songkick_getSimilarArtistsAPI(artistID) {
99 | return axios.post('/Songkick_getSimilarArtists', {
100 | id: artistID,
101 | });
102 | }
103 |
104 | export function Songkick_getEventSetlistAPI(eventID) {
105 | return axios.post('/Songkick_getEventSetlist', {
106 | id: eventID,
107 | });
108 | }
109 |
110 | export function Songkick_getMetroAreaCalendarAPI(metroID) {
111 | return axios.post('/Songkick_getMetroAreaCalendar', {
112 | id: metroID,
113 | });
114 | }
115 |
116 | export function Songkick_getVenueCalendarAPI(venueID) {
117 | return axios.post('/Songkick_getVenueCalendar', {
118 | id: venueID,
119 | });
120 | }
121 |
122 | export function Songkick_getArtistCalendarAPI(venueID) {
123 | return axios.post('/Songkick_getArtistCalendar', {
124 | id: venueID,
125 | });
126 | }
127 |
128 | export function Artist_artistInfoAPI(name) {
129 | if (name.length > 2) {
130 | return axios.post('/Artist_artistInfo', {
131 | name,
132 | });
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/client/containers/NavBar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import { Link } from 'react-router';
5 | import { getMyInfo, setTokens } from '../actions/actions';
6 | import NavLogin from '../components/NavLogin';
7 | import UserLogin from '../components/UserLogin';
8 | import SongkickSearch from './SongkickSearch';
9 | import { followArtist } from '../models/spotify';
10 |
11 |
12 | class NavBar extends Component {
13 |
14 | constructor(props) {
15 | super(props);
16 |
17 | this.state = {
18 | loggedIn: false,
19 | spotifyData: {
20 | username: '',
21 | image: '',
22 | },
23 | };
24 | }
25 |
26 | componentDidMount() {
27 | // grab url, send accessToken/refreshToken to actions
28 | const url = document.location.href.split('/');
29 | const self = this;
30 | if (url[5]) {
31 | // Spotify call to follow artist followArtist(url[5],'3TNt4aUIxgfy9aoaft5Jj2')
32 | this.setState({
33 | loggedIn: true,
34 | });
35 | this.props.setTokens(url[5], url[6]);
36 | this.props.getMyInfo().then(data => self._checkData(data.payload));
37 | }
38 | }
39 |
40 | _checkData(data) {
41 | if (data.display_name || data.images[0]) {
42 | this.setState({
43 | spotifyData: {
44 | username: data.display_name,
45 | image: data.images[0].url,
46 | },
47 | });
48 | } else {
49 | this.setState({
50 | spotifyData: {
51 | username: data.id,
52 | image: 'http://assets.audiomack.com/default-artist-image.jpg',
53 | },
54 | });
55 | }
56 | }
57 |
58 | render() {
59 | // have NavLinks comes back in after a specified Timeout to prevent pre-load erros
60 | setTimeout(function () {
61 | $('li a').css('z-index', 10);
62 | }, 4500);
63 |
64 | return (
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | Melody Map
75 |
76 |
77 |
78 | Artists
79 | Venues
80 |
81 |
82 |
83 | {!this.state.loggedIn ? : }
84 |
85 |
86 |
87 |
88 |
89 | );
90 | }
91 |
92 | }
93 |
94 | class NavLink extends Component {
95 | render() {
96 | return ;
97 | }
98 | }
99 |
100 | const mapDispatchToProps = (dispatch) => bindActionCreators({ getMyInfo, setTokens }, dispatch);
101 | export default connect(null, mapDispatchToProps)(NavBar);
102 |
--------------------------------------------------------------------------------
/client/containers/SongkickSearch.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import { Link } from 'react-router';
5 | import DatePicker from 'react-datepicker';
6 | import moment from 'moment';
7 | import { setLocation, fetchShows } from '../actions/actions';
8 | import { Google_geocoder, Songkick_getShows } from '../models/api';
9 |
10 |
11 | class SongkickSearch extends Component {
12 |
13 | constructor(props) {
14 | super(props);
15 |
16 | this.state = {
17 | startDate: moment(),
18 | endDate: moment(),
19 | city: '',
20 | search: false,
21 | };
22 | }
23 |
24 | render() {
25 | return (
26 |
27 | {
28 | this.props.visibleSearch ?
29 | this.state.search ?
30 |
55 | :
61 | :
62 | }
63 |
64 | );
65 | }
66 |
67 | _onSearchClick() {
68 | this.setState({
69 | search: true,
70 | });
71 | }
72 |
73 | _onStartChange(startDate) {
74 | this.setState({
75 | startDate,
76 | });
77 | }
78 |
79 | _onEndChange(endDate) {
80 | this.setState({
81 | endDate,
82 | });
83 | }
84 |
85 | _onCityChange(city) {
86 | this.setState({
87 | city,
88 | });
89 | }
90 |
91 | _onSubmit(event) {
92 | event.preventDefault();
93 | const startDate = this.state.startDate.toISOString().slice(0, 10);
94 | const endDate = this.state.endDate.toISOString().slice(0, 10);
95 | // get coordinate from city name
96 | if (this.state.city) {
97 | Google_geocoder(this.state.city).then(resp => {
98 | console.log('resp from navbar', resp);
99 | const lat = resp.data.results[0].geometry.location.lat;
100 | const long = resp.data.results[0].geometry.location.lng;
101 | this.props.fetchShows({
102 | long,
103 | lat,
104 | startDate,
105 | endDate,
106 | });
107 | this.props.setLocation({
108 | long,
109 | lat,
110 | });
111 | });
112 | } else {
113 | const lat = this.props.location.lat;
114 | const long = this.props.location.long;
115 | this.props.fetchShows({
116 | long,
117 | lat,
118 | startDate,
119 | endDate,
120 | });
121 | }
122 | // code below hides advanced search view after a submit
123 | // this.setState({
124 | // search: false
125 | // });
126 | }
127 |
128 | }
129 |
130 | const mapStateToProps = (state) => { return { location: state.location }; };
131 | const mapDispatchToProps = (dispatch) => bindActionCreators({ setLocation, fetchShows }, dispatch);
132 | export default connect(mapStateToProps, mapDispatchToProps)(SongkickSearch);
133 |
--------------------------------------------------------------------------------
/client/components/DrawMap.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { GoogleMapLoader, GoogleMap, Marker, InfoWindow } from 'react-google-maps';
3 | import { getDistanceFromLatLonInKm } from '../models/getDistanceFromLatLonInKm';
4 |
5 |
6 | export default class DrawMap extends Component {
7 |
8 | constructor(props) {
9 | super(props);
10 |
11 | this._Map = null;
12 | this.venues = props.venues;
13 | }
14 |
15 | _findClosestShow() {
16 | const location = this.props.location;
17 | const shows = this.props.shows;
18 |
19 | if (Array.isArray(shows)) {
20 | const array = shows.map(show => getDistanceFromLatLonInKm(+location.lat, +location.long, +show.venue.lat, +show.venue.lng));
21 | const sorted = array.slice().sort((a, b) => a - b);
22 | return shows[array.indexOf(sorted[0])];
23 | } else return null;
24 | }
25 |
26 |
27 | _getVenueInfo(show) {
28 | for (const key in this.venues) {
29 | if (key == show.venue.id) {
30 | return this.venues[key].address;
31 | }
32 | }
33 | }
34 |
35 |
36 | _setCenter() {
37 | const locA = this.props.selectedShow;
38 | // Loc B commented out because centering map on closest show hurts UX more than it helps
39 | // const locB = this._findClosestShow();
40 | const locC = this.props.location;
41 | let center;
42 |
43 | if (locA) center = {
44 | lat: +locA.venue.lat,
45 | lng: +locA.venue.lng,
46 | };
47 | // Loc B commented out because centering map on closest show hurts UX more than it helps
48 | // else if (locB) center = {lat: +locB.venue.lat, lng: +locB.venue.lng};
49 | else center = {
50 | lat: +locC.lat,
51 | lng: +locC.long,
52 | };
53 | return center;
54 | }
55 |
56 | _createMarkers() {
57 | if (Array.isArray(this.props.shows)) {
58 | return this.props.shows.map((show, index) => {
59 | // for(var key in this.venues){
60 | // // console.log("VENUEID SHOW",show.venue.id)
61 | // // console.log("KEY",key)
62 | // if(key == show.venue.id){
63 | // console.log("KEY",key, "VENUE ID",show.venue.id)
64 | // }
65 | // }
66 |
67 | return (
68 | this._onMarkerClickHandler(marker, show)}
73 | defaultAnimation={2}
74 | >
75 |
76 | { this.props.selectedShow === show ?
77 |
78 |
79 |
{ show.venue.displayName }
80 |
81 |
{show.performance[0].displayName}
82 |
{ this._getVenueInfo(show) }
83 |
(Directions to here)
84 |
85 |
86 | : null }
87 |
88 | );
89 | });
90 | }
91 | }
92 |
93 | _onMarkerClickHandler(marker, show) {
94 | this._Map.panTo(marker.latLng);
95 | this.props.selectShow(show);
96 | $(`#heading${show.id}`)[0].scrollIntoView(true);
97 | }
98 |
99 | render() {
100 | if (this.props.location.lat) {
101 | return (
102 | }
104 | googleMapElement={
105 | (this._Map = map)}
107 | zoomControl="true"
108 | defaultZoom={14}
109 | defaultOptions={{ styles, disableDefaultUI: true }}
110 | center={this._setCenter()}
111 | >
112 | { this.props.shows ? this._createMarkers.call(this) : null }
113 |
114 | }
115 | />
116 | );
117 | } else return null;
118 | }
119 |
120 | }
121 |
122 | let styles = [{ 'featureType': 'road', 'elementType': 'geometry', 'stylers': [{ 'lightness': 100 }, { 'visibility': 'simplified' }] }, { 'featureType': 'water', 'elementType': 'geometry', 'stylers': [{ 'visibility': 'on' }, { 'color': '#C6E2FF' }] }, { 'featureType': 'poi', 'elementType': 'geometry.fill', 'stylers': [{ 'color': '#C5E3BF' }] }, { 'featureType': 'road', 'elementType': 'geometry.fill', 'stylers': [{ 'color': '#D1D1B8' }] }];
123 |
--------------------------------------------------------------------------------
/client/containers/Venues.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import { redux_Venues } from '../actions/actions';
5 | import { fetchVenuesAPI, Songkick_getVenueAPI } from '../models/api';
6 | import GenVenue from '../components/GenVenue';
7 | import _ from 'lodash';
8 | import { isReduxLoaded } from '../models/helpers';
9 |
10 |
11 | class Venues extends Component {
12 |
13 | constructor(props) {
14 | super(props);
15 | this.state = {
16 | searchedVenues: {},
17 | term: '',
18 | notFound: false,
19 | showError: false,
20 | };
21 | }
22 |
23 | _venueSearch(term) {
24 | const mappedVenues = [];
25 | this.setState({
26 | searchedVenues: {},
27 | });
28 | fetchVenuesAPI(term).then(venues => {
29 | if (venues.data.length) {
30 | this.setState({
31 | notFound: false,
32 | showError: false,
33 | });
34 | venues.data.forEach((venue) => {
35 | mappedVenues.push(venue);
36 | });
37 | mappedVenues.forEach(venue => this._isInRedux(venue) ? this._getRedux(venue) : this._songkickSearch(venue));
38 | } else {
39 | this.setState({
40 | notFound: true,
41 | showError: true,
42 | });
43 | }
44 | });
45 | }
46 |
47 | _songkickSearch(venue) {
48 | Songkick_getVenueAPI(venue.id).then(venues => {
49 | if (venues.data.address) {
50 | this._addRedux(venues.data);
51 | }
52 | });
53 | }
54 |
55 | _isInRedux(venue) {
56 | if (this.props.venues && !this.props.venues[venue.id]) {
57 | return false;
58 | } else {
59 | return true;
60 | }
61 | }
62 |
63 | _getRedux(venue) {
64 | const searchedVenues = this.state.searchedVenues;
65 | searchedVenues[venue.id] = this.props.venues[venue.id];
66 | this.setState({
67 | searchedVenues,
68 | });
69 | }
70 |
71 | _addRedux(venue) {
72 | const Venues = this.props.venues;
73 | const searchedVenues = this.state.searchedVenues;
74 | searchedVenues[venue.id] = venue;
75 | Venues[venue.id] = venue;
76 | this.setState({
77 | searchedVenues,
78 | });
79 | redux_Venues(Venues);
80 | }
81 |
82 | _handleSubmit(event) {
83 | event.preventDefault();
84 | this._venueSearch(this.state.term);
85 | }
86 |
87 | _onInputChange(term) {
88 | this.setState({
89 | term,
90 | });
91 | }
92 |
93 |
94 | _errorFade() {
95 | const This = this;
96 | setTimeout(function () {
97 | This.setState({
98 | notFound: false,
99 | showError: false,
100 | });
101 | $('#venue-search-bar').find('input').val('');
102 | }, 3000);
103 | }
104 |
105 | _venueList() {
106 | return isReduxLoaded(this.state.searchedVenues) ? this.state.searchedVenues : this.props.venues;
107 | }
108 |
109 | _venueForm() {
110 | if (this.state.notFound) {
111 | return Search Not Found
;
112 | } else return (
113 |
121 | );
122 | }
123 |
124 | render() {
125 | this.state.showError ? this._errorFade() : null;
126 | return (
127 |
128 |
129 |
130 |
131 |
132 |
Venues
133 | {this._venueForm()}
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
147 |
148 | );
149 | }
150 |
151 | }
152 |
153 | const mapStateToProps = (state) => { return { shows: state.shows, venues: state.venues }; };
154 | const mapDispatchToProps = (dispatch) => bindActionCreators({ redux_Venues }, dispatch);
155 | export default connect(mapStateToProps)(Venues);
156 |
--------------------------------------------------------------------------------
/client/public/Show.css:
--------------------------------------------------------------------------------
1 | .panel-title {
2 | display: flex;
3 | flex-direction: row;
4 | padding: 0;
5 | }
6 |
7 | .panel-title img {
8 | display: inline-block;
9 | float: left;
10 | margin-right: 4px;
11 | }
12 |
13 | .panel-body {
14 | display: flex;
15 | flex-direction: column;
16 | padding: 0 10px;
17 | }
18 |
19 | #accordion .panel-heading {
20 | padding: 0;
21 | }
22 |
23 | .panel .panel-title {
24 | border-bottom: 0px;
25 | font-weight: 400;
26 | }
27 |
28 | .panel-group .panel+.panel {
29 | margin-top: -1px;
30 | }
31 | #venueAdress, #doorsOpen {
32 | font-size: 14px;
33 | display: inline-block;
34 | float: right;
35 | font-weight: 500;
36 | }
37 | div.panel-top {
38 | display: inline-block;
39 | width: 100%;
40 | margin-bottom: 15px;
41 | }
42 | .venue-marker{
43 | display: inline-block;
44 | margin-right: 8px;
45 | }
46 | #venueName {
47 | font-size: 1.7em;
48 | font-weight: 800;
49 | display: inline-block;
50 | }
51 |
52 | #accordion .panel-title > a {
53 | display: inline-block;
54 | padding: 16px 24px;
55 | border-radius: 0 !important;
56 | height: 75px;
57 | width: 100%;
58 | padding: 4px;
59 | line-height: 120%;
60 | outline: none;
61 | text-decoration: none;
62 | text-transform: none;
63 | }
64 |
65 | .panel-title a p.artist {
66 | color: #000;
67 | font-size: 1.7em;
68 | font-weight: 700;
69 | margin-top: 8px;
70 | margin-bottom: 8px;
71 | }
72 |
73 | .panel-title a p.venue {
74 | margin-bottom: 2px;
75 | }
76 |
77 | .panel-title a p.date {
78 | color: #000;
79 | }
80 |
81 | .panel-title .fa {
82 | margin-top: 21px;
83 | margin-right: 8px;
84 | float: right;
85 | }
86 |
87 |
88 | .accordion-venue {
89 | margin: 5px 0;
90 | font-size: 2em;
91 | }
92 |
93 |
94 | .accordion-band {
95 | display: flex;
96 | flex-direction: row;
97 | width: 100%;
98 | }
99 |
100 | .accordion-album-art {
101 | display: inline-block;
102 | height: 200px;
103 | width: 200px;
104 | margin-left: -4px;
105 | margin-right: 6px;
106 | margin-top: 2px;
107 | margin-bottom: 10px;
108 | border-radius: 500px;
109 | -webkit-box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 1);
110 | -moz-box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 1);
111 | box-shadow: 6px 6px 10px 0px rgba(0, 0, 0, 1);
112 | }
113 |
114 | .accordion-artist {
115 | margin: 7px;
116 | padding-top: 5px;
117 | font-size: 1.5em;
118 | }
119 |
120 | .left {
121 | display: inline-block;
122 | margin-left: 10px;
123 | }
124 |
125 | .popularity {
126 | display: inline-block;
127 | float: right;
128 | padding: 25px 0 25px 25px;
129 | }
130 | .text-name {
131 | display: inline-block;
132 | }
133 | .text-Popularity {
134 | float: right;
135 | margin: 4px;
136 | }
137 | .text-displayname {
138 | font-size: 20px;
139 | font-weight: 800;
140 | display: inline-block;
141 | margin-right: 6px;
142 | }
143 | .marker {
144 | margin-top: 5px;
145 | cursor: pointer;
146 | }
147 |
148 | .fa-map-marker:before {
149 | font-size: 40px;
150 | color: #f75a50;
151 | -webkit-text-stroke: 1px black;
152 | }
153 |
154 | #rightBtn {
155 | font-size: 20px;
156 | margin-top: 0px !important;
157 | margin-left: -11px !important;
158 | margin-right: -10px !important;
159 | width: 105% !important;
160 | }
161 |
162 |
163 | .band-info {
164 | display: flex;
165 | flex-direction: column;
166 | min-width: 202px;
167 | }
168 |
169 | .accordion-text {
170 | height: 135px;
171 | overflow-y: scroll;
172 | position: relative;
173 | padding-bottom: 20px;
174 | }
175 |
176 | .accordion-text-bottom-gradient {
177 | position: absolute;
178 | width: 100%;
179 | height: 35px;
180 | bottom: 10px;
181 | right: 10px;
182 | background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
183 | background: -webkit-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
184 | background: -moz-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
185 | background: -ms-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
186 | }
187 |
188 | .progress {
189 | height: 5px;
190 | }
191 |
192 | .progress-bar {
193 | height: 5px;
194 | }
195 |
196 | .accordion-album-band-name {
197 | display: inline-block;
198 | width: 200px;
199 | padding: 5px;
200 | margin: 5px 0;
201 | float: left;
202 | text-align: center;
203 | }
204 |
205 | @media screen and (max-width: 770px) {
206 | .accordion-album-art {
207 | height: 75px;
208 | width: 75px;
209 | border-radius: 500px;
210 | -webkit-box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 1);
211 | -moz-box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 1);
212 | box-shadow: 6px 6px 10px 0px rgba(0, 0, 0, 1);
213 | }
214 |
215 | .band-info {
216 | min-width: 77px;
217 | }
218 | }
219 |
220 | .accordion-album-band-name {
221 | width: 100px;
222 | }
223 |
224 | .popularity {
225 | position: relative;
226 | padding: 10px;
227 | }
228 |
--------------------------------------------------------------------------------
/server/models/m_spotifyApi.js:
--------------------------------------------------------------------------------
1 | // https://github.com/thelinmichael/spotify-web-api-node
2 | const SpotifyWebApi = require('spotify-web-api-node');
3 | const _ = require('lodash');
4 | const mongoose = require('mongoose');
5 | const db = require('../db');
6 | const ArtistModel = require('../ARTISTS_Schema');
7 | const lastFM = require('./m_lastFM');
8 | const {SPOTIFY_CLIENTID, SPOTIFY_CLIENTSECRET} = require('./api_keys');
9 |
10 | // credentials are optional
11 | const spotifyApi = new SpotifyWebApi({
12 | clientId: SPOTIFY_CLIENTID,
13 | clientSecret: SPOTIFY_CLIENTSECRET
14 | })
15 |
16 | //const cachedArtists = {};
17 | let Spotify_searchArtists = 0;
18 | exports.searchArtists = (name, songKickID) => {
19 | // check catched artists
20 |
21 | // const cacheArtist = cachedArtists[songKickID];
22 | // if (cacheArtist) {
23 | // console.log(`${++Spotify_searchArtists} found cachedArtists ${name}`)
24 | // //return catchArtist
25 | // return new Promise(function(resolve, reject) {
26 | // if(cacheArtist === "SongkickIDnotFound") {
27 | // resolve()
28 | // } else {
29 | // resolve(cacheArtist)
30 | // }
31 | // })
32 | // }
33 |
34 | return ArtistModel.findOne({
35 | "songKickID": songKickID
36 | })
37 | .then(artist => {
38 | if (artist) {
39 | console.log(`${++Spotify_searchArtists} found ${name}`)
40 | //cachedArtists[songKickID] = artist
41 | return artist
42 | } else {
43 | return addToDataBase(name)
44 | }
45 | })
46 |
47 | // artist whos names dont match will always make a new api call!
48 | function addToDataBase(Name) {
49 | return new Promise(function(resolve, reject) {
50 | spotifyApi.searchArtists(Name)
51 | .then(data => {
52 | let artist_return = null
53 | let foundName = false;
54 | data.body.artists.items.forEach((artist, i) => {
55 |
56 | // if songkick name is spotify name
57 | if (Name == artist.name) {
58 | foundName = true
59 | const Artist = new ArtistModel();
60 | console.log(`${++Spotify_searchArtists} adding ${Name}`)
61 |
62 |
63 | Artist.songKickID = songKickID
64 | Artist.spotifyURL = artist.external_urls.spotify
65 | Artist.id = artist.id
66 | Artist.name = artist.name
67 | Artist.images = artist.images
68 | Artist.img = artist.images.length ? artist.images[1].url : "http://assets.audiomack.com/default-artist-image.jpg"
69 | Artist.popularity = artist.popularity
70 | Artist.followers = artist.followers.total
71 |
72 | // Add Top Tracks
73 | spotifyApi.getArtistTopTracks(artist.id, "US").then(data => {
74 | Artist.topTracks = data.body.tracks.map(track => {
75 | return {
76 | preview_url: track.preview_url,
77 | popularity: track.popularity,
78 | name: track.name,
79 | id: track.id
80 | }
81 | })
82 |
83 | }).catch(err => console.log(err))
84 |
85 | // Add Alubm cover images
86 | spotifyApi.getArtistAlbums(artist.id).then(data => {
87 | Artist.albumsImages = data.body.items.map(album => {
88 | return {
89 | images: album.images,
90 | name: album.name
91 | }
92 | })
93 | }).catch(err => console.log(err))
94 |
95 | // Add Bio
96 | lastFM.getInfo(Name.toString()).then(data => {
97 | if (data && data.artist) {
98 | Artist.lastFM_imgs = data.artist.image
99 | Artist.summaryBio = data.artist.bio.summary
100 | Artist.fullBio = data.artist.bio.content
101 | Artist.onTour = data.artist.ontour
102 | Artist.genre = data.artist.tags.tag
103 | Artist.relatedArtists = data.artist.similar
104 | }
105 | }).catch(err => console.log("ERROR: lastFM.getInfo"))
106 |
107 | // give API calls 2 secs
108 | setTimeout(function() {
109 | Artist.save(function(err) {
110 | if (err) return console.log(err);
111 | });
112 | //cachedArtists[songKickID] = Artist
113 | resolve(Artist)
114 | }, 2000)
115 | //cachedArtists[songKickID] = Artist
116 | }
117 | })
118 | // found no name matches
119 | if(!foundName) {
120 | //cachedArtists[songKickID] = "SongkickIDnotFound"
121 | resolve()
122 | }
123 | }).catch(err => console.log("ERROR", Name));
124 | })
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/client/containers/Artists.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import { redux_Artists } from '../actions/actions';
5 | import { fetchArtistsAPI, Spotify_searchArtistsAPI } from '../models/api';
6 | import ArtistList from '../components/ArtistList';
7 | import { isReduxLoaded } from '../models/helpers';
8 | import _ from 'lodash';
9 |
10 | class Artists extends Component {
11 | constructor(props) {
12 | super(props);
13 | this.state = {
14 | searchedArtists: {},
15 | term: '',
16 | notFound: false,
17 | showError: false,
18 | };
19 | }
20 |
21 | _artistSearch(term) {
22 | this.setState({
23 | searchedArtists: {},
24 | });
25 | fetchArtistsAPI(term).then(artists => {
26 | if (artists.data.length) {
27 | this.setState({
28 | notFound: false,
29 | showError: false,
30 | });
31 | let mappedArtists;
32 | mappedArtists = this._mapData(artists);
33 | mappedArtists.forEach(artist => this._isInRedux(artist) ? this._getRedux(artist) : this._spotifySearch(artist));
34 | } else {
35 | this.setState({
36 | notFound: true,
37 | showError: true,
38 | });
39 | }
40 | });
41 | }
42 |
43 | _spotifySearch(artist) {
44 | Spotify_searchArtistsAPI(artist).then(spotify => {
45 | if (spotify.data) {
46 | this._addRedux(spotify.data, artist);
47 | }
48 | });
49 | }
50 |
51 | _isInRedux(artist) {
52 | if (this.props.artists && !this.props.artists[artist.name]) {
53 | return false;
54 | } else {
55 | return true;
56 | }
57 | }
58 |
59 | _mapData(artists) {
60 | return artists.data.map(artist => {
61 | return {
62 | onTourUntil: artist.onTourUntil,
63 | name: artist.displayName,
64 | id: artist.id,
65 | };
66 | });
67 | }
68 |
69 | _getRedux(artist) {
70 | const searchedArtists = this.state.searchedArtists;
71 | searchedArtists[artist.name] = this.props.artists[artist.name];
72 | this.setState({
73 | searchedArtists,
74 | });
75 | }
76 |
77 | _addRedux(spotify, artist) {
78 | const Artists = this.props.artists;
79 | const searchedArtists = this.state.searchedArtists;
80 | spotify['onTourUntil'] = artist.onTourUntil;
81 | searchedArtists[spotify.name] = spotify;
82 | Artists[spotify.name] = spotify;
83 | this.setState({
84 | searchedArtists,
85 | });
86 | redux_Artists(Artists);
87 | }
88 |
89 | _handleSubmit(event) {
90 | event.preventDefault();
91 | this._artistSearch(this.state.term);
92 | }
93 |
94 | _onInputChange(term) {
95 | this.setState({
96 | term,
97 | });
98 | }
99 |
100 | _errorFade() {
101 | const This = this;
102 | setTimeout(function () {
103 | This.setState({
104 | notFound: false,
105 | showError: false,
106 | });
107 | $('#artist-search-bar').find('input').val('');
108 | }, 3000);
109 | }
110 |
111 | _artistList() {
112 | return isReduxLoaded(this.state.searchedArtists) ? this.state.searchedArtists : this.props.artists;
113 | }
114 |
115 | _artistForm() {
116 | if (this.state.notFound) {
117 | return Search Not Found
;
118 | } else return (
119 |
127 | );
128 | }
129 |
130 | render() {
131 | this.state.showError ? this._errorFade() : null;
132 | return (
133 |
134 |
135 |
136 |
137 |
138 |
139 |
Artists
140 | {this._artistForm()}
141 |
142 |
143 |
144 |
145 |
148 |
149 |
150 |
155 |
156 | );
157 | }
158 | }
159 |
160 | const mapStateToProps = (state) => { return { artists: state.artists }; };
161 | const mapDispatchToProps = (dispatch) => bindActionCreators({ redux_Artists }, dispatch);
162 | export default connect(mapStateToProps, mapDispatchToProps)(Artists);
163 |
--------------------------------------------------------------------------------
/client/containers/Show.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import { Link } from 'react-router';
5 | import moment from 'moment';
6 | import { Spotify_searchArtistsAPI, Songkick_getVenueAPI } from '../models/api';
7 | import { redux_Artists, redux_Venues, selectShow } from '../actions/actions';
8 | import Speaker from './Speaker';
9 | import { topTrack, toggleSound } from '../models/helpers';
10 | import DropdownArtists from '../components/DropdownArtists';
11 |
12 |
13 | class Show extends Component {
14 |
15 | constructor(props) {
16 | super(props);
17 | this.state = {
18 | bands: [],
19 | clicked: false,
20 | venueInfo: null,
21 | };
22 | }
23 |
24 | componentWillMount() {
25 | this._getVenue(this.props.venueID);
26 | this._setArtistInfo(this.props.showArtists);
27 | }
28 |
29 |
30 | render() {
31 | const props = this.props;
32 | const thisArtist = props.artists[props.showArtists[0].displayName];
33 | const img = thisArtist ? thisArtist.img ? thisArtist.img : '/assets/artist-image.jpg' : '/assets/artist-image.jpg';
34 | return (
35 |
67 | );
68 | }
69 |
70 | _getVenue(id) {
71 | if (this.props.venues[id]) {
72 | this.setState({
73 | venueInfo: this.props.venues[id],
74 | });
75 | } else {
76 | Songkick_getVenueAPI(id).then(venue => this._addVenueRedux(venue.data));
77 | }
78 | }
79 |
80 | _addVenueRedux(venue) {
81 | const reduxVenues = this.props.venues;
82 | // Build venue entry in redux state
83 | reduxVenues[venue.id] = venue;
84 | // add to redux venues
85 | redux_Venues(reduxVenues);
86 | this.setState({
87 | venueInfo: venue,
88 | });
89 | }
90 |
91 |
92 | _doorsOpen() {
93 | // doorsOpen variable set to display pretty date with moment.js
94 | let doorsOpen = new Date(this.props.startDate).toISOString();
95 | doorsOpen = doorsOpen.split(/[T\.]/);
96 | doorsOpen[1] = this.props.doorsOpen;
97 | doorsOpen = moment(doorsOpen[0].concat('T').concat(doorsOpen[1]).concat('.').concat(doorsOpen[2])).calendar();
98 | return doorsOpen;
99 | }
100 |
101 | _setArtistInfo(showArtists) {
102 | let count = 0;
103 | const bandMembers = [];
104 | showArtists.forEach(Artist => {
105 | // add bandMembers names to array
106 | bandMembers.push(Artist.displayName);
107 | count++;
108 | if (count === showArtists.length) {
109 | this.setState({
110 | bands: bandMembers,
111 | });
112 | }
113 | });
114 | }
115 |
116 | // Tests selected show in redux state and conditionally sets
117 | // inline style property for show list item if it is selected show
118 | _checkSelected(propsSelected) {
119 | return (propsSelected) ? 'active panel-title list-group-item' : 'panel-title list-group-item';
120 | }
121 |
122 | // Sends the show's id back to the parent (ShowList.js) on click
123 | _onClickHandler(DOMString, event) {
124 | event.preventDefault();
125 | this.props.sendToState(this.props.id);
126 | // get tracks only on click
127 | if (!this.state.clicked) {
128 | this.setState({
129 | clicked: true,
130 | });
131 | }
132 | setTimeout(function () {
133 | $('.Main').scrollTo(`#${DOMString}`, 250);
134 | }, 400);
135 | }
136 |
137 | }
138 |
139 | const mapStateToProps = (state) => { return { shows: state.shows, artists: state.artists, venues: state.venues }; };
140 | const mapDispatchToProps = (dispatch) => bindActionCreators({ selectShow, redux_Venues }, dispatch);
141 | export default connect(mapStateToProps, mapDispatchToProps)(Show);
142 |
--------------------------------------------------------------------------------
/client/containers/VenueDetail.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import { Link } from 'react-router';
5 | import NavBar from './NavBar';
6 | import UpcomingShows from '../components/UpcomingShows.js';
7 | import { Songkick_getVenueCalendarAPI, Google_placeIdAPI, Google_photoAPI } from '../models/api';
8 | import { redux_Venues } from '../actions/actions';
9 | import _ from 'lodash';
10 | import { GOOGLE_PLACES_API_KEY } from '../../server/models/api_keys';
11 |
12 | // cron-job: make sure that upcoming show data does not persist for too long in db
13 |
14 | class VenueDetail extends Component {
15 |
16 | constructor(props) {
17 | super(props);
18 | this.state = {
19 | upcomingShows: null,
20 | currVenue: this.props.venues[this.props.params.venueId],
21 | place: null,
22 | photo: null,
23 | location: false,
24 | };
25 | }
26 |
27 | componentDidMount() {
28 | this._updateVenueObj(this.props.params.venueId);
29 | }
30 |
31 | _updateVenueObj(venueId) {
32 | const redux_Venue = this.props.venues;
33 | const venue = redux_Venue[this.props.params.venueId];
34 |
35 | if (!this.props.venues[venueId].upcomingShows.length) {
36 | Songkick_getVenueCalendarAPI(venueId).then((gotshows) => {
37 | redux_Venue[venue.id].upcomingShows = gotshows.data;
38 | this.setState({
39 | upcomingShows: gotshows.data,
40 | });
41 | redux_Venues(redux_Venue);
42 | });
43 | } else {
44 | this.setState({
45 | upcomingShows: redux_Venue[venue.id].upcomingShows,
46 | });
47 | }
48 | }
49 |
50 | _displayUpcomingShows() {
51 | const showObjs = this.state.upcomingShows;
52 | return showObjs.map(function (show, index) {
53 | return ( );
54 | });
55 | }
56 |
57 | _embedGoogleClick(e) {
58 | $(e.target).children('iframe').css('pointer-events', 'auto');
59 | }
60 |
61 | _embedGoogleLeave(e) {
62 | $(e.target).children('iframe').css('pointer-events', 'none');
63 | }
64 |
65 | render() {
66 | window.scrollTo(0, 0);
67 |
68 | const params = this.props.params;
69 | const redux_Venue = this.props.venues;
70 | const venue = redux_Venue[params.venueId];
71 | const venueNameForMap = venue.name.split(' ').join('+');
72 | // const state = this.state
73 | // console.log(venue.website)
74 | // formatting for venue website link
75 | if (venue.website) {
76 | let website = venue.website.slice(7);
77 | if (website.charAt(website.length - 1) === '/') {
78 | website = website.slice(0, -1);
79 | }
80 | }
81 |
82 | return (
83 |
84 |
85 |
86 |
87 | {venue.photo ?
: null}
88 |
91 |
92 | {venue.website ? {`${venue.website}`} : null}
93 | {venue.address ? {venue.address} : null}
94 | {venue.phone ? { `Phone: ${venue.phone}` } : null}
95 | {venue.price ? { `Price: ${venue.price}` } : null}
96 | {venue.rating ? { `Rating: ${venue.rating}` } : null}
97 | {venue.capactiy && venue.capacity !== 'N/A' ? { `Capactiy: ${venue.capactiy}` } : null}
98 | {venue.ageRestriction && venue.ageRestriction !== 'N/A' ? { `Age Restriction: ${venue.ageRestriction}` } : null}
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
112 | {/* Google Places Venue */}
113 |
119 | {/* Google Street View Venue */}
120 |
126 |
127 |
128 |
129 |
130 |
Upcoming Shows
131 | {this.state.upcomingShows ?
{this._displayUpcomingShows()}
: 'No Shows...?'}
132 |
133 |
134 |
135 |
136 |
137 |
138 | );
139 | }
140 |
141 | }
142 | const mapStateToProps = (state) => { return { venues: state.venues }; };
143 | const mapDispatchToProps = (dispatch) => bindActionCreators({ redux_Venues }, dispatch);
144 | export default connect(mapStateToProps, mapDispatchToProps)(VenueDetail);
145 |
--------------------------------------------------------------------------------
/git_workflow.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## General Git Workflow
4 |
5 | 1. Clone down the master directly (do not fork):
6 |
7 | -> git clone masterURL yourdirectory
8 |
9 | 2. Create a new feature/issue branch from master and name the branch "issue#". If it's a bug fix, name the branch "bug#". # should be the associated issue number on the GitHub repo.
10 |
11 | -> git checkout -b issue3
12 |
13 | OR
14 |
15 | -> git checkout -b bug11
16 |
17 | 3. Make changes and commit to your feature branch.
18 |
19 | -> git add .
20 |
21 | 4. ALWAYS sync up with latest master before pushing to remote feature branch:
22 |
23 | -> git pull --rebase origin master
24 |
25 | 5. Fix any merge conflicts if necessary.
26 |
27 | 6. Push changes to remote feature branch:
28 |
29 | -> git push origin feat3
30 |
31 | 7. Generate pull request:
32 |
33 | -> base: master
34 | -> compare: feat3
35 |
36 | 8. Fix any issues highlighted by reviewer if necessary.
37 |
38 | 9. When everything checks out, reviewer merges pull request to master.
39 |
40 | 10. When a pull request is merged and closed, delete feat3 branch.
41 |
42 |
43 |
44 | ## Detailed Workflow
45 |
46 | ### Cut a namespaced feature branch from master
47 |
48 | Your branch should follow this naming convention:
49 | - issue#
50 | - bug#
51 |
52 | Where # associates directly with the issue number in the GitHub repo
53 |
54 | These commands will help you do this:
55 |
56 | # Creates your branch and brings you there
57 |
58 | git checkout -b `your-branch-name`
59 |
60 | ### Make commits to your feature branch.
61 |
62 | Prefix each commit like so
63 | - (feat) Added a new feature
64 | - (fix) Fixed inconsistent tests [Fixes #0]
65 | - (refactor) ...
66 | - (cleanup) ...
67 | - (test) ...
68 | - (doc) ...
69 |
70 | Make changes and commits on your branch, and make sure that you
71 | only make changes that are relevant to this branch. If you find
72 | yourself making unrelated changes, make a new branch for those
73 | changes.
74 |
75 | #### Commit Message Guidelines
76 |
77 | - Commit messages should be written in the present tense; e.g. "Fix continuous
78 | integration script".
79 | - The first line of your commit message should be a brief summary of what the
80 | commit changes. Aim for about 70 characters max. Remember: This is a summary,
81 | not a detailed description of everything that changed.
82 | - If you want to explain the commit in more depth, following the first line should
83 | be a blank line and then a more detailed description of the commit. This can be
84 | as detailed as you want, so dig into details here and keep the first line short.
85 |
86 | ### Rebase upstream changes into your branch
87 |
88 | Once you are done making changes, you can begin the process of getting
89 | your code merged into the main repo. Step 1 is to rebase upstream
90 | changes to the master branch into yours by running this command
91 | from your branch:
92 |
93 | git pull --rebase upstream master
94 |
95 | This will start the rebase process. You must commit all of your changes
96 | before doing this. If there are no conflicts, this should just roll all
97 | of your changes back on top of the changes from upstream, leading to a
98 | nice, clean, linear commit history.
99 |
100 | If there are conflicting changes, git will start yelling at you part way
101 | through the rebasing process. Git will pause rebasing to allow you to sort
102 | out the conflicts. You do this the same way you solve merge conflicts,
103 | by checking all of the files git says have been changed in both histories
104 | and picking the versions you want. Be aware that these changes will show
105 | up in your pull request, so try and incorporate upstream changes as much
106 | as possible.
107 |
108 | When resolving conflicts, you will need to edit the appropriate files and
109 | `git add` them. However, you should not commit during the rebase process.
110 | Once you are done fixing conflicts for a specific commit, run:
111 |
112 | git rebase --continue
113 |
114 | This will continue the rebasing process. Once you are done fixing all
115 | conflicts you should run the existing tests to make sure you didn’t break
116 | anything, then run your new tests (there are new tests, right?) and
117 | make sure they work also.
118 |
119 | If rebasing broke anything, fix it, then repeat the above process until
120 | you get here again and nothing is broken and all the tests pass.
121 |
122 | ### Make a pull request
123 |
124 | Make a clear pull request from your fork and branch to the upstream master
125 | branch, detailing exactly what changes you made and what feature this
126 | should add. The clearer your pull request is the faster you can get
127 | your changes incorporated into this repo.
128 |
129 | At least one other person MUST give your changes a code review, and once
130 | they are satisfied they will merge your changes into upstream. Alternatively,
131 | they may have some requested changes. You should make more commits to your
132 | branch to fix these, then follow this process again from rebasing onwards.
133 |
134 | Note: A pull request will be immediately rejected if there are any conflicts!
135 |
136 | Once you get back here, make a comment requesting further review and
137 | someone will look at your code again. If they like it, it will get merged,
138 | else, just repeat again.
139 |
140 | Thanks for contributing!
141 |
142 | ### Guidelines
143 |
144 | Uphold the current code standard:
145 | - Keep your code [DRY][].
146 | - Apply the [boy scout rule][].
147 | - Follow the [Airbnb JS Style Guide](https://github.com/airbnb/javascript)
148 |
149 |
150 | ## Checklist:
151 |
152 | This is just to help you organize your process
153 |
154 | - [ ] Did I cut my work branch off of master (don't cut new branches from existing feature brances)?
155 | - [ ] Did I follow the correct naming convention for my branch?
156 | - [ ] Is my branch focused on a single main change?
157 | - [ ] Do all of my changes directly relate to this change?
158 | - [ ] Did I rebase the upstream master branch after I finished all my
159 | work?
160 | - [ ] Did I write a clear pull request message detailing what changes I made?
161 | - [ ] Did I get a code review?
162 | - [ ] Did I make any requested changes from that code review?
163 |
164 | If you follow all of these guidelines and make good changes, you should have
165 | no problem getting your changes merged in.
166 |
167 |
168 | [pull request]: https://help.github.com/articles/using-pull-requests/
169 | [DRY]: http://en.wikipedia.org/wiki/Don%27t_repeat_yourself
170 | [boy scout rule]: http://programmer.97things.oreilly.com/wiki/index.php/The_Boy_Scout_Rule
171 | [squashed]: http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html
172 |
173 | [tests]: tests/
174 |
--------------------------------------------------------------------------------
/server/models/m_songkick.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const Songkick = require('songkick-api');
3 | const Spotify = require('./m_spotifyApi');
4 | const {SONGKICK_FM_APIKEY} = require('./api_keys');
5 | const Google = require('./m_google')
6 | const client = new Songkick(SONGKICK_FM_APIKEY)
7 |
8 | // When specifying min_date or max_date, you need to use both parameters.
9 | // Use the same value for both to get events for a single day.
10 | // This search returns only upcoming events.
11 | exports.getShows = (data) => {
12 | sendArtistNamesToSpotify = shows => {
13 | let artists = []
14 | shows.forEach(show => artists.push(...show.performance))
15 | artists = _.uniq(artists.map(show => {
16 | return {
17 | name: show.artist.displayName,
18 | id: show.artist.id
19 | }
20 | }))
21 | artists.forEach(artist => Spotify.searchArtists(artist.name, artist.id))
22 | }
23 |
24 | exports.searchVenues = (query) => {
25 | return client.searchVenues(query)
26 | .then(data => data)
27 | .catch(err => console.error(err));
28 | }
29 |
30 | mapVenues = shows => {
31 | let venues = []
32 | venues = _.uniq(venues.map(show => {
33 | return {
34 | name: show.venue.displayName,
35 | id: show.venue.id,
36 | geo: {
37 | lat: show.venue.lat,
38 | long: show.venue.lng
39 | }
40 | }
41 | }))
42 | venues.forEach(venue => getVenue(venue.id))
43 | }
44 | // Search based on a songkick metro area id
45 | // austin 'geo:30.2669444,-97.7431'
46 | // `geo:${coords.lat},${coords.long}`
47 | let today = new Date()
48 | today = today.toISOString().slice(0, 10)
49 | console.log(`geo:${data.lat},${data.long}`, data.dateA || today, data.dateB || today);
50 | return client.searchEvents({
51 | "location": `geo:${data.lat},${data.long}`,
52 | "min_date": data.dateA || today,
53 | "max_date": data.dateB || today
54 | }).then((shows) => {
55 | if (shows) {
56 | mapVenues(shows)
57 | sendArtistNamesToSpotify(shows)
58 | let concerts = shows.slice();
59 | concerts.forEach(show => {
60 | if (show.venue.lat === null) show.venue.lat = show.location.lat;
61 | if (show.venue.lng === null) show.venue.lng = show.location.lng;
62 | });
63 | if (concerts.length < 10) {
64 | return client.searchEvents({
65 | "location": `geo:${data.lat},${data.long}`
66 | }).then(Shows => {
67 | mapVenues(shows)
68 | sendArtistNamesToSpotify(Shows)
69 | let concerts = Shows.slice();
70 | concerts.forEach(Show => {
71 | if (Show.venue.lat === null) Show.venue.lat = Show.location.lat;
72 | if (Show.venue.lng === null) Show.venue.lng = Show.location.lng;
73 | });
74 | return concerts;
75 | })
76 | } else {
77 | return concerts
78 | }
79 | } else return 'No concerts found for the given dates / location';
80 | })
81 | }
82 |
83 | exports.getArtists = (query) => {
84 | return client.searchArtists(query)
85 | .then(data => data)
86 | .catch(err => console.error(err));
87 | }
88 |
89 | let getVenue = 0;
90 | const VenueModel = require('../VENUE_Schema');
91 | exports.getVenue = (venueId) => {
92 |
93 | return VenueModel.findOne({
94 | "id": venueId
95 | })
96 | .then(venue => {
97 | if (venue) {
98 | console.log(`${++getVenue} found ${venueId}`)
99 | return venue
100 | } else {
101 | return getVenueInfo(venueId)
102 | }
103 | })
104 |
105 | function getVenueInfo(venueId) {
106 | return client.getVenue(venueId)
107 | .then(data => addToDatabase(data))
108 | .catch(err => console.error(err));
109 | }
110 |
111 | function addToDatabase(venue) {
112 | return new Promise(function(resolve, reject) {
113 | const Venue = new VenueModel();
114 | console.log(`${++getVenue} adding ${venueId}`)
115 | Venue.id = venueId
116 | Venue.capacity = venue.capacity || 'N/A'
117 | Venue.street = venue.street
118 | Venue.geo = {
119 | lat: venue.lat,
120 | long: venue.lng
121 | }
122 | Venue.city = venue.city.displayName
123 | Venue.state = venue.city.state ? venue.city.state.displayName : null
124 | Venue.website = venue.website
125 | Venue.name = venue.displayName
126 | Venue.address = venue.city.state ? `${venue.street} St, ${venue.city.displayName}, ${venue.city.state.displayName}` : null
127 | Venue.phone = venue.phone
128 | getPlaceInfo(Venue.name, Venue.geo.lat, Venue.geo.long, Venue, resolve)
129 | })
130 | }
131 |
132 | }
133 |
134 | function getPlaceInfo(name, lat, long, Venue, resolve) {
135 | //console.log(name, lat, long)
136 | Google.placeIdAPI(name, +lat, +long)
137 | .then(resp => {
138 |
139 | if (resp[0] && resp[0].id) {
140 | const venue = resp[0]
141 | Venue.rating = venue.rating
142 | Venue.icon = venue.icon
143 | Venue.googleID = venue.placeId
144 | Venue.price = venue.price_level
145 |
146 | if (venue.photos && venue.photos[0]) {
147 | //console.log(Venue.google.photos[0].photo_reference)
148 | getPlacePhoto(venue.photos[0].photo_reference, Venue, resolve)
149 | } else {
150 | Venue.save(function(err) {
151 | if (err) return console.log(err);
152 | });
153 | resolve(Venue)
154 | }
155 |
156 | } else {
157 | Venue.save(function(err) {
158 | if (err) return console.log(err);
159 | });
160 | resolve(Venue)
161 | }
162 | })
163 | .catch(err => resolve(Venue))
164 | }
165 |
166 | function getPlacePhoto(photoReference, Venue, resolve) {
167 | Google.photoAPI(photoReference)
168 | .then(photo => {
169 | Venue.photo = photo
170 | Venue.save(function(err) {
171 | if (err) return console.log(err);
172 | });
173 | resolve(Venue)
174 | })
175 | .catch(err => resolve(Venue))
176 | }
177 |
178 | exports.getArtistCalendar = (artistID) => {
179 | return client.getArtistCalendar(artistID)
180 | .then(data => data)
181 | .catch(err => console.error(err));
182 | }
183 |
184 | exports.getVenueCalendar = (venueID) => {
185 | return client.getVenueCalendar(venueID)
186 | .then(data => data)
187 | .catch(err => console.error(err));
188 | }
189 |
190 | exports.getMetroAreaCalendar = (metroID) => {
191 | return client.getMetroAreaCalendar(metroID)
192 | .then(data => data)
193 | .catch(err => console.error(err));
194 | }
195 |
196 | exports.getEventSetlist = (eventID) => {
197 | return client.getEventSetlist(eventID)
198 | .then(data => data)
199 | .catch(err => console.error(err));
200 | }
201 |
202 | exports.getSimilarArtists = (artistID) => {
203 | return client.getSimilarArtists(eventID)
204 | .then(data => data)
205 | .catch(err => console.error(err));
206 | }
207 |
--------------------------------------------------------------------------------
/client/containers/ArtistDetail.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import { Link, browserHistory } from 'react-router';
5 | import YTSearch from 'youtube-api-search';
6 | import _ from 'lodash';
7 | import NavBar from './NavBar';
8 | import UpcomingShowsDetail from '../components/UpcomingShowsDetail';
9 | import VideoDetail from '../components/VideoDetail';
10 | import AudioPlayer from '../components/AudioPlayer';
11 | import { redux_Artists } from '../actions/actions';
12 | import { Songkick_getArtistCalendarAPI, fetchArtistsAPI, Spotify_searchArtistsAPI } from '../models/api';
13 | import { getAlbumArt, topTrack, getBio, getArtistImg, getRandomAlbumArt } from '../models/helpers';
14 | import { YOUTUBE_KEY } from '../../server/models/api_keys';
15 |
16 |
17 | class ArtistDetail extends Component {
18 |
19 | constructor(props) {
20 | super(props);
21 | this.state = {
22 | videos: [],
23 | selectedVideo: null,
24 | shows: null,
25 | artist: null,
26 | albumArt: null,
27 | };
28 | }
29 |
30 | componentDidMount() {
31 | this.setState({
32 | artist: this._getArtist(this.props.params.artistName),
33 | });
34 | this._randomAlbumArt();
35 | }
36 |
37 | _randomAlbumArt() {
38 | this.setState({
39 | albumArt: getRandomAlbumArt(this.state.artist),
40 | });
41 | }
42 |
43 | componentWillReceiveProps() {
44 | this.setState({
45 | artist: this._getArtist(this.props.params.artistName),
46 | });
47 | }
48 |
49 | _render(artist) {
50 | const albumArt = this.state.albumArt ? this.state.albumArt : getAlbumArt(artist);
51 | return (
52 |
53 |
54 |
55 |
56 |
57 |
{`${artist.name}`}
58 |
{this._onTour(artist.onTour)}
59 | {this._spotifyFollow(this.props.spotifyUser.accessToken, artist.id)}
60 |
61 |
62 | {getBio(artist)}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
Similar Artists
71 |
{this._similarArtists(artist.relatedArtists)}
72 |
73 |
74 | );
75 | }
76 |
77 | _spotifyFollow(token, id) {
78 | const url = `https://embed.spotify.com/follow/1/?uri=spotify:artist:${id}&size=basic&theme=light&show-count=0`;
79 | if (token) {
80 | return (
81 |
82 |
91 |
92 | );
93 | } else {
94 | return null;
95 | }
96 | }
97 |
98 | render() {
99 | if (this.state.artist) {
100 | return this._render(this.state.artist);
101 | } else {
102 | return Loading
;
103 | }
104 | }
105 |
106 | _videoSearch(term) {
107 | YTSearch({
108 | key: YOUTUBE_KEY,
109 | term,
110 | }, (videos) => {
111 | this.setState({
112 | videos,
113 | selectedVideo: videos[0],
114 | });
115 | });
116 | }
117 |
118 | _onTour(tour) {
119 | if (tour === '1') {
120 | return 'ON TOUR';
121 | } else {
122 | return null;
123 | }
124 | }
125 |
126 | _getGenre(genres) {
127 | if (!genres) {
128 | return null;
129 | }
130 | else {
131 | return genres.map(genre => {
132 | return {genre.name} ;
133 | });
134 | }
135 | }
136 |
137 | _similarArtistsImg(img) {
138 | if (img['#text'] === '') {
139 | return 'http://assets.audiomack.com/default-artist-image.jpg';
140 | } else {
141 | return img['#text'];
142 | }
143 | }
144 |
145 | _similarArtists(artists) {
146 | if (!artists || !artists[0]) {
147 | return null;
148 | } else {
149 | const mapped = artists[0].artist.map(artist => {
150 | return {
151 | name: artist.name,
152 | image: this._similarArtistsImg(artist.image[3]),
153 | };
154 | });
155 | return mapped.map(artist => {
156 | return (
157 |
161 | );
162 | });
163 | }
164 | }
165 |
166 | _onSimilarClick(name) {
167 | browserHistory.push(`/artist/${name}`);
168 | setTimeout(function () {
169 | browserHistory.push(`/artist/${name}`);
170 | }, 50);
171 | }
172 |
173 | _isAristInRedux(name) {
174 | return this.props.artists[name] ? true : false;
175 | }
176 |
177 | _getArtistCalendar(id) {
178 | Songkick_getArtistCalendarAPI(id).then(shows => {
179 | this.setState({
180 | shows: shows.data,
181 | });
182 | });
183 | }
184 |
185 | _addArtistToRedux(artist) {
186 | const artists = this.props.artists;
187 | artists[artist.name] = artist;
188 | this._getArtistCalendar(artists[artist.name].songKickID);
189 | // update redux state with new artist
190 | this.setState({
191 | artist: artists[artist.name],
192 | });
193 | redux_Artists(artists);
194 | return artists[artist.name];
195 | }
196 |
197 | _getArtist(name) {
198 | this._videoSearch(name);
199 | // all arist can be redux state, but similar-artist
200 | if (this._isAristInRedux(name)) {
201 | this._getArtistCalendar(this.props.artists[name].songKickID);
202 | return this.props.artists[name];
203 | } else {
204 | fetchArtistsAPI(name)
205 | .then(artist => artist.data[0].id)
206 | .then(id => Spotify_searchArtistsAPI({ name, id })
207 | .then(artistInfo => this._addArtistToRedux(artistInfo.data)));
208 | }
209 | }
210 | }
211 | const mapStateToProps = (state) => { return { artists: state.artists, spotifyUser: state.spotifyUser }; };
212 | const mapDispatchToProps = (dispatch) => bindActionCreators({ redux_Artists }, dispatch);
213 | export default connect(mapStateToProps, mapDispatchToProps)(ArtistDetail);
214 |
--------------------------------------------------------------------------------
/server/routes.js:
--------------------------------------------------------------------------------
1 | const querystring = require('querystring');
2 | const express = require('express');
3 | const SpotifyWebApi = require('spotify-web-api-node');
4 | const { SPOTIFYWEB_CLIENTID, SPOTIFYWEB_CLIENTSECRET } = require('./models/api_keys');
5 |
6 | const router = new express.Router();
7 | // configure the express server
8 |
9 | //--------------SPOTIFY LOGIN ROUTES-------------------//
10 | //-----------------------------------------------------//
11 | const client_id = SPOTIFYWEB_CLIENTID;
12 | const client_secret = SPOTIFYWEB_CLIENTSECRET;
13 | const redirect_uri = 'http://melody-map.com/callback/'; // Your redirect uri
14 | const stateKey = 'spotify_auth_state';
15 | // your application requests authorization
16 | const scopes = ['user-read-private', 'user-read-email'];
17 | const spotifyApi = new SpotifyWebApi({
18 | clientId: client_id,
19 | clientSecret: client_secret,
20 | redirectUri: redirect_uri,
21 | });
22 |
23 | /** Generates a random string containing numbers and letters of N characters */
24 | const generateRandomString = function(length) {
25 | var text = '';
26 | var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
27 | for (var i = 0; i < length; i++) {
28 | text += possible.charAt(Math.floor(Math.random() * possible.length));
29 | }
30 | return text;
31 | };
32 |
33 |
34 | /**
35 | * The /login endpoint
36 | * Redirect the client to the spotify authorize url, but first set that user's
37 | * state in the cookie.
38 | */
39 | router.get('/login', function(req, res){
40 | const state = generateRandomString(16);
41 | res.cookie(stateKey, state);
42 | //application requests authorization
43 | res.redirect(spotifyApi.createAuthorizeURL(scopes,state));
44 | });
45 |
46 | /**
47 | * The /callback endpoint - hit after the user logs in to spotifyApi
48 | * Verify that the state we put in the cookie matches the state in the query
49 | * parameter. Then, if all is good, redirect the user to the user page. If all
50 | * is not good, redirect the user to an error page
51 | */
52 | router.get('/callback/', (req, res) => {
53 | const { code, state } = req.query;
54 | const storedState = req.cookies ? req.cookies[stateKey] : null;
55 | // first do state validation
56 | if (state === null || state !== storedState) {
57 | res.redirect('/#/error/state mismatch');
58 | // if the state is valid, get the authorization code and pass it on to the client
59 | } else {
60 | //res.clearCookie(stateKey);
61 | // Retrieve an access token and a refresh token
62 | spotifyApi.authorizationCodeGrant(code).then(data => {
63 | const { expires_in, access_token, refresh_token } = data.body;
64 |
65 | // Set the access token on the API object to use it in later calls
66 | spotifyApi.setAccessToken(access_token);
67 | spotifyApi.setRefreshToken(refresh_token);
68 |
69 |
70 | // use the access token to access the Spotify Web API
71 | spotifyApi.getMe().then(({ body }) => {
72 | //sends user information to the client
73 | res.send(body)
74 | });
75 |
76 | // we can also pass the token to the browser to make requests from there
77 | res.redirect(`/#/user/${access_token}/${refresh_token}`);
78 | }).catch(err => {
79 | console.log("error")
80 | res.redirect('/#/error/invalid token');
81 | console.log("error after redurect")
82 | });
83 | }
84 | });
85 |
86 | //------------------SHOW, VENUE, ARTIST ROUTES---------------//
87 | //-----------------------------------------------------//
88 | const Songkick = require("./models/m_songkick");
89 | const Spotify = require("./models/m_spotifyApi")
90 | const LastFM = require("./models/m_lastFM")
91 | const Artist = require("./models/m_artist")
92 | const Google = require("./models/m_google")
93 |
94 | router.post('/Google_placeIdAPI', function(req, res) {
95 | Google.placeIdAPI(req.body.name, req.body.lat, req.body.long)
96 | .then(data => res.send(data))
97 | .catch(error => console.log('error', error))
98 | })
99 |
100 | router.get('/Google_photoAPI', function(req, res) {
101 | Google.photoAPI(req.param('photoReference'))
102 | .then(data => res.send(data))
103 | .catch(error => console.log('BOOM', error))
104 | })
105 |
106 | let Spotify_getArtistRelatedArtists = 0;
107 | router.post('/Spotify_getArtistRelatedArtists', function(req, res) {
108 | console.log(`/Spotify_getArtistRelatedArtists ${++Spotify_getArtistRelatedArtists}`)
109 | Spotify.getArtistRelatedArtists(req.body.id)
110 | .then(data => res.send(data))
111 | .catch(error => console.log("error", error))
112 | })
113 |
114 |
115 | router.post('/Spotify_searchArtists', function(req, res) {
116 | Spotify.searchArtists(req.body.name, req.body.id)
117 | .then(data => res.send(JSON.stringify(data)))
118 | .catch(error => console.log("error", error))
119 | })
120 |
121 | // Artist Data
122 | let Artist_artistInfo = 0;
123 | router.post('/Artist_artistInfo', function(req, res) {
124 | console.log(`/Artist_artistInfo ${++Artist_artistInfo}`)
125 | Artist.artistInfo(req.body.name)
126 | .then(data => res.send(data))
127 | .catch(error => console.log("error", error))
128 | })
129 |
130 | let Songkick_getEventSetlist = 0;
131 | router.post('/Songkick_getEventSetlist', function(req, res) {
132 | console.log(`/Songkick_getEventSetlist ${++Songkick_getEventSetlist}`)
133 | Songkick.getEventSetlist(req.body.id)
134 | .then(data => res.send(data))
135 | .catch(error => console.log("error", error))
136 | })
137 |
138 | let Songkick_getMetroAreaCalendar = 0;
139 | router.post('/Songkick_getMetroAreaCalendar', function(req, res) {
140 | console.log(`/Songkick_getMetroAreaCalendar ${++Songkick_getMetroAreaCalendar}`)
141 | Songkick.getMetroAreaCalendar(req.body.id)
142 | .then(data => res.send(data))
143 | .catch(error => console.log("error", error))
144 | })
145 |
146 | let Songkick_getVenueCalendar = 0;
147 | router.post('/Songkick_getVenueCalendar', function(req, res) {
148 | console.log(`/Songkick_getVenueCalendar ${++Songkick_getVenueCalendar}`)
149 | Songkick.getVenueCalendar(req.body.id)
150 | .then(data => res.send(data))
151 | .catch(error => console.log("error", error))
152 | })
153 |
154 | let Songkick_getArtistCalendar = 0;
155 | router.post('/Songkick_getArtistCalendar', function(req, res) {
156 | console.log(`/Songkick_getArtistCalendar ${++Songkick_getArtistCalendar}`)
157 | Songkick.getArtistCalendar(req.body.id)
158 | .then(data => res.send(data))
159 | .catch(error => console.log("error", error))
160 | })
161 |
162 | router.post('/Songkick_getVenue', function(req, res) {
163 | Songkick.getVenue(req.body.id)
164 | .then(data => res.send(data))
165 | .catch(error => console.log("error", error))
166 | })
167 |
168 | router.post('/fetchShows', function(req, res) {
169 | console.log("/fetchShows", req.body);
170 | Songkick.getShows(req.body)
171 | .then(data => res.send(data))
172 | .catch(error => console.log("error", error))
173 | })
174 |
175 | router.post('/fetchArtists', function(req,res){
176 | console.log("/fetchArtists", req.body)
177 | Songkick.getArtists(req.body)
178 | .then(data => res.send(data))
179 | .catch(error => console.log("error", error))
180 | })
181 |
182 | router.post('/fetchVenues', function(req,res){
183 | console.log("/fetchVenues", req.body)
184 | Songkick.searchVenues(req.body)
185 | .then(data => res.send(data))
186 | .catch(error => console.log("error", error))
187 | })
188 |
189 | module.exports = router;
190 |
--------------------------------------------------------------------------------
/client/public/react-datepicker.css:
--------------------------------------------------------------------------------
1 | .react-datepicker__tether-element-attached-top .react-datepicker__triangle, .react-datepicker__tether-element-attached-bottom .react-datepicker__triangle, .react-datepicker__year-read-view--down-arrow {
2 | margin-left: -8px;
3 | position: absolute;
4 | }
5 | .react-datepicker__tether-element-attached-top .react-datepicker__triangle, .react-datepicker__tether-element-attached-bottom .react-datepicker__triangle, .react-datepicker__year-read-view--down-arrow, .react-datepicker__tether-element-attached-top .react-datepicker__triangle::before, .react-datepicker__tether-element-attached-bottom .react-datepicker__triangle::before, .react-datepicker__year-read-view--down-arrow::before {
6 | box-sizing: content-box;
7 | position: absolute;
8 | border: 8px solid transparent;
9 | height: 0;
10 | width: 1px;
11 | }
12 | .react-datepicker__tether-element-attached-top .react-datepicker__triangle::before, .react-datepicker__tether-element-attached-bottom .react-datepicker__triangle::before, .react-datepicker__year-read-view--down-arrow::before {
13 | content: "";
14 | z-index: -1;
15 | border-width: 8px;
16 | left: -8px;
17 | border-bottom-color: #aeaeae;
18 | }
19 |
20 | .react-datepicker__tether-element-attached-top .react-datepicker__triangle {
21 | top: 0;
22 | margin-top: -8px;
23 | }
24 | .react-datepicker__tether-element-attached-top .react-datepicker__triangle, .react-datepicker__tether-element-attached-top .react-datepicker__triangle::before {
25 | border-top: none;
26 | border-bottom-color: #f0f0f0;
27 | }
28 | .react-datepicker__tether-element-attached-top .react-datepicker__triangle::before {
29 | top: -1px;
30 | border-bottom-color: #aeaeae;
31 | }
32 |
33 | .react-datepicker__tether-element-attached-bottom .react-datepicker__triangle, .react-datepicker__year-read-view--down-arrow {
34 | bottom: 0;
35 | margin-bottom: -8px;
36 | }
37 | .react-datepicker__tether-element-attached-bottom .react-datepicker__triangle, .react-datepicker__year-read-view--down-arrow, .react-datepicker__tether-element-attached-bottom .react-datepicker__triangle::before, .react-datepicker__year-read-view--down-arrow::before {
38 | border-bottom: none;
39 | border-top-color: #fff;
40 | }
41 | .react-datepicker__tether-element-attached-bottom .react-datepicker__triangle::before, .react-datepicker__year-read-view--down-arrow::before {
42 | bottom: -1px;
43 | border-top-color: #aeaeae;
44 | }
45 |
46 | .react-datepicker {
47 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
48 | font-size: 11px;
49 | background-color: #fff;
50 | color: #000;
51 | border: 1px solid #aeaeae;
52 | border-radius: 4px;
53 | display: inline-block;
54 | position: relative;
55 | }
56 |
57 | .react-datepicker__triangle {
58 | position: absolute;
59 | left: 50px;
60 | }
61 |
62 | .react-datepicker__tether-element-attached-bottom.react-datepicker__tether-element {
63 | margin-top: -20px;
64 | }
65 |
66 | .react-datepicker__header {
67 | text-align: center;
68 | background-color: #f0f0f0;
69 | border-bottom: 1px solid #aeaeae;
70 | border-top-left-radius: 4px;
71 | border-top-right-radius: 4px;
72 | padding-top: 8px;
73 | position: relative;
74 | }
75 |
76 | .react-datepicker__current-month {
77 | margin-top: 0;
78 | color: #000;
79 | font-weight: bold;
80 | font-size: 13px;
81 | }
82 | .react-datepicker__current-month--hasYearDropdown {
83 | margin-bottom: 16px;
84 | }
85 |
86 | .react-datepicker__navigation {
87 | line-height: 24px;
88 | text-align: center;
89 | cursor: pointer;
90 | position: absolute;
91 | top: 10px;
92 | width: 0;
93 | border: 6px solid transparent;
94 | }
95 | .react-datepicker__navigation--previous {
96 | left: 10px;
97 | border-right-color: #ccc;
98 | }
99 | .react-datepicker__navigation--previous:hover {
100 | border-right-color: #b3b3b3;
101 | }
102 | .react-datepicker__navigation--next {
103 | right: 10px;
104 | border-left-color: #ccc;
105 | }
106 | .react-datepicker__navigation--next:hover {
107 | border-left-color: #b3b3b3;
108 | }
109 | .react-datepicker__navigation--years {
110 | position: relative;
111 | top: 0;
112 | display: block;
113 | margin-left: auto;
114 | margin-right: auto;
115 | }
116 | .react-datepicker__navigation--years-previous {
117 | top: 4px;
118 | border-top-color: #ccc;
119 | }
120 | .react-datepicker__navigation--years-previous:hover {
121 | border-top-color: #b3b3b3;
122 | }
123 | .react-datepicker__navigation--years-upcoming {
124 | top: -4px;
125 | border-bottom-color: #ccc;
126 | }
127 | .react-datepicker__navigation--years-upcoming:hover {
128 | border-bottom-color: #b3b3b3;
129 | }
130 |
131 | .react-datepicker__month {
132 | margin: 5px;
133 | text-align: center;
134 | }
135 |
136 | .react-datepicker__day-name,
137 | .react-datepicker__day {
138 | color: #000;
139 | display: inline-block;
140 | width: 24px;
141 | line-height: 24px;
142 | text-align: center;
143 | margin: 2px;
144 | }
145 |
146 | .react-datepicker__day {
147 | cursor: pointer;
148 | }
149 | .react-datepicker__day:hover {
150 | border-radius: 4px;
151 | background-color: #f0f0f0;
152 | }
153 | .react-datepicker__day--today {
154 | font-weight: bold;
155 | }
156 | .react-datepicker__day--selected, .react-datepicker__day--in-range {
157 | border-radius: 4px;
158 | background-color: #216ba5;
159 | color: #fff;
160 | }
161 | .react-datepicker__day--selected:hover, .react-datepicker__day--in-range:hover {
162 | background-color: #1d5d90;
163 | }
164 | .react-datepicker__day--disabled {
165 | cursor: default;
166 | color: #ccc;
167 | }
168 | .react-datepicker__day--disabled:hover {
169 | background-color: transparent;
170 | }
171 |
172 | .react-datepicker__input-container {
173 | position: relative;
174 | display: inline-block;
175 | }
176 |
177 | .react-datepicker__year-read-view {
178 | width: 50%;
179 | left: 25%;
180 | position: absolute;
181 | bottom: 25px;
182 | border: 1px solid transparent;
183 | border-radius: 4px;
184 | }
185 | .react-datepicker__year-read-view:hover {
186 | cursor: pointer;
187 | }
188 | .react-datepicker__year-read-view:hover .react-datepicker__year-read-view--down-arrow {
189 | border-top-color: #b3b3b3;
190 | }
191 | .react-datepicker__year-read-view--down-arrow {
192 | border-top-color: #ccc;
193 | margin-bottom: 3px;
194 | left: 5px;
195 | top: 9px;
196 | position: relative;
197 | border-width: 6px;
198 | }
199 | .react-datepicker__year-read-view--selected-year {
200 | right: 6px;
201 | position: relative;
202 | }
203 |
204 | .react-datepicker__year-dropdown {
205 | background-color: #f0f0f0;
206 | position: absolute;
207 | width: 50%;
208 | left: 25%;
209 | top: 30px;
210 | text-align: center;
211 | border-radius: 4px;
212 | border: 1px solid #aeaeae;
213 | }
214 | .react-datepicker__year-dropdown:hover {
215 | cursor: pointer;
216 | }
217 |
218 | .react-datepicker__year-option {
219 | line-height: 20px;
220 | width: 100%;
221 | display: block;
222 | margin-left: auto;
223 | margin-right: auto;
224 | }
225 | .react-datepicker__year-option:first-of-type {
226 | border-top-left-radius: 4px;
227 | border-top-right-radius: 4px;
228 | }
229 | .react-datepicker__year-option:last-of-type {
230 | -webkit-user-select: none;
231 | -moz-user-select: none;
232 | -ms-user-select: none;
233 | user-select: none;
234 | border-bottom-left-radius: 4px;
235 | border-bottom-right-radius: 4px;
236 | }
237 | .react-datepicker__year-option:hover {
238 | background-color: #ccc;
239 | }
240 | .react-datepicker__year-option:hover .react-datepicker__navigation--years-upcoming {
241 | border-bottom-color: #b3b3b3;
242 | }
243 | .react-datepicker__year-option:hover .react-datepicker__navigation--years-previous {
244 | border-top-color: #b3b3b3;
245 | }
246 | .react-datepicker__year-option--selected {
247 | position: absolute;
248 | left: 30px;
249 | }
250 |
251 | .react-datepicker__close-icon {
252 | background-color: transparent;
253 | border: 0;
254 | cursor: pointer;
255 | display: inline-block;
256 | height: 0;
257 | outline: 0;
258 | padding: 0;
259 | vertical-align: middle;
260 | }
261 | .react-datepicker__close-icon::after {
262 | background-color: #216ba5;
263 | border-radius: 50%;
264 | bottom: 0;
265 | box-sizing: border-box;
266 | color: #fff;
267 | content: "\00d7";
268 | cursor: pointer;
269 | font-size: 12px;
270 | height: 16px;
271 | width: 16px;
272 | line-height: 1;
273 | margin: -8px auto 0;
274 | padding: 2px;
275 | position: absolute;
276 | right: 7px;
277 | text-align: center;
278 | top: 50%;
279 | }
280 |
281 | .react-datepicker__today-button {
282 | background: #f0f0f0;
283 | border-top: 1px solid #aeaeae;
284 | cursor: pointer;
285 | text-align: center;
286 | font-weight: bold;
287 | padding: 5px 0;
288 | }
289 |
290 | .react-datepicker__tether-element {
291 | z-index: 2147483647;
292 | }
293 |
--------------------------------------------------------------------------------