ng-easycommerce-v18 0.3.22-beta.1 → 0.3.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +8 -2
  2. package/esm2022/lib/classes/filters/attributes-filter.mjs +74 -4
  3. package/esm2022/lib/classes/filters/category-filter.mjs +105 -26
  4. package/esm2022/lib/classes/filters/filter-factory.mjs +7 -3
  5. package/esm2022/lib/classes/filters/filter.mjs +27 -2
  6. package/esm2022/lib/classes/filters/price_range-filter.mjs +3 -3
  7. package/esm2022/lib/constants/core.constants.service.mjs +12 -1
  8. package/esm2022/lib/ec-components/blocks-ec/block-products-ec/block-products-ec.component.mjs +5 -3
  9. package/esm2022/lib/ec-components/collection-ec/collection-ec.component.mjs +41 -17
  10. package/esm2022/lib/ec-components/filters-ec/filters-ec.component.mjs +42 -9
  11. package/esm2022/lib/ec-components/header-ec/header-ec.component.mjs +42 -27
  12. package/esm2022/lib/ec-components/price-range-filter/price-range-filter.component.mjs +13 -2
  13. package/esm2022/lib/ec-components/related-products-ec/related-products-ec.component.mjs +6 -4
  14. package/esm2022/lib/ec-components/widgets-ec/magnizoom-ec/magnizoom-ec.component.mjs +4 -2
  15. package/esm2022/lib/ec-services/analytics/facebook-pixel.service.mjs +4 -2
  16. package/esm2022/lib/ec-services/analytics/google-analytics.service.mjs +4 -2
  17. package/esm2022/lib/ec-services/filters.service.mjs +124 -18
  18. package/esm2022/lib/ec-services/options.service.mjs +27 -3
  19. package/esm2022/lib/ec-services/pagination.service.mjs +70 -22
  20. package/esm2022/lib/ec-services/products.service.mjs +5 -3
  21. package/fesm2022/ng-easycommerce-v18.mjs +586 -126
  22. package/fesm2022/ng-easycommerce-v18.mjs.map +1 -1
  23. package/lib/classes/filters/attributes-filter.d.ts +24 -0
  24. package/lib/classes/filters/category-filter.d.ts +30 -3
  25. package/lib/constants/core.constants.service.d.ts +7 -0
  26. package/lib/ec-components/collection-ec/collection-ec.component.d.ts +5 -4
  27. package/lib/ec-components/filters-ec/filters-ec.component.d.ts +13 -0
  28. package/lib/ec-components/price-range-filter/price-range-filter.component.d.ts +2 -0
  29. package/lib/ec-services/filters.service.d.ts +18 -1
  30. package/lib/ec-services/options.service.d.ts +4 -0
  31. package/lib/ec-services/pagination.service.d.ts +21 -5
  32. package/lib/ec-services/products.service.d.ts +1 -1
  33. package/package.json +1 -1
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
2
2
  import { InjectionToken, makeEnvironmentProviders, inject, Injectable, PLATFORM_ID, RendererFactory2, afterNextRender, signal, EnvironmentInjector, runInInjectionContext, Component, ChangeDetectorRef, HostListener, CUSTOM_ELEMENTS_SCHEMA, Input, Pipe, Injector, EventEmitter, Output, forwardRef, afterRender, ViewChild, Inject, computed, Renderer2, ChangeDetectionStrategy, Directive } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
4
4
  import { DOCUMENT, isPlatformBrowser, AsyncPipe, CommonModule, TitleCasePipe, JsonPipe, UpperCasePipe, Location } from '@angular/common';
5
- import { take, BehaviorSubject, shareReplay, map, catchError, of, filter, ReplaySubject, firstValueFrom, concatMap, throwError, switchMap, combineLatest } from 'rxjs';
5
+ import { take, BehaviorSubject, shareReplay, map, catchError, of, filter, ReplaySubject, firstValueFrom, concatMap, throwError, tap, distinctUntilChanged, switchMap, combineLatest, Subject, takeUntil } from 'rxjs';
6
6
  import { HttpClient, HttpHeaders } from '@angular/common/http';
7
7
  import * as i1$1 from '@ngx-translate/core';
8
8
  import { TranslateService, TranslateModule } from '@ngx-translate/core';
@@ -243,6 +243,11 @@ class CoreConstantsService {
243
243
  * Guarda la variable window del web browser.
244
244
  */
245
245
  window;
246
+ /**
247
+ * Tipo de ruta actual (ej: 'categories', 'attributes', etc).
248
+ * Lo usamos para que PaginationService sepa qué filtro esperar.
249
+ */
250
+ currentRouteType = null;
246
251
  constructor() {
247
252
  if (isPlatformBrowser(this.platformId)) {
248
253
  this.window = window;
@@ -520,6 +525,12 @@ class CoreConstantsService {
520
525
  return this.apiConstants.CHANNEL;
521
526
  }
522
527
  form_sender = false;
528
+ setCurrentRouteType(type) {
529
+ this.currentRouteType = type;
530
+ }
531
+ getCurrentRouteType() {
532
+ return this.currentRouteType;
533
+ }
523
534
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CoreConstantsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
524
535
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CoreConstantsService, providedIn: 'root' });
525
536
  }
