├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── assets ├── README.md ├── icons │ ├── cosmic-js.svg │ ├── email.svg │ ├── next.svg │ ├── pause-big.svg │ ├── pause.svg │ ├── play-big.svg │ ├── play.svg │ ├── prev.svg │ ├── send.svg │ └── skype.svg └── sass │ └── responsive.sass ├── bucket.json ├── components ├── Album.vue ├── Albums.vue ├── Author.vue ├── CommentForm.vue ├── Comments.vue ├── Contacts.vue ├── CosmicLogo.vue ├── Menubar.vue ├── Player.vue ├── Playlist.vue ├── README.md └── SvgIcon.vue ├── instance.config.js ├── layouts ├── README.md └── default.vue ├── lib ├── audio-waveform-svg-path.js ├── cosmic.js └── rgbaster.js ├── middleware └── README.md ├── nuxt.config.js ├── package.json ├── pages ├── README.md ├── _slug.vue └── index.vue ├── plugins ├── README.md └── pluralize.js ├── screenshot.png ├── static ├── README.md └── favicon.ico ├── store ├── README.md ├── comments.js ├── index.js └── player.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | extends: [ 11 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 12 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 13 | 'plugin:vue/essential' 14 | ], 15 | // required to lint *.vue files 16 | plugins: [ 17 | 'vue' 18 | ], 19 | // add your custom rules here 20 | rules: {} 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # logs 5 | npm-debug.log 6 | 7 | # Nuxt build 8 | .nuxt 9 | 10 | # Nuxt generate 11 | dist 12 | 13 | # other 14 | .tern-port 15 | yarn-error.log 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue.js Music Website 2 | 3 | ![screenshot](screenshot.png) 4 | 5 | ### [View Demo](https://cosmicjs.com/apps/vue-music-website) 6 | 7 | Personal music site built with Nuxt and [Cosmic JS](https://cosmicjs.com/buckets). 8 | 9 | ## How to 10 | 11 | ### Prepare your Cosmic JS bucket 12 | 13 | Import example [bucket.json](bucket.json) to your Cosmic JS bucket. 14 | 15 | ### Clone this repository 16 | 17 | ``` bash 18 | git clone https://github.com/cosmicjs/vue-music-website 19 | cd vue-music-website 20 | ``` 21 | 22 | ### Configure 23 | 24 | Edit `instance.config.js` to adjust app colors if you need. 25 | 26 | You can see that Cosmic JS access credentials are picked up from the next environment variables: 27 | 28 | ``` 29 | COSMIC_BUCKET 30 | COSMIC_READ_KEY 31 | COSMIC_WRITE_KEY 32 | ``` 33 | 34 | ### Setup and run 35 | 36 | ``` bash 37 | # install dependencies 38 | $ npm install # Or yarn install 39 | 40 | # serve with hot reload at localhost:3000 41 | $ npm run dev 42 | 43 | # build for production and launch server 44 | $ npm start 45 | 46 | # generate static project 47 | $ npm run generate 48 | ``` 49 | 50 | This app is based on Nuxt.js. 51 | For detailed explanation on how things work, checkout the [Nuxt.js docs](https://github.com/nuxt/nuxt.js). 52 | 53 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 4 | 5 | More information about the usage of this directory in the documentation: 6 | https://nuxtjs.org/guide/assets#webpacked 7 | 8 | **This directory is not required, you can delete it if you don't want to use it.** 9 | 10 | -------------------------------------------------------------------------------- /assets/icons/cosmic-js.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | -------------------------------------------------------------------------------- /assets/icons/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | -------------------------------------------------------------------------------- /assets/icons/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | -------------------------------------------------------------------------------- /assets/icons/pause-big.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | -------------------------------------------------------------------------------- /assets/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | -------------------------------------------------------------------------------- /assets/icons/play-big.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | -------------------------------------------------------------------------------- /assets/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | -------------------------------------------------------------------------------- /assets/icons/prev.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | -------------------------------------------------------------------------------- /assets/icons/send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | -------------------------------------------------------------------------------- /assets/icons/skype.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 70 | 71 | -------------------------------------------------------------------------------- /assets/sass/responsive.sass: -------------------------------------------------------------------------------- 1 | @mixin mobile() 2 | @media screen and (max-width: 767px) 3 | @content 4 | -------------------------------------------------------------------------------- /bucket.json: -------------------------------------------------------------------------------- 1 | {"bucket":{"_id":"5b688a0b44c5603e643f6a66","slug":"vue-music-website","title":"Vue Music Website","object_types":[{"title":"Albums","slug":"albums","singular":"Album","metafields":[{"object_type":"tracks","key":"tracks","title":"Tracks","type":"objects","children":null},{"value":"","key":"cover","title":"Cover","type":"file","children":null},{"required":true,"value":"","key":"released_at","title":"Released at","type":"date","children":null}],"options":{"slug_field":1,"content_editor":1},"preview_link":"","priority_locale":null,"localization":false,"locales":null},{"title":"Tracks","slug":"tracks","singular":"Track","metafields":[{"required":true,"value":"","key":"audio","title":"audio","type":"file","children":null}],"options":{"slug_field":1,"content_editor":1},"preview_link":"","priority_locale":null},{"title":"Authors","slug":"authors","singular":"Author","metafields":[{"value":"","key":"photo","title":"Photo","type":"file","children":null},{"value":"","key":"email","title":"E-mail","type":"text","children":null},{"value":"","key":"skype","title":"Skype","type":"text","children":null},{"object_type":"albums","value":"5b5c99087509004a3673b281,5b5c98fdbe1cd13897e204b9","key":"albums","title":"Albums","type":"objects","children":null}],"options":{"slug_field":1,"content_editor":1},"preview_link":"","priority_locale":null},{"title":"Comments","slug":"comments","singular":"Comment","metafields":[{"required":true,"value":"","key":"album_id","title":"Album ID","type":"text","children":null}],"options":{"slug_field":0,"content_editor":1},"preview_link":"","priority_locale":null,"localization":false,"locales":null}],"links":[],"objects":[{"_id":"5b688e3744c5603e643f6ab9","slug":"pharrell","title":"Pharrell","content":"Great track!","metafields":[{"key":"album_id","type":"text","value":"5b688a165669b934036d93c6","title":"album_id"}],"bucket":"5b688a0b44c5603e643f6a66","type_slug":"comments","created":"2018-08-06T18:06:47.295Z","created_at":"2018-08-06T18:06:47.295Z","modified_at":"2018-08-06T18:06:47.295Z","user_id":null,"status":"published","locale":null,"published_at":"2018-08-06T18:06:47.295Z","options":{"slug_field":false}},{"_id":"5b688a165669b934036d93c4","old_id":"5b61fa834307cc553f8ac013","bucket":"5b688a0b44c5603e643f6a66","slug":"friendship","title":"Friendship","content":"","metafields":[{"required":true,"value":"e71b7410-95b7-11e8-93c9-0d7cfa50638b-dr.mp3","key":"audio","title":"audio","type":"file","children":null}],"type_slug":"tracks","created":"2018-08-01T18:22:59.870Z","created_at":"2018-08-01T18:22:59.870Z","modified_at":"2018-08-01T18:22:59.870Z","user_id":"5357ef811693be2118000001","options":{"slug_field":1,"content_editor":1},"order":null,"status":"published","published_at":"2018-08-06T17:49:10.533Z"},{"_id":"5b688a165669b934036d93c3","old_id":"5b61faee8ef5335514ccc351","bucket":"5b688a0b44c5603e643f6a66","slug":"delays","title":"Delays","content":"","metafields":[{"required":true,"value":"1c3a6430-95b8-11e8-9802-396fa6cd57fb-minimal.mp3","key":"audio","title":"audio","type":"file","children":null}],"type_slug":"tracks","created":"2018-08-01T18:24:46.004Z","created_at":"2018-08-01T18:24:46.004Z","modified_at":"2018-08-01T18:24:46.004Z","user_id":"5357ef811693be2118000001","options":{"slug_field":1,"content_editor":1},"order":null,"status":"published","published_at":"2018-08-06T17:49:10.532Z"},{"_id":"5b688a165669b934036d93c2","old_id":"5b61fb7b533fa555ca04c28c","bucket":"5b688a0b44c5603e643f6a66","slug":"knock-over","title":"Knock Over","content":"","metafields":[{"required":true,"value":"4b35ac90-95b8-11e8-9802-396fa6cd57fb-ggg46.mp3","key":"audio","title":"audio","type":"file","children":null}],"type_slug":"tracks","created":"2018-08-01T18:27:07.622Z","created_at":"2018-08-01T18:27:07.622Z","modified_at":"2018-08-01T18:27:07.622Z","user_id":"5357ef811693be2118000001","options":{"slug_field":1,"content_editor":1},"order":null,"status":"published","published_at":"2018-08-06T17:49:10.531Z"},{"_id":"5b688a165669b934036d93c1","old_id":"5b61fc244307cc553f8ac01b","bucket":"5b688a0b44c5603e643f6a66","slug":"4th","title":"4th","content":"","metafields":[{"required":true,"value":"abad9100-95b8-11e8-a2fb-7b36f69e10f5-db.mp3","key":"audio","title":"audio","type":"file","children":null}],"type_slug":"tracks","created":"2018-08-01T18:29:56.151Z","created_at":"2018-08-01T18:29:56.151Z","modified_at":"2018-08-01T18:29:56.151Z","user_id":"5357ef811693be2118000001","options":{"slug_field":1,"content_editor":1},"order":null,"status":"published","published_at":"2018-08-06T17:49:10.530Z"},{"_id":"5b688a165669b934036d93c0","old_id":"5b5f65907f7cce7bc3ed0f71","bucket":"5b688a0b44c5603e643f6a66","slug":"demo-track","title":"Demo Track","content":"","metafields":[{"required":true,"value":"dfb97320-93fa-11e8-b883-8db8e122a793-drums.mp3","key":"audio","title":"audio","type":"file","children":null}],"type_slug":"tracks","created":"2018-07-30T13:17:17.279Z","created_at":"2018-07-30T13:17:17.279Z","modified_at":"2018-07-30T13:17:17.279Z","user_id":"5357ef811693be2118000001","options":{"slug_field":1,"content_editor":1},"order":null,"status":"published","published_at":"2018-08-06T17:49:10.529Z"},{"_id":"5b688e6bdd4944454e3b638d","slug":"daft-punk","title":"Daft Punk","content":"Dig it!","metafields":[{"key":"album_id","type":"text","value":"5b688a165669b934036d93c7","title":"album_id"}],"bucket":"5b688a0b44c5603e643f6a66","type_slug":"comments","created":"2018-08-06T18:07:39.059Z","created_at":"2018-08-06T18:07:39.059Z","modified_at":"2018-08-06T18:07:39.059Z","user_id":null,"status":"published","locale":null,"published_at":"2018-08-06T18:07:39.059Z","options":{"slug_field":false}},{"_id":"5b688a165669b934036d93bd","old_id":"5b61fcc6533fa555ca04c28d","bucket":"5b688a0b44c5603e643f6a66","slug":"the-massive","title":"The Massive","content":"","metafields":[{"required":true,"value":"1c644870-95ba-11e8-b52d-e18492f8c75e-gate.mp3","key":"audio","title":"audio","type":"file","children":null}],"type_slug":"tracks","created":"2018-08-01T18:32:38.574Z","created_at":"2018-08-01T18:32:38.574Z","modified_at":"2018-08-01T18:38:43.587Z","user_id":"5357ef811693be2118000001","options":{"content_editor":1,"slug_field":1},"order":null,"status":"published","published_at":"2018-08-06T17:49:10.527Z"},{"_id":"5b688a165669b934036d93c5","old_id":"5b5f65907f7cce7bc3ed0f73","bucket":"5b688a0b44c5603e643f6a66","slug":"sergey","title":"Sergey","content":"

