This commit is contained in:
mikgaw@st.amu.edu.pl 2024-02-18 20:59:52 +01:00
commit 8317d9d0ed
65 changed files with 13419 additions and 0 deletions

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

46
.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events.json
speed-measure-plugin.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

38
README.md Normal file
View File

@ -0,0 +1,38 @@
# CurrencyConverter
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.0.6.
## Prerequisites
- npm - Package Manager
- Node.js
- Angular CLI
## Setup and installation
- Clone the repository.
- Navigate to root project folder using any command line interface (e.g. Command Prompt on Windows).
- Run `npm install` to install packages.
- Run development server.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

124
angular.json Normal file
View File

@ -0,0 +1,124 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"currency-converter": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/currency-converter",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": false,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "currency-converter:build"
},
"configurations": {
"production": {
"browserTarget": "currency-converter:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "currency-converter:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "currency-converter:serve"
},
"configurations": {
"production": {
"devServerTarget": "currency-converter:serve:production"
}
}
}
}
}},
"defaultProject": "currency-converter"
}

12
browserslist Normal file
View File

@ -0,0 +1,12 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.

32
e2e/protractor.conf.js Normal file
View File

@ -0,0 +1,32 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

23
e2e/src/app.e2e-spec.ts Normal file
View File

@ -0,0 +1,23 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('Welcome to currency-converter!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

11
e2e/src/app.po.ts Normal file
View File

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get(browser.baseUrl) as Promise<any>;
}
getTitleText() {
return element(by.css('app-root h1')).getText() as Promise<string>;
}
}

13
e2e/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

32
karma.conf.js Normal file
View File

@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/currency-converter'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

