Basisimplementierung Songlist

This commit is contained in:
2019-11-24 15:57:20 +01:00
committed by smuddy
parent 9897e66d50
commit 87aeb62a2a
57 changed files with 777 additions and 59 deletions

View File

@@ -1,11 +1,22 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
const routes: Routes = [];
const routes: Routes = [
{
path: '',
redirectTo: 'songs',
pathMatch: 'full'
},
{
path: 'songs',
loadChildren: () => import('./songs/songs.module').then(m => m.SongsModule)
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
export class AppRoutingModule {
}

View File

@@ -0,0 +1,4 @@
<app-navigation></app-navigation>
<div class="content">
<router-outlet></router-outlet>
</div>

View File

@@ -0,0 +1,7 @@
h1 {
color: red;
}
.content {
margin-top: 80px;
}

View File

@@ -1,6 +1,6 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import {async, TestBed} from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import {AppComponent} from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
@@ -19,17 +19,4 @@ describe('AppComponent', () => {
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'wgenerator'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('wgenerator');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('wgenerator app is running!');
});
});

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import {Component} from '@angular/core';
@Component({
selector: 'app-root',

View File

@@ -1,11 +1,14 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {ServiceWorkerModule} from '@angular/service-worker';
import {environment} from '../environments/environment';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {ApplicationFrameModule} from "./application-frame/application-frame.module";
import {AngularFireModule} from "@angular/fire";
import {AngularFirestoreModule} from "@angular/fire/firestore";
@NgModule({
declarations: [
@@ -14,10 +17,18 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
imports: [
BrowserModule,
AppRoutingModule,
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
BrowserAnimationsModule
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production}),
BrowserAnimationsModule,
ApplicationFrameModule,
AngularFireModule.initializeApp(environment.firebase),
AngularFirestoreModule.enablePersistence(),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
export class AppModule {
}

View File

@@ -0,0 +1,20 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {NavigationComponent} from './navigation/navigation.component';
import {RouterModule} from "@angular/router";
@NgModule({
declarations: [
NavigationComponent
],
imports: [
CommonModule,
RouterModule
],
exports: [
NavigationComponent
]
})
export class ApplicationFrameModule {
}

View File

@@ -0,0 +1,5 @@
<nav class="head">
<a href="#" routerLink="/songs" routerLinkActive="active">Inhalt</a>
</nav>

View File

@@ -0,0 +1,40 @@
@import "../../../styles/styles";
@import "../../../styles/shadow";
nav {
&.head {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background: @navigation-background;
.card-3;
display: flex;
align-items: flex-end;
a {
display: block;
height: 60px;
color: @navigation-link-color;
font-size: 20px;
font-weight: bold;
text-decoration: none;
padding: 20px;
box-sizing: border-box;
background: transparent;
transition: @transition;
&:hover {
background: #eee;
}
&.active {
background: @primary-color;
color: #fff;
}
}
}
}

View File

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

View File

@@ -0,0 +1,16 @@
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.less']
})
export class NavigationComponent implements OnInit {
constructor() {
}
ngOnInit() {
}
}

View File

@@ -0,0 +1,10 @@
export interface Song {
comment: string;
final: boolean;
key: string;
number: number;
tempo: number;
text: string;
title: string;
type: string;
}

View File

@@ -0,0 +1,41 @@
import {async, TestBed} from '@angular/core/testing';
import {SongDataService} from './song-data.service';
import {AngularFirestore} from "@angular/fire/firestore";
import {of} from "rxjs";
describe('SongDataService', () => {
const songs = [
{title: 'title1'}
];
const angularFirestoreCollection = {
valueChanges: () => of(songs)
};
const mockAngularFirestore = {
collection: path => angularFirestoreCollection
};
beforeEach(() => TestBed.configureTestingModule({
providers: [
{provide: AngularFirestore, useValue: mockAngularFirestore}
]
}));
it('should be created', () => {
const service: SongDataService = TestBed.get(SongDataService);
expect(service).toBeTruthy();
});
it('should list songs', async(() => {
const service: SongDataService = TestBed.get(SongDataService);
service.list().subscribe(songs => {
expect(songs).toEqual(<any>[
{title: 'title1'}
]);
}
)
}));
});

View File

@@ -0,0 +1,20 @@
import {Injectable} from '@angular/core';
import {AngularFirestore, AngularFirestoreCollection} from "@angular/fire/firestore";
import {Song} from "../models/song";
import {Observable} from "rxjs";
@Injectable({
providedIn: 'root'
})
export class SongDataService {
private songCollection: AngularFirestoreCollection<Song>;
private songs: Observable<Song[]>;
constructor(private afs: AngularFirestore) {
this.songCollection = afs.collection<Song>('songs');
this.songs = this.songCollection.valueChanges();
}
public list = (): Observable<Song[]> => this.songs;
}

View File

@@ -0,0 +1,36 @@
import {async, TestBed} from '@angular/core/testing';
import {SongService} from './song.service';
import {SongDataService} from "./song-data.service";
import {of} from "rxjs";
describe('SongService', () => {
const songs = [
{title: 'title1'}
];
const mockSongDataService = {
list: () => of(songs)
};
beforeEach(() => TestBed.configureTestingModule({
providers: [
{provide: SongDataService, useValue: mockSongDataService}
]
}));
it('should be created', () => {
const service: SongService = TestBed.get(SongService);
expect(service).toBeTruthy();
});
it('should list songs', async(() => {
const service: SongService = TestBed.get(SongService);
service.list().subscribe(songs => {
expect(songs).toEqual(<any>[
{title: 'title1'}
]);
});
}));
});

View File

@@ -0,0 +1,16 @@
import {Injectable} from '@angular/core';
import {Observable} from "rxjs";
import {Song} from "../models/song";
import {SongDataService} from "./song-data.service";
@Injectable({
providedIn: 'root'
})
export class SongService {
constructor(private songDataService: SongDataService) {
}
public list = (): Observable<Song[]> => this.songDataService.list();
}

View File

@@ -0,0 +1,6 @@
<div class="list-item">
<div class="number">{{song.number}}</div>
<div>{{song.title}}</div>
<div>{{song.key}}</div>
<div>{{song.type | songType}}</div>
</div>

View File

@@ -0,0 +1,23 @@
@import "../../../../styles/styles";
.list-item {
padding: 10px 20px;
display: grid;
grid-template-columns: 50px auto 30px 100px;
& > div {
display: flex;
align-items: center;
}
cursor: pointer;
&:hover {
background: @primary-color;
}
}
.number {
font-size: 18px;
font-weight: bold;
}

View File

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

View File

@@ -0,0 +1,18 @@
import {Component, Input, OnInit} from '@angular/core';
import {Song} from "../../models/song";
@Component({
selector: 'app-list-item',
templateUrl: './list-item.component.html',
styleUrls: ['./list-item.component.less']
})
export class ListItemComponent implements OnInit {
@Input() public song: Song;
constructor() {
}
ngOnInit() {
}
}

View File

@@ -0,0 +1,3 @@
<app-card [padding]="false">
<app-list-item *ngFor="let song of songs" [song]="song"></app-list-item>
</app-card>

View File

@@ -0,0 +1,2 @@
.song-list {
}

View File

@@ -0,0 +1,47 @@
import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {SongListComponent} from './song-list.component';
import {of} from "rxjs";
import {SongService} from "../services/song.service";
import {NO_ERRORS_SCHEMA} from "@angular/core";
describe('SongListComponent', () => {
let component: SongListComponent;
let fixture: ComponentFixture<SongListComponent>;
const songs = [
{title: 'title1'}
];
const mockSongService = {
list: () => of(songs)
};
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SongListComponent],
providers: [
{provide: SongService, useValue: mockSongService}
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SongListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should read songs from SongService', fakeAsync(() => {
tick();
expect(component.songs).toEqual(<any>[
{title: 'title1'}
]);
}));
});