An electronic musician

","metafields":[{"value":"zezic51@yandex.ru","key":"email","title":"E-mail","type":"text","children":null},{"value":"polarphoks","key":"skype","title":"Skype","type":"text","children":null},{"object_type":"albums","value":"5b688a165669b934036d93c6,5b688a165669b934036d93c7","key":"albums","title":"Albums","type":"objects","children":null},{"value":"f88c6900-95b9-11e8-9802-396fa6cd57fb-CEiYQtjFgCU-200.jpg","key":"photo","title":"Photo","type":"file","children":null}],"type_slug":"authors","created":"2018-07-30T13:13:01.337Z","created_at":"2018-07-30T13:13:01.337Z","modified_at":"2018-08-01T18:37:45.585Z","user_id":"5357ef811693be2118000001","options":{"content_editor":1,"slug_field":1},"order":0,"status":"published","published_at":"2018-08-06T17:49:10.534Z"},{"_id":"5b688a165669b934036d93c6","old_id":"5b61fc4406e0ca55f5f9cbf1","bucket":"5b688a0b44c5603e643f6a66","slug":"extra-clean","title":"Extra Clean","content":"

This album was released August 1st for the Cosmic JS powered app demo.

","metafields":[{"object_type":"tracks","value":"5b688a165669b934036d93c4,5b688a165669b934036d93c3,5b688a165669b934036d93c2,5b688a165669b934036d93c1,5b688a165669b934036d93bd","key":"tracks","title":"Tracks","type":"objects","children":null},{"value":"f5072f50-95b8-11e8-a2fb-7b36f69e10f5-extra-clean.png","key":"cover","title":"Cover","type":"file","children":null},{"required":true,"value":"2018-08-01","key":"released_at","title":"Released at","type":"date","children":null}],"type_slug":"albums","created":"2018-08-01T18:30:28.011Z","created_at":"2018-08-01T18:30:28.011Z","modified_at":"2018-08-01T18:41:34.763Z","user_id":"5357ef811693be2118000001","options":{"content_editor":1,"slug_field":1},"order":0,"status":"published","published_at":"2018-08-06T17:49:10.535Z"},{"_id":"5b688a165669b934036d93c7","old_id":"5b5f65907f7cce7bc3ed0f74","bucket":"5b688a0b44c5603e643f6a66","slug":"example-album","title":"Example Album","content":"

