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 ""
}
}
}

View File

@@ -1,4 +1,3 @@
// Custom Theming for Angular Material
// For more information: https://material.angular.io/guide/theming
@import '~@angular/material/theming';

View File

@@ -1,3 +1,6 @@
import {firebase} from "./firebase";
export const environment = {
production: true
production: true,
firebase: firebase
};

View File

@@ -2,8 +2,11 @@
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
import {firebase} from "./firebase";
export const environment = {
production: false
production: false,
firebase: firebase
};
/*

View File

@@ -4,15 +4,15 @@
<meta charset="utf-8">
<title>Wgenerator</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#1976d2">
<meta content="width=device-width, initial-scale=1" name="viewport">
<link href="favicon.ico" rel="icon" type="image/x-icon">
<link href="manifest.webmanifest" rel="manifest">
<meta content="#1976d2" name="theme-color">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<app-root></app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript>
<app-root></app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript>
</body>
</html>

View File

@@ -1,9 +1,9 @@
import 'hammerjs';
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import {enableProdMode} from '@angular/core';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import {AppModule} from './app/app.module';
import {environment} from './environments/environment';
if (environment.production) {
enableProdMode();

View File

@@ -55,7 +55,7 @@
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************

View File

@@ -1,4 +0,0 @@
/* You can add global styles to this file, and also import other style files */
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }

19
src/styles/shadow.less Normal file
View File

@@ -0,0 +1,19 @@
.card-1 {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
}
.card-2 {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
}
.card-3 {
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
}
.card-4 {
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
}
.card-5 {
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.30), 0 15px 12px rgba(0, 0, 0, 0.22);
}

21
src/styles/styles.less Normal file
View File

@@ -0,0 +1,21 @@
@primary-color: #5285ff;
@navigation-background: #fffffff1;
@navigation-link-color: #555;
@transition: all 300ms ease-in-out;
html, body {
height: 100%;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
font-size: 14px;
background: #373b44; /* fallback for old browsers */
background: -webkit-linear-gradient(to top, #373b44, #4286f4); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(to top, #373b44, #4286f4); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
background-attachment: fixed;
}

View File

@@ -1,11 +1,8 @@
// 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';
import {getTestBed} from '@angular/core/testing';
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';
declare const require: any;