contentoh-components-library 21.6.49 → 21.6.50

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 (90) hide show
  1. package/.claude/launch.json +14 -0
  2. package/.env.test +1 -0
  3. package/dist/ai/prompts/attribute.prompt.js +28 -0
  4. package/dist/ai/prompts/description.prompt.js +33 -0
  5. package/dist/ai/schemas/attribute.schema.js +35 -0
  6. package/dist/ai/schemas/description.schema.js +27 -0
  7. package/dist/components/atoms/AssetPreview/AssetPreview.stories.js +118 -0
  8. package/dist/components/atoms/AssetPreview/index.js +150 -0
  9. package/dist/components/atoms/AssetPreview/styles.js +28 -0
  10. package/dist/components/atoms/IconFile/index.js +85 -2
  11. package/dist/components/atoms/IconFile/styles.js +1 -1
  12. package/dist/components/atoms/ProductImage/ProductImage.stories.js +70 -2
  13. package/dist/components/atoms/ProductImage/index.js +100 -27
  14. package/dist/components/atoms/ProductImage/styles.js +3 -1
  15. package/dist/components/molecules/EditionActiveImage/index.js +4 -2
  16. package/dist/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +147 -0
  17. package/dist/components/molecules/GalleryAssetElement/index.js +178 -0
  18. package/dist/components/molecules/GalleryAssetElement/styles.js +26 -0
  19. package/dist/components/molecules/GalleryElement/index.js +13 -4
  20. package/dist/components/molecules/GalleryHeader/index.js +96 -24
  21. package/dist/components/molecules/GalleryHeader/styles.js +4 -1
  22. package/dist/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +56 -0
  23. package/dist/components/molecules/GalleryTrayTabs/index.js +60 -0
  24. package/dist/components/molecules/GalleryTrayTabs/styles.js +20 -0
  25. package/dist/components/molecules/ImageSelector/index.js +1 -0
  26. package/dist/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +171 -0
  27. package/dist/components/organisms/DigitalAssetsTray/index.js +211 -0
  28. package/dist/components/organisms/DigitalAssetsTray/styles.js +22 -0
  29. package/dist/components/organisms/ImagePreviewer/index.js +1 -0
  30. package/dist/components/organisms/ProductImageModal/index.js +3 -2
  31. package/dist/components/organisms/PropagateChangesModal/index.js +12 -1
  32. package/dist/components/organisms/PropagateChangesModal/non-propagable.js +45 -0
  33. package/dist/components/pages/ProviderProductEdition/index.js +11 -1
  34. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +330 -15
  35. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -2
  36. package/dist/components/pages/RetailerProductEdition/index.js +605 -414
  37. package/dist/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
  38. package/dist/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +45 -145
  39. package/dist/global-files/data.js +16 -3
  40. package/dist/global-files/digital-assets/assetTypes.js +480 -0
  41. package/dist/global-files/digital-assets/assets.actions.js +64 -0
  42. package/dist/global-files/digital-assets/assets.adapter.js +304 -0
  43. package/dist/global-files/digital-assets/assets.mock.js +136 -0
  44. package/dist/global-files/digital-assets/assets.reducer.js +220 -0
  45. package/dist/global-files/digital-assets/uploadAssets.js +179 -0
  46. package/dist/global-files/digital-assets/useDigitalAssets.js +522 -0
  47. package/dist/index.js +142 -90
  48. package/docs/activos-digitales.md +273 -0
  49. package/package.json +1 -1
  50. package/src/components/atoms/AssetPreview/AssetPreview.stories.js +78 -0
  51. package/src/components/atoms/AssetPreview/index.js +119 -0
  52. package/src/components/atoms/AssetPreview/styles.js +190 -0
  53. package/src/components/atoms/IconFile/index.js +28 -1
  54. package/src/components/atoms/IconFile/styles.js +8 -0
  55. package/src/components/atoms/ProductImage/ProductImage.stories.js +38 -0
  56. package/src/components/atoms/ProductImage/index.js +95 -20
  57. package/src/components/atoms/ProductImage/styles.js +92 -7
  58. package/src/components/molecules/EditionActiveImage/index.js +2 -2
  59. package/src/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +102 -0
  60. package/src/components/molecules/GalleryAssetElement/index.js +162 -0
  61. package/src/components/molecules/GalleryAssetElement/styles.js +159 -0
  62. package/src/components/molecules/GalleryElement/index.js +9 -0
  63. package/src/components/molecules/GalleryHeader/index.js +65 -13
  64. package/src/components/molecules/GalleryHeader/styles.js +2 -1
  65. package/src/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +32 -0
  66. package/src/components/molecules/GalleryTrayTabs/index.js +41 -0
  67. package/src/components/molecules/GalleryTrayTabs/styles.js +52 -0
  68. package/src/components/molecules/ImageSelector/index.js +1 -0
  69. package/src/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +101 -0
  70. package/src/components/organisms/DigitalAssetsTray/index.js +189 -0
  71. package/src/components/organisms/DigitalAssetsTray/styles.js +183 -0
  72. package/src/components/organisms/ImagePreviewer/index.js +1 -0
  73. package/src/components/organisms/ProductImageModal/index.js +1 -0
  74. package/src/components/organisms/PropagateChangesModal/index.js +11 -1
  75. package/src/components/organisms/PropagateChangesModal/non-propagable.js +29 -0
  76. package/src/components/pages/ProviderProductEdition/index.js +10 -1
  77. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +201 -0
  78. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -0
  79. package/src/components/pages/RetailerProductEdition/index.js +366 -140
  80. package/src/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
  81. package/src/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +51 -104
  82. package/src/global-files/data.js +17 -2
  83. package/src/global-files/digital-assets/assetTypes.js +419 -0
  84. package/src/global-files/digital-assets/assets.actions.js +50 -0
  85. package/src/global-files/digital-assets/assets.adapter.js +229 -0
  86. package/src/global-files/digital-assets/assets.mock.js +122 -0
  87. package/src/global-files/digital-assets/assets.reducer.js +197 -0
  88. package/src/global-files/digital-assets/uploadAssets.js +109 -0
  89. package/src/global-files/digital-assets/useDigitalAssets.js +329 -0
  90. package/src/index.js +4 -0
