feat: implement logout functionality and enhance contact layout

This commit is contained in:
Bangara Raju Kottedi 2025-11-17 13:35:33 +05:30
parent 38f305067a
commit e83e2b2161
13 changed files with 205 additions and 133 deletions

View File

@ -114,5 +114,12 @@
</ul>
</div>
<hr class="logout-divider" />
<div class="logout-section">
<button class="logout-btn" (click)="logout()">
<span class="icon">🔓</span>
<span>Logout</span>
</button>
</div>
</aside>

View File

@ -1,3 +1,62 @@
img {
border-radius: inherit;
}
.logout-section {
margin-top: 14px;
padding-bottom: 8px;
display: flex;
justify-content: center;
}
.logout-btn {
width: 90%;
margin: 0 auto;
padding: 14px 18px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
background: #191919; // darker for better contrast
border: 1px solid rgba(227, 179, 65, 0.18); // subtle gold border
border-radius: 14px;
cursor: pointer;
color: #e3b341; // gold text
font-size: 15px;
font-weight: 500;
transition: 0.3s ease;
/* Subtle depth */
box-shadow:
inset 0 0 8px rgba(255, 255, 255, 0.03),
0 4px 18px rgba(0, 0, 0, 0.45);
&:hover {
background: rgba(227, 179, 65, 0.12);
border-color: rgba(227, 179, 65, 0.35);
box-shadow:
0 0 12px rgba(227, 179, 65, 0.25),
inset 0 0 10px rgba(227, 179, 65, 0.1);
color: #fff;
}
}
.icon {
font-size: 18px;
line-height: 0;
}
.logout-divider {
border: 0;
height: 1px;
background: rgba(255, 255, 255, 0.06);
margin: 18px 0 14px 0;
}

View File

