First commit

This commit is contained in:
2019-05-08 16:38:12 +08:00
commit d504997def
172 changed files with 66423 additions and 0 deletions

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

27
tapit-frontend/README.md Normal file
View File

@@ -0,0 +1,27 @@
# TapitFrontend
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.3.1.
## 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).

136
tapit-frontend/angular.json Normal file
View File

@@ -0,0 +1,136 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"tapit-frontend": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/tapit-frontend",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": [],
"es5BrowserSupport": true
},
"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": "tapit-frontend:build"
},
"configurations": {
"production": {
"browserTarget": "tapit-frontend:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "tapit-frontend:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"src/styles.css"
],
"scripts": [],
"assets": [
"src/favicon.ico",
"src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"tapit-frontend-e2e": {
"root": "e2e/",
"projectType": "application",
"prefix": "",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "tapit-frontend:serve"
},
"configurations": {
"production": {
"devServerTarget": "tapit-frontend:serve:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "tapit-frontend"
}

View File

@@ -0,0 +1,28 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
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.e2e.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

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 tapit-frontend!');
});
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,
}));
});
});

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

View File

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

10514
tapit-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
{
"name": "tapit-frontend",
"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": "~7.2.0",
"@angular/common": "~7.2.0",
"@angular/compiler": "~7.2.0",
"@angular/core": "~7.2.0",
"@angular/forms": "~7.2.0",
"@angular/platform-browser": "~7.2.0",
"@angular/platform-browser-dynamic": "~7.2.0",
"@angular/router": "~7.2.0",
"core-js": "^2.5.4",
"rxjs": "~6.3.3",
"tslib": "^1.9.0",
"zone.js": "~0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.13.0",
"@angular/cli": "~7.3.1",
"@angular/compiler-cli": "~7.2.0",
"@angular/language-service": "~7.2.0",
"@types/node": "~8.9.4",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.1.1",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.2.2"
}
}

View File

@@ -0,0 +1,37 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MainComponent } from './main/main.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { CampaignComponent } from './campaign/campaign.component';
import { CampaignNewComponent } from './campaign-new/campaign-new.component';
import { CampaignViewComponent } from './campaign-view/campaign-view.component';
import { PhonebookComponent } from './phonebook/phonebook.component';
import { PhonebookNewComponent } from './phonebook-new/phonebook-new.component';
import { TextTemplateComponent } from './text-template/text-template.component';
import { TextTemplateNewComponent } from './text-template-new/text-template-new.component';
import { ProviderComponent } from './provider/provider.component';
import { ProfileComponent } from './profile/profile.component';
const routes: Routes = [
{ path: '', component: MainComponent },
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
{ path: 'profile', component: ProfileComponent },
{ path: 'campaign', component: CampaignComponent },
{ path: 'campaign/new', component: CampaignNewComponent },
{ path: 'campaign/:id/view', component: CampaignViewComponent },
{ path: 'phonebook', component: PhonebookComponent },
{ path: 'phonebook/new', component: PhonebookNewComponent },
{ path: 'phonebook/:id/edit', component: PhonebookNewComponent },
{ path: 'text-template', component: TextTemplateComponent },
{ path: 'text-template/new', component: TextTemplateNewComponent },
{ path: 'text-template/:id/edit', component: TextTemplateNewComponent },
{ path: 'provider', component: ProviderComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

View File

@@ -0,0 +1,42 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" routerLink="/">Tap It!</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav">
<li *ngFor="let navlink of navlinks" data-toggle="collapse" data-target="#navbarNav" class="nav-item">
<a class="nav-link" *ngIf="navlink.loginOnly === authService.loggedin" [ngClass]="{'active': router.url === navlink.link}" routerLink="/{{ navlink.link }}">
{{ navlink.name }}
<span *ngIf="this.router.url === navlink.link" class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item dropdown" *ngIf="authService.loggedin === true">
<a class="nav-link dropdown-toggle" routerLink="{{ router.url }}" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Settings
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" routerLink="/profile">Profile</a>
<a class="dropdown-item" routerLink="/provider">Providers</a>
<a class="dropdown-item" routerLink="/globalsettings">Global Settings</a>
</div>
</li>
</ul>
<ul class="navbar-nav ml-auto">
<li *ngIf="authService.loggedin" data-toggle="collapse" data-target="#navbarNav" class="nav-item">
<a class="nav-link" routerLink="/" (click)="authService.logout()">Log Out</a>
</li>
<li *ngIf="!authService.loggedin" data-toggle="collapse" data-target="#navbarNav" class="nav-item">
<a class="nav-link" routerLink="/login">Login</a>
</li>
</ul>
</div>
</nav>
<main class="container-fluid pt-2">
<router-outlet></router-outlet>
</main>
<div class="fixed-bottom"><app-notification></app-notification></div>

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 'tapit-frontend'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('tapit-frontend');
});
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 tapit-frontend!');
});
});

View File

@@ -0,0 +1,37 @@
import { Component } from '@angular/core';
import { RouterModule, Routes, Router } from '@angular/router';
import { AuthService } from './auth.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'tapit-frontend';
navlinks = [
{
link: '/campaign',
name: 'Campaigns',
loginOnly: true,
},
{
link: '/phonebook',
name: 'Phonebook',
loginOnly: true,
},
{
link: '/text-template',
name: 'Text Templates',
loginOnly: true,
},
{
link: '/web-template',
name: 'Web Templates',
loginOnly: true,
},
];
constructor( private router: Router, private authService: AuthService) {
authService.getUser();
}
}

View File

