feat(social-media): implement social media component and integrate into store footer
This commit is contained in:
@@ -44,11 +44,14 @@
|
|||||||
<div class="col-12 col-md-6 col-xl-3 d-grid gap-3 align-content-start">
|
<div class="col-12 col-md-6 col-xl-3 d-grid gap-3 align-content-start">
|
||||||
<h2 class="store-layout__footer-heading">Seguinos:</h2>
|
<h2 class="store-layout__footer-heading">Seguinos:</h2>
|
||||||
<div class="d-flex align-items-center gap-3">
|
<div class="d-flex align-items-center gap-3">
|
||||||
@for (social of socialLinks; track social.label) {
|
@for (social of socialMedia; track social.code) {
|
||||||
<span class="fs-5 text-white">
|
<app-social-media
|
||||||
<i [class]="social.iconClass" aria-hidden="true"></i>
|
class="text-white"
|
||||||
<span class="visually-hidden">{{ social.label }}</span>
|
[code]="social.code"
|
||||||
</span>
|
[icon]="social.icon"
|
||||||
|
[name]="social.name"
|
||||||
|
[url]="social.url"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { SocialMedia } from '../../../services/tenant.interface';
|
||||||
|
import { SocialMediaComponent } from '../../../../shared/components/social-media/social-media.component';
|
||||||
|
|
||||||
export type StoreFooterSection = {
|
export type StoreFooterSection = {
|
||||||
heading: string;
|
heading: string;
|
||||||
links: string[];
|
links: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StoreSocialLink = {
|
|
||||||
label: string;
|
|
||||||
iconClass: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-store-footer',
|
selector: 'app-store-footer',
|
||||||
imports: [],
|
imports: [SocialMediaComponent],
|
||||||
templateUrl: './store-footer.component.html',
|
templateUrl: './store-footer.component.html',
|
||||||
styleUrl: './store-footer.component.scss'
|
styleUrl: './store-footer.component.scss',
|
||||||
})
|
})
|
||||||
export class StoreFooterComponent {
|
export class StoreFooterComponent {
|
||||||
@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 }) socialMedia: SocialMedia[] = [];
|
||||||
@Input() logoUrl: string | null = null;
|
@Input() logoUrl: string | null = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<app-store-footer
|
<app-store-footer
|
||||||
[currentYear]="currentYear"
|
[currentYear]="currentYear"
|
||||||
[footerSections]="footerSections"
|
[footerSections]="footerSections"
|
||||||
[socialLinks]="socialLinks"
|
[socialMedia]="tenant()?.social_media ?? []"
|
||||||
[logoUrl]="tenant()?.footer_logo ?? null"
|
[logoUrl]="tenant()?.footer_logo ?? null"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,7 +25,33 @@ const tenant: Tenant = {
|
|||||||
header_bg_color: '#313131',
|
header_bg_color: '#313131',
|
||||||
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',
|
||||||
|
social_media: [
|
||||||
|
{
|
||||||
|
code: 'instagram',
|
||||||
|
icon: 'instagram',
|
||||||
|
name: 'Instagram',
|
||||||
|
url: 'https://instagram.com/test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'whatsapp',
|
||||||
|
icon: 'whatsapp',
|
||||||
|
name: 'WhatsApp',
|
||||||
|
url: 'https://wa.me/5493410000000',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'facebook',
|
||||||
|
icon: 'facebook',
|
||||||
|
name: 'Facebook',
|
||||||
|
url: 'https://facebook.com/test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'linkedin',
|
||||||
|
icon: 'fa-brands fa-linkedin-in',
|
||||||
|
name: 'LinkedIn',
|
||||||
|
url: 'https://linkedin.com/company/test',
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('StoreLayoutComponent', () => {
|
describe('StoreLayoutComponent', () => {
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ import { Component, computed, inject, OnInit, signal } from '@angular/core';
|
|||||||
import { Router, RouterOutlet } from '@angular/router';
|
import { Router, RouterOutlet } from '@angular/router';
|
||||||
import { TenantService } from '../../services/tenant.service';
|
import { TenantService } from '../../services/tenant.service';
|
||||||
import { CartService } from '../../services/cart/cart.service';
|
import { CartService } from '../../services/cart/cart.service';
|
||||||
import {
|
import { StoreFooterComponent, StoreFooterSection } from './store-footer/store-footer.component';
|
||||||
StoreFooterComponent,
|
|
||||||
StoreFooterSection,
|
|
||||||
StoreSocialLink,
|
|
||||||
} from './store-footer/store-footer.component';
|
|
||||||
import { StoreHeaderComponent } from './store-header/store-header.component';
|
import { StoreHeaderComponent } from './store-header/store-header.component';
|
||||||
import { CartComponent, CartItemMock } from '../../../shared/components/cart/cart.component';
|
import { CartComponent, CartItemMock } from '../../../shared/components/cart/cart.component';
|
||||||
import { ButtonComponent } from '../../../shared/components/button/button.component';
|
import { ButtonComponent } from '../../../shared/components/button/button.component';
|
||||||
@@ -122,22 +118,4 @@ export class StoreLayoutComponent implements OnInit {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
protected readonly socialLinks: StoreSocialLink[] = [
|
|
||||||
{
|
|
||||||
label: 'Instagram',
|
|
||||||
iconClass: 'fa-brands fa-instagram',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'WhatsApp',
|
|
||||||
iconClass: 'fa-brands fa-whatsapp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Facebook',
|
|
||||||
iconClass: 'fa-brands fa-facebook',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'LinkedIn',
|
|
||||||
iconClass: 'fa-brands fa-linkedin-in',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ export interface Menu {
|
|||||||
route: string;
|
route: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SocialMedia {
|
||||||
|
code: string;
|
||||||
|
icon: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Tenant {
|
export interface Tenant {
|
||||||
id: number;
|
id: number;
|
||||||
codigo: string;
|
codigo: string;
|
||||||
@@ -47,6 +54,7 @@ export interface Tenant {
|
|||||||
hero_config?: HeroConfig | null;
|
hero_config?: HeroConfig | null;
|
||||||
event_config?: EventConfig | null;
|
event_config?: EventConfig | null;
|
||||||
main_carousel_images?: string[];
|
main_carousel_images?: string[];
|
||||||
|
social_media?: SocialMedia[];
|
||||||
menues?: Menu[];
|
menues?: Menu[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<a
|
||||||
|
class="social-media"
|
||||||
|
[href]="url()"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
[attr.aria-label]="name()"
|
||||||
|
[attr.data-social-media]="code()"
|
||||||
|
[title]="name()"
|
||||||
|
>
|
||||||
|
<i [class]="iconClass()" aria-hidden="true"></i>
|
||||||
|
<span class="visually-hidden">{{ name() }}</span>
|
||||||
|
</a>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
:host {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-media {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
transition:
|
||||||
|
color 0.15s ease-in-out,
|
||||||
|
background-color 0.15s ease-in-out,
|
||||||
|
transform 0.15s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: rgba(255, 255, 255, 0.12);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid #ffffff;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { SocialMediaComponent } from './social-media.component';
|
||||||
|
|
||||||
|
describe('SocialMediaComponent', () => {
|
||||||
|
let fixture: ComponentFixture<SocialMediaComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SocialMediaComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SocialMediaComponent);
|
||||||
|
fixture.componentRef.setInput('code', 'instagram');
|
||||||
|
fixture.componentRef.setInput('icon', 'instagram');
|
||||||
|
fixture.componentRef.setInput('name', 'Instagram');
|
||||||
|
fixture.componentRef.setInput('url', 'https://instagram.com/acme');
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a safe external social media link', () => {
|
||||||
|
const link = fixture.nativeElement.querySelector('a') as HTMLAnchorElement;
|
||||||
|
const icon = fixture.nativeElement.querySelector('i') as HTMLElement;
|
||||||
|
|
||||||
|
expect(link.getAttribute('href')).toBe('https://instagram.com/acme');
|
||||||
|
expect(link.getAttribute('target')).toBe('_blank');
|
||||||
|
expect(link.getAttribute('rel')).toBe('noopener noreferrer');
|
||||||
|
expect(link.getAttribute('aria-label')).toBe('Instagram');
|
||||||
|
expect(link.dataset['socialMedia']).toBe('instagram');
|
||||||
|
expect(icon.className).toBe('fa-brands fa-instagram');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a complete Font Awesome class', () => {
|
||||||
|
fixture.componentRef.setInput('icon', 'fa-brands fa-linkedin-in');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const icon = fixture.nativeElement.querySelector('i') as HTMLElement;
|
||||||
|
|
||||||
|
expect(icon.className).toBe('fa-brands fa-linkedin-in');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-social-media',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './social-media.component.html',
|
||||||
|
styleUrl: './social-media.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class SocialMediaComponent {
|
||||||
|
readonly code = input.required<string>();
|
||||||
|
readonly icon = input.required<string>();
|
||||||
|
readonly name = input.required<string>();
|
||||||
|
readonly url = input.required<string>();
|
||||||
|
|
||||||
|
protected readonly iconClass = computed(() => {
|
||||||
|
const icon = this.icon().trim();
|
||||||
|
|
||||||
|
return icon.includes('fa-') ? icon : `fa-brands fa-${icon}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user