@ -3,6 +3,7 @@ import { IContactModel } from './contact.model';
import { AdminService } from '../services/admin.service';
import { BaseComponent } from '../base.component';
import { CommonModule } from '@angular/common';
import { AuthService } from '../../auth/auth.service';
@Component({
selector: 'app-contact',
@ -13,6 +14,7 @@ import { CommonModule } from '@angular/common';
export class Contact extends BaseComponent<IContactModel> implements OnInit {
sideBarExpanded = false;
displayName!: string;
authSvc: AuthService = inject(AuthService);
constructor(){
const svc = inject(AdminService);
super(svc);
@ -28,4 +30,8 @@ export class Contact extends BaseComponent<IContactModel> implements OnInit {
this.assignData(response);
});
}
logout() {
this.authSvc.logout();
}
}

View File

@ -56,9 +56,9 @@
<a>
<figure class="project-img">
<!-- <div class="project-item-icon-box">
<div class="project-item-icon-box">
<ion-icon name="eye-outline"></ion-icon>
</div> -->
</div>
<img src="{{imagesOrigin + project.imagePath}}" alt="{{project.name}}" loading="lazy">
</figure>

View File

@ -1,4 +1,4 @@
import { Component, inject, OnInit } from '@angular/core';
import { Component, CUSTOM_ELEMENTS_SCHEMA, inject, OnInit } from '@angular/core';
import { IProject } from '../models/project.model';
import { BaseComponent } from '../base.component';
import { AdminService } from '../services/admin.service';
@ -10,7 +10,8 @@ import { CommonModule } from '@angular/common';
selector: 'app-projects',
templateUrl: './projects.html',
styleUrl: './projects.scss',
imports: [CommonModule]
imports: [CommonModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class Projects extends BaseComponent<IProjects> implements OnInit {
filter = 'All';

View File

@ -4,13 +4,12 @@ import { routes } from './app.routes';
import { provideHttpClient, withFetch, withInterceptors} from '@angular/common/http';
import { loadingInterceptor } from './interceptors/loading-interceptor';
import { AuthInterceptor } from './interceptors/auth-interceptor';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideHttpClient(withInterceptors([loadingInterceptor, AuthInterceptor]), withFetch()),
provideRouter(routes), provideClientHydration(withEventReplay())
provideRouter(routes)
]
};

View File

@ -19,6 +19,11 @@ export const routes: Routes = [
redirectTo: 'admin/about',
pathMatch: 'full'
},
{
path: 'admin',
redirectTo: 'admin/about',
pathMatch: 'full'
},
{
path: 'admin',
component: AdminLayout,

View File

@ -25,6 +25,7 @@ export class AuthService {
router = inject(Router);
private readonly storageKey = 'accessToken';
tokenReady$ = new BehaviorSubject<boolean | null>(null);
constructor() {
console.log('🔥 AuthService constructor started');
@ -36,31 +37,31 @@ export class AuthService {
return `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
}
// // Call on app start, or from guard
// async ensureTokenReady(): Promise<void>{
// if(this.tokenReady$.value) return;
// Call on app start, or from guard
async ensureTokenReady(): Promise<void>{
if(this.tokenReady$.value) return;
// const stored = this.safeGetToken();
const stored = this.safeGetToken();
// // try to restore from storage
// if(stored){
// this.accessTokenSub.next(stored);
// try to restore from storage
if(stored){
this.accessTokenSub.next(stored);
this.tokenReady$.next(true);
return;
}
// // Optionally: try a silent refresh on startup to restore session using HttpOnly cookie
// try {
// const res = await firstValueFrom(this.refreshToken());
// this.safeSetToken(res.accessToken);
// }
// catch{
// console.warn('Silent token refresh failed on startup');
// }
// finally{
// this.tokenReady$.next(true);
// return;
// }
// // // Optionally: try a silent refresh on startup to restore session using HttpOnly cookie
// // try {
// // const res = await firstValueFrom(this.refreshToken());
// // this.safeSetToken(res.accessToken);
// // }
// // catch{
// // console.warn('Silent token refresh failed on startup');
// // }
// // finally{
// // this.tokenReady$.next(true);
// // }
// }
}
get currentToken(): string | null {
return this.safeGetToken();
@ -123,7 +124,7 @@ export class AuthService {
logout(): Observable<void> {
this.accessToken = null;
this.safeRemoveToken();
this.router.navigate(['/login']);
this.router.navigate(['/admin/login']);
return of();
}

View File

@ -1,7 +1,13 @@
<div class="verify-wrapper">
<div class="verify-card">
@if(!isOtpSent()){
<h2 class="title">🔐 Login</h2>
}
@if(isOtpSent() && !isVerified()){
<h2 class="title">🔐 OTP Verification</h2>
}
<!-- Step 1: Enter Email -->
@if (!isOtpSent()) {
@ -35,17 +41,5 @@
</button>
</div>
}
<!-- Step 3: Success -->
<!-- @if (isVerified()) {
<div>
<p class="success-msg">{{ message() }}✅</p>
</div>
}
@if (isError()) {
<p class="error-message">{{ message() }}</p>
} -->
</div>
</div>

View File

@ -9,13 +9,14 @@ import {
import { BehaviorSubject, catchError, filter, Observable, switchMap, take, throwError } from 'rxjs';
import { AuthService } from '../auth/auth.service';
let refreshInProgress = false;
const refreshSubject = new BehaviorSubject<string | null>(null);
export const AuthInterceptor: HttpInterceptorFn = (
req: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
let refreshInProgress = false;
const refreshSubject = new BehaviorSubject<string | null>(null);
const authSvc: AuthService = inject(AuthService);
let headers = req.headers;
// add API key

View File

@ -1,11 +1,10 @@
@if(!loggedIn()){
<router-outlet></router-outlet>
} @else {
<main>
@if(loggedIn()){
<app-contact></app-contact>
}
<div class="main-content">
@if(loggedIn()){
<nav class="navbar">
<ul class="navbar-list">
<li class="navbar-item">
@ -22,11 +21,12 @@
</li>
</ul>
</nav>
}
<div class="page-container">
<router-outlet></router-outlet>
</div>
</div>
</main>
}

View File

@ -17,6 +17,5 @@ export class AdminLayout {
this.authSvc.accessTokenSub.subscribe(token => {
this.loggedIn.set(token !== null);
});
console.log("AdminLayout constructed");
}
}

View File

@ -321,7 +321,7 @@
transition: var(--transition-2);
}
.sidebar.active { max-height: 405px; }
.sidebar.active { max-height: 505px; }
.sidebar-info {
position: relative;