@@ -0,0 +1,48 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { MainComponent } from './main/main.component';
import { CampaignComponent } from './campaign/campaign.component';
import { CampaignNewComponent } from './campaign-new/campaign-new.component';
import { NotificationComponent } from './notification/notification.component';
import { PhonebookComponent } from './phonebook/phonebook.component';
import { PhonebookNewComponent } from './phonebook-new/phonebook-new.component';
import { TextTemplateComponent } from './text-template/text-template.component';
import { TextTemplateNewComponent } from './text-template-new/text-template-new.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { ProviderComponent } from './provider/provider.component';
import { ProfileComponent } from './profile/profile.component';
import { CampaignViewComponent } from './campaign-view/campaign-view.component';
@NgModule({
declarations: [
AppComponent,
MainComponent,
CampaignComponent,
CampaignNewComponent,
NotificationComponent,
PhonebookComponent,
PhonebookNewComponent,
TextTemplateComponent,
TextTemplateNewComponent,
LoginComponent,
RegisterComponent,
ProviderComponent,
ProfileComponent,
CampaignViewComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

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

View File

@@ -0,0 +1,131 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { NotificationService } from './notification.service';
import { Observable, of } from 'rxjs';
export class User {
username: string;
password: string;
name: string;
email: string;
secretCode: string;
}
export class UserNotification {
resultType: string;
text: string;
payload: User;
}
@Injectable({
providedIn: 'root'
})
export class AuthService {
currUser = new User();
loggedin = false;
loginUrl = 'api/login';
logoutUrl = 'api/logout';
registerUrl = 'api/register';
myselfUrl = 'api/myself';
httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
}),
};
login(username: string, password: string) {
this.currUser.username = username;
this.currUser.password = password;
this.http.post<UserNotification>(this.loginUrl, this.currUser, this.httpOptions).subscribe(usermessage => {
if (usermessage.payload !== null) {
this.loggedin = true;
// update user
this.currUser.username = usermessage.payload.username;
this.currUser.email = usermessage.payload.email;
this.currUser.name = usermessage.payload.name;
this.notificationService.addNotification(usermessage.resultType, usermessage.text);
this.router.navigate(['/campaign']);
} else {
this.notificationService.addNotification(usermessage.resultType, usermessage.text);
}
},
err => {
this.notificationService.addNotification('failure', 'Error in logging in');
});
this.currUser.password = '';
}
register(username: string, password: string, email: string, name: string, secretCode: string) {
this.currUser.username = username;
this.currUser.password = password;
this.currUser.email = email;
this.currUser.name = name;
this.currUser.secretCode = secretCode;
this.http.post<UserNotification>(this.registerUrl, this.currUser, this.httpOptions).subscribe(usermessage => {
if (usermessage.payload !== null) {
this.loggedin = true;
this.notificationService.addNotification(usermessage.resultType, usermessage.text);
this.router.navigate(['/campaign']);
// update user
this.currUser.username = usermessage.payload.username;
this.currUser.email = usermessage.payload.email;
this.currUser.name = usermessage.payload.name;
} else {
this.notificationService.addNotification(usermessage.resultType, usermessage.text);
}
});
this.currUser.secretCode = '';
}
logout() {
this.http.post<UserNotification>(this.logoutUrl, '', this.httpOptions).subscribe(usermessage => {
this.notificationService.addNotification(usermessage.resultType, usermessage.text);
this.loggedin = false;
this.currUser = new User();
this.router.navigate(['/']);
});
}
getUser(): User {
this.http.get<User>(this.myselfUrl, this.httpOptions).subscribe(thisUser => {
this.currUser = thisUser;
if (this.currUser.username !== '') {
this.loggedin = true;
} else {
this.router.navigate(['/']);
}
// separate one to redirect main to campaign dashboard
if (this.router.url === '/' || this.router.url === '') {
this.router.navigate(['/campaign']);
}
},
err => {
this.router.navigate(['/']);
});
return this.currUser;
}
getUserObs(): Observable<User> {
return this.http.get<User>(this.myselfUrl, this.httpOptions);
}
updateUser(user: User) {
this.currUser = user;
this.http.put<UserNotification>(this.myselfUrl, this.currUser, this.httpOptions).subscribe(usermessage => {
this.notificationService.addNotification(usermessage.resultType, usermessage.text);
},
err => {
this.notificationService.addNotification('failure', 'Error in updating profile');
});
this.currUser.password = '';
}
constructor(private http: HttpClient, private router: Router, private notificationService: NotificationService) { }
}

View File

@@ -0,0 +1,45 @@
<div class="row p-2">
<div class="col-12">
<div class="row mt-3 mb-3">
<div class="col-12 d-flex">
<label for="campaignName" class="pr-2 mt-auto mb-auto">Campaign Name</label>
<input type="text" class="flex-grow-1" id="campaignName" [(ngModel)]="newCampaign.name" placeholder="Campaign Name">
</div>
</div>
<div class="row mt-3 mb-3">
<div class="col-12 d-flex">
<label for="newFromNum" class="pr-2 mt-auto mb-auto">From Number</label>
<input type="text" class="flex-grow-1" id="newFromNum" [(ngModel)]="newCampaign.fromNumber" placeholder="From Number">
</div>
</div>
<!-- Add phonebook & template via list -->
<div class="form-group">
<label for="provider-select">Provider</label>
<select class="form-control" [(ngModel)]="newCampaign.providerTag" id="provider-select">
<option></option>
<option *ngFor="let providerEnum of providerService.providerEnums" [ngValue]="providerEnum.tag">{{providerEnum.name}}</option>
</select>
</div>
<div class="form-group">
<label for="phonebook-select">Phonebook</label>
<select class="form-control" [(ngModel)]="newCampaign.phonebookId" id="phonebook-select">
<option></option>
<option *ngFor="let phonebook of phonebookService.phonebooks" [ngValue]="phonebook.id">{{phonebook.name}}: Size {{phonebook.size}}</option>
</select>
</div>
<div class="form-group">
<label for="text-template-select">Text Template</label>
<select class="form-control" [(ngModel)]="newCampaign.textTemplateId" id="text-template-select">
<option></option>
<option *ngFor="let textTemplate of textTemplateService.textTemplates" [ngValue]="textTemplate.id">{{textTemplate.name}}</option>
</select>
</div>
<div class="row mt-4">
<div class="col-12 d-flex">
<button type="button" (click)="submitNewCampaignRun()" class="btn btn-primary mr-2">Start</button>
<button type="button" (click)="submitNewCampaign()" class="btn btn-secondary ml-2">Save</button>
<button type="button" *ngIf="router.url !== '/campaign/new'" (click)="askDelete()" class="btn btn-danger ml-auto" data-toggle="modal" data-target="#completeModal">Delete</button>
</div>
</div>
</div>