11574
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "currency-converter",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "^8.0.3",
"@angular/cdk": "^8.1.1",
"@angular/common": "~8.0.3",
"@angular/compiler": "~8.0.3",
"@angular/core": "^8.0.3",
"@angular/flex-layout": "^8.0.0-beta.26",
"@angular/forms": "~8.0.3",
"@angular/material": "^8.1.1",
"@angular/platform-browser": "~8.0.3",
"@angular/platform-browser-dynamic": "~8.0.3",
"@angular/router": "~8.0.3",
"hammerjs": "^2.0.8",
"rxjs": "~6.4.0",
"tslib": "^1.9.0",
"zone.js": "~0.9.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.800.6",
"@angular/cli": "~8.0.6",
"@angular/compiler-cli": "~8.0.3",
"@angular/language-service": "~8.0.3",
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "^5.0.0",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.1.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.15.0",
"typescript": "~3.4.3"
}
}

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from './about.component';
const routes: Routes = [
{
path: '',
component: AboutComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AboutRoutingModule { }

View File

@ -0,0 +1,54 @@
<article itemprop="text">
<p>Angular Currency Converter/Calculator</p>
<p>Working with latest Angular 8.</p>
<p>This project was generated with <a href="https://github.com/angular/angular-cli">Angular CLI</a> version 8.0.6.</p>
<h1>Currency Converter/Calculator</h1>
<p>Used localStorage to limit number of requests to api endpoint.</p>
<p>Default base conversion currency is EUR (Euro)</p>
<h2>Prerequisites</h2>
<ul>
<li><a href="https://www.npmjs.com/">npm - Package Manager</a></li>
<li><a href="https://nodejs.org/en/">Node.js</a></li>
<li><a href="https://github.com/angular/angular-cli">Angular CLI</a></li>
</ul>
<h2>Setup and installation</h2>
<ol>
<li>Clone the repository.</li>
<li>Navigate to root project folder using any command line interface (e.g. Command Prompt on Windows).</li>
<li>Run <code>npm install</code> to install packages.</li>
<li>Run development server.</li>
</ol>
<h2>Development server</h2>
<p>Run <code>ng serve</code> for a dev server. Navigate to <code>http://localhost:4200/</code>.</p>
<h2>Project structure</h2>
<p><code>/src</code> - source folder, has the general Angular CLI project structure.</p>
<ul>
<li><code>/app</code> - contains AppComponent along with the following subfolders:
<ul>
<li><code>/currency-converter</code> - Currency Converter feature module and related components, models, etc..</li>
<li><code>/exchange-rates</code> - Exchange Rates feature module and related components, models, etc..</li>
<li><code>/about</code> - About feature Module, contains component/page with instructions for setup and installation</li>
<li><code>/shared</code> - Shared Module, contains shared services and other shared elements</li>
</ul>
</li>
</ul>
<h2>Usage</h2>
<p>On first load, application displays welcome message.</p>
<p>On successful load, application navigates user automatically to Exchange Rates page where user can select base
currency and filter quote currencies.</p>
<p>Clicking on quote currency navigates user to Currency Converter where user can enter desired amount to convert.
Currency select form fields are prepopulated but user can make its own selection of currency.</p>
<p>Clicking on Convert button Conversion Details section is displayed with resulting amount and exchange rates of
selected
currencies.</p>
<p>Additionaly, user can swap currencies by clicking on Reverse icon button located between currency select form
fields.</p>
<h2>To Do</h2>
<ul>
<li>Unit test application components/services</li>
<li>Implement cache</li>
<li>Implement Validators and Error Handlers</li>
<li>Extract Util functions and reusable components</li>
</ul>
</article>

View File

@ -0,0 +1,3 @@
article {
padding: 24px;
}

View File

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

View File

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

View File

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AboutComponent } from './about.component';
import { AboutRoutingModule } from './about-routing.module';
@NgModule({
declarations: [AboutComponent],
imports: [
CommonModule,
AboutRoutingModule
]
})
export class AboutModule { }

View File

@ -0,0 +1,28 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: 'currency-converter',
loadChildren: () => import('./currency-converter/currency-converter.module').then(mod => mod.CurrencyConverterModule)
},
{
path: 'exchange-rates',
loadChildren: () => import('./exchange-rates/exchange-rates.module').then(mod => mod.ExchangeRatesModule)
},
{
path: 'about',
loadChildren: () => import('./about/about.module').then(mod => mod.AboutModule)
},
{
path: '',
redirectTo: 'exchange-rates',
pathMatch: 'full'
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@ -0,0 +1,19 @@
<mat-toolbar color="primary">
<div fxFlex fxLayoutGap="25px">
<a routerLink="/currency-converter">
Currency Converter
</a>
<a class="exchange-link" routerLink="/exchange-rates">Browse currency exchange rates</a>
</div>
<div fxFlex fxLayout fxLayoutAlign="flex-end" fxHide.xs>
<ul fxLayout fxLayoutGap="20px" class="navigation-items">
<li>
<a routerLink="/about">
<span class="label">About</span>
</a>
</li>
</ul>
</div>
</mat-toolbar>
<router-outlet></router-outlet>

View File

@ -0,0 +1,48 @@
mat-sidenav-container,
mat-sidenav-content,
mat-sidenav {
height: 100%;
}
mat-sidenav {
width: 250px;
}
a {
text-decoration: none;
color: white;
}
a:hover,
a:active {
color: lightgray;
}
.navigation-items {
list-style: none;
padding: 0;
margin: 0;
cursor: pointer;
}
.icon {
display: inline-block;
height: 30px;
margin: 0 auto;
padding-right: 5px;
text-align: center;
vertical-align: middle;
width: 15%;
}
.label {
display: inline-block;
line-height: 30px;
margin: 0;
width: 85%;
}
a.exchange-link {
font-size: 14px;
padding-bottom: 6px;
}

View File

@ -0,0 +1,35 @@
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.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'currency-converter'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('currency-converter');
});
it('should render title in a h1 tag', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to currency-converter!');
});
});

10
src/app/app.component.ts Normal file
View File

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'Currency Converter';
}

34
src/app/app.module.ts Normal file
View File

