[go: up one dir, main page]

0% found this document useful (0 votes)
3 views17 pages

DoConnect Frontend Setup

Uploaded by

Chandan Chandu
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views17 pages

DoConnect Frontend Setup

Uploaded by

Chandan Chandu
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 17

DoConnect Frontend Implementation - Angular

Client
Prerequisites
Node.js 18+ and npm
Angular CLI 18
Angular Material (optional for UI)

Step 1: Create Angular Project

# Install Angular CLI globally


npm install -g @angular/cli@latest

# Create new Angular project


ng new DoConnect.Client --routing --style=css
cd DoConnect.Client

# Install required dependencies


npm install @angular/material @angular/cdk @angular/animations
npm install @microsoft/signalr
npm install jwt-decode
npm install rxjs
npm install bootstrap (optional)

Step 2: Project Structure Setup

Update angular.json for Bootstrap (optional)

{
"projects": {
"DoConnect.Client": {
"architect": {
"build": {
"options": {
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.css"
],
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
]
}
}
}
}
}
}

Step 3: Models and Interfaces

src/app/shared/models/user.model.ts

export interface User {


id: number;
firstName: string;
lastName: string;
email: string;
userName: string;
role: UserRole;
createdAt: Date;
lastLoginAt?: Date;
}

export interface RegisterRequest {


firstName: string;
lastName: string;
email: string;
userName: string;
password: string;
role?: UserRole;
}

export interface LoginRequest {


userName: string;
password: string;
}

export interface AuthResponse {


token: string;
expires: Date;
user: User;
}

export enum UserRole {


User = 0,
Admin = 1
}

src/app/shared/models/question.model.ts

import { User } from './user.model';


import { Answer } from './answer.model';
import { ImageFile } from './image.model';

export interface Question {


id: number;
title: string;
content: string;
topic: string;
status: QuestionStatus;
createdAt: Date;
updatedAt?: Date;
user: User;
approvedBy?: User;
images: ImageFile[];
answers: Answer[];
answerCount: number;
}

export interface CreateQuestionRequest {


title: string;
content: string;
topic: string;
}

export interface UpdateQuestionRequest {


title?: string;
content?: string;
topic?: string;
}

export interface ApproveQuestionRequest {


questionId: number;
status: QuestionStatus;
}

export enum QuestionStatus {


Pending = 0,
Approved = 1,
Rejected = 2
}

src/app/shared/models/answer.model.ts

import { User } from './user.model';


import { ImageFile } from './image.model';

export interface Answer {


id: number;
content: string;
status: AnswerStatus;
createdAt: Date;
updatedAt?: Date;
questionId: number;
user: User;
approvedBy?: User;
images: ImageFile[];
}

export interface CreateAnswerRequest {


content: string;
questionId: number;
}

export interface UpdateAnswerRequest {


content?: string;
}

export interface ApproveAnswerRequest {


answerId: number;
status: AnswerStatus;
}

export enum AnswerStatus {


Pending = 0,
Approved = 1,
Rejected = 2
}

src/app/shared/models/image.model.ts

export interface ImageFile {


id: number;
fileName: string;
filePath: string;
contentType: string;
fileSize: number;
uploadedAt: Date;
}

Step 4: Core Services

src/app/services/auth.service.ts

import { Injectable } from '@angular/core';


import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, tap } from 'rxjs';
import { Router } from '@angular/router';
import { jwtDecode } from 'jwt-decode';
import {
User,
LoginRequest,
RegisterRequest,
AuthResponse,
UserRole
} from '../shared/models/user.model';
import { environment } from '../../environments/environment';

interface JwtPayload {
nameid: string;
unique_name: string;
email: string;
role: string;
firstName: string;
lastName: string;
exp: number;
}

