feat(store-layout): add dynamic background images to header and footer components

This commit is contained in:
2026-08-12 14:08:56 -03:00
parent c1da31711f
commit d45199a0ef
11 changed files with 57 additions and 9 deletions

View File

@@ -1,4 +1,7 @@
<footer class="store-layout__footer mt-auto text-light"> <footer
class="store-layout__footer mt-auto text-light"
[style.background-image]="backgroundImageUrl ? 'url(' + backgroundImageUrl + ')' : null"
>
<div class="container-xl px-3 px-md-4 py-4 py-md-5 store-layout__footer-content"> <div class="container-xl px-3 px-md-4 py-4 py-md-5 store-layout__footer-content">
<div class="row g-4 store-layout__footer-grid"> <div class="row g-4 store-layout__footer-grid">
<div class="col-12 store-layout__brand-column"> <div class="col-12 store-layout__brand-column">

View File

@@ -1,5 +1,8 @@
.store-layout__footer { .store-layout__footer {
background-color: var(--tenant-footer-bg); background-color: var(--tenant-footer-bg);
background-repeat: no-repeat;
background-position: center;
background-size: cover;
} }
.store-layout__mobile-divider { .store-layout__mobile-divider {

View File

@@ -25,6 +25,7 @@ export class StoreFooterComponent {
@Input({ required: true }) footerSections: StoreFooterSection[] = []; @Input({ required: true }) footerSections: StoreFooterSection[] = [];
@Input({ required: true }) socialMedia: SocialMedia[] = []; @Input({ required: true }) socialMedia: SocialMedia[] = [];
@Input() logoUrl: string | null = null; @Input() logoUrl: string | null = null;
@Input() backgroundImageUrl: string | null = null;
@Input() storeName: string | null = null; @Input() storeName: string | null = null;
readonly logoutClick = output<void>(); readonly logoutClick = output<void>();
} }

View File

@@ -1,4 +1,7 @@
<header class="store-layout__header"> <header
class="store-layout__header"
[style.background-image]="backgroundImageUrl() ? 'url(' + backgroundImageUrl() + ')' : null"
>
<div class="store-layout__header-main"> <div class="store-layout__header-main">
<div class="container-xl px-3 px-md-4 py-3 py-lg-4"> <div class="container-xl px-3 px-md-4 py-3 py-lg-4">
<div class="row align-items-center gy-3 gy-md-0"> <div class="row align-items-center gy-3 gy-md-0">
@@ -74,12 +77,14 @@
</app-button> </app-button>
} }
@if (displayCart()) {
<app-cart-icon <app-cart-icon
[quantity]="cartQuantity()" [quantity]="cartQuantity()"
ariaLabel="Carrito de compras" ariaLabel="Carrito de compras"
title="Carrito" title="Carrito"
(click)="cartClick.emit()" (click)="cartClick.emit()"
/> />
}
<div class="store-layout__user-menu d-none d-md-inline-flex position-relative"> <div class="store-layout__user-menu d-none d-md-inline-flex position-relative">
<app-icon-button <app-icon-button

View File