View File

@@ -0,0 +1,22 @@
import {Component, OnInit} from '@angular/core';
import {SongService} from "../services/song.service";
import {Song} from "../models/song";
@Component({
selector: 'app-songs',
templateUrl: './song-list.component.html',
styleUrls: ['./song-list.component.less']
})
export class SongListComponent implements OnInit {
public songs: Song[];
constructor(private songService: SongService) {
}
ngOnInit() {
this.songService.list().subscribe(songs => {
this.songs = songs.sort((a, b) => a.number - b.number);
});
}
}

View File

@@ -0,0 +1,20 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {SongListComponent} from "./song-list.component";
import {ListItemComponent} from './list-item/list-item.component';
import {CardModule} from "../../widget-modules/components/card/card.module";
import {SongTypeTranslaterModule} from "../../widget-modules/pipes/song-type-translater/song-type-translater.module";
@NgModule({
declarations: [SongListComponent, ListItemComponent],
exports: [SongListComponent],
imports: [
CommonModule,
CardModule,
SongTypeTranslaterModule
]
})
export class SongListModule {
}

View File

@@ -0,0 +1 @@
<p>song works with songId: {{songId}}!</p>

View File

View File

@@ -0,0 +1,39 @@
import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {SongComponent} from './song.component';
import {of} from "rxjs";
import {ActivatedRoute} from "@angular/router";
describe('SongComponent', () => {
let component: SongComponent;
let fixture: ComponentFixture<SongComponent>;
const mockActivatedRoute = {
params: of({songId: '4711'})
};
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SongComponent],
providers: [
{provide: ActivatedRoute, useValue: mockActivatedRoute}
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SongComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should provide songId', fakeAsync(() => {
tick();
expect(component.songId).toBe('4711');
}));
});