@Injectable({
providedIn: 'root'
})
export class AuthService {
private readonly API_URL = `${environment.apiUrl}/auth`;
private currentUserSubject = new BehaviorSubject<User | null>(null);
private tokenSubject = new BehaviorSubject<string | null>(null);

public currentUser$ = this.currentUserSubject.asObservable();


public token$ = this.tokenSubject.asObservable();

constructor(
private http: HttpClient,
private router: Router
) {
this.loadTokenFromStorage();
}

private loadTokenFromStorage(): void {


const token = localStorage.getItem('doconnect_token');
if (token && this.isTokenValid(token)) {
this.tokenSubject.next(token);
const user = this.getUserFromToken(token);
this.currentUserSubject.next(user);
} else {
this.clearAuthData();
}
}

register(registerData: RegisterRequest): Observable<AuthResponse> {


return this.http.post<AuthResponse>(`${this.API_URL}/register`, registerData)
.pipe(
tap(response => this.setAuthData(response))
);
}

login(loginData: LoginRequest): Observable<AuthResponse> {


return this.http.post<AuthResponse>(`${this.API_URL}/login`, loginData)
.pipe(
tap(response => this.setAuthData(response))
);
}

logout(): void {
this.clearAuthData();
this.router.navigate(['/login']);
}

getProfile(): Observable<User> {
return this.http.get<User>(`${this.API_URL}/profile`);
}
private setAuthData(authResponse: AuthResponse): void {
localStorage.setItem('doconnect_token', authResponse.token);
this.tokenSubject.next(authResponse.token);
this.currentUserSubject.next(authResponse.user);
}

private clearAuthData(): void {


localStorage.removeItem('doconnect_token');
this.tokenSubject.next(null);
this.currentUserSubject.next(null);
}

private isTokenValid(token: string): boolean {


try {
const decoded = jwtDecode<JwtPayload>(token);
const currentTime = Date.now() / 1000;
return decoded.exp > currentTime;
} catch {
return false;
}
}

private getUserFromToken(token: string): User | null {


try {
const decoded = jwtDecode<JwtPayload>(token);
return {
id: parseInt(decoded.nameid),
userName: decoded.unique_name,
email: decoded.email,
firstName: decoded.firstName,
lastName: decoded.lastName,
role: decoded.role === 'Admin' ? UserRole.Admin : UserRole.User,
createdAt: new Date(),
lastLoginAt: new Date()
};
} catch {
return null;
}
}

get currentUser(): User | null {


return this.currentUserSubject.value;
}

get token(): string | null {


return this.tokenSubject.value;
}

get isAuthenticated(): boolean {


return !!this.token && !!this.currentUser;
}

get isAdmin(): boolean {


return this.currentUser?.role === UserRole.Admin;
}
}
src/app/services/question.service.ts

import { Injectable } from '@angular/core';


import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import {
Question,
CreateQuestionRequest,
UpdateQuestionRequest
} from '../shared/models/question.model';
import { environment } from '../../environments/environment';

export interface QuestionQueryParams {


search?: string;
topic?: string;
page?: number;
pageSize?: number;
}

@Injectable({
providedIn: 'root'
})
export class QuestionService {
private readonly API_URL = `${environment.apiUrl}/question`;

constructor(private http: HttpClient) {}

getQuestions(params?: QuestionQueryParams): Observable<Question[]> {


let httpParams = new HttpParams();

if (params?.search) httpParams = httpParams.set('search', params.search);


if (params?.topic) httpParams = httpParams.set('topic', params.topic);
if (params?.page) httpParams = httpParams.set('page', params.page.toString());
if (params?.pageSize) httpParams = httpParams.set('pageSize', params.pageSize.toStrin

return this.http.get<Question[]>(this.API_URL, { params: httpParams });


}

getQuestion(id: number): Observable<Question> {


return this.http.get<Question>(`${this.API_URL}/${id}`);
}

createQuestion(question: CreateQuestionRequest, images?: File[]): Observable<Question>


const formData = new FormData();
formData.append('title', question.title);
formData.append('content', question.content);
formData.append('topic', question.topic);

if (images) {
images.forEach(image => {
formData.append('images', image, image.name);
});
}

return this.http.post<Question>(this.API_URL, formData);


}
updateQuestion(id: number, question: UpdateQuestionRequest): Observable<Question> {
return this.http.put<Question>(`${this.API_URL}/${id}`, question);
}

deleteQuestion(id: number): Observable<any> {


return this.http.delete(`${this.API_URL}/${id}`);
}

getMyQuestions(): Observable<Question[]> {
return this.http.get<Question[]>(`${this.API_URL}/my-questions`);
}

getTopics(): Observable<{topic: string, count: number}[]> {


return this.http.get<{topic: string, count: number}[]>(`${this.API_URL}/topics`);
}
}

