libreria-astro-lefebvre 0.0.56 → 0.0.57

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libreria-astro-lefebvre",
3
- "version": "0.0.56",
3
+ "version": "0.0.57",
4
4
  "description": "Librería de componentes Astro, React y Vue para Lefebvre",
5
5
  "author": "Equipo web desarrollo Lefebvre",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { extractImageUrl } from '../../lib/functions.js';
2
+ import { extractImageUrl, extractAllCrops } from '../../lib/functions.js';
3
3
 
4
4
  const {
5
5
  title,
@@ -19,7 +19,18 @@ const {
19
19
  alignItems = 'start',
20
20
  } = Astro.props;
21
21
 
22
- const imageUrl = extractImageUrl(image);
22
+ // Extraer todos los crops definidos en el carbin (base, skyscraper)
23
+ // Funciona igual con JSON de PageBuilder o con objeto simple { base: "url", skyscraper: "url" }
24
+ const crops = extractAllCrops(image) as Record<string, string>;
25
+ const imageBase = crops['base'] || crops['original'] || crops['default'] || '';
26
+ const imageSkyscraper = crops['skyscraper'] || ''; // Disponible para uso en el template
27
+
28
+ // También se podría usar getCropByName si se quiere lógica más compleja
29
+ // const baseUrl = getCropByName(image, 'base');
30
+ // const skyUrl = getCropByName(image, 'skyscraper', { fallback: baseUrl });
31
+
32
+ // Fallback: si solo viene una URL directa, usar extractImageUrl
33
+ const imageUrl = imageBase || extractImageUrl(image);
23
34
  ---
24
35
 
25
36
  <section class="w-full flex items-center justify-center">
@@ -96,8 +107,12 @@ const imageUrl = extractImageUrl(image);
96
107
  )}
97
108
 
98
109
  <div class={`w-full lg:w-1/2 p-4 flex items-center ${orientation === 'left' ? 'justify-end' : 'justify-start'} aspect-auto`}>
99
- <img src={image} alt={title} class="max-w-xl w-[75%] h-auto object-cover rounded-lg m-auto" loading="lazy" />
110
+ <img src={imageUrl} alt={title} class="max-w-xl w-[75%] h-auto object-cover rounded-lg m-auto" loading="lazy" />
100
111
  </div>
112
+
113
+ {/* <div class={`w-full lg:w-1/2 p-4 flex items-center ${orientation === 'left' ? 'justify-end' : 'justify-start'} aspect-square`}>
114
+ <img src={imageSkyscraper} alt={title} class="w-[100%] h-[100%] object-cover rounded-lg" loading="lazy"/>
115
+ </div> */}
101
116
  </div>
102
117
  </article>
