diff --git a/src/app/core/guards/menu.guard.ts b/src/app/core/guards/menu.guard.ts index f388316..0490df5 100644 --- a/src/app/core/guards/menu.guard.ts +++ b/src/app/core/guards/menu.guard.ts @@ -1,13 +1,7 @@ import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; +import { findMenu } from '../services/menu.utils'; import { TenantService } from '../services/tenant.service'; -import { Menu } from '../services/tenant.interface'; - -function containsMenu(menues: Menu[], menuCode: string): boolean { - return menues.some( - menu => menu.code === menuCode || containsMenu(menu.submenues ?? [], menuCode) - ); -} export const hasMenuGuard = (menuCode: string): CanActivateFn => { return () => { @@ -20,7 +14,7 @@ export const hasMenuGuard = (menuCode: string): CanActivateFn => { return router.createUrlTree(['/']); } - const hasMenu = containsMenu(tenant.menues ?? [], menuCode); + const hasMenu = findMenu(tenant.menues ?? [], menuCode) !== undefined; if (hasMenu) { return true; diff --git a/src/app/core/layout/store-layout/store-layout.component.ts b/src/app/core/layout/store-layout/store-layout.component.ts index 64e0bf6..22d56d5 100644 --- a/src/app/core/layout/store-layout/store-layout.component.ts +++ b/src/app/core/layout/store-layout/store-layout.component.ts @@ -120,8 +120,8 @@ export class StoreLayoutComponent implements OnInit { heading: 'Ayuda', links: [ { label: 'Contacto' }, - { label: 'Ayuda' }, - { label: 'Preguntas frecuentes' }, + { label: 'Ayuda', routerLink: '/ayuda' }, + { label: 'Preguntas frecuentes', routerLink: '/ayuda/preguntas-frecuentes' }, ], }, ]; diff --git a/src/app/core/services/menu.utils.ts b/src/app/core/services/menu.utils.ts new file mode 100644 index 0000000..3919613 --- /dev/null +++ b/src/app/core/services/menu.utils.ts @@ -0,0 +1,17 @@ +import { Menu } from './tenant.interface'; + +export function findMenu(menues: readonly Menu[], menuCode: string): Menu | undefined { + for (const menu of menues) { + if (menu.code === menuCode) { + return menu; + } + + const submenu = findMenu(menu.submenues ?? [], menuCode); + + if (submenu) { + return submenu; + } + } + + return undefined; +} diff --git a/src/app/features/store/pages/help-page/components/help-sidebar/help-sidebar.html b/src/app/features/store/pages/help-page/components/help-sidebar/help-sidebar.html new file mode 100644 index 0000000..f5768c0 --- /dev/null +++ b/src/app/features/store/pages/help-page/components/help-sidebar/help-sidebar.html @@ -0,0 +1,22 @@ + diff --git a/src/app/features/store/pages/help-page/components/help-sidebar/help-sidebar.scss b/src/app/features/store/pages/help-page/components/help-sidebar/help-sidebar.scss new file mode 100644 index 0000000..9c79cee --- /dev/null +++ b/src/app/features/store/pages/help-page/components/help-sidebar/help-sidebar.scss @@ -0,0 +1,60 @@ +.help-sidebar { + display: flex; + flex-direction: column; +} + +.help-sidebar__title { + margin: 0 0 0.75rem; + color: #1a1a1a; + font-size: 1rem; + font-weight: 700; + line-height: 1.25; +} + +.help-sidebar__divider { + height: 1px; + margin-bottom: 1.25rem; + background: #dedede; +} + +.help-sidebar__nav { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.help-sidebar__link { + color: #888888; + font-size: 0.875rem; + font-weight: 500; + line-height: 1.35; + text-decoration: none; + transition: color 150ms ease; + + &:hover, + &:focus-visible { + color: #5a5a5a; + } + + &:focus-visible { + border-radius: 2px; + outline: 2px solid var(--color-primary); + outline-offset: 3px; + } +} + +.help-sidebar__link--active { + color: var(--color-primary); + font-weight: 600; +} + +.help-sidebar__link--unavailable { + cursor: default; +} + +@media (max-width: 767.98px) { + .help-sidebar__nav { + flex-flow: row wrap; + gap: 0.75rem 1.25rem; + } +} diff --git a/src/app/features/store/pages/help-page/components/help-sidebar/help-sidebar.ts b/src/app/features/store/pages/help-page/components/help-sidebar/help-sidebar.ts new file mode 100644 index 0000000..5c26e93 --- /dev/null +++ b/src/app/features/store/pages/help-page/components/help-sidebar/help-sidebar.ts @@ -0,0 +1,42 @@ +import { Component, computed, inject } from '@angular/core'; +import { RouterLink, RouterLinkActive } from '@angular/router'; + +import { findMenu } from '../../../../../../core/services/menu.utils'; +import { Menu } from '../../../../../../core/services/tenant.interface'; +import { TenantService } from '../../../../../../core/services/tenant.service'; + +const MENU_LABELS: Record = { + 'help.faq': 'Preguntas frecuentes', + 'help.contact': 'Contacto', + 'help.payment-methods': 'Medios de pago', + 'help.shipping': 'Envíos', + 'help.terms-and-conditions': 'Términos y condiciones', +}; + +@Component({ + selector: 'app-help-sidebar', + imports: [RouterLink, RouterLinkActive], + templateUrl: './help-sidebar.html', + styleUrl: './help-sidebar.scss', +}) +export class HelpSidebar { + private readonly tenantService = inject(TenantService); + + protected readonly submenues = computed( + () => findMenu(this.tenantService.tenant()?.menues ?? [], 'help')?.submenues ?? [], + ); + + protected menuLabel(menu: Menu): string { + return MENU_LABELS[menu.code] ?? this.fallbackLabel(menu.code); + } + + protected isAvailable(menu: Menu): boolean { + return menu.code === 'help.faq'; + } + + private fallbackLabel(code: string): string { + const label = code.split('.').at(-1)?.replaceAll('-', ' ') ?? code; + + return label.charAt(0).toUpperCase() + label.slice(1); + } +} diff --git a/src/app/features/store/pages/help-page/pages/faq-page/faq-page.html b/src/app/features/store/pages/help-page/pages/faq-page/faq-page.html new file mode 100644 index 0000000..2d62fb4 --- /dev/null +++ b/src/app/features/store/pages/help-page/pages/faq-page/faq-page.html @@ -0,0 +1,19 @@ +
+

PREGUNTAS FRECUENTES

+ + @if (faqs().length > 0) { + + @for (faq of faqs(); track $index; let last = $last) { + +

{{ faq.answer }}

+
+ } +
+ } @else { +

No hay preguntas frecuentes disponibles.

+ } +
diff --git a/src/app/features/store/pages/help-page/pages/faq-page/faq-page.scss b/src/app/features/store/pages/help-page/pages/faq-page/faq-page.scss new file mode 100644 index 0000000..04f180d --- /dev/null +++ b/src/app/features/store/pages/help-page/pages/faq-page/faq-page.scss @@ -0,0 +1,28 @@ +:host { + display: block; + width: 100%; +} + +.faq-page { + width: 100%; + max-width: 760px; +} + +.faq-page__title { + margin: 0 0 0.5rem; + color: #a0a0a0; + font-size: 0.9375rem; + font-weight: 700; + line-height: 1.3; +} + +.faq-page p { + margin: 0; +} + +.faq-page__empty { + padding: 1rem 0; + border-bottom: 1px solid #dedede; + color: #888888; + font-size: 0.8125rem; +} diff --git a/src/app/features/store/pages/help-page/pages/faq-page/faq-page.spec.ts b/src/app/features/store/pages/help-page/pages/faq-page/faq-page.spec.ts new file mode 100644 index 0000000..5e949c3 --- /dev/null +++ b/src/app/features/store/pages/help-page/pages/faq-page/faq-page.spec.ts @@ -0,0 +1,82 @@ +import { signal } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Tenant } from '../../../../../../core/services/tenant.interface'; +import { TenantService } from '../../../../../../core/services/tenant.service'; +import { FaqPage } from './faq-page'; + +const tenant: Tenant = { + id: 1, + codigo: 'test', + nombre: 'Test', + dominio: 'localhost', + primary_color: '#6376f3', + secondary_color: '#a0a0a0', + danger_color: '#ff8888', + success_color: '#198754', + header_bg_color: '#ffffff', + footer_bg_color: '#202020', + header_logo: '', + footer_logo: '', + menues: [ + { + id: 1, + code: 'help', + parent_menu_code: null, + content_type: 'dynamic', + route: '/ayuda', + submenues: [ + { + id: 2, + code: 'help.faq', + parent_menu_code: 'help', + content_type: 'static', + route: '/ayuda/preguntas-frecuentes', + static_content: [ + { + pregunta: '¿Hay algún límite de compra?', + respuesta: 'No hay un límite general de compra.', + }, + { + pregunta: '¿Cuáles son los medios de pago disponibles?', + respuesta: 'Podés seleccionarlos al finalizar tu compra.', + }, + ], + submenues: [], + }, + ], + }, + ], +}; + +describe('FaqPage', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FaqPage], + providers: [ + { + provide: TenantService, + useValue: { + tenant: signal(tenant).asReadonly(), + }, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(FaqPage); + fixture.detectChanges(); + }); + + it('renders the tenant FAQ content using the reusable accordion', () => { + const element = fixture.nativeElement as HTMLElement; + const buttons = element.querySelectorAll('.accordion-button'); + + expect(element.textContent).toContain('PREGUNTAS FRECUENTES'); + expect(buttons).toHaveLength(2); + expect(buttons[0].textContent).toContain('¿Hay algún límite de compra?'); + expect(buttons[1].getAttribute('aria-expanded')).toBe('true'); + expect(element.textContent).toContain('Podés seleccionarlos al finalizar tu compra.'); + }); +}); diff --git a/src/app/features/store/pages/help-page/pages/faq-page/faq-page.ts b/src/app/features/store/pages/help-page/pages/faq-page/faq-page.ts new file mode 100644 index 0000000..7d84d5a --- /dev/null +++ b/src/app/features/store/pages/help-page/pages/faq-page/faq-page.ts @@ -0,0 +1,47 @@ +import { Component, computed, inject } from '@angular/core'; + +import { findMenu } from '../../../../../../core/services/menu.utils'; +import { TenantService } from '../../../../../../core/services/tenant.service'; +import { AccordionItemComponent } from '../../../../../../shared/components/accordion/accordion-item.component'; +import { AccordionComponent } from '../../../../../../shared/components/accordion/accordion.component'; + +type Faq = { + question: string; + answer: string; +}; + +@Component({ + selector: 'app-faq-page', + imports: [AccordionComponent, AccordionItemComponent], + templateUrl: './faq-page.html', + styleUrl: './faq-page.scss', +}) +export class FaqPage { + private readonly tenantService = inject(TenantService); + + protected readonly faqs = computed(() => { + const staticContent = findMenu( + this.tenantService.tenant()?.menues ?? [], + 'help.faq', + )?.static_content; + + if (!Array.isArray(staticContent)) { + return []; + } + + return staticContent.flatMap((item): Faq[] => { + if ( + typeof item !== 'object' || + item === null || + !('pregunta' in item) || + !('respuesta' in item) || + typeof item.pregunta !== 'string' || + typeof item.respuesta !== 'string' + ) { + return []; + } + + return [{ question: item.pregunta, answer: item.respuesta }]; + }); + }); +} diff --git a/src/app/features/store/pages/help-page/sidebar-layout/help-sidebar-layout.html b/src/app/features/store/pages/help-page/sidebar-layout/help-sidebar-layout.html new file mode 100644 index 0000000..2658476 --- /dev/null +++ b/src/app/features/store/pages/help-page/sidebar-layout/help-sidebar-layout.html @@ -0,0 +1,7 @@ +
+ + +
+ +
+
diff --git a/src/app/features/store/pages/help-page/sidebar-layout/help-sidebar-layout.scss b/src/app/features/store/pages/help-page/sidebar-layout/help-sidebar-layout.scss new file mode 100644 index 0000000..d1ca2d4 --- /dev/null +++ b/src/app/features/store/pages/help-page/sidebar-layout/help-sidebar-layout.scss @@ -0,0 +1,25 @@ +:host { + display: block; +} + +.help-layout { + display: grid; + grid-template-columns: minmax(190px, 250px) minmax(0, 1fr); + gap: clamp(3rem, 8vw, 6.25rem); + width: 100%; +} + +.help-layout__sidebar { + min-width: 0; +} + +.help-layout__content { + min-width: 0; +} + +@media (max-width: 767.98px) { + .help-layout { + grid-template-columns: 1fr; + gap: 2rem; + } +} diff --git a/src/app/features/store/pages/help-page/sidebar-layout/help-sidebar-layout.ts b/src/app/features/store/pages/help-page/sidebar-layout/help-sidebar-layout.ts new file mode 100644 index 0000000..e030876 --- /dev/null +++ b/src/app/features/store/pages/help-page/sidebar-layout/help-sidebar-layout.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +import { HelpSidebar } from '../components/help-sidebar/help-sidebar'; + +@Component({ + selector: 'app-help-sidebar-layout', + imports: [RouterOutlet, HelpSidebar], + templateUrl: './help-sidebar-layout.html', + styleUrl: './help-sidebar-layout.scss', +}) +export class HelpSidebarLayout {} diff --git a/src/app/features/store/store.routes.ts b/src/app/features/store/store.routes.ts index 1953421..eedae57 100644 --- a/src/app/features/store/store.routes.ts +++ b/src/app/features/store/store.routes.ts @@ -77,6 +77,29 @@ export const routes: Routes = [ }, ], }, + { + path: 'ayuda', + canActivate: [hasMenuGuard('help')], + loadComponent: () => + import('./pages/help-page/sidebar-layout/help-sidebar-layout').then( + (m) => m.HelpSidebarLayout, + ), + children: [ + { + path: 'preguntas-frecuentes', + canActivate: [hasMenuGuard('help.faq')], + loadComponent: () => + import('./pages/help-page/pages/faq-page/faq-page').then( + (m) => m.FaqPage, + ), + }, + { + path: '', + redirectTo: 'preguntas-frecuentes', + pathMatch: 'full', + }, + ], + }, { path: 'mi-cuenta', canActivate: [authGuard], diff --git a/src/app/shared/components/accordion/accordion.component.scss b/src/app/shared/components/accordion/accordion.component.scss index c64f3a2..33a9217 100644 --- a/src/app/shared/components/accordion/accordion.component.scss +++ b/src/app/shared/components/accordion/accordion.component.scss @@ -8,4 +8,5 @@ --bs-accordion-border-radius: 0; --bs-accordion-inner-border-radius: 0; --bs-accordion-bg: transparent; + border-bottom: 1px solid var(--bs-accordion-border-color); }