diff --git a/client/package.json b/client/package.json index 2e00ec649bcfe456bef7283b02cc36f5c741f734..edef3c6d9e8ef53b84e7ff11b7252978790eb2b6 100644 --- a/client/package.json +++ b/client/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "start": "ng serve", + "start": "ng serve --proxy-config proxy.conf.json", "build": "ng build", "test": "ng test", "lint": "ng lint", diff --git a/client/proxy.conf.json b/client/proxy.conf.json new file mode 100644 index 0000000000000000000000000000000000000000..073c1d74ab8faeeaa52c99586b4cfb83d9f85e1d --- /dev/null +++ b/client/proxy.conf.json @@ -0,0 +1,6 @@ +{ + "/api": { + "target": "http://localhost:3000", + "secure": false + } + } \ No newline at end of file diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 9dda020f553784e279ee3261905643fc0bdb4d44..15fb2c5826cf6db2157224957e954c5df0894cfe 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -3,7 +3,7 @@ import { RouterModule, Routes } from '@angular/router'; import { PostFormComponent } from './posts/post-form/post-form.component'; const routes: Routes = [ - { path: 'postForm', component: PostFormComponent } + { path: 'annonse/ny', component: PostFormComponent } ]; @NgModule({ diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 008d62ffa4ea86d0af22ae53ee5711ba1859e7ea..fb29b9167c4d5cdd2f077f9fba26de35f258c19a 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -1,9 +1,11 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; +import { HttpClientModule } from '@angular/common/http'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { PostModule } from './posts/post.module'; +import { SharedModule } from './shared/shared.module'; @NgModule({ declarations: [ @@ -12,7 +14,9 @@ import { PostModule } from './posts/post.module'; imports: [ BrowserModule, AppRoutingModule, - PostModule + PostModule, + SharedModule, + HttpClientModule ], providers: [], bootstrap: [AppComponent] diff --git a/client/src/app/models/deserializable.model.ts b/client/src/app/models/deserializable.model.ts index 28a3f4b28713b94f67d84a6b012ef6827defef35..8c8e258ad55ce5f0a764b64c464ba8fff463f663 100644 --- a/client/src/app/models/deserializable.model.ts +++ b/client/src/app/models/deserializable.model.ts @@ -1,3 +1,3 @@ export interface Deserializable { - deserialize(input: string): this; + deserialize(input: Object): this; } \ No newline at end of file diff --git a/client/src/app/models/post.model.ts b/client/src/app/models/post.model.ts index c712beb79683e4be2bd8457ed5d2a65830218786..3e4c006cb7243a96675b70a20b0623a594870703 100644 --- a/client/src/app/models/post.model.ts +++ b/client/src/app/models/post.model.ts @@ -2,72 +2,119 @@ import { Deserializable } from "./deserializable.model"; import { Serializable } from "./serializable.model"; export class Post implements Deserializable, Serializable { + private id: number; private title: string; private description: string; private timestamp: Date; - private user: string; + private owner: string; + private imageUrl: string; + private price: number; + private categoryid: number; constructor(input: any = null) { if (input) { + this.id = input.id; this.title = input.title; this.description = input.description; - this.timestamp = new Date(input.timestamp); - this.user = input.user; + this.timestamp = input.timestamp; + this.owner = input.owner; + this.imageUrl = input.imageUrl; + this.price = input.price; + this.categoryid = input.categoryid; } else { - this.title = ""; - this.description = ""; + this.id = 0; + this.title = null; + this.description = null; this.timestamp = new Date(); - this.user = ""; + this.owner = null; + this.imageUrl = null; + this.price = null; + this.categoryid = null; } } - deserialize(input: string): this { - const obj = JSON.parse(input); - Object.assign(this, obj); + deserialize(input: Object): this { + Object.assign(this, input); this.timestamp = new Date(this.timestamp); return this; } - serialize(): string { - return JSON.stringify({ + serialize(): Object { + return { + id: this.id, title: this.title, description: this.description, timestamp: this.timestamp.valueOf(), - user: this.user - }); + owner: this.owner, + imageUrl: this.imageUrl, + price: this.price, + categoryid: this.categoryid + }; } - get getTitle () { + get getId() { + return this.id; + } + + set setId(id: number) { + this.id = id; + } + + get getTitle() { return this.title; } - set setTitle (title: string) { + set setTitle(title: string) { this.title = title; } - get getDescription () { + get getDescription() { return this.description; } - set setDescription (description: string) { + set setDescription(description: string) { this.description = description; } - get getTimestamp () { + get getTimestamp() { return this.timestamp; } - set setTimestamp (timestamp: Date) { + set setTimestamp(timestamp: Date) { this.timestamp = timestamp; } - get getUser () { - return this.user; + get getOwner() { + return this.owner; + } + + set setOwner(owner: string) { + this.owner = owner; + } + + get getImageUrl() { + return this.imageUrl; + } + + set setImageUrl(imageUrl: string) { + this.imageUrl = imageUrl; + } + + get getPrice() { + return this.price; + } + + set setPrice(price: number) { + this.price = price; + } + + get getCategory() { + return this.categoryid; } - set setUser (user: string) { - this.user = user; + set setCategory(categoryid: number) { + this.categoryid = categoryid; } } \ No newline at end of file diff --git a/client/src/app/models/serializable.model.ts b/client/src/app/models/serializable.model.ts index 6de9e54cd0c72e4b077d9a9719c6f3c8b8a0e953..c985231b89d8481692b085038800874524855d8e 100644 --- a/client/src/app/models/serializable.model.ts +++ b/client/src/app/models/serializable.model.ts @@ -1,3 +1,3 @@ export interface Serializable { - serialize(): string; + serialize(): Object; } \ No newline at end of file diff --git a/client/src/app/posts/post-form/post-form.component.html b/client/src/app/posts/post-form/post-form.component.html index ea9250cd54564922d03bf0e8b329aa7217881b7f..e89803521b22e8a339b97868c51233890838d3f4 100644 --- a/client/src/app/posts/post-form/post-form.component.html +++ b/client/src/app/posts/post-form/post-form.component.html @@ -1,4 +1,18 @@ -<p>Post form!</p> +<h3>Lag annonse</h3> -<p>Title: {{deserializedPost.getTitle}}</p> -<p>Description: {{deserializedPost.getDescription}}</p> +<app-text-input [(inputModel)]="title" label="Tittel" (blur)="checkForm()"></app-text-input> + +<app-text-input [(inputModel)]="description" label="Beskrivelse" (blur)="checkForm()"></app-text-input> + +<app-number-input [(inputModel)]="price" label="Pris" (blur)="checkForm()"></app-number-input> + +<app-select [(inputModel)]="categoryid" (change)="checkForm()" label="Kategori"> + <option value="0" selected>Velg kategori</option> + <option value="1">Antikviteter og Kunst</option> + <option value="2">Dyr og Utstyr</option> + <option value="3">Elektronikk og Hvitevarer</option> +</app-select> + +<p>{{statusMessage}}</p> + +<app-button (click)="publishPost()" text="Publiser"></app-button> \ No newline at end of file diff --git a/client/src/app/posts/post-form/post-form.component.spec.ts b/client/src/app/posts/post-form/post-form.component.spec.ts index edf6f54aec58f4ecd2deb0658f5c5034078ca8fa..7e82535b45a1a2c9cfa883b910a956bd3dc4ff89 100644 --- a/client/src/app/posts/post-form/post-form.component.spec.ts +++ b/client/src/app/posts/post-form/post-form.component.spec.ts @@ -1,14 +1,20 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; import { PostFormComponent } from './post-form.component'; describe('PostFormComponent', () => { let component: PostFormComponent; let fixture: ComponentFixture<PostFormComponent>; + let router: Router; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ PostFormComponent ] + declarations: [ PostFormComponent ], + imports: [ HttpClientTestingModule, RouterTestingModule, FormsModule ] }) .compileComponents(); }); @@ -17,23 +23,52 @@ describe('PostFormComponent', () => { fixture = TestBed.createComponent(PostFormComponent); component = fixture.componentInstance; fixture.detectChanges(); + + router = TestBed.inject(Router); }); it('should create', () => { expect(component).toBeTruthy(); }); - it('should serialize Post', () => { - expect(component.serializedPost == "").toBeFalse(); + it('should validate form', () => { + expect(component.checkForm()).toBeFalse(); + expect(component.statusMessage).toBe("Tittelen kan ikke være tom"); + + component.title = "Title"; + expect(component.checkForm()).toBeFalse(); + expect(component.statusMessage).toBe("Beskrivelsen kan ikke være tom"); + + component.description = "Description"; + component.price = -100; + expect(component.checkForm()).toBeFalse(); + expect(component.statusMessage).toBe("Prisen kan ikke være negativ"); - try{ - const obj = JSON.parse(component.serializedPost); - }catch{ - fail("Could not serialize"); - } - }) + component.price = null; + expect(component.checkForm()).toBeFalse(); + expect(component.statusMessage).toBe("Annonsen må ha en pris"); - it('should deserialize Post', () => { - expect(component.deserializedPost.getUser).toEqual("Admin"); - }) + component.price = 50; + expect(component.checkForm()).toBeFalse(); + expect(component.statusMessage).toBe("Annonsen må ha en kategori"); + + component.categoryid = 2; + expect(component.checkForm()).toBeTrue(); + expect(component.statusMessage).toBe(""); + }); + + it('should stop publishing invalid post', fakeAsync(() => { + component.publishPost(); + expect(component.statusMessage).toBe("Tittelen kan ikke være tom"); + })); + + it('should route after publishing post', () => { + component.title = "Title"; + component.description = "Description"; + component.price = 50; + component.categoryid = 2; + component.publishPost(); + + expect(router.url).toBe('/'); + }); }); diff --git a/client/src/app/posts/post-form/post-form.component.ts b/client/src/app/posts/post-form/post-form.component.ts index 7f49d9133fb35e63b975fa9da0ccb053b82406bc..5864463a491b3e0ac1ee152b7db60a0017e1b836 100644 --- a/client/src/app/posts/post-form/post-form.component.ts +++ b/client/src/app/posts/post-form/post-form.component.ts @@ -1,5 +1,7 @@ import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; import { Post } from 'src/app/models/post.model'; +import { PostService } from '../post.service'; @Component({ selector: 'app-post-form', @@ -8,20 +10,75 @@ import { Post } from 'src/app/models/post.model'; }) export class PostFormComponent implements OnInit { - serializedPost: string = ""; - deserializedPost: Post = new Post(); - - constructor() { } - - ngOnInit(): void { - let post = new Post({ - title: "TestAnnonse", - description: "Beskrivelse", - timestamp: 1612952332000, - user: "Admin" - }); - - this.serializedPost = post.serialize(); - this.deserializedPost.deserialize(post.serialize()); + title: string = ""; + description: string = ""; + price: number = 0; + categoryid: number = 0; + + statusMessage: string = ""; + + constructor(private postService: PostService, private router: Router) { } + + ngOnInit() { + } + + /** + * Validates form. + */ + checkForm(): boolean { + if (this.title == "") { + this.setStatusMessage("Tittelen kan ikke være tom"); + return false; + } + if (this.description == "") { + this.setStatusMessage("Beskrivelsen kan ikke være tom"); + return false; + } + if (this.price < 0) { + this.setStatusMessage("Prisen kan ikke være negativ"); + return false; + } + if (this.price == null) { + this.setStatusMessage("Annonsen må ha en pris"); + return false; + } + if (this.categoryid < 1) { + this.setStatusMessage("Annonsen må ha en kategori"); + return false; + } + + this.setStatusMessage(""); + return true; + } + + /** + * Publishes post if it is valid. + */ + publishPost() { + if (this.checkForm()) { + let newPost = new Post({ + title: this.title, + description: this.description, + timestamp: new Date(), + owner: "admin", + imageUrl: "", + price: this.price, + categoryid: this.categoryid + }); + + this.postService.addPost(newPost).then(status => { + console.log("Post was added: " + status); + this.router.navigateByUrl("/"); + }).catch(error => { + console.log("Error adding post: " + error); + }); + } + } + + /** + * Sets a status message. + */ + setStatusMessage(message: string){ + this.statusMessage = message; } -} +} \ No newline at end of file diff --git a/client/src/app/posts/post.module.ts b/client/src/app/posts/post.module.ts index 80a6a344243bbd98e2a125dca6b7815ca61c5a2c..5599cc30a3eb472c97bfcc9d6212aacfda387cb6 100644 --- a/client/src/app/posts/post.module.ts +++ b/client/src/app/posts/post.module.ts @@ -1,6 +1,8 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { PostFormComponent } from './post-form/post-form.component'; +import { SharedModule } from '../shared/shared.module'; +import { FormsModule } from '@angular/forms'; @@ -9,7 +11,10 @@ import { PostFormComponent } from './post-form/post-form.component'; PostFormComponent ], imports: [ - CommonModule + CommonModule, + SharedModule, + FormsModule ] }) + export class PostModule { } diff --git a/client/src/app/posts/post.service.spec.ts b/client/src/app/posts/post.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..24c073bf45c74c0c777218e4220d8f29d1f7179b --- /dev/null +++ b/client/src/app/posts/post.service.spec.ts @@ -0,0 +1,128 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { Post } from '../models/post.model'; + +import { PostService } from './post.service'; + +describe('PostService', () => { + let service: PostService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ HttpClientTestingModule ] + }); + service = TestBed.inject(PostService); + httpMock = TestBed.inject(HttpTestingController); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('getPost', () => { + it('should get a post', () => { + service.getPost(1).then(post => { + expect(post.getId).toBe(1); + expect(post.getTitle).toBe("Test"); + }).catch(error => { + fail(); + }); + + const req = httpMock.expectOne("api/post/1"); + expect(req.request.method).toBe("GET"); + req.flush({ + data: [{ + id: 1, + title: "Test", + description: "TestDescription", + timestamp: 23947298, + owner: "user", + imageUrl: null, + price: 49, + categoryid: 2 + }] + }); + }); + + it('should reject on invalid post', () => { + service.getPost(2).then(post => { + fail(); + }).catch(error => {}); + + const req = httpMock.expectOne("api/post/2"); + expect(req.request.method).toBe("GET"); + req.flush({ + data: [{ + id: 0, + title: "Test", + description: "TestDescription", + timestamp: 23947298 + }] + }); + }); + + it('should reject on http error', () => { + service.getPost(2).then(post => { + fail(); + }).catch(error => {}); + + const req = httpMock.expectOne("api/post/2"); + expect(req.request.method).toBe("GET"); + req.error(new ErrorEvent("400")); + }); + }); + + describe('addPost', () => { + it('should add a post', () => { + let post = new Post({ + id: 1, + title: "Test", + description: "TestDescription", + timestamp: 23947298, + owner: "user", + imageUrl: null, + price: 49, + categoryid: 2 + }); + + service.addPost(post) + .then(post => {}) + .catch(error => { + fail(); + }); + + const req = httpMock.expectOne("api/post/"); + expect(req.request.method).toBe("POST"); + expect(req.request.body).toEqual(post.serialize()); + req.flush({ + data: [{ + status: "success" + }] + }); + }); + + it('should reject on http error', () => { + let post = new Post({ + id: 1, + title: "Test", + description: "TestDescription", + timestamp: 23947298, + owner: "user", + imageUrl: null, + price: 49, + categoryid: 2 + }); + + service.addPost(post).then(post => { + fail(); + }).catch(error => {}); + + const req = httpMock.expectOne("api/post/"); + expect(req.request.method).toBe("POST"); + expect(req.request.body).toEqual(post.serialize()); + req.error(new ErrorEvent("400")); + }); + }); +}); + diff --git a/client/src/app/posts/post.service.ts b/client/src/app/posts/post.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..e476800c7e5c82b55cb3c2c5f4ad54c4bfe784c3 --- /dev/null +++ b/client/src/app/posts/post.service.ts @@ -0,0 +1,69 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Post } from '../models/post.model'; + +@Injectable({ + providedIn: 'root' +}) +export class PostService { + + postUrl = "api/post/"; + + constructor(private http: HttpClient) { } + + /** + * Get post from database by id. + */ + getPost(id: number): Promise<Post> { + return new Promise<Post>( + (resolve, reject) => { + this.get_post(id).subscribe((data: any) => { + try { + const post = new Post(); + post.deserialize(data.data[0]); + if (post.getId == 0) { + reject("Could not find Post with id: " + id); + return; + } + resolve(post); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + + private get_post(id: number) { + return this.http.get(this.postUrl + id); + } + + /** + * Adds post to database. + */ + addPost(post: Post): Promise<string> { + return new Promise<string>( + (resolve, reject) => { + this.add_post(post).subscribe((data: any) => { + try { + resolve(data.status); + } catch (err: any) { + reject(err); + } + }, + (err: any) => { + console.log(err.message); + reject(err); + }); + } + ); + } + + private add_post(post: Post) { + return this.http.post(this.postUrl, post.serialize()); + } +} diff --git a/client/src/app/shared/button/button.component.html b/client/src/app/shared/button/button.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e9923aa14c1888b601d72e264f828fe50b25bcdd --- /dev/null +++ b/client/src/app/shared/button/button.component.html @@ -0,0 +1 @@ +<button>{{text}}</button> \ No newline at end of file diff --git a/client/src/app/shared/button/button.component.scss b/client/src/app/shared/button/button.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..2b4a21691f5cafa5567332acbf906bd2c7ef3e96 --- /dev/null +++ b/client/src/app/shared/button/button.component.scss @@ -0,0 +1,3 @@ +button{ + padding: 5px; +} \ No newline at end of file diff --git a/client/src/app/shared/button/button.component.spec.ts b/client/src/app/shared/button/button.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..534a5bace070a9697ef6a711b5ef0ea5e4ed3044 --- /dev/null +++ b/client/src/app/shared/button/button.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ButtonComponent } from './button.component'; + +describe('ButtonComponent', () => { + let component: ButtonComponent; + let fixture: ComponentFixture<ButtonComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ButtonComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/shared/button/button.component.ts b/client/src/app/shared/button/button.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..605e843429c308bf9d6376e96b188b082580bcc7 --- /dev/null +++ b/client/src/app/shared/button/button.component.ts @@ -0,0 +1,15 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'app-button', + templateUrl: './button.component.html', + styleUrls: ['./button.component.scss'] +}) +export class ButtonComponent { + + @Input() + text: string; + + constructor() { } + +} diff --git a/client/src/app/shared/number-input/number-input.component.html b/client/src/app/shared/number-input/number-input.component.html new file mode 100644 index 0000000000000000000000000000000000000000..f8ce8b67272283b12e63b800ad8c9adedba48fe9 --- /dev/null +++ b/client/src/app/shared/number-input/number-input.component.html @@ -0,0 +1,12 @@ +<label> + {{label}} + + <input + type="number" + [placeholder]="placeholder" + [(ngModel)]="inputModel" + (ngModelChange)="inputModelChange.emit(inputModel)" + (change)="change.emit($event)" + (focus)="focus.emit($event)" + (blur)="blur.emit($event)"> +</label> \ No newline at end of file diff --git a/client/src/app/shared/number-input/number-input.component.scss b/client/src/app/shared/number-input/number-input.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..00cdc2a928d04283ff39dc2652060db7868d0e76 --- /dev/null +++ b/client/src/app/shared/number-input/number-input.component.scss @@ -0,0 +1,3 @@ +input{ + padding: 5px; +} \ No newline at end of file diff --git a/client/src/app/shared/number-input/number-input.component.spec.ts b/client/src/app/shared/number-input/number-input.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ff4b9f58e39fd3382d19ef53f1a2e00d68d8806 --- /dev/null +++ b/client/src/app/shared/number-input/number-input.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NumberInputComponent } from './number-input.component'; + +describe('NumberInputComponent', () => { + let component: NumberInputComponent; + let fixture: ComponentFixture<NumberInputComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NumberInputComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NumberInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/shared/number-input/number-input.component.ts b/client/src/app/shared/number-input/number-input.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..7552012c3161631122fd63086d2b894d156a2c50 --- /dev/null +++ b/client/src/app/shared/number-input/number-input.component.ts @@ -0,0 +1,33 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'app-number-input', + templateUrl: './number-input.component.html', + styleUrls: ['./number-input.component.scss'] +}) +export class NumberInputComponent { + + @Input() + label: string = ""; + + @Input() + inputModel: number; + + @Input() + placeholder: string = ""; + + @Output() + inputModelChange = new EventEmitter<number>(); + + @Output() + change = new EventEmitter(); + + @Output() + focus = new EventEmitter(); + + @Output() + blur = new EventEmitter(); + + constructor() { } + +} diff --git a/client/src/app/shared/select/select.component.html b/client/src/app/shared/select/select.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e7e4e7f86b22bf7eda4ff7658b280319b5732faf --- /dev/null +++ b/client/src/app/shared/select/select.component.html @@ -0,0 +1,13 @@ +<label> + {{label}} + + <select + [(ngModel)]="inputModel" + (ngModelChange)="inputModelChange.emit($event)" + (change)="change.emit($event)" + (focus)="focus.emit($event)" + (blur)="blur.emit($event)"> + + <ng-content></ng-content> + </select> +</label> \ No newline at end of file diff --git a/client/src/app/shared/select/select.component.scss b/client/src/app/shared/select/select.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..ffd6836a1cd0aa70df1d79d3954db1353fb96358 --- /dev/null +++ b/client/src/app/shared/select/select.component.scss @@ -0,0 +1,3 @@ +select{ + padding: 5px; +} \ No newline at end of file diff --git a/client/src/app/shared/select/select.component.spec.ts b/client/src/app/shared/select/select.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..2fb4207eb6dd5656d0ae4555b2ae7dbec7b18249 --- /dev/null +++ b/client/src/app/shared/select/select.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectComponent } from './select.component'; + +describe('SelectComponent', () => { + let component: SelectComponent; + let fixture: ComponentFixture<SelectComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SelectComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/shared/select/select.component.ts b/client/src/app/shared/select/select.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..81e1d4b76487d922f1a8e89a08ac637aed69db77 --- /dev/null +++ b/client/src/app/shared/select/select.component.ts @@ -0,0 +1,33 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'app-select', + templateUrl: './select.component.html', + styleUrls: ['./select.component.scss'] +}) +export class SelectComponent { + + @Input() + label: string = ""; + + @Input() + inputModel: any; + + @Input() + placeholder: string = ""; + + @Output() + inputModelChange = new EventEmitter<any>(); + + @Output() + change = new EventEmitter(); + + @Output() + focus = new EventEmitter(); + + @Output() + blur = new EventEmitter(); + + constructor() { } + +} diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..8459044413a154cba6617960c61140ff610119a9 --- /dev/null +++ b/client/src/app/shared/shared.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TextInputComponent } from './text-input/text-input.component'; +import { FormsModule } from '@angular/forms'; +import { NumberInputComponent } from './number-input/number-input.component'; +import { ButtonComponent } from './button/button.component'; +import { SelectComponent } from './select/select.component'; + + +@NgModule({ + declarations: [ + TextInputComponent, + NumberInputComponent, + ButtonComponent, + SelectComponent + ], + exports: [ + TextInputComponent, + NumberInputComponent, + ButtonComponent, + SelectComponent + ], + imports: [ + CommonModule, + FormsModule + ] +}) +export class SharedModule { } diff --git a/client/src/app/shared/text-input/text-input.component.html b/client/src/app/shared/text-input/text-input.component.html new file mode 100644 index 0000000000000000000000000000000000000000..52b28fdbbc7203ad4112161373da422493c40812 --- /dev/null +++ b/client/src/app/shared/text-input/text-input.component.html @@ -0,0 +1,12 @@ +<label> + {{label}} + + <input + type="text" + [placeholder]="placeholder" + [(ngModel)]="inputModel" + (ngModelChange)="inputModelChange.emit(inputModel)" + (change)="change.emit($event)" + (focus)="focus.emit($event)" + (blur)="blur.emit($event)"> +</label> \ No newline at end of file diff --git a/client/src/app/shared/text-input/text-input.component.scss b/client/src/app/shared/text-input/text-input.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..00cdc2a928d04283ff39dc2652060db7868d0e76 --- /dev/null +++ b/client/src/app/shared/text-input/text-input.component.scss @@ -0,0 +1,3 @@ +input{ + padding: 5px; +} \ No newline at end of file diff --git a/client/src/app/shared/text-input/text-input.component.spec.ts b/client/src/app/shared/text-input/text-input.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f895776fe5022ace055990ea6f39868f964b89ec --- /dev/null +++ b/client/src/app/shared/text-input/text-input.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TextInputComponent } from './text-input.component'; + +describe('TextInputComponent', () => { + let component: TextInputComponent; + let fixture: ComponentFixture<TextInputComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TextInputComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TextInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/shared/text-input/text-input.component.ts b/client/src/app/shared/text-input/text-input.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..91d87eda8cfd55ced747f08e947a7a4f069b46d6 --- /dev/null +++ b/client/src/app/shared/text-input/text-input.component.ts @@ -0,0 +1,33 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'app-text-input', + templateUrl: './text-input.component.html', + styleUrls: ['./text-input.component.scss'] +}) +export class TextInputComponent { + + @Input() + label: string = ""; + + @Input() + inputModel: string; + + @Input() + placeholder: string = ""; + + @Output() + inputModelChange = new EventEmitter<string>(); + + @Output() + change = new EventEmitter(); + + @Output() + focus = new EventEmitter(); + + @Output() + blur = new EventEmitter(); + + constructor() { } + +} diff --git a/client/tsconfig.json b/client/tsconfig.json index 94a24fe1e0e50466983817942286c5d1103f5092..b8d1a820f45ae5489b9f62d8a3daa1e9c27ae39a 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -5,7 +5,7 @@ "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, - "strict": true, + "strict": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "sourceMap": true,