├── .gitattributes ├── client ├── src │ ├── app │ │ ├── root.html │ │ ├── components │ │ │ ├── topfive │ │ │ │ ├── topfive.scss │ │ │ │ ├── topfive.module.js │ │ │ │ ├── topfive.component.spec.js │ │ │ │ ├── topfive.component.js │ │ │ │ └── dataMock.js │ │ │ ├── home │ │ │ │ ├── aside │ │ │ │ │ ├── fav-aside.scss │ │ │ │ │ ├── fav-aside.module.js │ │ │ │ │ ├── fav-aside.component.js │ │ │ │ │ └── fav-aside.component.spec.js │ │ │ │ ├── lyrics │ │ │ │ │ ├── formatting-text.filter.js │ │ │ │ │ ├── lyrics.module.js │ │ │ │ │ ├── lyrics.component.js │ │ │ │ │ ├── formatting-text.spec.js │ │ │ │ │ ├── lyrics.scss │ │ │ │ │ ├── lyrics.html │ │ │ │ │ └── lyrics.component.spec.js │ │ │ │ ├── search-form │ │ │ │ │ ├── search-form.module.js │ │ │ │ │ ├── search-from.component.spec.js │ │ │ │ │ ├── search-form.scss │ │ │ │ │ └── search-form.component.js │ │ │ │ ├── home.scss │ │ │ │ ├── home.html │ │ │ │ ├── home.module.js │ │ │ │ ├── home.component.js │ │ │ │ ├── dataMock.js │ │ │ │ └── home.component.spec.js │ │ │ ├── components.module.js │ │ │ └── favorites │ │ │ │ ├── favorites-page.html │ │ │ │ ├── favorites-page.component.js │ │ │ │ ├── favorites.module.js │ │ │ │ ├── favorites.service.js │ │ │ │ ├── favorites.scss │ │ │ │ ├── favorites.component.js │ │ │ │ ├── favorites.service.spec.js │ │ │ │ ├── favorites.component.spec.js │ │ │ │ ├── favorites-page.component.spec.js │ │ │ │ └── dataMock.js │ │ ├── root.scss │ │ ├── common │ │ │ ├── app.scss │ │ │ ├── app-footer │ │ │ │ ├── app-footer.component.js │ │ │ │ ├── app-footer.module.js │ │ │ │ ├── app-footer.html │ │ │ │ └── app-footer.scss │ │ │ ├── app.component.js │ │ │ ├── app-nav │ │ │ │ ├── app-nav.module.js │ │ │ │ ├── app-nav.component.js │ │ │ │ ├── app-nav.html │ │ │ │ └── app-nav.scss │ │ │ ├── services │ │ │ │ ├── services.module.js │ │ │ │ ├── vagalume-request.service.js │ │ │ │ ├── vagalume-request.service.spec.js │ │ │ │ └── dataMock.js │ │ │ ├── common.module.js │ │ │ ├── app.module.js │ │ │ └── app.spec.js │ │ ├── root.component.js │ │ └── root.module.js │ ├── img │ │ └── favicon.ico │ ├── styles │ │ ├── _base.scss │ │ ├── _variables.scss │ │ └── _components.scss │ └── index.html ├── postcss.config.js ├── dist │ ├── img │ │ └── favicon.ico │ ├── css │ │ ├── styles.css.map │ │ └── styles.css │ └── index.html ├── .travis.yml ├── docker-compose.yml ├── Dockerfile ├── index.js ├── karma.conf.js ├── package.json └── webpack.config.js ├── deploy.sh ├── .gitignore ├── README.md └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /client/src/app/root.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /client/src/app/components/topfive/topfive.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/app/root.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "base"; 3 | @import "components"; -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | } -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | cd client/ && npm run build && cd .. && git subtree push --prefix client/dist origin gh-pages 2 | -------------------------------------------------------------------------------- /client/src/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/my-lyrics-finder/master/client/src/img/favicon.ico -------------------------------------------------------------------------------- /client/dist/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delete/my-lyrics-finder/master/client/dist/img/favicon.ico -------------------------------------------------------------------------------- /client/src/app/common/app.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | display: flex; 3 | min-height: 100vh; 4 | flex-direction: column; 5 | } -------------------------------------------------------------------------------- /client/dist/css/styles.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"css/styles.css","sourceRoot":""} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | client/node_modules 2 | server/node_modules 3 | /dist 4 | /.tmp 5 | /.sass-cache 6 | client/app/bower_components 7 | *.log 8 | -------------------------------------------------------------------------------- /client/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '6.5' 5 | - '7' 6 | before_script: 7 | - 'npm install' 8 | -------------------------------------------------------------------------------- /client/src/app/root.component.js: -------------------------------------------------------------------------------- 1 | import templateUrl from './root.html'; 2 | 3 | export const rootComponent = { 4 | templateUrl, 5 | }; 6 | -------------------------------------------------------------------------------- /client/src/app/common/app-footer/app-footer.component.js: -------------------------------------------------------------------------------- 1 | import templateUrl from './app-footer.html'; 2 | 3 | export const footerComponent = { 4 | templateUrl 5 | }; 6 | -------------------------------------------------------------------------------- /client/src/app/components/home/aside/fav-aside.scss: -------------------------------------------------------------------------------- 1 | .aside { 2 | &__title { 3 | text-align: center; 4 | margin-top: 50px; 5 | } 6 | 7 | &__icon { 8 | color: #ce3e3e; 9 | } 10 | } -------------------------------------------------------------------------------- /client/src/app/common/app.component.js: -------------------------------------------------------------------------------- 1 | export const appComponent = { 2 | template: ` 3 | 4 |
5 | 6 | ` 7 | }; 8 | -------------------------------------------------------------------------------- /client/docker-compose.yml: -------------------------------------------------------------------------------- 1 | web: 2 | container_name: my-lyric-client 3 | build: . 4 | command: node server.js 5 | volumes: 6 | - .:/app 7 | ports: 8 | - "8081:8081" 9 | environment: 10 | PORT: 8081 11 | -------------------------------------------------------------------------------- /client/src/app/common/app-nav/app-nav.module.js: -------------------------------------------------------------------------------- 1 | import { navComponent } from './app-nav.component'; 2 | import './app-nav.scss'; 3 | 4 | export const appNav = angular 5 | .module('common.app-nav', []) 6 | .component('appNav', navComponent) 7 | .name; 8 | -------------------------------------------------------------------------------- /client/src/app/common/app-footer/app-footer.module.js: -------------------------------------------------------------------------------- 1 | import { footerComponent } from './app-footer.component'; 2 | import './app-footer.scss'; 3 | 4 | export const appFooter = angular 5 | .module('common.app-footer', []) 6 | .component('appFooter', footerComponent) 7 | .name; 8 | -------------------------------------------------------------------------------- /client/src/app/components/home/aside/fav-aside.module.js: -------------------------------------------------------------------------------- 1 | import { favAsideComponent } from './fav-aside.component'; 2 | import './fav-aside.scss'; 3 | 4 | export const favAside = angular 5 | .module('components.home.aside', []) 6 | .component('favAside', favAsideComponent) 7 | .name; 8 | -------------------------------------------------------------------------------- /client/src/app/components/home/lyrics/formatting-text.filter.js: -------------------------------------------------------------------------------- 1 | export function FormattingText($sce) { 2 | 'ngInject' 3 | return function(input) { 4 | if (input) { 5 | input = input.split("\n").join("
"); 6 | } 7 | return $sce.trustAsHtml(input); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /client/src/app/common/app-nav/app-nav.component.js: -------------------------------------------------------------------------------- 1 | import templateUrl from './app-nav.html'; 2 | 3 | export const navComponent = { 4 | templateUrl, 5 | controller: class NavComponent { 6 | constructor($state){ 7 | 'ngInject'; 8 | this.state = $state; 9 | } 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /client/src/app/common/app-footer/app-footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:argon 2 | 3 | RUN mkdir -p /install 4 | 5 | WORKDIR /install 6 | 7 | COPY package.json /install/ 8 | 9 | ENV NODE_PATH=/install 10 | 11 | RUN npm install 12 | 13 | ENV NODE_PATH=/install/node_modules 14 | 15 | WORKDIR /app/ 16 | 17 | COPY . /app/ 18 | 19 | EXPOSE 8081 20 | -------------------------------------------------------------------------------- /client/src/app/components/home/search-form/search-form.module.js: -------------------------------------------------------------------------------- 1 | import { searchFormComponent } from './search-form.component'; 2 | 3 | import './search-form.scss'; 4 | 5 | export const searchForm = angular 6 | .module('components.home.searchForm', []) 7 | .component('searchForm', searchFormComponent) 8 | .name; 9 | -------------------------------------------------------------------------------- /client/src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | } 6 | 7 | body { 8 | font: 300 14px/1.4 'Helvetica Neue', Helvetica, Arial, sans-serif; 9 | display: flex; 10 | min-height: 100vh; 11 | flex-direction: column; 12 | background-color: $bg-color; 13 | } 14 | 15 | ul { 16 | list-style: none; 17 | } -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const path = require('path'); 5 | 6 | const app = express(); 7 | const rootPath = path.normalize(__dirname); 8 | 9 | app.use(express.static(rootPath + '/dist')); 10 | 11 | const port = 8081; 12 | 13 | app.listen(port, (err) => console.log('Client listening on 8081')); 14 | -------------------------------------------------------------------------------- /client/src/app/components/components.module.js: -------------------------------------------------------------------------------- 1 | import { favorites } from './favorites/favorites.module'; 2 | import { home } from './home/home.module'; 3 | import { topfive } from './topfive/topfive.module'; 4 | 5 | export const components = angular 6 | .module('components', [ 7 | home, 8 | favorites, 9 | topfive 10 | ]) 11 | .name; 12 | -------------------------------------------------------------------------------- /client/src/app/common/services/services.module.js: -------------------------------------------------------------------------------- 1 | import ngResource from 'angular-resource'; 2 | 3 | import { VagalumeRequestService } from './vagalume-request.service.js'; 4 | 5 | 6 | export const services = angular 7 | .module('common.services', [ 8 | ngResource 9 | ]) 10 | .service('VagalumeRequestService', VagalumeRequestService) 11 | .name; 12 | -------------------------------------------------------------------------------- /client/src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | $deep-koama: #363368; 3 | $moody-blue: #776ECE; 4 | $cold-purple: #9D81CE; 5 | $biloba-flower: #AF91CF; 6 | $hawkes-blue: #D8DAE5; 7 | $ghost-white: #F4F4F8; 8 | $bg-color: #f9f9f9; 9 | 10 | 11 | // Animations 12 | @keyframes flyin { 13 | from { 14 | transform: translateY(100%); 15 | opacity:0; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/app/components/home/lyrics/lyrics.module.js: -------------------------------------------------------------------------------- 1 | import { lyricsComponent } from './lyrics.component'; 2 | import { FormattingText } from './formatting-text.filter'; 3 | 4 | import './lyrics.scss'; 5 | 6 | export const lyrics = angular 7 | .module('components.home.lyrics', []) 8 | .component('lyrics', lyricsComponent) 9 | .filter('FormattingText', FormattingText) 10 | .name; 11 | -------------------------------------------------------------------------------- /client/src/app/common/common.module.js: -------------------------------------------------------------------------------- 1 | import { app } from './app.module'; 2 | 3 | import { appNav } from './app-nav/app-nav.module'; 4 | import { appFooter } from './app-footer/app-footer.module'; 5 | import { services } from './services/services.module'; 6 | 7 | 8 | export const common = angular 9 | .module('common', [ 10 | app, 11 | appNav, 12 | appFooter, 13 | services 14 | ]) 15 | .name; 16 | -------------------------------------------------------------------------------- /client/src/app/common/app-footer/app-footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | border-top: 1px solid #e5e5e5; 4 | padding-bottom: 10px; 5 | margin-top: 100px; 6 | 7 | i { 8 | margin-top: 10px; 9 | } 10 | 11 | &__text { 12 | color: #b6b6b6; 13 | font-size: 12px; 14 | } 15 | &__icon { 16 | font-size: 15px !important; 17 | 18 | font-size: 20px; 19 | &-heart { 20 | color: #ce3e3e; 21 | } 22 | 23 | &-headset { 24 | color: $moody-blue; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /client/src/app/components/home/home.scss: -------------------------------------------------------------------------------- 1 | .home { 2 | display:flex; 3 | justify-content: space-around; 4 | flex-wrap: wrap; 5 | 6 | @media screen and (min-width: 1200px) { 7 | max-width: 80%; 8 | margin: 0 auto; 9 | } 10 | 11 | &__top { 12 | min-width: 310px; 13 | display: initial; 14 | } 15 | 16 | &__center { 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | } 21 | 22 | &__form { 23 | margin-bottom: 50px; 24 | } 25 | } -------------------------------------------------------------------------------- /client/src/app/common/app.module.js: -------------------------------------------------------------------------------- 1 | import uiRouter from 'angular-ui-router'; 2 | 3 | import './app.scss'; 4 | 5 | import { appComponent } from './app.component'; 6 | 7 | export const app = angular 8 | .module('common.app', [ 9 | uiRouter 10 | ]) 11 | .component('app', appComponent) 12 | .config(configRoute) 13 | .name; 14 | 15 | function configRoute($stateProvider) { 16 | 'ngInject'; 17 | 18 | $stateProvider 19 | .state('app', { 20 | redirectTo: 'home', 21 | template: '', 22 | }); 23 | } -------------------------------------------------------------------------------- /client/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My Lyrics Finder 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My Lyrics Finder 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /client/src/app/components/home/home.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 7 | 8 |
9 | 10 |
11 | 14 | 15 | 16 |
17 |
18 | 19 |
20 | 23 | 24 |
25 | 26 |
-------------------------------------------------------------------------------- /client/src/app/components/home/lyrics/lyrics.component.js: -------------------------------------------------------------------------------- 1 | import templateUrl from './lyrics.html'; 2 | 3 | export const lyricsComponent = { 4 | bindings: { 5 | myLyrics: '<', 6 | onClick: '&' 7 | }, 8 | templateUrl, 9 | controller: class LyricsComponent { 10 | constructor(FavoritesService){ 11 | 'ngInject'; 12 | this._FavoritesService = FavoritesService; 13 | } 14 | 15 | isFavorite(lyrics) { 16 | return this._FavoritesService.isFavorite(lyrics); 17 | } 18 | 19 | clickButton(lyrics) { 20 | this.onClick({ 21 | $event: { 22 | lyrics: lyrics 23 | } 24 | }); 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /client/src/app/root.module.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import uiRouter from 'angular-ui-router'; 3 | 4 | import { rootComponent } from './root.component'; 5 | import { common } from './common/common.module'; 6 | import { components } from './components/components.module'; 7 | 8 | import './root.scss'; 9 | 10 | 11 | export const root = angular 12 | .module('root', [ 13 | uiRouter, 14 | common, 15 | components 16 | ]) 17 | .component('root', rootComponent) 18 | .config(configRoute) 19 | .name; 20 | 21 | function configRoute($locationProvider, $urlRouterProvider) { 22 | 'ngInject'; 23 | 24 | $locationProvider.html5Mode({ 25 | enabled: true, 26 | requireBase: false 27 | }); 28 | $urlRouterProvider.otherwise('/'); 29 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # My lyrics finder 2 | A single page application powered by MEAN (es6) stack, to access the [Vagalume's API](http://api.vagalume.com.br/) 3 | and provides a way to search yours loved songs' lyrics and save them as favorites. 4 | 5 | ======= 6 | 7 | # Running 8 | 9 | * Clone the project: 10 | ``` 11 | git clone https://github.com/delete/my-lyrics-finder.git 12 | ``` 13 | 14 | * Enter the root directory: 15 | 16 | ``` 17 | cd my-lyrics-finder 18 | ``` 19 | 20 | ## Client side 21 | 22 | ### Developer 23 | 24 | `npm install && npm start` 25 | 26 | 27 | ### Running in containers with Docker 28 | 29 | * Building: 30 | ``` 31 | $ docker-compose build && docker-compose up 32 | ``` 33 | 34 | * Accessing: 35 | 36 | ``` 37 | http://YOU_LOCAL_IP/ 38 | ``` 39 | 40 | 41 | -------------------------------------------------------------------------------- /client/src/app/components/home/lyrics/formatting-text.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { FormattingText } from './formatting-text.filter'; 3 | 4 | const fakeSce = { 5 | trustAsHtml: txt => txt 6 | } 7 | 8 | const formattingText = FormattingText(fakeSce); 9 | 10 | describe('Filter: FormattingText', () => { 11 | 12 | it('should change "\\n" for
tag from input text', () => { 13 | let expected = 'Testing
this
for
sure!'; 14 | let text = 'Testing\nthis\nfor\nsure!'; 15 | let actual = formattingText(text); 16 | 17 | expect(actual).toBe(expected); 18 | }); 19 | 20 | it('should return undefined if nothing was given', () => { 21 | let actual = formattingText(); 22 | 23 | expect(actual).toBeUndefined(); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /client/src/app/components/favorites/favorites-page.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

My Favorites

5 |
6 | 12 | 13 |
14 | 15 |
16 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /client/src/app/components/favorites/favorites-page.component.js: -------------------------------------------------------------------------------- 1 | import templateUrl from './favorites-page.html'; 2 | 3 | export const favoritesPageComponent = { 4 | templateUrl, 5 | controller: class FavoritesPageComponent { 6 | constructor(VagalumeRequestService, FavoritesService) { 7 | 'ngInject'; 8 | 9 | this.vagalumeFavorite = FavoritesService; 10 | this.vagalume = VagalumeRequestService; 11 | } 12 | 13 | hasFavorites() { 14 | return this.vagalumeFavorite.hasFavorites(); 15 | } 16 | 17 | clearFavorites() { 18 | this.vagalumeFavorite.clearFavorites(); 19 | } 20 | 21 | delFavorite(event) { 22 | this.vagalumeFavorite.delFavorite(event.lyrics); 23 | } 24 | 25 | showLyrics(lyrics){ 26 | this.vagalume.lyrics = lyrics; 27 | } 28 | 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/app/components/topfive/topfive.module.js: -------------------------------------------------------------------------------- 1 | import uiRouter from 'angular-ui-router'; 2 | 3 | import { app } from '../../common/app.module'; 4 | 5 | import { topfiveComponent } from './topfive.component'; 6 | 7 | 8 | export const topfive = angular 9 | .module('components.topfive', [ 10 | uiRouter, 11 | app 12 | ]) 13 | .component('topfive', topfiveComponent) 14 | .config(configRoute) 15 | .name; 16 | 17 | function configRoute($stateProvider) { 18 | 'ngInject'; 19 | 20 | $stateProvider 21 | .state('topfive', { 22 | parent: 'app', 23 | url: '/topfive', 24 | component: 'topfive', 25 | resolve: { 26 | lyricsOnRank: (VagalumeRequestService) => { 27 | return VagalumeRequestService.rank() 28 | .$promise.then( data => data.mus.day.all ); 29 | } 30 | } 31 | }); 32 | } -------------------------------------------------------------------------------- /client/src/app/components/topfive/topfive.component.spec.js: -------------------------------------------------------------------------------- 1 | import { top5Lyrics } from './dataMock'; 2 | 3 | describe('Component: Topfive', () => { 4 | beforeEach(() => { 5 | angular.mock.module('components.topfive'); 6 | }); 7 | 8 | describe('Controller', () => { 9 | let $componentController; 10 | let controller; 11 | const mockLyrics = top5Lyrics.mus.day.all; 12 | 13 | beforeEach(inject(($injector) => { 14 | $componentController = $injector.get('$componentController'); 15 | controller = $componentController('topfive', 16 | { $scope: {} }, 17 | { lyricsOnRank: mockLyrics } 18 | ); 19 | })); 20 | 21 | it('should bind to the correct total of lyrics', () => { 22 | const expected = top5Lyrics.mus.day.all.length; // 5 23 | const actual = controller.lyricsOnRank.length; 24 | 25 | expect(actual).toEqual(expected); 26 | }); 27 | 28 | }); 29 | }); -------------------------------------------------------------------------------- /client/src/app/common/app.spec.js: -------------------------------------------------------------------------------- 1 | // describe('Contact', () => { 2 | // beforeEach(() => { 3 | // angular.mock.module('common.app'); 4 | // angular.mock.module(($stateProvider) => { 5 | // $stateProvider.state('mockApp', { 6 | // redirectTo: 'home', 7 | // url: '/' 8 | // }); 9 | // }); 10 | // }); 11 | 12 | // describe('Routes', () => { 13 | // let $state; 14 | // let $location; 15 | // let $rootScope; 16 | 17 | // function goTo(url) { 18 | // $location.url(url); 19 | // $rootScope.$digest(); 20 | // } 21 | 22 | // beforeEach(inject(($injector) => { 23 | // $state = $injector.get('$state'); 24 | // $location = $injector.get('$location'); 25 | // $rootScope = $injector.get('$rootScope'); 26 | // })); 27 | 28 | // it('should go to the home state', () => { 29 | // goTo('/'); 30 | // expect($state.current.name).toEqual('home') 31 | // }); 32 | // }); 33 | // }); -------------------------------------------------------------------------------- /client/src/app/components/home/search-form/search-from.component.spec.js: -------------------------------------------------------------------------------- 1 | describe('Component: Search form', () => { 2 | beforeEach(() => { 3 | angular.mock.module('components.home.searchForm'); 4 | }); 5 | 6 | describe('Controller', () => { 7 | let $componentController; 8 | let controller; 9 | const mockClick = angular.noop; 10 | 11 | beforeEach(inject(($injector) => { 12 | $componentController = $injector.get('$componentController'); 13 | controller = $componentController('searchForm', 14 | { $scope: {} }, 15 | { onClick: mockClick} 16 | ); 17 | })); 18 | 19 | it('should call onClick with the correct payload', () => { 20 | const mockLyrics = {artist: 'Pearl Jam', music: 'Alive'}; 21 | const payload = { $event: { lyrics: mockLyrics } }; 22 | 23 | spyOn(controller, 'onClick'); 24 | controller.clickButton(mockLyrics); 25 | expect(controller.onClick).toHaveBeenCalledWith(payload); 26 | }); 27 | }); 28 | }); -------------------------------------------------------------------------------- /client/src/app/components/home/aside/fav-aside.component.js: -------------------------------------------------------------------------------- 1 | // import templateUrl from './favorites.html'; 2 | 3 | export const favAsideComponent = { 4 | bindings: { 5 | lastFive: '<', 6 | onClick: '&' 7 | }, 8 | template: ` 9 | 25 | `, 26 | controller: class favAsideComponent { 27 | clickButton(event) { 28 | this.onClick({ 29 | $event: { 30 | lyrics: event.lyrics 31 | } 32 | }); 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /client/src/app/components/home/home.module.js: -------------------------------------------------------------------------------- 1 | import uiRouter from 'angular-ui-router'; 2 | 3 | import { common } from '../../common/common.module'; 4 | import { app } from '../../common/app.module'; 5 | 6 | import { favAside } from './aside/fav-aside.module'; 7 | import { lyrics } from './lyrics/lyrics.module'; 8 | import { searchForm } from './search-form/search-form.module'; 9 | 10 | import { homeComponent } from './home.component'; 11 | import { favAsideComponent } from './aside/fav-aside.component' 12 | 13 | import './home.scss'; 14 | 15 | export const home = angular 16 | .module('components.home', [ 17 | uiRouter, 18 | common, // use VagalumeService 19 | app, 20 | favAside, 21 | lyrics, 22 | searchForm 23 | ]) 24 | .component('home', homeComponent) 25 | .config(configRoute) 26 | .name; 27 | 28 | function configRoute($stateProvider) { 29 | 'ngInject'; 30 | 31 | $stateProvider 32 | .state('home', { 33 | parent: 'app', 34 | url: '/', 35 | component: 'home' 36 | }); 37 | } -------------------------------------------------------------------------------- /client/src/app/components/favorites/favorites.module.js: -------------------------------------------------------------------------------- 1 | import uiRouter from 'angular-ui-router'; 2 | import 'ngstorage'; 3 | 4 | import { services } from '../../common/services/services.module'; 5 | 6 | import { favoritesPageComponent } from './favorites-page.component'; 7 | import { favoritesComponent } from './favorites.component'; 8 | import { FavoritesService } from './favorites.service'; 9 | 10 | import './favorites.scss'; 11 | 12 | export const favorites = angular 13 | .module('components.favorites', [ 14 | uiRouter, 15 | 'ngStorage', 16 | services // use VagalumeRequestService 17 | ]) 18 | .component('favoritesPage', favoritesPageComponent) 19 | .component('favorites', favoritesComponent) 20 | .config(configRoute) 21 | .service('FavoritesService', FavoritesService) 22 | .name; 23 | 24 | 25 | function configRoute($stateProvider){ 26 | 'ngInject'; 27 | 28 | $stateProvider 29 | .state('favorites', { 30 | parent: 'app', 31 | url: '/favorites', 32 | component: 'favoritesPage' 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 Fellipe Pinheiro 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 5 | associated documentation files (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 7 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or 11 | substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 14 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 16 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 17 | OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /client/src/app/common/services/vagalume-request.service.js: -------------------------------------------------------------------------------- 1 | export function VagalumeRequestService($resource) { 2 | 'ngInject'; 3 | 4 | const mainUrl = 'http://api.vagalume.com.br/'; 5 | 6 | const service = { 7 | search: search, 8 | rank: rank 9 | }; 10 | 11 | return service; 12 | 13 | 14 | function search (artist, music) { 15 | const params = { 16 | 'art': artist, 17 | 'mus': music 18 | }; 19 | return $resource( 20 | `${mainUrl}search.php?art=:art&mus=:mus&extra=ytid`, 21 | { 22 | art: '@art', 23 | mus: '@mus' 24 | } 25 | ).get(params, function(data) { 26 | return data; 27 | }); 28 | } 29 | 30 | function rank (limit = 5, period = 'day') { 31 | const params = { 32 | 'scope': 'all', 33 | 'type': 'mus', 34 | 'period': period, 35 | 'limit': limit 36 | }; 37 | return $resource( 38 | `${mainUrl}rank.php?type=:type&period=:period&scope=:scope&limit=:limit`, 39 | { 40 | scope: '@scope', 41 | type: '@type', 42 | period: '@period', 43 | limit: '@limit' 44 | } 45 | ).get(params, (data) => { 46 | return data; 47 | }); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /client/karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('./webpack.config'); 2 | 3 | module.exports = (config) => { 4 | config.set({ 5 | basePath: '', 6 | frameworks: ['jasmine'], 7 | webpack: webpackConfig, 8 | files: [ 9 | 'node_modules/angular/angular.js', 10 | 'node_modules/angular-ui-router/release/angular-ui-router.js', 11 | 'node_modules/angular-mocks/angular-mocks.js', 12 | 'node_modules/babel-polyfill/dist/polyfill.js', 13 | 'src/app/root.module.js', 14 | 'src/app/**/*.spec.js', 15 | ], 16 | exclude: [ 17 | ], 18 | plugins: [ 19 | require('karma-jasmine'), 20 | require('karma-chrome-launcher'), 21 | require('karma-phantomjs-launcher'), 22 | require('karma-spec-reporter'), 23 | require('karma-webpack'), 24 | ], 25 | preprocessors: { 26 | 'src/app/root.module.js': ['webpack'], 27 | 'src/app/**/*.js': ['webpack'], 28 | 'src/app/**/*.spec.js': ['webpack'] 29 | }, 30 | reporters: ['spec'], 31 | port: 9876, 32 | colors: true, 33 | logLevel: config.LOG_INFO, 34 | autoWatch: true, 35 | browsers: ['Chrome'], 36 | singleRun: false, 37 | concurrency: Infinity, 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /client/src/app/components/topfive/topfive.component.js: -------------------------------------------------------------------------------- 1 | export const topfiveComponent = { 2 | bindings: { 3 | lyricsOnRank: '<' 4 | }, 5 | template: ` 6 |
7 |
8 |
9 |

Top 5 of the day

10 |
11 |
12 | {{$ctrl.errorMessage}} 13 |
14 |
15 |
    16 |
  • 19 | 20 |

    21 | {{ music.art.name }} 22 |

    23 | {{ music.views }} 24 | 26 |

    27 | {{ music.name }} 28 |

    29 |
  • 30 |
31 |
32 |
33 |
34 | ` 35 | }; 36 | -------------------------------------------------------------------------------- /client/src/app/components/home/search-form/search-form.scss: -------------------------------------------------------------------------------- 1 | .panel { 2 | 3 | &__heading { 4 | padding: 20px; 5 | text-align: center; 6 | margin-top: 50px; 7 | } 8 | 9 | &__title { 10 | color: $deep-koama; 11 | font-size: 2em; 12 | } 13 | 14 | &__body { 15 | padding: 20px; 16 | padding-left: 5px; 17 | 18 | @media screen and (min-width: 768px) { 19 | padding: 20px; 20 | } 21 | } 22 | } 23 | 24 | .form { 25 | 26 | &_inline { 27 | display: flex; 28 | flex-wrap: wrap; 29 | align-items: baseline; 30 | justify-content: center; 31 | } 32 | 33 | &__input { 34 | margin: 10px; 35 | padding: 10px; 36 | font-size: 1.2em; 37 | border: 1px solid #dedede; 38 | border-radius: 3px; 39 | color: $deep-koama; 40 | background-color: transparent; 41 | } 42 | 43 | &__input:focus { 44 | border: 1px solid $moody-blue; 45 | } 46 | 47 | &__button { 48 | padding: 6px 10px; 49 | background-color: transparent; 50 | color: $deep-koama; 51 | position: absolute; 52 | right: -48px; 53 | top: 11px; 54 | transition: all .3s; 55 | 56 | @media screen and (min-width: 768px) { 57 | right: -57px; 58 | } 59 | 60 | &:hover { 61 | background-color: $deep-koama; 62 | border-radius: 3px; 63 | color: #fff; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /client/src/app/components/home/home.component.js: -------------------------------------------------------------------------------- 1 | import templateUrl from './home.html'; 2 | 3 | export const homeComponent = { 4 | templateUrl, 5 | controller: class HomeComponent { 6 | constructor($scope, VagalumeRequestService, FavoritesService) { 7 | 'ngInject'; 8 | 9 | this._VagalumeService = VagalumeRequestService; 10 | this._FavoritesService = FavoritesService; 11 | 12 | this.$onInit = () => { 13 | this.lyrics = {}; 14 | this._lastFive(); 15 | } 16 | } 17 | 18 | addOrDelFavorite(event) { 19 | var lyrics = event.lyrics; 20 | 21 | if (this._FavoritesService.isFavorite(lyrics)){ 22 | this._FavoritesService.delFavorite(lyrics); 23 | }else{ 24 | this._FavoritesService.addFavorite(lyrics); 25 | } 26 | // Update favorite array 27 | this._lastFive() 28 | }; 29 | 30 | _lastFive() { 31 | this.lastFive = this._FavoritesService.lastFive(); 32 | } 33 | 34 | doSearch(event){ 35 | var vm = this; 36 | var lyrics = event.lyrics; 37 | if ( lyrics ) { 38 | this._VagalumeService.search(lyrics.artist, lyrics.music) 39 | .$promise.then( data => vm.lyrics = data); 40 | } 41 | }; 42 | 43 | openLyrics(event){ 44 | this.lyrics = event.lyrics; 45 | } 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /client/src/app/components/home/aside/fav-aside.component.spec.js: -------------------------------------------------------------------------------- 1 | import { lastFiveLyrics } from '../dataMock'; 2 | 3 | describe('Component: Aside', () => { 4 | beforeEach(() => { 5 | angular.mock.module('components.home.aside'); 6 | }); 7 | 8 | describe('Controller', () => { 9 | let $componentController; 10 | let controller; 11 | const mockLastFiveLyrics = lastFiveLyrics.mus.day.all; 12 | const mockClick = angular.noop; 13 | 14 | beforeEach(inject(($injector) => { 15 | $componentController = $injector.get('$componentController'); 16 | controller = $componentController('favAside', 17 | { $scope: {} }, 18 | { lastFive: mockLastFiveLyrics, onClick: mockClick} 19 | ); 20 | })); 21 | 22 | it('should bind to the correct total of lyrics', () => { 23 | const expected = lastFiveLyrics.mus.day.all.length; // 5 24 | const actual = controller.lastFive.length; 25 | 26 | expect(actual).toEqual(5); 27 | }); 28 | 29 | it('should call onClick with the correct payload', () => { 30 | const payload = { $event: { lyrics: lastFiveLyrics.mus.day.all[0] } }; 31 | const event = { lyrics: lastFiveLyrics.mus.day.all[0] }; 32 | 33 | spyOn(controller, 'onClick'); 34 | controller.clickButton(event); 35 | expect(controller.onClick).toHaveBeenCalledWith(payload); 36 | }); 37 | }); 38 | }); -------------------------------------------------------------------------------- /client/src/app/components/home/lyrics/lyrics.scss: -------------------------------------------------------------------------------- 1 | .lyrics { 2 | padding: 25px 0 0 25px; 3 | 4 | &__heading { 5 | display: flex; 6 | justify-content: center; 7 | position: relative; 8 | } 9 | 10 | &__panel { 11 | display: flex; 12 | justify-content: center; 13 | flex-wrap: wrap; 14 | padding: 0 15px; 15 | 16 | @media screen and (min-width: 690px) { 17 | flex-wrap: nowrap; 18 | padding: 0; 19 | margin-bottom: 50px; 20 | } 21 | } 22 | 23 | &__lyrics { 24 | text-align: right; 25 | border-bottom: 1px solid #dedede; 26 | padding-bottom: 25px; 27 | text-align: justify; 28 | 29 | @media screen and (min-width: 690px) { 30 | border-right: 1px solid #dedede; 31 | border-bottom: 0; 32 | padding-right: 25px; 33 | padding-bottom: 0; 34 | text-align: right; 35 | } 36 | } 37 | 38 | &__translate { 39 | text-align: left; 40 | } 41 | 42 | &__title { 43 | color: $deep-koama; 44 | } 45 | 46 | &__heart-icon { 47 | position: absolute; 48 | left: 0; 49 | bottom: -10px; 50 | transition: all .3s; 51 | } 52 | 53 | &__heart-icon:hover { 54 | transform: scale(1.5); 55 | } 56 | 57 | &__heart-icon i { 58 | font-size: 3.5em; 59 | color: #ce3e3e; 60 | } 61 | 62 | &__link-icon { 63 | margin-left: 5px; 64 | } 65 | 66 | &__link-icon i { 67 | font-size: 1.8em; 68 | } 69 | } -------------------------------------------------------------------------------- /client/src/app/components/home/search-form/search-form.component.js: -------------------------------------------------------------------------------- 1 | // import templateUrl from './favorites.html'; 2 | 3 | export const searchFormComponent = { 4 | bindings: { 5 | onClick: '&' 6 | }, 7 | template: ` 8 |
9 |

10 | Find my favorite lyrics 11 |

12 |
13 |
14 |
15 | 16 |
17 | 21 |
22 | 23 |
24 | 28 | 29 | 36 | 37 |
38 | `, 39 | controller: class SearchFormComponent { 40 | clickButton(lyrics) { 41 | this.onClick({ 42 | $event: { 43 | lyrics: lyrics 44 | } 45 | }); 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /client/src/styles/_components.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | cursor: pointer; 3 | } 4 | 5 | 6 | .text { 7 | &_center { 8 | text-align: center; 9 | } 10 | 11 | &_right { 12 | text-align: right; 13 | } 14 | 15 | &_left { 16 | text-align: left; 17 | } 18 | } 19 | 20 | 21 | .list-group { 22 | width: 100%; 23 | animation: 1.25s flyin ease; 24 | 25 | &__item { 26 | position: relative; 27 | margin: 20px 10px; 28 | transition: all .5s ease; 29 | } 30 | 31 | &__img { 32 | border-radius: 20px; 33 | margin: 10px 0; 34 | } 35 | 36 | &__title { 37 | color: $deep-koama; 38 | } 39 | 40 | &__sub-title { 41 | text-align: center; 42 | color: $deep-koama; 43 | } 44 | 45 | &__badge { 46 | position: absolute; 47 | right: -10px; 48 | top: 19px; 49 | padding: 3px; 50 | border-radius: 5px; 51 | background-color: $cold-purple; 52 | color: $ghost-white; 53 | } 54 | } 55 | 56 | 57 | .container { 58 | width: 1060px; 59 | max-width: 100%; 60 | 61 | @media (min-width: 1200px) { 62 | width: 1170px; 63 | } 64 | 65 | &_center { 66 | margin: 0 auto; 67 | } 68 | 69 | &__flex_column { 70 | @extend .container; 71 | display: flex; 72 | flex-direction: column; 73 | align-items: center; 74 | } 75 | } 76 | 77 | 78 | .page { 79 | display: flex; 80 | flex-direction: column; 81 | align-items: center; 82 | padding-top: 50px; 83 | 84 | &__title { 85 | color: $deep-koama; 86 | margin-bottom: 50px; 87 | } 88 | } -------------------------------------------------------------------------------- /client/src/app/components/home/lyrics/lyrics.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ $ctrl.myLyrics.art.name }} - {{ $ctrl.myLyrics.mus[0].name }} 5 |

6 | 10 | link 11 | 12 |
13 | 28 |
29 | 30 |
31 |
32 |

Original

33 |
34 |

35 |
36 |
37 | 38 |
39 |

Translate

40 |
41 |

42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /client/src/app/common/app-nav/app-nav.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 36 |
37 |
38 |
-------------------------------------------------------------------------------- /client/src/app/components/favorites/favorites.service.js: -------------------------------------------------------------------------------- 1 | export function FavoritesService($localStorage) { 2 | 'ngInject'; 3 | const service = { 4 | 5 | storage: $localStorage.$default({ 6 | favorites: [] 7 | }), 8 | isFavorite: isFavorite, 9 | lastFive: lastFive, 10 | addFavorite: addFavorite, 11 | delFavorite: delFavorite, 12 | clearFavorites: clearFavorites, 13 | hasFavorites: hasFavorites, 14 | total: total 15 | }; 16 | 17 | return service; 18 | 19 | function isFavorite (lyrics) { 20 | if (lyrics !== null && lyrics.type === 'exact') { 21 | for (var i = 0; i < total(); i++) { 22 | if (service.storage.favorites[i].mus[0].id === lyrics.mus[0].id) { 23 | return true; 24 | } 25 | } 26 | return false; 27 | } 28 | } 29 | 30 | function lastFive () { 31 | var favorites = service.storage.favorites; 32 | var lastFive = favorites.slice( Math.max(favorites.length - 5, 0) ); 33 | return lastFive; 34 | } 35 | 36 | function addFavorite (lyrics) { 37 | if (!isFavorite(lyrics)) { 38 | service.storage.favorites.unshift(lyrics); 39 | } 40 | } 41 | 42 | function delFavorite (lyrics) { 43 | if (isFavorite(lyrics)) { 44 | let index = null; 45 | index = service.storage.favorites.indexOf(lyrics); 46 | service.storage.favorites.splice(index, 1); 47 | } 48 | } 49 | 50 | function clearFavorites () { 51 | service.storage.$reset({ 52 | favorites: [] 53 | }); 54 | } 55 | 56 | function hasFavorites () { 57 | return total() > 0; 58 | } 59 | 60 | function total () { 61 | return service.storage.favorites.length; 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /client/src/app/components/favorites/favorites.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | display: flex; 3 | max-width: 272px; 4 | max-height: 108px; 5 | 6 | &__right { 7 | background-color: $moody-blue; 8 | display: flex; 9 | } 10 | 11 | &__left { 12 | background-color: $hawkes-blue; 13 | padding: 30px; 14 | min-width: 110px; 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | } 19 | 20 | &__button { 21 | background-color: $moody-blue; 22 | padding: 30px; 23 | } 24 | 25 | &__button:hover { 26 | background-color: $cold-purple; 27 | } 28 | 29 | &__icon { 30 | font-size: 40px !important; 31 | color: $deep-koama; 32 | } 33 | 34 | &__title { 35 | font-size: 20px; 36 | text-align: center; 37 | } 38 | 39 | &__sub-title { 40 | font-size: 15px; 41 | } 42 | } 43 | 44 | .favorites_fav { 45 | display: flex; 46 | flex-wrap: wrap; 47 | justify-content: space-around; 48 | } 49 | 50 | .favorites_home-page { 51 | display: flex; 52 | flex-wrap: wrap; 53 | justify-content: center; 54 | } 55 | 56 | @media screen and (min-width: 1050px) { 57 | 58 | .favorites_home-page { 59 | flex-direction: column; 60 | align-items: center; 61 | } 62 | } 63 | 64 | .btn-remove-all { 65 | background-color: $moody-blue; 66 | padding: 20px; 67 | color: $ghost-white; 68 | font-size: 13px; 69 | font-weight: 600; 70 | 71 | &:hover { 72 | background-color: $cold-purple; 73 | } 74 | } 75 | 76 | .empty { 77 | text-align: center; 78 | margin-top: 20px; 79 | 80 | &__icon { 81 | margin: 0 auto; 82 | } 83 | 84 | &__text { 85 | color: $deep-koama; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /client/src/app/components/favorites/favorites.component.js: -------------------------------------------------------------------------------- 1 | // import templateUrl from './favorites.html'; 2 | 3 | export const favoritesComponent = { 4 | bindings: { 5 | favoriteLyrics: '<', 6 | onClick: '&', 7 | btnIcon: '@', 8 | btnTitle: '@', 9 | layoutClass: '@' 10 | }, 11 | template: ` 12 | 37 | `, 38 | controller: class FavoritesComponent { 39 | clickButton(lyrics) { 40 | this.onClick({ 41 | $event: { 42 | lyrics: lyrics 43 | } 44 | }); 45 | } 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /client/src/app/components/favorites/favorites.service.spec.js: -------------------------------------------------------------------------------- 1 | import { FavoritesService } from './favorites.service'; 2 | import { mockLyrics } from './dataMock'; 3 | 4 | 'use strict'; 5 | 6 | describe('Service: FavoritesService', () => { 7 | 8 | let favoritesService = null; 9 | 10 | const fakeLocalStorage = { 11 | $default: (objectToSave) => { 12 | objectToSave.$reset = () => objectToSave.favorites = []; 13 | return objectToSave; 14 | } 15 | } 16 | 17 | beforeEach( () => { 18 | favoritesService = FavoritesService(fakeLocalStorage); 19 | }); 20 | 21 | it('favorites should start empty', () => { 22 | expect(favoritesService.total()).toBe(0); 23 | }); 24 | 25 | it('should add one lyrics in favorites', () => { 26 | favoritesService.addFavorite(mockLyrics[0]); 27 | 28 | expect(favoritesService.total()).toBe(1); 29 | expect(favoritesService.hasFavorites()).toBe(true); 30 | }); 31 | 32 | it('lyrics should be favorited', () => { 33 | favoritesService.addFavorite(mockLyrics[0]); 34 | favoritesService.addFavorite(mockLyrics[1]); 35 | 36 | expect(favoritesService.isFavorite(mockLyrics[1])).toBe(true); 37 | }); 38 | 39 | it('should clear all favorites', () => { 40 | mockLyrics.map(favoritesService.addFavorite); 41 | favoritesService.clearFavorites(); 42 | 43 | expect(favoritesService.hasFavorites()).toBe(false); 44 | expect(favoritesService.total()).toBe(0); 45 | }); 46 | 47 | it('should exist only onde lyrics in favorites after delete', () => { 48 | favoritesService.addFavorite(mockLyrics[0]); 49 | favoritesService.addFavorite(mockLyrics[1]); 50 | favoritesService.delFavorite(mockLyrics[0]); 51 | 52 | expect(favoritesService.total()).toBe(1); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-lyrics-finder", 3 | "version": "0.2.0", 4 | "author": "@delete", 5 | "description": "Get lyrics from vagamule's API", 6 | "scripts": { 7 | "start": "webpack-dev-server -d --progress --colors --inline --hot --open --content-base bin/", 8 | "build": "NODE_ENV=production webpack --progress --hide-modules", 9 | "deploy": "git subtree push --prefix dist origin gh-pages", 10 | "test": "karma start --auto-watch" 11 | }, 12 | "license": "MIT", 13 | "repository": "https://github.com/delete/my-lyrics-finder", 14 | "dependencies": { 15 | "angular": "^1.8.0", 16 | "angular-resource": "^1.6.2", 17 | "angular-ui-router": "1.0.0-beta.3", 18 | "babel-polyfill": "^6.23.0", 19 | "clean-webpack-plugin": "^0.1.10", 20 | "copy-webpack-plugin": "^3.0.1", 21 | "extract-text-webpack-plugin": "^2.0.0-rc.3", 22 | "font-awesome": "^4.7.0", 23 | "ng-annotate-webpack-plugin": "^0.1.3", 24 | "ngstorage": "^0.3.11", 25 | "postcss-loader": "^1.3.3", 26 | "sw-precache": "^4.0.0", 27 | "webpack": "^2.2", 28 | "webpack-dev-server": "^3.1.11" 29 | }, 30 | "devDependencies": { 31 | "angular-mocks": "^1.5.8", 32 | "babel-core": "^6.11.4", 33 | "babel-loader": "^6.2.10", 34 | "babel-preset-es2015": "^6.14.0", 35 | "css-loader": "^0.26.1", 36 | "file-loader": "^0.9.0", 37 | "html-loader": "^0.4.3", 38 | "jasmine-core": "^2.4.1", 39 | "karma": "^0.13.19", 40 | "karma-chrome-launcher": "^0.2.2", 41 | "karma-jasmine": "^0.3.6", 42 | "karma-phantomjs-launcher": "^1.0.1", 43 | "karma-spec-reporter": "0.0.23", 44 | "karma-webpack": "^1.8.0", 45 | "ng-annotate-loader": "^0.2.0", 46 | "ngtemplate-loader": "^1.3.1", 47 | "node-sass": "^4.5.0", 48 | "sass-loader": "^5.0.1", 49 | "style-loader": "^0.13.1", 50 | "webpack": "^2.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/src/app/components/favorites/favorites.component.spec.js: -------------------------------------------------------------------------------- 1 | import { mockLyrics } from './dataMock'; 2 | 3 | describe('Component: Favorites', () => { 4 | 5 | beforeEach(angular.mock.module('components.favorites')); 6 | 7 | let $componentController; 8 | let controller; 9 | const mockClick = angular.noop; 10 | const mockIcon = 'delete'; 11 | const mockTitle = 'Remove this lyrics from favorites'; 12 | const mockClass = 'favorites_fav'; 13 | 14 | beforeEach(inject(($injector) => { 15 | $componentController = $injector.get('$componentController'); 16 | controller = $componentController('favorites', 17 | { $scope: {} }, 18 | { 19 | favoriteLyrics: mockLyrics, 20 | onClick: mockClick, 21 | btnIcon: mockIcon, 22 | btnTitle: mockTitle, 23 | layoutClass: mockClass 24 | } 25 | ); 26 | })); 27 | 28 | it('should bind to the correct total of 6 lyrics', () => { 29 | const expected = mockLyrics.length; // 6 30 | const actual = controller.favoriteLyrics.length; 31 | 32 | expect(actual).toEqual(expected); 33 | }); 34 | 35 | it('should call onClick when clickButton is called', () => { 36 | const payload = { $event: { lyrics: mockLyrics[0] } }; 37 | 38 | spyOn(controller, 'onClick'); 39 | controller.clickButton(mockLyrics[0]); 40 | 41 | expect(controller.onClick).toHaveBeenCalledWith(payload); 42 | }); 43 | 44 | it('should bind to the correct icon button', () => { 45 | const expected = mockIcon; 46 | const actual = controller.btnIcon; 47 | 48 | expect(actual).toEqual(expected); 49 | }); 50 | 51 | it('should bind to the correct title button', () => { 52 | const expected = mockTitle; 53 | const actual = controller.btnTitle; 54 | 55 | expect(actual).toEqual(expected); 56 | }); 57 | 58 | it('should bind to the correct layout class', () => { 59 | const expected = mockClass; 60 | const actual = controller.layoutClass; 61 | 62 | expect(actual).toEqual(expected); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /client/src/app/components/home/lyrics/lyrics.component.spec.js: -------------------------------------------------------------------------------- 1 | import { lastFiveLyrics } from '../dataMock'; 2 | 3 | describe('Component: Lyrics', () => { 4 | beforeEach(angular.mock.module('components.home.lyrics')); 5 | 6 | beforeEach(() => { 7 | angular.mock.module('components.favorites', ($provide) => { 8 | $provide.value('FavoritesService', { 9 | isFavorite: angular.noop 10 | }); 11 | }); 12 | }); 13 | 14 | describe('Controller', () => { 15 | let $componentController; 16 | let controller; 17 | const mockLyrics = lastFiveLyrics.mus.day.all[0]; 18 | const mockClick = angular.noop; 19 | 20 | beforeEach(inject(($injector) => { 21 | $componentController = $injector.get('$componentController'); 22 | controller = $componentController('lyrics', 23 | { $scope: {} }, 24 | { myLyrics: mockLyrics, onClick: mockClick} 25 | ); 26 | })); 27 | 28 | it('DI should be defined', () => { 29 | expect(controller._FavoritesService).toBeDefined(); 30 | }); 31 | 32 | describe('Controller', () => { 33 | it('should bind to the correct lyrics', () => { 34 | const expected = mockLyrics; // 5 35 | const actual = controller.myLyrics; 36 | 37 | expect(actual).toEqual(expected); 38 | }); 39 | 40 | it('should call onClick with the correct payload', () => { 41 | const payload = { $event: { lyrics: mockLyrics } }; 42 | const event = mockLyrics; 43 | 44 | spyOn(controller, 'onClick'); 45 | controller.clickButton(event); 46 | expect(controller.onClick).toHaveBeenCalledWith(payload); 47 | }); 48 | 49 | it('should call _FavoritesService.isFavorite with the correct payload when isFavorite is called', () => { 50 | const payload = mockLyrics; 51 | 52 | spyOn(controller._FavoritesService, 'isFavorite'); 53 | controller.isFavorite(payload); 54 | expect(controller._FavoritesService.isFavorite).toHaveBeenCalledWith(payload); 55 | }); 56 | }); 57 | }); 58 | }); -------------------------------------------------------------------------------- /client/src/app/components/favorites/favorites-page.component.spec.js: -------------------------------------------------------------------------------- 1 | import { mockLyrics } from './dataMock'; 2 | 3 | describe('Component: Favorites page', () => { 4 | beforeEach(angular.mock.module('components.favorites')); 5 | 6 | beforeEach(() => { 7 | angular.mock.module('components.favorites', ($provide) => { 8 | $provide.value('VagalumeService', { 9 | lyrics: mockLyrics 10 | }); 11 | $provide.value('FavoritesService', { 12 | hasFavorites: angular.noop, 13 | clearFavorites: angular.noop, 14 | delFavorite: angular.noop 15 | }); 16 | }); 17 | }); 18 | 19 | let $componentController; 20 | let controller; 21 | 22 | beforeEach(inject(($injector) => { 23 | $componentController = $injector.get('$componentController'); 24 | controller = $componentController('favoritesPage', 25 | { $scope: {} }, 26 | {} 27 | ); 28 | })); 29 | 30 | it('DI should be defined', () => { 31 | let actual = controller.vagalumeFavorite; 32 | expect(actual).toBeDefined(); 33 | 34 | actual = controller.vagalume; 35 | expect(actual).toBeDefined(); 36 | }); 37 | 38 | describe('Controller', () => { 39 | it('should call vagalumeFavorite.hasFavorites() when hasFavorites() is called', () => { 40 | spyOn(controller.vagalumeFavorite, 'hasFavorites'); 41 | controller.hasFavorites(); 42 | 43 | expect(controller.vagalumeFavorite.hasFavorites).toHaveBeenCalled(); 44 | }); 45 | 46 | it('should call vagalumeFavorite.clearFavorites() when clearFavorites() is called', () => { 47 | spyOn(controller.vagalumeFavorite, 'clearFavorites'); 48 | controller.clearFavorites(); 49 | 50 | expect(controller.vagalumeFavorite.clearFavorites).toHaveBeenCalled(); 51 | }); 52 | 53 | it('should call vagalumeFavorite.delFavorite() when delFavorite() is called', () => { 54 | const event = { lyrics: mockLyrics[0] }; 55 | 56 | spyOn(controller.vagalumeFavorite, 'delFavorite'); 57 | controller.delFavorite(event); 58 | 59 | expect(controller.vagalumeFavorite.delFavorite).toHaveBeenCalledWith(mockLyrics[0]); 60 | }); 61 | 62 | it('should have a lyrics on vagalume.lyrics when showLyrics() is called', () => { 63 | const lyrics = mockLyrics[0]; 64 | 65 | controller.showLyrics(lyrics); 66 | 67 | expect(controller.vagalume.lyrics).toEqual(lyrics); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /client/src/app/common/app-nav/app-nav.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | &__navbar { 3 | display: flex; 4 | justify-content: center; 5 | box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.12); 6 | background-color: $moody-blue; 7 | } 8 | } 9 | 10 | .navbar { 11 | align-items: center; 12 | display:flex; 13 | justify-content: space-between; 14 | padding: 15px; 15 | 16 | &__item { 17 | flex: 2; 18 | 19 | &_small { 20 | flex: 1; 21 | } 22 | } 23 | 24 | &__item-mobile { 25 | display: flex; 26 | flex-direction: columns; 27 | justify-content: flex-end; 28 | 29 | @media screen and (min-width: 768px) { 30 | & { 31 | display: none; 32 | } 33 | } 34 | } 35 | 36 | &__item-desktop { 37 | display: none; 38 | } 39 | 40 | @media screen and (min-width: 768px) { 41 | &__item-desktop { 42 | display: block; 43 | } 44 | } 45 | 46 | &__brand { 47 | text-decoration: none; 48 | outline: none; 49 | color: $ghost-white; 50 | font-weight: 600; 51 | font-size: 16px; 52 | } 53 | 54 | &__menu-button { 55 | background-color: transparent; 56 | 57 | &:hover i { 58 | color: $hawkes-blue; 59 | } 60 | 61 | & i { 62 | font-size: 2em; 63 | } 64 | } 65 | 66 | &__nav { 67 | background-color: $moody-blue; 68 | border-bottom-left-radius: 3px; 69 | position: absolute; 70 | transform: translate(0, -1500px); 71 | transition: all .3s; 72 | right: 0; 73 | width: 100%; 74 | top: 58px; 75 | z-index: 3; 76 | display: flex; 77 | justify-content: space-around; 78 | flex-direction: column; 79 | bottom: 0; 80 | 81 | &_open { 82 | transform: translate(0,0); 83 | box-shadow: 0px -2px 0px rgba(0, 0, 0, 0.12); 84 | } 85 | } 86 | 87 | &__link { 88 | text-decoration: none; 89 | outline: none; 90 | color: $hawkes-blue; 91 | font-size: 2em; 92 | font-weight: 100; 93 | padding: 2em; 94 | text-align: center; 95 | display: block; 96 | margin: 30px 10px; 97 | } 98 | 99 | &__link:hover, &__link.active { 100 | background-color: $moody-blue; 101 | border-radius: 3px; 102 | font-weight: 400; 103 | } 104 | 105 | @media screen and (min-width: 768px) { 106 | &__nav { 107 | background-color: transparent; 108 | border-bottom-left-radius: 0; 109 | padding: 0; 110 | position: static; 111 | transform: translate(0,0); 112 | transition: all .3s; 113 | right: initial; 114 | width: 100%; 115 | top: initial; 116 | display: flex; 117 | justify-content: space-around; 118 | flex-direction: row; 119 | } 120 | 121 | &__link { 122 | padding: 0; 123 | display: initial; 124 | margin: 0; 125 | font-size: 14px; 126 | } 127 | &__link:hover, &__link.active { 128 | padding-bottom: 15px; 129 | border-bottom: 5px solid $ghost-white; 130 | border-radius: 0; 131 | } 132 | } 133 | } 134 | 135 | .nav { 136 | &__item { 137 | padding: 10px; 138 | text-align: center; 139 | 140 | &:hover, &__item.active { 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const cleanPlugin = require('clean-webpack-plugin'); 2 | const copyPlugin = require('copy-webpack-plugin'); 3 | const extractPlugin = require('extract-text-webpack-plugin'); 4 | const webpack = require('webpack'); 5 | var path = require('path') 6 | 7 | const root = `${__dirname}/src`; 8 | const dist = `${__dirname}/dist`; 9 | const paths = { 10 | app: `${root}/app/root.module.js`, 11 | styles: `${root}/styles`, 12 | static: { 13 | index: `${root}/index.html`, 14 | manifest: `${root}/manifest.json`, 15 | images: `${root}/img/**/*` 16 | }, 17 | }; 18 | 19 | // Plugins 20 | const prep = { 21 | clean: new cleanPlugin([ 22 | dist, 23 | ]), 24 | copy: new copyPlugin([{ 25 | from: paths.static.index, 26 | }, { 27 | from: paths.static.manifest, 28 | }, { 29 | from: paths.static.images, 30 | to: 'img/', 31 | flatten: true, 32 | }]), 33 | }; 34 | 35 | // Loaders 36 | const scriptsLoader = { 37 | test: /\.js$/, 38 | exclude: /node_modules/, 39 | loaders: [{ 40 | loader: 'ng-annotate-loader', 41 | },{ 42 | loader: 'babel-loader', 43 | query: { 44 | presets: ['es2015'] 45 | } 46 | } 47 | ], 48 | }; 49 | 50 | const markupLoader = { 51 | test: /\.html$/, 52 | loader: 'ngtemplate-loader!html-loader', 53 | }; 54 | 55 | const fontsLoader = { 56 | test: /\.(eot|svg|ttf|woff|woff2)$/, 57 | loader: 'file-loader?name=fonts/[name].[ext]', 58 | }; 59 | 60 | const extractSass = new extractPlugin({ 61 | filename: "css/styles.css", 62 | disable: process.env.NODE_ENV === "development" 63 | }); 64 | 65 | const stylesLoader = { 66 | test: /\.scss$/, 67 | loader: extractSass.extract({ 68 | loader: [{ 69 | loader: "css-loader?importLoaders=1" 70 | },{ 71 | loader: 'postcss-loader', 72 | },{ 73 | loader: "sass-loader", 74 | options: { 75 | data: '@import "variables";', 76 | includePaths: [paths.styles, root] 77 | } 78 | }], 79 | // use style-loader in development 80 | fallbackLoader: "style-loader" 81 | }) 82 | }; 83 | 84 | // Config object 85 | const config = { 86 | entry: { 87 | bundle: paths.app, 88 | }, 89 | devtool: '#eval-source-map', 90 | module: { 91 | rules: [ 92 | scriptsLoader, 93 | markupLoader, 94 | fontsLoader, 95 | stylesLoader 96 | ], 97 | }, 98 | plugins: [ 99 | prep.clean, 100 | prep.copy, 101 | extractSass 102 | ], 103 | 104 | output: { 105 | path: path.resolve(__dirname, './dist'), 106 | publicPath: '/', 107 | filename: 'js/app.[name].js', 108 | }, 109 | devServer: { 110 | port: 8080, 111 | historyApiFallback: true, 112 | host: '0.0.0.0' 113 | }, 114 | }; 115 | 116 | module.exports = config; 117 | 118 | if (process.env.NODE_ENV === 'production') { 119 | module.exports.devtool = '#source-map' 120 | 121 | module.exports.plugins = (module.exports.plugins || []).concat([ 122 | new webpack.DefinePlugin({ 123 | 'process.env': { 124 | NODE_ENV: '"production"' 125 | } 126 | }), 127 | new webpack.optimize.UglifyJsPlugin({ 128 | sourceMap: true, 129 | compress: { 130 | warnings: false 131 | } 132 | }), 133 | new webpack.LoaderOptionsPlugin({ 134 | minimize: true 135 | }) 136 | ]) 137 | } 138 | 139 | 140 | -------------------------------------------------------------------------------- /client/src/app/common/services/vagalume-request.service.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { lyrics, top3Lyrics, top10Lyrics } from './dataMock'; 3 | 4 | describe('Service: VagalumeRequestService', function () { 5 | let $rootScope; 6 | let VagalumeRequestService; 7 | let $httpBackend; 8 | 9 | beforeEach(angular.mock.module('common.services')); 10 | 11 | beforeEach(inject( ($injector) => { 12 | $rootScope = $injector.get('$rootScope'); 13 | $httpBackend = $injector.get('$httpBackend'); 14 | VagalumeRequestService = $injector.get('VagalumeRequestService'); 15 | })); 16 | 17 | it('Scope should be defined', () => { 18 | expect($rootScope).toBeDefined(); 19 | }); 20 | 21 | it('Service should be defined', () => { 22 | expect(VagalumeRequestService).toBeDefined(); 23 | }); 24 | 25 | it('httpBackend should be defined', () => { 26 | expect($httpBackend).toBeDefined(); 27 | }); 28 | 29 | describe('Search method', () => { 30 | it('The search should return the right lyrics', () => { 31 | const artist = 'PearlJam'; 32 | const music = 'Black'; 33 | const respond = lyrics; 34 | const result = search(artist, music, respond); 35 | 36 | expect(result.art.id).toEqual(lyrics.art.id); 37 | }); 38 | 39 | it('The search should return notfound when artist does not exist', () => { 40 | const artist = 'PearlJamaaaaa'; 41 | const music = 'Black'; 42 | const expected = 'notfound'; 43 | const respond = '{"type":"notfound"}'; 44 | const result = search(artist, music, respond); 45 | 46 | expect(result.type).toEqual(expected); 47 | }); 48 | 49 | it('The search should return song_notfound when music does not exist', () => { 50 | const artist = 'PearlJam'; 51 | const music = 'Blackaaaaa'; 52 | const expected = 'song_notfound'; 53 | const respond = '{"type":"song_notfound","art":{"id":"","name":"Pearl Jam","url":""}}'; 54 | const result = search(artist, music, respond); 55 | 56 | expect(result.type).toEqual(expected); 57 | }); 58 | 59 | const search = (artist, music, respond) => { 60 | let expectedUrl = `http://api.vagalume.com.br/search.php?art=${artist}&mus=${music}&extra=ytid`; 61 | 62 | $httpBackend.expectGET(expectedUrl) 63 | .respond(respond); 64 | 65 | const result = VagalumeRequestService.search(artist, music); 66 | 67 | $httpBackend.flush(); 68 | 69 | return result; 70 | } 71 | }); 72 | 73 | describe('Rank method', () => { 74 | it('Rank should return 3 top lyrics', () => { 75 | const limit = 3; 76 | const expected = 3; 77 | const result = getRank(limit, top3Lyrics); 78 | 79 | expect(result.mus.day.all.length).toEqual(expected); 80 | }); 81 | 82 | it('Rank should return 10 top lyrics when 0 was given as limit', () => { 83 | const limit = 0; 84 | const expected = 10; 85 | const result = getRank(limit, top10Lyrics); 86 | 87 | expect(result.mus.day.all.length).toEqual(expected); 88 | }); 89 | 90 | const getRank = (limit, respond) => { 91 | let period = 'day'; 92 | let expectedUrl = `http://api.vagalume.com.br/rank.php?type=mus&period=${period}&scope=all&limit=${limit}`; 93 | 94 | $httpBackend.expectGET(expectedUrl).respond(respond); 95 | 96 | const result = VagalumeRequestService.rank(limit, 'day'); 97 | 98 | $httpBackend.flush(); 99 | 100 | return result; 101 | } 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /client/src/app/components/topfive/dataMock.js: -------------------------------------------------------------------------------- 1 | export const top5Lyrics = { 2 | "mus": { 3 | "day": { 4 | "period": { 5 | "year": "2017", 6 | "day": "08", 7 | "month": "03" 8 | }, 9 | "all": [{ 10 | "id": "3ade68b8g0621b0b3", 11 | "name": "Trem Bala", 12 | "url": "https:\/\/www.vagalume.com.br\/ana-vilela\/trem-bala.html", 13 | "uniques": "0", 14 | "views": "17847", 15 | "rank": "0.0", 16 | "art": { 17 | "id": "3ade68b7g3fc13ea3", 18 | "name": "Ana Vilela", 19 | "url": "https:\/\/www.vagalume.com.br\/ana-vilela\/", 20 | "pic_small": "https:\/\/s2.vagalume.com\/ana-vilela\/images\/profile.jpg", 21 | "pic_medium": "https:\/\/s2.vagalume.com\/ana-vilela\/images\/ana-vilela.jpg" 22 | } 23 | }, { 24 | "id": "3ade68b8g7ee6fea3", 25 | "name": "Mulher (sexo Fr\u00e1gil)", 26 | "url": "https:\/\/www.vagalume.com.br\/erasmo-carlos\/mulher-sexo-fragil.html", 27 | "uniques": "0", 28 | "views": "13620", 29 | "rank": "0.0", 30 | "art": { 31 | "id": "3ade68b5g8248eda3", 32 | "name": "Erasmo Carlos", 33 | "url": "https:\/\/www.vagalume.com.br\/erasmo-carlos\/", 34 | "pic_small": "https:\/\/s2.vagalume.com\/erasmo-carlos\/images\/profile.jpg", 35 | "pic_medium": "https:\/\/s2.vagalume.com\/erasmo-carlos\/images\/erasmo-carlos.jpg" 36 | }, 37 | "albd": "Mulher", 38 | "alby": "1981", 39 | "alburl": "mulher" 40 | }, { 41 | "id": "3ade68b8gb645b0b3", 42 | "name": "Eu N\u00e3o Quero Casar", 43 | "url": "https:\/\/www.vagalume.com.br\/thiago-matheus\/eu-nao-quero-casar.html", 44 | "uniques": "0", 45 | "views": "10833", 46 | "rank": "0.0", 47 | "art": { 48 | "id": "3ade68b7g27d72ea3", 49 | "name": "Thiago Matheus", 50 | "url": "https:\/\/www.vagalume.com.br\/thiago-matheus\/", 51 | "pic_small": "https:\/\/s2.vagalume.com\/thiago-matheus\/images\/profile.jpg", 52 | "pic_medium": "https:\/\/s2.vagalume.com\/thiago-matheus\/images\/thiago-matheus.jpg" 53 | } 54 | }, { 55 | "id": "3ade68b8g0f56b0b3", 56 | "name": "Vidinha de Balada", 57 | "url": "https:\/\/www.vagalume.com.br\/henrique-e-juliano\/vidinha-de-balada.html", 58 | "uniques": "0", 59 | "views": "9958", 60 | "rank": "0.0", 61 | "art": { 62 | "id": "3ade68b7g73521ea3", 63 | "name": "Henrique e Juliano", 64 | "url": "https:\/\/www.vagalume.com.br\/henrique-e-juliano\/", 65 | "pic_small": "https:\/\/s2.vagalume.com\/henrique-e-juliano\/images\/profile.jpg", 66 | "pic_medium": "https:\/\/s2.vagalume.com\/henrique-e-juliano\/images\/henrique-e-juliano.jpg" 67 | } 68 | }, { 69 | "id": "3ade68b8g4a06b0b3", 70 | "name": "Shape Of You", 71 | "url": "https:\/\/www.vagalume.com.br\/ed-sheeran\/shape-of-you.html", 72 | "uniques": "0", 73 | "views": "8058", 74 | "rank": "0.0", 75 | "art": { 76 | "id": "3ade68b7g30dd1ea3", 77 | "name": "Ed Sheeran", 78 | "url": "https:\/\/www.vagalume.com.br\/ed-sheeran\/", 79 | "pic_small": "https:\/\/s2.vagalume.com\/ed-sheeran\/images\/profile.jpg", 80 | "pic_medium": "https:\/\/s2.vagalume.com\/ed-sheeran\/images\/ed-sheeran.jpg" 81 | }, 82 | "albd": "\u00f7", 83 | "alby": "2017", 84 | "alburl": "-10" 85 | }] 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /client/src/app/components/home/dataMock.js: -------------------------------------------------------------------------------- 1 | export const lastFiveLyrics = { 2 | "mus": { 3 | "day": { 4 | "period": { 5 | "year": "2017", 6 | "day": "08", 7 | "month": "03" 8 | }, 9 | "all": [{ 10 | "id": "3ade68b8g0621b0b3", 11 | "name": "Trem Bala", 12 | "url": "https:\/\/www.vagalume.com.br\/ana-vilela\/trem-bala.html", 13 | "uniques": "0", 14 | "views": "17847", 15 | "rank": "0.0", 16 | "art": { 17 | "id": "3ade68b7g3fc13ea3", 18 | "name": "Ana Vilela", 19 | "url": "https:\/\/www.vagalume.com.br\/ana-vilela\/", 20 | "pic_small": "https:\/\/s2.vagalume.com\/ana-vilela\/images\/profile.jpg", 21 | "pic_medium": "https:\/\/s2.vagalume.com\/ana-vilela\/images\/ana-vilela.jpg" 22 | } 23 | }, { 24 | "id": "3ade68b8g7ee6fea3", 25 | "name": "Mulher (sexo Fr\u00e1gil)", 26 | "url": "https:\/\/www.vagalume.com.br\/erasmo-carlos\/mulher-sexo-fragil.html", 27 | "uniques": "0", 28 | "views": "13620", 29 | "rank": "0.0", 30 | "art": { 31 | "id": "3ade68b5g8248eda3", 32 | "name": "Erasmo Carlos", 33 | "url": "https:\/\/www.vagalume.com.br\/erasmo-carlos\/", 34 | "pic_small": "https:\/\/s2.vagalume.com\/erasmo-carlos\/images\/profile.jpg", 35 | "pic_medium": "https:\/\/s2.vagalume.com\/erasmo-carlos\/images\/erasmo-carlos.jpg" 36 | }, 37 | "albd": "Mulher", 38 | "alby": "1981", 39 | "alburl": "mulher" 40 | }, { 41 | "id": "3ade68b8gb645b0b3", 42 | "name": "Eu N\u00e3o Quero Casar", 43 | "url": "https:\/\/www.vagalume.com.br\/thiago-matheus\/eu-nao-quero-casar.html", 44 | "uniques": "0", 45 | "views": "10833", 46 | "rank": "0.0", 47 | "art": { 48 | "id": "3ade68b7g27d72ea3", 49 | "name": "Thiago Matheus", 50 | "url": "https:\/\/www.vagalume.com.br\/thiago-matheus\/", 51 | "pic_small": "https:\/\/s2.vagalume.com\/thiago-matheus\/images\/profile.jpg", 52 | "pic_medium": "https:\/\/s2.vagalume.com\/thiago-matheus\/images\/thiago-matheus.jpg" 53 | } 54 | }, { 55 | "id": "3ade68b8g0f56b0b3", 56 | "name": "Vidinha de Balada", 57 | "url": "https:\/\/www.vagalume.com.br\/henrique-e-juliano\/vidinha-de-balada.html", 58 | "uniques": "0", 59 | "views": "9958", 60 | "rank": "0.0", 61 | "art": { 62 | "id": "3ade68b7g73521ea3", 63 | "name": "Henrique e Juliano", 64 | "url": "https:\/\/www.vagalume.com.br\/henrique-e-juliano\/", 65 | "pic_small": "https:\/\/s2.vagalume.com\/henrique-e-juliano\/images\/profile.jpg", 66 | "pic_medium": "https:\/\/s2.vagalume.com\/henrique-e-juliano\/images\/henrique-e-juliano.jpg" 67 | } 68 | }, { 69 | "id": "3ade68b8g4a06b0b3", 70 | "name": "Shape Of You", 71 | "url": "https:\/\/www.vagalume.com.br\/ed-sheeran\/shape-of-you.html", 72 | "uniques": "0", 73 | "views": "8058", 74 | "rank": "0.0", 75 | "art": { 76 | "id": "3ade68b7g30dd1ea3", 77 | "name": "Ed Sheeran", 78 | "url": "https:\/\/www.vagalume.com.br\/ed-sheeran\/", 79 | "pic_small": "https:\/\/s2.vagalume.com\/ed-sheeran\/images\/profile.jpg", 80 | "pic_medium": "https:\/\/s2.vagalume.com\/ed-sheeran\/images\/ed-sheeran.jpg" 81 | }, 82 | "albd": "\u00f7", 83 | "alby": "2017", 84 | "alburl": "-10" 85 | }] 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /client/src/app/components/home/home.component.spec.js: -------------------------------------------------------------------------------- 1 | import { lastFiveLyrics } from './dataMock'; 2 | 3 | describe('Component: Home', () => { 4 | beforeEach(angular.mock.module('components.home')); 5 | 6 | beforeEach(() => { 7 | angular.mock.module('components.favorites', ($provide) => { 8 | $provide.value('VagalumeService', { 9 | lyrics: lastFiveLyrics, 10 | search: () => {$promise: {then: angular.noop}} 11 | }); 12 | $provide.value('FavoritesService', { 13 | isFavorite: angular.noop, 14 | lastFive: angular.noop, 15 | addFavorite: angular.noop, 16 | delFavorite: angular.noop, 17 | storage: { 18 | favorites: [] 19 | } 20 | }); 21 | }); 22 | }); 23 | 24 | describe('Controller', () => { 25 | let $componentController; 26 | let controller; 27 | const mockLastFiveLyrics = lastFiveLyrics.mus.day.all; 28 | 29 | beforeEach(inject(($injector) => { 30 | $componentController = $injector.get('$componentController'); 31 | controller = $componentController('home', 32 | { $scope: {} }, 33 | {} 34 | ); 35 | })); 36 | 37 | it('DI should be defined', () => { 38 | let actual = controller._FavoritesService; 39 | expect(actual).toBeDefined(); 40 | 41 | actual = controller._VagalumeService; 42 | expect(actual).toBeDefined(); 43 | }); 44 | 45 | it('should call _lastFive() on $onInit()', () => { 46 | spyOn(controller, '_lastFive'); 47 | controller.$onInit(); 48 | 49 | expect(controller._lastFive).toHaveBeenCalled(); 50 | }); 51 | 52 | it('should initiate lyrics as {} on $onInit()', () => { 53 | controller.$onInit(); 54 | 55 | expect(controller.lyrics).toBeDefined(); 56 | }); 57 | 58 | describe('Functions', () => { 59 | it('should call _FavoritesService.isFavorite when addOrDelFavorite is called with NO favorite lyrics', () => { 60 | const event = { lyrics: lastFiveLyrics.mus.day.all[0] }; 61 | 62 | spyOn(controller._FavoritesService, 'isFavorite').and.returnValue(false); 63 | spyOn(controller._FavoritesService, 'addFavorite'); 64 | controller.addOrDelFavorite(event); 65 | 66 | expect(controller._FavoritesService.isFavorite).toHaveBeenCalledWith(event.lyrics); 67 | expect(controller._FavoritesService.addFavorite).toHaveBeenCalledWith(event.lyrics); 68 | }); 69 | 70 | it('should call _FavoritesService.delFavorite when addOrDelFavorite is called with 1 favorite lyrics', () => { 71 | const event = { lyrics: lastFiveLyrics.mus.day.all[0] }; 72 | 73 | spyOn(controller._FavoritesService, 'isFavorite').and.returnValue(true); 74 | spyOn(controller._FavoritesService, 'delFavorite'); 75 | controller.addOrDelFavorite(event); 76 | 77 | expect(controller._FavoritesService.isFavorite).toHaveBeenCalledWith(event.lyrics); 78 | expect(controller._FavoritesService.delFavorite).toHaveBeenCalledWith(event.lyrics); 79 | }); 80 | 81 | it('should call _lastFive when addOrDelFavorite is called', () => { 82 | const event = { lyrics: lastFiveLyrics.mus.day.all[0] }; 83 | 84 | spyOn(controller, '_lastFive'); 85 | controller.addOrDelFavorite(event); 86 | expect(controller._lastFive).toHaveBeenCalled(); 87 | }); 88 | 89 | it('should has a lyrics when openLyrics is called', () => { 90 | const event = { lyrics: lastFiveLyrics.mus.day.all[0] }; 91 | 92 | controller.openLyrics(event); 93 | expect(controller.lyrics).toEqual(event.lyrics); 94 | }); 95 | }); 96 | }); 97 | }); -------------------------------------------------------------------------------- /client/src/app/components/favorites/dataMock.js: -------------------------------------------------------------------------------- 1 | export const mockLyrics = [ 2 | { 3 | "type": "exact", 4 | "art": { 5 | "id": "3ade68b5ge177ed3213213a3", 6 | "name": "Pearl Jam", 7 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/" 8 | }, 9 | "mus": [{ 10 | "id": "3ade68b643456g3e1eeda3", 11 | "name": "Black", 12 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black.html", 13 | "lang": 2, 14 | "text": "", 15 | "translate": [{ 16 | "id": "3adeadasda68b7g2ece3ea3", 17 | "lang": 1, 18 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black-traducao.html", 19 | "text": "" 20 | }] 21 | }], 22 | "badwords": false 23 | }, 24 | { 25 | "type": "exact", 26 | "art": { 27 | "id": "3ade68b5231344ge177eda3", 28 | "name": "Pearl Jam", 29 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/" 30 | }, 31 | "mus": [{ 32 | "id": "3ade68b65454g3e1eeda3", 33 | "name": "Black", 34 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black.html", 35 | "lang": 2, 36 | "text": "", 37 | "translate": [{ 38 | "id": "3ade68b7g2ecdasdsade3ea3", 39 | "lang": 1, 40 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black-traducao.html", 41 | "text": "" 42 | }] 43 | }], 44 | "badwords": false 45 | }, 46 | { 47 | "type": "exact", 48 | "art": { 49 | "id": "3ade632348b5ge177eda3", 50 | "name": "Pearl Jam", 51 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/" 52 | }, 53 | "mus": [{ 54 | "id": "3ade68bawe2e26g3e1eeda3", 55 | "name": "Black", 56 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black.html", 57 | "lang": 2, 58 | "text": "", 59 | "translate": [{ 60 | "id": "3ade68dawdb7g2ece3ea3", 61 | "lang": 1, 62 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black-traducao.html", 63 | "text": "" 64 | }] 65 | }], 66 | "badwords": false 67 | }, 68 | { 69 | "type": "exact", 70 | "art": { 71 | "id": "3ade6sadsd28b5ge177eda3", 72 | "name": "Pearl Jam", 73 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/" 74 | }, 75 | "mus": [{ 76 | "id": "3ade6dsadsgth8b6g3e1eeda3", 77 | "name": "Black", 78 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black.html", 79 | "lang": 2, 80 | "text": "", 81 | "translate": [{ 82 | "id": "3ade68ghgfhwb7g2ece3ea3", 83 | "lang": 1, 84 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black-traducao.html", 85 | "text": "" 86 | }] 87 | }], 88 | "badwords": false 89 | }, 90 | { 91 | "type": "exact", 92 | "art": { 93 | "id": "3ade68dsase2b5ge177eda3", 94 | "name": "Pearl Jam", 95 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/" 96 | }, 97 | "mus": [{ 98 | "id": "3ade68b6ghgfhj3e1eeda3", 99 | "name": "Black", 100 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black.html", 101 | "lang": 2, 102 | "text": "", 103 | "translate": [{ 104 | "id": "3ade68b7g2ejghjghce3ea3", 105 | "lang": 1, 106 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black-traducao.html", 107 | "text": "" 108 | }] 109 | }], 110 | "badwords": false 111 | }, 112 | { 113 | "type": "exact", 114 | "art": { 115 | "id": "3afggdfgede68b5ge177eda3", 116 | "name": "Pearl Jam", 117 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/" 118 | }, 119 | "mus": [{ 120 | "id": "3ade68b6g3e1eehgfhgda3", 121 | "name": "Black", 122 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black.html", 123 | "lang": 2, 124 | "text": "", 125 | "translate": [{ 126 | "id": "3adsdfdfe68b7g2ece3ea3", 127 | "lang": 1, 128 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black-traducao.html", 129 | "text": "" 130 | }] 131 | }], 132 | "badwords": false 133 | } 134 | ]; -------------------------------------------------------------------------------- /client/dist/css/styles.css: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}@keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}.app{display:-webkit-box;display:-ms-flexbox;display:flex;min-height:100vh;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}@-webkit-keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}@keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}.header__navbar{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;box-shadow:0 2px 2px 0 rgba(0,0,0,.12);background-color:#776ece}.header__navbar,.navbar{display:-webkit-box;display:-ms-flexbox;display:flex}.navbar{-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:15px}.navbar__item{-webkit-box-flex:2;-ms-flex:2;flex:2}.navbar__item_small{-webkit-box-flex:1;-ms-flex:1;flex:1}.navbar__item-mobile{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:columns;flex-direction:columns;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}@media screen and (min-width:768px){.navbar__item-mobile{display:none}}.navbar__item-desktop{display:none}@media screen and (min-width:768px){.navbar__item-desktop{display:block}}.navbar__brand{text-decoration:none;outline:none;color:#f4f4f8;font-weight:600;font-size:16px}.navbar__menu-button{background-color:transparent}.navbar__menu-button:hover i{color:#d8dae5}.navbar__menu-button i{font-size:2em}.navbar__nav{background-color:#776ece;border-bottom-left-radius:3px;position:absolute;-webkit-transform:translateY(-1500px);transform:translateY(-1500px);transition:all .3s;right:0;width:100%;top:58px;z-index:3;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;bottom:0}.navbar__nav_open{-webkit-transform:translate(0);transform:translate(0);box-shadow:0 -2px 0 rgba(0,0,0,.12)}.navbar__link{text-decoration:none;outline:none;color:#d8dae5;font-size:2em;font-weight:100;padding:2em;text-align:center;display:block;margin:30px 10px}.navbar__link.active,.navbar__link:hover{background-color:#776ece;border-radius:3px;font-weight:400}@media screen and (min-width:768px){.navbar__nav{background-color:transparent;border-bottom-left-radius:0;padding:0;position:static;-webkit-transform:translate(0);transform:translate(0);transition:all .3s;right:auto;width:100%;top:auto;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar__link{padding:0;display:initial;margin:0;font-size:14px}.navbar__link.active,.navbar__link:hover{padding-bottom:15px;border-bottom:5px solid #f4f4f8;border-radius:0}}.nav__item{padding:10px;text-align:center}@-webkit-keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}@keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}.footer{width:100%;border-top:1px solid #e5e5e5;padding-bottom:10px;margin-top:100px}.footer i{margin-top:10px}.footer__text{color:#b6b6b6;font-size:12px}.footer__icon{font-size:15px!important;font-size:20px}.footer__icon-heart{color:#ce3e3e}.footer__icon-headset{color:#776ece}@-webkit-keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}@keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}.card{max-width:272px;max-height:108px}.card,.card__right{display:-webkit-box;display:-ms-flexbox;display:flex}.card__right{background-color:#776ece}.card__left{background-color:#d8dae5;padding:30px;min-width:110px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.card__button{background-color:#776ece;padding:30px}.card__button:hover{background-color:#9d81ce}.card__icon{font-size:40px!important;color:#363368}.card__title{font-size:20px;text-align:center}.card__sub-title{font-size:15px}.favorites_fav{-ms-flex-pack:distribute;justify-content:space-around}.favorites_fav,.favorites_home-page{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.favorites_home-page{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}@media screen and (min-width:1050px){.favorites_home-page{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center}}.btn-remove-all{background-color:#776ece;padding:20px;color:#f4f4f8;font-size:13px;font-weight:600}.btn-remove-all:hover{background-color:#9d81ce}.empty{text-align:center;margin-top:20px}.empty__icon{margin:0 auto}.empty__text{color:#363368}@-webkit-keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}@keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}.aside__title{text-align:center;margin-top:50px}.aside__icon{color:#ce3e3e}@-webkit-keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}@keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}.lyrics{padding:25px 0 0 25px}.lyrics__heading{position:relative}.lyrics__heading,.lyrics__panel{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.lyrics__panel{-ms-flex-wrap:wrap;flex-wrap:wrap;padding:0 15px}@media screen and (min-width:690px){.lyrics__panel{-ms-flex-wrap:nowrap;flex-wrap:nowrap;padding:0;margin-bottom:50px}}.lyrics__lyrics{text-align:right;border-bottom:1px solid #dedede;padding-bottom:25px;text-align:justify}@media screen and (min-width:690px){.lyrics__lyrics{border-right:1px solid #dedede;border-bottom:0;padding-right:25px;padding-bottom:0;text-align:right}}.lyrics__translate{text-align:left}.lyrics__title{color:#363368}.lyrics__heart-icon{position:absolute;left:0;bottom:-10px;transition:all .3s}.lyrics__heart-icon:hover{-webkit-transform:scale(1.5);transform:scale(1.5)}.lyrics__heart-icon i{font-size:3.5em;color:#ce3e3e}.lyrics__link-icon{margin-left:5px}.lyrics__link-icon i{font-size:1.8em}@-webkit-keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}@keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}.panel__heading{padding:20px;text-align:center;margin-top:50px}.panel__title{color:#363368;font-size:2em}.panel__body{padding:20px;padding-left:5px}@media screen and (min-width:768px){.panel__body{padding:20px}}.form_inline{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.form__input{margin:10px;padding:10px;font-size:1.2em;border:1px solid #dedede;border-radius:3px;color:#363368;background-color:transparent}.form__input:focus{border:1px solid #776ece}.form__button{padding:6px 10px;background-color:transparent;color:#363368;position:absolute;right:-48px;top:11px;transition:all .3s}@media screen and (min-width:768px){.form__button{right:-57px}}.form__button:hover{background-color:#363368;border-radius:3px;color:#fff}@-webkit-keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}@keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}.home{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around;-ms-flex-wrap:wrap;flex-wrap:wrap}@media screen and (min-width:1200px){.home{max-width:80%;margin:0 auto}}.home__top{min-width:310px;display:initial}.home__center{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.home__form{margin-bottom:50px}@-webkit-keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}@keyframes flyin{0%{-webkit-transform:translateY(100%);transform:translateY(100%);opacity:0}}*{margin:0;padding:0;border:0}body{font:300 14px/1.4 Helvetica Neue,Helvetica,Arial,sans-serif;display:-webkit-box;display:-ms-flexbox;display:flex;min-height:100vh;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;background-color:#f9f9f9}ul{list-style:none}.btn{cursor:pointer}.text_center{text-align:center}.text_right{text-align:right}.text_left{text-align:left}.list-group{width:100%;-webkit-animation:1.25s flyin ease;animation:1.25s flyin ease}.list-group__item{position:relative;margin:20px 10px;transition:all .5s ease}.list-group__img{border-radius:20px;margin:10px 0}.list-group__title{color:#363368}.list-group__sub-title{text-align:center;color:#363368}.list-group__badge{position:absolute;right:-10px;top:19px;padding:3px;border-radius:5px;background-color:#9d81ce;color:#f4f4f8}.container,.container__flex_column{width:1060px;max-width:100%}@media (min-width:1200px){.container,.container__flex_column{width:1170px}}.container_center{margin:0 auto}.container__flex_column,.page{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.page{padding-top:50px}.page__title{color:#363368;margin-bottom:50px} 2 | /*# sourceMappingURL=styles.css.map*/ -------------------------------------------------------------------------------- /client/src/app/common/services/dataMock.js: -------------------------------------------------------------------------------- 1 | export const lyrics = { 2 | "type": "exact", 3 | "art": { 4 | "id": "3ade68b5ge177ed3213213a3", 5 | "name": "Pearl Jam", 6 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/" 7 | }, 8 | "mus": [{ 9 | "id": "3ade68b643456g3e1eeda3", 10 | "name": "Black", 11 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black.html", 12 | "lang": 2, 13 | "text": "", 14 | "translate": [{ 15 | "id": "3adeadasda68b7g2ece3ea3", 16 | "lang": 1, 17 | "url": "http:\/\/www.vagalume.com.br\/pearl-jam\/black-traducao.html", 18 | "text": "" 19 | }] 20 | }], 21 | "badwords": false 22 | }; 23 | 24 | export const top3Lyrics = { 25 | "mus": { 26 | "day": { 27 | "period": { 28 | "year": "2017", 29 | "day": "08", 30 | "month": "03" 31 | }, 32 | "all": [{ 33 | "id": "3ade68b8g0621b0b3", 34 | "name": "Trem Bala", 35 | "url": "https:\/\/www.vagalume.com.br\/ana-vilela\/trem-bala.html", 36 | "uniques": "0", 37 | "views": "17270", 38 | "rank": "0.0", 39 | "art": { 40 | "id": "3ade68b7g3fc13ea3", 41 | "name": "Ana Vilela", 42 | "url": "https:\/\/www.vagalume.com.br\/ana-vilela\/", 43 | "pic_small": "https:\/\/s2.vagalume.com\/ana-vilela\/images\/profile.jpg", 44 | "pic_medium": "https:\/\/s2.vagalume.com\/ana-vilela\/images\/ana-vilela.jpg" 45 | } 46 | }, { 47 | "id": "3ade68b8g7ee6fea3", 48 | "name": "Mulher (sexo Fr\u00e1gil)", 49 | "url": "https:\/\/www.vagalume.com.br\/erasmo-carlos\/mulher-sexo-fragil.html", 50 | "uniques": "0", 51 | "views": "13477", 52 | "rank": "0.0", 53 | "art": { 54 | "id": "3ade68b5g8248eda3", 55 | "name": "Erasmo Carlos", 56 | "url": "https:\/\/www.vagalume.com.br\/erasmo-carlos\/", 57 | "pic_small": "https:\/\/s2.vagalume.com\/erasmo-carlos\/images\/profile.jpg", 58 | "pic_medium": "https:\/\/s2.vagalume.com\/erasmo-carlos\/images\/erasmo-carlos.jpg" 59 | }, 60 | "albd": "Mulher", 61 | "alby": "1981", 62 | "alburl": "mulher" 63 | }, { 64 | "id": "3ade68b8gb645b0b3", 65 | "name": "Eu N\u00e3o Quero Casar", 66 | "url": "https:\/\/www.vagalume.com.br\/thiago-matheus\/eu-nao-quero-casar.html", 67 | "uniques": "0", 68 | "views": "10623", 69 | "rank": "0.0", 70 | "art": { 71 | "id": "3ade68b7g27d72ea3", 72 | "name": "Thiago Matheus", 73 | "url": "https:\/\/www.vagalume.com.br\/thiago-matheus\/", 74 | "pic_small": "https:\/\/s2.vagalume.com\/thiago-matheus\/images\/profile.jpg", 75 | "pic_medium": "https:\/\/s2.vagalume.com\/thiago-matheus\/images\/thiago-matheus.jpg" 76 | } 77 | }] 78 | } 79 | } 80 | }; 81 | 82 | export const top10Lyrics = { 83 | "mus": { 84 | "day": { 85 | "period": { 86 | "year": "2017", 87 | "day": "08", 88 | "month": "03" 89 | }, 90 | "all": [{ 91 | "id": "3ade68b8g0621b0b3", 92 | "name": "Trem Bala", 93 | "url": "https:\/\/www.vagalume.com.br\/ana-vilela\/trem-bala.html", 94 | "uniques": "0", 95 | "views": "17847", 96 | "rank": "0.0", 97 | "art": { 98 | "id": "3ade68b7g3fc13ea3", 99 | "name": "Ana Vilela", 100 | "url": "https:\/\/www.vagalume.com.br\/ana-vilela\/", 101 | "pic_small": "https:\/\/s2.vagalume.com\/ana-vilela\/images\/profile.jpg", 102 | "pic_medium": "https:\/\/s2.vagalume.com\/ana-vilela\/images\/ana-vilela.jpg" 103 | } 104 | }, { 105 | "id": "3ade68b8g7ee6fea3", 106 | "name": "Mulher (sexo Fr\u00e1gil)", 107 | "url": "https:\/\/www.vagalume.com.br\/erasmo-carlos\/mulher-sexo-fragil.html", 108 | "uniques": "0", 109 | "views": "13620", 110 | "rank": "0.0", 111 | "art": { 112 | "id": "3ade68b5g8248eda3", 113 | "name": "Erasmo Carlos", 114 | "url": "https:\/\/www.vagalume.com.br\/erasmo-carlos\/", 115 | "pic_small": "https:\/\/s2.vagalume.com\/erasmo-carlos\/images\/profile.jpg", 116 | "pic_medium": "https:\/\/s2.vagalume.com\/erasmo-carlos\/images\/erasmo-carlos.jpg" 117 | }, 118 | "albd": "Mulher", 119 | "alby": "1981", 120 | "alburl": "mulher" 121 | }, { 122 | "id": "3ade68b8gb645b0b3", 123 | "name": "Eu N\u00e3o Quero Casar", 124 | "url": "https:\/\/www.vagalume.com.br\/thiago-matheus\/eu-nao-quero-casar.html", 125 | "uniques": "0", 126 | "views": "10833", 127 | "rank": "0.0", 128 | "art": { 129 | "id": "3ade68b7g27d72ea3", 130 | "name": "Thiago Matheus", 131 | "url": "https:\/\/www.vagalume.com.br\/thiago-matheus\/", 132 | "pic_small": "https:\/\/s2.vagalume.com\/thiago-matheus\/images\/profile.jpg", 133 | "pic_medium": "https:\/\/s2.vagalume.com\/thiago-matheus\/images\/thiago-matheus.jpg" 134 | } 135 | }, { 136 | "id": "3ade68b8g0f56b0b3", 137 | "name": "Vidinha de Balada", 138 | "url": "https:\/\/www.vagalume.com.br\/henrique-e-juliano\/vidinha-de-balada.html", 139 | "uniques": "0", 140 | "views": "9958", 141 | "rank": "0.0", 142 | "art": { 143 | "id": "3ade68b7g73521ea3", 144 | "name": "Henrique e Juliano", 145 | "url": "https:\/\/www.vagalume.com.br\/henrique-e-juliano\/", 146 | "pic_small": "https:\/\/s2.vagalume.com\/henrique-e-juliano\/images\/profile.jpg", 147 | "pic_medium": "https:\/\/s2.vagalume.com\/henrique-e-juliano\/images\/henrique-e-juliano.jpg" 148 | } 149 | }, { 150 | "id": "3ade68b8g4a06b0b3", 151 | "name": "Shape Of You", 152 | "url": "https:\/\/www.vagalume.com.br\/ed-sheeran\/shape-of-you.html", 153 | "uniques": "0", 154 | "views": "8058", 155 | "rank": "0.0", 156 | "art": { 157 | "id": "3ade68b7g30dd1ea3", 158 | "name": "Ed Sheeran", 159 | "url": "https:\/\/www.vagalume.com.br\/ed-sheeran\/", 160 | "pic_small": "https:\/\/s2.vagalume.com\/ed-sheeran\/images\/profile.jpg", 161 | "pic_medium": "https:\/\/s2.vagalume.com\/ed-sheeran\/images\/ed-sheeran.jpg" 162 | }, 163 | "albd": "\u00f7", 164 | "alby": "2017", 165 | "alburl": "-10" 166 | }, { 167 | "id": "3ade68b7g02c47ea3", 168 | "name": "Mulher", 169 | "url": "https:\/\/www.vagalume.com.br\/elba-ramalho\/mulher.html", 170 | "uniques": "0", 171 | "views": "8003", 172 | "rank": "0.0", 173 | "art": { 174 | "id": "3ade68b5g0248eda3", 175 | "name": "Elba Ramalho", 176 | "url": "https:\/\/www.vagalume.com.br\/elba-ramalho\/", 177 | "pic_small": "https:\/\/s2.vagalume.com\/elba-ramalho\/images\/profile.jpg", 178 | "pic_medium": "https:\/\/s2.vagalume.com\/elba-ramalho\/images\/elba-ramalho.jpg" 179 | } 180 | }, { 181 | "id": "3ade68b8g65e2b0b3", 182 | "name": "Kakaka", 183 | "url": "https:\/\/www.vagalume.com.br\/ney-alves\/kakaka.html", 184 | "uniques": "0", 185 | "views": "7787", 186 | "rank": "0.0", 187 | "art": { 188 | "id": "3ade68b7gd3013ea3", 189 | "name": "Ney Alves", 190 | "url": "https:\/\/www.vagalume.com.br\/ney-alves\/", 191 | "pic_small": "https:\/\/s2.vagalume.com\/ney-alves\/images\/profile.jpg", 192 | "pic_medium": "https:\/\/s2.vagalume.com\/ney-alves\/images\/ney-alves.jpg" 193 | } 194 | }, { 195 | "id": "3ade68b8gdafd9fa3", 196 | "name": "Mulher", 197 | "url": "https:\/\/www.vagalume.com.br\/projota\/mulher.html", 198 | "uniques": "0", 199 | "views": "7491", 200 | "rank": "0.0", 201 | "art": { 202 | "id": "3ade68b7ge78a0ea3", 203 | "name": "Projota", 204 | "url": "https:\/\/www.vagalume.com.br\/projota\/", 205 | "pic_small": "https:\/\/s2.vagalume.com\/projota\/images\/profile.jpg", 206 | "pic_medium": "https:\/\/s2.vagalume.com\/projota\/images\/projota.jpg" 207 | }, 208 | "vevoID": "BR8JE1300019", 209 | "albd": "Muita Luz (Mixtape)", 210 | "alby": "2013", 211 | "alburl": "muita-luz-mixtape" 212 | }, { 213 | "id": "3ade68b8g4a06b0b3", 214 | "name": "Shape Of You (tradu\u00e7\u00e3o)", 215 | "url": "https:\/\/www.vagalume.com.br\/ed-sheeran\/shape-of-you-traducao.html", 216 | "uniques": "0", 217 | "views": "5585", 218 | "rank": "0.0", 219 | "art": { 220 | "id": "3ade68b7g30dd1ea3", 221 | "name": "Ed Sheeran", 222 | "url": "https:\/\/www.vagalume.com.br\/ed-sheeran\/", 223 | "pic_small": "https:\/\/s2.vagalume.com\/ed-sheeran\/images\/profile.jpg", 224 | "pic_medium": "https:\/\/s2.vagalume.com\/ed-sheeran\/images\/ed-sheeran.jpg" 225 | } 226 | }, { 227 | "id": "3ade68b8g8f8ea0b3", 228 | "name": "Starboy (Feat. Daft Punk) (tradu\u00e7\u00e3o)", 229 | "url": "https:\/\/www.vagalume.com.br\/the-weeknd\/starboy-feat-daft-punk-traducao.html", 230 | "uniques": "0", 231 | "views": "4292", 232 | "rank": "0.0", 233 | "art": { 234 | "id": "3ade68b7gf30e1ea3", 235 | "name": "The Weeknd", 236 | "url": "https:\/\/www.vagalume.com.br\/the-weeknd\/", 237 | "pic_small": "https:\/\/s2.vagalume.com\/the-weeknd\/images\/profile.jpg", 238 | "pic_medium": "https:\/\/s2.vagalume.com\/the-weeknd\/images\/the-weeknd.jpg" 239 | } 240 | }] 241 | } 242 | } 243 | } --------------------------------------------------------------------------------