Me cambie a react~~

This commit is contained in:
Daniel Cortes
2020-05-30 04:10:59 -04:00
parent ca1157978c
commit 094cac9310
67 changed files with 7955 additions and 7529 deletions

34
src/app.jsx Normal file
View File

@@ -0,0 +1,34 @@
import React from "react";
import SearchBar from "./components/SearchBar";
export class Nav extends React.Component {
render() {
return (
<nav className='nav'>
<h1>MusicList</h1>
<ul className='nav-links'>
<li className='link'><a href='/login'>Iniciar Sesion</a></li>
<li className='link'><a href='/signup'>Registrate</a></li>
</ul>
</nav>
)
}
}
export function Main() {
return (
<main>
<Nav/>
<h1>Busca la musica que te gusta!</h1>
<SearchBar/>
</main>
)
}
export function NoRoute() {
return (
<div>
<h1>Esa pagina no existe</h1>
</div>
)
}

View File

@@ -1,16 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {HomepageComponent} from "./homepage/homepage.component";
import {SearchComponent} from "./search/search.component";
const routes: Routes = [
{path: '', component: HomepageComponent},
{path: 'search', component: SearchComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@@ -1 +0,0 @@
<router-outlet></router-outlet>

View File

@@ -1 +0,0 @@

View File

@@ -1,35 +0,0 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'client'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('client');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('client app is running!');
});
});

View File

@@ -1,10 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'client';
}

View File

