diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 6b0938b..cfc52c6 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -3,11 +3,14 @@ import { RouterModule, Routes } from "@angular/router"; import { ProfileComponent } from "./profile/profile.component"; import { SignInComponent } from "./auth/pages/signin/signin.component"; import { SignUpComponent } from "./auth/pages/signup/signup.component"; +import { JobsComponent } from "./jobs/pages/jobs/jobs.component"; const routes: Routes = [ - { path: "", component: ProfileComponent }, + { path: "", redirectTo: "account/profile", pathMatch: "full" }, + { path: "account/profile", component: ProfileComponent }, { path: "account/signin", component: SignInComponent }, { path: "account/signup", component: SignUpComponent }, + { path: "account/jobs", component: JobsComponent }, ]; @NgModule({ diff --git a/src/app/app.component.html b/src/app/app.component.html index e62f30a..37a0d9c 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,4 +1,10 @@ - +

WAW

+ +
+ + +
+ diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7798dcd..5effc4e 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,25 +1,43 @@ import { NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { MatToolbarModule } from "@angular/material/toolbar"; -import { MatIconModule } from "@angular/material/icon"; -import { MatFormFieldModule } from "@angular/material/form-field"; -import { MatInputModule } from "@angular/material/input"; -import { MatSlideToggleModule } from "@angular/material/slide-toggle"; +import { HttpClientModule } from "@angular/common/http"; +import { FormsModule } from "@angular/forms"; import { AppRoutingModule } from "./app-routing.module"; import { AppComponent } from "./app.component"; import { ProfileComponent } from "./profile/profile.component"; import { SignInComponent } from "./auth/pages/signin/signin.component"; import { SignUpComponent } from "./auth/pages/signup/signup.component"; +import { JobsComponent } from "./jobs/pages/jobs/jobs.component"; + +import { MatToolbarModule } from "@angular/material/toolbar"; +import { MatIconModule } from "@angular/material/icon"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatSlideToggleModule } from "@angular/material/slide-toggle"; +import { MatTableModule } from "@angular/material/table"; +import { MatButtonModule } from "@angular/material/button"; +import { MatPaginatorModule } from "@angular/material/paginator"; +import { MatSortModule } from "@angular/material/sort"; +import { MatTooltipModule } from "@angular/material/tooltip"; +import { MatSelectModule } from "@angular/material/select"; export const imports: NonNullable = [ BrowserAnimationsModule, + HttpClientModule, + FormsModule, MatToolbarModule, MatIconModule, MatFormFieldModule, MatInputModule, MatSlideToggleModule, + MatTableModule, + MatButtonModule, + MatPaginatorModule, + MatSortModule, + MatTooltipModule, + MatSelectModule, ]; @NgModule({ @@ -28,6 +46,7 @@ export const imports: NonNullable = [ ProfileComponent, SignInComponent, SignUpComponent, + JobsComponent, ], imports: [BrowserModule, AppRoutingModule, ...imports], providers: [], diff --git a/src/app/common/model/column-definition.ts b/src/app/common/model/column-definition.ts new file mode 100644 index 0000000..915b97b --- /dev/null +++ b/src/app/common/model/column-definition.ts @@ -0,0 +1,42 @@ +export type InputTextDef = { + type: "text"; + minLength?: number; + maxLength?: number; +}; + +export type InputNumberDef = { + type: "number"; + min?: number; + max?: number; +}; + +export type DropdownOptions = { + label: string; + value: string | number; + default?: boolean; +}; + +export type DropdownDef = { + type: "dropdown"; + options: DropdownOptions[]; +}; + +export type ToggleDef = { + type: "toggle"; + trueLabel?: string; + falseLabel?: string; +}; + +export type ColumnStyles = { + cellClassName?: string; + containerClassName?: string; +}; + +export type ColumnDefinition = { + label: string; + key: keyof T; + required?: boolean; + hidden?: boolean; + type: string; + styles?: ColumnStyles; +} & (InputTextDef | InputNumberDef | DropdownDef | ToggleDef); diff --git a/src/app/jobs/model/job-offer.ts b/src/app/jobs/model/job-offer.ts new file mode 100644 index 0000000..bde247c --- /dev/null +++ b/src/app/jobs/model/job-offer.ts @@ -0,0 +1,7 @@ +export interface JobOffer { + id: number; + title: string; + description: string; + salaryRange: string; + published: boolean; +} diff --git a/src/app/jobs/pages/jobs/jobs.component.css b/src/app/jobs/pages/jobs/jobs.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/jobs/pages/jobs/jobs.component.html b/src/app/jobs/pages/jobs/jobs.component.html new file mode 100644 index 0000000..6c54719 --- /dev/null +++ b/src/app/jobs/pages/jobs/jobs.component.html @@ -0,0 +1,103 @@ +
+

Active Job Offers

+
+ + + {{ field.label }} + + + + {{ option.label }} + + + + + {{ field.label }} + + + + +
+ + + + + + + + + + + +
+ {{ column.label }} + +
+ {{ this.getDisplayableColumn(item, column) }} +
+
Actions + + +
+ +
diff --git a/src/app/jobs/pages/jobs/jobs.component.spec.ts b/src/app/jobs/pages/jobs/jobs.component.spec.ts new file mode 100644 index 0000000..a83e1d9 --- /dev/null +++ b/src/app/jobs/pages/jobs/jobs.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { JobsComponent } from "./jobs.component"; + +import { imports } from "src/app/app.module"; + +describe("JobsComponent", () => { + let component: JobsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...imports], + declarations: [JobsComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(JobsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/jobs/pages/jobs/jobs.component.ts b/src/app/jobs/pages/jobs/jobs.component.ts new file mode 100644 index 0000000..dc69cfc --- /dev/null +++ b/src/app/jobs/pages/jobs/jobs.component.ts @@ -0,0 +1,143 @@ +import { AfterViewInit, Component, OnInit, ViewChild } from "@angular/core"; +import { NgForm } from "@angular/forms"; +import { MatPaginator } from "@angular/material/paginator"; +import { MatSort } from "@angular/material/sort"; +import { MatTableDataSource } from "@angular/material/table"; +import { ColumnDefinition } from "src/app/common/model/column-definition"; +import { JobOffer } from "../../model/job-offer"; +import { JobsService } from "../../services/jobs.service"; + +@Component({ + selector: "app-jobs", + templateUrl: "./jobs.component.html", + styleUrls: ["./jobs.component.css"], +}) +export class JobsComponent implements OnInit, AfterViewInit { + currentItem: Partial = {}; + dataSource = new MatTableDataSource(); + columns: ColumnDefinition[] = [ + { key: "id", label: "ID", hidden: true, type: "number" }, + { + key: "title", + label: "Title", + type: "text", + required: true, + styles: { + cellClassName: "w-56", + containerClassName: "py-2 pr-4", + }, + }, + { + key: "description", + label: "Description", + type: "text", + styles: { + cellClassName: "w-96", + containerClassName: "py-2 pr-4", + }, + }, + { key: "salaryRange", label: "Salary Range", type: "text", required: true }, + { + key: "published", + label: "Published", + type: "toggle", + trueLabel: "Published", + falseLabel: "Unpublished", + }, + ]; + displayedColumns = [...this.columns.map(item => item.key), "actions"]; + + @ViewChild("jobsForm", { static: false }) + jobsForm!: NgForm; + + @ViewChild(MatPaginator, { static: true }) + paginator!: MatPaginator; + + @ViewChild(MatSort) + sort!: MatSort; + + constructor(private jobsService: JobsService) {} + + get isEditMode() { + return !!this.currentItem.id; + } + + ngOnInit() { + this.getAll(); + } + + ngAfterViewInit() { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + startEdit(item: JobOffer) { + this.currentItem = { ...item }; + } + + cancelEdit() { + this.currentItem = {}; + this.jobsForm.resetForm(); + } + + createJob(item: JobOffer) { + this.jobsService.create(item).subscribe(response => { + this.dataSource.data = [...this.dataSource.data, response]; + }); + } + + getAll() { + this.jobsService.getAll().subscribe(response => { + this.dataSource.data = response; + }); + } + + deleteJob(id: number) { + this.jobsService.delete(id).subscribe(() => { + this.dataSource.data = this.dataSource.data.filter( + current => current.id !== id + ); + }); + } + + updateJob(id: number, item: JobOffer) { + this.jobsService.update(id, item).subscribe(response => { + this.dataSource.data = this.dataSource.data.map(current => { + if (current.id === id) return response; + return current; + }); + }); + } + + handleSubmit() { + console.log("handling submit..."); + if (!this.jobsForm.form.valid) return; + console.log("passed validation..."); + const job = this.currentItem as JobOffer; + if (this.isEditMode) { + console.log("sending update..."); + this.updateJob(job.id, job); + } else { + console.log("sending create..."); + this.createJob(job); + } + this.cancelEdit(); + console.log("finished..."); + } + + getDisplayableColumn(item: JobOffer, column: ColumnDefinition) { + const value = item[column.key]; + if (column.type === "toggle") { + return (value ? column.trueLabel : column.falseLabel) || value; + } + if (column.type === "dropdown") { + const match = column.options.find(i => i.value === value); + return match?.label || value; + } + return value; + } + + useMatFormField(field: ColumnDefinition) { + return !field.hidden && ["text", "number", "dropdown"].includes(field.type); + } +} diff --git a/src/app/jobs/services/jobs.service.spec.ts b/src/app/jobs/services/jobs.service.spec.ts new file mode 100644 index 0000000..613d320 --- /dev/null +++ b/src/app/jobs/services/jobs.service.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from "@angular/core/testing"; +import { HttpClientModule } from "@angular/common/http"; + +import { JobsService } from "./jobs.service"; + +describe("JobsService", () => { + let service: JobsService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientModule], + }); + service = TestBed.inject(JobsService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/jobs/services/jobs.service.ts b/src/app/jobs/services/jobs.service.ts new file mode 100644 index 0000000..20fc96b --- /dev/null +++ b/src/app/jobs/services/jobs.service.ts @@ -0,0 +1,74 @@ +import { Injectable } from "@angular/core"; +import { + HttpClient, + HttpErrorResponse, + HttpHeaders, +} from "@angular/common/http"; +import { catchError, Observable, retry, throwError } from "rxjs"; +import { JobOffer } from "../model/job-offer"; +import { environment } from "src/environments/environment"; + +@Injectable({ + providedIn: "root", +}) +export class JobsService { + private basePath = `${environment.apiUrlBase}/jobOffers`; + + private httpOptions = { + headers: new HttpHeaders({ + "Content-Type": "application/json", + }), + }; + + constructor(private http: HttpClient) {} + + handleError(error: HttpErrorResponse) { + if (error.error instanceof ErrorEvent) { + console.error( + `An error ocurred ${error.status}, body was ${error.error}` + ); + } else { + console.error( + `Backend returned code ${error.status}, body was: ${error.error}` + ); + } + return throwError( + () => + new Error("Something happened with request, please try again later.") + ); + } + + create(item: JobOffer): Observable { + return this.http + .post(this.basePath, JSON.stringify(item), this.httpOptions) + .pipe(retry(2), catchError(this.handleError)); + } + + getAll(): Observable { + return this.http + .get(this.basePath, this.httpOptions) + .pipe(retry(2), catchError(this.handleError)); + } + + getById(id: number): Observable { + return this.http + .get(`${this.basePath}/${id}`, this.httpOptions) + .pipe(retry(2), catchError(this.handleError)); + } + + update(id: number, item: JobOffer): Observable { + return this.http + .put( + `${this.basePath}/${id}`, + JSON.stringify(item), + this.httpOptions + ) + .pipe(retry(2), catchError(this.handleError)); + } + + delete(id: number) { + return this.http + .delete(`${this.basePath}/${id}`, this.httpOptions) + .pipe(retry(2), catchError(this.handleError)); + } +} diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 52c0017..5843749 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -3,5 +3,5 @@ import { Environment } from "./model"; export const environment: Environment = { production: true, apiUrlBase: - "https://my-json-server.typicode.com/futureleadersupc/waw-backend-json/", + "https://my-json-server.typicode.com/futureleadersupc/waw-backend-json", }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index e8a37f1..0a4a5a7 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -6,7 +6,7 @@ import { Environment } from "./model"; export const environment: Environment = { production: false, - apiUrlBase: "http://localhost:4201/", + apiUrlBase: "http://localhost:4201", }; /*