feat: enhance store layout with dynamic logo handling and loading states
This commit is contained in:
@@ -5,12 +5,19 @@
|
|||||||
<div
|
<div
|
||||||
class="store-layout__brand-slot d-inline-flex align-items-center gap-3"
|
class="store-layout__brand-slot d-inline-flex align-items-center gap-3"
|
||||||
data-testid="store-footer-brand-slot"
|
data-testid="store-footer-brand-slot"
|
||||||
aria-label="Espacio reservado para logo de Sonder"
|
aria-label="Logo del comercio"
|
||||||
>
|
>
|
||||||
<span class="store-layout__brand-mark store-layout__brand-mark--footer" aria-hidden="true">
|
@if (logoUrl && logoLoaded) {
|
||||||
S
|
<img class="store-layout__brand-logo" [src]="logoUrl" alt="Logo del comercio" />
|
||||||
</span>
|
} @else {
|
||||||
<span class="store-layout__brand-text store-layout__brand-text--footer">SONDER</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
|
|||||||
@@ -6,6 +6,42 @@
|
|||||||
min-width: 10rem;
|
min-width: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.store-layout__brand-logo-shell {
|
||||||
|
position: relative;
|
||||||
|
width: min(100%, 12rem);
|
||||||
|
height: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-layout__brand-logo {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 2.75rem;
|
||||||
|
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(255, 255, 255, 0.12) 0%,
|
||||||
|
rgba(255, 255, 255, 0.24) 50%,
|
||||||
|
rgba(255, 255, 255, 0.12) 100%
|
||||||
|
);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: store-footer-logo-skeleton 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.store-layout__brand-mark {
|
.store-layout__brand-mark {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@@ -33,6 +69,16 @@
|
|||||||
color: #f8f8f8;
|
color: #f8f8f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes store-footer-logo-skeleton {
|
||||||
|
from {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.store-layout__brand-slot {
|
.store-layout__brand-slot {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -17,7 +17,25 @@ export type StoreSocialLink = {
|
|||||||
styleUrl: './store-footer.component.scss'
|
styleUrl: './store-footer.component.scss'
|
||||||
})
|
})
|
||||||
export class StoreFooterComponent {
|
export class StoreFooterComponent {
|
||||||
|
private logoUrlValue: string | null = null;
|
||||||
|
|
||||||
@Input({ required: true }) currentYear = new Date().getFullYear();
|
@Input({ required: true }) currentYear = new Date().getFullYear();
|
||||||
@Input({ required: true }) footerSections: StoreFooterSection[] = [];
|
@Input({ required: true }) footerSections: StoreFooterSection[] = [];
|
||||||
@Input({ required: true }) socialLinks: StoreSocialLink[] = [];
|
@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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,19 @@
|
|||||||
<div
|
<div
|
||||||
class="store-layout__brand-slot d-inline-flex align-items-center gap-3 flex-shrink-0"
|
class="store-layout__brand-slot d-inline-flex align-items-center gap-3 flex-shrink-0"
|
||||||
data-testid="store-header-brand-slot"
|
data-testid="store-header-brand-slot"
|
||||||
aria-label="Espacio reservado para logo de Sonder"
|
aria-label="Logo del comercio"
|
||||||
>
|
>
|
||||||
<span class="store-layout__brand-mark" aria-hidden="true">S</span>
|
@if (logoUrl && logoLoaded) {
|
||||||
<span class="store-layout__brand-text">SONDER</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex flex-column flex-md-row align-items-stretch align-items-md-center gap-3 ms-auto w-100 w-lg-auto">
|
<div class="d-flex flex-column flex-md-row align-items-stretch align-items-md-center gap-3 ms-auto w-100 w-lg-auto">
|
||||||
|
|||||||
@@ -2,23 +2,34 @@
|
|||||||
min-width: 10rem;
|
min-width: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-layout__brand-mark {
|
.store-layout__brand-logo-shell {
|
||||||
display: grid;
|
position: relative;
|
||||||
place-items: center;
|
width: min(100%, 12rem);
|
||||||
width: 2.75rem;
|
|
||||||
height: 2.75rem;
|
height: 2.75rem;
|
||||||
border: 1px dashed #bbbbbb;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
color: #373737;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-layout__brand-text {
|
.store-layout__brand-logo {
|
||||||
font-size: 1.8rem;
|
display: block;
|
||||||
font-weight: 700;
|
width: 100%;
|
||||||
letter-spacing: -0.08em;
|
height: 100%;
|
||||||
|
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-size: 200% 100%;
|
||||||
|
animation: store-header-logo-skeleton 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-layout__search {
|
.store-layout__search {
|
||||||
@@ -31,15 +42,21 @@
|
|||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes store-header-logo-skeleton {
|
||||||
|
from {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.store-layout__brand-slot {
|
.store-layout__brand-slot {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-layout__brand-text {
|
|
||||||
font-size: 1.45rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.store-layout__search {
|
.store-layout__search {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,5 +12,23 @@ export type StoreHeaderAction = {
|
|||||||
styleUrl: './store-header.component.scss'
|
styleUrl: './store-header.component.scss'
|
||||||
})
|
})
|
||||||
export class StoreHeaderComponent {
|
export class StoreHeaderComponent {
|
||||||
|
private logoUrlValue: string | null = null;
|
||||||
|
|
||||||
@Input({ required: true }) headerActions: StoreHeaderAction[] = [];
|
@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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="store-layout d-flex flex-column flex-grow-1">
|
<div class="store-layout d-flex flex-column flex-grow-1">
|
||||||
<app-store-header [headerActions]="headerActions" />
|
<app-store-header [headerActions]="headerActions" [logoUrl]="tenant()?.header_logo ?? null" />
|
||||||
|
|
||||||
<main class="flex-grow-1 d-block">
|
<main class="flex-grow-1 d-block">
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
@@ -9,5 +9,6 @@
|
|||||||
[currentYear]="currentYear"
|
[currentYear]="currentYear"
|
||||||
[footerSections]="footerSections"
|
[footerSections]="footerSections"
|
||||||
[socialLinks]="socialLinks"
|
[socialLinks]="socialLinks"
|
||||||
|
[logoUrl]="tenant()?.footer_logo ?? null"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
import { TenantService } from '../../services/tenant.service';
|
||||||
import { StoreFooterComponent, StoreFooterSection, StoreSocialLink } from './store-footer/store-footer.component';
|
import { StoreFooterComponent, StoreFooterSection, StoreSocialLink } from './store-footer/store-footer.component';
|
||||||
import { StoreHeaderComponent, StoreHeaderAction } from './store-header/store-header.component';
|
import { StoreHeaderComponent, StoreHeaderAction } from './store-header/store-header.component';
|
||||||
|
|
||||||
@@ -10,7 +11,10 @@ import { StoreHeaderComponent, StoreHeaderAction } from './store-header/store-he
|
|||||||
styleUrl: './store-layout.component.scss'
|
styleUrl: './store-layout.component.scss'
|
||||||
})
|
})
|
||||||
export class StoreLayoutComponent {
|
export class StoreLayoutComponent {
|
||||||
|
private readonly tenantService = inject(TenantService);
|
||||||
|
|
||||||
protected readonly currentYear = new Date().getFullYear();
|
protected readonly currentYear = new Date().getFullYear();
|
||||||
|
protected readonly tenant = this.tenantService.tenant;
|
||||||
|
|
||||||
protected readonly headerActions: StoreHeaderAction[] = [
|
protected readonly headerActions: StoreHeaderAction[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ describe('TenantService', () => {
|
|||||||
|
|
||||||
await expect(bootstrapPromise).resolves.toEqual(response.data);
|
await expect(bootstrapPromise).resolves.toEqual(response.data);
|
||||||
expect(service.tenant()).toEqual(response.data);
|
expect(service.tenant()).toEqual(response.data);
|
||||||
|
expect(service.getTenant()).toEqual(response.data);
|
||||||
|
|
||||||
httpController.verify();
|
httpController.verify();
|
||||||
});
|
});
|
||||||
@@ -81,6 +82,7 @@ describe('TenantService', () => {
|
|||||||
|
|
||||||
await expect(service.bootstrap()).resolves.toBeNull();
|
await expect(service.bootstrap()).resolves.toBeNull();
|
||||||
expect(service.tenant()).toBeNull();
|
expect(service.tenant()).toBeNull();
|
||||||
|
expect(service.getTenant()).toBeNull();
|
||||||
|
|
||||||
httpController.expectNone(() => true);
|
httpController.expectNone(() => true);
|
||||||
httpController.verify();
|
httpController.verify();
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export class TenantService {
|
|||||||
|
|
||||||
readonly tenant = this.tenantState.asReadonly();
|
readonly tenant = this.tenantState.asReadonly();
|
||||||
|
|
||||||
|
getTenant(): Tenant | null {
|
||||||
|
return this.tenantState();
|
||||||
|
}
|
||||||
|
|
||||||
async bootstrap(): Promise<Tenant | null> {
|
async bootstrap(): Promise<Tenant | null> {
|
||||||
if (!isPlatformBrowser(this.platformId)) {
|
if (!isPlatformBrowser(this.platformId)) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user