View File

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

View File

@@ -0,0 +1,35 @@
import { Component, OnInit } from '@angular/core';
import { CampaignService, Campaign } from '../campaign.service';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { ProviderService } from '../provider.service';
import { PhonebookService } from '../phonebook.service';
import { TextTemplateService } from '../text-template.service';
@Component({
selector: 'app-campaign-new',
templateUrl: './campaign-new.component.html',
styleUrls: ['./campaign-new.component.css']
})
export class CampaignNewComponent implements OnInit {
constructor(
private campaignService: CampaignService,
private router: Router,
private providerService: ProviderService,
private phonebookService: PhonebookService,
private textTemplateService: TextTemplateService) { }
newCampaign: Campaign = new Campaign();
submitNewCampaign() {
this.campaignService.addCampaign(this.newCampaign);
}
submitNewCampaignRun() {
this.campaignService.addCampaignRun(this.newCampaign);
}
ngOnInit() {
}
}

View File

@@ -0,0 +1,3 @@
.campaign-details:read-only {
background-color: white;
}

View File

@@ -0,0 +1,77 @@
<div class="row">
<div class="col-12 mb-3 d-flux">
<button type="button" *ngIf="currCampaign.currentStatus === 'Running'" (click)="pauseCampaign()" class="btn btn-warning mr-2">Pause Campaign</button>
<button type="button" *ngIf="currCampaign.currentStatus !== 'Running'" (click)="startCampaign()" class="btn btn-primary mr-2">Start Campaign</button>
<button type="button" class="btn btn-danger ml-auto" data-toggle="modal" data-target="#completeModal">Delete</button>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">Campaign Name</span>
</div>
<input type="text" class="form-control campaign-details" value="{{ currCampaign.name }}" readonly>
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">Campaign Size</span>
</div>
<input type="text" class="form-control campaign-details" value="{{ currCampaign.size }}" readonly>
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">Campaign Status</span>
</div>
<input type="text" class="form-control campaign-details" value="{{ currCampaign.currentStatus }}" readonly>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<table class="table table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">From</th>
<th scope="col">To</th>
<th scope="col">Currrent Status</th>
<th scope="col">Time Sent</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let job of currCampaign.jobs">
<tr>
<td>{{ job.fromNum }}</td>
<td>{{ job.toNum }}</td>
<td>{{ job.currentStatus }}</td>
<td>{{ job.timeSent | date:'dd-MMM-yyyy'}}</td>
</tr>
</ng-container>
</tbody>
</table>
</div>
</div>
<div class="modal fade" id="completeModal" tabindex="-1" role="dialog" aria-labelledby="completeModal" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{{ currCampaign.name }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the campaign?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" (click)="deleteCampaign()" data-dismiss="modal">Delete Campaign</button>
</div>
</div>
</div>
</div>

View File

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

View File

@@ -0,0 +1,69 @@
import { Component, OnInit } from '@angular/core';
import { CampaignService, Campaign, Job, CampaignNotification } from '../campaign.service';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { NotificationService } from '../notification.service';
@Component({
selector: 'app-campaign-view',
templateUrl: './campaign-view.component.html',
styleUrls: ['./campaign-view.component.css']
})
export class CampaignViewComponent implements OnInit {
currCampaign: Campaign = new Campaign();
id = 0;
constructor(
private campaignService: CampaignService,
private router: Router,
private route: ActivatedRoute,
private notificationService: NotificationService
) { }
startCampaign() {
this.campaignService.startCampaign(this.currCampaign).subscribe(campaignNotification => {
this.notificationService.addNotification(campaignNotification.resultType, campaignNotification.text);
this.campaignService.getCampaignObs(this.id).subscribe(campaign => {
this.currCampaign = campaign;
});
},
err => {
this.notificationService.addNotification('failure', 'Error in starting campaign');
});
}
pauseCampaign() {
this.campaignService.pauseCampaign(this.currCampaign).subscribe(campaignNotification => {
this.notificationService.addNotification(campaignNotification.resultType, campaignNotification.text);
},
err => {
this.notificationService.addNotification('failure', 'Error in pausing campaign');
});
}
deleteCampaign() {
this.campaignService.deleteCampaign(this.currCampaign);
}
updateThisCampaign() {
this.campaignService.getCampaignObs(this.id).subscribe(campaign => {
this.currCampaign = campaign;
});
}
ngOnInit() {
const idParam = 'id';
this.route.params.subscribe( params => {
this.id = parseInt(params[idParam], 10);
});
this.updateThisCampaign();
const intervalId = setInterval(() => {
this.updateThisCampaign();
if (!this.router.url.includes('/campaign')) {
clearInterval(intervalId);
}
}, 2000);
}
}

View File

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

View File

@@ -0,0 +1,115 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Router } from '@angular/router';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { NotificationService } from './notification.service';
export class Campaign {
id: number;
name: string;
fromNumber: string;
size: number;
currentStatus: string;
createDate: Date;
phonebookId: number;
textTemplateId: number;
webTemplateId: number;
providerTag: string;
jobs: Job[];
}
export class Job {
id: number;
currentStatus: string;
timeSent: Date;
fromNum: string;
toNum: string;
}
export class CampaignNotification {
resultType: string;
text: string;
payload: Campaign;
}
@Injectable({
providedIn: 'root'
})
export class CampaignService {
campaigns: Campaign[] = [];
campaignUrl = '/api/campaign';
httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
}),
};
getCampaigns() {
this.http.get<Campaign[]>(this.campaignUrl).subscribe(campaigns => {
if (campaigns === null) {
this.campaigns = [];
} else {
this.campaigns = campaigns;
}
});
}
getCampaignObs(id: number): Observable<Campaign> {
return this.http.get<Campaign>(this.campaignUrl + '/' + id.toString());
}
addCampaign(newCampaign: Campaign) {
this.http.post<CampaignNotification>(this.campaignUrl, newCampaign, this.httpOptions).subscribe(campaignNotification => {
this.notificationService.addNotification(campaignNotification.resultType, campaignNotification.text);
this.campaigns.push(campaignNotification.payload);
if (campaignNotification.payload !== null) {
this.router.navigate(['/campaign']);
}
},
err => {
this.notificationService.addNotification('failure', 'Error in creating template');
});
}
addCampaignRun(newCampaign: Campaign) {
this.http.post<CampaignNotification>(this.campaignUrl, newCampaign, this.httpOptions).subscribe(campaignNotification => {
this.notificationService.addNotification(campaignNotification.resultType, campaignNotification.text);
this.campaigns.push(campaignNotification.payload);
if (campaignNotification.payload !== null) {
this.startCampaign(campaignNotification.payload).subscribe();
this.router.navigate(['/campaign']);
}
},
err => {
this.notificationService.addNotification('failure', 'Error in creating template');
});
}
deleteCampaign(campaign: Campaign) {
this.http.delete<CampaignNotification>(this.campaignUrl + '/' + campaign.id.toString(), this.httpOptions)
.subscribe(campaignNotification => {
this.notificationService.addNotification(campaignNotification.resultType, campaignNotification.text);
this.router.navigate(['/campaign']);
},
err => {
this.notificationService.addNotification('failure', 'Error in deleting campaign');
});
}
startCampaign(campaign: Campaign) {
return this.http.get<CampaignNotification>(this.campaignUrl + '/' + campaign.id.toString() + '/' + 'start');
}
pauseCampaign(campaign: Campaign) {
return this.http.get<CampaignNotification>(this.campaignUrl + '/' + campaign.id.toString() + '/' + 'pause');
}
constructor(private http: HttpClient, private router: Router, private notificationService: NotificationService) {
this.campaigns = [];
this.getCampaigns();
}
}

