Saturday, August 16, 2025
0 comments

Build a User/Admin Dashboard in Angular 20: A Beginner’s Guide to Login, CRUD, Role-Based Routing, and API Integration with JSONPlaceholder

 



What You’ll Build:
  • Login System: Authenticate users with a simple username/password form.
  • User CRUD: Create, read, update, and delete users via JSONPlaceholder.
  • Role-Based Routing: Restrict /admin to authenticated admin users.
  • API Integration: Fetch and manage user data from JSONPlaceholder.
  • Reusable Components: Header, footer, and sidebar for consistent UI.
  • Lifecycle Hooks, Forms, Services: Implement robust application logic.

Project SetupPrerequisites
  • Node.js (v18+ recommended)
  • Angular CLI (npm install -g @angular/cli)
  • Code editor (e.g., VS Code)
  • Basic understanding of Angular concepts from previous modules
Step 1: Create the Project
bash
ng new user-admin-dashboard --style=scss --routing
cd user-admin-dashboard
ng add @angular/material
  • Select the Indigo/Pink theme.
  • Enable typography and animations.
Step 2: Generate Components, Services, Guards, and Pipes
bash
ng g component components/login
ng g component components/user-dashboard
ng g component components/admin-dashboard
ng g component components/user-list
ng g component components/user-form
ng g component components/header
ng g component components/footer
ng g component components/sidebar
ng g component components/not-found
ng g service services/auth
ng g service services/user
ng g service services/storage
ng g guard guards/auth
ng g pipe pipes/filter-by-role
  • Choose CanActivate for AuthGuard.
Step 3: Install DependenciesEnsure all dependencies are installed:
bash
npm install

