├── .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 |  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 | 69 | -------------------------------------------------------------------------------- /assets/icons/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 69 | -------------------------------------------------------------------------------- /assets/icons/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 69 | -------------------------------------------------------------------------------- /assets/icons/pause-big.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 69 | -------------------------------------------------------------------------------- /assets/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 69 | -------------------------------------------------------------------------------- /assets/icons/play-big.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 69 | -------------------------------------------------------------------------------- /assets/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 69 | -------------------------------------------------------------------------------- /assets/icons/prev.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 69 | -------------------------------------------------------------------------------- /assets/icons/send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 69 | -------------------------------------------------------------------------------- /assets/icons/skype.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | 2 | .album 3 | 4 | section.main(:style='style', v-if='album') 5 | .container 6 | .cover-and-info 7 | .cover( 8 | v-if='album.metadata.cover', 9 | :style='{backgroundImage: `url("${album.metadata.cover.imgix_url}")`}' 10 | ) 11 | .info 12 | h2.title {{ album.title }} 13 | .release-date released {{ releaseDate }} 14 | playlist.playlist(:album='album') 15 | 16 | section.player(v-if='album', :style='style2') 17 | .container 18 | player(:album='album') 19 | 20 | section.breadcrumbs(v-if='album') 21 | .container 22 | nuxt-link.crumb(:to='{name: "index"}') Discography 23 | .separator 24 | nuxt-link.crumb(:to='{}') {{ album.title }} 25 | 26 | section.about-and-comments(v-if='album') 27 | .container 28 | .pane.about-and-form 29 | .pane-title About 30 | .about(v-html='album.content') 31 | comment-form(:album='album') 32 | .pane 33 | .pane-title {{ comments.length }} {{ 'comment' | pluralize(comments.length) }} 34 | comments(:comments='comments') 35 | 36 | 37 | 111 | 112 | 251 | -------------------------------------------------------------------------------- /components/Albums.vue: -------------------------------------------------------------------------------- 1 | 2 | .albums 3 | .empty-message(v-if='!albums || albums.length === 0') Empty 4 | router-link.album( 5 | v-for='album in albums', 6 | :key='album.slug', 7 | :to='{ name: "slug", params: { slug: album.slug } }' 8 | ) 9 | .cover(:style='album.metadata.cover ? {backgroundImage: `url("${album.metadata.cover.imgix_url}")`} : {}') 10 | .cd(v-if='!album.metadata.cover') 11 | .info 12 | .name {{ album.title }} 13 | .released {{ $moment(album.metadata.released_at).format('MMMM D, YYYY') }} 14 | .tracks-count(v-if='album.metadata.tracks') {{ album.metadata.tracks.length }} {{ 'track' | pluralize(album.metadata.tracks.length) }} 15 | 16 | 17 | 27 | 28 | 104 | -------------------------------------------------------------------------------- /components/Author.vue: -------------------------------------------------------------------------------- 1 | 2 | .author 3 | .empty-message(v-if='!author') Please, create an "Author" object at Cosmic JS dashboard and add some "Albums" to it. 4 | template(v-else) 5 | .photo( 6 | v-if='author.metadata.photo', 7 | :style='{backgroundImage: `url("${author.metadata.photo.imgix_url}")`}' 8 | ) 9 | .info 10 | h1.name {{ author.title }} 11 | .bio(v-html='author.content') 12 | 13 | 14 | 24 | 25 | 53 | -------------------------------------------------------------------------------- /components/CommentForm.vue: -------------------------------------------------------------------------------- 1 | 2 | .comment-form 3 | .input 4 | input(type='text', v-model='name', placeholder='Your name') 5 | .input(:class='{error}') 6 | textarea(v-model='comment', placeholder='Leave your comment', @keyup.enter='send') 7 | .send(@click='send'): svg-icon(name='send') 8 | transition(name='fade') 9 | .overlay(v-if='loading') 10 | .circle 11 | .circle 12 | .circle 13 | 14 | 15 | 79 | 80 | 159 | -------------------------------------------------------------------------------- /components/Comments.vue: -------------------------------------------------------------------------------- 1 | 2 | .comments 3 | transition-group(name='list', tag='div') 4 | .comment(v-for='comment in comments', :key='comment._id') 5 | .head 6 | .author {{ comment.title }} 7 | .date {{ $moment(comment.created_at).format('HH:mm MMMM D')}} 8 | .message {{ comment.content | striphtml }} 9 | 10 | 11 | 29 | 30 | 55 | -------------------------------------------------------------------------------- /components/Contacts.vue: -------------------------------------------------------------------------------- 1 | 2 | .contacts 3 | a.contact(v-if='author.metadata.email', :href='`mailto:${author.metadata.email}`') 4 | .icon: svg-icon(name='email') 5 | .text {{ author.metadata.email }} 6 | a.contact(v-if='author.metadata.skype', :href='`skype:${author.metadata.skype}`') 7 | .icon: svg-icon(name='skype') 8 | .text {{ author.metadata.skype }} 9 | 10 | 11 | 25 | 26 | 45 | -------------------------------------------------------------------------------- /components/CosmicLogo.vue: -------------------------------------------------------------------------------- 1 | 2 | a.cosmic-logo(href='https://cosmicjs.com') 3 | .icon: svg-icon(name='cosmic') 4 | .text 5 | .pre Proudly Powered by 6 | .brand Cosmic JS 7 | 8 | 9 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /components/Menubar.vue: -------------------------------------------------------------------------------- 1 | 2 | .menubar(:style='style') 3 | author 4 | section 5 | .title Discography 6 | albums 7 | section(v-if='author && (author.metadata.email || author.metadata.skype)') 8 | .title Contact me 9 | contacts 10 | section.last 11 | cosmic-logo 12 | 13 | 14 | 40 | 41 | 57 | -------------------------------------------------------------------------------- /components/Player.vue: -------------------------------------------------------------------------------- 1 | 2 | .player 3 | .title-and-controls 4 | .track-info 5 | .title {{ trackToDisplay.title }} 6 | .author by {{ author.title }} 7 | .controls 8 | button.control.prev(:class='{disabled: !ableToPrev}', @click='playPrev'): svg-icon(name='prev') 9 | button.control.toggle(@click='handleToggle') 10 | svg-icon(v-if='showIsPlaying', name='pauseBig') 11 | svg-icon(v-else, name='playBig') 12 | button.control.next(:class='{disabled: !ableToNext}', @click='pickNextOrStop'): svg-icon(name='next') 13 | .waveform-container 14 | .svg-container(ref='svg', @click.stop='handleSeek') 15 | transition(name='fade') 16 | svg.waveform( 17 | v-if='trackToDisplay.path', 18 | viewBox='0 -1 992 2', 19 | preserveAspectRatio='none' 20 | ) 21 | g 22 | path(:d='trackToDisplay.path') 23 | .dummy-line(v-else) 24 | .overlay( 25 | :style='{color: keyColor, transform: `translateX(-${100 - progressToShow}%)`}' 26 | ) 27 | svg.waveform( 28 | v-if='trackToDisplay.path', 29 | viewBox='0 -1 992 2', 30 | preserveAspectRatio='none', 31 | :style='{transform: `translateX(${100 - progressToShow}%) scaleY(48)`}' 32 | ) 33 | g 34 | path(:d='trackToDisplay.path') 35 | .dummy-line(v-else) 36 | .nav-dots 37 | .nav-dot( 38 | v-for='track in album.metadata.tracks', 39 | :style='{backgroundColor: track == trackToDisplay ? keyColor : "currentColor"}' 40 | ) 41 | 42 | 43 | 163 | 164 | 292 | -------------------------------------------------------------------------------- /components/Playlist.vue: -------------------------------------------------------------------------------- 1 | 2 | .playlist 3 | .track( 4 | v-for='track in tracks', 5 | :style='{color: (track == currentTrack && isPlaying) ? activeColor : "inherit"}', 6 | @click='handleClick(track)' 7 | ) 8 | .button 9 | button.pause(v-if='track == currentTrack && isPlaying'): svg-icon(name='pause') 10 | button.play(v-else): svg-icon(name='play') 11 | .title {{ track.title }} 12 | .duration(v-if='track.duration') {{ $moment.unix(track.duration).format('mm:ss') }} 13 | .duration.loading(v-else) 14 | 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 | 2 | svg(:viewBox="icon.viewBox", :width="icon.viewBox.split(' ')[2]", :height="icon.viewBox.split(' ')[3]") 3 | use(:xlink:href="'#'+icon.id") 4 | 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 | 2 | .app-layout(:class='layoutClass') 3 | menubar.menubar 4 | nuxt.page-content 5 | 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