src/app/services/answer.service.ts

import { Injectable } from '@angular/core';


import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import {
Answer,
CreateAnswerRequest,
UpdateAnswerRequest
} from '../shared/models/answer.model';
import { environment } from '../../environments/environment';

@Injectable({
providedIn: 'root'
})
export class AnswerService {
private readonly API_URL = `${environment.apiUrl}/answer`;

constructor(private http: HttpClient) {}

getAnswersForQuestion(questionId: number): Observable<Answer[]> {


return this.http.get<Answer[]>(`${this.API_URL}/question/${questionId}`);
}

getAnswer(id: number): Observable<Answer> {


return this.http.get<Answer>(`${this.API_URL}/${id}`);
}

createAnswer(answer: CreateAnswerRequest, images?: File[]): Observable<Answer> {


const formData = new FormData();
formData.append('content', answer.content);
formData.append('questionId', answer.questionId.toString());

if (images) {
images.forEach(image => {
formData.append('images', image, image.name);
});
}

return this.http.post<Answer>(this.API_URL, formData);


}

updateAnswer(id: number, answer: UpdateAnswerRequest): Observable<Answer> {


return this.http.put<Answer>(`${this.API_URL}/${id}`, answer);
}

deleteAnswer(id: number): Observable<any> {


return this.http.delete(`${this.API_URL}/${id}`);
}

getMyAnswers(): Observable<Answer[]> {
return this.http.get<Answer[]>(`${this.API_URL}/my-answers`);
}
}

src/app/services/file.service.ts

import { Injectable } from '@angular/core';


import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { ImageFile } from '../shared/models/image.model';
import { environment } from '../../environments/environment';

@Injectable({
providedIn: 'root'
})
export class FileService {
private readonly API_URL = `${environment.apiUrl}/file`;

constructor(private http: HttpClient) {}

uploadImage(file: File, questionId?: number, answerId?: number): Observable<ImageFile>


const formData = new FormData();
formData.append('file', file, file.name);

let params = new HttpParams();


if (questionId) params = params.set('questionId', questionId.toString());
if (answerId) params = params.set('answerId', answerId.toString());

return this.http.post<ImageFile>(`${this.API_URL}/upload`, formData, { params });


}

deleteImage(imageId: number): Observable<any> {


return this.http.delete(`${this.API_URL}/${imageId}`);
}

getImageUrl(imageId: number): string {


return `${this.API_URL}/${imageId}`;
}

isValidImageFile(file: File): boolean {


const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp
const maxSize = 5 * 1024 * 1024; // 5MB

return allowedTypes.includes(file.type) && file.size <= maxSize;


}
}

src/app/services/signalr.service.ts

import { Injectable } from '@angular/core';


import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
import { BehaviorSubject, Observable } from 'rxjs';
import { AuthService } from './auth.service';
import { environment } from '../../environments/environment';

export interface Notification {


message: string;
timestamp: Date;
}