@@ -1,6 +1,9 @@
.store-layout__header { .store-layout__header {
color: #ffffff; color: #ffffff;
background-color: var(--tenant-header-bg); background-color: var(--tenant-header-bg);
background-repeat: no-repeat;
background-position: center;
background-size: cover;
} }
.store-layout__header-main { .store-layout__header-main {

View File

@@ -29,6 +29,7 @@ export class StoreHeaderComponent {
private readonly router = inject(Router); private readonly router = inject(Router);
readonly logoUrl = input<string | null>(null); readonly logoUrl = input<string | null>(null);
readonly backgroundImageUrl = input<string | null>(null);
readonly cartQuantity = input<number>(0); readonly cartQuantity = input<number>(0);
readonly user = input<AuthUser | null>(null); readonly user = input<AuthUser | null>(null);
readonly isAuthenticated = input<boolean>(false); readonly isAuthenticated = input<boolean>(false);
@@ -37,6 +38,7 @@ export class StoreHeaderComponent {
readonly categories = input<readonly Category[]>([]); readonly categories = input<readonly Category[]>([]);
readonly displayCategories = input(true); readonly displayCategories = input(true);
readonly displaySeachBar = input(true); readonly displaySeachBar = input(true);
readonly displayCart = input(true);
readonly cartClick = output<void>(); readonly cartClick = output<void>();
readonly ticketsClick = output<void>(); readonly ticketsClick = output<void>();
readonly loginClick = output<void>(); readonly loginClick = output<void>();

View File

@@ -3,6 +3,7 @@
ngSkipHydration ngSkipHydration
[cartQuantity]="cartQuantity()" [cartQuantity]="cartQuantity()"
[logoUrl]="tenant()?.header_logo ?? null" [logoUrl]="tenant()?.header_logo ?? null"
[backgroundImageUrl]="tenant()?.header_bg_image ?? null"
[user]="user()" [user]="user()"
[isAuthenticated]="isAuthenticated()" [isAuthenticated]="isAuthenticated()"
[accountNavigationMenus]="accountNavigationMenus()" [accountNavigationMenus]="accountNavigationMenus()"
@@ -10,6 +11,7 @@
[categories]="tenant()?.categories ?? []" [categories]="tenant()?.categories ?? []"
[displayCategories]="tenant()?.display_categories ?? true" [displayCategories]="tenant()?.display_categories ?? true"
[displaySeachBar]="tenant()?.display_seach_bar ?? true" [displaySeachBar]="tenant()?.display_seach_bar ?? true"
[displayCart]="displayCart()"
(cartClick)="isCartOpen.set(!isCartOpen())" (cartClick)="isCartOpen.set(!isCartOpen())"
(ticketsClick)="onTicketsClick()" (ticketsClick)="onTicketsClick()"
(loginClick)="onLoginClick()" (loginClick)="onLoginClick()"
@@ -18,7 +20,7 @@
(categorySelect)="onCategorySelect($event)" (categorySelect)="onCategorySelect($event)"
/> />
@if (isCartOpen()) { @if (displayCart() && isCartOpen()) {
<div class="store-layout__cart-overlay" (click)="isCartOpen.set(false)"></div> <div class="store-layout__cart-overlay" (click)="isCartOpen.set(false)"></div>
<div class="store-layout__cart-dropdown card border-0 shadow-lg"> <div class="store-layout__cart-dropdown card border-0 shadow-lg">
<app-cart <app-cart
@@ -58,6 +60,7 @@
[footerSections]="footerSections()" [footerSections]="footerSections()"
[socialMedia]="tenant()?.social_media ?? []" [socialMedia]="tenant()?.social_media ?? []"
[logoUrl]="tenant()?.footer_logo ?? null" [logoUrl]="tenant()?.footer_logo ?? null"
[backgroundImageUrl]="tenant()?.footer_bg_image ?? null"
[storeName]="tenant()?.nombre ?? null" [storeName]="tenant()?.nombre ?? null"
(logoutClick)="onLogoutClick()" (logoutClick)="onLogoutClick()"
/> />

View File

@@ -30,6 +30,8 @@ const tenant: Tenant = {
footer_bg_color: '#313131', footer_bg_color: '#313131',
header_logo: 'https://example.com/header.png', header_logo: 'https://example.com/header.png',
footer_logo: 'https://example.com/footer.png', footer_logo: 'https://example.com/footer.png',
header_bg_image: 'https://example.com/header-background.png',
footer_bg_image: 'https://example.com/footer-background.png',
categories: [], categories: [],
menues: [ menues: [
{ {
@@ -210,6 +212,7 @@ describe('StoreLayoutComponent', () => {
categories: [{ id: 1, nombre: 'Remeras', subcategories: [] }], categories: [{ id: 1, nombre: 'Remeras', subcategories: [] }],
display_categories: false, display_categories: false,
display_seach_bar: false, display_seach_bar: false,
display_cart: false,
}); });
const fixture = TestBed.createComponent(StoreLayoutComponent); const fixture = TestBed.createComponent(StoreLayoutComponent);
@@ -219,6 +222,13 @@ describe('StoreLayoutComponent', () => {
expect(compiled.querySelector('#store-search-input')).toBeNull(); expect(compiled.querySelector('#store-search-input')).toBeNull();
expect(compiled.querySelector('.store-layout__category-trigger')).toBeNull(); expect(compiled.querySelector('.store-layout__category-trigger')).toBeNull();
expect(compiled.querySelector('app-cart-icon')).toBeNull();
(fixture.componentInstance as any).isCartOpen.set(true);
fixture.detectChanges();
expect(compiled.querySelector('app-cart')).toBeNull();
expect(compiled.querySelector('.store-layout__cart-overlay')).toBeNull();
const header = fixture.debugElement.query(By.directive(StoreHeaderComponent)); const header = fixture.debugElement.query(By.directive(StoreHeaderComponent));
(header.componentInstance as any).toggleMobileMenu(); (header.componentInstance as any).toggleMobileMenu();
@@ -245,6 +255,12 @@ describe('StoreLayoutComponent', () => {
`app-store-footer img.store-layout__brand-logo[src="${tenant.footer_logo}"]`, `app-store-footer img.store-layout__brand-logo[src="${tenant.footer_logo}"]`,
), ),
).not.toBeNull(); ).not.toBeNull();
expect(
(compiled.querySelector('.store-layout__header') as HTMLElement).style.backgroundImage,
).toContain(tenant.header_bg_image!);
expect(
(compiled.querySelector('.store-layout__footer') as HTMLElement).style.backgroundImage,
).toContain(tenant.footer_bg_image!);
expect(compiled.querySelector('.fa-cart-shopping')).not.toBeNull(); expect(compiled.querySelector('.fa-cart-shopping')).not.toBeNull();
expect(compiled.querySelector('.fa-circle-user')).not.toBeNull(); expect(compiled.querySelector('.fa-circle-user')).not.toBeNull();
expect(compiled.querySelector('.fa-instagram')).not.toBeNull(); expect(compiled.querySelector('.fa-instagram')).not.toBeNull();

View File

@@ -35,6 +35,7 @@ export class StoreLayoutComponent implements OnInit {
protected readonly isCartOpen = signal(false); protected readonly isCartOpen = signal(false);
protected readonly isCreatingPurchase = signal(false); protected readonly isCreatingPurchase = signal(false);
protected readonly displayCart = computed(() => this.tenant()?.display_cart ?? true);
protected readonly cartSubtotal = computed(() => { protected readonly cartSubtotal = computed(() => {
const cart = this.cartService.cart(); const cart = this.cartService.cart();

View File

@@ -104,6 +104,8 @@ export interface Tenant {
footer_bg_color: string; footer_bg_color: string;
header_logo: string; header_logo: string;
footer_logo: string; footer_logo: string;
header_bg_image?: string | null;
footer_bg_image?: string | null;
website_type_code?: string | null; website_type_code?: string | null;
website_type?: WebsiteType | null; website_type?: WebsiteType | null;
event_date_text?: string | null; event_date_text?: string | null;
@@ -116,6 +118,7 @@ export interface Tenant {
search_items_per_page?: number; search_items_per_page?: number;
display_categories?: boolean; display_categories?: boolean;
display_seach_bar?: boolean; display_seach_bar?: boolean;
display_cart?: boolean;
social_media?: SocialMedia[]; social_media?: SocialMedia[];
menues?: Menu[]; menues?: Menu[];
categories: Category[]; categories: Category[];

View File

@@ -53,6 +53,14 @@
} }
} }
::ng-deep .hero-title h1 {
margin: 0;
color: inherit;
font-size: inherit;
font-weight: 700;
line-height: inherit;
}
::ng-deep .hero-description, ::ng-deep .hero-description,
.hero-description { .hero-description {
color: #666666; color: #666666;