This is an example album. You can edit it in your Cosmic JS bucket dashboard.

","metafields":[{"object_type":"tracks","value":"5b688a165669b934036d93c0","key":"tracks","title":"Tracks","type":"objects","children":null},{"value":"617fe980-93fa-11e8-b641-9ffb2bd9d07f-4TE3S0cs_yk.jpg","key":"cover","title":"Cover","type":"file","children":null},{"required":true,"value":"2018-07-01","key":"released_at","title":"Released at","type":"date","children":null}],"type_slug":"albums","created":"2018-07-30T13:12:27.669Z","created_at":"2018-07-30T13:12:27.669Z","modified_at":"2018-07-30T13:17:22.719Z","user_id":"5357ef811693be2118000001","options":{"content_editor":1,"slug_field":1},"order":1,"status":"published","published_at":"2018-08-06T17:49:10.536Z"}],"media":[{"_id":"5b688a165669b934036d93c8","name":"617fe980-93fa-11e8-b641-9ffb2bd9d07f-4TE3S0cs_yk.jpg","original_name":"4TE3S0cs_yk.jpg","size":52155,"type":"image/jpeg","bucket":"5b688a0b44c5603e643f6a66","created":"2018-07-30T13:13:42.310Z","location":"https://s3-us-west-2.amazonaws.com/cosmicjs","folder":null},{"_id":"5b688a165669b934036d93c9","name":"dfb97320-93fa-11e8-b883-8db8e122a793-drums.mp3","original_name":"drums.mp3","size":279430,"type":"audio/mpeg","bucket":"5b688a0b44c5603e643f6a66","created":"2018-07-30T13:17:14.062Z","location":"https://s3-us-west-2.amazonaws.com/cosmicjs","folder":null},{"_id":"5b688a165669b934036d93ca","name":"1c644870-95ba-11e8-b52d-e18492f8c75e-gate.mp3","original_name":"gate.mp3","size":2126171,"type":"audio/mpeg","bucket":"5b688a0b44c5603e643f6a66","created":"2018-08-01T18:38:41.080Z","location":"https://s3-us-west-2.amazonaws.com/cosmicjs","folder":null},{"_id":"5b688a165669b934036d93cb","name":"e71b7410-95b7-11e8-93c9-0d7cfa50638b-dr.mp3","original_name":"dr.mp3","size":1532866,"type":"audio/mpeg","bucket":"5b688a0b44c5603e643f6a66","created":"2018-08-01T18:22:52.712Z","location":"https://s3-us-west-2.amazonaws.com/cosmicjs","folder":null},{"_id":"5b688a165669b934036d93cc","name":"1c3a6430-95b8-11e8-9802-396fa6cd57fb-minimal.mp3","original_name":"minimal.mp3","size":2132997,"type":"audio/mpeg","bucket":"5b688a0b44c5603e643f6a66","created":"2018-08-01T18:24:21.824Z","location":"https://s3-us-west-2.amazonaws.com/cosmicjs","folder":null},{"_id":"5b688a165669b934036d93cd","name":"4b35ac90-95b8-11e8-9802-396fa6cd57fb-ggg46.mp3","original_name":"ggg46.mp3","size":2009630,"type":"audio/mpeg","bucket":"5b688a0b44c5603e643f6a66","created":"2018-08-01T18:25:40.566Z","location":"https://s3-us-west-2.amazonaws.com/cosmicjs","folder":null},{"_id":"5b688a165669b934036d93ce","name":"abad9100-95b8-11e8-a2fb-7b36f69e10f5-db.mp3","original_name":"db.mp3","size":2025140,"type":"audio/mpeg","bucket":"5b688a0b44c5603e643f6a66","created":"2018-08-01T18:28:22.425Z","location":"https://s3-us-west-2.amazonaws.com/cosmicjs","folder":null},{"_id":"5b688a165669b934036d93cf","name":"f5072f50-95b8-11e8-a2fb-7b36f69e10f5-extra-clean.png","original_name":"extra-clean.png","size":31061,"type":"image/png","bucket":"5b688a0b44c5603e643f6a66","created":"2018-08-01T18:30:25.309Z","location":"https://s3-us-west-2.amazonaws.com/cosmicjs","folder":null},{"_id":"5b688a165669b934036d93d0","name":"f88c6900-95b9-11e8-9802-396fa6cd57fb-CEiYQtjFgCU-200.jpg","original_name":"CEiYQtjFgCU-200.jpg","size":11099,"type":"image/jpeg","bucket":"5b688a0b44c5603e643f6a66","created":"2018-08-01T18:37:40.664Z","location":"https://s3-us-west-2.amazonaws.com/cosmicjs","folder":null}],"media_folders":[],"extensions":[],"thumbnail":"617fe980-93fa-11e8-b641-9ffb2bd9d07f-4TE3S0cs_yk.jpg"}} 2 | -------------------------------------------------------------------------------- /components/Album.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 111 | 112 | 251 | -------------------------------------------------------------------------------- /components/Albums.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 104 | -------------------------------------------------------------------------------- /components/Author.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | 53 | -------------------------------------------------------------------------------- /components/CommentForm.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 79 | 80 | 159 | -------------------------------------------------------------------------------- /components/Comments.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 29 | 30 | 55 | -------------------------------------------------------------------------------- /components/Contacts.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | 26 | 45 | -------------------------------------------------------------------------------- /components/CosmicLogo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /components/Menubar.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 40 | 41 | 57 | -------------------------------------------------------------------------------- /components/Player.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 163 | 164 | 292 | -------------------------------------------------------------------------------- /components/Playlist.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 77 | 78 | 125 | -------------------------------------------------------------------------------- /components/README.md: -------------------------------------------------------------------------------- 1 | # COMPONENTS 2 | 3 | The components directory contains your Vue.js Components. 4 | Nuxt.js doesn't supercharge these components. 5 | 6 | **This directory is not required, you can delete it if you don't want to use it.** 7 | 8 | -------------------------------------------------------------------------------- /components/SvgIcon.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 38 | 39 | 44 | -------------------------------------------------------------------------------- /instance.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | keyColorBG: '#5339F1', 3 | keyColorFG: '#FFFFFF', 4 | secondColorBG: '#1A1A1A', 5 | secondColorFG: '#FFFFFF', 6 | loadingColor: '#FFCC00', 7 | slug: process.env.COSMIC_BUCKET || 'music-site', 8 | read_key: process.env.COSMIC_READ_KEY || '', 9 | write_key: process.env.COSMIC_WRITE_KEY || '' 10 | } 11 | -------------------------------------------------------------------------------- /layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 2 | 3 | This directory contains your Application Layouts. 4 | 5 | More information about the usage of this directory in the documentation: 6 | https://nuxtjs.org/guide/views#layouts 7 | 8 | **This directory is not required, you can delete it if you don't want to use it.** 9 | 10 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | 22 | 52 | 53 | 64 | 65 | -------------------------------------------------------------------------------- /lib/audio-waveform-svg-path.js: -------------------------------------------------------------------------------- 1 | function parseArrayBufferResponse(response) { 2 | if (!response.ok) { 3 | throw new Error(`${response.status} (${response.statusText})`); 4 | } 5 | 6 | return response.arrayBuffer(); 7 | } 8 | 9 | export default class AudioSVGWaveform { 10 | constructor({url, buffer}) { 11 | this.url = url || null; 12 | this.audioBuffer = buffer || null; 13 | this.context = new AudioContext(); 14 | } 15 | 16 | _getPeaks(channelData, peaks, channelNumber) { 17 | const peaksCount = 992; 18 | const sampleSize = this.audioBuffer.length / peaksCount; 19 | const sampleStep = ~~(sampleSize / 10) || 1; 20 | const mergedPeaks = Array.isArray(peaks) ? peaks : []; 21 | 22 | for (let peakNumber = 0; peakNumber < peaksCount; peakNumber++) { 23 | const start = ~~(peakNumber * sampleSize); 24 | const end = ~~(start + sampleSize); 25 | let min = channelData[0]; 26 | let max = channelData[0]; 27 | 28 | for (let sampleIndex = start; sampleIndex < end; sampleIndex += sampleStep) { 29 | const value = channelData[sampleIndex]; 30 | 31 | if (value > max) { 32 | max = value; 33 | } 34 | if (value < min) { 35 | min = value; 36 | } 37 | } 38 | 39 | if (channelNumber === 0 || max > mergedPeaks[2 * peakNumber]) { 40 | mergedPeaks[2 * peakNumber] = max; 41 | } 42 | 43 | if (channelNumber === 0 || min < mergedPeaks[2 * peakNumber + 1]) { 44 | mergedPeaks[2 * peakNumber + 1] = min; 45 | } 46 | } 47 | 48 | return mergedPeaks; 49 | } 50 | /** 51 | * @return {String} path of SVG path element 52 | */ 53 | _svgPath(peaks) { 54 | const totalPeaks = peaks.length; 55 | 56 | let d = ''; 57 | // "for" is used for faster iteration 58 | for (let peakNumber = 0; peakNumber < totalPeaks; peakNumber++) { 59 | if (peakNumber % 2 === 0) { 60 | d += ` M${~~(peakNumber / 2)}, ${peaks.shift()}`; 61 | } else { 62 | d += ` L${~~(peakNumber / 2)}, ${peaks.shift()}`; 63 | } 64 | } 65 | return d; 66 | } 67 | 68 | async loadFromUrl() { 69 | if (!this.url) { 70 | return null; 71 | } 72 | 73 | const response = await fetch(this.url); 74 | const arrayBuffer = await parseArrayBufferResponse(response); 75 | 76 | this.audioBuffer = await this.context.decodeAudioData(arrayBuffer); 77 | 78 | return this.audioBuffer; 79 | } 80 | 81 | getPath(channelsPreprocess) { 82 | if (!this.audioBuffer) { 83 | console.log('No audio buffer to proccess'); 84 | return null; 85 | } 86 | 87 | const numberOfChannels = this.audioBuffer.numberOfChannels; 88 | let channels = []; 89 | 90 | for (let channelNumber = 0; channelNumber < numberOfChannels; channelNumber++) { 91 | channels.push(this.audioBuffer.getChannelData(channelNumber)); 92 | } 93 | 94 | if (typeof channelsPreprocess === 'function') { 95 | channels = channels.reduce(channelsPreprocess, []); 96 | } 97 | 98 | const peaks = channels.reduce( 99 | // change places of arguments in _getPeaks call 100 | (mergedPeaks, channelData, ...args) => this._getPeaks(channelData, mergedPeaks, ...args), [] 101 | ); 102 | 103 | return this._svgPath(peaks); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/cosmic.js: -------------------------------------------------------------------------------- 1 | import Cosmic from 'cosmicjs' 2 | const cosmic = Cosmic() 3 | 4 | const config = { 5 | slug: process.env.slug, 6 | read_key: process.env.read_key, 7 | write_key: process.env.write_key 8 | } 9 | 10 | export default function getBucket () { 11 | return cosmic.bucket(config) 12 | } 13 | -------------------------------------------------------------------------------- /lib/rgbaster.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Helper functions. 4 | var getContext = function(width, height){ 5 | var canvas = document.createElement("canvas"); 6 | canvas.setAttribute('width', width); 7 | canvas.setAttribute('height', height); 8 | return canvas.getContext('2d'); 9 | }; 10 | 11 | var getImageData = function(img, loaded){ 12 | 13 | var imgObj = new Image() 14 | imgObj.crossOrigin = "Anonymous" 15 | var imgSrc = img.src || img 16 | 17 | imgObj.onload = function() { 18 | var context = getContext(imgObj.width, imgObj.height); 19 | context.drawImage(imgObj, 0, 0); 20 | var imageData = context.getImageData(0, 0, imgObj.width, imgObj.height); 21 | if (loaded) { 22 | loaded(imageData.data); 23 | } 24 | } 25 | 26 | imgObj.src = imgSrc; 27 | if ( imgObj.complete || imgObj.complete === undefined ) { 28 | imgObj.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=="; 29 | imgObj.src = imgSrc; 30 | } 31 | }; 32 | 33 | var makeRGB = function(name){ 34 | return ['rgb(', name, ')'].join(''); 35 | }; 36 | 37 | var mapPalette = function(palette){ 38 | var arr = []; 39 | for (var prop in palette) { arr.push( frmtPobj(prop, palette[prop]) ) }; 40 | arr.sort(function(a, b) { return (b.count - a.count) }); 41 | return arr; 42 | }; 43 | 44 | var fitPalette = function(arr, fitSize) { 45 | if (arr.length > fitSize ) { 46 | return arr.slice(0,fitSize); 47 | } else { 48 | for (var i = arr.length-1 ; i < fitSize-1; i++) { arr.push( frmtPobj('0,0,0', 0) ) }; 49 | return arr; 50 | }; 51 | }; 52 | 53 | var frmtPobj = function(a,b){ 54 | return {name: makeRGB(a), count: b}; 55 | } 56 | 57 | 58 | // RGBaster Object 59 | // --------------- 60 | // 61 | var PALETTESIZE = 10; 62 | 63 | var RGBaster = {}; 64 | 65 | RGBaster.colors = function(img, opts){ 66 | 67 | opts = opts || {}; 68 | var exclude = opts.exclude || [ ], // for example, to exclude white and black: [ '0,0,0', '255,255,255' ] 69 | paletteSize = opts.paletteSize || PALETTESIZE; 70 | 71 | getImageData(img, function(data){ 72 | 73 | var colorCounts = {}, 74 | rgbString = '', 75 | rgb = [], 76 | colors = { 77 | dominant: { name: '', count: 0 }, 78 | palette: [] 79 | }; 80 | 81 | var i = 0; 82 | for (; i < data.length; i += 4) { 83 | rgb[0] = data[i]; 84 | rgb[1] = data[i+1]; 85 | rgb[2] = data[i+2]; 86 | rgbString = rgb.join(","); 87 | 88 | // skip undefined data and transparent pixels 89 | if (rgb.indexOf(undefined) !== -1 || data[i + 3] === 0) { 90 | continue; 91 | } 92 | 93 | // Ignore those colors in the exclude list. 94 | if ( exclude.indexOf( makeRGB(rgbString) ) === -1 ) { 95 | if ( rgbString in colorCounts ) { 96 | colorCounts[rgbString] = colorCounts[rgbString] + 1; 97 | } 98 | else{ 99 | colorCounts[rgbString] = 1; 100 | } 101 | } 102 | 103 | } 104 | 105 | if ( opts.success ) { 106 | var palette = fitPalette( mapPalette(colorCounts), paletteSize+1 ); 107 | opts.success({ 108 | dominant: palette[0].name, 109 | secondary: palette[1].name, 110 | palette: palette.map(function(c){ return c.name; }).slice(1) 111 | }); 112 | } 113 | }); 114 | }; 115 | 116 | export default RGBaster 117 | -------------------------------------------------------------------------------- /middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | This directory contains your Application Middleware. 4 | The middleware lets you define custom function to be ran before rendering a page or a group of pages (layouts). 5 | 6 | More information about the usage of this directory in the documentation: 7 | https://nuxtjs.org/guide/routing#middleware 8 | 9 | **This directory is not required, you can delete it if you don't want to use it.** 10 | 11 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | const instanceConfig = require('./instance.config.js') 2 | 3 | module.exports = { 4 | env: instanceConfig, 5 | /* 6 | ** Headers of the page 7 | */ 8 | head: { 9 | title: 'cosmicjs-music-site', 10 | meta: [ 11 | { charset: 'utf-8' }, 12 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 13 | { hid: 'description', name: 'description', content: 'Personal music site with Nuxt and Cosmic JS' } 14 | ], 15 | link: [ 16 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, 17 | { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Montserrat:500,600&subset=cyrillic' } 18 | ] 19 | }, 20 | /* 21 | ** Customize the progress bar color 22 | */ 23 | loading: { color: instanceConfig.loadingColor }, 24 | modules: [ '@nuxtjs/moment' ], 25 | plugins: [ '~/plugins/pluralize' ], 26 | /* 27 | ** Build configuration 28 | */ 29 | build: { 30 | /* 31 | ** Run ESLint on save 32 | */ 33 | extend (config, { isDev, isClient }) { 34 | if (isDev && isClient) { 35 | config.module.rules.push({ 36 | enforce: 'pre', 37 | test: /\.(js|vue)$/, 38 | loader: 'eslint-loader', 39 | exclude: /(node_modules)/ 40 | }) 41 | } 42 | 43 | const urlLoader = config.module.rules.find((rule) => rule.loader === 'url-loader') 44 | urlLoader.test = /\.(png|jpe?g|gif)$/ 45 | 46 | config.module.rules.push({ 47 | test: /\.svg$/, 48 | use: [ 49 | { loader: 'svg-sprite-loader'}, 50 | { 51 | loader: 'svgo-loader', 52 | options: { 53 | plugins: [ 54 | {removeTitle: true}, 55 | {convertColors: {shorthex: false, currentColor: true}}, 56 | {convertPathData: false}, 57 | {removeDimensions: false} 58 | ] 59 | } 60 | } 61 | ] 62 | }) 63 | } 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cosmicjs-music-site", 3 | "version": "1.0.0", 4 | "description": "Personal music site with Nuxt and Cosmic JS", 5 | "author": "Sergey Ukolov ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "nuxt", 9 | "build": "nuxt build", 10 | "start": "npm run build; HOST=0.0.0.0 nuxt start", 11 | "generate": "nuxt generate", 12 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", 13 | "precommit": "npm run lint" 14 | }, 15 | "dependencies": { 16 | "@nuxtjs/moment": "^1.0.0", 17 | "babel-preset-es2015": "^6.24.1", 18 | "chroma-js": "^1.3.7", 19 | "cosmicjs": "^3.2.12", 20 | "moment": "^2.22.2", 21 | "node-sass": "^4.9.2", 22 | "nuxt": "^1.0.0", 23 | "pug": "^2.0.3", 24 | "pug-plain-loader": "^1.0.0", 25 | "sass-loader": "^7.0.3", 26 | "striptags": "^3.1.1", 27 | "svg-sprite-loader": "^3.8.0", 28 | "svgo": "^1.0.0", 29 | "svgo-loader": "^2.1.0" 30 | }, 31 | "devDependencies": { 32 | "babel-eslint": "^8.2.1", 33 | "eslint": "^4.15.0", 34 | "eslint-friendly-formatter": "^3.0.0", 35 | "eslint-loader": "^1.7.1", 36 | "eslint-plugin-vue": "^4.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pages/README.md: -------------------------------------------------------------------------------- 1 | # PAGES 2 | 3 | This directory contains your Application Views and Routes. 4 | The framework reads all the .vue files inside this directory and creates the router of your application. 5 | 6 | More information about the usage of this directory in the documentation: 7 | https://nuxtjs.org/guide/routing 8 | 9 | -------------------------------------------------------------------------------- /pages/_slug.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 45 | 46 | 48 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # PLUGINS 2 | 3 | This directory contains your Javascript plugins that you want to run before instantiating the root vue.js application. 4 | 5 | More information about the usage of this directory in the documentation: 6 | https://nuxtjs.org/guide/plugins 7 | 8 | **This directory is not required, you can delete it if you don't want to use it.** 9 | 10 | -------------------------------------------------------------------------------- /plugins/pluralize.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | Vue.filter('pluralize', (word, amount) => amount > 1 ? `${word}s` : word) 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/vue-music-website/0b882128ded75c8d6024b7c69948d7b695375937/screenshot.png -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | This directory contains your static files. 4 | Each file inside this directory is mapped to /. 5 | 6 | Example: /static/robots.txt is mapped as /robots.txt. 7 | 8 | More information about the usage of this directory in the documentation: 9 | https://nuxtjs.org/guide/assets#static 10 | 11 | **This directory is not required, you can delete it if you don't want to use it.** 12 | 13 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/vue-music-website/0b882128ded75c8d6024b7c69948d7b695375937/static/favicon.ico -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | This directory contains your Vuex Store files. 4 | Vuex Store option is implemented in the Nuxt.js framework. 5 | Creating a index.js file in this directory activate the option in the framework automatically. 6 | 7 | More information about the usage of this directory in the documentation: 8 | https://nuxtjs.org/guide/vuex-store 9 | 10 | **This directory is not required, you can delete it if you don't want to use it.** 11 | 12 | -------------------------------------------------------------------------------- /store/comments.js: -------------------------------------------------------------------------------- 1 | import getBucket from '~/lib/cosmic' 2 | 3 | export const state = () => ({ 4 | collections: [] 5 | }) 6 | 7 | export const mutations = { 8 | setComments (state, { albumId, comments }) { 9 | const col = state.collections.find(col => col.albumId === albumId) 10 | if (!col) { 11 | state.collections.push({ albumId, comments }) 12 | } else { 13 | col.comments = comments 14 | } 15 | }, 16 | addComment (state, { albumId, comment }) { 17 | const col = state.collections.find(col => col.albumId === albumId) 18 | if (!col) { 19 | state.collections.push({ albumId, comments: [comment] }) 20 | } else { 21 | col.comments.unshift(comment) 22 | } 23 | } 24 | } 25 | 26 | export const actions = { 27 | async fetchComments ({ commit }, albumId) { 28 | const bucket = await getBucket() 29 | const response = await bucket.getObjects({ 30 | type: 'comments', 31 | hide_metafields: true, 32 | metafield_key: 'album_id', 33 | metafield_value: albumId 34 | }) 35 | const comments = response.objects 36 | commit('setComments', { albumId, comments }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import getBucket from '~/lib/cosmic' 3 | 4 | export const state = () => ({ 5 | author: null 6 | }) 7 | 8 | export const getters = { 9 | albums: (state) => state.author ? state.author.metadata.albums : [] 10 | } 11 | 12 | export const mutations = { 13 | setAuthor (state, author) { 14 | state.author = author 15 | }, 16 | setTrackDuration (state, { albumId, trackId, duration }) { 17 | const album = state.author.metadata.albums.find(album => album._id === albumId) 18 | const track = album.metadata.tracks.find(track => track._id === trackId) 19 | Vue.set(track, 'duration', duration) 20 | }, 21 | setTrackPath (state, { albumId, trackId, path }) { 22 | const album = state.author.metadata.albums.find(album => album._id === albumId) 23 | const track = album.metadata.tracks.find(track => track._id === trackId) 24 | Vue.set(track, 'path', path) 25 | }, 26 | setAlbumColor (state, { albumId, color }) { 27 | const album = state.author.metadata.albums.find(album => album._id === albumId) 28 | Vue.set(album, 'color', color) 29 | } 30 | } 31 | 32 | export const actions = { 33 | async nuxtServerInit ({ commit }) { 34 | const bucket = await getBucket() 35 | const response = await bucket.getObjects({ 36 | type: 'authors', 37 | limit: 1, 38 | hide_metafields: true 39 | }) 40 | if (response.status !== 'empty') { 41 | const author = response.objects[0] 42 | commit('setAuthor', author) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /store/player.js: -------------------------------------------------------------------------------- 1 | class Player { 2 | constructor () { 3 | this.audio = null 4 | } 5 | pause () { 6 | this.audio.pause() 7 | } 8 | continue () { 9 | this.audio.play() 10 | } 11 | seek (ratio) { 12 | if (this.audio) { 13 | this.audio.currentTime = this.audio.duration * ratio 14 | } 15 | } 16 | play (track, onEnded, onTimeUpdate) { 17 | if (this.audio && !this.audio.paused) { 18 | this.audio.pause() 19 | } 20 | delete this.audio 21 | onTimeUpdate({target: {currentTime: 0, duration: 1}}) 22 | const url = track.metadata.audio.url 23 | this.audio = new Audio(url) 24 | this.audio.play() 25 | this.audio.addEventListener('ended', onEnded) 26 | this.audio.addEventListener('timeupdate', onTimeUpdate) 27 | } 28 | } 29 | 30 | const player = new Player() 31 | 32 | export const state = () => ({ 33 | playlist: [], 34 | nextTrack: {}, 35 | currentTrack: {}, 36 | progress: 0, 37 | isPlaying: false 38 | }) 39 | 40 | export const mutations = { 41 | setPlaylist (state, playlist) { 42 | state.playlist = playlist 43 | }, 44 | setNextTrack (state, track) { 45 | state.nextTrack = track 46 | }, 47 | setCurrentTrack (state, track) { 48 | state.currentTrack = track 49 | }, 50 | setIsPlaying (state, isPlaying) { 51 | state.isPlaying = isPlaying 52 | }, 53 | setProgress (state, e) { 54 | state.progress = e.target.currentTime / (e.target.duration / 100) 55 | } 56 | } 57 | 58 | export const getters = { 59 | playlistHasNext (state) { 60 | const idx = state.playlist.indexOf(state.currentTrack) 61 | if (idx === -1 || idx === state.playlist.length - 1) { 62 | return false 63 | } 64 | return true 65 | }, 66 | playlistHasPrev (state) { 67 | const idx = state.playlist.indexOf(state.currentTrack) 68 | if (idx === -1 || idx === 0) { 69 | return false 70 | } 71 | return true 72 | } 73 | } 74 | 75 | export const actions = { 76 | pause ({ commit }) { 77 | commit('setIsPlaying', false) 78 | player.pause() 79 | }, 80 | continue ({ commit }) { 81 | commit('setIsPlaying', true) 82 | player.continue() 83 | }, 84 | playNextTrack ({ commit, dispatch, state }) { 85 | commit('setCurrentTrack', state.nextTrack) 86 | player.play( 87 | state.nextTrack, 88 | () => dispatch('pickNextOrStop'), 89 | (e) => commit('setProgress', e) 90 | ) 91 | commit('setIsPlaying', true) 92 | }, 93 | pickNextOrStop ({ state, commit, dispatch }) { 94 | const idx = state.playlist.indexOf(state.currentTrack) 95 | if (idx !== -1 && idx !== state.playlist.length - 1) { 96 | commit('setNextTrack', state.playlist[idx + 1]) 97 | dispatch('playNextTrack') 98 | } else { 99 | commit('setIsPlaying', false) 100 | } 101 | }, 102 | playPrev ({ commit, dispatch, state }) { 103 | const idx = state.playlist.indexOf(state.currentTrack) 104 | if (idx !== -1) { 105 | commit('setNextTrack', state.playlist[idx - 1]) 106 | dispatch('playNextTrack') 107 | } 108 | }, 109 | toggle ({ state, dispatch }) { 110 | if (state.isPlaying) { 111 | dispatch('pause') 112 | } else { 113 | dispatch('continue') 114 | } 115 | }, 116 | seek({ commit }, ratio) { 117 | player.seek(ratio) 118 | } 119 | } 120 | --------------------------------------------------------------------------------