Me cambie a react~~
This commit is contained in:
34
src/app.jsx
Normal file
34
src/app.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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 { }
|
||||
@@ -1 +0,0 @@
|
||||
<router-outlet></router-outlet>
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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!');
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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 { }
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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()}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 |
@@ -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
177
src/components/Paginate.jsx
Normal 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">«</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">»</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
108
src/components/Search.jsx
Normal 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})
|
||||
})
|
||||
}
|
||||
}
|
||||
38
src/components/SearchBar.jsx
Normal file
38
src/components/SearchBar.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export const environment = {
|
||||
production: true
|
||||
};
|
||||
@@ -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.
|
||||
BIN
src/favicon.ico
BIN
src/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 948 B |
@@ -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
21
src/index.jsx
Normal 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')
|
||||
);
|
||||
12
src/main.ts
12
src/main.ts
@@ -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));
|
||||
@@ -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
1
src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
9
src/services/search_service.js
Normal file
9
src/services/search_service.js
Normal 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
|
||||
}
|
||||
@@ -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
52
src/styles/main.css
Normal 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
78
src/styles/reset.css
Normal 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;
|
||||
}
|
||||
}
|
||||
25
src/test.ts
25
src/test.ts
@@ -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);
|
||||
Reference in New Issue
Block a user