feat: enhance store layout with dynamic logo handling and loading states
This commit is contained in:
@@ -5,12 +5,19 @@
|
||||
<div
|
||||
class="store-layout__brand-slot d-inline-flex align-items-center gap-3"
|
||||
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">
|
||||
S
|
||||
</span>
|
||||
<span class="store-layout__brand-text store-layout__brand-text--footer">SONDER</span>
|
||||
@if (logoUrl && logoLoaded) {
|
||||
<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 class="d-grid gap-2">
|
||||
|
||||
@@ -6,6 +6,42 @@
|
||||
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 {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -33,6 +69,16 @@
|
||||
color: #f8f8f8;
|
||||
}
|
||||
|
||||
@keyframes store-footer-logo-skeleton {
|
||||
from {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.store-layout__brand-slot {
|
||||
min-width: 0;
|
||||
|
||||
@@ -17,7 +17,25 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,19 @@
|
||||
<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="Espacio reservado para logo de Sonder"
|
||||
aria-label="Logo del comercio"
|
||||
>
|
||||
<span class="store-layout__brand-mark" aria-hidden="true">S</span>
|
||||
<span class="store-layout__brand-text">SONDER</span>
|
||||
@if (logoUrl && logoLoaded) {
|
||||
<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 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;
|
||||
}
|
||||
|
||||
.store-layout__brand-mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 2.75rem;
|
||||
.store-layout__brand-logo-shell {
|
||||
position: relative;
|
||||
width: min(100%, 12rem);
|
||||
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 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.08em;
|
||||
.store-layout__brand-logo {
|
||||
display: block;
|
||||
width: 100%;
|
||||
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 {
|
||||
@@ -31,15 +42,21 @@
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
@keyframes store-header-logo-skeleton {
|
||||
from {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.store-layout__brand-slot {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.store-layout__brand-text {
|
||||
font-size: 1.45rem;
|
||||
}
|
||||
|
||||
.store-layout__search {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -12,5 +12,23 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<router-outlet />
|
||||
@@ -9,5 +9,6 @@
|
||||
[currentYear]="currentYear"
|
||||
[footerSections]="footerSections"
|
||||
[socialLinks]="socialLinks"
|
||||
[logoUrl]="tenant()?.footer_logo ?? null"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { TenantService } from '../../services/tenant.service';
|
||||
import { StoreFooterComponent, StoreFooterSection, StoreSocialLink } from './store-footer/store-footer.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'
|
||||
})
|
||||
export class StoreLayoutComponent {
|
||||
private readonly tenantService = inject(TenantService);
|
||||
|
||||
protected readonly currentYear = new Date().getFullYear();
|
||||
protected readonly tenant = this.tenantService.tenant;
|
||||
|
||||
protected readonly headerActions: StoreHeaderAction[] = [
|
||||
{
|
||||
|
||||
@@ -44,6 +44,7 @@ describe('TenantService', () => {
|
||||
|
||||
await expect(bootstrapPromise).resolves.toEqual(response.data);
|
||||
expect(service.tenant()).toEqual(response.data);
|
||||
expect(service.getTenant()).toEqual(response.data);
|
||||
|
||||
httpController.verify();
|
||||
});
|
||||
@@ -81,6 +82,7 @@ describe('TenantService', () => {
|
||||
|
||||
await expect(service.bootstrap()).resolves.toBeNull();
|
||||
expect(service.tenant()).toBeNull();
|
||||
expect(service.getTenant()).toBeNull();
|
||||
|
||||
httpController.expectNone(() => true);
|
||||
httpController.verify();
|
||||
|
||||
@@ -16,6 +16,10 @@ export class TenantService {
|
||||
|
||||
readonly tenant = this.tenantState.asReadonly();
|
||||
|
||||
getTenant(): Tenant | null {
|
||||
return this.tenantState();
|
||||
}
|
||||
|
||||
async bootstrap(): Promise<Tenant | null> {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user