View File

@@ -0,0 +1,31 @@
<div class="row">
<div class="col-12">
<button class="btn btn-primary" routerLink="/campaign/new">New Campaign</button>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<table class="table table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">Name</th>
<th scope="col">Status</th>
<th scope="col">Target Size</th>
<th scope="col">Create Date</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let campaign of campaignService.campaigns">
<tr routerLink="/campaign/{{ campaign.id }}/view">
<td>{{ campaign.name }}</td>
<td>{{ campaign.currentStatus }}</td>
<td>{{ campaign.size }}</td>
<td>{{ campaign.createDate | date:'dd-MMM-yyyy'}}</td>
</tr>
</ng-container>
<p *ngIf="campaignService.campaigns.length === 0">No campaigns created yet. Create compaigns by clicking <a routerLink="/campaign/new">here</a></p>
</tbody>
</table>
</div>
</div>

View File

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

View File

@@ -0,0 +1,24 @@
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { CampaignService } from '../campaign.service';
@Component({
selector: 'app-campaign',
templateUrl: './campaign.component.html',
styleUrls: ['./campaign.component.css']
})
export class CampaignComponent implements OnInit {
constructor(private campaignService: CampaignService, private router: Router) { }
ngOnInit() {
this.campaignService.getCampaigns();
const intervalId = setInterval(() => {
this.campaignService.getCampaigns();
if (!this.router.url.includes('/campaign')) {
clearInterval(intervalId);
}
}, 2000);
}
}

View File

@@ -0,0 +1,27 @@
<div class="row">
<div class="col-12">
<div class="row mt-3">
<div class="col-12 d-flex">
<h4>Login</h4>
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<label for="login-username" class="pr-2 mt-auto mb-auto">Username</label>
<input type="text" class="flex-grow-1" id="login-username" [(ngModel)]="username" >
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<label for="login-password" class="pr-2 mt-auto mb-auto">Password</label>
<input type="password" class="flex-grow-1" id="login-password" [(ngModel)]="password" >
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<button type="button" (click)="login()" class="btn btn-primary mr-3">Login</button>
<button type="button" (click)="routeRegister()" class="btn btn-primary">Register</button>
</div>
</div>
</div>
<div>

View File

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

View File

@@ -0,0 +1,27 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../auth.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
username: string;
password: string;
login() {
this.authService.login(this.username, this.password);
}
routeRegister() {
this.router.navigate(['/register']);
}
constructor(private authService: AuthService, private router: Router) { }
ngOnInit() {
}
}

View File

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

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MainComponent } from './main.component';
describe('MainComponent', () => {
let component: MainComponent;
let fixture: ComponentFixture<MainComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MainComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MainComponent);
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-main',
templateUrl: './main.component.html',
styleUrls: ['./main.component.css']
})
export class MainComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

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

View File

@@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
export class Notification {
id: number;
resultType: string; // enum success or failure or info
text: string;
}
@Injectable({
providedIn: 'root'
})
export class NotificationService {
notifications: Notification[] = [];
currentCount = 0;
addNotification(resultType, text) {
const newNotification = new Notification();
newNotification.id = this.currentCount;
this.currentCount++;
newNotification.resultType = resultType;
newNotification.text = text;
this.notifications.push(newNotification);
setTimeout(() => this.closeNotification(newNotification), 3000);
}
closeNotification(notify: Notification) {
for (let i = 0; i < this.notifications.length; i++) {
if (this.notifications[i].id === notify.id) {
this.notifications.splice(i, 1);
break;
}
}
}
constructor() {
}
}

View File

@@ -0,0 +1,3 @@
<div class="alert notification col-11 mx-auto" *ngFor="let notification of notificationService.notifications" [ngClass]="{'alert-success': notification.resultType === 'success', 'alert-danger': notification.resultType ==='failure'}" (click)=notificationService.closeNotification(notification)>
{{ notification.text }}
</div>

