Pushed new version.

This commit is contained in:
Daniel Scheidle
2022-11-09 01:06:40 +01:00
parent 4a0538d5ee
commit 6b97c687d6
47 changed files with 900 additions and 161 deletions

View File

@@ -0,0 +1,17 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {SentimentComponent} from "./pages/sentiment/sentiment.component";
import {LandingPageComponent} from "./pages/landing-page/landing-page.component";
const routes: Routes = [
{path: 'sentiment/:symbol', component: SentimentComponent},
{path: '**', component: LandingPageComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}

View File

@@ -1,3 +0,0 @@
p {
font-family: Lato;
}

View File

@@ -1,4 +1 @@
<hello name="{{ name }}"></hello>
<p>
Start editing to see some magic happen :)
</p>
<router-outlet></router-outlet>

View File

@@ -0,0 +1,35 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await 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 'at-certification-stock'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('at-certification-stock');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain('at-certification-stock app is running!');
});
});

View File

@@ -1,10 +1,10 @@
import { Component, VERSION } from '@angular/core';
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
styleUrls: ['./app.component.css']
})
export class AppComponent {
name = 'Angular ' + VERSION.major;
export class AppComponent {
title = 'at-certification-stock';
}

View File

@@ -1,13 +1,48 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { HelloComponent } from './hello.component';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {HttpClientModule} from "@angular/common/http";
import {SentimentComponent} from './pages/sentiment/sentiment.component';
import {LandingPageComponent} from './pages/landing-page/landing-page.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MatCardModule} from "@angular/material/card";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatButtonModule} from "@angular/material/button";
import {MatInputModule} from "@angular/material/input";
import {MatDividerModule} from "@angular/material/divider";
import {FormsModule} from "@angular/forms";
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {MatSnackBarModule} from "@angular/material/snack-bar";
import {MatGridListModule} from "@angular/material/grid-list";
import { StockCardComponent } from './components/stock-card/stock-card.component';
import {MatIconModule} from "@angular/material/icon";
@NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [ AppComponent, HelloComponent ],
bootstrap: [ AppComponent ]
declarations: [
AppComponent,
SentimentComponent,
LandingPageComponent,
StockCardComponent
],
imports: [
FormsModule,
BrowserModule,
AppRoutingModule,
HttpClientModule,
BrowserAnimationsModule,
MatCardModule,
MatFormFieldModule,
MatButtonModule,
MatInputModule,
MatDividerModule,
MatSnackBarModule,
MatGridListModule,
MatIconModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
export class AppModule {
}

View File

@@ -0,0 +1,16 @@
.mat-card {
margin: 1rem;
}
.mat-card-title .mat-icon {
cursor: pointer;
float: right;
}
.mat-grid-tile .mat-icon {
font-size: 4rem;
height: 4rem;
width: 4rem;
}

View File

@@ -0,0 +1,27 @@
<mat-card>
<mat-card-title>{{company.description}} ({{company.symbol}})
<mat-icon (click)="remove()" fontIcon="close"></mat-icon>
</mat-card-title>
<mat-card-content>
<ng-container *ngIf="getQuote() | async; let quote">
<mat-grid-list cols="4" rowHeight="4:1">
<mat-grid-tile colspan="2"></mat-grid-tile>
<mat-grid-tile colspan="2" rowspan="3">
<mat-icon *ngIf="quote.dp>0" color="primary" fontIcon="arrow_upward"></mat-icon>
<mat-icon *ngIf="quote.dp<0" color="warn" fontIcon="arrow_downward"></mat-icon>
<mat-icon *ngIf="quote.dp==0" fontIcon="east"></mat-icon>
</mat-grid-tile>
<mat-grid-tile><p>Change today: {{quote.dp / 100 | percent: '1.1-1'}}</p></mat-grid-tile>
<mat-grid-tile><p>Opening price: {{quote.o | currency: 'USD'}}</p></mat-grid-tile>
<mat-grid-tile><p>Current price: {{quote.c | currency: 'USD'}}</p></mat-grid-tile>
<mat-grid-tile><p>High price: {{quote.h | currency: 'USD'}}</p></mat-grid-tile>
</mat-grid-list>
</ng-container>
</mat-card-content>
<mat-card-actions align="end">
<button mat-button [routerLink]="'/sentiment/' + company.symbol">Go to social sentiment details</button>
</mat-card-actions>
</mat-card>

View File

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

View File

@@ -0,0 +1,40 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {FinnhubService} from "../../services/finnhub.service";
import {Company, QuoteResponse} from "../../model/company-data";
import {Subject, Subscription} from "rxjs";
import {StorageService} from "../../services/storage.service";
@Component({
selector: 'app-stock-card',
templateUrl: './stock-card.component.html',
styleUrls: ['./stock-card.component.css']
})
export class StockCardComponent implements OnInit, OnDestroy {
@Input()
company = {} as Company;
private _quote$ = new Subject<QuoteResponse>();
private subs: Subscription[] = [];
constructor(public finnhubService: FinnhubService,
public storageService: StorageService) {
}
ngOnInit(): void {
this.subs.push(this.finnhubService.getQuote(this.company.symbol).subscribe(value => this._quote$.next(value)));
}
getQuote(): Subject<QuoteResponse> {
return this._quote$;
}
ngOnDestroy(): void {
this.subs.forEach(s => s.unsubscribe());
this._quote$.complete();
}
remove() {
this.storageService.remove(this.company.symbol);
}
}

View File

@@ -1,10 +0,0 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'hello',
template: `<h1>Hello {{name}}!</h1>`,
styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent {
@Input() name: string;
}

View File

@@ -0,0 +1,21 @@
export interface Company {
description: string;
displaySymbol: string;
symbol: string;
type: string;
}
export interface SearchResponse {
count: number;
result: Array<Company>;
}
export interface QuoteResponse {
c: number;
d: number;
dp: number;
h: number;
l: number;
o: number;
pc: number;
}

View File

@@ -0,0 +1,7 @@
.mat-card {
margin: 1rem;
}
.mat-divider {
margin: 0 1rem;
}

View File

@@ -0,0 +1,26 @@
<mat-card>
<mat-card-content>
<p>
Enter the symbol of a stock to track (i.e. AAPL, TSLA, GOOGL)
</p>
<mat-form-field>
<input id="stockInput"
matInput
[(ngModel)]="symbol"/>
</mat-form-field>
<button id="trackBtn"
mat-stroked-button
color="primary"
[disabled]="disabled"
(click)="addSymbol()">Track Stock
</button>
</mat-card-content>
</mat-card>
<mat-divider></mat-divider>
<ng-container *ngFor="let company of storageService.get() | async ">
<app-stock-card [company]="company"></app-stock-card>
</ng-container>

View File

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

View File

@@ -0,0 +1,52 @@
import {Component, OnInit} from '@angular/core';
import {FinnhubService} from "../../services/finnhub.service";
import {StorageService} from "../../services/storage.service";
import {MatSnackBar} from "@angular/material/snack-bar";
@Component({
selector: 'app-landing-page',
templateUrl: './landing-page.component.html',
styleUrls: ['./landing-page.component.css']
})
export class LandingPageComponent implements OnInit {
symbol = '';
disabled = false;
constructor(public finnhubService: FinnhubService,
public storageService: StorageService,
private snackBar: MatSnackBar) {
}
ngOnInit(): void {
}
addSymbol() {
console.log('search for ' + this.symbol)
// start loading...
this.disabled = true;
this.finnhubService.searchSymbol(this.symbol).subscribe(value => {
if (value.count > 0) {
let company = value.result.find(company => company.symbol == this.symbol);
if (company) {
this.storageService.add(company);
this.symbol = '';
} else {
this.snackBar.open('No unique result', 'close', {duration: 10000});
}
this.disabled = false
} else {
this.snackBar.open('No result', 'close', {duration: 10000});
this.disabled = false
}
}, (error) => {
this.snackBar.open('Communication error', 'close', {duration: 10000});
});
}
}

View File

@@ -0,0 +1 @@
<p>sentiment works!</p>

View File

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

View File

@@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
import {FinnhubService} from "../../services/finnhub.service";
@Component({
selector: 'app-sentiment',
templateUrl: './sentiment.component.html',
styleUrls: ['./sentiment.component.css']
})
export class SentimentComponent implements OnInit {
constructor(private finnhubService: FinnhubService) { }
ngOnInit(): void {
}
}

View File

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

View File

@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs";
import {QuoteResponse, SearchResponse} from "../model/company-data";
@Injectable({
providedIn: 'root'
})
export class FinnhubService {
private readonly token = { 'token': 'bu4f8kn48v6uehqi3cqg' };
private readonly api = 'https://finnhub.io/api/v1';
private readonly apiQuote = this.api + '/quote';
private readonly apiSearch = this.api + '/search';
constructor(private httpClient: HttpClient) { }
searchSymbol(q: string): Observable<SearchResponse> {
return this.httpClient.get<SearchResponse>(this.apiSearch, {params: { ...this.token, q }});
}
getQuote(symbol: string) {
return this.httpClient.get<QuoteResponse>(this.apiQuote, {params: { ...this.token, symbol }});
}
}

View File

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

View File

@@ -0,0 +1,41 @@
import {Injectable, OnDestroy} from '@angular/core';
import {BehaviorSubject} from "rxjs";
import {Company} from "../model/company-data";
@Injectable({
providedIn: 'root'
})
export class StorageService implements OnDestroy {
private storage: Company[] = [];
private _storage$ = new BehaviorSubject<Company[]>([]);
constructor() {
}
add(company: Company): void {
this.storage.push(company);
this._storage$.next(this.storage)
}
remove(symbol: string): void {
console.log('Remove ' + symbol)
let index = this.storage.findIndex(company => company.symbol == symbol);
if (index > -1) {
this.storage.splice(index, 1);
}
console.log(this.storage)
this._storage$.next(this.storage)
}
get(): BehaviorSubject<Company[]> {
return this._storage$;
}
ngOnDestroy(): void {
this._storage$.complete();
}
}