feat(account-page): implement sidebar layout with account sidebar and routing

This commit is contained in:
2026-07-23 15:40:55 -03:00
parent 44bcb9aeba
commit 21119ff303
10 changed files with 108 additions and 28 deletions

View File

@@ -1,7 +1,7 @@
<footer class="store-layout__footer mt-auto text-light"> <footer class="store-layout__footer mt-auto text-light">
<div class="container-xl px-3 px-md-4 py-4 py-md-5"> <div class="container-xl px-3 px-md-4 py-4 py-md-5">
<div class="row g-4"> <div class="row g-4">
<div class="col-12 col-md-6 col-xl-5 d-grid gap-3 align-content-start"> <div class="col-12">
<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"
@@ -15,13 +15,15 @@
</div> </div>
} }
</div> </div>
</div>
<div class="col-12 col-md-6 col-xl-5 d-grid gap-3 align-content-start">
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<div class="d-flex align-items-center gap-2 text-light-emphasis"> <div class="d-flex align-items-center gap-2 ">
<i class="fa-solid fa-location-dot" aria-hidden="true"></i> <i class="fa-solid fa-location-dot" aria-hidden="true"></i>
<span>Av. San Lorenzo 1542, Rosario</span> <span>Av. San Lorenzo 1542, Rosario</span>
</div> </div>
<div class="d-flex align-items-center gap-2 text-light-emphasis"> <div class="d-flex align-items-center gap-2 ">
<i class="fa-solid fa-phone" aria-hidden="true"></i> <i class="fa-solid fa-phone" aria-hidden="true"></i>
<span>54 9 (0341) 6658247</span> <span>54 9 (0341) 6658247</span>
</div> </div>
@@ -31,18 +33,33 @@
</div> </div>
@for (section of footerSections; track section.heading) { @for (section of footerSections; track section.heading) {
<div class="col-12 col-md-6 col-xl-2 d-grid gap-3 align-content-start"> <div class="col-12 col-md-6 col-xl-2 d-grid align-content-start">
<h2 class="store-layout__footer-heading">{{ section.heading }}</h2>
<div class="d-grid gap-2"> <div class="d-grid gap-2">
@for (link of section.links; track link) { @for (link of section.links; track link.label) {
<span class="store-layout__muted-text">{{ link }}</span> @if (link.routerLink) {
<a
class="store-layout__footer-link"
[routerLink]="link.routerLink"
>
{{ link.label }}
</a>
} @else if (link.action === 'logout') {
<button
type="button"
class="store-layout__footer-link store-layout__footer-action"
(click)="logoutClick.emit()"
>
{{ link.label }}
</button>
} @else {
<span class="store-layout__muted-text">{{ link.label }}</span>
}
} }
</div> </div>
</div> </div>
} }
<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 align-content-start">
<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 socialMedia; track social.code) { @for (social of socialMedia; track social.code) {
<app-social-media <app-social-media

View File

@@ -3,6 +3,7 @@
} }
.store-layout__brand-slot { .store-layout__brand-slot {
width: min(100%, 12rem);
min-width: 10rem; min-width: 10rem;
} }
@@ -18,6 +19,7 @@
height: 100%; height: 100%;
max-height: 2.75rem; max-height: 2.75rem;
object-fit: contain; object-fit: contain;
object-position: left center;
} }
.store-layout__brand-logo-skeleton { .store-layout__brand-logo-skeleton {
@@ -55,17 +57,30 @@
color: #ffffff; color: #ffffff;
} }
.store-layout__footer-heading {
margin: 0;
font-size: 0.95rem;
font-weight: 500;
color: #f8f8f8;
}
.store-layout__muted-text { .store-layout__muted-text {
color: rgba(255, 255, 255, 0.72); color: rgba(255, 255, 255, 0.72);
} }
.store-layout__footer-link {
width: fit-content;
color: rgba(255, 255, 255, 0.72);
text-decoration: none;
&:hover,
&:focus-visible {
color: #ffffff;
text-decoration: underline;
}
}
.store-layout__footer-action {
padding: 0;
border: 0;
background: transparent;
font: inherit;
text-align: left;
}
@keyframes store-footer-logo-skeleton { @keyframes store-footer-logo-skeleton {
from { from {
background-position: 200% 0; background-position: 200% 0;

View File

@@ -1,15 +1,22 @@
import { Component, Input } from '@angular/core'; import { Component, Input, output } from '@angular/core';
import { RouterLink } from '@angular/router';
import { SocialMedia } from '../../../services/tenant.interface'; import { SocialMedia } from '../../../services/tenant.interface';
import { SocialMediaComponent } from '../../../../shared/components/social-media/social-media.component'; import { SocialMediaComponent } from '../../../../shared/components/social-media/social-media.component';
export type StoreFooterLink = {
label: string;
routerLink?: string;
action?: 'logout';
};
export type StoreFooterSection = { export type StoreFooterSection = {
heading: string; heading: string;
links: string[]; links: StoreFooterLink[];
}; };
@Component({ @Component({
selector: 'app-store-footer', selector: 'app-store-footer',
imports: [SocialMediaComponent], imports: [RouterLink, SocialMediaComponent],
templateUrl: './store-footer.component.html', templateUrl: './store-footer.component.html',
styleUrl: './store-footer.component.scss', styleUrl: './store-footer.component.scss',
}) })
@@ -18,4 +25,5 @@ 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;
readonly logoutClick = output<void>();
} }

View File

@@ -40,5 +40,6 @@
[footerSections]="footerSections" [footerSections]="footerSections"
[socialMedia]="tenant()?.social_media ?? []" [socialMedia]="tenant()?.social_media ?? []"
[logoUrl]="tenant()?.footer_logo ?? null" [logoUrl]="tenant()?.footer_logo ?? null"
(logoutClick)="onLogoutClick()"
/> />
</div> </div>

View File

@@ -213,6 +213,35 @@ describe('StoreLayoutComponent', () => {
expect(router.navigate).toHaveBeenCalledWith(['/login']); expect(router.navigate).toHaveBeenCalledWith(['/login']);
}); });
it('provides account actions from the footer', () => {
const authService = TestBed.inject(AuthService);
const router = TestBed.inject(Router);
vi.spyOn(router, 'navigate');
const fixture = TestBed.createComponent(StoreLayoutComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
const footerLinks = Array.from(
compiled.querySelectorAll<HTMLAnchorElement>('app-store-footer a.store-layout__footer-link'),
);
const accountLink = footerLinks.find((link) => link.textContent?.trim() === 'Mi cuenta');
const purchasesLink = footerLinks.find((link) => link.textContent?.trim() === 'Mis compras');
const logoutButton = Array.from(
compiled.querySelectorAll<HTMLButtonElement>('app-store-footer button'),
).find((button) => button.textContent?.trim() === 'Cerrar sesion');
expect(accountLink?.getAttribute('href')).toBe('/mi-cuenta/datos-personales');
expect(purchasesLink?.getAttribute('href')).toBe('/mi-cuenta/compras');
expect(logoutButton).toBeDefined();
logoutButton!.click();
fixture.detectChanges();
expect(authService.logout).toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith(['/login']);
});
it('redirects to /checkout when cart buy button is clicked', () => { it('redirects to /checkout when cart buy button is clicked', () => {
const router = TestBed.inject(Router); const router = TestBed.inject(Router);
vi.spyOn(router, 'navigate'); vi.spyOn(router, 'navigate');

View File

@@ -110,11 +110,19 @@ export class StoreLayoutComponent implements OnInit {
protected readonly footerSections: StoreFooterSection[] = [ protected readonly footerSections: StoreFooterSection[] = [
{ {
heading: 'Cuenta', heading: 'Cuenta',
links: ['Mi cuenta', 'Mis compras', 'Cerrar sesion'], links: [
{ label: 'Mi cuenta', routerLink: '/mi-cuenta/datos-personales' },
{ label: 'Mis compras', routerLink: '/mi-cuenta/compras' },
{ label: 'Cerrar sesion', action: 'logout' },
],
}, },
{ {
heading: 'Ayuda', heading: 'Ayuda',
links: ['Contacto', 'Ayuda', 'Preguntas frecuentes'], links: [
{ label: 'Contacto' },
{ label: 'Ayuda' },
{ label: 'Preguntas frecuentes' },
],
}, },
]; ];

View File

@@ -1,4 +1,4 @@
<div class="account-layout"> <div class="sidebar-layout">
<app-account-sidebar class="account-sidebar"></app-account-sidebar> <app-account-sidebar class="account-sidebar"></app-account-sidebar>
<div class="account-content"> <div class="account-content">
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@@ -1,4 +1,4 @@
.account-layout { .sidebar-layout {
display: flex; display: flex;
margin: 0 auto; margin: 0 auto;
gap: 100px; gap: 100px;

View File

@@ -3,10 +3,10 @@ import { RouterOutlet } from '@angular/router';
import { AccountSidebar } from '../components/account-sidebar/account-sidebar'; import { AccountSidebar } from '../components/account-sidebar/account-sidebar';
@Component({ @Component({
selector: 'app-account-layout', selector: 'app-sidebar-layout',
standalone: true, standalone: true,
imports: [RouterOutlet, AccountSidebar], imports: [RouterOutlet, AccountSidebar],
templateUrl: './account-layout.html', templateUrl: './sidebar-layout.html',
styleUrl: './account-layout.scss', styleUrl: './sidebar-layout.scss',
}) })
export class AccountLayout {} export class SidebarLayout {}

View File

@@ -81,7 +81,9 @@ export const routes: Routes = [
path: 'mi-cuenta', path: 'mi-cuenta',
canActivate: [authGuard], canActivate: [authGuard],
loadComponent: () => loadComponent: () =>
import('./pages/account-page/account-layout/account-layout').then((m) => m.AccountLayout), import('./pages/account-page/sidebar-layout/sidebar-layout').then(
(m) => m.SidebarLayout,
),
children: [ children: [
{ {
path: 'datos-personales', path: 'datos-personales',