View File

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

View File

@@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { NotificationService } from '../notification.service';
@Component({
selector: 'app-notification',
templateUrl: './notification.component.html',
styleUrls: ['./notification.component.css']
})
export class NotificationComponent implements OnInit {
constructor(private notificationService: NotificationService) { }
ngOnInit() {
}
}

View File

@@ -0,0 +1,3 @@
.no-space-break {
white-space:nowrap;
}

View File

@@ -0,0 +1,84 @@
<div class="row p-2">
<div class="col-12">
<div class="row mt-3">
<div class="col-12 d-flex">
<label for="campaignName" class="pr-2 mt-auto mb-auto">Phonebook Name</label>
<input type="text" class="flex-grow-1" id="campaignName" [(ngModel)]="newPhonebook.name" placeholder="Phonebook Name">
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<label class="no-space-break mt-auto mb-auto pr-2" for="import-records">Import Records</label>
<div class="custom-file" id="import-records">
<input type="file" (change)="importPhoneRecords($event.target.files)" class="custom-file-input" id="customFile">
<label class="custom-file-label" for="customFile">Choose file</label>
</div>
</div>
</div>
<div class="row">
<div class="col-12 d-flex">
<p><small><em><a href="/assets/phonebook-template.xlsx">Download file template here.</a></em></small></p>
</div>
</div>
<div class="row mt-1">
<div class="col-12 d-flex">
<table class="table table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">First Name</th>
<th scope="col">Last Name</th>
<th scope="col">Alias</th>
<th scope="col">Phone Number</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let phoneRecord of newPhoneRecords">
<tr>
<td>{{ phoneRecord.firstName }}</td>
<td>{{ phoneRecord.lastName }}</td>
<td>{{ phoneRecord.alias }}</td>
<td>{{ phoneRecord.phoneNumber }}</td>
</tr>
</ng-container>
<tr (keyup.enter)="insertAdditionalRecord()">
<td><input type="text" [(ngModel)]="additionalRecord.firstName" class="form-control" placeholder="firstName"></td>
<td><input type="text" [(ngModel)]="additionalRecord.lastName" class="form-control" placeholder="lastName"></td>
<td><input type="text" [(ngModel)]="additionalRecord.alias" class="form-control" placeholder="alias"></td>
<td><input type="text" [(ngModel)]="additionalRecord.phoneNumber" class="form-control" placeholder="phoneNumber"></td>
</tr>
<tr>
<p><small><em>Press enter to insert additional record</em></small></p>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<button type="button" (click)="submitNewPhonebook()" class="btn btn-primary mr-2">Save Phonebook</button>
<button type="button" *ngIf="router.url !== '/phonebook/new'" class="btn btn-danger ml-auto" data-toggle="modal" data-target="#completeModal">Delete</button>
</div>
</div>
</div>
<div class="modal fade" id="completeModal" tabindex="-1" role="dialog" aria-labelledby="completeModal" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{{ newPhonebook.name }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the phonebook?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" (click)="deletePhonebook()" data-dismiss="modal">Delete Phonebook</button>
</div>
</div>
</div>
</div>

View File

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

View File

@@ -0,0 +1,70 @@
import { Component, OnInit } from '@angular/core';
import { PhonebookService, Phonebook, PhoneRecord } from '../phonebook.service';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
@Component({
selector: 'app-phonebook-new',
templateUrl: './phonebook-new.component.html',
styleUrls: ['./phonebook-new.component.css']
})
export class PhonebookNewComponent implements OnInit {
constructor(private phonebookService: PhonebookService, private router: Router, private route: ActivatedRoute) { }
id = 0;
newPhonebook: Phonebook = new Phonebook();
newPhoneRecords: PhoneRecord[] = [];
additionalRecord: PhoneRecord = new PhoneRecord();
insertAdditionalRecord() {
this.newPhoneRecords = this.newPhoneRecords.concat(this.additionalRecord);
this.additionalRecord = new PhoneRecord();
this.additionalRecord.phoneNumber = '';
}
importPhoneRecords(files: FileList) {
this.phonebookService.uploadPhonebook(files.item(0)).subscribe(data => {
this.newPhoneRecords = this.newPhoneRecords.concat(data);
});
}
submitNewPhonebook() {
if (this.router.url === '/phonebook/new') {
if (this.additionalRecord.phoneNumber !== '') {
this.insertAdditionalRecord();
}
this.newPhonebook.records = this.newPhoneRecords;
this.phonebookService.addPhonebook(this.newPhonebook);
} else {
this.editPhonebook();
}
}
deletePhonebook() {
this.phonebookService.deletePhonebook(this.newPhonebook);
}
editPhonebook() {
this.newPhonebook.records = this.newPhoneRecords;
this.phonebookService.editPhonebook(this.newPhonebook);
}
ngOnInit() {
this.additionalRecord = new PhoneRecord();
this.additionalRecord.phoneNumber = '';
// if page is edit
if (this.router.url !== '/phonebook/new') {
const idParam = 'id';
this.route.params.subscribe( params => {
this.id = parseInt(params[idParam], 10);
this.phonebookService.getPhonebookObs(this.id).subscribe(currPb => {
this.newPhonebook = currPb;
this.newPhoneRecords = this.newPhonebook.records;
});
});
}
}
}

View File

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

View File

@@ -0,0 +1,105 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Router } from '@angular/router';
import { NotificationService } from './notification.service';
export class Phonebook {
id: number;
name: string;
size: number;
createDate: Date;
records: PhoneRecord[];
}
export class PhoneRecord {
id: number;
firstName: string;
lastName: string;
alias: string;
phoneNumber: string;
}
export class PhonebookNotification {
resultType: string;
text: string;
payload: Phonebook;
}
@Injectable({
providedIn: 'root'
})
export class PhonebookService {
phonebooks: Phonebook[] = [];
phonebookUrl = '/api/phonebook';
phonebookImportUrl = '/api/import-phonebook';
httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
}),
};
getPhonebooks() {
this.http.get<Phonebook[]>(this.phonebookUrl).subscribe(phonebooks => {
if (phonebooks === null) {
this.phonebooks = [];
} else {
this.phonebooks = phonebooks;
}
});
}
getPhonebookObs(id: number): Observable<Phonebook> {
return this.http.get<Phonebook>(this.phonebookUrl + '/' + id.toString());
}
addPhonebook(phonebook: Phonebook) {
this.http.post<PhonebookNotification>(this.phonebookUrl, phonebook, this.httpOptions).subscribe(pbNotification => {
this.notificationService.addNotification(pbNotification.resultType, pbNotification.text);
this.phonebooks.push(pbNotification.payload);
if (pbNotification.payload !== null) {
this.router.navigate(['/phonebook']);
}
},
err => {
this.notificationService.addNotification('failure', 'Error in creating phonebook');
});
}
editPhonebook(phonebook: Phonebook) {
this.http.put<PhonebookNotification>(this.phonebookUrl + '/' + phonebook.id.toString(), phonebook, this.httpOptions)
.subscribe(pbNotification => {
this.notificationService.addNotification(pbNotification.resultType, pbNotification.text);
if (pbNotification.payload !== null) {
this.router.navigate(['/phonebook']);
}
},
err => {
this.notificationService.addNotification('failure', 'Error in editing phonebook');
});
}
deletePhonebook(phonebook: Phonebook) {
this.http.delete<PhonebookNotification>(this.phonebookUrl + '/' + phonebook.id.toString(), this.httpOptions)
.subscribe(pbNotification => {
this.notificationService.addNotification(pbNotification.resultType, pbNotification.text);
this.router.navigate(['/phonebook']);
},
err => {
this.notificationService.addNotification('failure', 'Error in deleting phonebook');
});
}
uploadPhonebook(file: File): Observable<PhoneRecord[]> {
const formData = new FormData();
formData.append('phonebookFile', file);
return this.http.post<PhoneRecord[]>(this.phonebookImportUrl, formData);
}
constructor(private http: HttpClient, private router: Router, private notificationService: NotificationService) {
this.getPhonebooks();
}
}

