├── .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 |
6 | 9 |
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 | 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 |
16 |
17 | 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 |
    158 | 159 | this._onSimilarClick.call(this, artist.name)}>{ artist.name } 160 |
    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 | --------------------------------------------------------------------------------