From 2cbe681995998f61bca58cdd90735172ee36c2dd Mon Sep 17 00:00:00 2001 From: Nahuel Eghar Coronel Date: Sun, 28 Jun 2026 23:09:26 +0000 Subject: [PATCH] refactor: migrate tenant bootstrap to use SSR state caching and update button components to use CSS tenant variables --- src/app/app.config.ts | 4 +- src/app/app.html | 18 +- src/app/app.routes.server.ts | 2 +- src/app/app.routes.spec.ts | 68 ++++++- src/app/app.routes.ts | 6 +- src/app/app.scss | 65 ++++++ src/app/app.spec.ts | 85 +++++++- src/app/app.ts | 62 +++++- .../store-footer/store-footer.component.html | 10 +- .../store-footer/store-footer.component.scss | 13 +- .../store-footer/store-footer.component.ts | 19 +- .../store-header/store-header.component.html | 22 +- .../store-header/store-header.component.scss | 62 +++++- .../store-header/store-header.component.ts | 19 +- .../store-layout/store-layout.component.scss | 8 +- .../store-layout.component.spec.ts | 39 +++- .../core/services/tenant-bootstrap.spec.ts | 13 +- src/app/core/services/tenant-bootstrap.ts | 2 +- .../core/services/tenant-ssr-cache.store.ts | 23 +++ src/app/core/services/tenant.service.spec.ts | 191 +++++++++++++++--- src/app/core/services/tenant.service.ts | 151 ++++++++++++-- .../reutilizables-test-page.component.html | 80 ++++++++ .../reutilizables-test-page.component.scss | 112 ++++++++++ .../reutilizables-test-page.component.spec.ts | 66 ++++++ .../reutilizables-test-page.component.ts | 5 +- .../components/button/button.component.scss | 80 +++++--- src/styles.scss | 6 + 27 files changed, 1042 insertions(+), 189 deletions(-) create mode 100644 src/app/core/services/tenant-ssr-cache.store.ts create mode 100644 src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.spec.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index e0d9b22..ec3a18a 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,4 +1,4 @@ -import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClient, withFetch } from '@angular/common/http'; import { ApplicationConfig, provideAppInitializer, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideClientHydration } from '@angular/platform-browser'; @@ -11,7 +11,7 @@ export const appConfig: ApplicationConfig = { provideBrowserGlobalErrorListeners(), provideRouter(routes), provideClientHydration(), - provideHttpClient(), + provideHttpClient(withFetch()), provideAppInitializer(tenantBootstrap) ] }; diff --git a/src/app/app.html b/src/app/app.html index 67e7bd4..210e47b 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1 +1,17 @@ - +@if (status() === 'ready') { + +} @else if (status() === 'not-found') { +
+
+ 404 +

No encontramos una tienda para este dominio

+

+ Verifica la configuracion del tenant o intenta nuevamente con un dominio valido. +

+
+
+} @else { + +} diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts index ffd37b1..2c5a11d 100644 --- a/src/app/app.routes.server.ts +++ b/src/app/app.routes.server.ts @@ -3,6 +3,6 @@ import { RenderMode, ServerRoute } from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { path: '**', - renderMode: RenderMode.Prerender + renderMode: RenderMode.Server } ]; diff --git a/src/app/app.routes.spec.ts b/src/app/app.routes.spec.ts index 110d968..e4ada11 100644 --- a/src/app/app.routes.spec.ts +++ b/src/app/app.routes.spec.ts @@ -1,15 +1,55 @@ +import { signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { provideRouter, Router } from '@angular/router'; import { App } from './app'; import { routes } from './app.routes'; +import { Tenant } from './core/services/tenant.interface'; +import { TenantService } from './core/services/tenant.service'; -async function renderAppAt(url: string) { +const tenant: Tenant = { + id: 1, + codigo: 'test', + nombre: 'Test Tenant', + dominio: 'localhost', + primary_color: '#6376F3', + secondary_color: '#A0A0A0', + danger_color: '#FF8888', + header_footer_bg_color: '#313131', + header_logo: 'https://example.com/header.png', + footer_logo: 'https://example.com/footer.png' +}; + +function createTenantServiceStub( + status: 'ready' | 'not-found' = 'ready', + currentTenant: Tenant | null = tenant +) { + const tenantState = signal(currentTenant); + const statusState = signal(status); + + return { + tenant: tenantState.asReadonly(), + status: statusState.asReadonly(), + getTenant: () => tenantState(), + bootstrap: vi.fn().mockResolvedValue(undefined) + }; +} + +async function renderAppAt( + url: string, + tenantService: ReturnType = createTenantServiceStub() +) { TestBed.resetTestingModule(); await TestBed.configureTestingModule({ imports: [App], - providers: [provideRouter(routes)] + providers: [ + provideRouter(routes), + { + provide: TenantService, + useValue: tenantService + } + ] }).compileComponents(); const router = TestBed.inject(Router); @@ -24,21 +64,37 @@ async function renderAppAt(url: string) { } describe('app routes', () => { - it('loads the store layout at /store', async () => { - const { fixture, router } = await renderAppAt('/store'); + it('loads the store layout at /', async () => { + const { fixture, router } = await renderAppAt('/'); const compiled = fixture.nativeElement as HTMLElement; - expect(router.url).toBe('/store'); + expect(router.url).toBe('/'); expect(compiled.querySelector('.store-layout')).not.toBeNull(); expect(compiled.querySelector('.store-home')).not.toBeNull(); expect(compiled.textContent).toContain('Tienda en construccion'); + expect(compiled.style.getPropertyValue('--tenant-primary')).toBe(tenant.primary_color); }); - it('keeps the root redirect under componentes-test', async () => { + it('loads componentes-test under a valid tenant', async () => { const { fixture, router } = await renderAppAt('/'); + await router.navigateByUrl('/componentes-test/reutilizables'); + await fixture.whenStable(); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; expect(router.url).toBe('/componentes-test/reutilizables'); expect(compiled.textContent).toContain('Componentes reutilizables'); }); + + it('renders the tenant not found screen for any route when the tenant is missing', async () => { + const { fixture } = await renderAppAt( + '/componentes-test/reutilizables', + createTenantServiceStub('not-found', null) + ); + const compiled = fixture.nativeElement as HTMLElement; + + expect(compiled.querySelector('.tenant-status')).not.toBeNull(); + expect(compiled.textContent).toContain('No encontramos una tienda para este dominio'); + }); }); diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 4d3c5b6..1bf87ad 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -8,11 +8,11 @@ export const routes: Routes = [ }, { path: 'store', - loadChildren: () => import('./features/store/store.routes').then((m) => m.routes) + pathMatch: 'full', + redirectTo: '' }, { path: '', - pathMatch: 'full', - redirectTo: 'componentes-test' + loadChildren: () => import('./features/store/store.routes').then((m) => m.routes) } ]; diff --git a/src/app/app.scss b/src/app/app.scss index 5d4e87f..e54216c 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -1,3 +1,68 @@ :host { display: block; + min-height: 100dvh; +} + +.tenant-status { + min-height: 100dvh; + display: grid; + place-items: center; + padding: 2rem; + background: + radial-gradient(circle at top, color-mix(in srgb, white 82%, var(--tenant-primary)) 0%, transparent 55%), + linear-gradient(180deg, #f8f8f8 0%, #efefef 100%); + color: #202020; +} + +.tenant-status__card { + width: min(100%, 34rem); + padding: 2rem; + border: 1px solid rgba(32, 32, 32, 0.1); + border-radius: 1.5rem; + background-color: rgba(255, 255, 255, 0.92); + box-shadow: 0 1.25rem 3rem rgba(0, 0, 0, 0.1); +} + +.tenant-status__code { + display: inline-block; + margin-bottom: 1rem; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.24em; + text-transform: uppercase; + color: var(--tenant-primary); +} + +.tenant-status__title { + margin: 0 0 0.75rem; + font-size: clamp(2rem, 4vw, 2.75rem); + line-height: 1.05; +} + +.tenant-status__description { + margin: 0; + font-size: 1rem; + line-height: 1.6; + color: rgba(32, 32, 32, 0.72); +} + +.tenant-status--loading { + background: + radial-gradient(circle at center, color-mix(in srgb, white 88%, var(--tenant-primary)) 0%, transparent 60%), + #f5f5f5; +} + +.tenant-status__spinner { + width: 3rem; + height: 3rem; + border: 0.25rem solid rgba(32, 32, 32, 0.08); + border-top-color: var(--tenant-primary); + border-radius: 999px; + animation: tenant-status-spin 0.8s linear infinite; +} + +@keyframes tenant-status-spin { + to { + transform: rotate(360deg); + } } diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts index d26bfac..72268d3 100644 --- a/src/app/app.spec.ts +++ b/src/app/app.spec.ts @@ -1,23 +1,88 @@ +import { signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; + import { App } from './app'; +import { Tenant } from './core/services/tenant.interface'; +import { TenantService } from './core/services/tenant.service'; + +const tenant: Tenant = { + id: 1, + codigo: 'test', + nombre: 'Test Tenant', + dominio: 'localhost', + primary_color: '#6376F3', + secondary_color: '#A0A0A0', + danger_color: '#FF8888', + header_footer_bg_color: '#313131', + header_logo: 'https://example.com/header.png', + footer_logo: 'https://example.com/footer.png' +}; + +function createTenantServiceStub(status: 'ready' | 'not-found', currentTenant: Tenant | null) { + const tenantState = signal(currentTenant); + const statusState = signal(status); + + return { + tenant: tenantState.asReadonly(), + status: statusState.asReadonly(), + getTenant: () => tenantState(), + bootstrap: vi.fn().mockResolvedValue(undefined) + }; +} describe('App', () => { - beforeEach(async () => { + it('creates the app when the tenant is ready', async () => { await TestBed.configureTestingModule({ imports: [App], + providers: [ + provideRouter([]), + { + provide: TenantService, + useValue: createTenantServiceStub('ready', tenant) + } + ] }).compileComponents(); + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.componentInstance).toBeTruthy(); + expect(fixture.nativeElement.style.getPropertyValue('--tenant-primary')).toBe( + tenant.primary_color + ); + expect(fixture.nativeElement.style.getPropertyValue('--tenant-secondary')).toBe( + tenant.secondary_color + ); + expect(fixture.nativeElement.style.getPropertyValue('--tenant-danger')).toBe( + tenant.danger_color + ); + expect(fixture.nativeElement.style.getPropertyValue('--tenant-primary-rgb')).toBe( + '99, 118, 243' + ); + expect(fixture.nativeElement.style.getPropertyValue('--tenant-secondary-rgb')).toBe( + '160, 160, 160' + ); + expect(fixture.nativeElement.style.getPropertyValue('--tenant-danger-rgb')).toBe( + '255, 136, 136' + ); }); - it('should create the app', () => { - const fixture = TestBed.createComponent(App); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); - }); + it('renders the tenant not found screen when the tenant is missing', async () => { + await TestBed.configureTestingModule({ + imports: [App], + providers: [ + provideRouter([]), + { + provide: TenantService, + useValue: createTenantServiceStub('not-found', null) + } + ] + }).compileComponents(); - it('should render title', async () => { const fixture = TestBed.createComponent(App); - await fixture.whenStable(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('router-outlet')).not.toBeNull(); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toContain('No encontramos una tienda para este dominio'); }); }); diff --git a/src/app/app.ts b/src/app/app.ts index fb4c9d5..56774b9 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,10 +1,66 @@ -import { Component } from '@angular/core'; +import { Component, computed, inject } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { TenantService } from './core/services/tenant.service'; +import { DEFAULT_TENANT_BRANDING } from './core/services/tenant-ssr-cache.store'; + +function hexToRgb(hex: string): string { + const cleanHex = hex.replace('#', '').trim(); + let r = 0, g = 0, b = 0; + if (cleanHex.length === 3) { + r = parseInt(cleanHex[0] + cleanHex[0], 16); + g = parseInt(cleanHex[1] + cleanHex[1], 16); + b = parseInt(cleanHex[2] + cleanHex[2], 16); + } else if (cleanHex.length >= 6) { + r = parseInt(cleanHex.slice(0, 2), 16); + g = parseInt(cleanHex.slice(2, 4), 16); + b = parseInt(cleanHex.slice(4, 6), 16); + } + return `${r}, ${g}, ${b}`; +} + @Component({ selector: 'app-root', imports: [RouterOutlet], templateUrl: './app.html', - styleUrl: './app.scss' + styleUrl: './app.scss', + host: { + '[style.--tenant-primary]': 'tenantPrimaryColor()', + '[style.--tenant-secondary]': 'tenantSecondaryColor()', + '[style.--tenant-danger]': 'tenantDangerColor()', + '[style.--tenant-primary-rgb]': 'tenantPrimaryColorRgb()', + '[style.--tenant-secondary-rgb]': 'tenantSecondaryColorRgb()', + '[style.--tenant-danger-rgb]': 'tenantDangerColorRgb()', + '[style.--tenant-header-footer-bg]': 'tenantHeaderFooterBgColor()' + } }) -export class App {} +export class App { + private readonly tenantService = inject(TenantService); + + protected readonly status = this.tenantService.status; + + protected readonly tenantPrimaryColor = computed( + () => this.tenantService.tenant()?.primary_color ?? DEFAULT_TENANT_BRANDING.primaryColor + ); + protected readonly tenantSecondaryColor = computed( + () => this.tenantService.tenant()?.secondary_color ?? DEFAULT_TENANT_BRANDING.secondaryColor + ); + protected readonly tenantDangerColor = computed( + () => this.tenantService.tenant()?.danger_color ?? DEFAULT_TENANT_BRANDING.dangerColor + ); + protected readonly tenantPrimaryColorRgb = computed(() => + hexToRgb(this.tenantPrimaryColor()) + ); + protected readonly tenantSecondaryColorRgb = computed(() => + hexToRgb(this.tenantSecondaryColor()) + ); + protected readonly tenantDangerColorRgb = computed(() => + hexToRgb(this.tenantDangerColor()) + ); + protected readonly tenantHeaderFooterBgColor = computed( + () => + this.tenantService.tenant()?.header_footer_bg_color ?? + DEFAULT_TENANT_BRANDING.headerFooterBgColor + ); +} + diff --git a/src/app/core/layout/store-layout/store-footer/store-footer.component.html b/src/app/core/layout/store-layout/store-footer/store-footer.component.html index ef60575..b672e3a 100644 --- a/src/app/core/layout/store-layout/store-footer/store-footer.component.html +++ b/src/app/core/layout/store-layout/store-footer/store-footer.component.html @@ -7,15 +7,11 @@ data-testid="store-footer-brand-slot" aria-label="Logo del comercio" > - @if (logoUrl && logoLoaded) { + @if (logoUrl) { } @else { } @@ -31,7 +27,7 @@ -

Copyright © {{ currentYear }} - Sonder

+

Copyright © {{ currentYear }} - Sonder

@for (section of footerSections; track section.heading) { @@ -39,7 +35,7 @@
@for (link of section.links; track link) { - {{ link }} + {{ link }} }
diff --git a/src/app/core/layout/store-layout/store-footer/store-footer.component.scss b/src/app/core/layout/store-layout/store-footer/store-footer.component.scss index 6eea43d..a3453f3 100644 --- a/src/app/core/layout/store-layout/store-footer/store-footer.component.scss +++ b/src/app/core/layout/store-layout/store-footer/store-footer.component.scss @@ -1,5 +1,5 @@ .store-layout__footer { - background-color: #323232; + background-color: var(--tenant-header-footer-bg); } .store-layout__brand-slot { @@ -20,13 +20,6 @@ object-fit: contain; } -.store-layout__brand-logo--preload { - opacity: 0; - position: absolute; - inset: 0; - pointer-events: none; -} - .store-layout__brand-logo-skeleton { display: block; width: 100%; @@ -69,6 +62,10 @@ color: #f8f8f8; } +.store-layout__muted-text { + color: rgba(255, 255, 255, 0.72); +} + @keyframes store-footer-logo-skeleton { from { background-position: 200% 0; diff --git a/src/app/core/layout/store-layout/store-footer/store-footer.component.ts b/src/app/core/layout/store-layout/store-footer/store-footer.component.ts index 4de54a7..febc6eb 100644 --- a/src/app/core/layout/store-layout/store-footer/store-footer.component.ts +++ b/src/app/core/layout/store-layout/store-footer/store-footer.component.ts @@ -17,25 +17,8 @@ export type StoreSocialLink = { styleUrl: './store-footer.component.scss' }) export class StoreFooterComponent { - private logoUrlValue: string | null = null; - @Input({ required: true }) currentYear = new Date().getFullYear(); @Input({ required: true }) footerSections: StoreFooterSection[] = []; @Input({ required: true }) socialLinks: StoreSocialLink[] = []; - - @Input() - set logoUrl(value: string | null) { - this.logoUrlValue = value; - this.logoLoaded = false; - } - - get logoUrl(): string | null { - return this.logoUrlValue; - } - - protected logoLoaded = false; - - protected onLogoLoad(): void { - this.logoLoaded = true; - } + @Input() logoUrl: string | null = null; } diff --git a/src/app/core/layout/store-layout/store-header/store-header.component.html b/src/app/core/layout/store-layout/store-header/store-header.component.html index 86abb35..35df4ba 100644 --- a/src/app/core/layout/store-layout/store-header/store-header.component.html +++ b/src/app/core/layout/store-layout/store-header/store-header.component.html @@ -1,20 +1,16 @@ -
-
+
+
- @if (logoUrl && logoLoaded) { + @if (logoUrl) { } @else { }
@@ -25,10 +21,14 @@ -
@@ -37,7 +37,7 @@ @for (action of headerActions; track action.label) {
+ +
+
+ Tenant Configuration +

Configuración Tenant

+ + +
+
+
+
Logo del Header
+ +
+
+ +
+
+
Logo del Footer
+ +
+
+
+ +
+ + +
+

Colores de Marca

+
+
+
+
+
+ Primario + {{ tenant()?.primary_color || 'Default' }} +
+
+
+ +
+
+
+
+ Secundario + {{ tenant()?.secondary_color || 'Default' }} +
+
+
+ +
+
+
+
+ Danger + {{ tenant()?.danger_color || 'Default' }} +
+
+
+
+
+
+
diff --git a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.scss b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.scss index 3f5ad41..092dfc1 100644 --- a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.scss +++ b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.scss @@ -70,6 +70,12 @@ h1 { gap: 0; } +.file-field { + display: grid; + gap: 0.75rem; +} + + .file-field { display: grid; gap: 0.75rem; @@ -86,3 +92,109 @@ h1 { grid-template-columns: minmax(0, 1fr); } } + +.configuracion-tennant { + margin-top: 2rem; + + .tenant-logo-card { + display: flex; + flex-direction: column; + gap: 0.5rem; + + &__label { + font-size: 0.875rem; + font-weight: 600; + color: #495057; + } + + &__container { + display: flex; + align-items: center; + justify-content: center; + height: 120px; + border-radius: 8px; + border: 1px solid #dee2e6; + padding: 1rem; + overflow: hidden; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05); + transition: transform 0.2s ease, box-shadow 0.2s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05), 0 4px 12px rgba(0, 0, 0, 0.08); + } + } + + &__img { + max-height: 100%; + max-width: 100%; + object-fit: contain; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1)); + } + + &__placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #6c757d; + font-size: 0.875rem; + + i { + font-size: 2rem; + opacity: 0.6; + } + } + } + + .header-footer-bg { + background-color: var(--tenant-header-footer-bg, #f8f9fa); + } + + .color-swatch-card { + display: flex; + flex-direction: column; + border-radius: 8px; + border: 1px solid #dee2e6; + overflow: hidden; + background-color: #ffffff; + transition: transform 0.2s ease, box-shadow 0.2s ease; + + &:hover { + transform: translateY(-3px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1) !important; + } + + &__preview { + height: 80px; + width: 100%; + border-bottom: 1px solid #dee2e6; + transition: opacity 0.2s ease; + + &:hover { + opacity: 0.95; + } + } + + &__body { + padding: 0.75rem 1rem; + display: flex; + flex-direction: column; + gap: 0.25rem; + } + + &__title { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 700; + color: #6c757d; + } + + &__value { + font-size: 0.875rem; + color: #212529; + font-family: var(--bs-font-monospace); + } + } +} diff --git a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.spec.ts b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.spec.ts new file mode 100644 index 0000000..25eb65e --- /dev/null +++ b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.spec.ts @@ -0,0 +1,66 @@ +import { signal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { ReutilizablesTestPageComponent } from './reutilizables-test-page.component'; +import { Tenant } from '../../../../core/services/tenant.interface'; +import { TenantService } from '../../../../core/services/tenant.service'; + +const tenant: Tenant = { + id: 1, + codigo: 'test', + nombre: 'Test Tenant', + dominio: 'localhost', + primary_color: '#6376F3', + secondary_color: '#A0A0A0', + danger_color: '#FF8888', + header_footer_bg_color: '#313131', + header_logo: 'https://example.com/header.png', + footer_logo: 'https://example.com/footer.png' +}; + +function createTenantServiceStub(currentTenant: Tenant | null) { + const tenantState = signal(currentTenant); + const statusState = signal<'ready' | 'not-found'>('ready'); + + return { + tenant: tenantState.asReadonly(), + status: statusState.asReadonly(), + getTenant: () => tenantState(), + bootstrap: vi.fn().mockResolvedValue(undefined) + }; +} + +describe('ReutilizablesTestPageComponent', () => { + it('renders the tenant configuration section with logo sources and swatch colors', async () => { + await TestBed.configureTestingModule({ + imports: [ReutilizablesTestPageComponent], + providers: [ + { + provide: TenantService, + useValue: createTenantServiceStub(tenant) + } + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(ReutilizablesTestPageComponent); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + + // Check section title + const sectionTitle = element.querySelector('.configuracion-tennant h2'); + expect(sectionTitle?.textContent).toContain('Configuración Tenant'); + + // Check header logo source + const headerImg = element.querySelector('.tenant-logo-card img[alt="Header Logo"]') as HTMLImageElement; + expect(headerImg?.src).toBe(tenant.header_logo); + + // Check footer logo source + const footerImg = element.querySelector('.tenant-logo-card img[alt="Footer Logo"]') as HTMLImageElement; + expect(footerImg?.src).toBe(tenant.footer_logo); + + // Check color hex values + expect(element.textContent).toContain(tenant.primary_color); + expect(element.textContent).toContain(tenant.secondary_color); + expect(element.textContent).toContain(tenant.danger_color); + }); +}); diff --git a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts index e1ee059..b212cf6 100644 --- a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts +++ b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts @@ -1,6 +1,7 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { ButtonComponent } from '../../../../shared/components/button/button.component'; import { InputComponent } from '../../../../shared/components/input/input.component'; +import { TenantService } from '../../../../core/services/tenant.service'; @Component({ selector: 'app-reutilizables-test-page', @@ -9,6 +10,8 @@ import { InputComponent } from '../../../../shared/components/input/input.compon styleUrl: './reutilizables-test-page.component.scss' }) export class ReutilizablesTestPageComponent { + private readonly tenantService = inject(TenantService); + protected readonly tenant = this.tenantService.tenant; protected textValue = 'Auriculares'; protected numberValue = '24'; protected email = 'usuario@empresa.com'; diff --git a/src/app/shared/components/button/button.component.scss b/src/app/shared/components/button/button.component.scss index 973f952..b226c71 100644 --- a/src/app/shared/components/button/button.component.scss +++ b/src/app/shared/components/button/button.component.scss @@ -1,7 +1,3 @@ -@use "sass:color"; - -@import "../../../../_theme"; - :host { display: inline-block; } @@ -9,13 +5,15 @@ .btn { min-height: 40px; font-weight: 700; - font-size:15px; - min-width:216px; + font-size: 15px; + min-width: 216px; } .btn-primary, .btn-secondary, .btn-danger { + --bs-btn-bg: transparent; + --bs-btn-border-color: transparent; --bs-btn-color: #fff; --bs-btn-hover-color: #fff; --bs-btn-active-color: #fff; @@ -23,44 +21,68 @@ .btn-outline-primary, .btn-outline-danger { + --bs-btn-bg: transparent; --bs-btn-hover-color: #fff; --bs-btn-active-color: #fff; } .btn-primary { - $primary-disabled-bg: color.mix(white, $primary, $weight: $primary-button-disabled-tint-amount); - - --bs-btn-hover-bg: #{color.mix(black, $primary, $weight: $primary-button-hover-shade-amount)}; - --bs-btn-hover-border-color: #{color.mix(black, $primary, $weight: $primary-button-hover-shade-amount)}; + --bs-btn-bg: var(--tenant-primary); + --bs-btn-border-color: var(--tenant-primary); + --bs-btn-hover-bg: color-mix(in srgb, black 40%, var(--tenant-primary)); + --bs-btn-hover-border-color: color-mix(in srgb, black 40%, var(--tenant-primary)); + --bs-btn-active-bg: color-mix(in srgb, black 44%, var(--tenant-primary)); + --bs-btn-active-border-color: color-mix(in srgb, black 44%, var(--tenant-primary)); --bs-btn-disabled-color: #fff; - --bs-btn-disabled-bg: #{$primary-disabled-bg}; - --bs-btn-disabled-border-color: #{$primary-disabled-bg}; + --bs-btn-disabled-bg: color-mix(in srgb, white 50%, var(--tenant-primary)); + --bs-btn-disabled-border-color: color-mix(in srgb, white 50%, var(--tenant-primary)); --bs-btn-disabled-opacity: 1; } .btn-secondary { - $secondary-hover-active-bg: color.mix(black, $secondary, $weight: $secondary-button-hover-active-shade-amount); - $secondary-disabled-bg: color.change($secondary, $alpha: $secondary-button-disabled-alpha); - - --bs-btn-hover-bg: #{$secondary-hover-active-bg}; - --bs-btn-hover-border-color: #{$secondary-hover-active-bg}; - --bs-btn-active-bg: #{$secondary-hover-active-bg}; - --bs-btn-active-border-color: #{$secondary-hover-active-bg}; + --bs-btn-bg: var(--tenant-secondary); + --bs-btn-border-color: var(--tenant-secondary); + --bs-btn-hover-bg: color-mix(in srgb, black 26%, var(--tenant-secondary)); + --bs-btn-hover-border-color: color-mix(in srgb, black 26%, var(--tenant-secondary)); + --bs-btn-active-bg: color-mix(in srgb, black 30%, var(--tenant-secondary)); + --bs-btn-active-border-color: color-mix(in srgb, black 30%, var(--tenant-secondary)); --bs-btn-disabled-color: #fff; - --bs-btn-disabled-bg: #{$secondary-disabled-bg}; - --bs-btn-disabled-border-color: #{$secondary-disabled-bg}; + --bs-btn-disabled-bg: color-mix(in srgb, transparent 50%, var(--tenant-secondary)); + --bs-btn-disabled-border-color: color-mix(in srgb, transparent 50%, var(--tenant-secondary)); --bs-btn-disabled-opacity: 1; } .btn-danger { - $danger-disabled-bg: color.change($danger, $alpha: $danger-button-disabled-alpha); - - --bs-btn-hover-bg: #{$danger-button-hover-active-bg}; - --bs-btn-hover-border-color: #{$danger-button-hover-active-bg}; - --bs-btn-active-bg: #{$danger-button-hover-active-bg}; - --bs-btn-active-border-color: #{$danger-button-hover-active-bg}; + --bs-btn-bg: var(--tenant-danger); + --bs-btn-border-color: var(--tenant-danger); + --bs-btn-hover-bg: color-mix(in srgb, black 38%, var(--tenant-danger)); + --bs-btn-hover-border-color: color-mix(in srgb, black 38%, var(--tenant-danger)); + --bs-btn-active-bg: color-mix(in srgb, black 44%, var(--tenant-danger)); + --bs-btn-active-border-color: color-mix(in srgb, black 44%, var(--tenant-danger)); --bs-btn-disabled-color: #fff; - --bs-btn-disabled-bg: #{$danger-disabled-bg}; - --bs-btn-disabled-border-color: #{$danger-disabled-bg}; + --bs-btn-disabled-bg: color-mix(in srgb, transparent 50%, var(--tenant-danger)); + --bs-btn-disabled-border-color: color-mix(in srgb, transparent 50%, var(--tenant-danger)); --bs-btn-disabled-opacity: 1; } + +.btn-outline-primary { + --bs-btn-color: var(--tenant-primary); + --bs-btn-border-color: var(--tenant-primary); + --bs-btn-hover-bg: var(--tenant-primary); + --bs-btn-hover-border-color: var(--tenant-primary); + --bs-btn-active-bg: color-mix(in srgb, black 12%, var(--tenant-primary)); + --bs-btn-active-border-color: color-mix(in srgb, black 12%, var(--tenant-primary)); + --bs-btn-disabled-color: color-mix(in srgb, transparent 35%, var(--tenant-primary)); + --bs-btn-disabled-border-color: color-mix(in srgb, transparent 35%, var(--tenant-primary)); +} + +.btn-outline-danger { + --bs-btn-color: var(--tenant-danger); + --bs-btn-border-color: var(--tenant-danger); + --bs-btn-hover-bg: var(--tenant-danger); + --bs-btn-hover-border-color: var(--tenant-danger); + --bs-btn-active-bg: color-mix(in srgb, black 12%, var(--tenant-danger)); + --bs-btn-active-border-color: color-mix(in srgb, black 12%, var(--tenant-danger)); + --bs-btn-disabled-color: color-mix(in srgb, transparent 35%, var(--tenant-danger)); + --bs-btn-disabled-border-color: color-mix(in srgb, transparent 35%, var(--tenant-danger)); +} diff --git a/src/styles.scss b/src/styles.scss index 3d10688..c67ee4d 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -9,6 +9,12 @@ :root { --bs-body-font-family: "Gotham", sans-serif; + --bs-primary: var(--tenant-primary); + --bs-primary-rgb: var(--tenant-primary-rgb); + --bs-secondary: var(--tenant-secondary); + --bs-secondary-rgb: var(--tenant-secondary-rgb); + --bs-danger: var(--tenant-danger); + --bs-danger-rgb: var(--tenant-danger-rgb); } label {