View File

@@ -0,0 +1,29 @@
<div class="row">
<div class="col-12">
<button class="btn btn-primary" routerLink="/phonebook/new">New Phonebook</button>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<table class="table table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">Name</th>
<th scope="col">Phonebook Size</th>
<th scope="col">Create Date</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let phonebook of phonebookService.phonebooks">
<tr routerLink="/phonebook/{{ phonebook.id }}/edit">
<td>{{ phonebook.name }}</td>
<td>{{ phonebook.size }}</td>
<td>{{ phonebook.createDate | date:'dd-MMM-yyyy'}}</td>
</tr>
</ng-container>
<p *ngIf="phonebookService.phonebooks.length === 0">No phonebooks created yet. Create phonebooks by clicking <a routerLink="/phonebook/new">here</a></p>
</tbody>
</table>
</div>
</div>

View File

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

View File

@@ -0,0 +1,17 @@
import { Component, OnInit } from '@angular/core';
import { PhonebookService } from '../phonebook.service';
@Component({
selector: 'app-phonebook',
templateUrl: './phonebook.component.html',
styleUrls: ['./phonebook.component.css']
})
export class PhonebookComponent implements OnInit {
constructor(private phonebookService: PhonebookService) { }
ngOnInit() {
this.phonebookService.getPhonebooks();
}
}

View File

@@ -0,0 +1,32 @@
<div class="row">
<div class="col-12">
<div class="row mt-3">
<div class="col-12 d-flex">
<h4>Settings for {{ currUser.username }}</h4>
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<label for="setting-displayname" class="pr-2 mt-auto mb-auto">Display Name</label>
<input type="text" class="flex-grow-1" id="setting-displayname" [(ngModel)]="currUser.name" >
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<label for="setting-email" class="pr-2 mt-auto mb-auto">Email</label>
<input type="text" class="flex-grow-1" id="setting-email" [(ngModel)]="currUser.email" >
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<label for="setting-password" class="pr-2 mt-auto mb-auto">Change Password</label>
<input type="password" class="flex-grow-1" id="setting-password" [(ngModel)]="currUser.password" >
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<button type="button" (click)="updateUser()" class="btn btn-primary">Submit</button>
</div>
</div>
</div>
<div>

View File

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

View File

@@ -0,0 +1,24 @@
import { Component, OnInit } from '@angular/core';
import { AuthService, User } from '../auth.service';
@Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {
constructor(private authService: AuthService) { }
currUser: User;
updateUser() {
this.authService.updateUser(this.currUser);
}
ngOnInit() {
this.authService.getUserObs().subscribe(user => {
this.currUser = JSON.parse(JSON.stringify(user));
});
}
}

View File

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

View File

@@ -0,0 +1,58 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { NotificationService } from './notification.service';
import { Observable, of } from 'rxjs';
export class TwilioProvider {
accountSID: string;
authToken: string;
}
export class TwilioProviderNotification {
resultType: string;
text: string;
payload: TwilioProvider;
}
@Injectable({
providedIn: 'root'
})
export class ProviderService {
twilioProviderSettings: TwilioProvider = new TwilioProvider();
twilioUrl = '/api/provider/twilio';
httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
}),
};
providerEnums = [
{name: 'Twilio', tag: 'twilio'},
];
getTwilioProvider() {
this.http.get<TwilioProvider>(this.twilioUrl, this.httpOptions).subscribe(thisTwilio => {
this.twilioProviderSettings = thisTwilio;
});
}
getTwilioProviderObs(): Observable<TwilioProvider> {
return this.http.get<TwilioProvider>(this.twilioUrl, this.httpOptions);
}
updateTwilioProvider(tProvider: TwilioProvider) {
this.http.post<TwilioProviderNotification>(this.twilioUrl, tProvider, this.httpOptions).subscribe(usermessage => {
this.notificationService.addNotification(usermessage.resultType, usermessage.text);
this.twilioProviderSettings = usermessage.payload;
},
err => {
this.notificationService.addNotification('failure', 'Error in updating Twilio provider');
});
}
constructor(private http: HttpClient, private notificationService: NotificationService) {
this.getTwilioProvider();
}
}