@@ -539,6 +550,10 @@ class OptionsService {
539
550
  * Maneja las peticiones a la API
540
551
  */
541
552
  connection = inject(ConnectionService);
553
+ /**
554
+ * Platform ID para verificar si estamos en el navegador
555
+ */
556
+ platformId = inject(PLATFORM_ID);
542
557
  /**
543
558
  * Constantes del core
544
559
  */
@@ -649,7 +664,26 @@ class OptionsService {
649
664
  * @returns
650
665
  */
651
666
  removeAccents(str) {
652
- return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
667
+ if (isPlatformBrowser(this.platformId) && typeof String.prototype.normalize === 'function') {
668
+ return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
669
+ }
670
+ // Fallback para SSR - remover acentos manualmente
671
+ return str.replace(/[àáâãäå]/g, 'a')
672
+ .replace(/[èéêë]/g, 'e')
673
+ .replace(/[ìíîï]/g, 'i')
674
+ .replace(/[òóôõö]/g, 'o')
675
+ .replace(/[ùúûü]/g, 'u')
676
+ .replace(/[ýÿ]/g, 'y')
677
+ .replace(/[ñ]/g, 'n')
678
+ .replace(/[ç]/g, 'c')
679
+ .replace(/[ÀÁÂÃÄÅ]/g, 'A')
680
+ .replace(/[ÈÉÊË]/g, 'E')
681
+ .replace(/[ÌÍÎÏ]/g, 'I')
682
+ .replace(/[ÒÓÔÕÖ]/g, 'O')
683
+ .replace(/[ÙÚÛÜ]/g, 'U')
684
+ .replace(/[ÝŸ]/g, 'Y')
685
+ .replace(/[Ñ]/g, 'N')
686
+ .replace(/[Ç]/g, 'C');
653
687
  }
654
688
  /**
655
689
  * Realiza un mapeo de los datos para darle un formato mas entendible a la
@@ -866,7 +900,9 @@ class FacebookPixelService {
866
900
  this.renderer.appendChild(this.document?.body, new_analityc_script);
867
901
  this.enabled = true;
868
902
  }
869
- setTimeout(() => this.callEvent('initialize'), 1000);
903
+ if (isPlatformBrowser(this.platformId)) {
904
+ setTimeout(() => this.callEvent('initialize'), 1000);
905
+ }
870
906
  }
871
907
  /**
872
908
  * Ejecuta el evento pasado por parametro.
@@ -1083,7 +1119,9 @@ class GoogleAnalyticsService {
1083
1119
  this.renderer.appendChild(this.document?.head, declaration);
1084
1120
  this.enabled = true;
1085
1121
  }
1086
- setTimeout(() => this.startListeningPageViews(gtm_id), 1000);
1122
+ if (isPlatformBrowser(this.platformId)) {
1123
+ setTimeout(() => this.startListeningPageViews(gtm_id), 1000);
1124
+ }
1087
1125
  }
1088
1126
  /**
1089
1127
  *
@@ -2748,6 +2786,31 @@ class User {
2748
2786
  }
2749
2787
  }
2750
2788
 
2789
+ /**
2790
+ * Función auxiliar para remover acentos de forma segura para SSR
2791
+ */
2792
+ function safeRemoveAccents(str) {
2793
+ if (typeof window !== 'undefined' && typeof String.prototype.normalize === 'function') {
2794
+ return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
2795
+ }
2796
+ // Fallback para SSR - remover acentos manualmente
2797
+ return str.replace(/[àáâãäå]/g, 'a')
2798
+ .replace(/[èéêë]/g, 'e')
2799
+ .replace(/[ìíîï]/g, 'i')
2800
+ .replace(/[òóôõö]/g, 'o')
2801
+ .replace(/[ùúûü]/g, 'u')
2802
+ .replace(/[ýÿ]/g, 'y')
2803
+ .replace(/[ñ]/g, 'n')
2804
+ .replace(/[ç]/g, 'c')
2805
+ .replace(/[ÀÁÂÃÄÅ]/g, 'A')
2806
+ .replace(/[ÈÉÊË]/g, 'E')
2807
+ .replace(/[ÌÍÎÏ]/g, 'I')
2808
+ .replace(/[ÒÓÔÕÖ]/g, 'O')
2809
+ .replace(/[ÙÚÛÜ]/g, 'U')
2810
+ .replace(/[ÝŸ]/g, 'Y')
2811
+ .replace(/[Ñ]/g, 'N')
2812
+ .replace(/[Ç]/g, 'C');
2813
+ }
2751
2814
  class Filter {
2752
2815
  data = [];
2753
2816
  multi = false;
@@ -2767,7 +2830,7 @@ class Filter {
2767
2830
  throw new Error("Method not implemented.");
2768
2831
  }
2769
2832
  removeAccents = (str) => {
2770
- return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
2833
+ return safeRemoveAccents(str);
2771
2834
  };
2772
2835
  setSelected(element, value) {
2773
2836
  //console.log(element, value);
@@ -2831,17 +2894,24 @@ class Filter {
2831
2894
  };
2832
2895
  }
2833
2896
 
2897
+ /**
2898
+ * CategoryFilter
2899
+ * - Gestiona el filtro de categorías.
2900
+ * - Mantiene la estructura de categorías (parents/children) y permite:
2901
+ * * Inicializar desde OptionsService.
2902
+ * * Marcar elementos según slug o query param.
2903
+ * * Generar porción de query string (&category=...).
2904
+ */
2834
2905
  class CategoryFilter extends Filter {
2835
- initialValues;
2836
2906
  _optionsService = inject(OptionsService);
2907
+ _filtersService = inject(FiltersService);
2837
2908
  data = [];
2838
2909
  multi = false;
2839
- constructor(initialValues) {
2910
+ constructor(initialValue) {
2840
2911
  super();
2841
- this.initialValues = initialValues;
2842
2912
  this._optionsService.getCategories().subscribe(res => {
2843
- //console.log(res);
2844
- this.setContent(res, initialValues);
2913
+ this.setContent(res, initialValue);
2914
+ this._filtersService.refreshFilters();
2845
2915
  });
2846
2916
  }
2847
2917
  type() {
@@ -2853,44 +2923,122 @@ class CategoryFilter extends Filter {
2853
2923
  this.data = categories;
2854
2924
  }
2855
2925
  getContent() {
2856
- throw new Error("Method not implemented.");
2926
+ return this.data;
2857
2927
  }
2858
- toUrlParams(actual_url = '', sublist) {
2859
- let elements = sublist || this.data;
2860
- elements.forEach((element) => {
2861
- if (element.type == 'sub' && !element.selected) {
2862
- const result = this.toUrlParams('', element.children);
2863
- !actual_url.includes(result) ? actual_url += result : null;
2864
- }
2865
- else if (element.selected) {
2866
- if (actual_url == '')
2867
- actual_url = '&category=' + element.value;
2868
- else
2869
- actual_url = element.value;
2870
- }
2871
- });
2872
- return actual_url;
2928
+ /**
2929
+ * toUrlParams
2930
+ * - Recorre la estructura y devuelve la porción de query string para la categoría seleccionada.
2931
+ * - Prioriza el primer selected encontrado (solo una categoría se envía).
2932
+ */
2933
+ toUrlParams() {
2934
+ const selectedCodes = [];
2935
+ const walk = (nodes) => {
2936
+ nodes.forEach(n => {
2937
+ if (n.selected) {
2938
+ selectedCodes.push(n.code || n.value);
2939
+ }
2940
+ if (Array.isArray(n.children) && n.children.length) {
2941
+ walk(n.children);
2942
+ }
2943
+ });
2944
+ };
2945
+ walk(this.data);
2946
+ if (selectedCodes.length > 0) {
2947
+ return `&category=${encodeURIComponent(selectedCodes[0])}`;
2948
+ }
2949
+ return '';
2873
2950
  }
2874
2951
  createElement(filter, original, initialValue) {
2875
2952
  if (filter.type == 'sub') {
2876
2953
  filter.multi = false;
2877
2954
  filter.shape = 'text';
2878
2955
  filter.value = original.code;
2879
- filter.selected = (initialValue && this.removeAccents(original.slug?.toLowerCase()) == this.removeAccents(initialValue?.toLowerCase()));
2956
+ filter.selected = (initialValue &&
2957
+ this.removeAccents(original.slug?.toLowerCase()) ===
2958
+ this.removeAccents(String(initialValue).toLowerCase()));
2880
2959
  filter.children = filter.children.map((child, i) => this.createElement(child, original.children[i], initialValue));
2881
2960
  }
2882
2961
  else {
2883
2962
  filter.value = original.code;
2884
- filter.selected = (initialValue && this.removeAccents(original.slug?.toLowerCase()) == this.removeAccents(initialValue?.toLowerCase()));
2963
+ filter.selected = (initialValue &&
2964
+ this.removeAccents(original.slug?.toLowerCase()) ===
2965
+ this.removeAccents(String(initialValue).toLowerCase()));
2885
2966
  }
2886
2967
  return filter;
2887
2968
  }
2969
+ /**
2970
+ * setFromSlug
2971
+ * - Marca como selected los nodos cuyo slug/path/name coincide con el slug proporcionado.
2972
+ * - Utiliza removeAccents para comparar sin tildes y en minúsculas.
2973
+ * - No retorna valor, solo modifica this.data.
2974
+ */
2975
+ setFromSlug(slug) {
2976
+ const target = this.removeAccents(String(slug).toLowerCase());
2977
+ const walk = (nodes) => {
2978
+ nodes.forEach(n => {
2979
+ // ADDED: iniciar sin selección explícita (asegura estado consistente)
2980
+ n.selected = false;
2981
+ const candidates = [
2982
+ n.slug,
2983
+ n.path && String(n.path).split('/').filter(Boolean).pop(),
2984
+ n.name
2985
+ ]
2986
+ .filter(Boolean)
2987
+ .map((s) => this.removeAccents(s.toLowerCase()));
2988
+ if (candidates.includes(target)) {
2989
+ n.selected = true;
2990
+ }
2991
+ if (Array.isArray(n.children) && n.children.length) {
2992
+ walk(n.children);
2993
+ }
2994
+ });
2995
+ };
2996
+ walk(this.data);
2997
+ }
2998
+ /**
2999
+ * hydrateFromQuery
3000
+ * - Marca la categoría según el parámetro de query 'category' si existe.
3001
+ * - Busca el code exacto en la estructura y marca el nodo correspondiente.
3002
+ * - Loggea si se encontró o no la categoría.
3003
+ */
3004
+ hydrateFromQuery(query) {
3005
+ const raw = query?.['category'];
3006
+ if (!raw)
3007
+ return;
3008
+ const code = String(raw).trim();
3009
+ if (!code)
3010
+ return;
3011
+ const mark = (nodes) => {
3012
+ let found = false;
3013
+ nodes.forEach(n => {
3014
+ if (n.code === code) {
3015
+ n.selected = true;
3016
+ found = true;
3017
+ }
3018
+ else {
3019
+ n.selected = false;
3020
+ }
3021
+ if (Array.isArray(n.children) && n.children.length) {
3022
+ if (mark(n.children))
3023
+ found = true;
3024
+ }
3025
+ });
3026
+ return found;
3027
+ };
3028
+ const ok = mark(this.data);
3029
+ }
2888
3030
  }
2889
3031
 
3032
+ /**
3033
+ * AttributesFilter
3034
+ * - Gestiona el filtro de atributos (estructura de padres/children).
3035
+ * - Soporta hidratación desde query params y aplicación de códigos seleccionados.
3036
+ */
2890
3037
  class AttributesFilter extends Filter {
2891
3038
  _optionsService = inject(OptionsService);
2892
3039
  data = [];
2893
3040
  multi = false;
3041
+ _codesFromQuery = null;
2894
3042
  constructor(initialValue, options) {
2895
3043
  super();
2896
3044
  if (options) {
@@ -2898,7 +3046,6 @@ class AttributesFilter extends Filter {
2898
3046
  }
2899
3047
  else {
2900
3048
  this._optionsService.getAttributes().subscribe(res => {
2901
- //console.log(res);
2902
3049
  this.setContent(res, initialValue);
2903
3050
  });
2904
3051
  }
@@ -2908,10 +3055,18 @@ class AttributesFilter extends Filter {
2908
3055
  const attributes = this._optionsService.generateMenu(options);
2909
3056
  attributes.forEach((filter) => filter = this.createElement(filter, options, initialValue));
2910
3057
  this.data = attributes;
3058
+ // Si ya teníamos códigos de la URL guardados, aplicarlos ahora
3059
+ if (this._codesFromQuery && this._codesFromQuery.length) {
3060
+ // ADDED: Aplicar códigos guardados tras cargar la data.
3061
+ this.applyCodes(this._codesFromQuery);
3062
+ }
2911
3063
  }
2912
3064
  getContent() {
2913
3065
  return this.data;
2914
3066
  }
3067
+ /**
3068
+ * ADDED: toUrlParams construye la porción de query string que representa las selecciones de atributos.
3069
+ */
2915
3070
  toUrlParams(actual_url = '&attributeCodes=', sublist, already) {
2916
3071
  let elements = sublist || this.data;
2917
3072
  let aux_url = '';
@@ -2933,12 +3088,14 @@ class AttributesFilter extends Filter {
2933
3088
  }
2934
3089
  }
2935
3090
  });
2936
- //console.log(actual_url + aux_url);
2937
3091
  return actual_url + aux_url;
2938
3092
  }
2939
3093
  ;
2940
3094
  cleanResult(text) { return text.replace('&attributeCodes=', ''); }
2941
3095
  ;
3096
+ /**
3097
+ * ADDED: createElement transforma cada nodo original en FilterElement y marca selección inicial si corresponde.
3098
+ */
2942
3099
  createElement(filter, original, initialValue) {
2943
3100
  if (filter.type == 'sub') {
2944
3101
  filter.multi = false;
@@ -2951,10 +3108,65 @@ class AttributesFilter extends Filter {
2951
3108
  else {
2952
3109
  filter.value = original.code;
2953
3110
  filter.multi = false;
2954
- filter.selected = (initialValue && this.removeAccents(original.slug?.toLowerCase()) == this.removeAccents(initialValue?.toLowerCase()));
3111
+ filter.selected = (initialValue &&
3112
+ this.removeAccents(original.slug?.toLowerCase()) === this.removeAccents(initialValue?.toLowerCase()));
2955
3113
  }
2956
3114
  return filter;
2957
3115
  }
3116
+ /** hidratar desde la URL */
3117
+ hydrateFromQuery(params) {
3118
+ const raw = params['attributeCodes'];
3119
+ if (!raw) {
3120
+ // nada en la URL → limpió filtro
3121
+ this._codesFromQuery = null;
3122
+ this.clearSelection();
3123
+ return;
3124
+ }
3125
+ const codes = String(raw)
3126
+ .split('<and>')
3127
+ .map(c => c.trim())
3128
+ .filter(Boolean);
3129
+ if (!codes.length) {
3130
+ this._codesFromQuery = null;
3131
+ this.clearSelection();
3132
+ return;
3133
+ }
3134
+ this._codesFromQuery = codes;
3135
+ // Si ya tengo data cargada, aplico ahora;
3136
+ // si no, se aplicará en setContent().
3137
+ if (this.data && this.data.length) {
3138
+ // ADDED: Aplicar códigos inmediatamente porque la data ya está disponible.
3139
+ this.applyCodes(codes);
3140
+ }
3141
+ }
3142
+ /**
3143
+ * ADDED: applyCodes recorre la estructura y marca selected=true en los children cuyo código está en 'codes'.
3144
+ * Además marca el parent como seleccionado si alguno de sus hijos lo está.
3145
+ */
3146
+ applyCodes(codes) {
3147
+ this.data?.forEach((parent) => {
3148
+ parent.selected = false;
3149
+ (parent.children || []).forEach((child) => {
3150
+ const code = child.code || child.value;
3151
+ const isSelected = codes.includes(code);
3152
+ child.selected = isSelected;
3153
+ if (isSelected) {
3154
+ parent.selected = true;
3155
+ }
3156
+ });
3157
+ });
3158
+ }
3159
+ /**
3160
+ * ADDED: clearSelection desmarca todos los parents y children (restablece el filtro).
3161
+ */
3162
+ clearSelection() {
3163
+ this.data?.forEach((parent) => {
3164
+ parent.selected = false;
3165
+ (parent.children || []).forEach((child) => {
3166
+ child.selected = false;
3167
+ });
3168
+ });
3169
+ }
2958
3170
  }
2959
3171
 
2960
3172
  class DynamicsFilter extends Filter {
@@ -3115,8 +3327,8 @@ class PriceRangeFilter extends Filter {
3115
3327
  if (options.minPrice != null && options.maxPrice != null) {
3116
3328
  this.minPrice = options.minPrice;
3117
3329
  this.maxPrice = options.maxPrice;
3118
- this.currentMinPrice = options.currentMinPrice ?? this.minPrice;
3119
- this.currentMaxPrice = options.currentMaxPrice ?? this.maxPrice;
3330
+ this.currentMinPrice = options.currentMinPrice ?? null;
3331
+ this.currentMaxPrice = options.currentMaxPrice ?? null;
3120
3332
  this._loaded = true;
3121
3333
  }
3122
3334
  };
@@ -3161,9 +3373,13 @@ class FilterFactory {
3161
3373
  create(filterType, productsFilter) {
3162
3374
  switch (filterType) {
3163
3375
  case 'categories':
3164
- return new CategoryFilter(productsFilter ? (productsFilter.type == 'categories' && productsFilter.value) : null);
3376
+ return new CategoryFilter(productsFilter?.type === 'categories'
3377
+ ? productsFilter.value
3378
+ : null);
3165
3379
  case 'attributes':
3166
- return new AttributesFilter(productsFilter ? (productsFilter.type == 'attributes' && productsFilter.value) : null);
3380
+ return new AttributesFilter(productsFilter?.type === 'attributes'
3381
+ ? productsFilter.value
3382
+ : null);
3167
3383
  case 'dynamics':
3168
3384
  return new DynamicsFilter();
3169
3385
  case 'sort':
@@ -3775,6 +3991,7 @@ class FiltersService {
3775
3991
  limit: 0
3776
3992
  };
3777
3993
  _filtersInitialized = false;
3994
+ _lastRouteQuery = null;
3778
3995
  constructor() {
3779
3996
  //this._defaultFilters = this._consts.getDefaultFilters()
3780
3997
  }
@@ -3795,10 +4012,20 @@ class FiltersService {
3795
4012
  let extra_params = '';
3796
4013
  this._filtersSubject.value.forEach(filter => {
3797
4014
  const extra = filter.toUrlParams();
3798
- extra.split('=')[1] != '' ? extra_params += extra : null;
4015
+ if (extra && extra.split('=')[1] !== '') {
4016
+ extra_params += extra;
4017
+ }
3799
4018
  });
3800
- if (search_value)
3801
- extra_params += ('&criteria[search][type]=contains&criteria[search][value]=' + search_value);
4019
+ // Si en la URL ya venía `attributeCodes` pero aún ningún filtro lo agregó,
4020
+ // lo volvemos a sumar a mano. Esto evita que al refrescar (SSR) se pierda el filtro
4021
+ // aunque el AttributesFilter todavía no haya terminado de hidratarse.
4022
+ const attributeCodesFromRoute = this._lastRouteQuery?.['attributeCodes'];
4023
+ if (attributeCodesFromRoute && !extra_params.includes('attributeCodes=')) {
4024
+ extra_params += `&attributeCodes=${attributeCodesFromRoute}`;
4025
+ }
4026
+ if (search_value) {
4027
+ extra_params += '&criteria[search][type]=contains&criteria[search][value]=' + search_value;
4028
+ }
3802
4029
  return this.productsFilterApi(extra_params);
3803
4030
  }
3804
4031
  isUpdated(paginationSettings) {
@@ -3814,7 +4041,7 @@ class FiltersService {
3814
4041
  this._paginationSettings = paginationSettings;
3815
4042
  return change;
3816
4043
  }
3817
- setFilters(paginationSettings, search_value) {
4044
+ setFilters(paginationSettings, search_value, routeQueryParams) {
3818
4045
  this._paginationSettings = paginationSettings;
3819
4046
  let final_filters = [];
3820
4047
  let filtersToProcess = this._optionsFilters?.includes('all')
@@ -3827,15 +4054,30 @@ class FiltersService {
3827
4054
  if (filter) {
3828
4055
  final_filters.push(filter);
3829
4056
  }
3830
- else {
3831
- // console.warn(`❌ Failed to create filter for type: ${filterType}`);
3832
- }
3833
4057
  });
3834
4058
  });
3835
4059
  this._defaultFilters?.forEach(filterDefault => {
3836
4060
  let filter = final_filters.find(filter => filter.type() == filterDefault.filter_type);
3837
4061
  filter && filterDefault.codes.forEach(value => filter.setSelected(value));
3838
4062
  });
4063
+ // hidratar price_range desde la URL
4064
+ if (routeQueryParams) {
4065
+ const pr = final_filters.find(f => f.type() === 'price_range');
4066
+ if (pr) {
4067
+ const rawMin = routeQueryParams['price_min'];
4068
+ const rawMax = routeQueryParams['price_max'];
4069
+ const min = rawMin != null ? Number(rawMin) : null;
4070
+ const max = rawMax != null ? Number(rawMax) : null;
4071
+ if ((min != null && !Number.isNaN(min)) || (max != null && !Number.isNaN(max))) {
4072
+ if (min != null && !Number.isNaN(min)) {
4073
+ pr.currentMinPrice = min;
4074
+ }
4075
+ if (max != null && !Number.isNaN(max)) {
4076
+ pr.currentMaxPrice = max;
4077
+ }
4078
+ }
4079
+ }
4080
+ }
3839
4081
  this._filtersSubject.next(final_filters);
3840
4082
  }
3841
4083
  getFilters(paginationSettings) {
@@ -3850,9 +4092,7 @@ class FiltersService {
3850
4092
  runInInjectionContext(this.environmentInjector, () => {
3851
4093
  const filterFactory = new FilterFactory();
3852
4094
  filtersToProcess?.forEach(filterType => {
3853
- // console.log('Creating filter for type:', filterType);
3854
4095
  const filter = filterFactory.create(filterType, settings);
3855
- // console.log('Created filter:', filter);
3856
4096
  if (filter) {
3857
4097
  final_filters.push(filter);
3858
4098
  }
@@ -3877,7 +4117,6 @@ class FiltersService {
3877
4117
  if (filterObj.type() !== 'price_range') {
3878
4118
  let index = final_filters.findIndex(filter => filter.type() == filterObj.type());
3879
4119
  final_filters[index].setSelected(filterElem, filterElem.value || filterElem.code);
3880
- console.log(index, final_filters);
3881
4120
  }
3882
4121
  this._filtersSubject.next(final_filters);
3883
4122
  // }
@@ -3887,13 +4126,6 @@ class FiltersService {
3887
4126
  runInInjectionContext(this.environmentInjector, () => {
3888
4127
  const filterFactory = new FilterFactory();
3889
4128
  const filter = [];
3890
- /* claves.forEach((key:any) => {
3891
- filter.push(filterFactory.create(key))
3892
- paginationFilters[key].forEach((value:any) => {
3893
- console.log(filter[0].createElement(value,paginationFilters[key]));
3894
- })
3895
-
3896
- }) */
3897
4129
  this._filtersSubject.value.forEach((value) => {
3898
4130
  if (value.type() == "attributes") {
3899
4131
  //value.data = [];
@@ -3913,6 +4145,95 @@ class FiltersService {
3913
4145
  pr.setSelected(min, max);
3914
4146
  this._filtersSubject.next([...filters]);
3915
4147
  }
4148
+ /**
4149
+ * Fuerza a emitir de nuevo los filtros actuales.
4150
+ * Útil cuando un filtro async (categorías, atributos) termina de cargarse
4151
+ * y necesitamos que PaginationService vuelva a armar la URL.
4152
+ */
4153
+ refreshFilters() {
4154
+ const current = this._filtersSubject.value;
4155
+ // Emitimos una copia para que los subscribers detecten el cambio
4156
+ this._filtersSubject.next([...current]);
4157
+ }
4158
+ /**
4159
+ * Punto central donde se hidratan los filtros a partir de la URL:
4160
+ * - Lee type/value de la ruta (ej. /collection/categories/:value).
4161
+ * - Lee query params (category, price_min, price_max, attributeCodes, search, etc.).
4162
+ * - Crea instancias de filtros y les delega la hidratación específica.
4163
+ *
4164
+ * Este método se llama tanto en SSR como en navegador.
4165
+ */
4166
+ hydrateFromRoute(paginationSettings, routeParams, routeQuery) {
4167
+ // hidratar search desde la URL ===
4168
+ const searchFromUrl = routeQuery?.['search'];
4169
+ if (typeof searchFromUrl === 'string' && searchFromUrl.trim() !== '') {
4170
+ // Guardamos el valor para que otros componentes (Header, Collection) lo reutilicen
4171
+ this._consts.searchValue = searchFromUrl.trim();
4172
+ }
4173
+ else {
4174
+ // Si no viene `search` en la URL, limpiamos el estado global para no arrastrar búsquedas viejas
4175
+ this._consts.searchValue = '';
4176
+ }
4177
+ // Guardamos la última query completa para usarla en generateFinalApi (parche attributeCodes)
4178
+ this._lastRouteQuery = routeQuery;
4179
+ // Guardamos las settings actuales (type, value, limit, etc.)
4180
+ this._paginationSettings = paginationSettings;
4181
+ // Creamos los filtros base (attributes, categories, dynamics, sort, price_range, etc.)
4182
+ let final_filters = [];
4183
+ let filtersToProcess = this._optionsFilters?.includes('all')
4184
+ ? ['attributes', 'categories', 'dynamics', 'sort', 'price_range']
4185
+ : this._optionsFilters;
4186
+ runInInjectionContext(this.environmentInjector, () => {
4187
+ const filterFactory = new FilterFactory();
4188
+ filtersToProcess?.forEach(filterType => {
4189
+ const filter = filterFactory.create(filterType, paginationSettings);
4190
+ if (filter) {
4191
+ final_filters.push(filter);
4192
+ }
4193
+ });
4194
+ });
4195
+ // Aplicamos defaultFilters si corresponde
4196
+ this._defaultFilters?.forEach(filterDefault => {
4197
+ const filter = final_filters.find(f => f.type() === filterDefault.filter_type);
4198
+ filter && filterDefault.codes.forEach(value => filter.setSelected(value));
4199
+ });
4200
+ // Hidratar filtros con slug y query params genéricos
4201
+ final_filters.forEach(f => {
4202
+ const anyFilter = f;
4203
+ // type/value en la URL: /collection/categories/:value
4204
+ const urlType = routeParams['type'];
4205
+ const urlValue = routeParams['value'];
4206
+ // Por ejemplo, el CategoryFilter implementa setFromSlug para marcar la categoría correcta
4207
+ if (urlType === 'categories' && urlValue && typeof anyFilter.setFromSlug === 'function') {
4208
+ anyFilter.setFromSlug(urlValue);
4209
+ }
4210
+ // query params genéricos: ?category=0101&price_min=...&price_max=...&attributeCodes=...
4211
+ // Cada filtro que lo soporte implementa hydrateFromQuery y decide qué leer.
4212
+ if (typeof anyFilter.hydrateFromQuery === 'function') {
4213
+ anyFilter.hydrateFromQuery(routeQuery);
4214
+ }
4215
+ });
4216
+ // Hidratar específicamente el PriceRangeFilter desde la URL
4217
+ const priceFilter = final_filters.find((f) => f instanceof PriceRangeFilter);
4218
+ if (priceFilter && routeQuery) {
4219
+ const minFromUrl = routeQuery['price_min'];
4220
+ const maxFromUrl = routeQuery['price_max'];
4221
+ if (minFromUrl != null || maxFromUrl != null) {
4222
+ const min = minFromUrl != null ? Number(minFromUrl) : priceFilter.minPrice;
4223
+ const max = maxFromUrl != null ? Number(maxFromUrl) : priceFilter.maxPrice;
4224
+ // usamos la API del filtro para setear el rango
4225
+ priceFilter.setSelected(min, max);
4226
+ }
4227
+ else {
4228
+ // si no hay nada en la URL, lo reseteamos
4229
+ priceFilter.reset();
4230
+ }
4231
+ }
4232
+ // Emitimos filtros ya hidratados para que PaginationService y Collection reaccionen
4233
+ this._filtersSubject.next(final_filters);
4234
+ // Marcamos que el sistema de filtros está listo (útil para mostrar spinners, etc.)
4235
+ this._readySubject.next(true);
4236
+ }
3916
4237
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FiltersService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3917
4238
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FiltersService, providedIn: 'root' });
3918
4239
  }
@@ -3924,17 +4245,55 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
3924
4245
  }], ctorParameters: () => [] });
3925
4246
 
3926
4247
  /**
3927
- * Servicio para manejar la paginación de los productos.
3928
- * @class PaginationService
4248
+ * Servicio para manejar la paginación y la carga de productos.
4249
+ *
4250
+ * Se encarga de:
4251
+ * - Escuchar los cambios en los filtros (categorías, atributos, precio, búsqueda, etc.).
4252
+ * - Construir la URL al backend con esos filtros.
4253
+ * - Pedir los productos al endpoint de product-search.
4254
+ * - Exponer un observable con la última página de productos (`paginationData$`)
4255
+ * y otros helpers (precio mínimo/máximo, siguiente página, etc.).
3929
4256
  */
3930
4257
  class PaginationService {
3931
4258
  _connectionService = inject(ConnectionService);
3932
4259
  _filtersService = inject(FiltersService);
3933
4260
  _constants = inject(CoreConstantsService);
3934
- paginationData$ = this._filtersService.filters$.pipe(shareReplay(1), filter(filters => filters && filters.length > 0), switchMap((filters) => {
4261
+ /**
4262
+ * Flujo principal: a partir de los filtros → arma URL → consulta backend → devuelve productos.
4263
+ *
4264
+ * Pasos:
4265
+ * 1) Espera a que `FiltersService` emita filtros válidos.
4266
+ * 2) Construye la URL final con `buildUrl()`.
4267
+ * 3) Evita repetir llamadas si la URL no cambió (`distinctUntilChanged`).
4268
+ * 4) Llama al backend con `getData(url)`.
4269
+ * 5) Comparte el último resultado con todos los suscriptores (`shareReplay(1)`).
4270
+ */
4271
+ paginationData$ = this._filtersService.filters$.pipe(tap(filters => {
4272
+ }), filter(filters => {
4273
+ if (!filters || !filters.length)
4274
+ return false;
4275
+ const categoryFilter = filters.find(f => f.type() === 'categories');
4276
+ const hasCategorySelected = !!categoryFilter?.getSelectedList()?.length;
4277
+ const isCategoryRoute = this._constants.currentRouteType === 'categories';
4278
+ // Si estamos en ruta de categorías y todavía no se marcó ninguna,
4279
+ // esperamos a que el CategoryFilter se hidrate antes de disparar la llamada.
4280
+ if (isCategoryRoute && !hasCategorySelected) {
4281
+ return false;
4282
+ }
4283
+ return true;
4284
+ }),
4285
+ // 2) Convertimos filtros -> URL
4286
+ map(filters => {
3935
4287
  const url = this.buildUrl(filters);
3936
- return this.getData(url);
3937
- }));
4288
+ return url;
4289
+ }),
4290
+ // 3) Solo seguimos si la URL cambió respecto de la emisión anterior
4291
+ distinctUntilChanged(),
4292
+ // 4) Llamamos al backend con la URL construida
4293
+ tap(url => {
4294
+ }), switchMap(url => this.getData(url)),
4295
+ // 5) Reutilizar resultado si alguien más se suscribe (evita repetir la petición)
4296
+ shareReplay(1));
3938
4297
  _dataPagination = signal({
3939
4298
  attributes: [],
3940
4299
  category: [],
@@ -4003,7 +4362,8 @@ class PaginationService {
4003
4362
  this._dataPagination.set({ ...response, called: true });
4004
4363
  this._finished = (response.page == response.pages);
4005
4364
  this._waiting = false;
4006
- // Emitir los valores de price_min y price_max a través de priceRangeSubject
4365
+ // Emitir los valores de price_min y price_max a través de priceRangeSubject,
4366
+ // para que otros componentes (ej. filtro de precio) puedan mostrarlos.
4007
4367
  this.priceRangeSubject.next({
4008
4368
  price_min: response.price_min,
4009
4369
  price_max: response.price_max
@@ -4063,27 +4423,36 @@ class PaginationService {
4063
4423
  * @returns
4064
4424
  */
4065
4425
  buildUrl(filters) {
4066
- if (this._constants.searchValue) {
4067
- return this._filtersService.generateFinalApi(this._constants.searchValue);
4068
- }
4069
- else {
4070
- return this._filtersService.generateFinalApi();
4071
- }
4426
+ const url = this._constants.searchValue
4427
+ ? this._filtersService.generateFinalApi(this._constants.searchValue)
4428
+ : this._filtersService.generateFinalApi();
4429
+ return url;
4072
4430
  }
4073
4431
  /**
4074
- * Devuelve un observable de los productos que devolvio la última página obtenida.
4432
+ * Devuelve un observable de los productos que devolvió la última página obtenida.
4075
4433
  * @param url
4076
4434
  * @returns
4077
4435
  */
4078
4436
  getData(url) {
4079
- this._resetSubject.next(true);
4080
- this._nextProductsSubject.next([]);
4081
- this._connectionService.get(url, { limit: 10, page: 1 }).pipe(map(res => {
4082
- //console.log(res)
4083
- this._nextProductsSubject.next(res.items);
4437
+ // Detectar la página desde la URL (?page=n). Si no viene, asumimos page=1.
4438
+ const pageMatch = url.match(/[?&]page=(\d+)/);
4439
+ const page = pageMatch ? Number(pageMatch[1]) : 1;
4440
+ // Si es la primera página, reseteamos estado interno
4441
+ if (page === 1) {
4442
+ this._resetSubject.next(true);
4443
+ this._nextProductsSubject.next([]);
4444
+ }
4445
+ // Llamamos al backend con el page correcto
4446
+ return this._connectionService.get(url, { limit: 10, page }).pipe(tap((res) => {
4447
+ // Actualizar datos de paginación + price_min/price_max
4084
4448
  res.links ? this.updatePageData(res) : this.finish(res);
4085
- })).subscribe();
4086
- return this.nextProducts$;
4449
+ }), map((res) => {
4450
+ const items = res.items ?? [];
4451
+ // Mantener nextProducts$ sincronizado (por compatibilidad)
4452
+ this._nextProductsSubject.next(items);
4453
+ // Lo que ve paginationData$ (switchMap) son estos items
4454
+ return items;
4455
+ }));
4087
4456
  }
4088
4457
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PaginationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4089
4458
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: PaginationService, providedIn: 'root' });
@@ -4148,9 +4517,11 @@ class ProductsService {
4148
4517
  * @param paginationSettings
4149
4518
  * @param searchValue
4150
4519
  */
4151
- getProductsForFilter(paginationSettings, searchValue) {
4520
+ getProductsForFilter(paginationSettings, searchValue, routeQueryParams) {
4152
4521
  searchValue ? this.searchValue.set(searchValue) : this.searchValue.set('');
4153
- paginationSettings && this._filtersService.setFilters(paginationSettings);
4522
+ if (paginationSettings) {
4523
+ this._filtersService.setFilters(paginationSettings, searchValue, routeQueryParams);
4524
+ }
4154
4525
  }
4155
4526
  /**
4156
4527
  * Actualiza los productos con los de la siguiente pagina.
@@ -6275,10 +6646,8 @@ class HeaderEcComponent extends MenuEcComponent {
6275
6646
  constructor() {
6276
6647
  super();
6277
6648
  this._channelService.channel$.subscribe(cfg => {
6278
- // console.log('Channel configuration:', cfg);
6279
6649
  this.showPricesOnlyToLoggedUsers = !!cfg.showPricesOnlyToLoggedUsers;
6280
6650
  this.hidePrices = !!cfg.hidePrices;
6281
- // console.log('hidePrices:', this.hidePrices);
6282
6651
  });
6283
6652
  }
6284
6653
  coreConstantsService = inject(CoreConstantsService);
@@ -6286,6 +6655,7 @@ class HeaderEcComponent extends MenuEcComponent {
6286
6655
  cdr = inject(ChangeDetectorRef); // Inyectamos ChangeDetectorRef para forzar la actualización
6287
6656
  ngOnInit() {
6288
6657
  this.channel = this.coreConstantsService.getChannel();
6658
+ this.searchValue = this.coreConstantsService.searchValue || '';
6289
6659
  this.onWindowScroll();
6290
6660
  this.detectRouteChange(); // Llamamos a la función que detecta el cambio de ruta
6291
6661
  // Usar el Observable del AuthService
@@ -6322,17 +6692,21 @@ class HeaderEcComponent extends MenuEcComponent {
6322
6692
  });
6323
6693
  }
6324
6694
  onWindowScroll() {
6325
- const scrollTop = window.scrollY;
6326
- this.isScrolled = scrollTop > 80;
6695
+ if (isPlatformBrowser(this.platformId)) {
6696
+ const scrollTop = window.scrollY;
6697
+ this.isScrolled = scrollTop > 80;
6698
+ }
6327
6699
  }
6328
6700
  isHomeFunction() {
6329
- const headerElement = document.querySelector('header');
6330
- if (headerElement) {
6331
- if (this.router.url !== '/home') {
6332
- headerElement.classList.add('show-menu');
6333
- }
6334
- else {
6335
- headerElement.classList.remove('show-menu');
6701
+ if (isPlatformBrowser(this.platformId)) {
6702
+ const headerElement = document.querySelector('header');
6703
+ if (headerElement) {
6704
+ if (this.router.url !== '/home') {
6705
+ headerElement.classList.add('show-menu');
6706
+ }
6707
+ else {
6708
+ headerElement.classList.remove('show-menu');
6709
+ }
6336
6710
  }
6337
6711
  }
6338
6712
  }
@@ -6356,27 +6730,38 @@ class HeaderEcComponent extends MenuEcComponent {
6356
6730
  }
6357
6731
  };
6358
6732
  borrarInput(inputId) {
6359
- if (inputId) {
6360
- const input = document.getElementById(inputId);
6361
- if (input) {
6362
- input.value = '';
6363
- }
6364
- }
6365
- else {
6366
- const inputs = ['searchInput1'];
6367
- inputs.forEach((id) => {
6368
- const input = document.getElementById(id);
6733
+ if (isPlatformBrowser(this.platformId)) {
6734
+ if (inputId) {
6735
+ const input = document.getElementById(inputId);
6369
6736
  if (input) {
6370
6737
  input.value = '';
6371
6738
  }
6372
- });
6739
+ }
6740
+ else {
6741
+ const inputs = ['searchInput1'];
6742
+ inputs.forEach((id) => {
6743
+ const input = document.getElementById(id);
6744
+ if (input) {
6745
+ input.value = '';
6746
+ }
6747
+ });
6748
+ }
6373
6749
  }
6374
6750
  this.searchValue = '';
6375
6751
  this.coreConstantsService.searchValue = '';
6376
- this.getCollectionSearch();
6752
+ // En lugar de relanzar la búsqueda con string vacío,
6753
+ // actualizamos la URL para quitar el query param `search` y resetear la paginación.
6754
+ this.router.navigate(['/collection'], {
6755
+ queryParams: {
6756
+ search: null,
6757
+ page: null
6758
+ },
6759
+ queryParamsHandling: 'merge'
6760
+ });
6377
6761
  }
6378
6762
  setupMobileMenu() {
6379
- // console.log('setupMobileMenu called');
6763
+ if (!isPlatformBrowser(this.platformId) || typeof document === 'undefined')
6764
+ return;
6380
6765
  const menuMobile = document.querySelector('.menuMobile');
6381
6766
  if (!(menuMobile instanceof HTMLElement))
6382
6767
  return;
@@ -6411,6 +6796,8 @@ class HeaderEcComponent extends MenuEcComponent {
6411
6796
  });
6412
6797
  }
6413
6798
  setupSearchInputs() {
6799
+ if (!isPlatformBrowser(this.platformId))
6800
+ return;
6414
6801
  const inputs = ['searchInput1', 'searchInput2'];
6415
6802
  inputs.forEach(id => {
6416
6803
  const input = document.getElementById(id);
@@ -7273,7 +7660,7 @@ class BlockProductsEcComponent extends BlockEcComponent {
7273
7660
  * Permite personalización de las imágenes de las flechas mediante @Input.
7274
7661
  */
7275
7662
  setupSwiperNavigation() {
7276
- if (this.meta?.styles?.carrousel !== false) {
7663
+ if (this.meta?.styles?.carrousel !== false && isPlatformBrowser(this.platformId)) {
7277
7664
  // Usar setTimeout para asegurar que el swiper esté inicializado
7278
7665
  setTimeout(() => {
7279
7666
  this.initializeSwiperWithCustomNavigation();
@@ -7285,6 +7672,8 @@ class BlockProductsEcComponent extends BlockEcComponent {
7285
7672
  * Esta función puede ser movida al componente base para reutilización.
7286
7673
  */
7287
7674
  initializeSwiperWithCustomNavigation() {
7675
+ if (!isPlatformBrowser(this.platformId))
7676
+ return;
7288
7677
  const prevButton = document.getElementById(`${this.meta?.code}-prev`);
7289
7678
  const nextButton = document.getElementById(`${this.meta?.code}-next`);
7290
7679
  const swiperElement = document.getElementById(this.meta?.code);
@@ -7650,7 +8039,9 @@ class MagnizoomEcComponent {
7650
8039
  this.image = this.document.createElement('img');
7651
8040
  this.image.onload = () => {
7652
8041
  this.lensSize = { width: this.image.width / 2, height: this.image.height / 2 };
7653
- setTimeout(() => this.render());
8042
+ if (isPlatformBrowser(this.platformId)) {
8043
+ setTimeout(() => this.render());
8044
+ }
7654
8045
  };
7655
8046
  this.image.src = src;
7656
8047
  }
@@ -8160,6 +8551,7 @@ class CollectionEcComponent {
8160
8551
  _productsService = inject(ProductsService);
8161
8552
  _activeRoute = inject(ActivatedRoute);
8162
8553
  _optionsService = inject(OptionsService);
8554
+ _filtersService = inject(FiltersService);
8163
8555
  params$ = this._activeRoute.params;
8164
8556
  //public ready = this._optionsService.ready
8165
8557
  queryParams$ = this._activeRoute.queryParams;
@@ -8171,40 +8563,63 @@ class CollectionEcComponent {
8171
8563
  defaultFilters = [];
8172
8564
  loading = false;
8173
8565
  countProducts = signal(0);
8174
- loaded = false;
8566
+ loaded = signal(false);
8175
8567
  optionsFilters = ['all'];
8176
8568
  filters_sort = [];
8177
- _filtersService = inject(FiltersService);
8178
8569
  filters$ = this._filtersService.filters$;
8179
8570
  ready$ = this._filtersService.ready$;
8180
8571
  window;
8181
8572
  isList = false;
8573
+ destroy$ = new Subject();
8182
8574
  setAsList = (value) => this.isList = value;
8183
8575
  ngOnInit() {
8184
- this.getProducts();
8185
- this.window?.scroll(0, 0);
8576
+ if (isPlatformBrowser(this.platformId)) {
8577
+ this.window?.scroll(0, 0);
8578
+ }
8579
+ combineLatest([this.params$, this.queryParams$])
8580
+ .pipe(map(([params, query]) => ({ params, query })), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), takeUntil(this.destroy$))
8581
+ .subscribe(({ params, query }) => {
8582
+ // Guardamos el tipo de ruta actual en las constantes (ej: 'categories', 'sections', etc.)
8583
+ const routeType = params['type'] || null;
8584
+ this.constanst.setCurrentRouteType(routeType);
8585
+ const paginationSettings = {
8586
+ latest: true,
8587
+ limit: 10,
8588
+ type: params['type'] || null,
8589
+ value: params['value'] || null
8590
+ };
8591
+ // Punto clave:
8592
+ // A partir de la URL (params + query) reconstruimos los filtros
8593
+ // (categorías, atributos, rango de precios, etc.)
8594
+ // Esto permite:
8595
+ // - Soportar F5 / recarga sin perder filtros
8596
+ // - Navegar con URL compartibles (deep linking)
8597
+ this._filtersService.hydrateFromRoute(paginationSettings, params, query);
8598
+ });
8186
8599
  }
8187
8600
  //protected readonly questions = signal<Question[]>([]);
8188
8601
  total = 0;
8189
8602
  platformId = inject(PLATFORM_ID);
8190
8603
  constructor() {
8604
+ // Guardamos window sólo en Browser para evitar errores en SSR
8191
8605
  if (isPlatformBrowser(this.platformId)) {
8192
8606
  this.window = window;
8193
8607
  }
8194
- }
8195
- getProducts() {
8196
- combineLatest([this.params$, this.queryParams$]).subscribe({
8197
- next: ([params, queryParams]) => {
8198
- const paginationSettings = {
8199
- latest: true,
8200
- limit: 10,
8201
- type: params['type'] || null,
8202
- value: params['value'] || null
8203
- };
8204
- this._productsService.getProductsForFilter(paginationSettings, queryParams["search"]);
8608
+ // Nos suscribimos al stream de productos.
8609
+ // Cuando ProductsService emite, marcamos `loaded` en true, pero solo en Browser.
8610
+ // En SSR no lo marcamos para evitar cambios de estado innecesarios.
8611
+ this.products$
8612
+ .pipe(takeUntil(this.destroy$))
8613
+ .subscribe(products => {
8614
+ if (isPlatformBrowser(this.platformId)) {
8615
+ this.loaded.set(true);
8205
8616
  }
8206
8617
  });
8207
8618
  }
8619
+ ngOnDestroy() {
8620
+ this.destroy$.next();
8621
+ this.destroy$.complete();
8622
+ }
8208
8623
  onScroll() {
8209
8624
  this.loading = true;
8210
8625
  this._productsService.updateProducts();
@@ -8832,6 +9247,7 @@ class FiltersEcComponent {
8832
9247
  injector = inject(Injector);
8833
9248
  isAuthenticated$ = this._authService.isAuthenticated();
8834
9249
  hidePrices = false;
9250
+ route = inject(ActivatedRoute);
8835
9251
  setSelect;
8836
9252
  ngOnInit() {
8837
9253
  }
@@ -8861,26 +9277,56 @@ class FiltersEcComponent {
8861
9277
  .pop()
8862
9278
  selectedOption && this._filtersService.setFilterSelected(this.filters[0], selectedOption) */
8863
9279
  }
9280
+ /**
9281
+ * Maneja el click sobre un elemento de filtro (categoría, atributo, etc.).
9282
+ *
9283
+ * - Para categorías: navega a la URL de la categoría.
9284
+ * - Para atributos: actualiza la query string de la URL con `attributeCodes`.
9285
+ * - Para otros filtros: delega en FiltersService para marcar seleccionado.
9286
+ *
9287
+ * La idea es que **la URL siempre represente los filtros aplicados**,
9288
+ * de modo que:
9289
+ * - al hacer F5 no se pierdan los filtros,
9290
+ * - los links sean compartibles (deep linking).
9291
+ */
8864
9292
  setSelected(filter, selected) {
8865
9293
  if (!filter || !selected) {
8866
- console.error('Filter or selected element is undefined:', { filter, selected });
8867
9294
  return;
8868
9295
  }
9296
+ // Si el elemento está marcado como no visible, no hacemos nada
9297
+ if (selected.isVisible === false)
9298
+ return;
8869
9299
  if (typeof filter.setSelected !== 'function') {
8870
- console.error('filter.setSelected is not a function. Filter might not be an instance of the expected class:', filter);
8871
9300
  return;
8872
9301
  }
8873
9302
  try {
8874
- // this._filtersService.setFilterSelected(filter, selected);
8875
9303
  if (filter.type() === 'categories') {
9304
+ // Para categorías usamos navegación por path (ej: /collection/categories/camas-elasticas)
8876
9305
  if (selected.path) {
8877
9306
  this.router.navigate([selected.path]);
8878
9307
  }
9308
+ return;
8879
9309
  }
8880
- else if (filter.type() === 'attributes') {
8881
- // Manejar la navegación para atributos
8882
- this._filtersService.setFilterSelected(filter, selected);
9310
+ if (filter.type() === 'attributes') {
9311
+ // El backend espera que le mandemos el "code" del atributo en el query param
9312
+ const code = selected.code || selected.value || null;
9313
+ // Actualizamos la URL manteniendo la ruta, pero agregando `attributeCodes`
9314
+ // Ejemplo de resultado:
9315
+ // /collection/categories/camas-elasticas?attributeCodes=ABC123&...
9316
+ //
9317
+ // También reseteamos la página a null para que vuelva a la primera página
9318
+ // cuando se aplica un nuevo filtro.
9319
+ this.router.navigate([], {
9320
+ relativeTo: this.route,
9321
+ queryParams: {
9322
+ attributeCodes: code,
9323
+ page: null
9324
+ },
9325
+ queryParamsHandling: 'merge'
9326
+ });
9327
+ return;
8883
9328
  }
9329
+ this._filtersService.setFilterSelected(filter, selected);
8884
9330
  }
8885
9331
  catch (error) {
8886
9332
  console.error("Error while setting selected filter:", error);
@@ -8914,7 +9360,9 @@ class FiltersEcComponent {
8914
9360
  return true;
8915
9361
  };
8916
9362
  scrollUp = () => {
8917
- window.scroll(0, 0);
9363
+ if (typeof window !== 'undefined') {
9364
+ window.scroll(0, 0);
9365
+ }
8918
9366
  return true;
8919
9367
  };
8920
9368
  hasAppliedFilters() {
@@ -10957,9 +11405,11 @@ class RelatedProductsEcComponent extends BlockEcComponent {
10957
11405
  this._relatedProductsSubject.next(relatedProducts);
10958
11406
  res.map((products) => this._analyticsService.callEvent('view_item_list', { products: products.items, item_list_name: products.title || 'Related Products', item_list_id: products.id || 'related-products' }));
10959
11407
  // Inicializar swiper después de que los datos estén disponibles
10960
- setTimeout(() => {
10961
- this.initSwiper();
10962
- }, 100);
11408
+ if (isPlatformBrowser(this.platformId)) {
11409
+ setTimeout(() => {
11410
+ this.initSwiper();
11411
+ }, 100);
11412
+ }
10963
11413
  });
10964
11414
  }
10965
11415
  initSwiper() {
@@ -11828,6 +12278,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
11828
12278
 
11829
12279
  class PriceRangeFilterComponent {
11830
12280
  _filtersService = inject(FiltersService);
12281
+ router = inject(Router);
12282
+ route = inject(ActivatedRoute);
11831
12283
  priceGap = 1;
11832
12284
  roundStep = 5;
11833
12285
  filter;
@@ -11864,7 +12316,15 @@ class PriceRangeFilterComponent {
11864
12316
  return;
11865
12317
  const min = filter.currentMinPrice ?? filter.minPrice;
11866
12318
  const max = filter.currentMaxPrice ?? filter.maxPrice;
11867
- this._filtersService.updatePriceRangeFilter(min, max);
12319
+ this.router.navigate([], {
12320
+ relativeTo: this.route,
12321
+ queryParams: {
12322
+ price_min: min !== filter.minPrice ? min : null,
12323
+ price_max: max !== filter.maxPrice ? max : null,
12324
+ page: null,
12325
+ },
12326
+ queryParamsHandling: 'merge'
12327
+ });
11868
12328
  }
11869
12329
  getMinValue(filter) {
11870
12330
  return (filter instanceof PriceRangeFilter && filter.currentMinPrice != null)