View File

@@ -0,0 +1,19 @@
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from "@angular/router";
@Component({
selector: 'app-song',
templateUrl: './song.component.html',
styleUrls: ['./song.component.less']
})
export class SongComponent implements OnInit {
public songId: string;
constructor(private activatedRoute: ActivatedRoute) {
}
public ngOnInit(): void {
this.activatedRoute.params.subscribe(params => this.songId = params.songId);
}
}

View File

@@ -0,0 +1,24 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {SongComponent} from "./song/song.component";
import {SongListComponent} from "./song-list/song-list.component";
const routes: Routes = [
{
path: '',
component: SongListComponent,
pathMatch: 'full'
},
{
path: ':songId',
component: SongComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SongsRoutingModule {
}

View File

@@ -0,0 +1,19 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {SongsRoutingModule} from './songs-routing.module';
import {SongComponent} from './song/song.component';
import {SongListModule} from "./song-list/song-list.module";
@NgModule({
declarations: [SongComponent],
imports: [
CommonModule,
SongsRoutingModule,
SongListModule,
]
})
export class SongsModule {
}

View File

@@ -0,0 +1,3 @@
<div [class.padding]="padding" class="card">
<ng-content></ng-content>
</div>

View File

@@ -0,0 +1,12 @@
@import "../../../../styles/shadow";
.card {
.card-3;
margin: 20px;
border-radius: 8px;
background: #fffe;
&.padding {
padding: 20px;
}
}

View File

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

View File

@@ -0,0 +1,17 @@
import {Component, Input, OnInit} from '@angular/core';
@Component({
selector: 'app-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.less']
})
export class CardComponent implements OnInit {
@Input() padding = true;
constructor() {
}
ngOnInit() {
}
}

View File

@@ -0,0 +1,14 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {CardComponent} from './card.component';
@NgModule({
declarations: [CardComponent],
exports: [CardComponent],
imports: [
CommonModule
]
})
export class CardModule {
}

View File

@@ -0,0 +1,16 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {SongTypePipe} from './song-type.pipe';
@NgModule({
declarations: [SongTypePipe],
exports: [
SongTypePipe
],
imports: [
CommonModule
]
})
export class SongTypeTranslaterModule {
}

View File

@@ -0,0 +1,8 @@
import {SongTypePipe} from './song-type.pipe';
describe('SongTypePipe', () => {
it('create an instance', () => {
const pipe = new SongTypePipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -0,0 +1,19 @@
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({
name: 'songType'
})
export class SongTypePipe implements PipeTransform {
transform(songTypeKey: string): string {
switch (songTypeKey) {
case "Worship":
return "Anbetung";
case "Praise":
return "Lobpreis";
default:
return ""
}
}
}