View File

@@ -0,0 +1,28 @@
<div class="row p-2">
<div class="col-12">
<div class="card">
<div class="card-header">
Twilio Settings
</div>
<div class="card-body">
<div class="row">
<div class="form-group col-12">
<label for="twilio-account-sid" class="pr-2 mt-auto mb-auto">Account SID</label>
<input type="text" class="flex-grow-1" id="twilio-account-sid" [(ngModel)]="currTwilioProvider.accountSID" placeholder="Twilio Account SID">
</div>
</div>
<div class="row">
<div class="form-group col-12">
<label for="twilio-auth-token" class="pr-2 mt-auto mb-auto">Auth Token</label>
<input type="text" class="flex-grow-1" id="twilio-auth-token" [(ngModel)]="currTwilioProvider.authToken" placeholder="Twilio Auth Token">
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<button type="button" (click)="submitProviders()" class="btn btn-primary ml-2">Save Provider Settings</button>
</div>
</div>
</div>
</div>

View File

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

View File

@@ -0,0 +1,26 @@
import { Component, OnInit } from '@angular/core';
import { ProviderService, TwilioProvider } from '../provider.service';
@Component({
selector: 'app-provider',
templateUrl: './provider.component.html',
styleUrls: ['./provider.component.css']
})
export class ProviderComponent implements OnInit {
currTwilioProvider: TwilioProvider = new TwilioProvider();
submitProviders() {
this.providerService.updateTwilioProvider(this.currTwilioProvider);
}
constructor(private providerService: ProviderService) { }
ngOnInit() {
this.providerService.getTwilioProviderObs().subscribe(currTwilio => {
this.currTwilioProvider = currTwilio;
});
}
}

View File

@@ -0,0 +1,44 @@
<div class="row">
<div class="col-12">
<div class="row mt-3">
<div class="col-12 d-flex">
<h4>Login</h4>
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<label for="login-name" class="pr-2 mt-auto mb-auto">Name</label>
<input type="text" class="flex-grow-1" id="login-name" [(ngModel)]="name" >
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<label for="login-username" class="pr-2 mt-auto mb-auto">Username</label>
<input type="text" class="flex-grow-1" id="register-username" [(ngModel)]="username" >
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<label for="login-password" class="pr-2 mt-auto mb-auto">Password</label>
<input type="password" class="flex-grow-1" id="register-password" [(ngModel)]="password" >
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<label for="login-username" class="pr-2 mt-auto mb-auto">Email</label>
<input type="text" class="flex-grow-1" id="register-email" [(ngModel)]="email" >
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<label for="secret-code" class="pr-2 mt-auto mb-auto">Secret Code</label>
<input type="text" class="flex-grow-1" id="secret-code" [(ngModel)]="secretCode" >
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<button type="button" (click)="register()" class="btn btn-primary">Register</button>
</div>
</div>
</div>
<div>

View File

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

View File

@@ -0,0 +1,25 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../auth.service';
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.css']
})
export class RegisterComponent implements OnInit {
username = '';
password = '';
email = '';
name = '';
secretCode = '';
register() {
this.authService.register(this.username, this.password, this.email, this.name, this.secretCode);
}
constructor(private authService: AuthService) { }
ngOnInit() {
}
}

View File

@@ -0,0 +1,3 @@
#new-text-preview {
font-family: "Courier New"
}

View File

