diff --git a/src/app/app.routes.spec.ts b/src/app/app.routes.spec.ts new file mode 100644 index 0000000..110d968 --- /dev/null +++ b/src/app/app.routes.spec.ts @@ -0,0 +1,44 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter, Router } from '@angular/router'; + +import { App } from './app'; +import { routes } from './app.routes'; + +async function renderAppAt(url: string) { + TestBed.resetTestingModule(); + + await TestBed.configureTestingModule({ + imports: [App], + providers: [provideRouter(routes)] + }).compileComponents(); + + const router = TestBed.inject(Router); + const fixture = TestBed.createComponent(App); + + fixture.detectChanges(); + await router.navigateByUrl(url); + await fixture.whenStable(); + fixture.detectChanges(); + + return { fixture, router }; +} + +describe('app routes', () => { + it('loads the store layout at /store', async () => { + const { fixture, router } = await renderAppAt('/store'); + const compiled = fixture.nativeElement as HTMLElement; + + expect(router.url).toBe('/store'); + expect(compiled.querySelector('.store-layout')).not.toBeNull(); + expect(compiled.querySelector('.store-home')).not.toBeNull(); + expect(compiled.textContent).toContain('Tienda en construccion'); + }); + + it('keeps the root redirect under componentes-test', async () => { + const { fixture, router } = await renderAppAt('/'); + const compiled = fixture.nativeElement as HTMLElement; + + expect(router.url).toBe('/componentes-test/reutilizables'); + expect(compiled.textContent).toContain('Componentes reutilizables'); + }); +}); diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index c1ff96f..4d3c5b6 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -6,6 +6,10 @@ export const routes: Routes = [ loadChildren: () => import('./features/componentes-test/componentes-test.routes').then((m) => m.routes) }, + { + path: 'store', + loadChildren: () => import('./features/store/store.routes').then((m) => m.routes) + }, { path: '', pathMatch: 'full', diff --git a/src/app/core/layout/store-layout/store-layout.component.html b/src/app/core/layout/store-layout/store-layout.component.html new file mode 100644 index 0000000..c17f63a --- /dev/null +++ b/src/app/core/layout/store-layout/store-layout.component.html @@ -0,0 +1,110 @@ +
+
+
+
+
+ + SONDER +
+ +
+ + +
+ @for (action of headerActions; track action.label) { + + } +
+
+
+
+ +
+
+ +
+
+
+ +
+ +
+ + +
diff --git a/src/app/core/layout/store-layout/store-layout.component.scss b/src/app/core/layout/store-layout/store-layout.component.scss new file mode 100644 index 0000000..2325374 --- /dev/null +++ b/src/app/core/layout/store-layout/store-layout.component.scss @@ -0,0 +1,288 @@ +:host { + display: flex; + min-height: 100dvh; + background-color: #f6f6f6; + color: #202020; +} + +.store-layout { + display: flex; + flex: 1; + flex-direction: column; +} + +.store-layout__container { + width: min(100% - 3rem, 75rem); + margin: 0 auto; +} + +.store-layout__header { + background-color: #ffffff; + border-bottom: 1px solid #e9e9e9; +} + +.store-layout__top-bar { + border-bottom: 1px solid #efefef; +} + +.store-layout__top-content { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + min-height: 5.5rem; + padding: 1rem 0; +} + +.store-layout__brand-slot { + display: inline-flex; + align-items: center; + gap: 0.75rem; + min-width: 10rem; +} + +.store-layout__brand-mark { + display: grid; + place-items: center; + width: 2.75rem; + 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__tools { + display: flex; + align-items: center; + gap: 1rem; + margin-left: auto; +} + +.store-layout__search { + display: flex; + align-items: center; + width: min(100%, 24rem); + min-width: 18rem; + border: 1px solid #313131; + border-radius: 0.35rem; + background-color: #ffffff; + overflow: hidden; +} + +.store-layout__search-input { + flex: 1; + min-width: 0; + padding: 0.7rem 0.85rem; + border: 0; + font-size: 0.95rem; + background: transparent; + + &:focus { + outline: none; + } +} + +.store-layout__search-button, +.store-layout__header-action, +.store-layout__category-trigger { + border: 0; + background: transparent; +} + +.store-layout__search-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.75rem; + height: 2.75rem; + color: #2c2c2c; +} + +.store-layout__actions { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.store-layout__header-action { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + font-size: 1rem; + color: #2c2c2c; +} + +.store-layout__category-bar { + min-height: 2.35rem; +} + +.store-layout__category-trigger { + display: inline-flex; + align-items: center; + gap: 0.4rem; + min-height: 2.35rem; + padding: 0; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: uppercase; + color: #202020; +} + +.store-layout__main { + flex: 1; + display: block; +} + +.store-layout__footer { + margin-top: auto; + background-color: #323232; + color: #f3f3f3; +} + +.store-layout__footer-content { + display: grid; + grid-template-columns: minmax(16rem, 1.6fr) repeat(2, minmax(10rem, 1fr)) auto; + gap: 2rem; + padding: 2.5rem 0; +} + +.store-layout__footer-brand-group, +.store-layout__footer-section, +.store-layout__social-group { + display: grid; + align-content: start; + gap: 0.9rem; +} + +.store-layout__brand-slot--footer { + min-width: 0; +} + +.store-layout__brand-mark--footer { + border-color: rgba(255, 255, 255, 0.35); + color: #ffffff; +} + +.store-layout__brand-text--footer { + color: #ffffff; +} + +.store-layout__contact-list { + display: grid; + gap: 0.45rem; +} + +.store-layout__contact-item { + display: flex; + align-items: center; + gap: 0.6rem; + font-size: 0.95rem; + color: #ececec; +} + +.store-layout__copyright { + margin: 0; + font-size: 0.85rem; + color: #d3d3d3; +} + +.store-layout__footer-heading { + margin: 0; + font-size: 0.95rem; + font-weight: 500; + color: #f8f8f8; +} + +.store-layout__footer-links { + display: grid; + gap: 0.55rem; +} + +.store-layout__footer-link { + font-size: 0.95rem; + color: #ececec; +} + +.store-layout__social-group { + justify-items: start; +} + +.store-layout__social-links { + display: flex; + align-items: center; + gap: 0.85rem; + padding-top: 0.1rem; +} + +.store-layout__social-link { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 1.1rem; + color: #ffffff; +} + +@media (max-width: 991.98px) { + .store-layout__top-content { + flex-wrap: wrap; + } + + .store-layout__tools { + flex: 1 1 100%; + justify-content: space-between; + } + + .store-layout__search { + flex: 1 1 auto; + width: auto; + } + + .store-layout__footer-content { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 767.98px) { + .store-layout__container { + width: min(100% - 1.5rem, 75rem); + } + + .store-layout__brand-slot { + min-width: 0; + } + + .store-layout__brand-text { + font-size: 1.45rem; + } + + .store-layout__tools { + flex-direction: column; + align-items: stretch; + } + + .store-layout__search { + min-width: 0; + } + + .store-layout__actions { + justify-content: flex-end; + } + + .store-layout__footer-content { + grid-template-columns: minmax(0, 1fr); + gap: 1.5rem; + padding: 2rem 0; + } +} diff --git a/src/app/core/layout/store-layout/store-layout.component.spec.ts b/src/app/core/layout/store-layout/store-layout.component.spec.ts new file mode 100644 index 0000000..842364c --- /dev/null +++ b/src/app/core/layout/store-layout/store-layout.component.spec.ts @@ -0,0 +1,41 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; + +import { StoreLayoutComponent } from './store-layout.component'; + +describe('StoreLayoutComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StoreLayoutComponent], + providers: [provideRouter([])] + }).compileComponents(); + }); + + it('renders the main layout sections', () => { + const fixture = TestBed.createComponent(StoreLayoutComponent); + fixture.detectChanges(); + + const compiled = fixture.nativeElement as HTMLElement; + + expect(compiled.querySelector('.store-layout__header')).not.toBeNull(); + expect(compiled.querySelector('.store-layout__category-trigger')).not.toBeNull(); + expect(compiled.querySelector('router-outlet')).not.toBeNull(); + expect(compiled.querySelector('.store-layout__footer')).not.toBeNull(); + }); + + it('renders the placeholder branding and action icons', () => { + const fixture = TestBed.createComponent(StoreLayoutComponent); + fixture.detectChanges(); + + const compiled = fixture.nativeElement as HTMLElement; + + expect(compiled.querySelector('[data-testid="store-header-brand-slot"]')).not.toBeNull(); + expect(compiled.querySelector('[data-testid="store-footer-brand-slot"]')).not.toBeNull(); + expect(compiled.querySelector('.fa-cart-shopping')).not.toBeNull(); + expect(compiled.querySelector('.fa-circle-user')).not.toBeNull(); + expect(compiled.querySelector('.fa-instagram')).not.toBeNull(); + expect(compiled.querySelector('.fa-whatsapp')).not.toBeNull(); + expect(compiled.querySelector('.fa-facebook')).not.toBeNull(); + expect(compiled.querySelector('.fa-linkedin-in')).not.toBeNull(); + }); +}); diff --git a/src/app/core/layout/store-layout/store-layout.component.ts b/src/app/core/layout/store-layout/store-layout.component.ts new file mode 100644 index 0000000..b71f3d4 --- /dev/null +++ b/src/app/core/layout/store-layout/store-layout.component.ts @@ -0,0 +1,68 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +type StoreHeaderAction = { + label: string; + iconClass: string; +}; + +type StoreFooterSection = { + heading: string; + links: string[]; +}; + +type StoreSocialLink = { + label: string; + iconClass: string; +}; + +@Component({ + selector: 'app-store-layout', + imports: [RouterOutlet], + templateUrl: './store-layout.component.html', + styleUrl: './store-layout.component.scss' +}) +export class StoreLayoutComponent { + protected readonly currentYear = new Date().getFullYear(); + + protected readonly headerActions: StoreHeaderAction[] = [ + { + label: 'Carrito', + iconClass: 'fa-solid fa-cart-shopping' + }, + { + label: 'Mi cuenta', + iconClass: 'fa-solid fa-circle-user' + } + ]; + + protected readonly footerSections: StoreFooterSection[] = [ + { + heading: 'Cuenta', + links: ['Mi cuenta', 'Mis compras', 'Cerrar sesion'] + }, + { + heading: 'Ayuda', + links: ['Contacto', 'Ayuda', 'Preguntas frecuentes'] + } + ]; + + 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' + } + ]; +} diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.html b/src/app/features/store/pages/store-home-page/store-home-page.component.html new file mode 100644 index 0000000..759ca67 --- /dev/null +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.html @@ -0,0 +1,21 @@ +
+
+
+ Store Layout +

Tienda en construccion

+

+ Esta pagina placeholder deja visible el shell del store mientras se conectan categorias, + catalogo y acciones reales. +

+
+ +
+ @for (highlight of highlights; track highlight) { +
+ +

{{ highlight }}

+
+ } +
+
+
diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.scss b/src/app/features/store/pages/store-home-page/store-home-page.component.scss new file mode 100644 index 0000000..8953f81 --- /dev/null +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.scss @@ -0,0 +1,96 @@ +:host { + display: block; +} + +.store-home { + padding: 3rem 0 4rem; +} + +.store-home__inner { + width: min(100% - 3rem, 75rem); + margin: 0 auto; + display: grid; + gap: 1.5rem; +} + +.store-home__hero { + padding: clamp(2rem, 4vw, 3rem); + border: 1px solid #e4e4e4; + border-radius: 1.5rem; + background: linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%); + box-shadow: 0 1.2rem 2.2rem rgba(17, 17, 17, 0.06); +} + +.store-home__eyebrow { + display: inline-block; + margin-bottom: 0.75rem; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #666666; +} + +h1 { + margin-bottom: 0.75rem; + font-size: clamp(2rem, 4vw, 3.25rem); + font-weight: 700; + letter-spacing: -0.05em; +} + +.store-home__lead { + max-width: 42rem; + margin: 0; + font-size: 1rem; + line-height: 1.6; + color: #4a4a4a; +} + +.store-home__highlights { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +} + +.store-home__card { + display: grid; + gap: 0.9rem; + padding: 1.5rem; + border: 1px solid #e4e4e4; + border-radius: 1.25rem; + background-color: #ffffff; + box-shadow: 0 0.75rem 1.5rem rgba(17, 17, 17, 0.04); +} + +.store-home__card-index { + font-size: 0.85rem; + font-weight: 700; + letter-spacing: 0.08em; + color: #707070; +} + +.store-home__card p { + margin: 0; + line-height: 1.6; + color: #333333; +} + +@media (max-width: 991.98px) { + .store-home__highlights { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 767.98px) { + .store-home { + padding: 2rem 0 3rem; + } + + .store-home__inner { + width: min(100% - 1.5rem, 75rem); + } + + .store-home__highlights { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.ts b/src/app/features/store/pages/store-home-page/store-home-page.component.ts new file mode 100644 index 0000000..b708858 --- /dev/null +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-store-home-page', + templateUrl: './store-home-page.component.html', + styleUrl: './store-home-page.component.scss' +}) +export class StoreHomePageComponent { + protected readonly highlights = [ + 'Header y footer listos para conectar con datos reales.', + 'Barra de categorias reservada para sumar dropdown despues.', + 'Placeholder inicial para validar layout, espaciados y responsive.' + ]; +} diff --git a/src/app/features/store/store.routes.ts b/src/app/features/store/store.routes.ts new file mode 100644 index 0000000..01a0110 --- /dev/null +++ b/src/app/features/store/store.routes.ts @@ -0,0 +1,17 @@ +import { Routes } from '@angular/router'; + +import { StoreLayoutComponent } from '../../core/layout/store-layout/store-layout.component'; +import { StoreHomePageComponent } from './pages/store-home-page/store-home-page.component'; + +export const routes: Routes = [ + { + path: '', + component: StoreLayoutComponent, + children: [ + { + path: '', + component: StoreHomePageComponent + } + ] + } +]; diff --git a/src/environments/environment.local.ts b/src/environments/environment.local.ts new file mode 100644 index 0000000..8c83d8c --- /dev/null +++ b/src/environments/environment.local.ts @@ -0,0 +1,6 @@ +export const environment = { + production: false, + nombre:"Desarrollo - activo", + url:"http://localhost:8000/api/", + urlDescarga:"url/" +}; diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 0000000..9bb711c --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,6 @@ +export const environment = { + production: false, + nombre:"Desarrollo - activo", + url:"url/api/", + urlDescarga:"url/" +};