Interfaz de home basica

This commit is contained in:
Daniel Cortes
2020-05-28 04:34:44 -04:00
parent 446ea8a4cf
commit 1985848922
22 changed files with 384 additions and 278 deletions

View File

@@ -1,8 +1,11 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {HomepageComponent} from "./homepage/homepage.component";
const routes: Routes = [];
const routes: Routes = [
{path: '', component: HomepageComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],

View File

@@ -1,66 +1 @@
<nav class="navbar is-primary" role="navigation" aria-label="main navigation">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item" href="/"><h1 class="title is-4 has-text-white">MusicList</h1></a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button is-info">
<strong>Sign up</strong>
</a>
<a class="button is-primary">Log in</a>
</div>
</div>
</div>
</div>
</div>
</nav>
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
<h1 class="title">Busca la musica que disfrutas!</h1>
<app-search></app-search>
</div>
</div>
</section>
<section class="section">
<div class="container">
<h1 class="is-size-4 ">Artistas populares</h1>
<div class="columns">
<div class="column" *ngFor="let _ of ' '.repeat(4).split(' ')">
<div class="card">
<div class="card-header">
<p class="card-header-title">Mago de oz</p>
</div>
<div class="card-image">
<figure class="image is-1by1">
<img src="https://ia801007.us.archive.org/9/items/mbid-fdf58ef9-c533-4335-bac6-28634f131a73/mbid-fdf58ef9-c533-4335-bac6-28634f131a73-23273731083.jpg">
</figure>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="section">
<div class="container">
<h1 class="is-size-4 ">Ultimas opiniones</h1>
<div class="columns">
<div class="column" *ngFor="let _ of ' '.repeat(2).split(' ')">
<div class="card">
<div class="card-header">
<p class="card-header-title">Jhon</p>
</div>
<div class="card-content">
Facere esse corrupti modi qui aut commodi. Saepe culpa corporis ut doloribus excepturi temporibus commodi
animi. Est ut ut enim dolorem fugit voluptatem. Qui sunt earum vel excepturi aliquam dolor numquam...
</div>
</div>
</div>
</div>
</div>
</section>
<router-outlet></router-outlet>

View File

@@ -3,13 +3,17 @@ import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { SearchComponent } from './search/search.component';
import { SearchHomeComponent } from './homepage/searchHome/searchHome.component';
import {HttpClientModule} from "@angular/common/http";
import { HomepageComponent } from './homepage/homepage.component';
import { NavComponent } from './nav/nav.component';
@NgModule({
declarations: [
AppComponent,
SearchComponent
SearchHomeComponent,
HomepageComponent,
NavComponent,
],
imports: [
HttpClientModule,

View File

@@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { BrainzService } from './brainz.service';
describe('BrainzService', () => {
let service: BrainzService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(BrainzService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -1,60 +0,0 @@
import {Injectable} from '@angular/core';
import {HttpClient, HttpErrorResponse, HttpHeaders} from '@angular/common/http';
import {Observable, of} from 'rxjs';
import {catchError, map, tap} from 'rxjs/operators';
import {BrainzError, Artist, BrowseReleaseGroup, CoverArt, ReleaseGroup} from './models/brainz';
@Injectable({
providedIn: 'root'
})
export class BrainzService {
private httpOptions = {
headers: new HttpHeaders({'content-type': 'application/json'})
};
private url = 'http://127.0.0.1:8000/api/brainz';
constructor(private http: HttpClient) {
}
private log(message: string) {
console.log(`BrainzService: ${message}`);
}
private handleError(operation: string) {
return (error: HttpErrorResponse): Observable<BrainzError> => {
const brainzError = error.error as BrainzError;
this.log(`${operation} failed: ${brainzError.status} - ${brainzError.error}`);
return of(brainzError);
};
}
getArtist(id: string): Observable<Artist | BrainzError> {
const url = `${this.url}/get/artist/${id}/`;
this.log(`Querying ${url}`);
return this.http.get<Artist>(url).pipe(
tap(artist => this.log(`getArtist found`)),
tap(artist => console.log(artist)),
catchError(this.handleError('getArtist'))
);
}
getDiscography(artistID: string): Observable<ReleaseGroup[] | BrainzError> {
const url = `${this.url}/browse/release-group/?artist=${artistID}&limit=10`;
this.log(`Querying ${url}`);
return this.http.get<BrowseReleaseGroup>(url).pipe(
map(discography => discography.release_groups),
tap(d => this.log(`getDiscography found`)),
tap(d => console.log(d)),
catchError(this.handleError('getDiscography'))
);
}
getCoverReleaseGroup(id: string): Observable<CoverArt | BrainzError> {
const url = `${this.url}/coverart/release-group/${id}/`;
return this.http.get<CoverArt>(url).pipe(
tap(cover => this.log(`cover found for ${id}`)),
catchError(this.handleError('getCoverReleaseGroup '))
);
};
}

View File

@@ -0,0 +1,10 @@
<app-nav></app-nav>
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
<h1 class="title">Busca la musica que disfrutas!</h1>
<app-search></app-search>
</div>
</div>
</section>

View File

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HomepageComponent } from './homepage.component';
describe('HomepageComponent', () => {
let component: HomepageComponent;
let fixture: ComponentFixture<HomepageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HomepageComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HomepageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-homepage',
templateUrl: './homepage.component.html',
styleUrls: ['./homepage.component.scss']
})
export class HomepageComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,41 @@
<div class="field has-addons">
<div class="control is-expanded">
<input #input class="input" type="search" (focus)="showList = true" (input)="searchTerm$.next(input.value)">
</div>
<div class="control">
<a class="button is-white"><span class="icon"><i class="fas fa-search"></i></span></a>
</div>
</div>
<div *ngIf="showList && areResults()" (mouseleave)="show()" class="autocomplete">
<div *ngIf="artists.length > 0">
<div class="header">
<p>Artistas</p>
</div>
<div *ngFor="let result of artists" class="artist">
{{ result.name }}
</div>
</div>
<div *ngIf="discs.length > 0">
<div class="header">
<p>Discos</p>
</div>
<div *ngFor="let result of discs" class="media disc">
<figure class="media-left">
<img *ngIf="result.cover_art" class="image is-64x64 cropped" src="{{result.cover_art.small}}" alt="{{result.title}} cover art">
<img *ngIf="!result.cover_art" class="image is-64x64 cropped" src="../../../assets/svg/placeholder.svg" alt="{{result.title}} missing cover art">
</figure>
<div class="media-content">
<p>{{result.title}}</p>
</div>
</div>
</div>
<div *ngIf="recordings.length > 0">
<div class="header">
<p>Canciones</p>
</div>
<div *ngFor="let result of recordings" class="recording">
{{ result.title }}
</div>
</div>
</div>

View File

@@ -0,0 +1,55 @@
@import "~bulma";
.media + .media {
margin-top: inherit;
border-top: inherit;
}
.field:not(:last-child) {
margin-bottom: 0;
}
.autocomplete {
position: absolute;
z-index: 2;
margin-top: 1em;
width: 40ch;
max-height: 30ch;
overflow-y: auto;
background-color: $body-background-color;
color: $text;
border: 1px solid $border;
.header {
padding: .6em .8em;
background: $grey-lightest;
font-weight: bold;
}
.artist, .disc, .recording {
padding-top: 1em;
padding-bottom: 1em;
padding-left: 1em;
&:not(:last-child) {
border-bottom: 1px solid $border;
}
&:hover {
background-color: $warning;
}
}
.disc {
padding-top: .5em;
padding-bottom: .5em;
padding-left: .5em;
}
}
.image.cropped{
object-fit: cover;
}

View File

@@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchComponent } from './search.component';
import { SearchHomeComponent } from './searchHome.component';
describe('SearchComponent', () => {
let component: SearchComponent;
let fixture: ComponentFixture<SearchComponent>;
let component: SearchHomeComponent;
let fixture: ComponentFixture<SearchHomeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SearchComponent ]
declarations: [ SearchHomeComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchComponent);
fixture = TestBed.createComponent(SearchHomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -0,0 +1,51 @@
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {BehaviorSubject, forkJoin, Subject} from "rxjs";
import {SearchService} from "../../search.service";
import {Artist, Disc, Recording} from "../../models/brainz";
@Component({
selector: 'app-search',
templateUrl: './searchHome.component.html',
styleUrls: ['./searchHome.component.scss']
})
export class SearchHomeComponent implements OnInit {
@ViewChild("input") input: ElementRef;
showList: boolean;
artists: Artist[] = [];
discs: Disc[] = [];
recordings: Recording[] = [];
searchTerm$ = new Subject<string>();
constructor(private searchService: SearchService) {
}
areResults() {
return this.artists.length > 0 || this.discs.length > 0 || this.recordings.length > 0;
}
show() {
if (this.input.nativeElement === document.activeElement) return true;
this.showList = false;
}
ngOnInit(): void {
this.searchTerm$.subscribe(() => {
this.artists = [];
this.discs = [];
this.recordings = [];
}
);
let searchArtist = this.searchService.searchArtist(this.searchTerm$).subscribe((result) => {
this.artists = result.artists;
});
let searchDisc = this.searchService.searchDisc(this.searchTerm$).subscribe((result) => {
this.discs = result.discs;
});
let searchRecording = this.searchService.searchRecording(this.searchTerm$).subscribe((result) => {
this.recordings = result.recordings;
});
}
}

View File

@@ -7,56 +7,74 @@ export function isBrainzError(object: any): boolean {
return 'status' in object && 'error' in object;
}
export interface LifeSpan {
begin: number;
end: number;
ended: boolean;
export interface Paginate {
total: number
current_page: number
last_page: number
per_page: number
}
export interface Area {
id: string;
type: string;
type_id: string;
name: string;
sort_name: string;
iso_3166_1_codes: string[];
disambiguation: string;
}
export interface Artist {
id: string;
isnis: string;
ipis: string;
name: string;
sort_name: string;
area: Area;
begin_area: Area;
end_area: Area;
country: string;
life_span: LifeSpan;
gender: string;
gender_id: string;
type: string;
disambiguation: string;
}
export interface ReleaseGroup {
id: string;
title: string;
first_release_date: string;
disambiguation: string;
primary_type: string;
primary_type_id: string;
secondary_types: string[];
secondary_type_ids: string[];
}
export interface BrowseReleaseGroup {
release_groups: ReleaseGroup[];
release_group_count: number;
release_group_offset: number;
export interface Tag {
name: string
count: number
}
export interface CoverArt {
link: string;
image: string
1200: string
500: string
250: string
large: string
small: string
}
export interface Artist {
id: string
name: string
sort_name: string
disambiguation: string
type: string
country: string
tags: Tag[]
}
export interface ArtistCredit {
id: string
name: string
sort_name: string
disambiguation: string
}
export interface ArtistSearch {
paginate: Paginate
artists: Artist[]
}
export interface Disc {
id: string
title: string
disambiguation: string
first_release_date: string
primary_type: string
secondary_type: string
cover_art: CoverArt
artists: ArtistCredit[]
}
export interface DiscSearch {
paginate: Paginate
discs: Disc[]
}
export interface Recording {
id: string
title: string
disambiguation: string
length: string
artist: ArtistCredit[]
}
export interface RecordingSearch {
paginate: Paginate
recordings: Recording[]
}

View File

@@ -0,0 +1,21 @@
<nav class="navbar is-primary" role="navigation" aria-label="main navigation">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item" href="/"><h1 class="title is-4 has-text-white">MusicList</h1></a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button is-info">
<strong>Sign up</strong>
</a>
<a class="button is-primary">Log in</a>
</div>
</div>
</div>
</div>
</div>
</nav>

View File

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NavComponent } from './nav.component';
describe('NavComponent', () => {
let component: NavComponent;
let fixture: ComponentFixture<NavComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ NavComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NavComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-nav',
templateUrl: './nav.component.html',
styleUrls: ['./nav.component.scss']
})
export class NavComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@@ -1,30 +1,64 @@
import {Injectable} from '@angular/core';
import {Observable, of} from "rxjs";
import {forkJoin, merge, Observable, of} from "rxjs";
import {HttpClient} from "@angular/common/http";
import {debounceTime, distinctUntilChanged, switchMap} from "rxjs/operators";
import {debounceTime, distinctUntilChanged, switchMap, tap} from "rxjs/operators";
import {ArtistSearch, DiscSearch, RecordingSearch} from "./models/brainz";
@Injectable({
providedIn: 'root'
})
export class SearchService {
baseUrl: string = 'https://api.cdnjs.com/libraries';
queryUrl: string = '?search=';
baseUrl: string = 'http://localhost:8000/api/brainz';
constructor(private http: HttpClient) {
}
search(terms: Observable<string>): Observable<any> {
searchArtist(terms: Observable<string>): Observable<ArtistSearch> {
return terms.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap(term => this.searchEntries(term))
switchMap(term => this.searchArtistQuery(term))
);
}
private searchEntries(term: string) {
if(!term){
return of({results: []});
searchDisc(terms: Observable<string>): Observable<DiscSearch> {
return terms.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap(term => this.searchDiscQuery(term))
);
}
return this.http.get(this.baseUrl + this.queryUrl + term);
searchRecording(terms: Observable<string>): Observable<RecordingSearch> {
return terms.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap(term => this.searchRecordingQuery(term))
);
}
private searchArtistQuery(term: string): Observable<ArtistSearch> {
if (!term) {
return of({paginate: {total: 0, current_page: 0, last_page: 0, per_page: 0}, artists: []} as ArtistSearch);
}
return this.http.get<ArtistSearch>(`${this.baseUrl}/artist?query=${term}`);
}
private searchDiscQuery(term: string): Observable<DiscSearch> {
if (!term) {
return of({paginate: {total: 0, current_page: 0, last_page: 0, per_page: 0}, discs: []} as DiscSearch);
}
return this.http.get<DiscSearch>(`${this.baseUrl}/disc?query=${term}`);
}
private searchRecordingQuery(term: string): Observable<RecordingSearch> {
if (!term) {
return of({paginate: {total: 0, current_page: 0, last_page: 0, per_page: 0}, recordings: []} as RecordingSearch);
}
return this.http.get<RecordingSearch>(`${this.baseUrl}/recording?query=${term}`);
}
}

View File

@@ -1,14 +0,0 @@
<div class="field has-addons">
<div class="control is-expanded">
<input #searchbox class="input" type="search" (input)="searchTerm$.next(searchbox.value)" (focus)="setFocus(true)" (blur)="setFocus(false)" >
</div>
<div class="control">
<a class="button is-white"><span class="icon"><i class="fas fa-search"></i></span></a>
</div>
</div>
<ul *ngIf="results.length !== 0 && isFocused" class="autocomplete">
<li *ngFor="let result of results | slice:0:9">
{{ result.name }}
</li>
</ul>

View File

@@ -1,30 +0,0 @@
@import "~bulma";
.field:not(:last-child){
margin-bottom: 0;
}
.autocomplete {
position: absolute;
z-index: 2;
margin-top: 1em;
width: 40ch;
max-height: 30ch;
overflow-y: auto;
background-color: $body-background-color;
color: $text;
border: 1px solid $border;
}
.autocomplete li {
padding-top: 1em;
padding-bottom: 1em;
padding-left: 1em;
}
.autocomplete li:not(:last-child) {
border-bottom: 1px solid $border;
}
.autocomplete li:hover {
background-color: $warning;
}

View File

@@ -1,26 +0,0 @@
import { Component, OnInit } from '@angular/core';
import {Subject} from "rxjs";
import {SearchService} from "../search.service";
@Component({
selector: 'app-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit {
results: Object[] = [];
searchTerm$ = new Subject<string>();
isFocused: boolean = false;
constructor(private searchService: SearchService) {
this.searchService.search(this.searchTerm$).subscribe(results => this.results = results.results);
}
setFocus(status: boolean): void {
this.isFocused = status;
}
ngOnInit(): void {
}
}