@@ -0,0 +1,64 @@
<div class="row p-2">
<div class="col-12">
<div class="row mt-3">
<div class="col-12 d-flex">
<label for="campaignName" class="pr-2 mt-auto mb-auto">Text Template Name</label>
<input type="text" class="flex-grow-1" id="campaignName" [(ngModel)]="newTextTemplate.name" placeholder="Text Template Name">
</div>
</div>
<div class="row mt-3">
<div class="form-group col-12">
<label for="text-template-area">Text Template</label>
<textarea class="form-control flex" [(ngModel)]="newTextTemplate.templateStr" (ngModelChange)="updatePreview()" id="text-template-area" rows="6"></textarea>
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<button type="button" (click)="submitNewTextTemplate()" class="btn btn-primary ml-2">Save Text Template</button>
<button type="button" *ngIf="router.url !== '/text-template/new'" class="btn btn-danger ml-auto" data-toggle="modal" data-target="#completeModal">Delete</button>
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<div class="form-group col-12">
<label for="tags-available" class="pr-2 mt-auto mb-auto">Tags Available</label>
<div id="tags-available">
<ul>
<li>{{ '{' }}firstName{{ '}' }}</li>
<li>{{ '{' }}lastName{{ '}' }}</li>
<li>{{ '{' }}alias{{ '}' }}</li>
<li>{{ '{' }}phoneNumber{{ '}' }}</li>
</ul>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<div class="form-group col-12">
<label for="new-text-preview" class="pr-2 mt-auto mb-auto">Preview</label>
<textarea class="form-control flex" [(ngModel)]="previewStr" id="new-text-preview" rows="6" disabled></textarea>
</div>
</div>
</div>
</div>
<div class="modal fade" id="completeModal" tabindex="-1" role="dialog" aria-labelledby="completeModal" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{{ newTextTemplate.name }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the text template?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" (click)="deleteTextTemplate()" data-dismiss="modal">Delete Text Template</button>
</div>
</div>
</div>
</div>

View File

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

View File

@@ -0,0 +1,58 @@
import { Component, OnInit } from '@angular/core';
import { TextTemplate, TextTemplateService } from '../text-template.service';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
@Component({
selector: 'app-text-template-new',
templateUrl: './text-template-new.component.html',
styleUrls: ['./text-template-new.component.css']
})
export class TextTemplateNewComponent implements OnInit {
newTextTemplate: TextTemplate = new TextTemplate();
previewStr: string;
id = 0;
submitNewTextTemplate() {
if (this.router.url === '/text-template/new') {
this.textTemplateService.addTextTemplate(this.newTextTemplate);
} else {
this.editTextTemplate();
}
}
updatePreview() {
let tempStr = '';
tempStr = this.newTextTemplate.templateStr;
tempStr = tempStr.replace('{firstName}', 'John');
tempStr = tempStr.replace('{lastName}', 'Smith');
tempStr = tempStr.replace('{alias}', 'Johnny');
tempStr = tempStr.replace('{phoneNumber}', '+6598765432');
this.previewStr = tempStr;
}
deleteTextTemplate() {
this.textTemplateService.deleteTextTemplate(this.newTextTemplate);
}
editTextTemplate() {
this.textTemplateService.editTextTemplate(this.newTextTemplate);
}
constructor(private textTemplateService: TextTemplateService, private router: Router, private route: ActivatedRoute) { }
ngOnInit() {
// if page is edit
if (this.router.url !== '/text-template/new') {
const idParam = 'id';
this.route.params.subscribe( params => {
this.id = parseInt(params[idParam], 10);
this.textTemplateService.getTextTemplateObs(this.id).subscribe(currTT => {
this.newTextTemplate = currTT;
});
});
}
}
}

View File

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

View File

@@ -0,0 +1,89 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { NotificationService } from './notification.service';
export class TextTemplate {
id: number;
name: string;
templateStr: string;
createDate: Date;
}
export class TextTemplateNotification {
resultType: string;
text: string;
payload: TextTemplate;
}
@Injectable({
providedIn: 'root'
})
export class TextTemplateService {
textTemplates: TextTemplate[] = [];
templateUrl = '/api/text-template';
httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
}),
};
getTextTemplates() {
this.http.get<TextTemplate[]>(this.templateUrl).subscribe(templates => {
if (templates === null) {
this.textTemplates = [];
} else {
this.textTemplates = templates;
}
});
}
getTextTemplateObs(id: number) {
return this.http.get<TextTemplate>(this.templateUrl + '/' + id.toString());
}
addTextTemplate(newTextTemplate: TextTemplate) {
this.http.post<TextTemplateNotification>(this.templateUrl, newTextTemplate, this.httpOptions).subscribe(templateNotification => {
this.notificationService.addNotification(templateNotification.resultType, templateNotification.text);
this.textTemplates.push(templateNotification.payload);
if (templateNotification.payload !== null) {
this.router.navigate(['/text-template']);
}
},
err => {
this.notificationService.addNotification('failure', 'Error in creating template');
});
}
deleteTextTemplate(textTemplate: TextTemplate) {
this.http.delete<TextTemplateNotification>(this.templateUrl + '/' + textTemplate.id.toString(), this.httpOptions)
.subscribe(templateNotification => {
this.notificationService.addNotification(templateNotification.resultType, templateNotification.text);
this.router.navigate(['/text-template']);
},
err => {
this.notificationService.addNotification('failure', 'Error in deleting text template');
});
}
editTextTemplate(textTemplate: TextTemplate) {
this.http.put<TextTemplateNotification>(this.templateUrl + '/' + textTemplate.id.toString(), textTemplate, this.httpOptions)
.subscribe(templateNotification => {
this.notificationService.addNotification(templateNotification.resultType, templateNotification.text);
if (templateNotification.payload !== null) {
this.router.navigate(['/text-template']);
}
},
err => {
this.notificationService.addNotification('failure', 'Error in editing text template');
});
}
constructor(private http: HttpClient, private router: Router, private notificationService: NotificationService) {
this.getTextTemplates();
}
}

View File

@@ -0,0 +1,27 @@
<div class="row">
<div class="col-12">
<button class="btn btn-primary" routerLink="/text-template/new">New Text Template</button>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<table class="table table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">Name</th>
<th scope="col">Create Date</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let textTemplate of textTemplateService.textTemplates">
<tr routerLink="/text-template/{{ textTemplate.id }}/edit">
<td>{{ textTemplate.name }}</td>
<td>{{ textTemplate.createDate | date:'dd-MMM-yyyy'}}</td>
</tr>
</ng-container>
<p *ngIf="textTemplateService.textTemplates.length === 0">No text template created yet. Create templates by clicking <a routerLink="/text-template/new">here</a></p>
</tbody>
</table>
</div>
</div>

View File

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

View File

@@ -0,0 +1,17 @@
import { Component, OnInit } from '@angular/core';
import { TextTemplateService } from '../text-template.service';
@Component({
selector: 'app-text-template',
templateUrl: './text-template.component.html',
styleUrls: ['./text-template.component.css']
})
export class TextTemplateComponent implements OnInit {
constructor(private textTemplateService: TextTemplateService) { }
ngOnInit() {
this.textTemplateService.getTextTemplates();
}
}

View File

@@ -0,0 +1,3 @@
export class Urls {
getCampaigns = 'http://127.0.0.1:8000/api/campaigns';
}

View File

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,331 @@
/*!
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/)
* Copyright 2011-2019 The Bootstrap Authors
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
[tabindex="-1"]:focus {
outline: 0 !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg {
overflow: hidden;
vertical-align: middle;
}
table {
border-collapse: collapse;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #6c757d;
text-align: left;
caption-side: bottom;
}
th {
text-align: inherit;
}
label {
display: inline-block;
margin-bottom: 0.5rem;
}
button {
border-radius: 0;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
select {
word-wrap: normal;
}
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button:not(:disabled),
[type="button"]:not(:disabled),
[type="reset"]:not(:disabled),
[type="submit"]:not(:disabled) {
cursor: pointer;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea {
overflow: auto;
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
max-width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
color: inherit;
white-space: normal;
}
progress {
vertical-align: baseline;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
outline-offset: -2px;
-webkit-appearance: none;
}
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
summary {
display: list-item;
cursor: pointer;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/)
* Copyright 2011-2019 The Bootstrap Authors
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More