Source CodeBelow is the complete source code for the user-admin-dashboard project, organized by file. The app uses JSONPlaceholder (https://jsonplaceholder.typicode.com/users) for user data, Angular Material for UI, and local storage for authentication.1. Environment Files
typescript
// src/environments/environment.ts (Development)
export const environment = {
  production: false,
  apiUrl: 'https://jsonplaceholder.typicode.com/users'
};
typescript
// src/environments/environment.prod.ts (Production)
export const environment = {
  production: true,
  apiUrl: 'https://jsonplaceholder.typicode.com/users'
};
2. Models
typescript
// src/app/models/user.model.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}
3. ServicesAuthService
typescript
// src/app/services/auth.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { User } from '../models/user.model';
import { StorageService } from './storage.service';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private currentUserSubject = new BehaviorSubject<User | null>(null);
  currentUser$ = this.currentUserSubject.asObservable();

  constructor(private storageService: StorageService) {
    const storedUser = this.storageService.getLocalStorage('currentUser');
    if (storedUser) {
      this.currentUserSubject.next(storedUser);
    }
  }

  login(username: string, password: string): boolean {
    // Mock admin credentials (replace with API call in production)
    const admins = [
      { username: 'admin1', password: 'admin123' },
      { username: 'admin2', password: 'admin456' }
    ];
    const isValid = admins.some(admin => admin.username === username && admin.password === password);
    if (isValid) {
      const user: User = {
        id: 0,
        name: username,
        email: `${username}@example.com`,
        role: 'admin'
      };
      this.currentUserSubject.next(user);
      this.storageService.setLocalStorage('currentUser', user);
      return true;
    }
    return false;
  }

  logout() {
    this.currentUserSubject.next(null);
    this.storageService.clearLocalStorage('currentUser');
  }

  isAuthenticated(): boolean {
    return !!this.currentUserSubject.getValue();
  }

  isAdmin(): boolean {
    const user = this.currentUserSubject.getValue();
    return user?.role === 'admin';
  }
}
UserService
typescript
// src/app/services/user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { User } from '../models/user.model';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private apiUrl = environment.apiUrl;

  constructor(private http: HttpClient) {}

  getUsers(): Observable<User[]> {
    return this.http.get<any[]>(this.apiUrl).pipe(
      map(users => users.map(user => ({
        id: user.id,
        name: user.name,
        email: user.email,
        role: user.id % 2 === 0 ? 'user' : 'admin' // Mock role assignment
      }))),
      catchError(error => {
        console.error('Error fetching users:', error);
        return of([]);
      })
    );
  }

  getUserById(id: number): Observable<User | undefined> {
    return this.http.get<any>(`${this.apiUrl}/${id}`).pipe(
      map(user => ({
        id: user.id,
        name: user.name,
        email: user.email,
        role: user.id % 2 === 0 ? 'user' : 'admin'
      })),
      catchError(error => {
        console.error('Error fetching user:', error);
        return of(undefined);
      })
    );
  }

  addUser(user: User): Observable<User> {
    return this.http.post<User>(this.apiUrl, user).pipe(
      catchError(error => {
        console.error('Error adding user:', error);
        return of(user); // Simulate success for JSONPlaceholder
      })
    );
  }

  updateUser(user: User): Observable<User> {
    return this.http.put<User>(`${this.apiUrl}/${user.id}`, user).pipe(
      catchError(error => {
        console.error('Error updating user:', error);
        return of(user); // Simulate success
      })
    );
  }

  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`).pipe(
      catchError(error => {
        console.error('Error deleting user:', error);
        return of(undefined);
      })
    );
  }
}
StorageService
typescript
// src/app/services/storage.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class StorageService {
  setLocalStorage(key: string, value: any) {
    localStorage.setItem(key, JSON.stringify(value));
  }

  getLocalStorage(key: string) {
    const data = localStorage.getItem(key);
    return data ? JSON.parse(data) : null;
  }

  clearLocalStorage(key: string) {
    localStorage.removeItem(key);
  }
}
4. GuardsAuthGuard
typescript
// src/app/guards/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

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

  canActivate(): boolean {
    if (this.authService.isAuthenticated() && this.authService.isAdmin()) {
      return true;
    }
    this.router.navigate(['/login']);
    return false;
  }
}
5. Pipes
typescript
// src/app/pipes/filter-by-role.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { User } from '../models/user.model';

@Pipe({
  name: 'filterByRole',
  standalone: true
})
export class FilterByRolePipe implements PipeTransform {
  transform(users: User[], role: string): User[] {
    if (!users || role === 'all') return users;
    return users.filter(user => user.role === role);
  }
}
6. App Routing
typescript
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { LoginComponent } from './components/login/login.component';
import { UserDashboardComponent } from './components/user-dashboard/user-dashboard.component';
import { AdminDashboardComponent } from './components/admin-dashboard/admin-dashboard.component';
import { NotFoundComponent } from './components/not-found/not-found.component';
import { AuthGuard } from './guards/auth.guard';

export const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { path: 'user', component: UserDashboardComponent },
  { path: 'admin', component: AdminDashboardComponent, canActivate: [AuthGuard] },
  { path: '', redirectTo: '/user', pathMatch: 'full' },
  { path: '**', component: NotFoundComponent }
];
7. Main Entry Point
typescript
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { provideAnimations } from '@angular/platform-browser/animations';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    provideAnimations()
  ]
}).catch(err => console.error(err));
8. App Component
typescript
// src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { HeaderComponent } from './components/header/header.component';
import { FooterComponent } from './components/footer/footer.component';
import { SidebarComponent } from './components/sidebar/sidebar.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  standalone: true,
  imports: [RouterOutlet, HeaderComponent, FooterComponent, SidebarComponent]
})
export class AppComponent {}
typescript
// src/app/app.component.html
<app-header></app-header>
<div class="container">
  <app-sidebar></app-sidebar>
  <main>
    <router-outlet></router-outlet>
  </main>
</div>
<app-footer></app-footer>
scss
// src/app/app.component.scss
.container {
  display: flex;
  min-height: calc(100vh - 128px);
}

main {
  flex: 1;
  padding: 20px;
}
9. Reusable ComponentsHeaderComponent
typescript
// src/app/components/header/header.component.ts
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, Router } from '@angular/router';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { AuthService } from '../../services/auth.service';
import { User } from '../../models/user.model';

@Component({
  selector: 'app-header',
  template: `
    <mat-toolbar color="primary">
      <span>User/Admin Dashboard</span>
      <nav>
        <a mat-button routerLink="/user" routerLinkActive="active">User Dashboard</a>
        <a mat-button *ngIf="!(currentUser$ | async)" routerLink="/login" routerLinkActive="active">Login</a>
        <span *ngIf="currentUser$ | async as user">Welcome, {{ user.name }}</span>
        <button mat-button *ngIf="currentUser$ | async" (click)="logout()">Logout</button>
        <a mat-button routerLink="/admin" routerLinkActive="active">Admin Dashboard</a>
      </nav>
    </mat-toolbar>
  `,
  styleUrls: ['./header.component.scss'],
  standalone: true,
  imports: [RouterLink, RouterLinkActive, MatToolbarModule, MatButtonModule]
})
export class HeaderComponent {
  currentUser$ = this.authService.currentUser$;

  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  logout() {
    this.authService.logout();
    this.router.navigate(['/user']);
  }
}
scss
// src/app/components/header/header.component.scss
mat-toolbar {
  display: flex;
  justify-content: space-between;
  padding: 0 20px;

  nav {
    display: flex;
    align-items: center;
    a, button, span {
      margin-left: 10px;
    }
    a.active {
      font-weight: bold;
      border-bottom: 2px solid white;
    }
  }
}
FooterComponent
typescript
// src/app/components/footer/footer.component.ts
import { Component } from '@angular/core';
import { MatToolbarModule } from '@angular/material/toolbar';

@Component({
  selector: 'app-footer',
  template: `
    <mat-toolbar color="primary">
      <span>&copy; 2025 User/Admin Dashboard</span>
    </mat-toolbar>
  `,
  styleUrls: ['./footer.component.scss'],
  standalone: true,
  imports: [MatToolbarModule]
})
export class FooterComponent {}
scss
// src/app/components/footer/footer.component.scss
mat-toolbar {
  justify-content: center;
  padding: 10px 0;
}
SidebarComponent
typescript
// src/app/components/sidebar/sidebar.component.ts
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
import { AuthService } from '../../services/auth.service';

@Component({
  selector: 'app-sidebar',
  template: `
    <mat-sidenav-container>
      <mat-sidenav mode="side" opened>
        <mat-nav-list>
          <a mat-list-item routerLink="/user" routerLinkActive="active">User Dashboard</a>
          <a mat-list-item routerLink="/admin" routerLinkActive="active" *ngIf="authService.isAdmin()">Admin Dashboard</a>
          <a mat-list-item routerLink="/login" routerLinkActive="active" *ngIf="!authService.isAuthenticated()">Login</a>
        </mat-nav-list>
      </mat-sidenav>
    </mat-sidenav-container>
  `,
  styleUrls: ['./sidebar.component.scss'],
  standalone: true,
  imports: [RouterLink, RouterLinkActive, MatSidenavModule, MatListModule]
})
export class SidebarComponent {
  constructor(public authService: AuthService) {}
}
scss
// src/app/components/sidebar/sidebar.component.scss
mat-sidenav {
  width: 200px;
  background-color: #f5f5f5;
  padding: 20px;

  mat-nav-list {
    a {
      color: #333;
      &.active {
        font-weight: bold;
        background-color: #e0f7fa;
      }
    }
  }
}
10. Feature ComponentsLoginComponent
typescript
// src/app/components/login/login.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { AuthService } from '../../services/auth.service';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss'],
  standalone: true,
  imports: [
    FormsModule,
    RouterLink,
    MatCardModule,
    MatButtonModule,
    MatFormFieldModule,
    MatInputModule
  ]
})
export class LoginComponent {
  username = '';
  password = '';
  errorMessage = '';

  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  onSubmit() {
    if (this.authService.login(this.username, this.password)) {
      this.router.navigate(['/admin']);
    } else {
      this.errorMessage = 'Invalid username or password';
    }
  }
}
typescript
// src/app/components/login/login.component.html
<mat-card class="container">
  <mat-card-header>
    <mat-card-title>Admin Login</mat-card-title>
  </mat-card-header>
  <mat-card-content>
    <form (ngSubmit)="onSubmit()">
      <mat-form-field appearance="fill">
        <mat-label>Username</mat-label>
        <input matInput [(ngModel)]="username" name="username" required>
        <mat-error *ngIf="form.username?.errors?.['required'] && form.username?.touched">
          Username is required
        </mat-error>
      </mat-form-field>
      <mat-form-field appearance="fill">
        <mat-label>Password</mat-label>
        <input matInput [(ngModel)]="password" name="password" type="password" required>
        <mat-error *ngIf="form.password?.errors?.['required'] && form.password?.touched">
          Password is required
        </mat-error>
      </mat-form-field>
      <div *ngIf="errorMessage" class="error">{{ errorMessage }}</div>
      <button mat-raised-button color="primary" type="submit" [disabled]="!form.valid">Login</button>
    </form>
    <a mat-button routerLink="/user">Back to User Dashboard</a>
  </mat-card-content>
</mat-card>
scss
// src/app/components/login/login.component.scss
.container {
  margin: 20px;
  max-width: 400px;
}

form {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

mat-form-field {
  width: 100%;
}

.error {
  color: red;
  font-size: 0.9em;
}
UserDashboardComponent
typescript
// src/app/components/user-dashboard/user-dashboard.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { UserService } from '../../services/user.service';
import { User } from '../../models/user.model';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatListModule } from '@angular/material/list';
import { UserListComponent } from '../user-list/user-list.component';

@Component({
  selector: 'app-user-dashboard',
  template: `
    <mat-card class="container">
      <mat-card-header>
        <mat-card-title>User Dashboard</mat-card-title>
      </mat-card-header>
      <mat-card-content>
        <app-user-list [users]="users$ | async"></app-user-list>
        <div *ngIf="isLoading" class="loading">Loading users...</div>
        <div *ngIf="error" class="error">{{ error }}</div>
      </mat-card-content>
    </mat-card>
  `,
  styleUrls: ['./user-dashboard.component.scss'],
  standalone: true,
  imports: [RouterLink, MatCardModule, MatButtonModule, MatListModule, UserListComponent]
})
export class UserDashboardComponent implements OnInit, OnDestroy {
  users$: Observable<User[]>;
  isLoading = false;
  error: string | null = null;

  constructor(private userService: UserService) {
    this.users$ = of([]);
  }

  ngOnInit() {
    this.isLoading = true;
    this.users$ = this.userService.getUsers().pipe(
      catchError(err => {
        this.error = 'Failed to load users: ' + err.message;
        this.isLoading = false;
        return of([]);
      }),
      finalize(() => this.isLoading = false)
    );
  }

  ngOnDestroy() {
    console.log('UserDashboardComponent: Destroyed');
  }
}
scss
// src/app/components/user-dashboard/user-dashboard.component.scss
.container {
  margin: 20px;
}

.loading {
  text-align: center;
  color: #3f51b5;
}

.error {
  text-align: center;
  color: red;
}
AdminDashboardComponent
typescript
// src/app/components/admin-dashboard/admin-dashboard.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { UserService } from '../../services/user.service';
import { User } from '../../models/user.model';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { UserListComponent } from '../user-list/user-list.component';
import { UserFormComponent } from '../user-form/user-form.component';
import { FilterByRolePipe } from '../../pipes/filter-by-role.pipe';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-admin-dashboard',
  template: `
    <mat-card class="container">
      <mat-card-header>
        <mat-card-title>Admin Dashboard</mat-card-title>
      </mat-card-header>
      <mat-card-content>
        <h3>Add New User</h3>
        <app-user-form (userSaved)="addUser($event)"></app-user-form>
        <h3>User List</h3>
        <mat-form-field appearance="fill">
          <mat-label>Filter by Role</mat-label>
          <mat-select [(ngModel)]="filterRole" (ngModelChange)="setFilterRole($event)">
            <mat-option value="all">All</mat-option>
            <mat-option value="admin">Admin</mat-option>
            <mat-option value="user">User</mat-option>
          </mat-select>
        </mat-form-field>
        <app-user-list [users]="(users$ | async | filterByRole:filterRole)" (editUser)="editUser($event)" (deleteUser)="deleteUser($event)"></app-user-list>
        <div *ngIf="isLoading" class="loading">Loading users...</div>
        <div *ngIf="error" class="error">{{ error }}</div>
      </mat-card-content>
    </mat-card>
  `,
  styleUrls: ['./admin-dashboard.component.scss'],
  standalone: true,
  imports: [
    RouterLink,
    MatCardModule,
    MatButtonModule,
    MatSelectModule,
    MatFormFieldModule,
    UserListComponent,
    UserFormComponent,
    FilterByRolePipe,
    FormsModule
  ]
})
export class AdminDashboardComponent implements OnInit, OnDestroy {
  users$: Observable<User[]>;
  filterRole = 'all';
  isLoading = false;
  error: string | null = null;

  constructor(private userService: UserService) {
    this.users$ = of([]);
  }

  ngOnInit() {
    this.isLoading = true;
    this.users$ = this.userService.getUsers().pipe(
      catchError(err => {
        this.error = 'Failed to load users: ' + err.message;
        this.isLoading = false;
        return of([]);
      }),
      finalize(() => this.isLoading = false)
    );
  }

  ngOnDestroy() {
    console.log('AdminDashboardComponent: Destroyed');
  }

  setFilterRole(role: string) {
    this.filterRole = role;
  }

  addUser(user: User) {
    this.isLoading = true;
    this.userService.addUser(user).subscribe({
      next: () => {
        this.users$ = this.userService.getUsers();
      },
      error: err => {
        this.error = 'Failed to add user: ' + err.message;
        this.isLoading = false;
      },
      complete: () => this.isLoading = false
    });
  }

  editUser(user: User) {
    this.isLoading = true;
    this.userService.updateUser(user).subscribe({
      next: () => {
        this.users$ = this.userService.getUsers();
      },
      error: err => {
        this.error = 'Failed to update user: ' + err.message;
        this.isLoading = false;
      },
      complete: () => this.isLoading = false
    });
  }

  deleteUser(id: number) {
    this.isLoading = true;
    this.userService.deleteUser(id).subscribe({
      next: () => {
        this.users$ = this.userService.getUsers();
      },
      error: err => {
        this.error = 'Failed to delete user: ' + err.message;
        this.isLoading = false;
      },
      complete: () => this.isLoading = false
    });
  }
}
scss
// src/app/components/admin-dashboard/admin-dashboard.component.scss
.container {
  margin: 20px;
}

mat-form-field {
  margin-bottom: 20px;
}

.loading {
  text-align: center;
  color: #3f51b5;
}

.error {
  text-align: center;
  color: red;
}
UserListComponent
typescript
// src/app/components/user-list/user-list.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { User } from '../../models/user.model';
import { MatListModule } from '@angular/material/list';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { AuthService } from '../../services/auth.service';

@Component({
  selector: 'app-user-list',
  template: `
    <mat-list>
      <mat-list-item *ngFor="let user of users">
        <span>{{ user.name }} ({{ user.role }}) - {{ user.email }}</span>
        <button mat-raised-button color="accent" *ngIf="authService.isAdmin()" (click)="editUser.emit(user)">Edit</button>
        <button mat-raised-button color="warn" *ngIf="authService.isAdmin()" (click)="deleteUser.emit(user.id)">Delete</button>
      </mat-list-item>
    </mat-list>
  `,
  styleUrls: ['./user-list.component.scss'],
  standalone: true,
  imports: [MatListModule, MatButtonModule, MatCardModule]
})
export class UserListComponent {
  @Input() users: User[] = [];
  @Output() editUser = new EventEmitter<User>();
  @Output() deleteUser = new EventEmitter<number>();

  constructor(public authService: AuthService) {}
}
scss
// src/app/components/user-list/user-list.component.scss
mat-list-item {
  margin: 5px 0;
  span {
    flex: 1;
  }
  button {
    margin-left: 10px;
  }
}
UserFormComponent
typescript
// src/app/components/user-form/user-form.component.ts
import { Component, EventEmitter, Output } from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormControl, Validators } from '@angular/forms';
import { User } from '../../models/user.model';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';

@Component({
  selector: 'app-user-form',
  template: `
    <mat-card>
      <mat-card-content>
        <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
          <mat-form-field appearance="fill">
            <mat-label>Name</mat-label>
            <input matInput formControlName="name">
            <mat-error *ngIf="userForm.get('name')?.hasError('required') && userForm.get('name')?.touched">
              Name is required
            </mat-error>
          </mat-form-field>
          <mat-form-field appearance="fill">
            <mat-label>Email</mat-label>
            <input matInput formControlName="email">
            <mat-error *ngIf="userForm.get('email')?.hasError('required') && userForm.get('email')?.touched">
              Email is required
            </mat-error>
            <mat-error *ngIf="userForm.get('email')?.hasError('email') && userForm.get('email')?.touched">
              Invalid email format
            </mat-error>
          </mat-form-field>
          <mat-form-field appearance="fill">
            <mat-label>Role</mat-label>
            <mat-select formControlName="role">
              <mat-option value="user">User</mat-option>
              <mat-option value="admin">Admin</mat-option>
            </mat-select>
          </mat-form-field>
          <button mat-raised-button color="primary" type="submit" [disabled]="userForm.invalid">Save</button>
        </form>
      </mat-card-content>
    </mat-card>
  `,
  styleUrls: ['./user-form.component.scss'],
  standalone: true,
  imports: [
    ReactiveFormsModule,
    MatCardModule,
    MatButtonModule,
    MatFormFieldModule,
    MatInputModule,
    MatSelectModule
  ]
})
export class UserFormComponent {
  @Output() userSaved = new EventEmitter<User>();

  userForm = new FormGroup({
    id: new FormControl(0),
    name: new FormControl('', [Validators.required]),
    email: new FormControl('', [Validators.required, Validators.email]),
    role: new FormControl('user', [Validators.required])
  });

  onSubmit() {
    if (this.userForm.valid) {
      const user: User = {
        id: this.userForm.value.id || Math.floor(Math.random() * 1000), // Simulate new ID
        name: this.userForm.value.name || '',
        email: this.userForm.value.email || '',
        role: this.userForm.value.role || 'user'
      };
      this.userSaved.emit(user);
      this.userForm.reset({ id: 0, name: '', email: '', role: 'user' });
    }
  }
}
scss
// src/app/components/user-form/user-form.component.scss
mat-card {
  margin: 20px 0;
  padding: 10px;
}

form {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

mat-form-field {
  width: 100%;
  max-width: 400px;
}
NotFoundComponent
typescript
// src/app/components/not-found/not-found.component.ts
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';

@Component({
  selector: 'app-not-found',
  template: `
    <mat-card class="container">
      <mat-card-header>
        <mat-card-title>404 - Page Not Found</mat-card-title>
      </mat-card-header>
      <mat-card-content>
        <a mat-button routerLink="/user">Go back to User Dashboard</a>
      </mat-card-content>
    </mat-card>
  `,
  styleUrls: ['./not-found.component.scss'],
  standalone: true,
  imports: [RouterLink, MatCardModule, MatButtonModule]
})
export class NotFoundComponent {}
scss
// src/app/components/not-found/not-found.component.scss
.container {
  margin: 20px;
  text-align: center;
}
11. Global Styles
scss
// src/styles.scss
@import '@angular/material/theming';
@include mat-core();

$primary: mat-palette($mat-indigo);
$accent: mat-palette($mat-pink, A200, A100, A400);
$theme: mat-light-theme($primary, $accent);
@include angular-material-theme($theme);

:root {
  --primary-color: #3f51b5;
  --accent-color: #ff4081;
  --background-color: #f5f5f5;
  --text-color: #333;
}

body {
  margin: 0;
  font-family: Roboto, 'Helvetica Neue', sans-serif;
  background-color: var(--background-color);
  color: var(--text-color);
}

Implementation DetailsLogin System
  • Component: LoginComponent uses a template-driven form to authenticate users.
  • Service: AuthService validates credentials and stores the user in local storage.
  • Behavior: Successful login redirects to /admin; failures show an error.
User CRUD
  • Components:
    • UserListComponent: Displays users with edit/delete buttons for admins.
    • UserFormComponent: Handles add/edit forms for users.
  • Service: UserService interacts with JSONPlaceholder for CRUD operations.
  • Behavior:
    • Create: UserFormComponent emits new user data to AdminDashboardComponent.
    • Read: UserService fetches users for both dashboards.
    • Update: Edit buttons trigger form population and updates via UserService.
    • Delete: Delete buttons trigger UserService.deleteUser.
Role-Based Routing
  • Guard: AuthGuard restricts /admin to authenticated admins.
  • Behavior: Unauthenticated users or non-admins are redirected to /login.
API Integration
  • Endpoint: JSONPlaceholder (https://jsonplaceholder.typicode.com/users).
  • Service: UserService maps API responses to the User model and handles errors.
  • Behavior: Fetches users, supports CRUD operations (simulated for POST/PUT/DELETE due to JSONPlaceholder’s mock nature).
Reusable Components
  • Header: Displays navigation and login/logout controls.
  • Footer: Shows copyright information.
  • Sidebar: Provides navigation links, conditionally showing admin routes.
  • Behavior: Consistent UI across routes, responsive design with Angular Material.
Lifecycle Hooks
  • OnInit: Initializes data fetching in UserDashboardComponent and AdminDashboardComponent.
  • OnDestroy: Logs component destruction for debugging.
Forms
  • Template-Driven: Used in LoginComponent for simplicity.
  • Reactive: Used in UserFormComponent for robust validation.
Services
  • AuthService: Manages authentication state.
  • UserService: Handles API interactions.
  • StorageService: Abstracts local storage operations.

DeploymentBuild
bash
ng build --prod
Local Server
  1. Install http-server:
    bash
    npm install -g http-server
  2. Serve:
    bash
    cd dist/user-admin-dashboard
    http-server -p 8080
  3. Visit http://localhost:8080.
GitHub Pages
  1. Install angular-cli-ghpages:
    bash
    npm install -g angular-cli-ghpages
  2. Build:
    bash
    ng build --prod --base-href /user-admin-dashboard/
  3. Deploy:
    bash
    ngh --dir=dist/user-admin-dashboard
  4. Enable GitHub Pages in the repository settings.
Firebase Hosting
  1. Install Firebase CLI:
    bash
    npm install -g firebase-tools
  2. Initialize:
    bash
    firebase init hosting
  3. Build and deploy:
    bash
    ng build --prod
    firebase deploy

Testing the Application
  1. Run ng serve and visit http://localhost:4200.
  2. Test features:
    • Login (/login): Log in with admin1/admin123. Verify redirection to /admin.
    • Admin Dashboard (/admin): Access restricted to admins. Add, edit, delete users.
    • User Dashboard (/user): View user list (no edit/delete for non-admins).
    • Logout (Header): Log out and verify redirection to /user.
    • Sidebar: Confirm admin routes are hidden for non-admins.
    • API: Verify user data loads from JSONPlaceholder.
    • 404 (/invalid): Check the Not Found page.

TroubleshootingTS2305: Module '"./app/app"' has no exported member 'App'Fix:
typescript
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, provideHttpClient } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { provideAnimations } from '@angular/platform-browser/animations';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    provideAnimations()
  ]
}).catch(err => console.error(err));
CORS Issues with JSONPlaceholder
  • JSONPlaceholder supports CORS, but if issues arise, use a proxy during development:
    json
    // angular.json
    "serve": {
      "options": {
        "proxyConfig": "proxy.conf.json"
      }
    }
    json
    // proxy.conf.json
    {
      "/users": {
        "target": "https://jsonplaceholder.typicode.com",
        "secure": true,
        "changeOrigin": true
      }
    }
Deployment Routing Issues
  • Ensure baseHref is set correctly for GitHub Pages.
  • Verify Firebase rewrites in firebase.json for SPA routing.

Best Practices
  • Environment Files: Use environment.ts for API URLs.
  • Services: Centralize API and auth logic.
  • Guards: Restrict routes based on roles.
  • Reusable Components: Keep header, footer, and sidebar consistent.
  • Forms: Use reactive forms for complex validation.
  • Lifecycle Hooks: Clean up subscriptions in ngOnDestroy.
  • Deployment: Test locally before deploying.

0 comments:

Featured Post

Master Angular 20 Basics: A Complete Beginner’s Guide with Examples and Best Practices

Welcome to the complete Angular 20 learning roadmap ! This series takes you step by step from basics to intermediate concepts , with hands...

Subscribe

 
Toggle Footer
Top