@ -0,0 +1,34 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
MatSidenavModule,
MatIconModule,
MatToolbarModule,
MatListModule
} from '@angular/material';
import { FlexLayoutModule } from '@angular/flex-layout';
import { CurrencyConverterModule } from './currency-converter/currency-converter.module';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
FlexLayoutModule,
MatSidenavModule,
MatIconModule,
MatToolbarModule,
MatListModule,
CurrencyConverterModule
],
bootstrap: [AppComponent]
})
export class AppModule {}

View File

@ -0,0 +1,18 @@
<mat-card *ngIf="result">
<div fxLayout="row" fxLayoutAlign="center">
<h3>{{ amount }} {{ fromCurrency | uppercase }} = {{ result | number: '1.5-5' }}
{{ toCurrency | uppercase }}
</h3>
</div>
<div fxLayout="column" fxLayoutAlign="center center">
<p>
1 {{ fromCurrency | uppercase }}
=
{{ (+toRate / +fromRate) | number: '1.5-5' }} {{ toCurrency | uppercase }}
</p>
<p>
1 {{ toCurrency | uppercase }} = {{ (+fromRate / +toRate) | number: '1.5-5' }}
{{ fromCurrency | uppercase }}
</p>
</div>
</mat-card>

View File

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

View File

@ -0,0 +1,31 @@
import { Component, OnInit, Input, OnChanges, SimpleChange, SimpleChanges } from '@angular/core';
import { MappedCurrencyRate } from 'src/app/shared/interfaces/currency-rate';
@Component({
selector: 'app-conversion-details',
templateUrl: './conversion-details.component.html',
styleUrls: ['./conversion-details.component.scss']
})
export class ConversionDetailsComponent implements OnChanges {
@Input() amount: number;
@Input() result: number;
@Input() fromCurrencyRate: MappedCurrencyRate;
@Input() toCurrencyRate: MappedCurrencyRate;
fromCurrency: string;
toCurrency: string;
toRate: number;
fromRate: number;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
this.fromCurrency = this.fromCurrencyRate && this.fromCurrencyRate.currency;
this.fromRate = this.fromCurrencyRate && this.fromCurrencyRate.rate;
this.toCurrency = this.toCurrencyRate && this.toCurrencyRate.currency;
this.toRate = this.toCurrencyRate && this.toCurrencyRate.rate;
}
}

View File

