- - -
- - - - - - - - - - - - {{ title }} app is running! - - - - - -
- - -

Resources

-

Here are some links to help you get started:

- - - - -

Next Steps

-

What do you want to do next with your app?

- - - -
-
- - - New Component -
- -
- - - Angular Material -
- -
- - - Add Dependency -
- -
- - - Run and Watch Tests -
- -
- - - Build for Production -
+
+
+
- - -
-
ng generate component xyz
-
ng add @angular/material
-
ng add _____
-
ng test
-
ng build --prod
-
- - - - - - - - - - - +
+
+
- - - - - - - - - - + +
+
+

�DevelopersCorner 2023 All rights reserved

+
+
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 2982797..1df37db 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,10 +1,22 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from "@angular/core"; +import { AuthService } from "./shared/services/auth.service"; @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] + selector: "app-root", + templateUrl: "./app.component.html", + styleUrls: ["./app.component.css"], }) -export class AppComponent { - title = 'developers-corner-angular'; +export class AppComponent implements OnInit { + title = "Developers Corner | Home Page"; + + constructor(private auth: AuthService) {} + ngOnInit(): void {} + + logout() { + this.auth.logout(); + } + + isLoggedIn(): boolean { + return this.auth.isLoggedIn(); + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index f657163..c3a7f5d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,16 +1,31 @@ -import { BrowserModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; +import { BrowserModule } from "@angular/platform-browser"; +import { NgModule } from "@angular/core"; -import { AppComponent } from './app.component'; +import { AppComponent } from "./app.component"; +import { UsersModule } from "./users/users.module"; +import { AppRoutingModule } from "./app-routing.module"; +import { HTTP_INTERCEPTORS } from "@angular/common/http"; +import { AuthInterceptor } from "./shared/http-interceptors/auth-interceptor"; +import { HomeModule } from "./home/home.module"; +import { QuestionsModule } from "./questions/questions.module"; +import { QuestionsService } from "./shared/services/questions.service"; +import { NotificationComponent } from "./notification/notification.component"; +import { NotificationService } from "./shared/services/notification.service"; @NgModule({ - declarations: [ - AppComponent - ], + declarations: [AppComponent, NotificationComponent], imports: [ - BrowserModule + BrowserModule, + UsersModule, + HomeModule, + QuestionsModule, + AppRoutingModule, + ], + providers: [ + { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, + QuestionsService, + NotificationService, ], - providers: [], - bootstrap: [AppComponent] + bootstrap: [AppComponent], }) -export class AppModule { } +export class AppModule {} diff --git a/src/app/home/home-routing.module.ts b/src/app/home/home-routing.module.ts new file mode 100644 index 0000000..27055fc --- /dev/null +++ b/src/app/home/home-routing.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; +import { AppRoutingTitles } from "../app-routing.titles"; +import { AuthGuard } from "../shared/guards/auth.guard"; +import { HomeComponent } from "./home/home.component"; + +const routes: Routes = [ + { + path: "", + component: HomeComponent, + canActivate: [AuthGuard], + data: { + title: AppRoutingTitles.QUESTIONS, + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class HomeRoutingModule {} diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts new file mode 100644 index 0000000..c2b683c --- /dev/null +++ b/src/app/home/home.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from "@angular/core"; + +import { SharedModule } from "../shared/shared.module"; +import { HomeRoutingModule } from "./home-routing.module"; +import { HomeComponent } from "./home/home.component"; +import { QuestionFormComponent } from "./question-form/question-form.component"; + +@NgModule({ + declarations: [HomeComponent, QuestionFormComponent], + imports: [SharedModule, HomeRoutingModule], + providers: [], +}) +export class HomeModule {} diff --git a/src/app/home/home/home.component.css b/src/app/home/home/home.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/home/home/home.component.html b/src/app/home/home/home.component.html new file mode 100644 index 0000000..134f9eb --- /dev/null +++ b/src/app/home/home/home.component.html @@ -0,0 +1,104 @@ +
+

Welcome to Developers Corner

+

This is the banner area

+
+
+ +
+

Check In

+ + +
+
+ +

Possible Solutions

+
+

{{ answer.title }}

+ {{ answer.title }} +

Rating: {{ answer.view_count }}

+
+
+
+
+ +
+ + diff --git a/src/app/home/home/home.component.spec.ts b/src/app/home/home/home.component.spec.ts new file mode 100644 index 0000000..f7a7491 --- /dev/null +++ b/src/app/home/home/home.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; + +import { HomeComponent } from "./home.component"; + +describe("HomeComponent", () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [HomeComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/home/home/home.component.ts b/src/app/home/home/home.component.ts new file mode 100644 index 0000000..acb3ae6 --- /dev/null +++ b/src/app/home/home/home.component.ts @@ -0,0 +1,33 @@ +import { Component, OnInit } from "@angular/core"; +import { MDNAnswers } from "src/app/shared/models/questions.model"; +import { QuestionsService } from "src/app/shared/services/questions.service"; + +@Component({ + selector: "app-home", + templateUrl: "./home.component.html", + styleUrls: ["./home.component.css"], +}) +export class HomeComponent implements OnInit { + displayAnswers: boolean = false; + answers: MDNAnswers[] = []; + + constructor(private questionService: QuestionsService) {} + + ngOnInit() {} + + onSubmit(value: { displayAnswers: boolean; tags: string; question: string }) { + this.displayAnswers = value.displayAnswers; + + this.fetchAnswersFromMdnAndStackoverflow(value.tags, value.question); + } + + newQuestion() { + this.displayAnswers = false; + } + + fetchAnswersFromMdnAndStackoverflow(tags: string, question: string) { + this.questionService + .fetchResourceFromStackOverFlow(tags, question) + .subscribe((data) => (this.answers = data.items)); + } +} diff --git a/src/app/home/question-form/question-form.component.css b/src/app/home/question-form/question-form.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/home/question-form/question-form.component.html b/src/app/home/question-form/question-form.component.html new file mode 100644 index 0000000..5e0c7d1 --- /dev/null +++ b/src/app/home/question-form/question-form.component.html @@ -0,0 +1,109 @@ + +
+
+ User Name + +
+ +
+
Username is required.
+
+ Username must be at least 3 characters long. +
+
+ +
+ + + + +
+ +
+
Role is required.
+
+ +
+ Tags + +
+ +
+
Tags is required.
+
+ +
+ +
+ +
+
Question is required.
+
+ +
+ +
+
diff --git a/src/app/home/question-form/question-form.component.spec.ts b/src/app/home/question-form/question-form.component.spec.ts new file mode 100644 index 0000000..c3849a4 --- /dev/null +++ b/src/app/home/question-form/question-form.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; + +import { QuestionFormComponent } from "./question-form.component"; + +describe("QuestionFormComponent", () => { + let component: QuestionFormComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [QuestionFormComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(QuestionFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/home/question-form/question-form.component.ts b/src/app/home/question-form/question-form.component.ts new file mode 100644 index 0000000..c81b714 --- /dev/null +++ b/src/app/home/question-form/question-form.component.ts @@ -0,0 +1,72 @@ +import { Component, OnInit, Output, EventEmitter } from "@angular/core"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { displayAnswersEvent } from "src/app/shared/models/questions.model"; + +import { AuthService } from "src/app/shared/services/auth.service"; +import { QuestionsService } from "src/app/shared/services/questions.service"; + +@Component({ + selector: "app-question-form", + templateUrl: "./question-form.component.html", + styleUrls: ["./question-form.component.css"], +}) +export class QuestionFormComponent implements OnInit { + questionForm!: FormGroup; + displayAnswers: boolean = false; + currentUserId: number = 0; + @Output() displayAnswersEvent = new EventEmitter(); + + constructor( + private fb: FormBuilder, + private auth: AuthService, + private questionService: QuestionsService + ) {} + + ngOnInit() { + this.questionForm = this.fb.group({ + username: ["", [Validators.required, Validators.minLength(3)]], + role: ["", [Validators.required]], + tags: ["", Validators.required], + question: ["", Validators.required], + userId: [], + }); + + this.auth + .getCurrentUser() + .subscribe((data) => (this.currentUserId = data.id)); + } + + get username() { + return this.questionForm.get("username"); + } + get role() { + return this.questionForm.get("role"); + } + get tags() { + return this.questionForm.get("tags"); + } + get question() { + return this.questionForm.get("question"); + } + + dataEvent() { + this.displayAnswers = true; + + const event = { + displayAnswers: this.displayAnswers, + tags: this.tags.value, + question: this.question.value, + }; + this.displayAnswersEvent.emit(event); + } + + onSubmit() { + this.questionForm.patchValue({ userId: this.currentUserId }); + + this.questionService + .create(this.questionForm.value) + .subscribe((data) => console.log("saved question", data)); + + this.dataEvent(); + } +} diff --git a/src/app/notification/notification.component.css b/src/app/notification/notification.component.css new file mode 100644 index 0000000..418debb --- /dev/null +++ b/src/app/notification/notification.component.css @@ -0,0 +1,52 @@ +.notifications { + position: fixed; + bottom: 0; + right: 0; + z-index: 1000; + min-width: 400px; + max-width: 400px; +} + +.notification { + margin: 5px; + border-radius: 5px; + color: #fff; + overflow: hidden; +} + +.title { + background: rgba(0, 0, 0, 0.6); + padding-left: 10px; + font-weight: bold; +} + +.notif-close { + border: none; + background: transparent; + color: red; + float: right; + outline: none; +} + +.message { + background: rgba(0, 0, 0, 0.4); + padding: 10px; + max-height: 200px; + overflow-y: auto; +} + +.info { + background-color: rgba(189, 189, 189, 0.9); +} + +.success { + background-color: rgba(27, 158, 119, 0.9); +} + +.warning { + background-color: rgba(217, 95, 2, 0.9); +} + +.error { + background-color: rgba(246, 71, 71, 0.9); +} diff --git a/src/app/notification/notification.component.html b/src/app/notification/notification.component.html new file mode 100644 index 0000000..c37b931 --- /dev/null +++ b/src/app/notification/notification.component.html @@ -0,0 +1,23 @@ +
+
+ +
+
+ + +
+
{{ notification.title }}
+ +
+ +
{{ notification.message }}
+
diff --git a/src/app/notification/notification.component.spec.ts b/src/app/notification/notification.component.spec.ts new file mode 100644 index 0000000..69192c5 --- /dev/null +++ b/src/app/notification/notification.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ NotificationComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NotificationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/notification/notification.component.ts b/src/app/notification/notification.component.ts new file mode 100644 index 0000000..5f0969b --- /dev/null +++ b/src/app/notification/notification.component.ts @@ -0,0 +1,63 @@ +import { Component, OnInit } from "@angular/core"; +import { Subscription } from "rxjs"; + +import { + Notification, + NotificationType, +} from "../shared/models/notification.model"; +import { NotificationService } from "../shared/services/notification.service"; + +@Component({ + selector: "app-notification", + templateUrl: "./notification.component.html", + styleUrls: ["./notification.component.css"], +}) +export class NotificationComponent implements OnInit { + notifications: Notification[] = []; + private _subscription: Subscription; + + constructor(private _notificationSvc: NotificationService) {} + + private _addNotification(notification: Notification) { + this.notifications.push(notification); + + if (notification.timeout !== 0) + setTimeout(() => this.close(notification), notification.timeout); + } + + ngOnInit() { + this._subscription = this._notificationSvc + .getObservable() + .subscribe((notification) => this._addNotification(notification)); + } + + ngOnDestroy() { + this._subscription.unsubscribe(); + } + + close(notification: Notification) { + this.notifications = this.notifications.filter( + (notif) => notif.id !== notification.id + ); + } + + className(notification: Notification): string { + let style: string; + + switch (notification.type) { + case NotificationType.success: + style = "success"; + break; + case NotificationType.warning: + style = "warning"; + break; + case NotificationType.error: + style = "error"; + break; + default: + style = "info"; + break; + } + return style; + } +} diff --git a/src/app/questions/question/question.component.css b/src/app/questions/question/question.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/questions/question/question.component.html b/src/app/questions/question/question.component.html new file mode 100644 index 0000000..c6fe203 --- /dev/null +++ b/src/app/questions/question/question.component.html @@ -0,0 +1,179 @@ +
+ + + + + + + + + + + + + + + + + + + + + +

You don't have any questions yet.

+ +
UsernameTagsRoleQuestionUserId
{{ question.username }}{{ question.tags }}{{ question.role }}{{ question.question }}{{ question.user.id }} + + +
+
+ + + + + + + diff --git a/src/app/questions/question/question.component.spec.ts b/src/app/questions/question/question.component.spec.ts new file mode 100644 index 0000000..0a695f5 --- /dev/null +++ b/src/app/questions/question/question.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { QuestionComponent } from './question.component'; + +describe('QuestionComponent', () => { + let component: QuestionComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ QuestionComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(QuestionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/questions/question/question.component.ts b/src/app/questions/question/question.component.ts new file mode 100644 index 0000000..1880a84 --- /dev/null +++ b/src/app/questions/question/question.component.ts @@ -0,0 +1,43 @@ +import { Component, OnInit, Input } from "@angular/core"; +import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; +import { Question } from "src/app/shared/models/questions.model"; +import { QuestionsService } from "src/app/shared/services/questions.service"; + +@Component({ + selector: "app-question", + templateUrl: "./question.component.html", + styleUrls: ["./question.component.css"], +}) +export class QuestionComponent implements OnInit { + @Input() questions: Question[] = []; + + updatedQuestion: Question; + + constructor( + private modalService: NgbModal, + private questionService: QuestionsService + ) {} + + ngOnInit() {} + + open(content, question) { + this.updatedQuestion = question; + this.updatedQuestion.userId = question.user.id; + this.updatedQuestion.role = question.user.type; + this.modalService + .open(content, { ariaLabelledBy: "Question Update" }) + .result.then((result) => console.log(result)); + } + + onSubmit() { + console.log("submited", this.updatedQuestion); + this.questionService + .update(this.updatedQuestion) + .subscribe((data) => console.log(data)); + } + + onRemove(id: number) { + this.questions = this.questions.filter((q) => q.id != id); + this.questionService.delete(id).subscribe((data) => console.log(data)); + } +} diff --git a/src/app/questions/questions-routing.module.ts b/src/app/questions/questions-routing.module.ts new file mode 100644 index 0000000..2587042 --- /dev/null +++ b/src/app/questions/questions-routing.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; +import { AppRoutingTitles } from "../app-routing.titles"; +import { AuthGuard } from "../shared/guards/auth.guard"; +import { QuestionsComponent } from "./questions/questions.component"; + +const routes: Routes = [ + { + path: "", + component: QuestionsComponent, + canActivate: [AuthGuard], + data: { + title: AppRoutingTitles.QUESTIONS, + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class QuestionsRoutingModule {} diff --git a/src/app/questions/questions.module.ts b/src/app/questions/questions.module.ts new file mode 100644 index 0000000..f885ae6 --- /dev/null +++ b/src/app/questions/questions.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from "@angular/core"; + +import { SharedModule } from "../shared/shared.module"; +import { QuestionsRoutingModule } from "./questions-routing.module"; +import { QuestionsComponent } from "./questions/questions.component"; +import { QuestionComponent } from "./question/question.component"; + +@NgModule({ + declarations: [QuestionsComponent, QuestionComponent], + imports: [SharedModule, QuestionsRoutingModule], + providers: [], +}) +export class QuestionsModule {} diff --git a/src/app/questions/questions/questions.component.css b/src/app/questions/questions/questions.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/questions/questions/questions.component.html b/src/app/questions/questions/questions.component.html new file mode 100644 index 0000000..c4f609c --- /dev/null +++ b/src/app/questions/questions/questions.component.html @@ -0,0 +1,2 @@ +

Your Questions

+ diff --git a/src/app/questions/questions/questions.component.spec.ts b/src/app/questions/questions/questions.component.spec.ts new file mode 100644 index 0000000..840a091 --- /dev/null +++ b/src/app/questions/questions/questions.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { QuestionsComponent } from './questions.component'; + +describe('QuestionsComponent', () => { + let component: QuestionsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ QuestionsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(QuestionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/questions/questions/questions.component.ts b/src/app/questions/questions/questions.component.ts new file mode 100644 index 0000000..139c272 --- /dev/null +++ b/src/app/questions/questions/questions.component.ts @@ -0,0 +1,26 @@ +import { Component, OnInit } from "@angular/core"; +import { Question } from "src/app/shared/models/questions.model"; +import { AuthService } from "src/app/shared/services/auth.service"; +import { QuestionsService } from "src/app/shared/services/questions.service"; + +@Component({ + selector: "app-questions", + templateUrl: "./questions.component.html", + styleUrls: ["./questions.component.css"], +}) +export class QuestionsComponent implements OnInit { + questions: Question[] = []; + + constructor( + private questionsService: QuestionsService, + private auth: AuthService + ) {} + + ngOnInit() { + this.auth.getCurrentUser().subscribe((data) => { + this.questionsService.getQuestionByUserId(data.id).subscribe((data) => { + this.questions = data; + }); + }); + } +} diff --git a/src/app/shared/components/global-modal/global-modal-options.ts b/src/app/shared/components/global-modal/global-modal-options.ts new file mode 100644 index 0000000..c43581e --- /dev/null +++ b/src/app/shared/components/global-modal/global-modal-options.ts @@ -0,0 +1,4 @@ +export type GlobalModalOptions = { + title: string; + body: any; +}; diff --git a/src/app/shared/components/global-modal/global-modal.component.css b/src/app/shared/components/global-modal/global-modal.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/components/global-modal/global-modal.component.html b/src/app/shared/components/global-modal/global-modal.component.html new file mode 100644 index 0000000..71662e5 --- /dev/null +++ b/src/app/shared/components/global-modal/global-modal.component.html @@ -0,0 +1,48 @@ + + + + + + + + +
+ +
{{ closeResult }}
diff --git a/src/app/shared/components/global-modal/global-modal.component.spec.ts b/src/app/shared/components/global-modal/global-modal.component.spec.ts new file mode 100644 index 0000000..02f6fc1 --- /dev/null +++ b/src/app/shared/components/global-modal/global-modal.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GlobalModalComponent } from './global-modal.component'; + +describe('GlobalModalComponent', () => { + let component: GlobalModalComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ GlobalModalComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GlobalModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/global-modal/global-modal.component.ts b/src/app/shared/components/global-modal/global-modal.component.ts new file mode 100644 index 0000000..e178553 --- /dev/null +++ b/src/app/shared/components/global-modal/global-modal.component.ts @@ -0,0 +1,41 @@ +import { Component } from "@angular/core"; +import { ModalDismissReasons, NgbModal } from "@ng-bootstrap/ng-bootstrap"; + +// import { GlobalModalOptions } from "./global-modal-options"; +// import { GlobalModalState } from "./global-modal.state"; + +@Component({ + selector: "app-global-modal", + templateUrl: "./global-modal.component.html", +}) +export class GlobalModalComponent { + closeResult = ""; + // @Input() title: string = ""; + // @Input() onOpenModal: (content: any) => void; + // options!: GlobalModalOptions; + + constructor(private modalService: NgbModal) {} + + open(content) { + this.modalService + .open(content, { ariaLabelledBy: "modal-basic-title" }) + .result.then( + (result) => { + this.closeResult = `Closed with: ${result}`; + }, + (reason) => { + this.closeResult = `Dismissed ${this.getDismissReason(reason)}`; + } + ); + } + + private getDismissReason(reason: any): string { + if (reason === ModalDismissReasons.ESC) { + return "by pressing ESC"; + } else if (reason === ModalDismissReasons.BACKDROP_CLICK) { + return "by clicking on a backdrop"; + } else { + return `with: ${reason}`; + } + } +} diff --git a/src/app/shared/components/global-modal/global-modal.directive.ts b/src/app/shared/components/global-modal/global-modal.directive.ts new file mode 100644 index 0000000..06df9f1 --- /dev/null +++ b/src/app/shared/components/global-modal/global-modal.directive.ts @@ -0,0 +1,11 @@ +import { Directive, TemplateRef } from "@angular/core"; +import { GlobalModalState } from "./global-modal.state"; + +@Directive({ + selector: "ng-template[appGlobalModal]", +}) +export class GlobalModalDirective { + constructor(modalTemplate: TemplateRef, state: GlobalModalState) { + state.template = modalTemplate; + } +} diff --git a/src/app/shared/components/global-modal/global-modal.service.ts b/src/app/shared/components/global-modal/global-modal.service.ts new file mode 100644 index 0000000..95168ea --- /dev/null +++ b/src/app/shared/components/global-modal/global-modal.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from "@angular/core"; +import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; +import { GlobalModalState } from "./global-modal.state"; +import { GlobalModalOptions } from "./global-modal-options"; +import { SharedModule } from "../../shared.module"; + +@Injectable({ + providedIn: SharedModule, +}) +export class GlobalModalService { + constructor( + private modalService: NgbModal, + private state: GlobalModalState + ) {} + + public show(options: GlobalModalOptions): void { + this.state.options = options; + this.state.modal = this.modalService.open(this.state.template, { + centered: true, + size: "lg", + }); + } +} diff --git a/src/app/shared/components/global-modal/global-modal.state.ts b/src/app/shared/components/global-modal/global-modal.state.ts new file mode 100644 index 0000000..3b55c0f --- /dev/null +++ b/src/app/shared/components/global-modal/global-modal.state.ts @@ -0,0 +1,10 @@ +import { Injectable, TemplateRef } from "@angular/core"; +import { NgbModalRef } from "@ng-bootstrap/ng-bootstrap"; +import { GlobalModalOptions } from "./global-modal-options"; + +@Injectable() +export class GlobalModalState { + options?: GlobalModalOptions; + modal?: NgbModalRef; + template?: TemplateRef; +} diff --git a/src/app/shared/guards/auth.guard.ts b/src/app/shared/guards/auth.guard.ts new file mode 100644 index 0000000..21eed20 --- /dev/null +++ b/src/app/shared/guards/auth.guard.ts @@ -0,0 +1,45 @@ +import { Route } from "@angular/compiler/src/core"; +import { Injectable } from "@angular/core"; +import { + CanActivate, + Router, + ActivatedRouteSnapshot, + RouterStateSnapshot, + UrlTree, + UrlSegment, +} from "@angular/router"; +import { Observable } from "rxjs"; + +import { AuthService } from "../services/auth.service"; + +@Injectable({ + providedIn: "root", +}) +export class AuthGuard implements CanActivate { + constructor(private auth: AuthService, private router: Router) {} + + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): + | Observable + | Promise + | boolean + | UrlTree { + return this.isLoggedIn(state.url); + } + + canLoad(route: Route, segments: UrlSegment[]): boolean { + return this.isLoggedIn(segments.join("/")); + } + + isLoggedIn(url: string): boolean { + if (this.auth.isLoggedIn()) return true; + + this.auth.redirectUrl = url; + this.router.navigate(["login"], { + queryParams: { returnUrl: url }, + }); + return false; + } +} diff --git a/src/app/shared/http-interceptors/auth-interceptor.ts b/src/app/shared/http-interceptors/auth-interceptor.ts new file mode 100644 index 0000000..fd271df --- /dev/null +++ b/src/app/shared/http-interceptors/auth-interceptor.ts @@ -0,0 +1,35 @@ +import { Injectable } from "@angular/core"; + +import { + HttpEvent, + HttpHandler, + HttpInterceptor, + HttpRequest, +} from "@angular/common/http"; +import { Observable } from "rxjs"; + +import { AuthService } from "../services/auth.service"; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + constructor(private auth: AuthService) {} + + intercept( + req: HttpRequest, + next: HttpHandler + ): Observable> { + const token = this.auth.getToken(); + + if (token) { + console.log( + "auth interceptor got called and added token================", + token + ); + const authReq = req.clone({ + setHeaders: { Authorization: "Bearer " + token }, + }); + + return next.handle(authReq); + } else return next.handle(req); + } +} diff --git a/src/app/shared/http-interceptors/content-type-interceptor.ts b/src/app/shared/http-interceptors/content-type-interceptor.ts new file mode 100644 index 0000000..b430d29 --- /dev/null +++ b/src/app/shared/http-interceptors/content-type-interceptor.ts @@ -0,0 +1 @@ +export class ContentTypeInterceptor {} diff --git a/src/app/shared/models/notification.model.ts b/src/app/shared/models/notification.model.ts new file mode 100644 index 0000000..1b83e0d --- /dev/null +++ b/src/app/shared/models/notification.model.ts @@ -0,0 +1,16 @@ +export class Notification { + constructor( + public id: number, + public type: NotificationType, + public title: string, + public message: string, + public timeout: number + ) {} +} + +export enum NotificationType { + success = 0, + warning = 1, + error = 2, + info = 3, +} diff --git a/src/app/shared/models/questions.model.ts b/src/app/shared/models/questions.model.ts new file mode 100644 index 0000000..3b2197e --- /dev/null +++ b/src/app/shared/models/questions.model.ts @@ -0,0 +1,22 @@ +export interface Question { + id?: number; + username: string; + role: string; + tags: string; + question: string; + userId?: number; +} + +export type displayAnswersEvent = { + displayAnswers: boolean; + tags: string; + question: string; +}; + +export type MDNAnswers = { + is_answered: boolean; + link: string; + tags: [string]; + title: string; + view_count: number; +}; diff --git a/src/app/shared/models/users.model.ts b/src/app/shared/models/users.model.ts new file mode 100644 index 0000000..8443830 --- /dev/null +++ b/src/app/shared/models/users.model.ts @@ -0,0 +1,34 @@ +export interface User { + id: number; + firstName: string; + lastName: string; + nickName?: string; + type: string; + email: string; + password: string; + createdAt: Date; + enabled: boolean; + username: string; + credentialsNonExpired: boolean; + accountNonExpired: boolean; + accountNonLocked: boolean; +} + +export interface RegisterUser { + id?: number; + firstName: string; + lastName: string; + nickName?: string; + type: string; + email: string; + password: string; +} + +export interface LoginUser { + email: string; + password: string; +} + +export interface AuthResponse { + token: string; +} diff --git a/src/app/shared/services/auth.service.ts b/src/app/shared/services/auth.service.ts new file mode 100644 index 0000000..5e6d8b0 --- /dev/null +++ b/src/app/shared/services/auth.service.ts @@ -0,0 +1,113 @@ +import { Injectable, OnDestroy } from "@angular/core"; +import { HttpClient, HttpErrorResponse } from "@angular/common/http"; +import { Router } from "@angular/router"; +import jwt_decode from "jwt-decode"; + +import { Observable, throwError } from "rxjs"; +import { catchError, tap } from "rxjs/operators"; + +import { environment } from "src/environments/environment"; +import { + AuthResponse, + LoginUser, + RegisterUser, + User, +} from "../models/users.model"; +import { NotificationService } from "./notification.service"; + +@Injectable() +export class AuthService implements OnDestroy { + redirectUrl: string; + + constructor( + private http: HttpClient, + private router: Router, + private notificationService: NotificationService + ) { + console.log("AuthService instance created."); + } + ngOnDestroy(): void { + console.log("AuthService instance destroyed."); + } + + register(user: RegisterUser): Observable { + return this.http + .post(environment.baseUrl + "/auth/register", user) + .pipe( + tap(({ token }) => { + this.setToken(token); + this.notificationService.success( + "Register User", + "Successfully registered." + ); + }), + catchError(this.handleError) + ); + } + + login(user: LoginUser): Observable { + return this.http + .post(environment.baseUrl + "/auth/login", user) + .pipe( + tap(({ token }) => { + this.setToken(token); + this.notificationService.success( + "Login User", + "Successfully logged in." + ); + }), + catchError(this.handleError) + ); + } + + logout() { + localStorage.removeItem("token"); + this.router.navigate(["/login"]); + this.notificationService.success("Logout", "Successfully logged out."); + } + + getToken(): string { + return localStorage.getItem("token"); + } + + setToken(token: string) { + localStorage.setItem("token", token); + } + + getCurrentUser(): Observable { + const email = this.decodeToken(this.getToken()).sub; + return this.http + .get(environment.baseUrl + `/users/email/${email}`) + .pipe( + tap((data) => + this.notificationService.info( + "Getting current user", + "Successfully retreived current user." + ) + ), + catchError(this.handleError) + ); + } + + isLoggedIn(): boolean { + return !!this.getToken(); + } + + decodeToken(token: string): { sub: string; iat: number; exp: number } { + return token ? jwt_decode(token) : null; + } + + handleError(error: HttpErrorResponse) { + this.notificationService.error( + "Something went wrong " + error.status, + error.message + ); + + return throwError( + () => + new Error( + `Something bad happend; please try again later. ${error.message}` + ) + ); + } +} diff --git a/src/app/shared/services/notification.service.ts b/src/app/shared/services/notification.service.ts new file mode 100644 index 0000000..af9445d --- /dev/null +++ b/src/app/shared/services/notification.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from "@angular/core"; +import { Observable, Subject } from "rxjs"; +import { Notification, NotificationType } from "../models/notification.model"; + +@Injectable() +export class NotificationService { + private _subject = new Subject(); + private _idx = 0; + + constructor() {} + + getObservable(): Observable { + return this._subject.asObservable(); + } + + info(title: string, message: string, timeout = 5000) { + this._subject.next( + new Notification( + this._idx++, + NotificationType.info, + title, + message, + timeout + ) + ); + } + + success(title: string, message: string, timeout = 5000) { + return this._subject.next( + new Notification( + this._idx++, + NotificationType.success, + title, + message, + timeout + ) + ); + } + + warning(title: string, message: string, timeout = 5000) { + return this._subject.next( + new Notification( + this._idx++, + NotificationType.warning, + title, + message, + timeout + ) + ); + } + + error(title: string, message: string, timeout = 0) { + this._subject.next( + new Notification( + this._idx++, + NotificationType.error, + title, + message, + timeout + ) + ); + } +} diff --git a/src/app/shared/services/questions.service.ts b/src/app/shared/services/questions.service.ts new file mode 100644 index 0000000..61d9ab1 --- /dev/null +++ b/src/app/shared/services/questions.service.ts @@ -0,0 +1,143 @@ +import { Injectable, OnDestroy } from "@angular/core"; +import { HttpClient, HttpErrorResponse } from "@angular/common/http"; + +import { Observable, throwError } from "rxjs"; +import { catchError, tap } from "rxjs/operators"; + +import { environment } from "src/environments/environment"; +import { Question } from "../models/questions.model"; +import { NotificationService } from "./notification.service"; + +@Injectable() +export class QuestionsService implements OnDestroy { + stackExchangeQuery: string = + "https://api.stackexchange.com/2.3/search?order=desc&sort=activity&site=stackoverflow&tagged="; + + constructor( + private http: HttpClient, + private notificationSvc: NotificationService + ) { + console.log("QuestionsService instance created."); + } + ngOnDestroy(): void { + console.log("QuestionsService instance destroyed."); + } + + create(question: Question): Observable { + return this.http + .post(environment.baseUrl + `/user/questions`, question) + .pipe( + tap((data) => + this.notificationSvc.success( + "Create question", + "Successfully created." + ) + ), + catchError(this.handleError) + ); + } + + getQuestionById(id: number): Observable { + return this.http + .get(environment.baseUrl + `/user/questions/${id}`) + .pipe( + tap(() => + this.notificationSvc.success( + "Questions", + "Successfully retreived a question by id." + ) + ), + catchError(this.handleError) + ); + } + + getQuestionByUserId(id: number): Observable { + return this.http + .get( + environment.baseUrl + `/user/questions/question?userId=${id}` + ) + .pipe( + tap(() => + this.notificationSvc.success( + "Questions", + "Successfully retreived users questions" + ) + ), + catchError(this.handleError) + ); + } + + getQuestionByUsername(username: string): Observable { + return this.http + .get( + environment.baseUrl + `/user/questions/question/${username}` + ) + .pipe(catchError(this.handleError)); + } + + getAllQuestions(): Observable { + return this.http.get(environment.baseUrl + `/user/questions`).pipe( + tap((data) => + this.notificationSvc.success( + "Questions", + "Successfully retreived all user questions" + ) + ), + catchError(this.handleError) + ); + } + + update(question: Question): Observable { + return this.http + .put( + environment.baseUrl + `/user/questions/${question.id}`, + question + ) + .pipe( + tap(() => + this.notificationSvc.success( + "Questions", + "Successfully updated a question." + ) + ), + catchError(this.handleError) + ); + } + + delete(id: number): Observable { + return this.http.delete(environment.baseUrl + `/user/questions/${id}`).pipe( + tap(() => + this.notificationSvc.success( + "Questions", + "Successfully deleted a question." + ) + ), + catchError(this.handleError) + ); + } + + // ************* Fetch data from SOVF and MDN ************** + fetchResourceFromStackOverFlow( + tags: string, + question: string + ): Observable { + this.stackExchangeQuery += tags + "&intitle=" + question; + return this.http.get(this.stackExchangeQuery).pipe( + tap(() => + this.notificationSvc.success( + "Questions", + "Successfully retreived Stackoverflow answers." + ) + ), + catchError(this.handleError) + ); + } + + private handleError({ error }: HttpErrorResponse) { + this.notificationSvc.error("Questions " + error.httpStatus, error.message); + return throwError( + () => + new Error(`Something bad happened; please try again later. ${error}`) + ); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts new file mode 100644 index 0000000..3058633 --- /dev/null +++ b/src/app/shared/shared.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { HttpClientModule } from "@angular/common/http"; +import { RouterModule } from "@angular/router"; +import { NgbModule } from "@ng-bootstrap/ng-bootstrap"; + +@NgModule({ + declarations: [], + imports: [CommonModule], + exports: [ + CommonModule, + ReactiveFormsModule, + FormsModule, + RouterModule, + HttpClientModule, + NgbModule, + ], + providers: [], +}) +export class SharedModule {} diff --git a/src/app/users/login/login.component.css b/src/app/users/login/login.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/users/login/login.component.html b/src/app/users/login/login.component.html new file mode 100644 index 0000000..2287afb --- /dev/null +++ b/src/app/users/login/login.component.html @@ -0,0 +1,72 @@ +
+
+
+

User Registration Form

+
+
+
+
+ + +
+ +
+
Email is required.
+
+ +
+ + +
+ +
+
Password is required.
+
+ Password must be at least 6 characters long. +
+
+ Password must be Minimum 6 characters, at least one uppercase + letter, one lowercase letter, one number and one special character. +
+
+ + +
+ + +
+
+
+
+
diff --git a/src/app/users/login/login.component.spec.ts b/src/app/users/login/login.component.spec.ts new file mode 100644 index 0000000..d6d85a8 --- /dev/null +++ b/src/app/users/login/login.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoginComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/users/login/login.component.ts b/src/app/users/login/login.component.ts new file mode 100644 index 0000000..9a5c932 --- /dev/null +++ b/src/app/users/login/login.component.ts @@ -0,0 +1,47 @@ +import { Component, OnInit } from "@angular/core"; +import { FormGroup, FormBuilder, Validators } from "@angular/forms"; +import { ActivatedRoute, Router } from "@angular/router"; + +import { AuthService } from "src/app/shared/services/auth.service"; + +@Component({ + selector: "app-login", + templateUrl: "./login.component.html", + styleUrls: ["./login.component.css"], +}) +export class LoginComponent implements OnInit { + loginForm!: FormGroup; + returnUrl: string; + constructor( + private fb: FormBuilder, + private auth: AuthService, + private route: ActivatedRoute, + private router: Router + ) {} + + ngOnInit() { + this.loginForm = this.fb.group({ + email: ["", [Validators.required, Validators.email]], + password: ["", [Validators.required, Validators.minLength(6)]], + }); + + // get return url from route parameters or default to '/' + this.returnUrl = this.route.snapshot.queryParams["returnUrl"] || "/"; + console.log(this.returnUrl); + } + + get email() { + return this.loginForm.get("email"); + } + get password() { + return this.loginForm.get("password"); + } + + onSubmit() { + if (this.loginForm.valid) { + this.auth + .login(this.loginForm.value) + .subscribe((data) => this.router.navigateByUrl(this.returnUrl)); + } + } +} diff --git a/src/app/users/profile/profile.component.css b/src/app/users/profile/profile.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/users/profile/profile.component.html b/src/app/users/profile/profile.component.html new file mode 100644 index 0000000..5bd9e2d --- /dev/null +++ b/src/app/users/profile/profile.component.html @@ -0,0 +1,213 @@ +
+

{{ user?.lastName }}'s Profile

+ + + + + + + + + + + + + + + + + + +
First NameLast NameEmailCreatedAt
{{ user?.firstName }}{{ user?.lastName }}{{ user?.email }} + {{ user?.createdAt | date : "medium" }} + + + +
+
+ + + + + + diff --git a/src/app/users/profile/profile.component.spec.ts b/src/app/users/profile/profile.component.spec.ts new file mode 100644 index 0000000..692b234 --- /dev/null +++ b/src/app/users/profile/profile.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ProfileComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ProfileComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/users/profile/profile.component.ts b/src/app/users/profile/profile.component.ts new file mode 100644 index 0000000..d3b8343 --- /dev/null +++ b/src/app/users/profile/profile.component.ts @@ -0,0 +1,43 @@ +import { Component, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; +import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; +import { User } from "src/app/shared/models/users.model"; +import { AuthService } from "src/app/shared/services/auth.service"; +import { UsersService } from "../users.service"; + +@Component({ + selector: "app-profile", + templateUrl: "./profile.component.html", + styleUrls: ["./profile.component.css"], +}) +export class ProfileComponent implements OnInit { + user: User; + + constructor( + private userService: UsersService, + private auth: AuthService, + private modalService: NgbModal, + private router: Router + ) {} + + ngOnInit() { + this.auth.getCurrentUser().subscribe((data) => (this.user = data)); + } + + open(user: User) { + this.user.password = ""; + this.modalService + .open(user, { ariaLabelledBy: "modal-basic-title" }) + .result.then((result) => console.log(result)); + } + + onSubmit() { + this.userService.update(this.user).subscribe((data) => console.log(data)); + } + + onRemove(id: number) { + this.userService.delete(id).subscribe((data) => console.log(data)); + this.auth.logout(); + this.router.navigate(["register"]); + } +} diff --git a/src/app/users/register/register.component.css b/src/app/users/register/register.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/users/register/register.component.html b/src/app/users/register/register.component.html new file mode 100644 index 0000000..6eae043 --- /dev/null +++ b/src/app/users/register/register.component.html @@ -0,0 +1,151 @@ +
+
+
+

User Registration Form

+
+
+
+
+ + +
+ +
+
First Name is required.
+
+ First Name must be at least 3 characters long. +
+
+ +
+ + +
+ +
+
Last Name is required.
+
+ Last Name must be at least 3 characters long. +
+
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+
Email is required.
+
+ +
+ + +
+ +
+
Password is required.
+
+ Password must be at least 6 characters long. +
+
+ Password must be Minimum 6 characters, at least one uppercase + letter, one lowercase letter, one number and one special character. +
+
+ + +
+ + +
+
+
+
+
diff --git a/src/app/users/register/register.component.spec.ts b/src/app/users/register/register.component.spec.ts new file mode 100644 index 0000000..6c19551 --- /dev/null +++ b/src/app/users/register/register.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RegisterComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/users/register/register.component.ts b/src/app/users/register/register.component.ts new file mode 100644 index 0000000..f6da37b --- /dev/null +++ b/src/app/users/register/register.component.ts @@ -0,0 +1,59 @@ +import { Component, OnInit } from "@angular/core"; +import { FormBuilder, Validators, FormGroup } from "@angular/forms"; +import { Router } from "@angular/router"; + +import { AuthService } from "src/app/shared/services/auth.service"; + +@Component({ + selector: "app-register", + templateUrl: "./register.component.html", + styleUrls: ["./register.component.css"], +}) +export class RegisterComponent implements OnInit { + registerForm!: FormGroup; + + constructor( + private fb: FormBuilder, + private auth: AuthService, + private router: Router + ) {} + + ngOnInit(): void { + this.registerForm = this.fb.group({ + firstName: ["", [Validators.required, Validators.minLength(3)]], + lastName: ["", [Validators.required, Validators.minLength(3)]], + nickName: [""], + type: ["", [Validators.required, Validators.minLength(6)]], + email: ["", [Validators.required, Validators.email]], + password: ["", [Validators.required, Validators.minLength(6)]], + }); + } + + get firstName() { + return this.registerForm.get("firstName"); + } + get lastName() { + return this.registerForm.get("lastName"); + } + get nickName() { + return this.registerForm.get("nickName"); + } + get type() { + return this.registerForm.get("type"); + } + get email() { + return this.registerForm.get("email"); + } + get password() { + return this.registerForm.get("password"); + } + + onSubmit() { + if (this.registerForm.valid) { + this.auth + .register(this.registerForm.value) + .subscribe((data) => console.log(data)); + this.router.navigate(["home"]); + } + } +} diff --git a/src/app/users/users-routing.module.ts b/src/app/users/users-routing.module.ts new file mode 100644 index 0000000..9294f67 --- /dev/null +++ b/src/app/users/users-routing.module.ts @@ -0,0 +1,38 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; +import { AppRoutingTitles } from "../app-routing.titles"; +import { AuthGuard } from "../shared/guards/auth.guard"; +import { LoginComponent } from "./login/login.component"; +import { ProfileComponent } from "./profile/profile.component"; +import { RegisterComponent } from "./register/register.component"; + +const routes: Routes = [ + { + path: "login", + component: LoginComponent, + data: { + title: AppRoutingTitles.LOGIN, + }, + }, + { + path: "register", + component: RegisterComponent, + data: { + title: AppRoutingTitles.REGISTER, + }, + }, + { + path: "profile", + component: ProfileComponent, + canActivate: [AuthGuard], + data: { + title: AppRoutingTitles.PROFILE, + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class UsersRoutingModule {} diff --git a/src/app/users/users.module.ts b/src/app/users/users.module.ts new file mode 100644 index 0000000..96a3ccd --- /dev/null +++ b/src/app/users/users.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from "@angular/core"; + +import { SharedModule } from "../shared/shared.module"; + +import { UsersComponent } from "./users/users.component"; +import { LoginComponent } from "./login/login.component"; +import { RegisterComponent } from "./register/register.component"; +import { UsersService } from "./users.service"; +import { UsersRoutingModule } from "./users-routing.module"; +import { AuthService } from "../shared/services/auth.service"; +import { ProfileComponent } from "./profile/profile.component"; + +@NgModule({ + declarations: [ + UsersComponent, + LoginComponent, + RegisterComponent, + ProfileComponent, + ], + imports: [SharedModule, UsersRoutingModule], + providers: [UsersService, AuthService], +}) +export class UsersModule {} diff --git a/src/app/users/users.service.ts b/src/app/users/users.service.ts new file mode 100644 index 0000000..66cfcdf --- /dev/null +++ b/src/app/users/users.service.ts @@ -0,0 +1,53 @@ +import { Injectable, OnDestroy } from "@angular/core"; +import { HttpClient, HttpErrorResponse } from "@angular/common/http"; + +import { Observable, throwError } from "rxjs"; +import { catchError } from "rxjs/operators"; + +import { AuthResponse, RegisterUser, User } from "../shared/models/users.model"; +import { environment } from "src/environments/environment"; + +@Injectable() +export class UsersService implements OnDestroy { + constructor(private http: HttpClient) { + console.log("UsersService instance created."); + } + ngOnDestroy(): void { + console.log("UsersService instance destroyed."); + } + + getUserByEmail(email: string): Observable { + return this.http + .get(environment.baseUrl + `/users/email/${email}`) + .pipe(catchError(this.handleError)); + } + + getUserById(id: number): Observable { + return this.http + .get(environment.baseUrl + `/${id}`) + .pipe(catchError(this.handleError)); + } + + update(user: RegisterUser): Observable { + return this.http + .put(environment.baseUrl + `/users/${user.id}`, user) + .pipe(catchError(this.handleError)); + } + + delete(id: number): Observable { + return this.http + .delete(environment.baseUrl + `/users/${id}`) + .pipe(catchError(this.handleError)); + } + + handleError(error: HttpErrorResponse) { + console.log(error); + + return throwError( + () => + new Error( + `Something bad happend; please try again later. ${error.message}` + ) + ); + } +} diff --git a/src/app/users/users/users.component.css b/src/app/users/users/users.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/users/users/users.component.html b/src/app/users/users/users.component.html new file mode 100644 index 0000000..065c5c6 --- /dev/null +++ b/src/app/users/users/users.component.html @@ -0,0 +1 @@ +

users works!

diff --git a/src/app/users/users/users.component.spec.ts b/src/app/users/users/users.component.spec.ts new file mode 100644 index 0000000..909b5ba --- /dev/null +++ b/src/app/users/users/users.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UsersComponent } from './users.component'; + +describe('UsersComponent', () => { + let component: UsersComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UsersComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UsersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/users/users/users.component.ts b/src/app/users/users/users.component.ts new file mode 100644 index 0000000..1eee32d --- /dev/null +++ b/src/app/users/users/users.component.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from "@angular/core"; + +@Component({ + selector: "app-users", + templateUrl: "./users.component.html", + styleUrls: ["./users.component.css"], +}) +export class UsersComponent implements OnInit { + constructor() {} + + ngOnInit() {} +} diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 7b4f817..ce2c499 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -3,7 +3,8 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, + baseUrl: "http://localhost:8083", }; /* diff --git a/src/index.html b/src/index.html index d195254..766db2f 100644 --- a/src/index.html +++ b/src/index.html @@ -1,13 +1,21 @@ - + - - - DevelopersCornerAngular - - - - - - - + + + Developers Corner + + + + + + + + + diff --git a/src/main.ts b/src/main.ts index c7b673c..3b2b7d0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,4 @@ +import 'hammerjs'; import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; diff --git a/src/styles.css b/src/styles.css index 90d4ee0..7e7239a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1 +1,4 @@ /* You can add global styles to this file, and also import other style files */ + +html, body { height: 100%; } +body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } From b1ce295ec8b8ba61de3397b3804052bb22df1cbe Mon Sep 17 00:00:00 2001 From: Mohammad Date: Thu, 9 Feb 2023 09:21:01 -0600 Subject: [PATCH 2/3] Fix bugs in tests/angular app --- .../question-form.component.html | 6 ++-- .../question/question.component.html | 30 ++++++++++++------- .../questions/question/question.component.ts | 14 ++++----- src/app/shared/models/questions.model.ts | 4 ++- src/app/users/login/login.component.html | 4 +-- src/app/users/profile/profile.component.html | 1 + src/app/users/profile/profile.component.ts | 2 ++ 7 files changed, 38 insertions(+), 23 deletions(-) diff --git a/src/app/home/question-form/question-form.component.html b/src/app/home/question-form/question-form.component.html index 5e0c7d1..c268a5a 100644 --- a/src/app/home/question-form/question-form.component.html +++ b/src/app/home/question-form/question-form.component.html @@ -4,9 +4,9 @@ User Name -
Username is required.
+
+ Username is required. +
Username must be at least 3 characters long.
diff --git a/src/app/questions/question/question.component.html b/src/app/questions/question/question.component.html index c6fe203..4471f86 100644 --- a/src/app/questions/question/question.component.html +++ b/src/app/questions/question/question.component.html @@ -12,7 +12,7 @@ - {{ question.username }} + {{ question.username }} {{ question.tags }} {{ question.role }} {{ question.question }} @@ -42,7 +42,7 @@
- +