@@ -1,28 +0,0 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { SearchBarComponent } from './homepage/searchBar/searchBar.component';
import {HttpClientModule} from "@angular/common/http";
import { HomepageComponent } from './homepage/homepage.component';
import { NavComponent } from './nav/nav.component';
import { SearchComponent } from './search/search.component';
@NgModule({
declarations: [
AppComponent,
SearchBarComponent,
HomepageComponent,
NavComponent,
SearchComponent,
],
imports: [
HttpClientModule,
BrowserModule,
AppRoutingModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@@ -1,10 +0,0 @@
<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-bar></app-search-bar>
</div>
</div>
</section>

View File

@@ -1,25 +0,0 @@
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

@@ -1,15 +0,0 @@
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

@@ -1,43 +0,0 @@
<div class="field has-addons">
<div class="control is-expanded">
<input #input class="input" type="search" (focus)="showList = true" [value]="query$.getValue()" (input)="query$.next(input.value)" (keyup.enter)="search()">
</div>
<div class="control">
<a class="button" [class]="button_color"><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

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

View File

@@ -1,66 +0,0 @@
import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
import {BehaviorSubject, forkJoin, Subject} from "rxjs";
import {SearchService} from "../../services/search.service";
import {Artist, Disc, Recording} from "../../models/brainz";
import {Router} from "@angular/router";
@Component({
selector: 'app-search-bar',
templateUrl: './searchBar.component.html',
styleUrls: ['./searchBar.component.scss']
})
export class SearchBarComponent implements OnInit {
@ViewChild("input") input: ElementRef;
showList: boolean;
artists: Artist[] = [];
discs: Disc[] = [];
recordings: Recording[] = [];
@Input() query$ = new BehaviorSubject<string>("");
@Input() button_color = "is-white";
@Input() autocomplete = true;
constructor(private searchService: SearchService, private router: Router) {
}
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.query$.subscribe(() => {
this.artists = [];
this.discs = [];
this.recordings = [];
}
);
let searchArtist = this.searchService.searchArtist(this.query$).subscribe((result) => {
this.artists = result.artists;
});
let searchDisc = this.searchService.searchDisc(this.query$).subscribe((result) => {
this.discs = result.discs;
});
let searchRecording = this.searchService.searchRecording(this.query$).subscribe((result) => {
this.recordings = result.recordings;
});
}
search() {
this.router.navigate(['search'], {
state: {
artists: this.artists,
discs: this.discs,
recordings: this.recordings
},
queryParams: {query: this.query$.getValue()}
});
}
}

View File

@@ -1,80 +0,0 @@
export interface BrainzError {
status: number;
error: string;
}
export function isBrainzError(object: any): boolean {
return 'status' in object && 'error' in object;
}
export interface Paginate {
total: number
current_page: number
last_page: number
per_page: number
}
export interface Tag {
name: string
count: number
}
export interface CoverArt {
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

@@ -1,21 +0,0 @@
<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

@@ -1,25 +0,0 @@
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

@@ -1,15 +0,0 @@
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,60 +0,0 @@
<app-nav></app-nav>
<section class="hero">
<div class="hero-body">
<div class="container">
<app-search-bar [query$]="query$" [autocomplete]="false" [button_color]="'is-primary'"></app-search-bar>
</div>
</div>
</section>
<div class="container tabs is-boxed">
<ul>
<li [class.is-active]="artists_active" (click)="show_artist()">
<a>
<span class="icon is-small"><i class="fas fa-user" aria-hidden="true"></i></span>
<span>Artistas</span>
</a>
</li>
<li [class.is-active]="discs_active" (click)="show_discs()">
<a>
<span class="icon is-small"><i class="fas fa-compact-disc" aria-hidden="true"></i></span>
<span>Discos</span>
</a>
</li>
<li [class.is-active]="songs_active" (click)="show_songs()">
<a>
<span class="icon is-small"><i class="fas fa-music" aria-hidden="true"></i></span>
<span>Canciones</span>
</a>
</li>
</ul>
</div>
<ng-template [ngIf]="artists_active">
<div class="container">
<div class="media" *ngFor="let artist of artists">
<div class="media-content">
<p>{{artist.name}}</p>
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="discs_active">
<div class="container">
<div class="media" *ngFor="let disc of discs">
<div class="media-content">
<p>{{disc.title}}</p>
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="songs_active">
<div class="container">
<div class="media" *ngFor="let song of recordings">
<div class="media-content">
<p>{{song.title}}</p>
</div>
</div>
</div>
</ng-template>

View File

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

View File

@@ -1,66 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from "@angular/router";
import {SearchService} from "../services/search.service";
import {map} from "rxjs/operators";
import {Artist, Disc, Recording} from "../models/brainz";
import {BehaviorSubject, Observable} from "rxjs";
@Component({
selector: 'app-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit {
query$ = new BehaviorSubject<string>("");
artists: Artist[] = [];
discs: Disc[] = [];
recordings: Recording[] = [];
artists_active = true;
discs_active = false;
songs_active= false;
constructor(private route: ActivatedRoute, private searchService: SearchService) {
this.route.queryParams.pipe(map(params => params['query'] as string)).subscribe((query) => {
this.query$.next(query);
});
}
ngOnInit(): void {
this.query$.subscribe(() => {
this.artists = [];
this.discs = [];
this.recordings = [];
});
this.searchService.searchArtist(this.query$).subscribe((result) => this.artists = result.artists);
this.searchService.searchDisc(this.query$).subscribe((result) => this.discs = result.discs);
this.searchService.searchRecording(this.query$).subscribe((result) => this.recordings = result.recordings);
}
show_artist() {
if(this.artists_active) return;
this.artists_active = true;
this.discs_active = false;
this.songs_active = false;
}
show_discs() {
if(this.discs_active) return;
this.artists_active = false;
this.discs_active = true;
this.songs_active = false;
}
show_songs() {
if(this.songs_active) return;
this.artists_active = false;
this.discs_active = false;
this.songs_active = true;
}
}

View File

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

View File

@@ -1,64 +0,0 @@
import {Injectable} from '@angular/core';
import {forkJoin, merge, Observable, of} from "rxjs";
import {HttpClient} from "@angular/common/http";
import {debounceTime, distinctUntilChanged, switchMap, tap} from "rxjs/operators";
import {ArtistSearch, DiscSearch, RecordingSearch} from "../models/brainz";
@Injectable({
providedIn: 'root'
})
export class SearchService {
baseUrl: string = 'http://localhost:8000/api/brainz';
constructor(private http: HttpClient) {
}
searchArtist(terms: Observable<string>, page: number = 1): Observable<ArtistSearch> {
return terms.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap(term => this.searchArtistQuery(term, page))
);
}
searchDisc(terms: Observable<string>, page: number = 1): Observable<DiscSearch> {
return terms.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap(term => this.searchDiscQuery(term, page))
);
}
searchRecording(terms: Observable<string>, page: number = 1): Observable<RecordingSearch> {
return terms.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap(term => this.searchRecordingQuery(term, page))
);
}
private searchArtistQuery(term: string, page: number): 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}&page=${page}`);
}
private searchDiscQuery(term: string, page: number): 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, page: number): 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,8 +0,0 @@
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#9281FF"/>
<circle cx="50%" cy="50%" r="40%" fill="white" />
<circle cx="50%" cy="50%" r="15%" fill="#9281FF" />
<circle cx="50%" cy="50%" r="12%" fill="white" />
<circle cx="50%" cy="50%" r="8%" fill="#9281FF" />
</svg>

Before

Width:  |  Height:  |  Size: 337 B

View File

@@ -1,8 +0,0 @@
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#eaeaea"/>
<circle cx="50%" cy="50%" r="40%" fill="white" />
<circle cx="50%" cy="50%" r="15%" fill="#eaeaea" />
<circle cx="50%" cy="50%" r="12%" fill="white" />
<circle cx="50%" cy="50%" r="8%" fill="#eaeaea" />
</svg>

Before

Width:  |  Height:  |  Size: 337 B

177
src/components/Paginate.jsx Normal file
View File

@@ -0,0 +1,177 @@
import React, {Component, Fragment} from 'react';
const LEFT_PAGE = 'LEFT';
const RIGHT_PAGE = 'RIGHT';
/**
* Helper method for creating a range of numbers
* range(1, 5) => [1, 2, 3, 4, 5]
*/
const range = (from, to, step = 1) => {
let i = from;
const range = [];
while (i <= to) {
range.push(i);
i += step;
}
return range;
}
export class Paginate extends Component {
constructor(props) {
super(props);
const {totalRecords = null, pageLimit = 30, pageNeighbours = 0} = props;
this.pageLimit = typeof pageLimit === 'number' ? pageLimit : 30;
this.totalRecords = typeof totalRecords === 'number' ? totalRecords : 0;
this.pageNeighbours = typeof pageNeighbours === 'number' ? Math.max(0, Math.min(pageNeighbours, 2)) : 0;
this.totalPages = Math.ceil(this.totalRecords / this.pageLimit);
this.state = {currentPage: 1};
}
componentDidMount() {
//this.gotoPage(1);
}
gotoPage = page => {
const {onPageChanged = f => f} = this.props;
const currentPage = Math.max(0, Math.min(page, this.totalPages));
this.setState({currentPage}, () => onPageChanged(this.makePageLink(currentPage)));
}
handleClick = page => evt => {
evt.preventDefault();
this.gotoPage(page);
}
handleMoveLeft = evt => {
evt.preventDefault();
this.gotoPage(this.state.currentPage - (this.pageNeighbours * 2) - 1);
}
handleMoveRight = evt => {
evt.preventDefault();
this.gotoPage(this.state.currentPage + (this.pageNeighbours * 2) + 1);
}
makePageLink = page => {
const {makeLink = f => f} = this.props;
return makeLink(page);
}
/**
* Let's say we have 10 pages and we set pageNeighbours to 2
* Given that the current page is 6
* The pagination control will look like the following:
*
* (1) < {4 5} [6] {7 8} > (10)
*
* (x) => terminal pages: first and last page(always visible)
* [x] => represents current page
* {...x} => represents page neighbours
*/
fetchPageNumbers = () => {
const totalPages = this.totalPages;
const currentPage = this.state.currentPage;
const pageNeighbours = this.pageNeighbours;
/**
* totalNumbers: the total page numbers to show on the control
* totalBlocks: totalNumbers + 2 to cover for the left(<) and right(>) controls
*/
const totalNumbers = (this.pageNeighbours * 2) + 3;
const totalBlocks = totalNumbers + 2;
if (totalPages > totalBlocks) {
const startPage = Math.max(2, currentPage - pageNeighbours);
const endPage = Math.min(totalPages - 1, currentPage + pageNeighbours);
let pages = range(startPage, endPage);
/**
* hasLeftSpill: has hidden pages to the left
* hasRightSpill: has hidden pages to the right
* spillOffset: number of hidden pages either to the left or to the right
*/
const hasLeftSpill = startPage > 2;
const hasRightSpill = (totalPages - endPage) > 1;
const spillOffset = totalNumbers - (pages.length + 1);
switch (true) {
// handle: (1) < {5 6} [7] {8 9} (10)
case (hasLeftSpill && !hasRightSpill): {
const extraPages = range(startPage - spillOffset, startPage - 1);
pages = [LEFT_PAGE, ...extraPages, ...pages];
break;
}
// handle: (1) {2 3} [4] {5 6} > (10)
case (!hasLeftSpill && hasRightSpill): {
const extraPages = range(endPage + 1, endPage + spillOffset);
pages = [...pages, ...extraPages, RIGHT_PAGE];
break;
}
// handle: (1) < {4 5} [6] {7 8} > (10)
case (hasLeftSpill && hasRightSpill):
default: {
pages = [LEFT_PAGE, ...pages, RIGHT_PAGE];
break;
}
}
return [1, ...pages, totalPages];
}
return range(1, totalPages);
}
render() {
if (!this.totalRecords || this.totalPages === 1) return null;
const {currentPage} = this.state;
const pages = this.fetchPageNumbers();
return (
<Fragment>
<nav aria-label="Countries Pagination">
<ul className="pagination">
{pages.map((page, index) => {
if (page === LEFT_PAGE) return (
<li key={index} className="page-item">
<a className="page-link" href={this.makePageLink(index)} aria-label="Previous"
onClick={this.handleMoveLeft}>
<span aria-hidden="true">&laquo;</span>
<span className="sr-only">Previous</span>
</a>
</li>
);
if (page === RIGHT_PAGE) return (
<li key={index} className="page-item">
<a className="page-link" href={this.makePageLink(index)} aria-label="Next"
onClick={this.handleMoveRight}>
<span aria-hidden="true">&raquo;</span>
<span className="sr-only">Next</span>
</a>
</li>
);
return (
<li key={index} className={`page-item${currentPage === page ? ' active' : ''}`}>
<a className="page-link" href={this.makePageLink(index)}
onClick={this.handleClick(page)}>{page}</a>
</li>
);
})}
</ul>
</nav>
</Fragment>
);
}
}

108
src/components/Search.jsx Normal file
View File

@@ -0,0 +1,108 @@
import React from "react";
import queryString from "query-string";
import ReactJson from "react-json-view";
import {searchArtist} from "../services/search_service";
import SearchBar from "./SearchBar";
import {Paginate} from "./Paginate";
import {navigate} from "@reach/router";
class SearchList extends React.Component {
constructor(props) {
super(props);
this.buildList(this.props.artists);
}
buildList(artists) {
}
render() {
return (
<div>
</div>
)
}
}
export class Search extends React.Component {
constructor(props) {
super(props);
this.state = {
artists: null
};
this.getParams = this.getParams.bind(this);
this.makeLink = this.makeLink.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
}
static getDerivedStateFromProps(props, state) {
if (props.location.search !== state.prevSearch) {
return {
artists: null,
prevSearch: props.location.search
}
}
return null;
}
componentDidMount() {
this.loadArtists(this.getParams().query, this.getParams().page);
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.artists == null) {
this.loadArtists(this.getParams().query, this.getParams().page);
}
}
makeLink(page) {
return `/search?query=${this.getParams().query}&page=${page}`
}
handlePageChange(page) {
navigate(page);
}
render() {
if (this.state.artists) {
const total = this.state.artists.paginate.total;
const pageLimit = this.state.artists.paginate.per_page;
return (
<main>
<h1>Busqueda</h1>
<SearchBar query={this.getParams().query}/>
<SearchList artists={this.state.artists.artists}/>
<Paginate totalRecords={total} pageLimit={pageLimit} pageNeighbours={1} onPageChanged={this.handlePageChange} makeLink={this.makeLink}/>
<ReactJson src={this.state.artists} enableClipboard={false} displayDataTypes={false}/>
</main>
);
} else {
return (
<main>
<h1>Busqueda</h1>
<SearchBar query={this.query}/>
<p>Loading...</p>
</main>
);
}
}
getParams() {
return queryString.parse(this.props.location.search);
}
loadArtists(query, page) {
if(!page){
page = 1;
}
searchArtist(query, page).then((response) => {
this.setState({artists: response})
})
}
}

View File

@@ -0,0 +1,38 @@
import React from "react";
import {FaSearch} from 'react-icons/fa';
import {navigate} from "@reach/router";
export default class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {query: ''};
if (this.props.query) {
this.state = {query: this.props.query};
}
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
const query = event.target.value;
this.setState({query: query});
}
handleSubmit(event, who) {
if (event.key === 'Enter' || who === 'button') {
navigate(`/search?query=${this.state.query}`);
}
}
render() {
return (
<div className='input-with-icon'>
<input className='full-width' value={this.state.query} onKeyUp={this.handleSubmit}
onChange={this.handleChange}/>
<button onClick={(e) => this.handleSubmit(e, 'button')}><FaSearch/></button>
</div>
);
}
}

View File

@@ -1,3 +0,0 @@
export const environment = {
production: true
};

View File

@@ -1,16 +0,0 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Client</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/svg+xml" href="assets/svg/favicon.svg">
</head>
<body>
<app-root></app-root>
</body>
</html>

21
src/index.jsx Normal file
View File

@@ -0,0 +1,21 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {Main, NoRoute} from "./app";
import {Search} from './components/Search';
import './styles/reset.css';
import './styles/main.css';
import {Router} from "@reach/router";
const App = () => (
<Router>
<Main path='/'/>
<Search path='/search'/>
<NoRoute default/>
</Router>
);
ReactDOM.render(
<App/>,
document.getElementById('root')
);

View File

@@ -1,12 +0,0 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

View File

@@ -1,63 +0,0 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

1
src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -0,0 +1,9 @@
import axios from 'axios'
let baseUrl = 'http://localhost:8000/api/brainz';
export async function searchArtist(query, page) {
const url = `${baseUrl}/artist?query=${query}&page=${page}`;
const response = await axios.get(url);
return response.data
}

View File

@@ -1,14 +0,0 @@
$purple: hsl(249, 100%, 75%);
$purple: #9380ff;
$blue: hsl(215, 100%, 74%);
$pink: hsl(272, 100%, 74%);
$yellow: hsl(49, 100%, 67%);
$primary: $purple;
$info: $blue;
$warning: $yellow;
@import "~bulma";
@import "~@fortawesome/fontawesome-free/css/all.css";

52
src/styles/main.css Normal file
View File

@@ -0,0 +1,52 @@
:root {
--gray: hsl(0, 0%, 85%);
}
body {
max-width: 75em;
margin: 0 auto;
padding: 0 1em;
font-family: sans-serif;
}
input {
border: 1px var(--gray) solid;
padding: .3em;
}
button {
border: 1px var(--gray) solid;
}
.nav{
display: flex;
min-height: 3.25em;
position: relative;
justify-content: space-between;
align-items: center;
}
.nav-links {
display: flex;
}
.nav-links .link {
margin-left: 1em;
}
.full-width {
width: 100%;
}
.input-with-icon {
display: flex;
}
.input-with-icon input {
border-right: none;
}
.input-with-icon button {
border-left: none;
background: white;
}

78
src/styles/reset.css Normal file
View File

@@ -0,0 +1,78 @@
/* Box sizing rules */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Remove default padding */
ul[class],
ol[class] {
padding: 0;
}
/* Remove default margin */
body,
h1,
h2,
h3,
h4,
p,
ul[class],
ol[class],
li,
figure,
figcaption,
blockquote,
dl,
dd {
margin: 0;
}
/* Set core body defaults */
body {
min-height: 100vh;
scroll-behavior: smooth;
text-rendering: optimizeSpeed;
line-height: 1.5;
}
/* Remove list styles on ul, ol elements with a class attribute */
ul[class],
ol[class] {
list-style: none;
}
/* A elements that don't have a class get default styles */
a:not([class]) {
text-decoration-skip-ink: auto;
}
/* Make images easier to work with */
img {
max-width: 100%;
display: block;
}
/* Natural flow and rhythm in articles by default */
article > * + * {
margin-top: 1em;
}
/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select {
font: inherit;
}
/* Remove all animations and transitions for people that prefer not to see them */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

View File

@@ -1,25 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);