refactor: migrate tenant bootstrap to use SSR state caching and update button components to use CSS tenant variables
This commit is contained in:
@@ -7,15 +7,11 @@
|
||||
data-testid="store-footer-brand-slot"
|
||||
aria-label="Logo del comercio"
|
||||
>
|
||||
@if (logoUrl && logoLoaded) {
|
||||
@if (logoUrl) {
|
||||
<img class="store-layout__brand-logo" [src]="logoUrl" alt="Logo del comercio" />
|
||||
} @else {
|
||||
<div class="store-layout__brand-logo-shell" aria-hidden="true">
|
||||
<span class="store-layout__brand-logo-skeleton"></span>
|
||||
|
||||
@if (logoUrl && !logoLoaded) {
|
||||
<img class="store-layout__brand-logo store-layout__brand-logo--preload" [src]="logoUrl" alt="" (load)="onLogoLoad()" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -31,7 +27,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mb-0 small text-white-50">Copyright © {{ currentYear }} - Sonder</p>
|
||||
<p class="mb-0 small store-layout__muted-text">Copyright © {{ currentYear }} - Sonder</p>
|
||||
</div>
|
||||
|
||||
@for (section of footerSections; track section.heading) {
|
||||
@@ -39,7 +35,7 @@
|
||||
<h2 class="store-layout__footer-heading">{{ section.heading }}</h2>
|
||||
<div class="d-grid gap-2">
|
||||
@for (link of section.links; track link) {
|
||||
<span class="text-light-emphasis">{{ link }}</span>
|
||||
<span class="store-layout__muted-text">{{ link }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
<header class="store-layout__header bg-white border-bottom">
|
||||
<div class="border-bottom">
|
||||
<header class="store-layout__header">
|
||||
<div class="store-layout__header-main">
|
||||
<div class="container-xl px-3 px-md-4 py-3 py-lg-4 d-flex flex-wrap align-items-center justify-content-between gap-3">
|
||||
<div
|
||||
class="store-layout__brand-slot d-inline-flex align-items-center gap-3 flex-shrink-0"
|
||||
data-testid="store-header-brand-slot"
|
||||
aria-label="Logo del comercio"
|
||||
>
|
||||
@if (logoUrl && logoLoaded) {
|
||||
@if (logoUrl) {
|
||||
<img class="store-layout__brand-logo" [src]="logoUrl" alt="Logo del comercio" />
|
||||
} @else {
|
||||
<div class="store-layout__brand-logo-shell" aria-hidden="true">
|
||||
<span class="store-layout__brand-logo-skeleton"></span>
|
||||
|
||||
@if (logoUrl && !logoLoaded) {
|
||||
<img class="store-layout__brand-logo store-layout__brand-logo--preload" [src]="logoUrl" alt="" (load)="onLogoLoad()" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -25,10 +21,14 @@
|
||||
<input
|
||||
id="store-search-input"
|
||||
type="search"
|
||||
class="form-control border-dark border-end-0 rounded-start"
|
||||
class="form-control store-layout__search-input border-end-0 rounded-start"
|
||||
placeholder="Buscar productos"
|
||||
/>
|
||||
<button type="button" class="btn btn-outline-dark border-start-0 rounded-end px-3" aria-label="Buscar">
|
||||
<button
|
||||
type="button"
|
||||
class="btn store-layout__search-button border-start-0 rounded-end px-3"
|
||||
aria-label="Buscar"
|
||||
>
|
||||
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -37,7 +37,7 @@
|
||||
@for (action of headerActions; track action.label) {
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link text-dark p-2 text-decoration-none"
|
||||
class="btn btn-link store-layout__action-button p-2 text-decoration-none"
|
||||
[attr.aria-label]="action.label"
|
||||
[attr.title]="action.label"
|
||||
>
|
||||
@@ -53,7 +53,7 @@
|
||||
<div class="container-xl px-3 px-md-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link d-inline-flex align-items-center gap-2 px-0 py-2 text-dark text-uppercase fw-bold text-decoration-none store-layout__category-trigger"
|
||||
class="btn btn-link d-inline-flex align-items-center gap-2 px-0 py-2 text-uppercase fw-bold text-decoration-none store-layout__category-trigger"
|
||||
aria-label="Categorias"
|
||||
>
|
||||
<span>Categorias</span>
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
.store-layout__header {
|
||||
color: #ffffff;
|
||||
background-color: var(--tenant-header-footer-bg);
|
||||
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.store-layout__header-main {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.store-layout__brand-slot {
|
||||
min-width: 10rem;
|
||||
}
|
||||
@@ -15,19 +25,17 @@
|
||||
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%;
|
||||
height: 100%;
|
||||
border-radius: 0.75rem;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.08) 0%, rgba(0, 0, 0, 0.14) 50%, rgba(0, 0, 0, 0.08) 100%);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0.12) 0%,
|
||||
rgba(255, 255, 255, 0.22) 50%,
|
||||
rgba(255, 255, 255, 0.12) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: store-header-logo-skeleton 1.2s ease-in-out infinite;
|
||||
}
|
||||
@@ -37,9 +45,47 @@
|
||||
min-width: 18rem;
|
||||
}
|
||||
|
||||
.store-layout__search-input {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
background-color: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.store-layout__search-input:focus {
|
||||
border-color: var(--tenant-primary);
|
||||
box-shadow: 0 0 0 0.25rem color-mix(in srgb, white 70%, var(--tenant-primary));
|
||||
}
|
||||
|
||||
.store-layout__search-button {
|
||||
color: #ffffff;
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
background-color: color-mix(in srgb, white 10%, var(--tenant-header-footer-bg));
|
||||
}
|
||||
|
||||
.store-layout__search-button:hover,
|
||||
.store-layout__search-button:focus-visible {
|
||||
color: #ffffff;
|
||||
border-color: rgba(255, 255, 255, 0.28);
|
||||
background-color: color-mix(in srgb, white 16%, var(--tenant-header-footer-bg));
|
||||
}
|
||||
|
||||
.store-layout__action-button {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.store-layout__action-button:hover,
|
||||
.store-layout__action-button:focus-visible {
|
||||
color: color-mix(in srgb, white 84%, var(--tenant-primary));
|
||||
}
|
||||
|
||||
.store-layout__category-trigger {
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.02em;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.store-layout__category-trigger:hover,
|
||||
.store-layout__category-trigger:focus-visible {
|
||||
color: color-mix(in srgb, white 84%, var(--tenant-primary));
|
||||
}
|
||||
|
||||
@keyframes store-header-logo-skeleton {
|
||||
|
||||
@@ -12,23 +12,6 @@ export type StoreHeaderAction = {
|
||||
styleUrl: './store-header.component.scss'
|
||||
})
|
||||
export class StoreHeaderComponent {
|
||||
private logoUrlValue: string | null = null;
|
||||
|
||||
@Input({ required: true }) headerActions: StoreHeaderAction[] = [];
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
:host {
|
||||
display: flex;
|
||||
min-height: 100dvh;
|
||||
background-color: #f6f6f6;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at top,
|
||||
color-mix(in srgb, white 88%, var(--tenant-primary)) 0%,
|
||||
transparent 58%
|
||||
),
|
||||
#f6f6f6;
|
||||
color: #202020;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,42 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { Tenant } from '../../services/tenant.interface';
|
||||
import { TenantService } from '../../services/tenant.service';
|
||||
import { StoreLayoutComponent } from './store-layout.component';
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
describe('StoreLayoutComponent', () => {
|
||||
beforeEach(async () => {
|
||||
const tenantState = signal(tenant);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [StoreLayoutComponent],
|
||||
providers: [provideRouter([])]
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{
|
||||
provide: TenantService,
|
||||
useValue: {
|
||||
tenant: tenantState.asReadonly(),
|
||||
status: signal<'ready'>('ready').asReadonly(),
|
||||
getTenant: () => tenantState(),
|
||||
bootstrap: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
}
|
||||
]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@@ -23,7 +52,7 @@ describe('StoreLayoutComponent', () => {
|
||||
expect(compiled.querySelector('app-store-footer .store-layout__footer')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders the placeholder branding and action icons', () => {
|
||||
it('renders the tenant branding and action icons', () => {
|
||||
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -31,6 +60,12 @@ describe('StoreLayoutComponent', () => {
|
||||
|
||||
expect(compiled.querySelector('[data-testid="store-header-brand-slot"]')).not.toBeNull();
|
||||
expect(compiled.querySelector('[data-testid="store-footer-brand-slot"]')).not.toBeNull();
|
||||
expect(
|
||||
compiled.querySelector(`app-store-header img.store-layout__brand-logo[src="${tenant.header_logo}"]`)
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
compiled.querySelector(`app-store-footer img.store-layout__brand-logo[src="${tenant.footer_logo}"]`)
|
||||
).not.toBeNull();
|
||||
expect(compiled.querySelector('.fa-cart-shopping')).not.toBeNull();
|
||||
expect(compiled.querySelector('.fa-circle-user')).not.toBeNull();
|
||||
expect(compiled.querySelector('.fa-instagram')).not.toBeNull();
|
||||
|
||||
Reference in New Issue
Block a user