103
118
  ) : (
package/src/index.ts CHANGED
@@ -127,5 +127,11 @@ export const components = {
127
127
  Video_2025_Valencia: Video_2025_Valencia,
128
128
  ReactButton: ReactButton
129
129
  };
130
- // 🆕 Exportar funciones helper adicionales
131
- export { extractImageUrl, parseImageData } from './lib/functions.js';
130
+ // 🆕 Exportar funciones helper de imagen
131
+ export {
132
+ extractImageUrl,
133
+ parseImageData,
134
+ extractAllCrops,
135
+ createImageProps,
136
+ getCropByName
137
+ } from './lib/functions.js';
@@ -115,28 +115,28 @@ export function extractImageUrl(value, options = {}) {
115
115
  */
116
116
  export function parseImageData(value, options = {}) {
117
117
  const { isProd = false } = options;
118
-
118
+
119
119
  if (!value) return { original: null, images: [], url: '' };
120
-
120
+
121
121
  try {
122
122
  const data = JSON.parse(decodeHtmlEntities(value));
123
-
123
+
124
124
  const resolvedOriginal = data.original ? {
125
125
  ...data.original,
126
126
  url: resolveUrl(data.original.url, isProd)
127
127
  } : null;
128
-
128
+
129
129
  const resolvedImages = (data.images || []).map(img => ({
130
130
  ...img,
131
131
  url: resolveUrl(img.url, isProd)
132
132
  }));
133
-
133
+
134
134
  return {
135
135
  original: resolvedOriginal,
136
136
  images: resolvedImages,
137
137
  url: resolvedImages[0]?.url || resolvedOriginal?.url || resolveUrl(data.url, isProd) || ''
138
138
  };
139
-
139
+
140
140
  } catch {
141
141
  return {
142
142
  original: null,
@@ -146,6 +146,161 @@ export function parseImageData(value, options = {}) {
146
146
  }
147
147
  }
148
148
 
149
+ // ============================================================================
150
+ // 🎯 Funciones para múltiples recortes (Multi-crop support)
151
+ // ============================================================================
152
+
153
+ /**
154
+ * Extrae todos los recortes de una imagen en formato key-value
155
+ *
156
+ * Soporta dos formatos de entrada:
157
+ * 1. JSON de Limbo (desde PageBuilder):
158
+ * {"original":{"url":"..."}, "images":[{"name":"base","url":"..."},{"name":"sky","url":"..."}]}
159
+ * 2. Objeto simple (uso estático sin PageBuilder):
160
+ * { base: "https://...", skyscraper: "https://..." }
161
+ *
162
+ * @param {string|object} value - JSON de Limbo, URL directa, u objeto {cropName: url}
163
+ * @param {Object} options - { isProd: boolean, includeOriginal: boolean }
164
+ * @returns {Object} Objeto con crops: { base: "url", skyscraper: "url", original?: "url" }
165
+ *
166
+ * @example
167
+ * // Desde PageBuilder (JSON de Limbo)
168
+ * const crops = extractAllCrops(image);
169
+ * const baseUrl = crops.base;
170
+ * const skyUrl = crops.skyscraper;
171
+ *
172
+ * @example
173
+ * // Uso estático (objeto simple)
174
+ * <Componente image={{ base: "url1", skyscraper: "url2" }} />
175
+ * const crops = extractAllCrops(image); // Devuelve { base: "url1", skyscraper: "url2" }
176
+ */
177
+ export function extractAllCrops(value, options = {}) {
178
+ const { isProd = false, includeOriginal = true } = options;
179
+
180
+ if (!value) return {};
181
+
182
+ // Caso 1: Ya es un objeto simple {key: url} - uso estático sin PageBuilder
183
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
184
+ // Verificar si es un objeto simple (todas las propiedades son strings URL)
185
+ const keys = Object.keys(value);
186
+ const isSimpleObject = keys.length > 0 && keys.every(key =>
187
+ typeof value[key] === 'string' || value[key] === null || value[key] === undefined
188
+ );
189
+
190
+ if (isSimpleObject) {
191
+ // Resolver URLs relativas si es necesario
192
+ const result = {};
193
+ for (const key of keys) {
194
+ if (value[key]) {
195
+ result[key] = resolveUrl(value[key], isProd);
196
+ }
197
+ }
198
+ return result;
199
+ }
200
+ }
201
+
202
+ // Caso 2: Es un string - puede ser JSON de Limbo o URL directa
203
+ if (typeof value === 'string') {
204
+ const normalizedValue = decodeHtmlEntities(value);
205
+
206
+ try {
207
+ const data = JSON.parse(normalizedValue);
208
+ const result = {};
209
+
210
+ // Extraer todos los crops del array images
211
+ if (data.images && Array.isArray(data.images)) {
212
+ for (const img of data.images) {
213
+ if (img && img.name && isValidImageUrl(img.url)) {
214
+ result[img.name] = resolveUrl(img.url, isProd);
215
+ }
216
+ }
217
+ }
218
+
219
+ // Incluir original si se solicita
220
+ if (includeOriginal && data.original?.url && isValidImageUrl(data.original.url)) {
221
+ result.original = resolveUrl(data.original.url, isProd);
222
+ }
223
+
224
+ // Si no hay crops pero hay URL directa en el JSON
225
+ if (Object.keys(result).length === 0 && data.url && isValidImageUrl(data.url)) {
226
+ result.default = resolveUrl(data.url, isProd);
227
+ }
228
+
229
+ return result;
230
+
231
+ } catch {
232
+ // No es JSON válido - tratar como URL directa
233
+ if (isValidImageUrl(normalizedValue)) {
234
+ return { default: resolveUrl(normalizedValue, isProd) };
235
+ }
236
+ }
237
+ }
238
+
239
+ return {};
240
+ }
241
+
242
+ /**
243
+ * Crea el objeto de props de imagen para uso estático (sin PageBuilder)
244
+ *
245
+ * Útil cuando se usa un componente directamente desde la librería
246
+ * y se quiere pasar múltiples crops de forma estructurada.
247
+ *
248
+ * @param {Object} crops - Objeto con los crops: { base: "url", skyscraper: "url" }
249
+ * @param {string} originalUrl - URL de la imagen original (opcional)
250
+ * @returns {string} JSON string compatible con el formato de Limbo
251
+ *
252
+ * @example
253
+ * // En un archivo .astro
254
+ * import { createImageProps } from 'libreria-astro-lefebvre/lib/functions';
255
+ *
256
+ * <Componente
257
+ * image={createImageProps({
258
+ * base: "https://example.com/base.jpg",
259
+ * skyscraper: "https://example.com/sky.jpg"
260
+ * })}
261
+ * />
262
+ *
263
+ * // También se puede pasar el objeto directamente (más simple):
264
+ * <Componente image={{ base: "url1", skyscraper: "url2" }} />
265
+ */
266
+ export function createImageProps(crops, originalUrl = null) {
267
+ if (!crops || typeof crops !== 'object') return '';
268
+
269
+ const images = Object.entries(crops).map(([name, url]) => ({
270
+ name,
271
+ url,
272
+ width: null,
273
+ height: null
274
+ }));
275
+
276
+ const result = {
277
+ original: originalUrl ? { url: originalUrl } : null,
278
+ images
279
+ };
280
+
281
+ return JSON.stringify(result);
282
+ }
283
+
284
+ /**
285
+ * Obtiene un crop específico por nombre, con fallback
286
+ *
287
+ * @param {string|object} value - JSON de Limbo u objeto simple
288
+ * @param {string} cropName - Nombre del crop a buscar
289
+ * @param {Object} options - { isProd: boolean, fallback: string }
290
+ * @returns {string} URL del crop o fallback
291
+ *
292
+ * @example
293
+ * const baseUrl = getCropByName(image, 'base');
294
+ * const skyUrl = getCropByName(image, 'skyscraper', { fallback: baseUrl });
295
+ */
296
+ export function getCropByName(value, cropName, options = {}) {
297
+ const { isProd = false, fallback = '' } = options;
298
+
299
+ const allCrops = extractAllCrops(value, { isProd });
300
+
301
+ return allCrops[cropName] || allCrops.original || allCrops.default || fallback;
302
+ }
303
+
149
304
  /**
150
305
  * Preprocesa campos de imagen para enviar a servidor de preview
151
306
  */
@@ -12,5 +12,13 @@ export {
12
12
  type LimboInitOptions
13
13
  } from './init';
14
14
 
15
+ // Exportar handler para API endpoint de token
16
+ export {
17
+ createLimboTokenHandler,
18
+ type LimboTokenConfig,
19
+ type LimboTokenResponse,
20
+ LIMBO_ENV_VARS
21
+ } from './tokenHandler';
22
+
15
23
  // El componente Astro se importa directamente:
16
24
  // import LimboProvider from 'libreria-astro-lefebvre/limbo/LimboProvider.astro';
package/src/limbo/init.ts CHANGED
@@ -114,7 +114,6 @@ export function generateLimboInitScript(config: LimboConfig): string {
114
114
  var targetSelector = selector || '${selector}';
115
115
  var inputs = document.querySelectorAll(targetSelector);
116
116
  var processed = 0;
117
- console.log('[Limbo] Rescan manual: buscando inputs con selector', targetSelector);
118
117
  inputs.forEach(function(input) {
119
118
  if (input.tagName === 'INPUT' && !input.dataset.limboProcessed) {
120
119
  try {
@@ -126,7 +125,6 @@ export function generateLimboInitScript(config: LimboConfig): string {
126
125
  }
127
126
  }
128
127
  });
129
- console.log('[Limbo] Rescan completado:', processed, 'inputs procesados de', inputs.length, 'encontrados');
130
128
  return processed;
131
129
  };
132
130
 
@@ -219,17 +217,13 @@ export function generateLimboInitScript(config: LimboConfig): string {
219
217
 
220
218
  // Debug: Escanear manualmente después de un delay para Vue/React hydration
221
219
  setTimeout(function() {
222
- console.log('[Limbo] Re-escaneando inputs después de hydration...');
223
220
  var inputs = document.querySelectorAll('${selector}');
224
- console.log('[Limbo] Encontrados ' + inputs.length + ' inputs con selector ${selector}');
225
221
  inputs.forEach(function(input, idx) {
226
- console.log('[Limbo] Input ' + idx + ':', input.tagName, input.className, input.id || input.name);
227
222
  // Forzar procesamiento si es un input válido
228
223
  if (input.tagName === 'INPUT' && !input.dataset.limboProcessed) {
229
224
  try {
230
225
  Limbo.autoInputs._processInput(input);
231
226
  input.dataset.limboProcessed = 'true';
232
- console.log('[Limbo] Input procesado manualmente:', input.id || input.name);
233
227
  } catch (e) {
234
228
  console.error('[Limbo] Error procesando input:', e);
235
229
  }
@@ -244,7 +238,6 @@ export function generateLimboInitScript(config: LimboConfig): string {
244
238
  return input.tagName === 'INPUT' && !input.dataset.limboProcessed;
245
239
  });
246
240
  if (pendingInputs.length > 0) {
247
- console.log('[Limbo] Segunda pasada: procesando ' + pendingInputs.length + ' inputs pendientes');
248
241
  pendingInputs.forEach(function(input) {
249
242
  try {
250
243
  Limbo.autoInputs._processInput(input);
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Limbo Token Handler
3
+ *
4
+ * Handler genérico para obtener tokens JWT de Limbo.
5
+ * Framework-agnostic: funciona en Astro, Next.js, Express, etc.
6
+ *
7
+ * La URL de la API de Limbo se determina automáticamente según `isProduction`:
8
+ * - Producción: https://limbo.lefebvre.es
9
+ * - Desarrollo: https://led-dev-limbo-dev.eu.els.local
10
+ * - Local: http://localhost:8000
11
+ *
12
+ * @example Astro
13
+ * ```ts
14
+ * // src/pages/api/limbo-token.ts
15
+ * import { createLimboTokenHandler } from 'libreria-astro-lefebvre/limbo';
16
+ *
17
+ * export const POST = createLimboTokenHandler({
18
+ * publicKey: import.meta.env.PUBLIC_LIMBO_PUBLIC_KEY,
19
+ * isProduction: import.meta.env.IS_PROD === 'TRUE'
20
+ * });
21
+ * ```
22
+ *
23
+ * @example Next.js App Router
24
+ * ```ts
25
+ * // app/api/limbo-token/route.ts
26
+ * import { createLimboTokenHandler } from 'libreria-astro-lefebvre/limbo';
27
+ *
28
+ * export const POST = createLimboTokenHandler({
29
+ * publicKey: process.env.PUBLIC_LIMBO_PUBLIC_KEY!,
30
+ * isProduction: process.env.NODE_ENV === 'production'
31
+ * });
32
+ * ```
33
+ */
34
+
35
+ /** URLs por defecto de la API de Limbo */
36
+ const LIMBO_API_URLS = {
37
+ production: 'https://limbo.lefebvre.es',
38
+ development: 'https://led-dev-limbo-dev.eu.els.local',
39
+ } as const;
40
+
41
+ export interface LimboTokenConfig {
42
+ /** Public Key del portal de Limbo (pk_xxx) */
43
+ publicKey: string;
44
+ /** ¿Usar API de producción? (default: false = desarrollo) */
45
+ isProduction?: boolean;
46
+ /** URL base de la API de Limbo (opcional, se determina por isProduction si no se especifica) */
47
+ limboApiUrl?: string;
48
+ /** Permitir override de publicKey desde el body de la request (default: true) */
49
+ allowPublicKeyOverride?: boolean;
50
+ }
51
+
52
+ export interface LimboTokenResponse {
53
+ success: boolean;
54
+ token?: string;
55
+ expires_in?: number;
56
+ error?: string;
57
+ details?: string;
58
+ hint?: string;
59
+ }
60
+
61
+ /**
62
+ * Crea un handler para obtener tokens JWT de Limbo.
63
+ *
64
+ * Este handler actúa como proxy para evitar problemas de CORS,
65
+ * haciendo la petición a Limbo desde el servidor.
66
+ *
67
+ * @param config - Configuración con publicKey e isProduction
68
+ * @returns Handler compatible con Astro, Next.js, y otros frameworks
69
+ */
70
+ export function createLimboTokenHandler(config: LimboTokenConfig) {
71
+ const {
72
+ publicKey: defaultPublicKey,
73
+ isProduction = false,
74
+ limboApiUrl: customApiUrl,
75
+ allowPublicKeyOverride = true,
76
+ } = config;
77
+
78
+ // Determinar URL de la API (custom > isProduction > desarrollo)
79
+ const limboApiUrl = customApiUrl || (isProduction ? LIMBO_API_URLS.production : LIMBO_API_URLS.development);
80
+
81
+ // Validar configuración al crear el handler
82
+ if (!defaultPublicKey) {
83
+ console.error('[Limbo Token Handler] PUBLIC_LIMBO_PUBLIC_KEY no está configurada');
84
+ }
85
+
86
+ console.log(`[Limbo Token Handler] Configurado para ${isProduction ? 'PRODUCCIÓN' : 'DESARROLLO'}: ${limboApiUrl}`);
87
+
88
+ /**
89
+ * Handler que procesa la petición de token
90
+ */
91
+ return async function handler({ request }: { request: Request }): Promise<Response> {
92
+ try {
93
+ // Determinar qué publicKey usar
94
+ let publicKey = defaultPublicKey;
95
+
96
+ // Permitir override desde el body si está habilitado
97
+ if (allowPublicKeyOverride) {
98
+ try {
99
+ const body = await request.json();
100
+ if (body.public_key) {
101
+ publicKey = body.public_key;
102
+ }
103
+ } catch {
104
+ // Si no hay body válido, usar la del entorno
105
+ }
106
+ }
107
+
108
+ // Validar que tenemos publicKey
109
+ if (!publicKey) {
110
+ return new Response(
111
+ JSON.stringify({
112
+ success: false,
113
+ error: 'PUBLIC_LIMBO_PUBLIC_KEY no configurada',
114
+ } satisfies LimboTokenResponse),
115
+ {
116
+ status: 400,
117
+ headers: { 'Content-Type': 'application/json' },
118
+ }
119
+ );
120
+ }
121
+
122
+ // Construir URL del endpoint de token
123
+ const tokenUrl = `${limboApiUrl}/auth/token`;
124
+ console.log('[Limbo Proxy] Requesting token from:', tokenUrl);
125
+ console.log('[Limbo Proxy] Using public_key:', publicKey.substring(0, 10) + '...');
126
+
127
+ // Hacer petición a Limbo
128
+ const response = await fetch(tokenUrl, {
129
+ method: 'POST',
130
+ headers: {
131
+ 'Content-Type': 'application/json',
132
+ },
133
+ body: JSON.stringify({
134
+ public_key: publicKey,
135
+ }),
136
+ });
137
+
138
+ // Manejar errores de la API
139
+ if (!response.ok) {
140
+ const errorText = await response.text();
141
+ console.error('[Limbo Proxy] Error response:', response.status, errorText);
142
+
143
+ return new Response(
144
+ JSON.stringify({
145
+ success: false,
146
+ error: `API error: ${response.status}`,
147
+ details: errorText,
148
+ } satisfies LimboTokenResponse),
149
+ {
150
+ status: response.status,
151
+ headers: { 'Content-Type': 'application/json' },
152
+ }
153
+ );
154
+ }
155
+
156
+ // Respuesta exitosa
157
+ const data = await response.json();
158
+ console.log('[Limbo Proxy] Token obtained successfully');
159
+
160
+ return new Response(JSON.stringify(data), {
161
+ status: 200,
162
+ headers: { 'Content-Type': 'application/json' },
163
+ });
164
+ } catch (error) {
165
+ console.error('[Limbo Proxy] Error completo:', error);
166
+
167
+ // Mensaje descriptivo para debug
168
+ let errorMsg = 'Unknown error';
169
+ if (error instanceof Error) {
170
+ errorMsg = error.message;
171
+ if (error.cause) {
172
+ errorMsg += ` (cause: ${JSON.stringify(error.cause)})`;
173
+ }
174
+ }
175
+
176
+ return new Response(
177
+ JSON.stringify({
178
+ success: false,
179
+ error: errorMsg,
180
+ hint: 'Verifica que la API de Limbo sea accesible desde el servidor',
181
+ } satisfies LimboTokenResponse),
182
+ {
183
+ status: 500,
184
+ headers: { 'Content-Type': 'application/json' },
185
+ }
186
+ );
187
+ }
188
+ };
189
+ }
190
+
191
+ /**
192
+ * URLs por defecto de la API de Limbo (exportadas para referencia).
193
+ */
194
+ export { LIMBO_API_URLS };
195
+
196
+ /**
197
+ * Helper para crear configuración desde variables de entorno comunes.
198
+ * Útil para documentación y ejemplos.
199
+ */
200
+ export const LIMBO_ENV_VARS = {
201
+ /** Variable de entorno para la Public Key (con prefijo PUBLIC_ para cliente) */
202
+ PUBLIC_KEY: 'PUBLIC_LIMBO_PUBLIC_KEY',
203
+ /** Variable de entorno para indicar si es producción */
204
+ IS_PROD: 'IS_PROD',
205
+ } as const;
206
+
207
+ export default createLimboTokenHandler;