@Injectable({
providedIn: 'root'
})
export class SignalRService {
private hubConnection: HubConnection | null = null;
private connectionStateSubject = new BehaviorSubject<boolean>(false);
private notificationsSubject = new BehaviorSubject<Notification[]>([]);

public connectionState$ = this.connectionStateSubject.asObservable();


public notifications$ = this.notificationsSubject.asObservable();

constructor(private authService: AuthService) {


this.authService.currentUser$.subscribe(user => {
if (user) {
this.startConnection();
} else {
this.stopConnection();
}
});
}

private async startConnection(): Promise<void> {


if (this.hubConnection?.state === 'Connected') {
return;
}

const token = this.authService.token;


if (!token) {
return;
}

this.hubConnection = new HubConnectionBuilder()


.withUrl(`${environment.apiUrl}/notificationHub`, {
accessTokenFactory: () => token
})
.withAutomaticReconnect()
.configureLogging(LogLevel.Information)
.build();

try {
await this.hubConnection.start();
console.log('SignalR connection established');
this.connectionStateSubject.next(true);
this.setupEventListeners();

// Join admin group if user is admin


if (this.authService.isAdmin) {
await this.hubConnection.invoke('JoinAdminGroup');
}
} catch (error) {
console.error('SignalR connection error:', error);
this.connectionStateSubject.next(false);
}
}

private setupEventListeners(): void {


if (!this.hubConnection) return;

this.hubConnection.on('NewQuestion', (notification: Notification) => {


this.addNotification(notification);
});

this.hubConnection.on('NewAnswer', (notification: Notification) => {


this.addNotification(notification);
});

this.hubConnection.onclose(() => {
console.log('SignalR connection closed');
this.connectionStateSubject.next(false);
});

this.hubConnection.onreconnecting(() => {
console.log('SignalR reconnecting...');
this.connectionStateSubject.next(false);
});

this.hubConnection.onreconnected(() => {
console.log('SignalR reconnected');
this.connectionStateSubject.next(true);
});
}

private addNotification(notification: Notification): void {


const currentNotifications = this.notificationsSubject.value;
const updatedNotifications = [notification, ...currentNotifications].slice(0, 50); //
this.notificationsSubject.next(updatedNotifications);
}

private async stopConnection(): Promise<void> {


if (this.hubConnection) {
try {
await this.hubConnection.stop();
console.log('SignalR connection stopped');
} catch (error) {
console.error('Error stopping SignalR connection:', error);
}
this.connectionStateSubject.next(false);
}
}

clearNotifications(): void {
this.notificationsSubject.next([]);
}

get isConnected(): boolean {


return this.connectionStateSubject.value;
}
}

src/app/services/admin.service.ts

import { Injectable } from '@angular/core';


import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import {
Question,
ApproveQuestionRequest
} from '../shared/models/question.model';
import {
Answer,
ApproveAnswerRequest
} from '../shared/models/answer.model';
import { User } from '../shared/models/user.model';
import { environment } from '../../environments/environment';

export interface DashboardStats {


totalUsers: number;
totalQuestions: number;
pendingQuestions: number;
approvedQuestions: number;
totalAnswers: number;
pendingAnswers: number;
approvedAnswers: number;
totalImages: number;
}

@Injectable({
providedIn: 'root'
})
export class AdminService {
private readonly API_URL = `${environment.apiUrl}/admin`;

constructor(private http: HttpClient) {}

getPendingQuestions(): Observable<Question[]> {
return this.http.get<Question[]>(`${this.API_URL}/questions/pending`);
}
getPendingAnswers(): Observable<Answer[]> {
return this.http.get<Answer[]>(`${this.API_URL}/answers/pending`);
}

updateQuestionStatus(id: number, request: ApproveQuestionRequest): Observable<any> {


return this.http.put(`${this.API_URL}/questions/${id}/status`, request);
}

updateAnswerStatus(id: number, request: ApproveAnswerRequest): Observable<any> {


return this.http.put(`${this.API_URL}/answers/${id}/status`, request);
}

getDashboardStats(): Observable<DashboardStats> {
return this.http.get<DashboardStats>(`${this.API_URL}/dashboard/stats`);
}

getAllUsers(page: number = 1, pageSize: number = 20): Observable<User[]> {


return this.http.get<User[]>(`${this.API_URL}/users`, {
params: { page: page.toString(), pageSize: pageSize.toString() }
});
}

deleteQuestion(id: number): Observable<any> {


return this.http.delete(`${this.API_URL}/questions/${id}`);
}

deleteAnswer(id: number): Observable<any> {


return this.http.delete(`${this.API_URL}/answers/${id}`);
}
}

