mirror of
https://github.com/Dansen999/at-certification-stock.git
synced 2026-01-11 21:43:34 +00:00
Pushed new version.
This commit is contained in:
17
src/app/app-routing.module.ts
Normal file
17
src/app/app-routing.module.ts
Normal 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 {
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
p {
|
||||
font-family: Lato;
|
||||
}
|
||||
@@ -1,4 +1 @@
|
||||
<hello name="{{ name }}"></hello>
|
||||
<p>
|
||||
Start editing to see some magic happen :)
|
||||
</p>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
35
src/app/app.component.spec.ts
Normal file
35
src/app/app.component.spec.ts
Normal 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!');
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
16
src/app/components/stock-card/stock-card.component.css
Normal file
16
src/app/components/stock-card/stock-card.component.css
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
27
src/app/components/stock-card/stock-card.component.html
Normal file
27
src/app/components/stock-card/stock-card.component.html
Normal 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>
|
||||
23
src/app/components/stock-card/stock-card.component.spec.ts
Normal file
23
src/app/components/stock-card/stock-card.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
40
src/app/components/stock-card/stock-card.component.ts
Normal file
40
src/app/components/stock-card/stock-card.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
21
src/app/model/company-data.ts
Normal file
21
src/app/model/company-data.ts
Normal 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;
|
||||
}
|
||||
7
src/app/pages/landing-page/landing-page.component.css
Normal file
7
src/app/pages/landing-page/landing-page.component.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.mat-card {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.mat-divider {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
26
src/app/pages/landing-page/landing-page.component.html
Normal file
26
src/app/pages/landing-page/landing-page.component.html
Normal 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>
|
||||
23
src/app/pages/landing-page/landing-page.component.spec.ts
Normal file
23
src/app/pages/landing-page/landing-page.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
52
src/app/pages/landing-page/landing-page.component.ts
Normal file
52
src/app/pages/landing-page/landing-page.component.ts
Normal 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});
|
||||
});
|
||||
}
|
||||
}
|
||||
0
src/app/pages/sentiment/sentiment.component.css
Normal file
0
src/app/pages/sentiment/sentiment.component.css
Normal file
1
src/app/pages/sentiment/sentiment.component.html
Normal file
1
src/app/pages/sentiment/sentiment.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<p>sentiment works!</p>
|
||||
23
src/app/pages/sentiment/sentiment.component.spec.ts
Normal file
23
src/app/pages/sentiment/sentiment.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
16
src/app/pages/sentiment/sentiment.component.ts
Normal file
16
src/app/pages/sentiment/sentiment.component.ts
Normal 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 {
|
||||
}
|
||||
|
||||
}
|
||||
16
src/app/services/finnhub.service.spec.ts
Normal file
16
src/app/services/finnhub.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
29
src/app/services/finnhub.service.ts
Normal file
29
src/app/services/finnhub.service.ts
Normal 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 }});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
src/app/services/storage.service.spec.ts
Normal file
16
src/app/services/storage.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
41
src/app/services/storage.service.ts
Normal file
41
src/app/services/storage.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user