@@ -0,0 +1,273 @@
1
+ # Activos digitales — estado del trabajo
2
+
3
+ > Documento de continuidad. Última actualización: 2026-09-23.
4
+ > **El circuito ya funciona de punta a punta en dev**, borrado en S3 incluido.
5
+ > El exportable está escrito y probado en local, sin commitear ni desplegar.
6
+ > Falta el póster.
7
+
8
+ ## La tarea
9
+
10
+ 1. Permitir el almacenamiento de activos digitales dentro de las bandejas de imágenes.
11
+ 2. Añadir en el exportable de los productos las URL de los activos almacenados,
12
+ para que el usuario los descargue por su cuenta.
13
+
14
+ El punto 1 está hecho y desplegado en dev. El punto 2 está escrito, **sin
15
+ commitear ni desplegar** (ver "El exportable").
16
+
17
+ Pantalla: `RetailerProductEdition`, pestaña Imágenes, bandeja *Activos digitales*.
18
+
19
+ ---
20
+
21
+ ## 1. Cómo funciona hoy
22
+
23
+ ```
24
+ NAVEGADOR LAMBDA S3
25
+ ────────────────────────────────────────────────────────────────────────
26
+ 1. Sueltas un archivo
27
+ 2. Valida en local
28
+ extensión, peso,
29
+ duración, cupo
30
+ │ (nada ha salido todavía)
31
+ 3. POST /articles-assets/presign ──────►
32
+ 4. Comprueba sesión y acceso
33
+ Valida extensión → tipo
34
+ Genera la clave, firma
35
+ ◄──────────────────────── url + fields + key
36
+ 5. POST multipart ──────────────────────────────────────────►
37
+ con barra de progreso 6. S3 aplica las
38
+ condiciones firmadas
39
+ ◄────────────────────────────────────────── 204
40
+ 7. Al terminar, se registra SOLO (no espera al botón de guardar)
41
+ PUT /articles-data?asset=true ──►
42
+ 8. Comprueba que la clave sea
43
+ de ese artículo; INSERT;
44
+ devuelve la lista completa
45
+ ```
46
+
47
+ Al reabrir la ficha, `read-data-images` devuelve el arreglo `assets` junto a las
48
+ imágenes, y la bandeja se llena sola.
49
+
50
+ ### Decisiones que dan forma a esto
51
+
52
+ - **Se valida dos veces.** En el navegador para no hacer esperar por un archivo
53
+ que se va a rechazar; en el servidor porque el front se puede saltar y quien
54
+ firma decide qué entra al bucket. La del servidor es la que manda.
55
+ - **El archivo nunca toca un Lambda.** Por API Gateway no cabe —10 MB y 29
56
+ segundos— y convertirlo a base64 en el navegador tumba la pestaña con un .psd
57
+ grande.
58
+ - **S3 aplica los límites, no el navegador.** Por eso es un POST multipart
59
+ firmado con `createPresignedPost` y no un PUT: solo el POST admite condiciones
60
+ que S3 verifica al recibir (tamaño, Content-Type, clave).
61
+ - **El tipo se decide por EXTENSIÓN, no por mime.** El navegador no sabe qué es
62
+ un .psd —manda mime vacío— y a un .ai le pone `application/pdf`. Por lo mismo
63
+ el `accept` del dropzone lleva extensiones además de mimes: con solo mimes,
64
+ react-dropzone descartaría esos archivos antes de que la validación llegue a
65
+ verlos, y al soltarlos no pasaría nada.
66
+ - **Sube al soltar y se registra solo al terminar.** Con la barra llena nadie
67
+ supone que falta un paso. El precio es que un archivo subido y luego quitado
68
+ antes de registrarse deja un huérfano en S3.
69
+ - **Solo mp4, webm y ogg cuentan como reproducibles.** Un .mov se acepta, se
70
+ guarda y se descarga, pero se muestra con icono y avisa al soltarlo: Chrome y
71
+ Firefox no decodifican QuickTime.
72
+ - **Agregar es inmediato; borrar espera al guardar.** Registrar completa algo que
73
+ ya pasó —el archivo está en S3—; borrar es destructivo y conviene poder
74
+ deshacerlo cerrando sin guardar.
75
+
76
+ ---
77
+
78
+ ## 2. Decisiones del manager (2026-09-22)
79
+
80
+ | # | Pregunta | Respuesta |
81
+ |---|---|---|
82
+ | 0 | ¿Renombrar la pestaña? | **Se queda como "Imágenes"** |
83
+ | 1 | ¿Qué tipos entran? | 360 (mp4/gif), video, gif, pdf, hojas de cálculo y archivos editables de diseño |
84
+ | 2 | ¿Del producto o de una cadena? | **General**, con `id_retailer = null`. La columna queda preparada |
85
+ | 3 | ¿ZIP del exportable o solo URL? | **Solo URL** |
86
+ | 4 | ¿Cuentan para el % y para aprobar/rechazar? | **No** |
87
+ | 5 | ¿Quién sube y borra? | Los mismos que suben imágenes |
88
+ | 6 | ¿Versionado? | **No hay versión.** Un activo general es el mismo para todas |
89
+ | 7 | Peso y cantidad máxima | **SIN RESPONDER** — provisionales en `assetTypes.js` |
90
+ | 8 | Duración de video | Sin responder; se conserva 10 s – 10 min |
91
+ | 9 | ¿URLs permanentes o firmadas? | **Permanentes, indefinidas** |
92
+ | 10 | ¿Se autoriza `rel_article_asset`? | **Sí** |
93
+
94
+ ### Lo que sigue abierto
95
+
96
+ **Pesos y cantidad (pregunta 7).** Provisionales, en un solo archivo por lado:
97
+ `assetTypes.js` (front) y `asset-types.js` (back). **Deben moverse juntos.**
98
+
99
+ | Tipo | Tope | Máximo por producto |
100
+ |---|---|---|
101
+ | 360 / video | 200 MB | **20 activos** |
102
+ | GIF | 20 MB | |
103
+ | Documento (pdf, doc, docx) | 50 MB | |
104
+ | Hoja de cálculo (xlsx, xls, csv) | 20 MB | |
105
+ | Editable (psd, ai, indd, aep…) | 500 MB | |
106
+ | Comprimido (zip, rar, 7z) | 500 MB | |
107
+
108
+ **Confirmar la lista de extensiones editables** que se propuso para el "etc":
109
+ `psd psb ai eps indd idml aep prproj sketch fig xd afphoto afdesign svg`.
110
+
111
+ ---
112
+
113
+ ## 3. Qué está hecho y dónde
114
+
115
+ ### Front — `components-library`, rama `feature/digital-assets`
116
+
117
+ **Seis commits, SIN empujar** (la rama no tiene remoto todavía). Sale de
118
+ `feature/multiple-retailer-category`, que aún no está en master.
119
+
120
+ ```
121
+ 65523ae feat: avisar cuántos cambios quedan sin guardar
122
+ b607eea feat: poder abrir un activo, y avisar cuando no se podrá ver
123
+ 82e1a7d feat: registrar el activo en cuanto termina de subir
124
+ 26986f1 feat: subida directa a S3 con URL prefirmada
125
+ 16bc7da fix(ficha): evitar que la edición reviente o se quede cargando
126
+ 003c80a feat: bandeja de 360, video, gif, documentos y editables
127
+ ```
128
+
129
+ | Archivo | Qué es |
130
+ |---|---|
131
+ | `global-files/digital-assets/assetTypes.js` | Catálogo de 7 tipos y 27 formatos, límites, validaciones y avisos |
132
+ | `global-files/digital-assets/assets.adapter.js` | Traducción backend ↔ interfaz. **Único archivo que conoce los nombres de campo del backend** |
133
+ | `global-files/digital-assets/assets.actions.js` | Vocabulario compartido entre el reducer y el hook |
134
+ | `global-files/digital-assets/assets.reducer.js` | Reducer + selectores `selectAssetsToSave`, `hasUnsavedAssets`, `countUnsavedAssets` |
135
+ | `global-files/digital-assets/useDigitalAssets.js` | Validación, duración, ciclo de vida de la subida y orquestación |
136
+ | `global-files/digital-assets/uploadAssets.js` | Presign + POST multipart a S3 con progreso |
137
+ | `global-files/digital-assets/assets.mock.js` | Activos con la forma cruda del backend |
138
+ | `atoms/AssetPreview/` | Previsualización: video, imagen o icono |
139
+ | `molecules/GalleryAssetElement/` | Fila de activo (aparte de `GalleryElement` a propósito) |
140
+ | `molecules/GalleryTrayTabs/` | Toggle `Imágenes (n) │ Activos digitales (n)` |
141
+ | `organisms/DigitalAssetsTray/` | Bandeja: dropzone propio, avisos, aviso de cambios sin guardar |
142
+
143
+ Modificados: `global-files/data.js`, `atoms/ProductImage`, `atoms/IconFile`,
144
+ `molecules/GalleryElement`, `molecules/GalleryHeader`, `ImageSelector`,
145
+ `EditionActiveImage`, `ImagePreviewer`, `ProductImageModal`,
146
+ `RetailerProductEdition/index.js`, su contexto y su reducer raíz, `src/index.js`
147
+ y los tres `.env`.
148
+
149
+ **El flag `enableDigitalAssets` está apagado por omisión.** La historia
150
+ `ImageEditor` lo enciende para poder ver la bandeja en Storybook.
151
+
152
+ ### Backend — `content-management-backend`, rama `feature/digital-assets`
153
+
154
+ **Seis commits, empujados.** Sin PR: no se mergea todavía.
155
+
156
+ ```
157
+ 0aafcdec fix: que el borrado en S3 sea posible y visible
158
+ ab38d16e fix: declarar el charset de rel_article_asset
159
+ a543ba30 fix: devolver el ref del cliente en el presign
160
+ 080d2481 feat: lectura y escritura de activos
161
+ 9c7b8176 feat: endpoint para firmar la subida de activos
162
+ 2c1d5f1f feat: tabla rel_article_asset
163
+ ```
164
+
165
+ | Archivo | Qué es |
166
+ |---|---|
167
+ | `docs/create-rel-article-asset.sql` | DDL. Sin `id_version`; `asset_type` es VARCHAR y no ENUM; charset utf8mb4 explícito |
168
+ | `CRUD/articles/articles-assets/asset-types.js` | Allowlist del servidor. Los editables se sirven como `octet-stream`, el .svg incluido |
169
+ | `CRUD/articles/articles-assets/presign-asset-upload.js` | `POST /articles-assets/presign` |
170
+ | `CRUD/articles/articles-data/utils/assets-actions.js` | Altas, cambios, bajas y lectura |
171
+ | `read-data-images.js`, `update-articles-data.js` | Devuelven/aceptan activos |
172
+ | `serverless.yml` | La función nueva y los permisos de S3 |
173
+
174
+ ### Estado del despliegue (dev)
175
+
176
+ | Pieza | Estado |
177
+ |---|---|
178
+ | Tabla `rel_article_asset` | Creada. **Necesita el `ALTER` de charset** (ver el .sql) |
179
+ | `presign-asset-upload` | Desplegada |
180
+ | `read-data-images` con `assets` | Desplegada |
181
+ | `update-articles-data?asset=true` | Desplegada |
182
+ | `ref` en la respuesta del presign (`a543ba30`) | Desplegado (deploy completo, 2026-09-23) |
183
+ | `s3:DeleteObject` en el rol (`0aafcdec`) | Desplegado y comprobado: al borrar un activo desaparece del bucket |
184
+ | Activos en el exportable (servicio `exports`) | **Sin commitear ni desplegar** |
185
+
186
+ Nada desplegado en test ni en prod.
187
+
188
+ ---
189
+
190
+ ## 4. Lo que falta
191
+
192
+ 1. **Commitear y desplegar el exportable.** El servicio es `exports`, no
193
+ `functions`. Hay que desplegar el orquestador (Lambda) y además
194
+ reconstruir la imagen de Docker del procesador (Fargate).
195
+ 2. **El póster con ffmpeg** tras la subida, para que los videos tengan miniatura
196
+ sin que el navegador la genere.
197
+ 3. **Huérfanos en S3**: un archivo subido que se quita antes de registrarse no se
198
+ borra nunca. O una regla de ciclo de vida sobre `assets/`, o un endpoint de
199
+ borrado por clave para ese caso.
200
+ 4. **El front no lee `orphaned`**: si S3 rechaza un borrado, la bandeja dice
201
+ "guardados con éxito" igual.
202
+
203
+ ### El exportable
204
+
205
+ Los activos salen como filas de *"Archivo Urls.xlsx"*, el mismo archivo que se
206
+ genera con el servicio `urls`. Se agrega una columna `Tipo`, que en las imágenes
207
+ dice `Imagen` y en los activos `Video`, `Editable`, etc.
208
+
209
+ | Archivo (`exports/nuevo-exportable/`) | Qué hace |
210
+ |---|---|
211
+ | `asset-urls.js` | `attachArticleAssets()` lee `rel_article_asset` + `article.upc`; `collectAssetUrlRows()` arma las filas |
212
+ | `handler-orchestrator.js` | Llama a `attachArticleAssets` junto a las demás cargas |
213
+ | `handler-processor.js` | Suma las filas de activos a las de imágenes cuando se pide `urls` |
214
+ | `xlsx-url-writer.js` | Columna `Tipo`; los activos van después de las imágenes de su producto |
215
+ | `asset-urls.test.js` | 9 pruebas. `node --test exports/nuevo-exportable/` |
216
+
217
+ Decisiones:
218
+ - **No se copian al bucket público**, a diferencia de lo que planteaba el plan
219
+ original. Las imágenes se copian porque cada cadena las recibe transformadas y
220
+ renombradas. Un activo se entrega tal cual, y ya está en S3 con
221
+ `public-read` y su Content-Type real. Se usa su URL de siempre: es permanente
222
+ (decisión 9) y es la misma que usa la ficha. Copiarlo duplicaría archivos de
223
+ hasta 500 MB en cada exportación, y ya no hace falta tocar `getContentType`.
224
+ - **Renombre = `<UPC>_<nombre original sin extensión>`**, y si no hay UPC, el
225
+ id del artículo. El UPC sale de `article.upc`.
226
+ - **Aparece aunque no se pidan imágenes**: basta con el servicio `urls`.
227
+ - **Un activo sale una vez por cadena**, aunque el producto venga en varias
228
+ categorías o versiones.
229
+ - **Si la tabla no existe** (`ER_NO_SUCH_TABLE`), el exportable sale sin
230
+ activos en vez de fallar. Así se puede desplegar antes de crear la tabla en
231
+ prod.
232
+
233
+ Limitación conocida: si una cadena nombra sus imágenes con otro atributo (SKU,
234
+ por ejemplo), sus activos no quedan junto a esas imágenes en el Excel, porque
235
+ van por UPC.
236
+
237
+ ---
238
+
239
+ ## 5. Hallazgos ajenos a este trabajo
240
+
241
+ - **El rol `general-dev-us-east-1-lambdaRole` no tiene permisos de S3 sobre las
242
+ imágenes de producto**, solo sobre el prefijo `assets/`. Pero `images-actions.js`
243
+ hace `copyObject` al bucket de respaldo al guardar y `deleteObject` en los dos
244
+ buckets al eliminar. Los errores mueren dentro de un `Promise.all` con try/catch
245
+ arriba. Si es lo que parece: el respaldo de imágenes no se está haciendo y las
246
+ imágenes borradas siguen ocupando el bucket. **Comprobar el bucket
247
+ `content-management-raw-images`**; si está congelado en una fecha vieja, lleva
248
+ tiempo roto.
249
+ - **La política del rol es en línea y la genera Serverless** (`general-dev-lambda`).
250
+ No tiene políticas administradas adjuntas: cualquier permiso agregado a mano se
251
+ borra en el siguiente despliegue.
252
+ - **`AddGroup.stories.js` está roto** (`bags is not defined`) y ensucia la consola
253
+ de Storybook.
254
+ - **`version-manager` y `articles-observations/read-observation` no permiten CORS**
255
+ desde `localhost:6006`. No impiden que la ficha cargue.
256
+
257
+ ---
258
+
259
+ ## 6. Trampas del repo que conviene recordar
260
+
261
+ - **No correr prettier sobre archivos existentes** de `components-library`. No hay
262
+ `.prettierrc` y la versión instalada es la 2.3.0, pero el código está formateado
263
+ con estilo de la 3. Formatear un archivo existente genera ~100 líneas de ruido.
264
+ - **Hay directorios versionados que solo difieren en mayúsculas**:
265
+ `editionActiveImage`/`EditionActiveImage`, `imageSelector`/`ImageSelector`,
266
+ `productImage`/`ProductImage`. En macOS son el mismo directorio, así que
267
+ aparecen siempre como modificados y `git add -A` falla. Se preparan a mano las
268
+ rutas en mayúscula; las otras son fantasmas que nadie importa.
269
+ - **`functions/CRUD/database-structure.txt` está desactualizado** para
270
+ `rel_article_image`: dice `article_id`/`image_id` pero los lambdas usan
271
+ `id_article`/`id_image`.
272
+ - **El stage `dev` apunta al bucket de imágenes de PRODUCCIÓN**
273
+ (`content-management-images-prod`). Cualquier archivo de prueba queda ahí.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contentoh-components-library",
3
- "version": "21.6.49",
3
+ "version": "21.6.50",
4
4
  "dependencies": {
5
5
  "@aws-amplify/auth": "^4.5.3",
6
6
  "@aws-amplify/datastore": "^3.11.0",
@@ -0,0 +1,78 @@
1
+ import { AssetPreview } from "./index";
2
+ import { fromBackendList } from "../../../global-files/digital-assets/assets.adapter";
3
+ import { backendAssetsMock } from "../../../global-files/digital-assets/assets.mock";
4
+
5
+ /*
6
+ Los activos del mock apuntan a claves de S3 que no existen todavía. Para poder
7
+ ver el componente antes de que haya backend, se sustituyen las URLs por
8
+ archivos públicos de ejemplo; el resto del objeto (tipo, nombre, peso,
9
+ duración) es el que devolvería el adaptador.
10
+ */
11
+ const SAMPLES = {
12
+ mp4: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4",
13
+ gif: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif",
14
+ };
15
+
16
+ const [giro360, gif360, video, pdf] = fromBackendList(backendAssetsMock).map(
17
+ (asset) => ({
18
+ ...asset,
19
+ url:
20
+ asset.ext === "mp4"
21
+ ? SAMPLES.mp4
22
+ : asset.ext === "gif"
23
+ ? SAMPLES.gif
24
+ : "",
25
+ poster: "",
26
+ })
27
+ );
28
+
29
+ export default {
30
+ title: "Components/atoms/AssetPreview",
31
+ component: AssetPreview,
32
+ argTypes: {
33
+ size: {
34
+ options: [
35
+ "catalogue-image-small-size",
36
+ "catalogue-image-big-size",
37
+ "carousel-image",
38
+ "catalogue-modal-image",
39
+ "main-image",
40
+ ],
41
+ control: { type: "select" },
42
+ },
43
+ },
44
+ };
45
+
46
+ const Template = (args) => <AssetPreview {...args} />;
47
+
48
+ export const Video360 = Template.bind({});
49
+ Video360.args = { asset: giro360, size: "catalogue-image-big-size" };
50
+
51
+ export const Gif360 = Template.bind({});
52
+ Gif360.args = { asset: gif360, size: "catalogue-image-big-size" };
53
+
54
+ export const VideoConControles = Template.bind({});
55
+ VideoConControles.args = { asset: video, size: "main-image", controls: true };
56
+
57
+ export const Pdf = Template.bind({});
58
+ Pdf.args = { asset: pdf, size: "catalogue-image-big-size" };
59
+
60
+ /** Un activo sin URL todavía —recién soltado en la bandeja— no debe romper. */
61
+ export const SinFuente = Template.bind({});
62
+ SinFuente.args = {
63
+ asset: { name: "manual_sin_subir.pdf", ext: "pdf", assetType: "pdf" },
64
+ size: "catalogue-image-big-size",
65
+ };
66
+
67
+ /** Cómo se ve la bandeja completa, que es el uso real. */
68
+ export const Bandeja = () => (
69
+ <div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
70
+ {[giro360, gif360, video, pdf].map((asset) => (
71
+ <AssetPreview
72
+ key={asset.id}
73
+ asset={asset}
74
+ size="catalogue-image-big-size"
75
+ />
76
+ ))}
77
+ </div>
78
+ );
@@ -0,0 +1,119 @@
1
+ import ReactImageFallback from "react-image-fallback";
2
+
3
+ import { Container } from "./styles";
4
+ import { IconFile } from "../IconFile";
5
+ import {
6
+ ASSET_PREVIEW,
7
+ formatDuration,
8
+ getAssetType,
9
+ getExtension,
10
+ resolvePreviewKind,
11
+ } from "../../../global-files/digital-assets/assetTypes";
12
+ import defaultProductImage from "../../../assets/images/defaultImages/defaultProductImage.png";
13
+
14
+ const PlayBadge = () => (
15
+ <span className="play-badge" aria-hidden="true">
16
+ <svg viewBox="0 0 24 24">
17
+ <circle cx="12" cy="12" r="11" />
18
+ <polygon points="9.5,7.5 17,12 9.5,16.5" />
19
+ </svg>
20
+ </span>
21
+ );
22
+
23
+ /**
24
+ * Previsualización de un activo digital o de una imagen de producto.
25
+ *
26
+ * Acepta el modelo de `assets.adapter` y también un objeto suelto
27
+ * (`{ url, mime, ext, name }`), para poder usarse donde todavía no hay activos
28
+ * adaptados.
29
+ *
30
+ * `controls` distingue los dos usos: en la bandeja se quieren miniaturas
31
+ * quietas; en el visor grande, un reproductor.
32
+ */
33
+ export const AssetPreview = ({
34
+ asset = {},
35
+ size = "catalogue-image-small-size",
36
+ controls = false,
37
+ showBadge = true,
38
+ showDuration = true,
39
+ onClick,
40
+ className = "",
41
+ }) => {
42
+ const { url, src, poster, mime, ext, name, assetType, duration } = asset;
43
+
44
+ const source = url || src || "";
45
+ const kind = resolvePreviewKind({ mime, ext, name });
46
+ const extension = (ext || getExtension(name) || "").toLowerCase();
47
+ const label = assetType
48
+ ? getAssetType(assetType).label
49
+ : extension.toUpperCase();
50
+
51
+ const badge =
52
+ showBadge && label ? <span className="asset-badge">{label}</span> : null;
53
+ const durationTag =
54
+ showDuration && duration ? (
55
+ <span className="asset-duration">{formatDuration(duration)}</span>
56
+ ) : null;
57
+
58
+ const body = () => {
59
+ if (!source) {
60
+ return (
61
+ <div className="asset-fallback">
62
+ <IconFile ext={extension} size="34" />
63
+ <p title={name}>{name}</p>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ if (kind === ASSET_PREVIEW.VIDEO) {
69
+ return (
70
+ <>
71
+ <video
72
+ // `#t=0.5` pide al navegador el fotograma del medio segundo en vez
73
+ // de una pantalla negra. Resuelve la miniatura sin descargar el
74
+ // archivo entero ni convertirlo a base64, que es lo que hacía el
75
+ // parche que este componente reemplaza.
76
+ src={poster ? source : `${source}#t=0.5`}
77
+ poster={poster || undefined}
78
+ controls={controls}
79
+ muted={!controls}
80
+ playsInline
81
+ preload="metadata"
82
+ />
83
+ {/* Sin controles, nada indica que el recuadro es reproducible. */}
84
+ {!controls && <PlayBadge />}
85
+ </>
86
+ );
87
+ }
88
+
89
+ if (kind === ASSET_PREVIEW.IMAGE) {
90
+ return (
91
+ <ReactImageFallback
92
+ src={source}
93
+ fallbackImage={defaultProductImage}
94
+ alt={name}
95
+ onClick={onClick}
96
+ />
97
+ );
98
+ }
99
+
100
+ return (
101
+ <div className="asset-fallback" onClick={onClick}>
102
+ <IconFile ext={extension} size="34" />
103
+ <p title={name}>{name}</p>
104
+ </div>
105
+ );
106
+ };
107
+
108
+ return (
109
+ <Container
110
+ className={`${size} ${className}`.trim()}
111
+ onClick={controls ? undefined : onClick}
112
+ $clickable={Boolean(onClick) && !controls}
113
+ >
114
+ {body()}
115
+ {badge}
116
+ {durationTag}
117
+ </Container>
118
+ );
119
+ };
@@ -0,0 +1,190 @@
1
+ import styled from "styled-components";
2
+ import { GlobalColors, FontFamily } from "../../../global-files/variables";
3
+
4
+ /*
5
+ Los tamaños replican los de `ProductImage` con los mismos nombres de clase,
6
+ para que el componente entre donde ya había una imagen sin tocar el layout
7
+ de la bandeja.
8
+ */
9
+ export const Container = styled.div`
10
+ position: relative;
11
+ width: fit-content;
12
+ display: flex;
13
+ justify-content: center;
14
+ align-items: center;
15
+
16
+ cursor: ${({ $clickable }) => ($clickable ? "pointer" : "default")};
17
+
18
+ img,
19
+ video {
20
+ object-fit: contain;
21
+ background: ${GlobalColors.gray_light};
22
+ }
23
+
24
+ .asset-fallback {
25
+ display: flex;
26
+ flex-direction: column;
27
+ justify-content: center;
28
+ align-items: center;
29
+ gap: 6px;
30
+ width: 100%;
31
+ height: 100%;
32
+ padding: 8px;
33
+ box-sizing: border-box;
34
+ background: ${GlobalColors.gray_light};
35
+ border-radius: 6px;
36
+
37
+ p {
38
+ margin: 0;
39
+ font-family: ${FontFamily.Raleway};
40
+ font-size: 10px;
41
+ color: ${GlobalColors.gray};
42
+ text-align: center;
43
+ overflow: hidden;
44
+ text-overflow: ellipsis;
45
+ display: -webkit-box;
46
+ -webkit-line-clamp: 2;
47
+ -webkit-box-orient: vertical;
48
+ word-break: break-all;
49
+ }
50
+ }
51
+
52
+ /* Solo aparece cuando el video se muestra sin controles. */
53
+ .play-badge {
54
+ position: absolute;
55
+ top: 50%;
56
+ left: 50%;
57
+ transform: translate(-50%, -50%);
58
+ width: 32px;
59
+ height: 32px;
60
+ pointer-events: none;
61
+
62
+ svg {
63
+ width: 100%;
64
+ height: 100%;
65
+ display: block;
66
+ }
67
+
68
+ circle {
69
+ fill: rgba(0, 0, 0, 0.55);
70
+ }
71
+
72
+ polygon {
73
+ fill: ${GlobalColors.white};
74
+ }
75
+ }
76
+
77
+ &:hover .play-badge circle {
78
+ fill: rgba(0, 0, 0, 0.75);
79
+ }
80
+
81
+ .asset-badge {
82
+ position: absolute;
83
+ top: 4px;
84
+ left: 4px;
85
+ padding: 1px 6px;
86
+ border-radius: 10px;
87
+ background: ${GlobalColors.color_purple};
88
+ color: ${GlobalColors.white};
89
+ font-family: ${FontFamily.Raleway_600};
90
+ font-size: 9px;
91
+ line-height: 14px;
92
+ letter-spacing: 0.02em;
93
+ pointer-events: none;
94
+ }
95
+
96
+ .asset-duration {
97
+ position: absolute;
98
+ bottom: 4px;
99
+ right: 4px;
100
+ padding: 1px 5px;
101
+ border-radius: 4px;
102
+ background: rgba(38, 38, 38, 0.75);
103
+ color: ${GlobalColors.white};
104
+ font-family: ${FontFamily.Raleway};
105
+ font-size: 9px;
106
+ line-height: 14px;
107
+ pointer-events: none;
108
+ }
109
+
110
+ &.carousel-image {
111
+ .play-badge {
112
+ width: 22px;
113
+ height: 22px;
114
+ }
115
+
116
+ img,
117
+ video,
118
+ .asset-fallback {
119
+ width: 85px;
120
+ height: 55px;
121
+ cursor: pointer;
122
+ }
123
+
124
+ & + * {
125
+ margin-left: 1px;
126
+ }
127
+ }
128
+
129
+ &.main-image {
130
+ img,
131
+ video,
132
+ .asset-fallback {
133
+ width: 340px;
134
+ height: 295px;
135
+ }
136
+ }
137
+
138
+ &.catalogue-image-big-size {
139
+ img,
140
+ video,
141
+ .asset-fallback {
142
+ width: 179px;
143
+ height: 179px;
144
+ }
145
+ }
146
+
147
+ &.catalogue-image-small-size {
148
+ img,
149
+ video,
150
+ .asset-fallback {
151
+ width: 60px;
152
+ height: 60px;
153
+ }
154
+
155
+ .asset-badge {
156
+ font-size: 8px;
157
+ padding: 0 4px;
158
+ }
159
+
160
+ /* En 60px un badge de 32 tapa el fotograma completo. */
161
+ .play-badge {
162
+ width: 20px;
163
+ height: 20px;
164
+ }
165
+ }
166
+
167
+ &.catalogue-modal-image {
168
+ flex: 1 0 50%;
169
+ cursor: pointer;
170
+
171
+ img,
172
+ video,
173
+ .asset-fallback {
174
+ width: 168px;
175
+ height: 108px;
176
+ }
177
+ }
178
+
179
+ &.expanded-modal-image {
180
+ width: 710px;
181
+ height: 710px;
182
+
183
+ img,
184
+ video,
185
+ .asset-fallback {
186
+ width: 710px;
187
+ height: 710px;
188
+ }
189
+ }
190
+ `;