Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/app/core/animations/fade.in-out.animation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { animate, style, transition, trigger } from '@angular/animations';

/**
* Angular animation trigger for fading elements in and out.
*
* This trigger can be used with Angular structural directives like `*ngIf` or `@if`
* to smoothly animate the appearance and disappearance of components or elements.
*
* ## Usage:
*
* In the component decorator:
* ```ts
* @Component({
* selector: 'my-component',
* templateUrl: './my.component.html',
* animations: [fadeInOut]
* })
* export class MyComponent {}
* ```
*
* In the template:
* ```html
* @if (show) {
* <div @fadeInOut>
* Fades in and out!
* </div>
* }
* ```
*
* ## Transitions:
* - **:enter** — Fades in from opacity `0` to `1` over `200ms`.
* - **:leave** — Fades out from opacity `1` to `0` over `200ms`.
*
* @returns An Angular `AnimationTriggerMetadata` object used for component animations.
*/
export const fadeInOutAnimation = trigger('fadeInOut', [
transition(':enter', [style({ opacity: 0 }), animate('200ms', style({ opacity: 1 }))]),
transition(':leave', [animate('200ms', style({ opacity: 0 }))]),
]);
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';

import { BreadcrumbComponent } from './breadcrumb.component';

describe('BreadcrumbComponent', () => {
describe('Component: Breadcrumb', () => {
let component: BreadcrumbComponent;
let fixture: ComponentFixture<BreadcrumbComponent>;

Expand Down
2 changes: 1 addition & 1 deletion src/app/core/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { BreadcrumbComponent } from './breadcrumb/breadcrumb.component';
export { FooterComponent } from './footer/footer.component';
export { HeaderComponent } from './header/header.component';
export { MaintenanceBannerComponent } from './maintenance-banner/maintenance-banner.component';
export { MaintenanceBannerComponent } from './osf-banners/maintenance-banner/maintenance-banner.component';
export { PageNotFoundComponent } from './page-not-found/page-not-found.component';
export { RootComponent } from './root/root.component';
export { SidenavComponent } from './sidenav/sidenav.component';
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
@if (maintenance && !dismissed) {
@if (maintenance() && !dismissed()) {
<p-message
styleClass="w-full"
[severity]="maintenance.severity"
[text]="maintenance.message"
[severity]="maintenance()?.severity"
[text]="maintenance()?.message"
[closable]="true"
(onClose)="dismiss()"
class="block p-3"
icon="pi pi-info-circle"
@fadeInOut
>
</p-message>
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';

import { MaintenanceBannerComponent } from './maintenance-banner.component';

describe('MaintenanceBannerComponent', () => {
describe('Component: Maintenance Banner', () => {
let fixture: ComponentFixture<MaintenanceBannerComponent>;
let httpClient: { get: jest.Mock };
let cookieService: jest.Mocked<CookieService>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { CookieService } from 'ngx-cookie-service';

import { MessageModule } from 'primeng/message';

import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';

import { fadeInOutAnimation } from '@core/animations/fade.in-out.animation';

import { MaintenanceModel } from '../models/maintenance.model';
import { MaintenanceService } from '../services/maintenance.service';

/**
* A banner component that displays a scheduled maintenance message to users.
*
* This component checks a cookie to determine whether the user has previously dismissed
* the banner. If not, it queries the maintenance status from the server and displays
* the maintenance message if one is active.
*
* The component supports animation via `fadeInOutAnimation` and is optimized with `OnPush` change detection.
*
* @example
* ```html
* <osf-maintenance-banner />
* ```
*/
@Component({
selector: 'osf-maintenance-banner',
imports: [CommonModule, MessageModule],
templateUrl: './maintenance-banner.component.html',
styleUrls: ['./maintenance-banner.component.scss'],
animations: [fadeInOutAnimation],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MaintenanceBannerComponent implements OnInit {
/**
* Signal to track whether the user has dismissed the banner.
*/
dismissed = signal<boolean>(false);

/**
* Signal that holds the current maintenance status fetched from the server.
*/
maintenance = signal<MaintenanceModel | null>(null);

/**
* Service used to fetch maintenance status from the backend.
*/
private readonly maintenanceService = inject(MaintenanceService);

/**
* Cookie service used to persist dismissal state in the browser.
*/
private readonly cookies = inject(CookieService);

/**
* The cookie name used to store whether the user dismissed the banner.
*/
private readonly cookieName = 'osf-maintenance-dismissed';

/**
* Duration (in hours) to persist the dismissal cookie.
*/
private readonly cookieDurationHours = 24;

/**
* Lifecycle hook that initializes the component:
* - Checks if dismissal cookie exists and sets `dismissed`
* - If not dismissed, triggers a fetch of current maintenance status
*/
ngOnInit(): void {
this.dismissed.set(this.cookies.check(this.cookieName));
if (!this.dismissed()) {
this.fetchMaintenanceStatus();
}
}

/**
* Fetches the current maintenance status from the backend via the `MaintenanceService`
* and sets it to the `maintenance` signal.
*
* If no maintenance is active, the signal remains `null`.
*/
private fetchMaintenanceStatus(): void {
this.maintenanceService.fetchMaintenanceStatus().subscribe((maintenance: MaintenanceModel | null) => {
this.maintenance.set(maintenance);
});
}

/**
* Dismisses the banner:
* - Sets a cookie to remember dismissal for 24 hours
* - Updates the `dismissed` and `maintenance` signals
*/
dismiss(): void {
this.cookies.set(this.cookieName, '1', this.cookieDurationHours, '/');
this.dismissed.set(true);
this.maintenance.set(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type MaintenanceSeverityType = 'info' | 'warn' | 'error';

export interface MaintenanceModel {
level: number;
message: string;
start: string;
end: string;
severity?: MaintenanceSeverityType;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<osf-maintenance-banner></osf-maintenance-banner>
29 changes: 29 additions & 0 deletions src/app/core/components/osf-banners/osf-banner.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

import { OSFBannerComponent } from './osf-banner.component';

import { OSFTestingModule } from '@testing/osf.testing.module';
import { MockComponentWithSignal } from '@testing/providers/component-provider.mock';

describe('Component: OSF Banner', () => {
let fixture: ComponentFixture<OSFBannerComponent>;
let component: OSFBannerComponent;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
OSFTestingModule,
OSFBannerComponent,
NoopAnimationsModule,
MockComponentWithSignal('osf-maintenance-banner'),
],
}).compileComponents();

fixture = TestBed.createComponent(OSFBannerComponent);
component = fixture.componentInstance;
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
});
26 changes: 26 additions & 0 deletions src/app/core/components/osf-banners/osf-banner.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { MaintenanceBannerComponent } from './maintenance-banner/maintenance-banner.component';

/**
* Wrapper component responsible for rendering all global or conditional banners.
*
* Currently, it includes the `MaintenanceBannerComponent`, which displays scheduled
* maintenance notices based on server configuration and cookie state.
*
* This component is structured to allow future expansion for additional banners (e.g., announcements, alerts).
*
* Change detection is set to `OnPush` to improve performance.
*
* @example
* ```html
* <osf-banner-component />
* ```
*/
@Component({
selector: 'osf-banner-component',
imports: [MaintenanceBannerComponent],
templateUrl: './osf-banner.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OSFBannerComponent {}
Loading