diff --git a/src/app/app.routes.spec.ts b/src/app/app.routes.spec.ts
index 6d2917f..b864ad5 100644
--- a/src/app/app.routes.spec.ts
+++ b/src/app/app.routes.spec.ts
@@ -15,6 +15,7 @@ const tenant: Tenant = {
primary_color: '#6376F3',
secondary_color: '#A0A0A0',
danger_color: '#FF8888',
+ success_color: '#198754',
header_footer_bg_color: '#313131',
header_logo: 'https://example.com/header.png',
footer_logo: 'https://example.com/footer.png'
diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts
index 72268d3..0547b4b 100644
--- a/src/app/app.spec.ts
+++ b/src/app/app.spec.ts
@@ -14,6 +14,7 @@ const tenant: Tenant = {
primary_color: '#6376F3',
secondary_color: '#A0A0A0',
danger_color: '#FF8888',
+ success_color: '#198754',
header_footer_bg_color: '#313131',
header_logo: 'https://example.com/header.png',
footer_logo: 'https://example.com/footer.png'
diff --git a/src/app/app.ts b/src/app/app.ts
index 56774b9..9a7ee44 100644
--- a/src/app/app.ts
+++ b/src/app/app.ts
@@ -3,6 +3,7 @@ import { RouterOutlet } from '@angular/router';
import { TenantService } from './core/services/tenant.service';
import { DEFAULT_TENANT_BRANDING } from './core/services/tenant-ssr-cache.store';
+import { ToastContainerComponent } from './shared/components/toast-container/toast-container.component';
function hexToRgb(hex: string): string {
const cleanHex = hex.replace('#', '').trim();
@@ -21,16 +22,18 @@ function hexToRgb(hex: string): string {
@Component({
selector: 'app-root',
- imports: [RouterOutlet],
+ imports: [RouterOutlet, ToastContainerComponent],
templateUrl: './app.html',
styleUrl: './app.scss',
host: {
'[style.--tenant-primary]': 'tenantPrimaryColor()',
'[style.--tenant-secondary]': 'tenantSecondaryColor()',
'[style.--tenant-danger]': 'tenantDangerColor()',
+ '[style.--tenant-success]': 'tenantSuccessColor()',
'[style.--tenant-primary-rgb]': 'tenantPrimaryColorRgb()',
'[style.--tenant-secondary-rgb]': 'tenantSecondaryColorRgb()',
'[style.--tenant-danger-rgb]': 'tenantDangerColorRgb()',
+ '[style.--tenant-success-rgb]': 'tenantSuccessColorRgb()',
'[style.--tenant-header-footer-bg]': 'tenantHeaderFooterBgColor()'
}
})
@@ -48,6 +51,9 @@ export class App {
protected readonly tenantDangerColor = computed(
() => this.tenantService.tenant()?.danger_color ?? DEFAULT_TENANT_BRANDING.dangerColor
);
+ protected readonly tenantSuccessColor = computed(
+ () => this.tenantService.tenant()?.success_color ?? DEFAULT_TENANT_BRANDING.successColor
+ );
protected readonly tenantPrimaryColorRgb = computed(() =>
hexToRgb(this.tenantPrimaryColor())
);
@@ -57,6 +63,9 @@ export class App {
protected readonly tenantDangerColorRgb = computed(() =>
hexToRgb(this.tenantDangerColor())
);
+ protected readonly tenantSuccessColorRgb = computed(() =>
+ hexToRgb(this.tenantSuccessColor())
+ );
protected readonly tenantHeaderFooterBgColor = computed(
() =>
this.tenantService.tenant()?.header_footer_bg_color ??
diff --git a/src/app/core/layout/store-layout/store-layout.component.spec.ts b/src/app/core/layout/store-layout/store-layout.component.spec.ts
index 1f3e904..c0d04a6 100644
--- a/src/app/core/layout/store-layout/store-layout.component.spec.ts
+++ b/src/app/core/layout/store-layout/store-layout.component.spec.ts
@@ -14,6 +14,7 @@ const tenant: Tenant = {
primary_color: '#6376F3',
secondary_color: '#A0A0A0',
danger_color: '#FF8888',
+ success_color: '#198754',
header_footer_bg_color: '#313131',
header_logo: 'https://example.com/header.png',
footer_logo: 'https://example.com/footer.png'
diff --git a/src/app/core/services/tenant-ssr-cache.store.ts b/src/app/core/services/tenant-ssr-cache.store.ts
index 417dbc8..fcb5688 100644
--- a/src/app/core/services/tenant-ssr-cache.store.ts
+++ b/src/app/core/services/tenant-ssr-cache.store.ts
@@ -17,6 +17,7 @@ export const DEFAULT_TENANT_BRANDING = {
primaryColor: '#102bda',
secondaryColor: '#9e9e9e',
dangerColor: '#fd4c4c',
+ successColor: '#198754',
headerFooterBgColor: '#313131'
} as const;
diff --git a/src/app/core/services/tenant.interface.ts b/src/app/core/services/tenant.interface.ts
index f884e5c..11801d9 100644
--- a/src/app/core/services/tenant.interface.ts
+++ b/src/app/core/services/tenant.interface.ts
@@ -8,6 +8,7 @@ export interface Tenant {
primary_color: string;
secondary_color: string;
danger_color: string;
+ success_color: string;
header_footer_bg_color: string;
header_logo: string;
footer_logo: string;
diff --git a/src/app/core/services/tenant.service.spec.ts b/src/app/core/services/tenant.service.spec.ts
index 3695597..158fafc 100644
--- a/src/app/core/services/tenant.service.spec.ts
+++ b/src/app/core/services/tenant.service.spec.ts
@@ -16,6 +16,7 @@ const tenant: Tenant = {
primary_color: '#6376F3',
secondary_color: '#A0A0A0',
danger_color: '#FF8888',
+ success_color: '#198754',
header_footer_bg_color: '#313131',
header_logo: 'https://example.com/header.png',
footer_logo: 'https://example.com/footer.png'
diff --git a/src/app/core/services/toast.service.spec.ts b/src/app/core/services/toast.service.spec.ts
new file mode 100644
index 0000000..cb28a55
--- /dev/null
+++ b/src/app/core/services/toast.service.spec.ts
@@ -0,0 +1,101 @@
+import { TestBed } from '@angular/core/testing';
+import { vi } from 'vitest';
+import { ToastService } from './toast.service';
+
+describe('ToastService', () => {
+ let service: ToastService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [ToastService]
+ });
+ service = TestBed.inject(ToastService);
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should initialize with an empty list of toasts', () => {
+ expect(service.toasts()).toEqual([]);
+ });
+
+ it('should show a toast and assign an id', () => {
+ const id = service.show('Hello world', 'info');
+ expect(id).toBeTruthy();
+ expect(service.toasts().length).toBe(1);
+ expect(service.toasts()[0]).toEqual({
+ id,
+ message: 'Hello world',
+ type: 'info',
+ duration: 3000
+ });
+ });
+
+ it('should add specific variant toasts via helper methods', () => {
+ const infoId = service.info('Info message');
+ const dangerId = service.danger('Danger message', 5000);
+ const successId = service.success('Success message', 0);
+
+ const toasts = service.toasts();
+ expect(toasts.length).toBe(3);
+
+ expect(toasts.find((t) => t.id === infoId)).toEqual({
+ id: infoId,
+ message: 'Info message',
+ type: 'info',
+ duration: 3000
+ });
+
+ expect(toasts.find((t) => t.id === dangerId)).toEqual({
+ id: dangerId,
+ message: 'Danger message',
+ type: 'danger',
+ duration: 5000
+ });
+
+ expect(toasts.find((t) => t.id === successId)).toEqual({
+ id: successId,
+ message: 'Success message',
+ type: 'success',
+ duration: 0
+ });
+ });
+
+ it('should dismiss a toast manually by id', () => {
+ const id1 = service.show('Toast 1');
+ const id2 = service.show('Toast 2');
+
+ expect(service.toasts().length).toBe(2);
+
+ service.dismiss(id1);
+
+ expect(service.toasts().length).toBe(1);
+ expect(service.toasts()[0].id).toBe(id2);
+ });
+
+ it('should automatically dismiss a toast after the duration', () => {
+ service.show('Auto dismiss toast', 'info', 2000);
+
+ expect(service.toasts().length).toBe(1);
+
+ vi.advanceTimersByTime(2000);
+
+ expect(service.toasts().length).toBe(0);
+ });
+
+ it('should not dismiss a toast automatically if duration is 0', () => {
+ service.show('Persistent toast', 'info', 0);
+
+ expect(service.toasts().length).toBe(1);
+
+ vi.advanceTimersByTime(10000);
+
+ expect(service.toasts().length).toBe(1);
+ });
+});
diff --git a/src/app/core/services/toast.service.ts b/src/app/core/services/toast.service.ts
new file mode 100644
index 0000000..bcdd4e4
--- /dev/null
+++ b/src/app/core/services/toast.service.ts
@@ -0,0 +1,47 @@
+import { Injectable, signal } from '@angular/core';
+
+export interface Toast {
+ id: string;
+ message: string;
+ type: 'success' | 'danger' | 'info';
+ duration?: number;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ToastService {
+ private readonly toastsSignal = signal
([]);
+ readonly toasts = this.toastsSignal.asReadonly();
+
+ show(message: string, type: 'success' | 'danger' | 'info' = 'info', duration = 3000): string {
+ const id = Math.random().toString(36).substring(2, 9);
+ const newToast: Toast = { id, message, type, duration };
+
+ this.toastsSignal.update((toasts) => [...toasts, newToast]);
+
+ if (duration > 0) {
+ setTimeout(() => {
+ this.dismiss(id);
+ }, duration);
+ }
+
+ return id;
+ }
+
+ info(message: string, duration = 3000): string {
+ return this.show(message, 'info', duration);
+ }
+
+ danger(message: string, duration = 3000): string {
+ return this.show(message, 'danger', duration);
+ }
+
+ success(message: string, duration = 3000): string {
+ return this.show(message, 'success', duration);
+ }
+
+ dismiss(id: string): void {
+ this.toastsSignal.update((toasts) => toasts.filter((t) => t.id !== id));
+ }
+}
diff --git a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html
index 37111d3..b81c780 100644
--- a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html
+++ b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html
@@ -24,6 +24,7 @@
+
+