@ -0,0 +1,41 @@
<div class="converter-container" fxLayout="column" fxLayoutGap="25px">
<mat-card>
<mat-card-content>
<form [formGroup]="currencyConverterForm" fxLayout="row" fxLayoutAlign="space-evenly center"
(ngSubmit)="convert()" novalidate>
<mat-form-field appearance="outline">
<input matInput placeholder="Amount" id="amount" [formControlName]="formNames.Amount" type="number">
</mat-form-field>
<mat-form-field appearance="outline">
<input type="text" placeholder="From" name="fromCurrency" aria-label="From" matInput [formControlName]="formNames.FromCurrency"
(keydown.enter)="selectCurrencyFromInput($event, formNames.FromCurrency)" [matAutocomplete]="fromAutocomplete">
<mat-autocomplete #fromAutocomplete="matAutocomplete">
<mat-option *ngFor="let fromItem of filteredFromCurrencies | async" [value]="fromItem">
{{fromItem}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<div class="converter-btn">
<button type="button" mat-icon-button class="reverse-btn" color="accent" (click)="swapCurrencies()"
aria-label="Swap currencies">
<mat-icon>compare_arrows</mat-icon>
</button>
</div>
<mat-form-field appearance="outline">
<input type="text" class="uppercase" placeholder="To" aria-label="To" matInput [formControlName]="formNames.ToCurrency"
(keydown.enter)="selectCurrencyFromInput($event, formNames.ToCurrency)" [matAutocomplete]="toAutocomplete">
<mat-autocomplete #toAutocomplete="matAutocomplete">
<mat-option *ngFor="let toItem of filteredToCurrencies | async" [value]="toItem">
{{toItem}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<div class="converter-btn">
<button type="button" mat-raised-button color="accent" (click)="convert()">Convert</button>
</div>
</form>
</mat-card-content>
</mat-card>
<app-conversion-details *ngIf="result" [amount]="amount" [result]="result" [fromCurrencyRate]="fromCurrencyRate"
[toCurrencyRate]="toCurrencyRate"></app-conversion-details>
</div>

View File

@ -0,0 +1,12 @@
.converter-container {
margin: 5%;
padding: 25px;
.converter-btn {
padding-bottom: 1.34375em;
}
.reverse-btn:hover {
border: 1px solid;
}
}

View File

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

View File

@ -0,0 +1,214 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';
import { MappedCurrencyRate } from 'src/app/shared/interfaces/currency-rate';
import { CurrencyExchangeService } from 'src/app/shared/services/currency-exchange.service';
import { ExchangeRatesApiService } from 'src/app/shared/services/exchange-rates-api.service';
import { Currency } from 'src/app/shared/enums/currency';
import { ExchangeRates } from 'src/app/shared/interfaces/exchange-rates';
import { FormNames } from 'src/app/shared/enums/form-names';
@Component({
selector: 'app-currency-converter',
templateUrl: './currency-converter.component.html',
styleUrls: ['./currency-converter.component.scss']
})
export class CurrencyConverterComponent implements OnInit {
currencyConverterForm: FormGroup;
filteredFromCurrencies: Observable<string[]>;
filteredToCurrencies: Observable<string[]>;
fromCurrencyRate: MappedCurrencyRate;
toCurrencyRate: MappedCurrencyRate;
result: string;
amount: number;
isLoading = true;
formNames = FormNames;
constructor(
private formBuilder: FormBuilder,
private currencyExchangeService: CurrencyExchangeService,
private exchangeRatesApiService: ExchangeRatesApiService,
private route: ActivatedRoute
) {}
ngOnInit() {
const baseCurrency =
this.route.snapshot.paramMap.get('from') || Currency.EUR;
const quoteCurrency =
this.route.snapshot.paramMap.get('to') || Currency.HRK;
this.currencyConverterForm = this.initForm(baseCurrency, quoteCurrency);
this.getExchangeRates(baseCurrency);
this.filteredFromCurrencies = this.getFromValueChanges(
FormNames.FromCurrency
);
this.filteredToCurrencies = this.getToValueChanges(FormNames.ToCurrency);
}
convert() {
this.fromCurrencyRate = this.filterSelectedValue(
this.currencyConverterForm.get(FormNames.FromCurrency).value
);
this.toCurrencyRate = this.filterSelectedValue(
this.currencyConverterForm.get(FormNames.ToCurrency).value
);
this.amount = Math.floor(
this.currencyConverterForm.get(FormNames.Amount).value
);
this.result = this.calculateExchangeRate(
this.fromCurrencyRate && this.fromCurrencyRate.rate,
this.toCurrencyRate && this.toCurrencyRate.rate
);
}
swapCurrencies() {
this.currencyConverterForm = this.formBuilder.group({
amount: [
this.currencyConverterForm.get(FormNames.Amount).value,
Validators.required
],
fromCurrency: [
this.currencyConverterForm.get(FormNames.ToCurrency).value,
Validators.required
],
toCurrency: [
this.currencyConverterForm.get(FormNames.FromCurrency).value,
Validators.required
]
});
const baseCurrencyCode = this.currencyConverterForm.get(
FormNames.FromCurrency
).value;
this.getExchangeRates(baseCurrencyCode);
this.filteredFromCurrencies = this.getFromValueChanges(
FormNames.FromCurrency
);
this.filteredToCurrencies = this.getToValueChanges(FormNames.ToCurrency);
this.convert();
}
getFromValueChanges(formControlName: string): Observable<string[]> {
return this.currencyConverterForm.get(formControlName).valueChanges.pipe(
startWith(''),
map(value =>
this.filterCurrencies(
value,
this.currencyExchangeService.fromCurrencies
)
)
);
}
getToValueChanges(formControlName: string): Observable<string[]> {
return this.currencyConverterForm.get(formControlName).valueChanges.pipe(
startWith(''),
map(value =>
this.filterCurrencies(value, this.currencyExchangeService.toCurrencies)
)
);
}
getExchangeRates(baseCurrencyCode: string) {
this.exchangeRatesApiService
.getLatestExchangeRates(baseCurrencyCode)
.subscribe(
(exchangeRate: ExchangeRates): void => {
this.currencyExchangeService.exchangeRates = this.mapExchangeRatesResponseData(
exchangeRate
);
this.currencyExchangeService.fromCurrencies = this.mapCurrencies();
this.currencyExchangeService.toCurrencies = this.mapCurrencies();
},
(error): void => {
console.error(`Error: ${error.message}`);
},
() => {
this.isLoading = false;
}
);
}
selectCurrencyFromInput(event: any, formName: string): void {
const inputCurrency = event.target.value.toUpperCase();
if (inputCurrency.length >= 2 && inputCurrency.length <= 3) {
const mappedCurrencies = this.mapCurrencies();
const matchedCurrency = mappedCurrencies
.find(currency => currency.includes(inputCurrency))
.toString();
this.currencyConverterForm.get(formName).setValue(matchedCurrency);
}
}
private mapExchangeRatesResponseData(
responseData: ExchangeRates
): MappedCurrencyRate[] {
const mappedRates = Object.keys(responseData.rates).map(
(item: string): MappedCurrencyRate => {
return {
currency: item,
rate: responseData.rates[item]
};
}
);
const baseRate = mappedRates.find(
cRate => cRate.currency === responseData.base
);
if (!baseRate) {
mappedRates.push({ currency: responseData.base, rate: 1 });
}
return mappedRates;
}
private initForm(fromCurrency: string, toCurrency: string) {
return this.formBuilder.group({
amount: [1, Validators.required],
fromCurrency: [fromCurrency, Validators.required],
toCurrency: [toCurrency, Validators.required]
});
}
private mapCurrencies(): string[] {
return this.currencyExchangeService.exchangeRates
.map((currency: MappedCurrencyRate) => {
return currency.currency;
})
.sort();
}
private filterCurrencies(value: string, arrayToFilter: string[]): string[] {
const filterValueLowercase = value.toLowerCase();
return arrayToFilter.filter(option =>
option.toLowerCase().includes(filterValueLowercase)
);
}
private filterSelectedValue(currencyCode: string): MappedCurrencyRate {
return this.currencyExchangeService.exchangeRates.find(
(item: MappedCurrencyRate) => {
return item.currency === currencyCode;
}
);
}
private calculateExchangeRate(fromRate: number, toRate: number): string {
return ((this.amount * toRate) / fromRate).toFixed(5);
}
}

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { CurrencyConverterComponent } from './components/currency-converter/currency-converter.component';
const routes: Routes = [
{
path: 'currency-converter/:from/:to',
component: CurrencyConverterComponent
},
{
path: '',
component: CurrencyConverterComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class CurrencyConverterRoutingModule { }

View File

@ -0,0 +1,44 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { CurrencyConverterRoutingModule } from './currency-converter-routing.module';
import { HttpClientModule } from '@angular/common/http';
import { CurrencyConverterComponent } from './components/currency-converter/currency-converter.component';
import {
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatButtonModule,
MatCardModule,
MatAutocompleteModule,
MatIconModule,
MatTableModule
} from '@angular/material';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ConversionDetailsComponent } from './components/conversion-details/conversion-details.component';
import { SharedModule } from '../shared/shared.module';
@NgModule({
declarations: [CurrencyConverterComponent, ConversionDetailsComponent],
imports: [
CommonModule,
CurrencyConverterRoutingModule,
HttpClientModule,
ReactiveFormsModule,
FormsModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatButtonModule,
MatCardModule,
MatAutocompleteModule,
MatIconModule,
MatTableModule,
FlexLayoutModule,
SharedModule
]
})
export class CurrencyConverterModule {}

View File

@ -0,0 +1,37 @@
<div class="currencies-container">
<form>
<div fxLayout="row" fxLayoutAlign="space-between center">
<mat-form-field>
<input type="text" placeholder="Select base currency" aria-label="Base currency" matInput
[formControl]="baseCurrencyControl" [matAutocomplete]="auto" (keydown.enter)="selectCurrencyFromInput($event)">
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let baseCurrencyOption of filteredBaseCurrencies | async"
(onSelectionChange)="getExchangeRates(baseCurrencyOption)" [value]="baseCurrencyOption">
{{baseCurrencyOption}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field>
<input matInput (keyup)="applySearchFilter($event.target.value)" placeholder="Search">
</mat-form-field>
</div>
</form>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="currency">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Quote Currency </th>
<td mat-cell *matCellDef="let element">
<a [routerLink]="['/currency-converter',baseCurrencyControl.value, element.currency]">{{element.currency}}</a>
</td>
</ng-container>
<ng-container matColumnDef="rate">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Rate </th>
<td mat-cell *matCellDef="let element"> {{element.rate | number: '1.5-5' }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</div>

View File

@ -0,0 +1,7 @@
.currencies-container {
margin: 5%;
table {
width: 100%;
}
}

View File

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

View File

@ -0,0 +1,133 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Observable } from 'rxjs';
import { startWith, map } from 'rxjs/operators';
import { FormControl } from '@angular/forms';
import { Currency } from 'src/app/shared/enums/currency';
import { MappedCurrencyRate } from 'src/app/shared/interfaces/currency-rate';
import { CurrencyExchangeService } from 'src/app/shared/services/currency-exchange.service';
import { ExchangeRatesApiService } from 'src/app/shared/services/exchange-rates-api.service';
import { ExchangeRates } from 'src/app/shared/interfaces/exchange-rates';
import {
MatTableDataSource,
MatSort,
MatOptionSelectionChange
} from '@angular/material';
@Component({
selector: 'app-exchange-rates',
templateUrl: './exchange-rates.component.html',
styleUrls: ['./exchange-rates.component.scss']
})
export class ExchangeRatesComponent implements OnInit {
dataSource: MatTableDataSource<MappedCurrencyRate>;
displayedColumns = ['currency', 'rate'];
filteredBaseCurrencies: Observable<string[]>;
baseCurrencyControl = new FormControl(Currency.EUR);
@ViewChild(MatSort, { static: true }) sort: MatSort;
constructor(
private currencyExchangeService: CurrencyExchangeService,
private exchangeRatesApiService: ExchangeRatesApiService
) {}
ngOnInit() {
this.getExchangeRates(Currency.EUR);
this.filteredBaseCurrencies = this.getBaseCurrencyValueChanges();
}
getExchangeRates(baseCurrencyCode: string) {
this.exchangeRatesApiService
.getLatestExchangeRates(baseCurrencyCode)
.subscribe(
(exchangeRates: ExchangeRates): void => {
this.currencyExchangeService.exchangeRates = this.mapExchangeRatesResponseData(
exchangeRates
);
this.dataSource = new MatTableDataSource(
this.currencyExchangeService.exchangeRates
);
this.dataSource.sort = this.sort;
this.currencyExchangeService.fromCurrencies = this.mapCurrencies();
this.currencyExchangeService.toCurrencies = this.mapCurrencies();
},
(error): void => {
console.error(`Error: ${error.message}`);
}
);
}
selectCurrencyFromInput(event: any): void {
const inputCurrency = event.target.value.toUpperCase();
if (inputCurrency.length >= 2 && inputCurrency.length <= 3) {
const mappedCurrencies = this.mapCurrencies();
const matchedCurrency = mappedCurrencies
.find(currency => currency.includes(inputCurrency))
.toString();
this.baseCurrencyControl.setValue(matchedCurrency);
this.getExchangeRates(matchedCurrency);
}
}
applySearchFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
}
private getBaseCurrencyValueChanges(): Observable<string[]> {
return this.baseCurrencyControl.valueChanges.pipe(
startWith(''),
map(value =>
this.filterCurrencies(
value,
this.currencyExchangeService.fromCurrencies
)
)
);
}
private mapExchangeRatesResponseData(
responseData: ExchangeRates
): MappedCurrencyRate[] {
const mappedRates = Object.keys(responseData.rates).map(
(item: string): MappedCurrencyRate => {
return {
currency: item,
rate: responseData.rates[item]
};
}
);
const baseRate = mappedRates.find(
cRate => cRate.currency === responseData.base
);
if (!baseRate) {
mappedRates.push({ currency: responseData.base, rate: 1 });
}
return mappedRates;
}
private filterCurrencies(value: string, arrayToFilter: string[]): string[] {
const filterValueLowercase = value.toLowerCase();
return arrayToFilter.filter(option =>
option.toLowerCase().includes(filterValueLowercase)
);
}
private mapCurrencies(): string[] {
return this.currencyExchangeService.exchangeRates
.map((currency: MappedCurrencyRate) => {
return currency.currency;
})
.sort();
}
}

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ExchangeRatesComponent } from './components/exchange-rates/exchange-rates.component';
const routes: Routes = [
{
path: '',
component: ExchangeRatesComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ExchangeRatesRoutingModule { }

View File

@ -0,0 +1,46 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ExchangeRatesComponent } from './components/exchange-rates/exchange-rates.component';
import { SharedModule } from '../shared/shared.module';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { ExchangeRatesRoutingModule } from './exchange-rates-routing.module';
import {
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatButtonModule,
MatCardModule,
MatAutocompleteModule,
MatIconModule,
MatTableModule,
MatSortModule
} from '@angular/material';
import { NgxMatSelectSearchModule } from 'ngx-mat-select-search';
import { FlexLayoutModule } from '@angular/flex-layout';
@NgModule({
declarations: [ExchangeRatesComponent],
imports: [
CommonModule,
ExchangeRatesRoutingModule,
FormsModule,
ReactiveFormsModule,
SharedModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatButtonModule,
MatCardModule,
MatAutocompleteModule,
MatIconModule,
MatTableModule,
MatSortModule,
NgxMatSelectSearchModule,
FlexLayoutModule
]
})
export class ExchangeRatesModule {}

View File

@ -0,0 +1,4 @@
export enum Currency {
EUR = 'EUR',
HRK = 'HRK'
}

View File

@ -0,0 +1,5 @@
export enum FormNames {
Amount = 'amount',
FromCurrency = 'fromCurrency',
ToCurrency = 'toCurrency',
}

View File

@ -0,0 +1,4 @@
export interface MappedCurrencyRate {
currency: string;
rate: number;
}

View File

@ -0,0 +1,6 @@
import { StringNumberPair } from 'src/app/shared/interfaces/string-number-pair';
export interface ExchangeRates {
base: string;
rates: StringNumberPair;
}

View File

@ -0,0 +1,3 @@
export interface StringNumberPair {
[key: string]: number;
}

View File

@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { CurrencyExchangeService } from './currency-exchange.service';
describe('CurrencyExchangeService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: CurrencyExchangeService = TestBed.get(CurrencyExchangeService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,14 @@
import { Injectable } from '@angular/core';
import { MappedCurrencyRate } from '../interfaces/currency-rate';
@Injectable({
providedIn: 'root'
})
export class CurrencyExchangeService {
public exchangeRates: MappedCurrencyRate[];
public fromCurrencies: string[] = [];
public toCurrencies: string[] = [];
constructor() { }
}

View File

@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { ExchangeRatesApiService } from './exchange-rates-api.service';
describe('ExchangeRatesApiService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: ExchangeRatesApiService = TestBed.get(ExchangeRatesApiService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ExchangeRates } from '../interfaces/exchange-rates';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class ExchangeRatesApiService {
private readonly apiEndpoint = 'https://api.ratesapi.io/api';
constructor(private readonly http: HttpClient) { }
getLatestExchangeRates(baseCurrency: string, quoteCurrency = ''): Observable<ExchangeRates> {
return this.http.get<ExchangeRates>(
`${this.apiEndpoint}/latest?base=${baseCurrency}&symbols=${quoteCurrency}`
);
}
}

View File

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

View File

@ -0,0 +1,40 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StorageService {
static setObject(key: string, data: object) {
try {
const serializedData = JSON.stringify(data);
localStorage.setItem(key, serializedData);
} catch (e) {
throw new Error('Provided data is not serializable!');
}
}
static getObject(key: string): object {
const item = localStorage.getItem(key);
return item && JSON.parse(item);
}
static setItem(key: string, data: string): string {
localStorage.setItem(key, data);
return data;
}
static getItem(key: string): string {
const data = localStorage.getItem(key);
return data;
}
static removeItem(key: string) {
localStorage.removeItem(key);
}
constructor() {
if (typeof Storage === 'undefined') {
throw new Error('StorageService: Local storage is not supported');
}
}
}

View File

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StorageService } from './services/storage.service';
import { ExchangeRatesApiService } from './services/exchange-rates-api.service';
import { CurrencyExchangeService } from './services/currency-exchange.service';
@NgModule({
imports: [
CommonModule
],
providers: [StorageService, ExchangeRatesApiService, CurrencyExchangeService]
})
export class SharedModule { }

0
src/assets/.gitkeep Normal file
View File

View File

@ -0,0 +1,3 @@
export const environment = {
production: true
};

View File

@ -0,0 +1,16 @@
// 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 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

31
src/index.html Normal file
View File

@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Currency Converter</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
div.welcome-msg {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
text-transform: uppercase;
color: #673ab7;
font-size: 2.5em;
text-align: center;
text-shadow: 2px 2px 10px rgba(0, 0, 0, 0.2);
}
</style>
</head>
<body>
<app-root>
<div class="welcome-msg">Hey stranger! <br /> Welcome to my awesome Currency Converter Web App.</div>
</app-root>
</body>
</html>

13
src/main.ts Normal file
View File

@ -0,0 +1,13 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import 'hammerjs';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

63
src/polyfills.ts Normal file
View File

@ -0,0 +1,63 @@
/**
* 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.ts';
*
* 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
*/

20
src/styles.scss Normal file
View File

@ -0,0 +1,20 @@
@import '@angular/material/prebuilt-themes/deeppurple-amber.css';
body {
margin: 0;
padding: 0;
}
.m-5 {
margin: 5%;
}
.my-5 {
margin: 5%;
}
.p-25 {
padding: 25px;
}
.py-25 {
padding: 25px;
}

20
src/test.ts Normal file
View File

@ -0,0 +1,20 @@
// 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: any;
// 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);

14
tsconfig.app.json Normal file
View File

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/test.ts",
"src/**/*.spec.ts"
]
}

22
tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
]
}
}

18
tsconfig.spec.json Normal file
View File

@ -0,0 +1,18 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

92
tslint.json Normal file
View File

@ -0,0 +1,92 @@
{
"extends": "tslint:recommended",
"rules": {
"array-type": false,
"arrow-parens": false,
"deprecation": {
"severity": "warning"
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"import-blacklist": [
true,
"rxjs/Rx"
],
"interface-name": false,
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-consecutive-blank-lines": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-use-before-declare": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"object-literal-sort-keys": false,
"ordered-imports": false,
"quotemark": [
true,
"single"
],
"trailing-comma": false,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true
},
"rulesDirectory": [
"codelyzer"
]
}