Step 5: HTTP Interceptor for JWT

src/app/interceptors/auth.interceptor.ts

import { Injectable } from '@angular/core';


import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/htt
import { Observable, catchError, throwError } from 'rxjs';
import { AuthService } from '../services/auth.service';
import { Router } from '@angular/router';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(
private authService: AuthService,
private router: Router
) {}

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {


const token = this.authService.token;

if (token) {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}

return next.handle(req).pipe(
catchError(error => {
if (error.status === 401) {
this.authService.logout();
this.router.navigate(['/login']);
}
return throwError(() => error);
})
);
}
}

Step 6: Guards

src/app/guards/auth.guard.ts

import { Injectable } from '@angular/core';


import { CanActivate, Router, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';

@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}

canActivate(): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | U


if (this.authService.isAuthenticated) {
return true;
}

return this.router.createUrlTree(['/login']);
}
}

src/app/guards/admin.guard.ts

import { Injectable } from '@angular/core';


import { CanActivate, Router, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';
@Injectable({
providedIn: 'root'
})
export class AdminGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}

canActivate(): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | U


if (this.authService.isAuthenticated && this.authService.isAdmin) {
return true;
}

if (!this.authService.isAuthenticated) {
return this.router.createUrlTree(['/login']);
}

return this.router.createUrlTree(['/questions']);
}
}

Step 7: Environment Configuration

src/environments/environment.ts

export const environment = {


production: false,
apiUrl: 'https://localhost:7001/api'
};

src/environments/environment.prod.ts

export const environment = {


production: true,
apiUrl: 'https://your-api-domain.com/api'
};

Step 8: App Routing Module

src/app/app-routing.module.ts

import { NgModule } from '@angular/core';


import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './guards/auth.guard';
import { AdminGuard } from './guards/admin.guard';

const routes: Routes = [


{ path: '', redirectTo: '/questions', pathMatch: 'full' },
{
path: 'auth',
loadChildren: () => import('./auth/auth.module').then(m => m.AuthModule)
},
{
path: 'questions',
loadChildren: () => import('./questions/questions.module').then(m => m.QuestionsModul
canActivate: [AuthGuard]
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canActivate: [AdminGuard]
},
{ path: '**', redirectTo: '/questions' }
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

Step 9: App Module Configuration

src/app/app.module.ts

import { NgModule } from '@angular/core';


import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';

// Angular Material modules


import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatBadgeModule } from '@angular/material/badge';
import { MatSnackBarModule } from '@angular/material/snack-bar';

import { AppRoutingModule } from './app-routing.module';


import { AppComponent } from './app.component';
import { AuthInterceptor } from './interceptors/auth.interceptor';
import { NavbarComponent } from './shared/components/navbar/navbar.component';
import { NotificationComponent } from './shared/components/notification/notification.comp

@NgModule({
declarations: [
AppComponent,
NavbarComponent,
NotificationComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
HttpClientModule,
ReactiveFormsModule,
FormsModule,

// Angular Material
MatToolbarModule,
MatButtonModule,
MatIconModule,
MatMenuModule,
MatBadgeModule,
MatSnackBarModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule { }

This completes the basic setup and core services for the Angular frontend. The next sections will
include all the components (login, questions, admin dashboard, etc.).

You might also like