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.).