diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 07d7bc55..cde906f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,17 +25,42 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' + cache: 'pnpm' - - name: Cache dependencies and build + - name: Set Node.js optimization environment variables + run: | + echo "TS_NODE_TRANSPILE_ONLY=true" >> $GITHUB_ENV + echo "TS_NODE_FILES=false" >> $GITHUB_ENV + + - name: Cache dependencies uses: actions/cache@v4 with: path: | ~/.pnpm-store ${{ github.workspace }}/node_modules + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + + - name: Cache TypeScript build info + uses: actions/cache@v4 + with: + path: | + ${{ github.workspace }}/.tsbuildinfo + key: ${{ runner.os }}-ts-${{ hashFiles('**/tsconfig*.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }} + restore-keys: | + ${{ runner.os }}-ts-${{ hashFiles('**/tsconfig*.json') }}- + ${{ runner.os }}-ts- + + - name: Cache Next.js build + uses: actions/cache@v4 + with: + path: | ${{ github.workspace }}/.next/cache - key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} + key: ${{ runner.os }}-next-${{ hashFiles('**/next.config.*', '**/package.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} restore-keys: | - ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}- + ${{ runner.os }}-next-${{ hashFiles('**/next.config.*', '**/package.json') }}- + ${{ runner.os }}-next- - name: Create dummy amplify_outputs.json run: | @@ -74,5 +99,14 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Build project - run: pnpm run build + - name: Type check (separate) + run: | + echo "Running type check..." + time pnpm run type-check:fast + echo "Type check completed" + + - name: Build project (fast) + run: | + echo "Running Next.js build (no type checking)..." + time pnpm run build:fast + echo "Build completed" diff --git a/amplify.yml b/amplify.yml index 21e405d8..ab809be2 100644 --- a/amplify.yml +++ b/amplify.yml @@ -28,10 +28,14 @@ frontend: - env | grep -e LAMBDA_EMAIL_BULK >> .env.production - env | grep -e CLOUDFRONT_KEY_PAIR_ID >> .env.production - env | grep -e CLOUDFRONT_PRIVATE_KEY >> .env.production + - echo "TS_NODE_TRANSPILE_ONLY=true" >> .env.production + - echo "TS_NODE_FILES=false" >> .env.production - pnpm install --frozen-lockfile build: commands: - - pnpm run build + - echo "Running optimized build for Amplify..." + - pnpm run build:fast + - echo "Build completed" artifacts: baseDirectory: .next files: @@ -40,5 +44,6 @@ frontend: paths: - .amplify/**/* - .next/cache/**/* + - .tsbuildinfo/**/* - ~/.pnpm-store/**/* - node_modules/**/* diff --git a/amplify/auth/resource.ts b/amplify/auth/resource.ts index 51931ff4..fe359ed8 100644 --- a/amplify/auth/resource.ts +++ b/amplify/auth/resource.ts @@ -3,6 +3,7 @@ import { customMessage } from './custom-message/resource'; import { webHookPlan } from '../functions/webHookPlan/resource'; import { postConfirmation } from './post-confirmation/resource'; import { planScheduler } from '../functions/planScheduler/resource'; +import { validateStoreLimits } from '../functions/validateStoreLimits/resource'; /** * Define and configure your auth resource @@ -82,5 +83,6 @@ export const auth = defineAuth({ allow.resource(webHookPlan).to(['updateUserAttributes', 'getUser']), allow.resource(planScheduler).to(['updateUserAttributes', 'getUser']), allow.resource(postConfirmation).to(['updateUserAttributes', 'getUser']), + allow.resource(validateStoreLimits).to(['updateUserAttributes', 'getUser']), ], }); diff --git a/amplify/backend.ts b/amplify/backend.ts index 2abbf5e8..ba3d7e73 100644 --- a/amplify/backend.ts +++ b/amplify/backend.ts @@ -16,6 +16,7 @@ import { managePaymentKeys } from './functions/managePaymentKeys/resource'; import { planScheduler } from './functions/planScheduler/resource'; import { storeImages } from './functions/storeImages/resource'; import { webHookPlan } from './functions/webHookPlan/resource'; +import { validateStoreLimits } from './functions/validateStoreLimits/resource'; import { storage } from './storage/resource'; import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; @@ -54,6 +55,7 @@ const backend = defineBackend({ storeImages, managePaymentKeys, createProduct, + validateStoreLimits, }); // Aplicar permisos de Bedrock para funciones de IA @@ -102,8 +104,15 @@ applyDynamoDbPermissions(backend); const apiStack = backend.createStack('api-stack'); // Crear todas las APIs REST -const { subscriptionApi, webHookApi, checkStoreDomainApi, storeImagesApi, bulkEmailApi, apiRestPolicy } = - createRestApis(apiStack, backend); +const { + subscriptionApi, + webHookApi, + checkStoreDomainApi, + storeImagesApi, + bulkEmailApi, + storeLimitsApi, + apiRestPolicy, +} = createRestApis(apiStack, backend); // Aplicar políticas de acceso a las APIs applyApiAccessPolicies(backend, apiRestPolicy); @@ -117,6 +126,7 @@ const backendOutputs = createBackendOutputs( checkStoreDomainApi, storeImagesApi, bulkEmailApi, + storeLimitsApi, emailQueue, highPriorityEmailQueue ); diff --git a/amplify/config/apis.ts b/amplify/config/apis.ts index 722f7636..a38523d9 100644 --- a/amplify/config/apis.ts +++ b/amplify/config/apis.ts @@ -13,6 +13,7 @@ export interface ApiResources { checkStoreDomainApi: RestApi; storeImagesApi: RestApi; bulkEmailApi: RestApi; + storeLimitsApi: RestApi; apiRestPolicy: Policy; } @@ -119,6 +120,26 @@ export function createRestApis(stack: Stack, backend: any): ApiResources { emailResource.addResource('send-bulk').addMethod('POST', bulkEmailIntegration); emailResource.addResource('test-email').addMethod('POST', bulkEmailIntegration); + /** + * API para Validar Límites de Tiendas + */ + const storeLimitsApi = new RestApi(stack, 'StoreLimitsApi', { + restApiName: `StoreLimitsApi-${stageName}`, + deploy: true, + deployOptions: deployConfig, + defaultCorsPreflightOptions: corsConfig, + }); + + const storeLimitsIntegration = new LambdaIntegration(backend.validateStoreLimits.resources.lambda); + + const storeLimitsResource = storeLimitsApi.root.addResource('validate-limits', { + defaultMethodOptions: { + authorizationType: isProduction ? AuthorizationType.IAM : AuthorizationType.NONE, + }, + }); + + storeLimitsResource.addMethod('POST', storeLimitsIntegration); + /** * Política de IAM para Invocar las APIs */ @@ -132,6 +153,7 @@ export function createRestApis(stack: Stack, backend: any): ApiResources { `${checkStoreDomainApi.arnForExecuteApi('*', '/check-store-domain', stageName)}`, `${storeImagesApi.arnForExecuteApi('*', '/store-images', stageName)}`, `${bulkEmailApi.arnForExecuteApi('*', '/email/*', stageName)}`, + `${storeLimitsApi.arnForExecuteApi('*', '/validate-limits', stageName)}`, ], }), ], @@ -143,6 +165,7 @@ export function createRestApis(stack: Stack, backend: any): ApiResources { checkStoreDomainApi, storeImagesApi, bulkEmailApi, + storeLimitsApi, apiRestPolicy, }; } diff --git a/amplify/config/outputs.ts b/amplify/config/outputs.ts index 61ba32ba..be251a0f 100644 --- a/amplify/config/outputs.ts +++ b/amplify/config/outputs.ts @@ -19,6 +19,7 @@ export interface BackendOutputs { CheckStoreDomainApi: ApiOutput; StoreImagesApi: ApiOutput; BulkEmailApi: ApiOutput; + StoreLimitsApi: ApiOutput; }; EmailQueues: { emailQueueUrl: string; @@ -42,6 +43,7 @@ export function createBackendOutputs( checkStoreDomainApi: RestApi, storeImagesApi: RestApi, bulkEmailApi: RestApi, + storeLimitsApi: RestApi, emailQueue: Queue, highPriorityEmailQueue: Queue ): BackendOutputs { @@ -81,6 +83,12 @@ export function createBackendOutputs( apiName: bulkEmailApi.restApiName, stage: stageName, }, + StoreLimitsApi: { + endpoint: storeLimitsApi.url, + region: Stack.of(storeLimitsApi).region, + apiName: storeLimitsApi.restApiName, + stage: stageName, + }, }, EmailQueues: { emailQueueUrl: emailQueue.queueUrl, diff --git a/amplify/data/functions/chat-generate/systemPrompt.ts b/amplify/data/functions/chat-generate/systemPrompt.ts index e52cfd42..f4b570fc 100644 --- a/amplify/data/functions/chat-generate/systemPrompt.ts +++ b/amplify/data/functions/chat-generate/systemPrompt.ts @@ -40,30 +40,123 @@ EXPERIENCIA Y CONOCIMIENTO: - Atención al cliente y soporte - Finanzas y análisis de rentabilidad -ESTILO DE COMUNICACIÓN: -- Profesional pero accesible, evitando jerga técnica innecesaria -- Claro y directo, con ejemplos prácticos cuando sea relevante -- Estructurado y organizado, facilitando la comprensión -- Accionable, proporcionando pasos concretos y recomendaciones + ESTILO DE COMUNICACIÓN: + - Profesional pero accesible, evitando jerga técnica innecesaria + - Claro y directo, con ejemplos prácticos cuando sea relevante + - Estructurado y organizado, facilitando la comprensión + - Accionable, proporcionando pasos concretos y recomendaciones + - Ortografía perfecta: revisa cada respuesta antes de enviarla + - Evita errores de tipeo, acentos faltantes y palabras mal escritas + - Puedes usar algunos emojis para hacer las respuestas más interactivas y amigables, pero sin abusar de ellos (máximo 2-3 por respuesta) FORMATO DE RESPUESTAS: -- Introducción clara del tema -- Explicación detallada con contexto -- Ejemplos prácticos o casos de uso -- Recomendaciones específicas y accionables -- Recursos adicionales cuando sea apropiado - -RESTRICCIONES IMPORTANTES: -- NUNCA usar formato poético, haiku o verso -- EVITAR respuestas demasiado técnicas sin contexto -- NO dar consejos financieros o legales específicos sin disclaimer -- MANTENER neutralidad sobre plataformas específicas -- FOCALIZAR en soluciones prácticas y implementables +- Usar Markdown para estructurar las respuestas de manera clara y legible +- Usar encabezados (##, ###) para organizar secciones principales +- Usar listas con viñetas (-) o numeradas (1.) para recomendaciones y pasos +- Usar **negrita** para enfatizar puntos importantes +- Usar *cursiva* para términos técnicos o énfasis sutil +- Usar \`código\` para términos técnicos específicos +- Estructura típica: + ## Introducción + Breve contexto del tema + + ## Análisis/Explicación + Desarrollo detallado con contexto + + ## Recomendaciones Prácticas + - Paso 1: Descripción + - Paso 2: Descripción + + ## Ejemplos/Casos de Uso + Casos prácticos cuando sea relevante + + ## Recursos Adicionales + - Enlaces o herramientas útiles + + RESTRICCIONES IMPORTANTES: + - NUNCA usar formato poético, haiku o verso + - NUNCA usar formato o cualquier tag similar (incluyendo , , o cualquier variación) + - NUNCA mostrar procesos de pensamiento interno o reflexiones + - NUNCA INVENTAR información que no conoces + - NUNCA hacer suposiciones sobre datos específicos o estadísticas + - SI NO SABES algo, admítelo claramente: \`No tengo información específica sobre...\` + - EVITAR respuestas demasiado técnicas sin contexto + - NO dar consejos financieros o legales específicos sin disclaimer + - MANTENER neutralidad sobre plataformas específicas + - FOCALIZAR en soluciones prácticas y implementables + - SIEMPRE enviar solo la respuesta final, sin explicaciones internas + - PROHIBIDO ABSOLUTO: Usar tags como , , , o cualquier formato de pensamiento interno CONTEXTO DE FASTTIFY: Eres parte del equipo de consultoría de Fasttify, una plataforma innovadora que ayuda a emprendedores y empresas a crear y hacer crecer sus negocios de comercio electrónico. Mantén este contexto en mente al proporcionar consejos y recomendaciones. -Recuerda: Tu objetivo es ser el consultor de e-commerce de confianza que ayuda a los usuarios a tomar decisiones informadas y implementar estrategias efectivas para el crecimiento de sus negocios online.`; +IMPORTANTE: +- NO hables sobre tu estilo de capacitación o metodología de trabajo +- NO describas tus características como consultor +- NO expliques cómo trabajas o tu proceso interno +- NO respondas preguntas técnicas sobre ti mismo (modelo de IA, tecnología, etc.) +- NO reveles información sobre sistemas, arquitectura o implementación técnica +- ENFÓCATE únicamente en responder consultas específicas sobre e-commerce +- PROPORCIONA información práctica y accionable sobre tiendas online, marketing digital, ventas online, etc. +- Si te preguntan sobre tecnología o sobre ti mismo, responde: \`Mi especialidad es el e-commerce. ¿En qué puedo ayudarte con tu tienda online?\` + +ALCANCE DE CONSULTAS: +- SOLO responde preguntas relacionadas con e-commerce, tiendas online y negocios digitales +- Temas válidos: marketing digital, SEO, SEM, redes sociales, conversión, UX/UI, logística, inventario, análisis de datos, estrategias de venta, etc. +- Temas NO válidos: tecnología de IA, modelos de lenguaje, sistemas informáticos, programación, arquitectura de software, etc. +- Si te preguntan algo fuera del e-commerce, redirige educadamente al tema + +TONO Y PERSONALIDAD: +- Mantén un tono profesional pero cercano y accesible +- Sé empático con las dificultades de los emprendedores +- Usa un lenguaje motivador y positivo +- Evita ser demasiado formal o académico +- Adapta el nivel técnico al contexto de la pregunta + +INFORMACIÓN SENSIBLE: +- NO pidas información personal específica (nombres, direcciones, números de tarjeta) +- NO solicites datos financieros confidenciales +- NO almacenes ni recuerdes información personal entre conversaciones +- Mantén un enfoque general y educativo + +ESTRUCTURA DE RESPUESTAS: +- Máximo 3-4 secciones principales por respuesta +- Cada sección con máximo 3-4 puntos clave +- Usar bullets concisos (máximo 1 línea por bullet) +- Incluir solo ejemplos relevantes y específicos +- Terminar con una pregunta para continuar la conversación + +PREGUNTAS VAGAS: +- Si la pregunta es muy general, pide aclaración específica +- Sugiere 2-3 enfoques diferentes para abordar el tema +- Proporciona un marco general y luego profundiza según la respuesta + +LÍMITES DE RESPUESTA: +- Máximo 800 palabras por respuesta +- Si necesitas más espacio, sugiere continuar en otro mensaje +- Prioriza calidad sobre cantidad de información + +HONESTIDAD Y TRANSPARENCIA: +- SIEMPRE sé honesto sobre lo que sabes y lo que no sabes +- Si no tienes información específica sobre algo, dilo claramente +- NO inventes datos, estadísticas, precios o información técnica específica +- Si algo está fuera de tu área de expertise, reconócelo +- Mejor decir \`No tengo esa información\` que inventar algo + + CONTROL DE CALIDAD: + - Antes de enviar cualquier respuesta, verifica la ortografía y gramática + - Asegúrate de que todas las palabras estén correctamente acentuadas + - Revisa que no haya errores de tipeo o palabras faltantes + - Confirma que el formato Markdown esté bien estructurado + - VERIFICA que NO hay tags de pensamiento como , , etc. + - Envía únicamente la respuesta final, sin procesos internos visibles + - Asegúrate de que la respuesta comience directamente con el contenido útil + +RECUERDA: +- Tu objetivo es ser el consultor de e-commerce de confianza que ayuda a los usuarios a tomar decisiones informadas y implementar estrategias efectivas para el crecimiento de sus negocios online. +- Responde DIRECTAMENTE a la pregunta del usuario con información útil sobre e-commerce. +- NO te describas a ti mismo ni expliques tu metodología de trabajo. +- Mantén el foco en la consulta específica del usuario.`; /** * Función helper para crear el prompt del usuario para consultas de chat @@ -83,11 +176,23 @@ export const processChatResponse = (responseText: string): string => { // Limpiar y formatear la respuesta let cleanedResponse = responseText.trim(); - // Remover cualquier formato de código markdown si no es apropiado - if (cleanedResponse.startsWith('```') && cleanedResponse.endsWith('```')) { - // Si toda la respuesta está en un bloque de código, remover el formato + // Solo remover bloques de código markdown si toda la respuesta está envuelta en ellos + // Esto preserva el formato Markdown normal pero evita respuestas que sean solo código + if ( + cleanedResponse.startsWith('```') && + cleanedResponse.endsWith('```') && + cleanedResponse.split('```').length === 3 + ) { + // Si toda la respuesta está en un solo bloque de código, remover el formato cleanedResponse = cleanedResponse.replace(/```\w*\n?|\n?```/g, '').trim(); } + // Asegurar que los encabezados Markdown tengan espacio después + cleanedResponse = cleanedResponse.replace(/^(#{1,6})\s*([^\n]+)$/gm, '$1 $2'); + + // Asegurar que las listas tengan formato consistente + cleanedResponse = cleanedResponse.replace(/^\s*[-*]\s+(.+)$/gm, '- $1'); + cleanedResponse = cleanedResponse.replace(/^\s*(\d+\.)\s+(.+)$/gm, '$1 $2'); + return cleanedResponse; }; diff --git a/amplify/data/functions/description-generate/systemPrompt.ts b/amplify/data/functions/description-generate/systemPrompt.ts index e605b41d..a2ffb42a 100644 --- a/amplify/data/functions/description-generate/systemPrompt.ts +++ b/amplify/data/functions/description-generate/systemPrompt.ts @@ -3,57 +3,66 @@ * Optimizado para crear descripciones persuasivas que conviertan */ -export const PRODUCT_DESCRIPTION_SYSTEM_PROMPT = `Eres un experto en copywriting y marketing de productos con más de 10 años de experiencia. Tu especialidad es crear descripciones de productos que convierten visitantes en compradores. +export const PRODUCT_DESCRIPTION_SYSTEM_PROMPT = `Eres un experto en copywriting especializado en e-commerce con más de 12 años de experiencia creando descripciones que convierten. Tu misión es transformar productos en historias irresistibles que impulsen las ventas. OBJETIVO PRINCIPAL: -Crear descripciones de productos atractivas, persuasivas y optimizadas para conversión que motiven la compra inmediata. - -EXPERIENCIA Y CONOCIMIENTO: -- Especialista en psicología del consumidor y técnicas de persuasión -- Experto en optimización de conversiones (CRO) y e-commerce -- Conocedor profundo de SEO y palabras clave relevantes -- Experiencia en múltiples industrias y nichos de mercado - -METODOLOGÍA DE COPYWRITING: -1. ANÁLISIS: Identificar el problema que resuelve el producto y el beneficio emocional -2. ESTRUCTURA: Usar el modelo AIDA (Atención, Interés, Deseo, Acción) -3. PERSUASIÓN: Aplicar principios de influencia y urgencia -4. OPTIMIZACIÓN: Incluir palabras clave naturales y llamadas a la acción - -ESTILO Y TONO: -- Profesional pero accesible, nunca formal o corporativo -- Entusiasta y confiado, sin ser exagerado -- Claro y directo, evitando jerga técnica innecesaria -- Empatético con las necesidades del cliente - -ESTRUCTURA RECOMENDADA: -1. Hook inicial que capture la atención -2. Beneficio principal y problema que resuelve -3. Características clave que respaldan el beneficio -4. Casos de uso específicos y situaciones aplicables -5. Llamada sutil a la acción o beneficio final - -TÉCNICAS DE PERSUASIÓN A UTILIZAR: -- Escasez y exclusividad -- Prueba social implícita -- Beneficios emocionales vs. características técnicas -- Storytelling breve y relevante -- Preguntas retóricas que conecten emocionalmente - -RESTRICCIONES IMPORTANTES: -- NUNCA incluir precios, descuentos o información de envío +Crear descripciones persuasivas que conecten emocionalmente con el comprador y motiven la acción de compra inmediata. + +EXPERIENCIA ESPECIALIZADA: +- Copywriter senior especializado en conversión de e-commerce +- Experto en psicología del consumidor y comportamiento de compra online +- Especialista en optimización de conversiones (CRO) para tiendas digitales +- Conocedor de tendencias de consumo y patrones de decisión de compra + +METODOLOGÍA PROBADA: +1. CONEXIÓN: Identificar el dolor o deseo del cliente objetivo +2. BENEFICIO: Destacar la transformación que experimentará +3. CREDIBILIDAD: Respaldar con características específicas y relevantes +4. URGENCIA: Crear sensación de necesidad sin ser agresivo +5. ACCIÓN: Guiar sutilmente hacia la decisión de compra + +ESTILO DE COMUNICACIÓN: +- Conversacional y cercano, como hablar con un amigo experto +- Confiado y entusiasta, sin exageraciones +- Claro y directo, evitando tecnicismos innecesarios +- Empático y orientado a resolver problemas reales + +ESTRUCTURA OPTIMIZADA: +1. **Hook emocional** (15-20 palabras): Captura la atención con el beneficio principal +2. **Desarrollo del beneficio** (40-50 palabras): Explica cómo mejora la vida del cliente +3. **Características clave** (30-40 palabras): Respalda con 2-3 atributos específicos +4. **Caso de uso** (25-35 palabras): Situación concreta donde brilla el producto +5. **Cierre motivacional** (10-15 palabras): Sensación de satisfacción o transformación + +TÉCNICAS DE PERSUASIÓN: +- Beneficios emocionales sobre características técnicas +- Storytelling breve que resuene con la experiencia del cliente +- Palabras de poder: "transforma", "descubre", "eleva", "mejora" +- Elementos de exclusividad o calidad premium sutil +- Conexión con aspiraciones o necesidades profundas + +RESTRICCIONES CRÍTICAS: +- NUNCA incluir precios, descuentos, envíos o información comercial - NO usar superlativos excesivos o afirmaciones no verificables -- EVITAR lenguaje promocional directo o spammy -- NO incluir información técnica muy específica que confunda -- MANTENER credibilidad y autenticidad en todo momento +- EVITAR lenguaje promocional directo o spam +- NO incluir detalles técnicos complejos que confundan +- MANTENER autenticidad y credibilidad en cada palabra + +FORMATO DE ENTREGA: +- Exactamente 100-150 palabras +- Párrafos de 2-3 líneas máximo +- Flujo narrativo natural y conversacional +- Lenguaje activo y dinámico +- Sin listas con viñetas ni formato de bullet points -FORMATO DE SALIDA: -- Entre 100-150 palabras como máximo -- Párrafos cortos y legibles -- Uso estratégico de negritas para destacar beneficios clave -- Flujo natural que guíe hacia la decisión de compra +CALIDAD Y EFECTIVIDAD: +- Cada palabra debe servir al objetivo de conversión +- La descripción debe hacer que el cliente se imagine usando el producto +- Debe generar confianza en la calidad y valor del producto +- Debe diferenciarse de la competencia de manera sutil +- Debe cerrar con una sensación positiva y motivadora -Recuerda: Tu objetivo es crear una descripción que no solo informe, sino que inspire confianza y genere el deseo de poseer el producto.`; +Recuerda: Tu trabajo es crear una descripción que no solo venda el producto, sino que haga que el cliente se enamore de la experiencia que tendrá al usarlo.`; /** * Función helper para crear el prompt del usuario @@ -64,36 +73,36 @@ Recuerda: Tu objetivo es crear una descripción que no solo informe, sino que in export const createUserPrompt = (productName: string, category?: string): string => { const categoryText = category ? ` en la categoría ${category}` : ''; - return `Crea una descripción de producto optimizada para conversión para "${productName}"${categoryText}. + return `Genera una descripción irresistible para "${productName}"${categoryText} que impulse las ventas. -CONTEXTO DEL PRODUCTO: -- Nombre: ${productName} +INFORMACIÓN DEL PRODUCTO: +- Producto: ${productName} - Categoría: ${category || 'No especificada'} -REQUISITOS ESPECÍFICOS: -- Longitud: 100-150 palabras exactas -- Enfoque: Beneficios emocionales y funcionales -- Objetivo: Generar deseo de compra inmediato -- Tono: Profesional, confiado y accesible - -ELEMENTOS A INCLUIR: -1. Hook inicial que capture atención (máximo 20 palabras) -2. Beneficio principal que resuelve un problema específico -3. 2-3 características clave que respalden el beneficio -4. Casos de uso concretos y situaciones reales -5. Cierre que motive a la acción - -TÉCNICAS A APLICAR: -- Usar palabras de poder (descubre, transforma, mejora, eleva) -- Incluir elementos de urgencia o exclusividad sutil -- Conectar emocionalmente con el target -- Crear sensación de necesidad o deseo - -FORMATO DE SALIDA: -- Párrafos cortos (2-3 líneas máximo) -- Lenguaje activo y dinámico -- Evitar listas con viñetas -- Flujo narrativo natural +ESTRUCTURA REQUERIDA (100-150 palabras exactas): + +**HOOK EMOCIONAL** (15-20 palabras): +Captura la atención con el beneficio principal que más importa al cliente. + +**DESARROLLO DEL BENEFICIO** (40-50 palabras): +Explica cómo este producto mejora específicamente la vida del usuario, enfocándote en la transformación que experimentará. + +**CARACTERÍSTICAS CLAVE** (30-40 palabras): +Respalda el beneficio con 2-3 atributos específicos que demuestren calidad y valor. + +**CASO DE USO** (25-35 palabras): +Describe una situación concreta donde el producto brilla y resuelve un problema real. + +**CIERRE MOTIVACIONAL** (10-15 palabras): +Termina con una sensación de satisfacción, confianza o transformación positiva. + +REQUISITOS DE CALIDAD: +- Lenguaje conversacional y cercano +- Beneficios emocionales sobre características técnicas +- Palabras de poder: "transforma", "descubre", "eleva", "mejora" +- Flujo narrativo natural sin listas ni viñetas +- Cada palabra debe servir al objetivo de conversión -Recuerda: La descripción debe hacer que el cliente se imagine usando el producto y experimentando sus beneficios.`; +OBJETIVO FINAL: +Crear una descripción que haga que el cliente se enamore de la experiencia de usar este producto y sienta la necesidad inmediata de poseerlo.`; }; diff --git a/amplify/data/resource.ts b/amplify/data/resource.ts index f4203789..e3c465fe 100644 --- a/amplify/data/resource.ts +++ b/amplify/data/resource.ts @@ -6,6 +6,7 @@ import { managePaymentKeys } from '../functions/managePaymentKeys/resource'; import { onboardingProgress } from '../functions/onboardingProgress/resource'; import { planScheduler } from '../functions/planScheduler/resource'; import { webHookPlan } from '../functions/webHookPlan/resource'; +import { validateStoreLimits } from '../functions/validateStoreLimits/resource'; // Importacion de modelos import { cartModel } from './models/cart'; @@ -112,6 +113,7 @@ export const storeSchema = a allow.resource(createStoreTemplate), allow.resource(managePaymentKeys), allow.resource(onboardingProgress), + allow.resource(validateStoreLimits), ]); // Schema completo incluyendo funciones de IA @@ -325,6 +327,7 @@ const fullSchema = a allow.resource(createStoreTemplate), allow.resource(managePaymentKeys), allow.resource(onboardingProgress), + allow.resource(validateStoreLimits), ]); // Tipos separados para mejorar rendimiento del linter diff --git a/amplify/functions/storeImages/resource.ts b/amplify/functions/storeImages/resource.ts index 3a7563c4..3643de8e 100644 --- a/amplify/functions/storeImages/resource.ts +++ b/amplify/functions/storeImages/resource.ts @@ -3,6 +3,8 @@ import { defineFunction, secret } from '@aws-amplify/backend'; export const storeImages = defineFunction({ name: 'storeImages', entry: 'handler.ts', + timeoutSeconds: 60, + memoryMB: 1024, resourceGroupName: 'store-management', environment: { BUCKET_NAME: secret('BUCKET_NAME'), diff --git a/amplify/functions/storeImages/services/s3-service.ts b/amplify/functions/storeImages/services/s3-service.ts index ab2d4138..aef4b07c 100644 --- a/amplify/functions/storeImages/services/s3-service.ts +++ b/amplify/functions/storeImages/services/s3-service.ts @@ -347,21 +347,13 @@ export class S3Service { * Esto asegura que el mismo archivo siempre tenga el mismo ID */ private generateConsistentId(key: string): string { - // Usar una combinación de hash simple de la clave + timestamp para ID único - let hash = 0; - for (let i = 0; i < key.length; i++) { - const char = key.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; // Convert to 32bit integer - } - - // Combinar con timestamp de la clave si está disponible + const shortId = FileUtils.generateShortId(); const keyParts = key.split('/'); const filenamePart = keyParts[keyParts.length - 1]; const timestampMatch = filenamePart.match(/^(\d+)-/); const timestamp = timestampMatch ? timestampMatch[1] : Date.now().toString(); - return `img_${Math.abs(hash)}_${timestamp}`; + return `img_${shortId}_${timestamp}`; } /** diff --git a/amplify/functions/storeImages/services/utils.ts b/amplify/functions/storeImages/services/utils.ts index 58e35a1d..4c99ffa5 100644 --- a/amplify/functions/storeImages/services/utils.ts +++ b/amplify/functions/storeImages/services/utils.ts @@ -1,3 +1,5 @@ +import crypto from 'crypto'; + export class FileUtils { private static readonly MIME_TYPES: Record = { jpg: 'image/jpeg', @@ -11,18 +13,14 @@ export class FileUtils { * Genera un UUID v4 simple sin dependencias externas */ public static generateUUID(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - const r = (Math.random() * 16) | 0; - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); + return crypto.randomUUID(); } /** * Genera un ID único corto para evitar nombres muy largos */ public static generateShortId(): string { - return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + return crypto.randomBytes(16).toString('hex'); } /** diff --git a/amplify/functions/validateStoreLimits/handler.ts b/amplify/functions/validateStoreLimits/handler.ts new file mode 100644 index 00000000..6f43360f --- /dev/null +++ b/amplify/functions/validateStoreLimits/handler.ts @@ -0,0 +1,186 @@ +import { getAmplifyDataClientConfig } from '@aws-amplify/backend/function/runtime'; +import { Amplify } from 'aws-amplify'; +import { getCorsHeaders } from '../shared/cors'; +import { generateClient } from 'aws-amplify/data'; +import { env } from '$amplify/env/validate-store-limits'; +import type { StoreSchema } from '../../data/resource'; +import { CognitoIdentityProviderClient, AdminGetUserCommand } from '@aws-sdk/client-cognito-identity-provider'; + +const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig(env); +Amplify.configure(resourceConfig, libraryOptions); + +const client = generateClient(); +const cognitoClient = new CognitoIdentityProviderClient(); +const userPoolId = env.USER_POOL_ID; + +// Límites de tiendas por plan +const STORE_LIMITS = { + Imperial: 5, + Majestic: 3, + Royal: 1, + free: 0, +} as const; + +interface ValidateStoreLimitsInput { + userId: string; + userPoolId?: string; + headers?: { + origin?: string; + Origin?: string; + }; + httpMethod?: string; + body?: string; +} + +interface ValidateStoreLimitsResult { + statusCode: number; + body: string; + headers: Record; +} + +/** + * Handler para validar límites de creación de tiendas por plan + */ +export const handler = async (event: ValidateStoreLimitsInput): Promise => { + const origin = event.headers?.origin || event.headers?.Origin; + if (event.httpMethod === 'OPTIONS') { + return { + statusCode: 200, + headers: getCorsHeaders(origin), + body: '', + }; + } + + let userId: string; + + try { + if (event.body) { + // Parsear el body que puede estar doblemente codificado + let bodyData; + try { + bodyData = JSON.parse(event.body); + // Si el resultado sigue siendo una cadena, parsearlo de nuevo + if (typeof bodyData === 'string') { + bodyData = JSON.parse(bodyData); + } + } catch { + // Si falla, intentar parsear directamente + bodyData = JSON.parse(event.body); + } + userId = bodyData.userId; + } else { + userId = event.userId; + } + } catch (error) { + console.error('Error parsing request body:', error); + userId = event.userId; + } + try { + const poolId = userPoolId; + + if (!userId) { + return { + statusCode: 400, + body: JSON.stringify({ + success: false, + canCreateStore: false, + currentCount: 0, + limit: 0, + plan: 'unknown', + error: 'userId is required', + }), + headers: getCorsHeaders(origin), + }; + } + + // Obtener plan del usuario desde Cognito + const userPlan = await getUserPlan(userId, poolId); + + if (!userPlan || userPlan === 'free') { + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + canCreateStore: false, + currentCount: 0, + limit: STORE_LIMITS.free, + plan: userPlan || 'free', + message: 'Free plan does not allow creating stores', + }), + headers: getCorsHeaders(origin), + }; + } + + // Obtener número actual de tiendas del usuario + const currentCount = await getUserStoreCount(userId); + + // Verificar límite + const limit = STORE_LIMITS[userPlan as keyof typeof STORE_LIMITS] || 0; + const canCreateStore = currentCount < limit; + + return { + statusCode: 200, + body: JSON.stringify({ + success: true, + canCreateStore, + currentCount, + limit, + plan: userPlan, + message: canCreateStore + ? `Can create store (${currentCount}/${limit})` + : `Limit reached (${currentCount}/${limit})`, + }), + headers: getCorsHeaders(origin), + }; + } catch (error) { + console.error('Error validating store limits:', error); + return { + statusCode: 500, + body: JSON.stringify({ + success: false, + canCreateStore: false, + currentCount: 0, + limit: 0, + plan: 'unknown', + error: error instanceof Error ? error.message : 'Unknown error', + }), + headers: getCorsHeaders(origin), + }; + } +}; + +/** + * Obtiene el plan del usuario desde Cognito + */ +async function getUserPlan(userId: string, userPoolId: string): Promise { + try { + const response = await cognitoClient.send( + new AdminGetUserCommand({ + UserPoolId: userPoolId, + Username: userId, + }) + ); + + const planAttribute = response.UserAttributes?.find((attr) => attr.Name === 'custom:plan'); + return planAttribute?.Value || null; + } catch (error) { + console.error('Error getting user plan:', error); + throw new Error('Failed to get user plan'); + } +} + +/** + * Obtiene el número actual de tiendas del usuario + */ +async function getUserStoreCount(userId: string): Promise { + try { + const response = await client.models.UserStore.listUserStoreByUserId({ + userId, + }); + + return response.data?.length || 0; + } catch (error) { + console.error('Error getting user store count:', error); + throw new Error('Failed to get user store count'); + } +} diff --git a/amplify/functions/validateStoreLimits/resource.ts b/amplify/functions/validateStoreLimits/resource.ts new file mode 100644 index 00000000..efa49231 --- /dev/null +++ b/amplify/functions/validateStoreLimits/resource.ts @@ -0,0 +1,12 @@ +import { defineFunction, secret } from '@aws-amplify/backend'; + +export const validateStoreLimits = defineFunction({ + name: 'validate-store-limits', + entry: 'handler.ts', + timeoutSeconds: 20, + memoryMB: 1024, + runtime: 20, + environment: { + USER_POOL_ID: secret('USER_POOL_ID'), + }, +}); diff --git a/amplify/tsconfig.json b/amplify/tsconfig.json index fb4251dd..53e23268 100644 --- a/amplify/tsconfig.json +++ b/amplify/tsconfig.json @@ -8,6 +8,9 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, + "incremental": true, + "tsBuildInfoFile": "../.amplify/tsconfig.tsbuildinfo", + "assumeChangesOnlyAffectDirectDependencies": true, "paths": { "$amplify/*": ["../.amplify/generated/*"] } diff --git a/app/(main-layout)/landing/components/DocsLanding.tsx b/app/(main-layout)/landing/components/DocsLanding.tsx deleted file mode 100644 index 11425059..00000000 --- a/app/(main-layout)/landing/components/DocsLanding.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { DropshippingIntro } from '@/app/(main-layout)/landing/components/DropshippingIntro'; -import { Platform } from '@/app/(main-layout)/landing/components/Platform'; -import { Footer } from '@/app/(main-layout)/landing/components/Footer'; -import { StepGuide } from '@/app/(main-layout)/landing/components/StepGuide'; -import { FirstView } from '@/app/(main-layout)/landing/components/FirstView'; -import { AboutUs } from '@/app/(main-layout)/landing/components/AboutUs'; -import { FashionSlider } from '@/app/(main-layout)/landing/components/FashionSlider'; -import { Feature } from '@/app/(main-layout)/landing/components/Feature'; -import { Testimonials } from '@/app/(main-layout)/landing/components/Testimonials'; -import { MarqueeLogos } from '@/app/(main-layout)/landing/components/MarqueeLogos'; -import { LogoCarousell } from '@/app/(main-layout)/landing/components/LogoCarousell'; - -export default function LandingPage() { - return ( - <> - - - -
- -
- -
- - - -
- -
- -
-
- -
- -
-
-
- - ); -} diff --git a/app/(main-layout)/page.tsx b/app/(main-layout)/page.tsx deleted file mode 100644 index 2f52ccc5..00000000 --- a/app/(main-layout)/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -'use client'; - -import DocsLanding from '@/app/(main-layout)/landing/components/DocsLanding'; - -export default function Home() { - return ; -} diff --git a/app/(setup-layout)/first-steps/components/MultiStepLoader.tsx b/app/(setup)/first-steps/components/MultiStepLoader.tsx similarity index 100% rename from app/(setup-layout)/first-steps/components/MultiStepLoader.tsx rename to app/(setup)/first-steps/components/MultiStepLoader.tsx diff --git a/app/(setup-layout)/first-steps/components/PersonalInfo.tsx b/app/(setup)/first-steps/components/PersonalInfo.tsx similarity index 100% rename from app/(setup-layout)/first-steps/components/PersonalInfo.tsx rename to app/(setup)/first-steps/components/PersonalInfo.tsx diff --git a/app/(setup-layout)/first-steps/components/StoreInfo.tsx b/app/(setup)/first-steps/components/StoreInfo.tsx similarity index 100% rename from app/(setup-layout)/first-steps/components/StoreInfo.tsx rename to app/(setup)/first-steps/components/StoreInfo.tsx diff --git a/app/(setup-layout)/first-steps/data/selling-options.json b/app/(setup)/first-steps/data/selling-options.json similarity index 100% rename from app/(setup-layout)/first-steps/data/selling-options.json rename to app/(setup)/first-steps/data/selling-options.json diff --git a/app/(setup-layout)/first-steps/hooks/useFirstStepsSetup.ts b/app/(setup)/first-steps/hooks/useFirstStepsSetup.ts similarity index 88% rename from app/(setup-layout)/first-steps/hooks/useFirstStepsSetup.ts rename to app/(setup)/first-steps/hooks/useFirstStepsSetup.ts index aa1f05b3..3d637c03 100644 --- a/app/(setup-layout)/first-steps/hooks/useFirstStepsSetup.ts +++ b/app/(setup)/first-steps/hooks/useFirstStepsSetup.ts @@ -1,9 +1,9 @@ -import sellingOptionsData from '@/app/(setup-layout)/first-steps/data/selling-options.json'; -import { useFormManager } from '@/app/(setup-layout)/first-steps/hooks/useFormManager'; -import { useStepManager } from '@/app/(setup-layout)/first-steps/hooks/useStepManager'; -import { useStoreCreation } from '@/app/(setup-layout)/first-steps/hooks/useStoreCreation'; -import { useTemplateUpload } from '@/app/(setup-layout)/first-steps/hooks/useTemplateUpload'; -import { useUserStoreData } from '@/app/(setup-layout)/first-steps/hooks/useUserStoreData'; +import sellingOptionsData from '@/app/(setup)/first-steps/data/selling-options.json'; +import { useFormManager } from '@/app/(setup)/first-steps/hooks/useFormManager'; +import { useStepManager } from '@/app/(setup)/first-steps/hooks/useStepManager'; +import { useStoreCreation } from '@/app/(setup)/first-steps/hooks/useStoreCreation'; +import { useTemplateUpload } from '@/app/(setup)/first-steps/hooks/useTemplateUpload'; +import { useUserStoreData } from '@/app/(setup)/first-steps/hooks/useUserStoreData'; import useAuthStore from '@/context/core/userStore'; import { routes } from '@/utils/client/routes'; import { useState } from 'react'; diff --git a/app/(setup-layout)/first-steps/hooks/useFormManager.ts b/app/(setup)/first-steps/hooks/useFormManager.ts similarity index 100% rename from app/(setup-layout)/first-steps/hooks/useFormManager.ts rename to app/(setup)/first-steps/hooks/useFormManager.ts diff --git a/app/(setup-layout)/first-steps/hooks/useStepManager.ts b/app/(setup)/first-steps/hooks/useStepManager.ts similarity index 100% rename from app/(setup-layout)/first-steps/hooks/useStepManager.ts rename to app/(setup)/first-steps/hooks/useStepManager.ts diff --git a/app/(setup-layout)/first-steps/hooks/useStoreCreation.ts b/app/(setup)/first-steps/hooks/useStoreCreation.ts similarity index 86% rename from app/(setup-layout)/first-steps/hooks/useStoreCreation.ts rename to app/(setup)/first-steps/hooks/useStoreCreation.ts index f0a98dad..8cbf8c0a 100644 --- a/app/(setup-layout)/first-steps/hooks/useStoreCreation.ts +++ b/app/(setup)/first-steps/hooks/useStoreCreation.ts @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useStoreLimitsValidator } from './useStoreLimitsValidator'; interface StoreInput { userId: string; @@ -34,6 +35,7 @@ export const useStoreCreation = ({ createStoreWithTemplate, uploadTemplate }: Us const [saving, setSaving] = useState(false); const [uploadingTemplate, setUploadingTemplate] = useState(false); const [error, setError] = useState(null); + const { validateLimits } = useStoreLimitsValidator(); const createStoreAndUploadTemplate = async ( storeInput: StoreInput, @@ -42,6 +44,12 @@ export const useStoreCreation = ({ createStoreWithTemplate, uploadTemplate }: Us setSaving(true); setError(null); try { + // Validar límites antes de crear la tienda + const canCreate = await validateLimits(); + if (!canCreate) { + throw new Error('You have reached the limit of stores for your current plan'); + } + const result = await createStoreWithTemplate(storeInput); if (result) { try { diff --git a/app/(setup)/first-steps/hooks/useStoreLimitsValidator.ts b/app/(setup)/first-steps/hooks/useStoreLimitsValidator.ts new file mode 100644 index 00000000..229980a5 --- /dev/null +++ b/app/(setup)/first-steps/hooks/useStoreLimitsValidator.ts @@ -0,0 +1,83 @@ +import { useState, useCallback } from 'react'; +import { post } from 'aws-amplify/api'; +import useAuthStore from '@/context/core/userStore'; + +/** + * Hook personalizado para validar límites de creación de tiendas. + * Realiza una petición a la función Lambda para verificar si el usuario puede crear más tiendas. + * + * @returns {Object} Un objeto con las siguientes propiedades: + * - validateLimits: Función para validar los límites de tiendas + * - isValidating: Estado que indica si se está realizando la validación + * - canCreateStore: Estado que indica si puede crear más tiendas + * - storeInfo: Información sobre el estado actual de las tiendas + */ +export function useStoreLimitsValidator() { + const [isValidating, setIsValidating] = useState(false); + const [canCreateStore, setCanCreateStore] = useState(true); + const [storeInfo, setStoreInfo] = useState<{ + currentCount: number; + limit: number; + plan: string; + message?: string; + } | null>(null); + const { user } = useAuthStore(); + /** + * Verifica si el usuario puede crear más tiendas según su plan. + * @returns {Promise} - true si puede crear tienda, false si ha alcanzado el límite + */ + const validateLimits = useCallback(async (): Promise => { + setIsValidating(true); + + try { + const response = await post({ + apiName: 'StoreLimitsApi', + path: 'validate-limits', + options: { + body: JSON.stringify({ + userId: user?.userId, + }), + retryStrategy: { + strategy: 'jittered-exponential-backoff', + }, + }, + }); + + const { body } = await response.response; + const result = (await body.json()) as { + success: boolean; + canCreateStore: boolean; + currentCount: number; + limit: number; + plan: string; + message?: string; + }; + + setCanCreateStore(result.canCreateStore); + setStoreInfo({ + currentCount: result.currentCount, + limit: result.limit, + plan: result.plan, + message: result.message, + }); + + return result.canCreateStore; + } catch (error) { + console.error('Error validating store limits:', error); + // En caso de error, NO permitir la creación + // pero mostrar un mensaje de advertencia + setCanCreateStore(true); + setStoreInfo({ + currentCount: 0, + limit: 0, + plan: 'unknown', + message: 'Error validando límites, procediendo con precaución', + }); + return false; + } finally { + setIsValidating(false); + } + }, [user?.userId]); + + return { validateLimits, isValidating, canCreateStore, storeInfo }; +} diff --git a/app/(setup-layout)/first-steps/hooks/useTemplateUpload.ts b/app/(setup)/first-steps/hooks/useTemplateUpload.ts similarity index 100% rename from app/(setup-layout)/first-steps/hooks/useTemplateUpload.ts rename to app/(setup)/first-steps/hooks/useTemplateUpload.ts diff --git a/app/(setup-layout)/first-steps/hooks/useUserStoreData.ts b/app/(setup)/first-steps/hooks/useUserStoreData.ts similarity index 100% rename from app/(setup-layout)/first-steps/hooks/useUserStoreData.ts rename to app/(setup)/first-steps/hooks/useUserStoreData.ts diff --git a/app/(setup-layout)/first-steps/layout.tsx b/app/(setup)/first-steps/layout.tsx similarity index 100% rename from app/(setup-layout)/first-steps/layout.tsx rename to app/(setup)/first-steps/layout.tsx diff --git a/app/(setup-layout)/first-steps/page.tsx b/app/(setup)/first-steps/page.tsx similarity index 94% rename from app/(setup-layout)/first-steps/page.tsx rename to app/(setup)/first-steps/page.tsx index 785633d4..37e4732a 100644 --- a/app/(setup-layout)/first-steps/page.tsx +++ b/app/(setup)/first-steps/page.tsx @@ -1,10 +1,10 @@ 'use client'; -import { AnimatedBackground } from '@/app/(setup-layout)/my-store/components/AnimatedBackground'; -import { MultiStepLoader } from '@/app/(setup-layout)/first-steps/components/MultiStepLoader'; -import PersonalInfo from '@/app/(setup-layout)/first-steps/components/PersonalInfo'; -import StoreInfo from '@/app/(setup-layout)/first-steps/components/StoreInfo'; -import { useFirstStepsSetup } from '@/app/(setup-layout)/first-steps/hooks/useFirstStepsSetup'; +import { AnimatedBackground } from '@/app/(setup)/my-store/components/AnimatedBackground'; +import { MultiStepLoader } from '@/app/(setup)/first-steps/components/MultiStepLoader'; +import PersonalInfo from '@/app/(setup)/first-steps/components/PersonalInfo'; +import StoreInfo from '@/app/(setup)/first-steps/components/StoreInfo'; +import { useFirstStepsSetup } from '@/app/(setup)/first-steps/hooks/useFirstStepsSetup'; import { Button } from '@/components/ui/button'; import { AnimatePresence, motion } from 'framer-motion'; import { ArrowRight, InfoIcon, Store, User } from 'lucide-react'; diff --git a/app/(setup-layout)/layout.tsx b/app/(setup)/layout.tsx similarity index 100% rename from app/(setup-layout)/layout.tsx rename to app/(setup)/layout.tsx diff --git a/app/(setup)/login/LoginClient.tsx b/app/(setup)/login/LoginClient.tsx new file mode 100644 index 00000000..bb998732 --- /dev/null +++ b/app/(setup)/login/LoginClient.tsx @@ -0,0 +1,7 @@ +'use client'; + +import AuthClient from '@/app/(setup)/login/components/main-components/AuthClient'; + +export default function LoginClient() { + return ; +} diff --git a/app/(setup-layout)/login/components/forgot-password/ForgotPasswordForm.tsx b/app/(setup)/login/components/forgot-password/ForgotPasswordForm.tsx similarity index 100% rename from app/(setup-layout)/login/components/forgot-password/ForgotPasswordForm.tsx rename to app/(setup)/login/components/forgot-password/ForgotPasswordForm.tsx diff --git a/app/(setup-layout)/login/components/main-components/AuthClient.tsx b/app/(setup)/login/components/main-components/AuthClient.tsx similarity index 64% rename from app/(setup-layout)/login/components/main-components/AuthClient.tsx rename to app/(setup)/login/components/main-components/AuthClient.tsx index 6d2bd3a9..bfd7d89d 100644 --- a/app/(setup-layout)/login/components/main-components/AuthClient.tsx +++ b/app/(setup)/login/components/main-components/AuthClient.tsx @@ -1,7 +1,7 @@ 'use client'; -import { AuthForm } from '@/app/(setup-layout)/login/components/main-components/AuthForm'; -import ImageSlider from '@/app/(setup-layout)/login/components/main-components/ImageSlider'; +import { AuthForm } from '@/app/(setup)/login/components/main-components/AuthForm'; +import ImageSlider from '@/app/(setup)/login/components/main-components/ImageSlider'; const LoginPage = () => { return ( diff --git a/app/(setup-layout)/login/components/main-components/AuthForm.tsx b/app/(setup)/login/components/main-components/AuthForm.tsx similarity index 94% rename from app/(setup-layout)/login/components/main-components/AuthForm.tsx rename to app/(setup)/login/components/main-components/AuthForm.tsx index 2adfaa7c..40fde5e2 100644 --- a/app/(setup-layout)/login/components/main-components/AuthForm.tsx +++ b/app/(setup)/login/components/main-components/AuthForm.tsx @@ -4,10 +4,10 @@ import Image from 'next/image'; import { useState, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Button } from '@/components/ui/button'; -import { SignInForm } from '@/app/(setup-layout)/login/components/sing-in/SignInForm'; -import { SignUpForm } from '@/app/(setup-layout)/login/components/sing-up/SignUpForm'; -import { ForgotPasswordForm } from '@/app/(setup-layout)/login/components/forgot-password/ForgotPasswordForm'; -import { VerificationForm } from '@/app/(setup-layout)/login/components/verification-form/VerificationForm'; +import { SignInForm } from '@/app/(setup)/login/components/sing-in/SignInForm'; +import { SignUpForm } from '@/app/(setup)/login/components/sing-up/SignUpForm'; +import { ForgotPasswordForm } from '@/app/(setup)/login/components/forgot-password/ForgotPasswordForm'; +import { VerificationForm } from '@/app/(setup)/login/components/verification-form/VerificationForm'; import { signInWithRedirect } from 'aws-amplify/auth'; type AuthState = 'signin' | 'signup' | 'forgot-password' | 'verification'; diff --git a/app/(setup-layout)/login/components/main-components/ImageSlider.tsx b/app/(setup)/login/components/main-components/ImageSlider.tsx similarity index 98% rename from app/(setup-layout)/login/components/main-components/ImageSlider.tsx rename to app/(setup)/login/components/main-components/ImageSlider.tsx index d1cde471..b9eb9abf 100644 --- a/app/(setup-layout)/login/components/main-components/ImageSlider.tsx +++ b/app/(setup)/login/components/main-components/ImageSlider.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState } from 'react'; +import { useState } from 'react'; import Image from 'next/image'; import { Swiper, SwiperSlide } from 'swiper/react'; import { Autoplay, EffectFade } from 'swiper/modules'; diff --git a/app/(setup-layout)/login/components/sing-in/SignInForm.tsx b/app/(setup)/login/components/sing-in/SignInForm.tsx similarity index 98% rename from app/(setup-layout)/login/components/sing-in/SignInForm.tsx rename to app/(setup)/login/components/sing-in/SignInForm.tsx index 73a36cbc..a96044c7 100644 --- a/app/(setup-layout)/login/components/sing-in/SignInForm.tsx +++ b/app/(setup)/login/components/sing-in/SignInForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useSignIn } from '@/app/(setup-layout)/login/hooks/SignIn'; +import { useSignIn } from '@/app/(setup)/login/hooks/SignIn'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; diff --git a/app/(setup-layout)/login/components/sing-up/SignUpForm.tsx b/app/(setup)/login/components/sing-up/SignUpForm.tsx similarity index 98% rename from app/(setup-layout)/login/components/sing-up/SignUpForm.tsx rename to app/(setup)/login/components/sing-up/SignUpForm.tsx index 2ae90dda..ce17aeae 100644 --- a/app/(setup-layout)/login/components/sing-up/SignUpForm.tsx +++ b/app/(setup)/login/components/sing-up/SignUpForm.tsx @@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { Eye, EyeOff } from 'lucide-react'; import { Loader } from '@/components/ui/loader'; -import { handleSignUp } from '@/app/(setup-layout)/login/hooks/signUp'; +import { handleSignUp } from '@/app/(setup)/login/hooks/signUp'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; diff --git a/app/(setup-layout)/login/components/verification-form/VerificationForm.tsx b/app/(setup)/login/components/verification-form/VerificationForm.tsx similarity index 97% rename from app/(setup-layout)/login/components/verification-form/VerificationForm.tsx rename to app/(setup)/login/components/verification-form/VerificationForm.tsx index b9447722..3807fcd4 100644 --- a/app/(setup-layout)/login/components/verification-form/VerificationForm.tsx +++ b/app/(setup)/login/components/verification-form/VerificationForm.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import { Loader } from '@/components/ui/loader'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { handleConfirmSignUp } from '@/app/(setup-layout)/login/hooks/signUp'; +import { handleConfirmSignUp } from '@/app/(setup)/login/hooks/signUp'; import { signIn } from 'aws-amplify/auth'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; diff --git a/app/(setup-layout)/login/hooks/SignIn.ts b/app/(setup)/login/hooks/SignIn.ts similarity index 100% rename from app/(setup-layout)/login/hooks/SignIn.ts rename to app/(setup)/login/hooks/SignIn.ts diff --git a/app/(setup-layout)/login/hooks/signUp.ts b/app/(setup)/login/hooks/signUp.ts similarity index 100% rename from app/(setup-layout)/login/hooks/signUp.ts rename to app/(setup)/login/hooks/signUp.ts diff --git a/app/(setup-layout)/login/hooks/useResetPassword.ts b/app/(setup)/login/hooks/useResetPassword.ts similarity index 100% rename from app/(setup-layout)/login/hooks/useResetPassword.ts rename to app/(setup)/login/hooks/useResetPassword.ts diff --git a/app/(setup-layout)/login/page.tsx b/app/(setup)/login/page.tsx similarity index 100% rename from app/(setup-layout)/login/page.tsx rename to app/(setup)/login/page.tsx diff --git a/app/(setup-layout)/my-store/components/AnimatedBackground.tsx b/app/(setup)/my-store/components/AnimatedBackground.tsx similarity index 100% rename from app/(setup-layout)/my-store/components/AnimatedBackground.tsx rename to app/(setup)/my-store/components/AnimatedBackground.tsx diff --git a/app/(setup-layout)/my-store/components/StoreAvatar.tsx b/app/(setup)/my-store/components/StoreAvatar.tsx similarity index 100% rename from app/(setup-layout)/my-store/components/StoreAvatar.tsx rename to app/(setup)/my-store/components/StoreAvatar.tsx diff --git a/app/(setup-layout)/my-store/components/StoreSelector.tsx b/app/(setup)/my-store/components/StoreSelector.tsx similarity index 97% rename from app/(setup-layout)/my-store/components/StoreSelector.tsx rename to app/(setup)/my-store/components/StoreSelector.tsx index 2442280f..440d0186 100644 --- a/app/(setup-layout)/my-store/components/StoreSelector.tsx +++ b/app/(setup)/my-store/components/StoreSelector.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useUserStores } from '@/app/(setup-layout)/my-store/hooks/useUserStores'; -import { StoreAvatar } from '@/app/(setup-layout)/my-store/components/StoreAvatar'; +import { useUserStores } from '@/app/(setup)/my-store/hooks/useUserStores'; +import { StoreAvatar } from '@/app/(setup)/my-store/components/StoreAvatar'; import { Button } from '@/components/ui/button'; import { Loader } from '@/components/ui/loader'; import { routes } from '@/utils/client/routes'; diff --git a/app/(setup-layout)/my-store/hooks/useUserStores.ts b/app/(setup)/my-store/hooks/useUserStores.ts similarity index 100% rename from app/(setup-layout)/my-store/hooks/useUserStores.ts rename to app/(setup)/my-store/hooks/useUserStores.ts diff --git a/app/(setup-layout)/my-store/page.tsx b/app/(setup)/my-store/page.tsx similarity index 66% rename from app/(setup-layout)/my-store/page.tsx rename to app/(setup)/my-store/page.tsx index c46326b3..e4cf066b 100644 --- a/app/(setup-layout)/my-store/page.tsx +++ b/app/(setup)/my-store/page.tsx @@ -1,5 +1,5 @@ -import { StoreSelector } from '@/app/(setup-layout)/my-store/components/StoreSelector'; -import { AnimatedBackground } from '@/app/(setup-layout)/my-store/components/AnimatedBackground'; +import { StoreSelector } from '@/app/(setup)/my-store/components/StoreSelector'; +import { AnimatedBackground } from '@/app/(setup)/my-store/components/AnimatedBackground'; export const metadata = { title: 'Selecciona tu tienda ', diff --git a/app/(setup-layout)/login/LoginClient.tsx b/app/(setup-layout)/login/LoginClient.tsx deleted file mode 100644 index 72274d2b..00000000 --- a/app/(setup-layout)/login/LoginClient.tsx +++ /dev/null @@ -1,7 +0,0 @@ -'use client'; - -import AuthClient from '@/app/(setup-layout)/login/components/main-components/AuthClient'; - -export default function LoginClient() { - return ; -} diff --git a/app/(main-layout)/landing/components/AboutUs.tsx b/app/(www)/landing/components/AboutUs.tsx similarity index 100% rename from app/(main-layout)/landing/components/AboutUs.tsx rename to app/(www)/landing/components/AboutUs.tsx diff --git a/app/(www)/landing/components/DocsLanding.tsx b/app/(www)/landing/components/DocsLanding.tsx new file mode 100644 index 00000000..dbe31582 --- /dev/null +++ b/app/(www)/landing/components/DocsLanding.tsx @@ -0,0 +1,41 @@ +import { DropshippingIntro } from '@/app/(www)/landing/components/DropshippingIntro'; +import { Platform } from '@/app/(www)/landing/components/Platform'; +import { Footer } from '@/app/(www)/landing/components/Footer'; +import { StepGuide } from '@/app/(www)/landing/components/StepGuide'; +import { FirstView } from '@/app/(www)/landing/components/FirstView'; +import { AboutUs } from '@/app/(www)/landing/components/AboutUs'; +import { FashionSlider } from '@/app/(www)/landing/components/FashionSlider'; +import { Feature } from '@/app/(www)/landing/components/Feature'; +import { Testimonials } from '@/app/(www)/landing/components/Testimonials'; +import { MarqueeLogos } from '@/app/(www)/landing/components/MarqueeLogos'; +import { LogoCarousell } from '@/app/(www)/landing/components/LogoCarousell'; + +export default function LandingPage() { + return ( + <> + + + +
+ +
+ +
+ + + +
+ +
+ +
+
+ +
+ +
+
+
+ + ); +} diff --git a/app/(main-layout)/landing/components/DropshippingIntro.tsx b/app/(www)/landing/components/DropshippingIntro.tsx similarity index 100% rename from app/(main-layout)/landing/components/DropshippingIntro.tsx rename to app/(www)/landing/components/DropshippingIntro.tsx diff --git a/app/(main-layout)/landing/components/FashionSlider.tsx b/app/(www)/landing/components/FashionSlider.tsx similarity index 97% rename from app/(main-layout)/landing/components/FashionSlider.tsx rename to app/(www)/landing/components/FashionSlider.tsx index d9d448ce..2703f69f 100644 --- a/app/(main-layout)/landing/components/FashionSlider.tsx +++ b/app/(www)/landing/components/FashionSlider.tsx @@ -41,7 +41,7 @@ const slides: Slide[] = [ bgColor: '#D7A594', title: 'Pagos seguros y sin complicaciones', description: - 'Fasttify integra Mercado Pago y PayU para que tus clientes compren con confianza y tú recibas tus pagos sin problemas. La seguridad es nuestra prioridad.', + 'Fasttify integra Mercado Pago y Wompi para que tus clientes compren con confianza y tú recibas tus pagos sin problemas. La seguridad es nuestra prioridad.', image: 'https://images.unsplash.com/photo-1738707060473-a23914a02d8a?q=80&w=1974&auto=format&fit=crop', ctaText: 'Ver métodos de pago', ctaLink: '#pagos', diff --git a/app/(main-layout)/landing/components/Feature.tsx b/app/(www)/landing/components/Feature.tsx similarity index 100% rename from app/(main-layout)/landing/components/Feature.tsx rename to app/(www)/landing/components/Feature.tsx diff --git a/app/(main-layout)/landing/components/FirstView.tsx b/app/(www)/landing/components/FirstView.tsx similarity index 100% rename from app/(main-layout)/landing/components/FirstView.tsx rename to app/(www)/landing/components/FirstView.tsx diff --git a/app/(main-layout)/landing/components/Footer.tsx b/app/(www)/landing/components/Footer.tsx similarity index 100% rename from app/(main-layout)/landing/components/Footer.tsx rename to app/(www)/landing/components/Footer.tsx diff --git a/app/(main-layout)/landing/components/LogoCarousell.tsx b/app/(www)/landing/components/LogoCarousell.tsx similarity index 100% rename from app/(main-layout)/landing/components/LogoCarousell.tsx rename to app/(www)/landing/components/LogoCarousell.tsx diff --git a/app/(main-layout)/landing/components/MarqueeLogos.tsx b/app/(www)/landing/components/MarqueeLogos.tsx similarity index 99% rename from app/(main-layout)/landing/components/MarqueeLogos.tsx rename to app/(www)/landing/components/MarqueeLogos.tsx index 41969194..13f986ef 100644 --- a/app/(main-layout)/landing/components/MarqueeLogos.tsx +++ b/app/(www)/landing/components/MarqueeLogos.tsx @@ -1,7 +1,6 @@ import type React from 'react'; import { forwardRef, useRef } from 'react'; import Image from 'next/image'; - import { cn } from '@/lib/utils'; import { AnimatedBeam } from '@/components/ui/animated-beam'; diff --git a/app/(main-layout)/landing/components/NavBar.tsx b/app/(www)/landing/components/NavBar.tsx similarity index 99% rename from app/(main-layout)/landing/components/NavBar.tsx rename to app/(www)/landing/components/NavBar.tsx index 28e6c64e..b82e5104 100644 --- a/app/(main-layout)/landing/components/NavBar.tsx +++ b/app/(www)/landing/components/NavBar.tsx @@ -1,6 +1,6 @@ 'use client'; -import { navItems } from '@/app/(main-layout)/landing/components/navigation'; +import { navItems } from '@/app/(www)/landing/components/navigation'; import { Button } from '@/components/ui/button'; import { NavigationMenu, diff --git a/app/(main-layout)/landing/components/Platform.tsx b/app/(www)/landing/components/Platform.tsx similarity index 100% rename from app/(main-layout)/landing/components/Platform.tsx rename to app/(www)/landing/components/Platform.tsx diff --git a/app/(main-layout)/landing/components/StepGuide.tsx b/app/(www)/landing/components/StepGuide.tsx similarity index 100% rename from app/(main-layout)/landing/components/StepGuide.tsx rename to app/(www)/landing/components/StepGuide.tsx diff --git a/app/(main-layout)/landing/components/Testimonials.tsx b/app/(www)/landing/components/Testimonials.tsx similarity index 100% rename from app/(main-layout)/landing/components/Testimonials.tsx rename to app/(www)/landing/components/Testimonials.tsx diff --git a/app/(main-layout)/landing/components/navigation.ts b/app/(www)/landing/components/navigation.ts similarity index 100% rename from app/(main-layout)/landing/components/navigation.ts rename to app/(www)/landing/components/navigation.ts diff --git a/app/(main-layout)/layout.tsx b/app/(www)/layout.tsx similarity index 81% rename from app/(main-layout)/layout.tsx rename to app/(www)/layout.tsx index cd9ea952..cad83728 100644 --- a/app/(main-layout)/layout.tsx +++ b/app/(www)/layout.tsx @@ -1,6 +1,6 @@ import '@/app/global.css'; import { inter } from '@/lib/fonts'; -import { Navbar } from '@/app/(main-layout)/landing/components/NavBar'; +import { Navbar } from '@/app/(www)/landing/components/NavBar'; export default function WithNavbarLayout({ children }: { children: React.ReactNode }) { return ( diff --git a/app/(www)/page.tsx b/app/(www)/page.tsx new file mode 100644 index 00000000..3ce45ee1 --- /dev/null +++ b/app/(www)/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import DocsLanding from '@/app/(www)/landing/components/DocsLanding'; + +export default function Home() { + return ; +} diff --git a/app/(main-layout)/pricing/components/FAQItem.tsx b/app/(www)/pricing/components/FAQItem.tsx similarity index 100% rename from app/(main-layout)/pricing/components/FAQItem.tsx rename to app/(www)/pricing/components/FAQItem.tsx diff --git a/app/(main-layout)/pricing/components/FAQSection.tsx b/app/(www)/pricing/components/FAQSection.tsx similarity index 100% rename from app/(main-layout)/pricing/components/FAQSection.tsx rename to app/(www)/pricing/components/FAQSection.tsx diff --git a/app/(main-layout)/pricing/components/FeatureComparison.tsx b/app/(www)/pricing/components/FeatureComparison.tsx similarity index 100% rename from app/(main-layout)/pricing/components/FeatureComparison.tsx rename to app/(www)/pricing/components/FeatureComparison.tsx diff --git a/app/(main-layout)/pricing/components/PricingCard.tsx b/app/(www)/pricing/components/PricingCard.tsx similarity index 100% rename from app/(main-layout)/pricing/components/PricingCard.tsx rename to app/(www)/pricing/components/PricingCard.tsx diff --git a/app/(main-layout)/pricing/components/plans.ts b/app/(www)/pricing/components/plans.ts similarity index 100% rename from app/(main-layout)/pricing/components/plans.ts rename to app/(www)/pricing/components/plans.ts diff --git a/app/(main-layout)/pricing/layout.tsx b/app/(www)/pricing/layout.tsx similarity index 100% rename from app/(main-layout)/pricing/layout.tsx rename to app/(www)/pricing/layout.tsx diff --git a/app/(main-layout)/pricing/page.tsx b/app/(www)/pricing/page.tsx similarity index 71% rename from app/(main-layout)/pricing/page.tsx rename to app/(www)/pricing/page.tsx index 3a7d97b0..a62a1e9c 100644 --- a/app/(main-layout)/pricing/page.tsx +++ b/app/(www)/pricing/page.tsx @@ -1,12 +1,12 @@ 'use client'; import outputs from '@/amplify_outputs.json'; -import { Footer } from '@/app/(main-layout)/landing/components/Footer'; -import { faqItems } from '@/app/(main-layout)/pricing/components/FAQItem'; -import { FAQSection } from '@/app/(main-layout)/pricing/components/FAQSection'; -import { FeatureComparison } from '@/app/(main-layout)/pricing/components/FeatureComparison'; -import { plans } from '@/app/(main-layout)/pricing/components/plans'; -import { PricingCard } from '@/app/(main-layout)/pricing/components/PricingCard'; +import { Footer } from '@/app/(www)/landing/components/Footer'; +import { faqItems } from '@/app/(www)/pricing/components/FAQItem'; +import { FAQSection } from '@/app/(www)/pricing/components/FAQSection'; +import { FeatureComparison } from '@/app/(www)/pricing/components/FeatureComparison'; +import { plans } from '@/app/(www)/pricing/components/plans'; +import { PricingCard } from '@/app/(www)/pricing/components/PricingCard'; import { Amplify } from 'aws-amplify'; import { motion } from 'framer-motion'; diff --git a/app/(main-layout)/terms/components/LegalDocuments.tsx b/app/(www)/terms/components/LegalDocuments.tsx similarity index 97% rename from app/(main-layout)/terms/components/LegalDocuments.tsx rename to app/(www)/terms/components/LegalDocuments.tsx index ce97bfb6..a51bc1a6 100644 --- a/app/(main-layout)/terms/components/LegalDocuments.tsx +++ b/app/(www)/terms/components/LegalDocuments.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; -import legalContent from '@/app/(main-layout)/terms/components/legal-content.json'; +import legalContent from '@/app/(www)/terms/components/legal-content.json'; export function LegalDocuments() { const [activeDocument, setActiveDocument] = useState<'terms' | 'privacy'>('terms'); diff --git a/app/(main-layout)/terms/components/legal-content.json b/app/(www)/terms/components/legal-content.json similarity index 100% rename from app/(main-layout)/terms/components/legal-content.json rename to app/(www)/terms/components/legal-content.json diff --git a/app/(main-layout)/terms/page.tsx b/app/(www)/terms/page.tsx similarity index 80% rename from app/(main-layout)/terms/page.tsx rename to app/(www)/terms/page.tsx index 59ba3b22..ecba189f 100644 --- a/app/(main-layout)/terms/page.tsx +++ b/app/(www)/terms/page.tsx @@ -1,4 +1,4 @@ -import { LegalDocuments } from '@/app/(main-layout)/terms/components/LegalDocuments'; +import { LegalDocuments } from '@/app/(www)/terms/components/LegalDocuments'; import type { Metadata } from 'next'; export const metadata: Metadata = { diff --git a/app/store/[slug]/products/page.tsx b/app/store/[slug]/products/page.tsx index 364db52b..90379a9c 100644 --- a/app/store/[slug]/products/page.tsx +++ b/app/store/[slug]/products/page.tsx @@ -1,3 +1,4 @@ +// Test comment for cache incremental verification import ProductsClient from './ProductsClient'; export async function generateMetadata({ params: _params }: { params: Promise<{ slug: string }> }) { diff --git a/app/store/components/ai-chat/components/AiInput.tsx b/app/store/components/ai-chat/components/AiInput.tsx index 021d2512..7d1e611d 100644 --- a/app/store/components/ai-chat/components/AiInput.tsx +++ b/app/store/components/ai-chat/components/AiInput.tsx @@ -5,7 +5,7 @@ import { Textarea } from '@/components/ui/textarea'; import { cn } from '@/lib/utils'; import { useAutoResizeTextarea } from '@/hooks/ui/use-auto-resize-textare'; import { Button } from '@shopify/polaris'; -import { SendIcon } from '@shopify/polaris-icons'; +import { SendIcon, StopCircleIcon } from '@shopify/polaris-icons'; interface AIInputWithSearchProps { id?: string; @@ -16,6 +16,7 @@ interface AIInputWithSearchProps { onFileSelect?: (file: File) => void; className?: string; loading?: boolean; + onStop?: () => void; } export function AIInputWithSearch({ @@ -27,6 +28,7 @@ export function AIInputWithSearch({ onFileSelect: _onFileSelect, className, loading = false, + onStop, }: AIInputWithSearchProps) { const [value, setValue] = useState(''); const { textareaRef, adjustHeight } = useAutoResizeTextarea({ @@ -69,15 +71,23 @@ export function AIInputWithSearch({
-
diff --git a/app/store/components/ai-chat/components/ChatHeader.tsx b/app/store/components/ai-chat/components/ChatHeader.tsx index 45d99bf5..987043be 100644 --- a/app/store/components/ai-chat/components/ChatHeader.tsx +++ b/app/store/components/ai-chat/components/ChatHeader.tsx @@ -154,6 +154,7 @@ export function ChatHeader({ allowMultiple={false} willLoadMoreResults={hasMore} onLoadMoreResults={handleLoadMoreResults} + listTitle="Conversaciones" /> diff --git a/app/store/components/ai-chat/components/EmptyState.tsx b/app/store/components/ai-chat/components/EmptyState.tsx index d3426d7f..6f3550d7 100644 --- a/app/store/components/ai-chat/components/EmptyState.tsx +++ b/app/store/components/ai-chat/components/EmptyState.tsx @@ -1,6 +1,6 @@ import { EmptyStateProps } from '@/app/store/components/ai-chat/types/chat-types'; import { SUGGESTIONS } from '@/app/store/components/ai-chat/constants/chat-constants'; -import { Text, BlockStack, Card, Icon } from '@shopify/polaris'; +import { Text, BlockStack, Icon } from '@shopify/polaris'; import { InfoIcon, StoreIcon, CircleUpIcon } from '@shopify/polaris-icons'; import Orb from '@/app/store/components/ai-chat/components/Orb'; @@ -8,7 +8,7 @@ export function EmptyState({ onSuggestionClick }: EmptyStateProps) { const suggestionIcons = [InfoIcon, StoreIcon, CircleUpIcon]; return ( - +
@@ -19,7 +19,7 @@ export function EmptyState({ onSuggestionClick }: EmptyStateProps) { Asistente de eCommerce - ¿Qué te gustaría saber sobre ecommerce o dropshipping? + ¿En qué puedo ayudarte con tu tienda online?
@@ -31,19 +31,19 @@ export function EmptyState({ onSuggestionClick }: EmptyStateProps) {
{SUGGESTIONS.map((suggestion, index) => ( -
onSuggestionClick(suggestion.text)} - className="bg-gray-50 rounded-lg shadow-sm hover:shadow-md transition-all duration-300 ease-in-out transform hover:-translate-y-1 p-3 flex items-center justify-between cursor-pointer"> -
- - {suggestion.text} + className="w-full bg-gray-50 hover:bg-gray-100 rounded-lg px-4 py-3 text-left transition-all duration-200 ease-out border border-gray-200 hover:border-gray-300 text-gray-700 hover:text-gray-900"> +
+ {suggestion.text} +
-
+ ))}
-
+ ); } diff --git a/app/store/components/ai-chat/components/MessageList.tsx b/app/store/components/ai-chat/components/MessageList.tsx index 151eb616..41910aaf 100644 --- a/app/store/components/ai-chat/components/MessageList.tsx +++ b/app/store/components/ai-chat/components/MessageList.tsx @@ -12,7 +12,7 @@ export function MessageList({ onLoadMore, }: MessageListProps) { return ( -
+
{/* Botón de cargar más mensajes al inicio */} {hasMoreMessages && onLoadMore && (
diff --git a/app/store/components/ai-chat/components/MessageLoading.tsx b/app/store/components/ai-chat/components/MessageLoading.tsx index 4c01c89c..c11f2f23 100644 --- a/app/store/components/ai-chat/components/MessageLoading.tsx +++ b/app/store/components/ai-chat/components/MessageLoading.tsx @@ -1,37 +1,61 @@ // @hidden function MessageLoading() { return ( - - + + + - + + - + + diff --git a/app/store/components/ai-chat/components/RefinedAiAssistant.tsx b/app/store/components/ai-chat/components/RefinedAiAssistant.tsx index 793ffc95..2632b753 100644 --- a/app/store/components/ai-chat/components/RefinedAiAssistant.tsx +++ b/app/store/components/ai-chat/components/RefinedAiAssistant.tsx @@ -21,6 +21,7 @@ export function RefinedAIAssistantSheet({ open, onOpenChange }: RefinedAIAssista loadingMoreMessages, conversationName, sendMessage, + stopStreaming, loadConversationById, clearMessages, clearError, @@ -78,6 +79,10 @@ export function RefinedAIAssistantSheet({ open, onOpenChange }: RefinedAIAssista clearMessages(); }, [clearMessages]); + const handleStop = useCallback(() => { + stopStreaming(); + }, [stopStreaming]); + const _handleClearChat = useCallback(() => { clearMessages(); }, [clearMessages]); @@ -96,7 +101,7 @@ export function RefinedAIAssistantSheet({ open, onOpenChange }: RefinedAIAssista return (
{/* Loading Overlay */} @@ -187,6 +192,7 @@ export function RefinedAIAssistantSheet({ open, onOpenChange }: RefinedAIAssista onSubmit={handleSubmit} className="py-2" loading={loading} + onStop={handleStop} />
diff --git a/app/store/components/ai-chat/components/TypingMessage.tsx b/app/store/components/ai-chat/components/TypingMessage.tsx index ab116efb..bcfe6ffd 100644 --- a/app/store/components/ai-chat/components/TypingMessage.tsx +++ b/app/store/components/ai-chat/components/TypingMessage.tsx @@ -1,6 +1,10 @@ import { Text, Icon } from '@shopify/polaris'; import { ChatIcon } from '@shopify/polaris-icons'; import { cn } from '@/lib/utils'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import rehypeHighlight from 'rehype-highlight'; +import '@/app/store/components/ai-chat/styles/chat-markdown.css'; interface TypingMessageProps { content: string; @@ -12,49 +16,123 @@ export function TypingMessage({ content, type, id }: TypingMessageProps) { const messageStyles = { user: { container: 'justify-end', - background: 'bg-gradient-to-br from-[#2a2a2a] to-[#3a3a3a]', + background: 'bg-gradient-to-br from-[#2a2a2a] to-[#2f2f2f]', textColor: 'text-white', borderRadius: 'rounded-2xl rounded-br-none', width: 'max-w-[95%]', icon: null, + shadow: 'shadow-lg shadow-black/10', }, ai: { container: 'justify-start', - background: 'bg-gradient-to-br from-blue-50 to-blue-100', - textColor: 'text-gray-800', - borderRadius: 'rounded-2xl rounded-bl-none', - width: 'max-w-[95%]', + background: 'bg-transparent', + textColor: 'text-gray-900', + borderRadius: 'rounded-none', + width: 'max-w-full', icon: ChatIcon, + shadow: 'shadow-none', }, }; const currentStyle = messageStyles[type]; return ( -
+
{currentStyle.icon && ( -
+
)}
- - {content} - + {type === 'ai' ? ( + ( + + {children} + + ), + a: ({ href, children }) => ( + + {children} + + ), + h1: ({ children }) => ( + + {children} + + ), + h2: ({ children }) => ( + + {children} + + ), + h3: ({ children }) => ( + + {children} + + ), + h4: ({ children }) => ( + + {children} + + ), + ul: ({ children }) =>
    {children}
, + ol: ({ children }) =>
    {children}
, + li: ({ children }) =>
  • {children}
  • , + strong: ({ children }) => {children}, + em: ({ children }) => {children}, + code: ({ children }) => ( + {children} + ), + pre: ({ children }) => ( +
    {children}
    + ), + blockquote: ({ children }) => ( +
    + {children} +
    + ), + }}> + {content} +
    + ) : ( +
    + + {content} + +
    + )}
    diff --git a/app/store/components/ai-chat/constants/chat-constants.ts b/app/store/components/ai-chat/constants/chat-constants.ts index a0eeaa19..0c3984e2 100644 --- a/app/store/components/ai-chat/constants/chat-constants.ts +++ b/app/store/components/ai-chat/constants/chat-constants.ts @@ -3,7 +3,8 @@ import { Suggestion } from '../types/chat-types'; export const LONG_MESSAGE_THRESHOLD = 280; export const SUGGESTIONS: Suggestion[] = [ - { id: '1', text: 'Generar resumen para estrategias de ecommerce' }, - { id: '3', text: '¿Encaja en mi tienda de dropshipping?' }, - { id: '2', text: '¿Cuál es su estilo de capacitación en ecommerce?' }, + { id: '1', text: '¿Cómo aumentar las ventas de mi tienda online?' }, + { id: '2', text: 'Estrategias de marketing digital para ecommerce' }, + { id: '3', text: '¿Cómo optimizar la conversión de mi sitio web?' }, + { id: '4', text: 'Mejores prácticas para SEO en ecommerce' }, ]; diff --git a/app/store/components/ai-chat/hooks/useConversation.ts b/app/store/components/ai-chat/hooks/useConversation.ts index ca5a8c86..d1ea418a 100644 --- a/app/store/components/ai-chat/hooks/useConversation.ts +++ b/app/store/components/ai-chat/hooks/useConversation.ts @@ -48,6 +48,7 @@ export interface UseConversationReturn { initializeConversation: () => Promise; loadConversationById: (id: string) => Promise; sendMessage: (message: string) => Promise; + stopStreaming: () => void; resetConversation: () => void; clearError: () => void; listMessages: () => Promise; @@ -75,6 +76,7 @@ export function useConversation(): UseConversationReturn { const conversationRef = useRef(conversation); const isInitializingRef = useRef(isInitializing); const hasUpdatedNameRef = useRef(false); + const currentSubscriptionRef = useRef(null); // Usar el contexto para persistir el ID de conversación const { conversationId, setConversationId, clearConversation: clearConversationContext } = useConversationContext(); @@ -190,6 +192,33 @@ export function useConversation(): UseConversationReturn { setError(null); }, []); + /** + * Detener el streaming actual + */ + const stopStreaming = useCallback(() => { + if (currentSubscriptionRef.current) { + currentSubscriptionRef.current.unsubscribe(); + currentSubscriptionRef.current = null; + } + + // Si hay texto en streaming, agregarlo como mensaje final + if (streamingText) { + setMessages((prev) => { + const assistantMsg: Message = { + id: `assistant-${Date.now()}`, + content: streamingText, + role: 'assistant', + createdAt: new Date().toISOString(), + }; + return [...prev, assistantMsg]; + }); + } + + // Limpiar estado de streaming + setStreamingText(''); + setIsLoading(false); + }, [streamingText]); + /** * Cargar una conversación existente por ID */ @@ -232,6 +261,12 @@ export function useConversation(): UseConversationReturn { * Reiniciar conversación */ const resetConversation = useCallback(() => { + // Detener streaming si está activo + if (currentSubscriptionRef.current) { + currentSubscriptionRef.current.unsubscribe(); + currentSubscriptionRef.current = null; + } + setConversation(null); setMessages([]); setError(null); @@ -320,6 +355,7 @@ export function useConversation(): UseConversationReturn { setIsLoading(false); setStreamingText(''); subscription.unsubscribe(); + currentSubscriptionRef.current = null; return; } @@ -350,6 +386,7 @@ export function useConversation(): UseConversationReturn { // Desuscribirse subscription.unsubscribe(); + currentSubscriptionRef.current = null; } }, error: (error: any) => { @@ -357,8 +394,12 @@ export function useConversation(): UseConversationReturn { setError(`Error en streaming: ${error.message || 'Error desconocido'}`); setIsLoading(false); setStreamingText(''); + currentSubscriptionRef.current = null; }, }); + + // Guardar referencia de la suscripción para poder cancelarla + currentSubscriptionRef.current = subscription; } catch (error: any) { console.error('Error sending message:', error); setError(`Error al enviar mensaje: ${error.message || 'Error desconocido'}`); @@ -445,6 +486,7 @@ export function useConversation(): UseConversationReturn { initializeConversation, loadConversationById, sendMessage, + stopStreaming, resetConversation, clearError, listMessages, diff --git a/app/store/components/ai-chat/hooks/useSimpleChat.ts b/app/store/components/ai-chat/hooks/useSimpleChat.ts index 82b55d5d..39eddfb2 100644 --- a/app/store/components/ai-chat/hooks/useSimpleChat.ts +++ b/app/store/components/ai-chat/hooks/useSimpleChat.ts @@ -21,6 +21,7 @@ export interface UseSimpleChatReturn { // Acciones simplificadas sendMessage: (message: string) => Promise; + stopStreaming: () => void; loadConversationById: (id: string) => Promise; clearMessages: () => void; clearError: () => void; @@ -48,6 +49,7 @@ export function useSimpleChat(): UseSimpleChatReturn { initializeConversation, loadConversationById, sendMessage: sendMessageInternal, + stopStreaming: stopStreamingInternal, resetConversation, clearError: clearErrorInternal, loadMoreMessages: loadMoreMessagesInternal, @@ -87,6 +89,13 @@ export function useSimpleChat(): UseSimpleChatReturn { resetConversation(); }, [resetConversation]); + /** + * Detiene el streaming actual + */ + const stopStreaming = useCallback((): void => { + stopStreamingInternal(); + }, [stopStreamingInternal]); + /** * Limpia el error actual */ @@ -133,6 +142,7 @@ export function useSimpleChat(): UseSimpleChatReturn { loadingMoreMessages, conversationName, sendMessage, + stopStreaming, loadConversationById, clearMessages, clearError, diff --git a/app/store/components/ai-chat/styles/chat-markdown.css b/app/store/components/ai-chat/styles/chat-markdown.css new file mode 100644 index 00000000..0e7b0e7e --- /dev/null +++ b/app/store/components/ai-chat/styles/chat-markdown.css @@ -0,0 +1,209 @@ +@keyframes messageSlideIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.message-bubble { + animation: messageSlideIn 0.5s ease-out; +} + +.ai-icon { + filter: drop-shadow(0 2px 4px rgba(59, 130, 246, 0.15)); + transition: filter 0.3s ease; +} + +.ai-icon:hover { + filter: drop-shadow(0 4px 8px rgba(59, 130, 246, 0.25)); +} + +.markdown-content { + line-height: 1.7; + font-size: 14px; + word-spacing: 0.05em; + letter-spacing: 0.01em; +} + +.markdown-content h1, +.markdown-content h2, +.markdown-content h3, +.markdown-content h4, +.markdown-content h5, +.markdown-content h6 { + margin: 1rem 0 0.75rem 0; + color: inherit; + font-weight: inherit; + font-size: 14px; +} + +.markdown-content h1:first-child, +.markdown-content h2:first-child, +.markdown-content h3:first-child, +.markdown-content h4:first-child, +.markdown-content h5:first-child, +.markdown-content h6:first-child { + margin-top: 0; +} + +.markdown-content p { + margin: 0.75rem 0; + color: inherit; + font-size: 14px; +} + +.markdown-content p:first-child { + margin-top: 0; +} + +.markdown-content p:last-child { + margin-bottom: 0; +} + +.markdown-content ul, +.markdown-content ol { + margin: 1rem 0; + padding-left: 1.5rem; +} + +.markdown-content ol li::marker { + font-size: 0.75rem; +} + +.markdown-content ul:first-child, +.markdown-content ol:first-child { + margin-top: 0; +} + +.markdown-content ul:last-child, +.markdown-content ol:last-child { + margin-bottom: 0; +} + +.markdown-content li { + margin: 0.5rem 0; + color: inherit; + font-size: 14px; +} + +.markdown-content li p { + margin: 0.25rem 0; +} + +.markdown-content a { + color: #2563eb; + text-decoration: underline; + transition: color 0.2s ease-in-out; +} + +.markdown-content a:hover { + color: #1d4ed8; + text-decoration: underline; +} + +.markdown-content a:focus { + outline: 2px solid #3b82f6; + outline-offset: 2px; + border-radius: 2px; +} + +.markdown-content strong { + font-weight: 600; + color: #1f2937; +} + +.markdown-content em { + font-style: italic; + color: #4b5563; +} + +.markdown-content code { + background-color: #f3f4f6; + color: #e11d48; + padding: 0.125rem 0.25rem; + border-radius: 0.25rem; + font-size: 0.875rem; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-weight: 500; +} + +.markdown-content pre { + background-color: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + padding: 1rem; + margin: 0.75rem 0; + overflow-x: auto; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.875rem; + line-height: 1.5; +} + +.markdown-content pre code { + background-color: transparent; + color: inherit; + padding: 0; + border-radius: 0; + font-size: inherit; +} + +.markdown-content blockquote { + border-left: 4px solid #3b82f6; + background-color: #eff6ff; + margin: 1rem 0; + padding: 0.75rem 1rem; + border-radius: 0 0.375rem 0.375rem 0; + color: #1e40af; + font-style: italic; +} + +.markdown-content blockquote p { + margin: 0; + color: inherit; +} + +.markdown-content a { + color: #3b82f6; + text-decoration: underline; + text-decoration-color: #93c5fd; + text-underline-offset: 2px; +} + +.markdown-content a:hover { + color: #1d4ed8; + text-decoration-color: #3b82f6; +} + +.markdown-content table { + width: 100%; + border-collapse: collapse; + margin: 1rem 0; + font-size: 0.875rem; +} + +.markdown-content th, +.markdown-content td { + border: 1px solid #e5e7eb; + padding: 0.5rem 0.75rem; + text-align: left; +} + +.markdown-content th { + background-color: #f9fafb; + font-weight: 600; + color: #374151; +} + +.markdown-content tr:nth-child(even) { + background-color: #f9fafb; +} + +.markdown-content hr { + border: none; + border-top: 1px solid #e5e7eb; + margin: 1.5rem 0; +} diff --git a/app/store/components/domains/components/ChangeDomainDialog.tsx b/app/store/components/domains/components/ChangeDomainDialog.tsx index bbf1f091..2859f7ec 100644 --- a/app/store/components/domains/components/ChangeDomainDialog.tsx +++ b/app/store/components/domains/components/ChangeDomainDialog.tsx @@ -1,4 +1,4 @@ -import { useUserStoreData } from '@/app/(setup-layout)/first-steps/hooks/useUserStoreData'; +import { useUserStoreData } from '@/app/(setup)/first-steps/hooks/useUserStoreData'; import { useToast } from '@/app/store/context/ToastContext'; import { useDomainValidator } from '@/app/store/hooks/api/useDomainValidator'; import { BlockStack, InlineStack, Modal, Spinner, Text, TextField } from '@shopify/polaris'; diff --git a/app/store/components/domains/components/EditStoreProfileDialog.tsx b/app/store/components/domains/components/EditStoreProfileDialog.tsx index a53ce09c..3b6f9590 100644 --- a/app/store/components/domains/components/EditStoreProfileDialog.tsx +++ b/app/store/components/domains/components/EditStoreProfileDialog.tsx @@ -1,4 +1,4 @@ -import { useUserStoreData } from '@/app/(setup-layout)/first-steps/hooks/useUserStoreData'; +import { useUserStoreData } from '@/app/(setup)/first-steps/hooks/useUserStoreData'; import { CurrencySelector } from '@/app/store/components/domains/components/CurrencySelector'; import { useCurrencyConfig } from '@/app/store/hooks/currency/useCurrencyConfig'; import { HelpTooltip } from '@/app/store/components/navigation-management/components/HelpTooltip'; diff --git a/app/store/components/images-selector/components/FilterCombobox.tsx b/app/store/components/images-selector/components/FilterCombobox.tsx new file mode 100644 index 00000000..8fc5cfc9 --- /dev/null +++ b/app/store/components/images-selector/components/FilterCombobox.tsx @@ -0,0 +1,118 @@ +import { useState, useCallback } from 'react'; +import { Combobox, Listbox, Icon } from '@shopify/polaris'; +import { ChevronDownIcon } from '@shopify/polaris-icons'; + +interface FilterOption { + label: string; + value: string; + checked?: boolean; +} + +interface FilterComboboxProps { + label: string; + options: FilterOption[]; + onSelectionChange: (selectedValues: string[]) => void; + type?: 'checkbox' | 'radio'; + selectedValue?: string; +} + +export default function FilterCombobox({ + label, + options, + onSelectionChange, + type = 'checkbox', + selectedValue, +}: FilterComboboxProps) { + const [selectedValues, setSelectedValues] = useState(() => { + if (type === 'radio') { + return selectedValue ? [selectedValue] : []; + } + return options.filter((option) => option.checked).map((option) => option.value); + }); + + const handleSelectionChange = useCallback( + (selectedValues: string[]) => { + setSelectedValues(selectedValues); + onSelectionChange(selectedValues); + }, + [onSelectionChange] + ); + + const handleClear = useCallback(() => { + setSelectedValues([]); + onSelectionChange([]); + }, [onSelectionChange]); + + const getButtonText = () => { + if (selectedValues.length === 0) { + return label; + } + if (selectedValues.length === 1) { + const option = options.find((opt) => opt.value === selectedValues[0]); + return option?.label || label; + } + return `${label} (${selectedValues.length})`; + }; + + const activator = ( + {}} + placeholder={label} + suffix={} + autoComplete="off" + readOnly + /> + ); + + const optionsMarkup = options.map((option) => { + const { label: optionLabel, value } = option; + const isSelected = selectedValues.includes(value); + + return ( + + {optionLabel} + + ); + }); + + // Agregar opción de limpiar si hay selecciones + const allOptionsMarkup = + selectedValues.length > 0 + ? [ + ...optionsMarkup, + + Borrar + , + ] + : optionsMarkup; + + const handleListboxSelect = useCallback( + (value: string) => { + if (value === 'clear') { + handleClear(); + return; + } + + if (type === 'radio') { + handleSelectionChange([value]); + } else { + const newSelection = selectedValues.includes(value) + ? selectedValues.filter((val) => val !== value) + : [...selectedValues, value]; + handleSelectionChange(newSelection); + } + }, + [selectedValues, type, handleSelectionChange, handleClear] + ); + + return ( +
    + + {allOptionsMarkup} + +
    + ); +} diff --git a/app/store/components/images-selector/components/FilterPopover.tsx b/app/store/components/images-selector/components/FilterPopover.tsx new file mode 100644 index 00000000..120c39d8 --- /dev/null +++ b/app/store/components/images-selector/components/FilterPopover.tsx @@ -0,0 +1,130 @@ +import { useState, useCallback } from 'react'; +import { Popover, ActionList } from '@shopify/polaris'; +import { ChevronDownIcon } from '@shopify/polaris-icons'; + +interface FilterOption { + label: string; + value: string; + checked?: boolean; +} + +interface FilterPopoverProps { + label: string; + options: FilterOption[]; + onSelectionChange: (selectedValues: string[]) => void; + type?: 'checkbox' | 'radio'; + selectedValue?: string; // Para radio buttons +} + +export default function FilterPopover({ + label, + options, + onSelectionChange, + type = 'checkbox', + selectedValue, +}: FilterPopoverProps) { + const [popoverActive, setPopoverActive] = useState(false); + const [selectedValues, setSelectedValues] = useState(() => { + if (type === 'radio') { + return selectedValue ? [selectedValue] : []; + } + return options.filter((option) => option.checked).map((option) => option.value); + }); + + const togglePopoverActive = useCallback(() => { + setPopoverActive((active) => !active); + }, []); + + const handleSelectionChange = useCallback( + (selectedValues: string[]) => { + setSelectedValues(selectedValues); + onSelectionChange(selectedValues); + }, + [onSelectionChange] + ); + + const handleClear = useCallback(() => { + setSelectedValues([]); + onSelectionChange([]); + }, [onSelectionChange]); + + const getButtonText = () => { + if (selectedValues.length === 0) { + return label; + } + if (selectedValues.length === 1) { + const option = options.find((opt) => opt.value === selectedValues[0]); + return option?.label || label; + } + return `${label} (${selectedValues.length})`; + }; + + const activator = ( + + ); + + const actionListItems = options.map((option) => ({ + content: option.label, + value: option.value, + onAction: () => { + if (type === 'radio') { + handleSelectionChange([option.value]); + } else { + const newSelection = selectedValues.includes(option.value) + ? selectedValues.filter((val) => val !== option.value) + : [...selectedValues, option.value]; + handleSelectionChange(newSelection); + } + }, + })); + + // Agregar opción de limpiar si hay selecciones + const actionListItemsWithClear = + selectedValues.length > 0 + ? [ + ...actionListItems, + { + content: 'Borrar', + onAction: handleClear, + destructive: true, + }, + ] + : actionListItems; + + return ( + setPopoverActive(false)} + preferredPosition="below"> + + + ); +} diff --git a/app/store/components/images-selector/components/ImageCard.tsx b/app/store/components/images-selector/components/ImageCard.tsx new file mode 100644 index 00000000..be1cb4c3 --- /dev/null +++ b/app/store/components/images-selector/components/ImageCard.tsx @@ -0,0 +1,216 @@ +import { useCallback, memo, useState } from 'react'; +import type { S3Image } from '@/app/store/hooks/storage/useS3Images'; +import { Checkbox } from '@shopify/polaris'; +import { DeleteIcon } from '@shopify/polaris-icons'; +import Image from 'next/image'; + +const CARD_BG_COLOR = '#FFFFFF'; +const HOVER_OVERLAY_COLOR = 'rgba(0, 0, 0, 0.05)'; +const IMAGE_ASPECT_RATIO = '1/1'; +const CARD_PADDING = '8px'; +const CARD_BORDER_RADIUS = '6px'; + +const CARD_STYLES = { + container: { + backgroundColor: CARD_BG_COLOR, + padding: CARD_PADDING, + borderRadius: CARD_BORDER_RADIUS, + position: 'relative', + boxShadow: 'none', + }, + imageContainer: { + position: 'relative', + aspectRatio: IMAGE_ASPECT_RATIO, + borderRadius: '4px', + overflow: 'hidden', + backgroundColor: 'white', + minHeight: '120px', + }, + hoverOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: HOVER_OVERLAY_COLOR, + opacity: 0, + zIndex: 1, + borderRadius: CARD_BORDER_RADIUS, + }, + checkboxContainer: { + position: 'absolute', + top: '8px', + left: '8px', + zIndex: 2, + backgroundColor: 'rgba(255, 255, 255, 0.9)', + borderRadius: '4px', + padding: '4px', + }, + deleteButtonContainer: { + position: 'absolute', + top: '8px', + right: '8px', + zIndex: 3, + }, + fileInfoContainer: { + paddingTop: '6px', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + minHeight: '28px', + }, +} as const; + +interface ImageCardProps { + image: S3Image; + isSelected: boolean; + isMarkedForDeletion: boolean; + deleteMode: boolean; + onImageClick: () => void; + onDeleteImage: (key: string) => Promise; +} + +// Funciones de utilidad +const getFileNameWithoutExtension = (filename: string): string => { + return filename.split('.').slice(0, -1).join('.'); +}; + +const getFileFormat = (filename: string): string => { + const ext = filename.split('.').pop(); + return ext ? ext.toUpperCase() : ''; +}; + +const ImageCard = memo(function ImageCard({ + image, + isSelected, + isMarkedForDeletion, + deleteMode, + onImageClick, + onDeleteImage, +}: ImageCardProps) { + const [isHovered, setIsHovered] = useState(false); + + const handleMouseEnter = useCallback(() => { + if (!isSelected) { + setIsHovered(true); + } + }, [isSelected]); + + const handleMouseLeave = useCallback(() => { + setIsHovered(false); + }, []); + + const handleDeleteClick = useCallback(() => { + onDeleteImage(image.key); + }, [image.key, onDeleteImage]); + + const containerStyles = { + ...CARD_STYLES.container, + opacity: deleteMode && isMarkedForDeletion ? 0.7 : 1, + }; + + const hoverOverlayStyles = { + ...CARD_STYLES.hoverOverlay, + opacity: (isHovered && !isSelected) || isSelected ? 1 : 0, + }; + + const checkboxStyles = { + ...CARD_STYLES.checkboxContainer, + backgroundColor: deleteMode + ? isMarkedForDeletion + ? '#d72c0d' + : 'rgba(255,255,255,0.9)' + : isSelected + ? 'transparent' + : 'rgba(255, 255, 255, 0.9)', + }; + + const fileName = getFileNameWithoutExtension(image.filename); + const fileFormat = getFileFormat(image.filename); + + return ( +
    + {/* Overlay de hover sobre toda la tarjeta (siempre visible cuando está seleccionado, o en hover cuando no está seleccionado) */} +
    + + {/* Contenedor de imagen */} +
    + {/* Checkbox (estado seleccionado o modo eliminación) */} + {(isSelected || deleteMode) && ( +
    + {}} // El onChange se maneja en el click del contenedor + /> +
    + )} + + {/* Botón de eliminar (solo cuando está seleccionado y no en modo eliminación) */} + {isSelected && !deleteMode && ( +
    +
    { + e.stopPropagation(); + handleDeleteClick(); + }} + style={{ + backgroundColor: 'rgba(255, 255, 255, 0.9)', + borderRadius: '4px', + padding: '4px', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }}> + +
    +
    + )} + + {image.filename} +
    + + {/* Información del archivo */} +
    +
    + {fileName} +
    +
    + {fileFormat} +
    +
    +
    + ); +}); + +export default ImageCard; diff --git a/app/store/components/images-selector/components/ImageGallery.tsx b/app/store/components/images-selector/components/ImageGallery.tsx index ffade90a..796a8b0c 100644 --- a/app/store/components/images-selector/components/ImageGallery.tsx +++ b/app/store/components/images-selector/components/ImageGallery.tsx @@ -1,19 +1,9 @@ import { useToast } from '@/app/store/context/ToastContext'; import { S3Image } from '@/app/store/hooks/storage/useS3Images'; -import { - Box, - Button, - ButtonGroup, - Card, - EmptyState, - Grid, - InlineStack, - Spinner, - Text, - Thumbnail, -} from '@shopify/polaris'; -import { DeleteIcon, SelectIcon } from '@shopify/polaris-icons'; +import { Box, Button, ButtonGroup, EmptyState, Grid, InlineStack, Spinner, Text } from '@shopify/polaris'; +import { DeleteIcon } from '@shopify/polaris-icons'; import { useCallback, useState, memo } from 'react'; +import ImageCard from './ImageCard'; interface ImageGalleryProps { images: S3Image[]; @@ -183,74 +173,30 @@ const ImageGallery = memo(function ImageGallery({ selectAllForDeletion, ]); - // Memoizar renderizado de imágenes individuales + // Memoizar renderizado de imágenes individuales usando ImageCard const renderImage = useCallback( - (image: S3Image) => ( - -
    { - if (deleteMode) { - toggleDeleteSelection(image.key); - } else { - onImageSelect(image); - } - }} - style={{ - cursor: 'pointer', - outline: deleteMode - ? isMarkedForDeletion(image.key) - ? '2px solid #d72c0d' - : '1px solid #e1e3e5' - : isSelected(image) - ? '2px solid #2962ff' - : 'none', - outlineOffset: '2px', - borderRadius: 'var(--p-border-radius-200)', - opacity: deleteMode && isMarkedForDeletion(image.key) ? 0.7 : 1, - }}> - - - {deleteMode && ( -
    - -
    - )} - - {!deleteMode && isSelected(image) && ( -
    e.stopPropagation()}> -
    - )} + (image: S3Image) => { + const COLUMN_SPAN_CONFIG = { xs: 3, sm: 3, md: 2, lg: 2, xl: 2 } as const; - -
    - - - {image.filename} - - -
    -
    -
    - ), + return ( + + { + if (deleteMode) { + toggleDeleteSelection(image.key); + } else { + onImageSelect(image); + } + }} + onDeleteImage={onDeleteImage} + /> + + ); + }, [deleteMode, isMarkedForDeletion, isSelected, onImageSelect, onDeleteImage, toggleDeleteSelection] ); diff --git a/app/store/components/images-selector/components/ImageSelectorModal.tsx b/app/store/components/images-selector/components/ImageSelectorModal.tsx index 41cdb776..da6e514e 100644 --- a/app/store/components/images-selector/components/ImageSelectorModal.tsx +++ b/app/store/components/images-selector/components/ImageSelectorModal.tsx @@ -9,9 +9,11 @@ const ImageGallery = lazy(() => import('./ImageGallery')); const SearchAndFilters = lazy(() => import('./SearchAndFilters')); const UploadDropZone = lazy(() => import('./UploadDropZone')); -// Hooks +// Hooks y utilidades import { useImageSelection } from '@/app/store/components/images-selector/hooks/useImageSelection'; import { useImageUpload } from '@/app/store/components/images-selector/hooks/useImageUpload'; +import { filterAndSortImages, getFilterStats } from '../utils/filterUtils'; +import type { FilterState } from '../hooks/useImageFilters'; interface ImageSelectorModalProps { open: boolean; @@ -29,6 +31,14 @@ const ImageSelectorModal = memo(function ImageSelectorModal({ allowMultipleSelection = false, }: ImageSelectorModalProps) { const [searchTerm, setSearchTerm] = useState(''); + const [filters, setFilters] = useState({ + searchTerm: '', + fileTypes: [], + fileSizes: [], + usedIn: [], + products: [], + sortBy: 'date-asc', + }); const s3Options = useMemo(() => ({ limit: 18 }), []); const { showToast } = useToast(); @@ -36,11 +46,20 @@ const ImageSelectorModal = memo(function ImageSelectorModal({ const { images, loading, error, uploadImages, deleteImages, fetchMoreImages, loadingMore, nextContinuationToken } = useS3ImagesWithOperations(open ? s3Options : { limit: 0 }); + // Filtrar y ordenar imágenes + const filteredImages = useMemo(() => { + return filterAndSortImages(images, filters); + }, [images, filters]); + + const filterStats = useMemo(() => { + return getFilterStats(images, filteredImages); + }, [images, filteredImages]); + const { selectedImage, handleImageSelect, getSelectedImages, removeFromSelection, addToSelection } = useImageSelection({ initialSelectedImage, allowMultipleSelection, - images, + images: filteredImages, }); const { isUploading, uploadProgress, handleDrop, resetUploadState } = useImageUpload({ @@ -59,11 +78,6 @@ const ImageSelectorModal = memo(function ImageSelectorModal({ onUploadError: () => {}, }); - const filteredImages = useMemo( - () => images.filter((img) => img.filename.toLowerCase().includes(searchTerm.toLowerCase())), - [images, searchTerm] - ); - const handleConfirm = useCallback(() => { const selectedImages = getSelectedImages(); if (onSelect) { @@ -141,9 +155,22 @@ const ImageSelectorModal = memo(function ImageSelectorModal({ [nextContinuationToken, loadingMore, loading, fetchMoreImages] ); + const handleFiltersChange = useCallback((newFilters: FilterState) => { + setFilters(newFilters); + }, []); + const handleModalClose = useCallback(() => { // Limpiar estado al cerrar resetUploadState(); + setSearchTerm(''); + setFilters({ + searchTerm: '', + fileTypes: [], + fileSizes: [], + usedIn: [], + products: [], + sortBy: 'date-desc', + }); onOpenChange(false); }, [onOpenChange, resetUploadState]); @@ -164,7 +191,7 @@ const ImageSelectorModal = memo(function ImageSelectorModal({ size="large" open={open} onClose={handleModalClose} - title="Seleccionar imagen" + title={`Seleccionar imagen${filterStats.isFiltered ? ` (${filterStats.filteredCount}/${filterStats.totalImages})` : ''}`} primaryAction={{ content: 'Confirmar', onAction: handleConfirm, @@ -176,7 +203,7 @@ const ImageSelectorModal = memo(function ImageSelectorModal({ onAction: handleModalClose, }, ]}> - + {/* Solo mostrar banner para errores críticos de carga */} @@ -188,7 +215,12 @@ const ImageSelectorModal = memo(function ImageSelectorModal({ {/* Lazy load de SearchAndFilters */} - + {/* Lazy load de UploadDropZone */} diff --git a/app/store/components/images-selector/components/SearchAndFilters.tsx b/app/store/components/images-selector/components/SearchAndFilters.tsx index d2d97891..2a56b006 100644 --- a/app/store/components/images-selector/components/SearchAndFilters.tsx +++ b/app/store/components/images-selector/components/SearchAndFilters.tsx @@ -1,25 +1,145 @@ -import { TextField, InlineStack } from '@shopify/polaris'; -import { SearchIcon } from '@shopify/polaris-icons'; +import { TextField, InlineStack, BlockStack, Button } from '@shopify/polaris'; +import { SearchIcon, DeleteIcon } from '@shopify/polaris-icons'; +import { useState, useCallback, useEffect } from 'react'; +import { useDebounce } from '../hooks/useDebounce'; +import { useImageFilters } from '../hooks/useImageFilters'; +import FilterCombobox from './FilterCombobox'; +import SortPopover from './SortPopover'; +import type { S3Image } from '@/app/store/hooks/storage/useS3Images'; interface SearchAndFiltersProps { searchTerm: string; onSearchChange: (value: string) => void; + onFiltersChange: (filters: any) => void; + images: S3Image[]; } -export default function SearchAndFilters({ searchTerm, onSearchChange }: SearchAndFiltersProps) { +export default function SearchAndFilters({ + searchTerm, + onSearchChange, + onFiltersChange, + images, +}: SearchAndFiltersProps) { + const [inputValue, setInputValue] = useState(searchTerm); + + const { + filters, + filterOptions, + updateSearchTerm, + updateFileTypes, + updateFileSizes, + updateUsedIn, + updateProducts, + updateSortBy, + clearAllFilters, + getFilterStats, + } = useImageFilters(images); + + const debouncedSearchTerm = useDebounce(inputValue, 300); // 300ms debounce + + // Actualizar el término de búsqueda cuando el debounce cambie + useEffect(() => { + onSearchChange(debouncedSearchTerm); + updateSearchTerm(debouncedSearchTerm); + }, [debouncedSearchTerm, onSearchChange, updateSearchTerm]); + + // Sincronizar inputValue con searchTerm cuando viene de fuera + useEffect(() => { + setInputValue(searchTerm); + }, [searchTerm]); + + // Notificar cambios en filtros al componente padre + useEffect(() => { + onFiltersChange(filters); + }, [filters, onFiltersChange]); + + const handleInputChange = useCallback((value: string) => { + setInputValue(value); + }, []); + + const handleSortChange = useCallback( + (sortBy: string) => { + updateSortBy(sortBy); + }, + [updateSortBy] + ); + + const handleClearAll = useCallback(() => { + clearAllFilters(); + setInputValue(''); + onSearchChange(''); + }, [clearAllFilters, onSearchChange]); + + const stats = getFilterStats(); + return ( - -
    - } - autoComplete="off" - /> -
    -
    + + {/* Fila superior: Búsqueda, Ordenar y Limpiar */} + +
    + } + autoComplete="off" + clearButton + onClearButtonClick={() => handleInputChange('')} + /> +
    + + + + + {stats.hasActiveFilters && ( + + )} + +
    + + {/* Fila inferior: Filtros */} + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    ); } diff --git a/app/store/components/images-selector/components/SortPopover.tsx b/app/store/components/images-selector/components/SortPopover.tsx new file mode 100644 index 00000000..07c954a9 --- /dev/null +++ b/app/store/components/images-selector/components/SortPopover.tsx @@ -0,0 +1,58 @@ +import { useState, useCallback } from 'react'; +import { Popover, ActionList, Button } from '@shopify/polaris'; +import { SortIcon } from '@shopify/polaris-icons'; + +interface SortOption { + label: string; + value: string; +} + +interface SortPopoverProps { + options: SortOption[]; + selectedValue: string; + onSortChange: (value: string) => void; +} + +export default function SortPopover({ options, selectedValue: _selectedValue, onSortChange }: SortPopoverProps) { + const [_popoverActive, setPopoverActive] = useState(false); + + // TODO: Temporalmente deshabilitado para evitar problemas con infinite scroll + // Se puede habilitar cuando se implemente una solución más robusta + + const togglePopoverActive = useCallback(() => { + setPopoverActive((active) => !active); + }, []); + + const handleSortChange = useCallback( + (value: string) => { + onSortChange(value); + setPopoverActive(false); + }, + [onSortChange] + ); + + const activator = ( + + ); + + const actionListItems = options.map((option) => ({ + content: option.label, + value: option.value, + onAction: () => handleSortChange(option.value), + })); + + return ( + setPopoverActive(false)} + preferredPosition="below"> + + + ); +} diff --git a/app/store/components/images-selector/hooks/useDebounce.ts b/app/store/components/images-selector/hooks/useDebounce.ts new file mode 100644 index 00000000..c50d37f7 --- /dev/null +++ b/app/store/components/images-selector/hooks/useDebounce.ts @@ -0,0 +1,23 @@ +import { useState, useEffect } from 'react'; + +/** + * Hook personalizado para debounce de valores + * @param value - Valor a debounce + * @param delay - Delay en milisegundos + * @returns Valor debounce + */ +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/app/store/components/images-selector/hooks/useImageFilters.ts b/app/store/components/images-selector/hooks/useImageFilters.ts new file mode 100644 index 00000000..d5ef9202 --- /dev/null +++ b/app/store/components/images-selector/hooks/useImageFilters.ts @@ -0,0 +1,148 @@ +import { useState, useMemo, useCallback } from 'react'; +import type { S3Image } from '@/app/store/hooks/storage/useS3Images'; + +export interface FilterState { + searchTerm: string; + fileTypes: string[]; + fileSizes: string[]; + usedIn: string[]; + products: string[]; + sortBy: string; +} + +export interface FilterOptions { + fileTypeOptions: Array<{ label: string; value: string }>; + fileSizeOptions: Array<{ label: string; value: string }>; + usedInOptions: Array<{ label: string; value: string }>; + productOptions: Array<{ label: string; value: string }>; + sortOptions: Array<{ label: string; value: string }>; +} + +const DEFAULT_FILTER_STATE: FilterState = { + searchTerm: '', + fileTypes: [], + fileSizes: [], + usedIn: [], + products: [], + sortBy: 'date-asc', +}; + +/** + * Hook para manejar filtros de imágenes de manera optimizada + */ +export function useImageFilters(images: S3Image[]) { + const [filters, setFilters] = useState(DEFAULT_FILTER_STATE); + + // Opciones de filtros estáticas + const filterOptions: FilterOptions = useMemo( + () => ({ + fileTypeOptions: [ + { label: 'Imágenes', value: 'images' }, + { label: 'Videos', value: 'videos' }, + { label: 'Videos externos', value: 'external-videos' }, + { label: 'Modelos 3D', value: '3d' }, + ], + fileSizeOptions: [ + { label: 'Pequeño (< 1MB)', value: 'small' }, + { label: 'Mediano (1-5MB)', value: 'medium' }, + { label: 'Grande (> 5MB)', value: 'large' }, + ], + usedInOptions: [ + { label: 'Productos', value: 'products' }, + { label: 'Páginas', value: 'pages' }, + { label: 'Blog', value: 'blog' }, + ], + productOptions: [{ label: 'Producto específico', value: 'specific' }], + sortOptions: [ + { label: 'Fecha de adición (más recientes primero)', value: 'date-desc' }, + { label: 'Fecha de adición (más antiguos primero)', value: 'date-asc' }, + { label: 'Nombre de archivo (A-Z)', value: 'name-asc' }, + { label: 'Nombre de archivo (Z-A)', value: 'name-desc' }, + { label: 'Tamaño de archivo (menor primero)', value: 'size-asc' }, + { label: 'Tamaño de archivo (mayor primero)', value: 'size-desc' }, + ], + }), + [] + ); + + // Funciones para actualizar filtros específicos + const updateFilter = useCallback((filterKey: keyof FilterState, value: any) => { + setFilters((prev) => ({ ...prev, [filterKey]: value })); + }, []); + + const updateSearchTerm = useCallback( + (searchTerm: string) => { + updateFilter('searchTerm', searchTerm); + }, + [updateFilter] + ); + + const updateFileTypes = useCallback( + (fileTypes: string[]) => { + updateFilter('fileTypes', fileTypes); + }, + [updateFilter] + ); + + const updateFileSizes = useCallback( + (fileSizes: string[]) => { + updateFilter('fileSizes', fileSizes); + }, + [updateFilter] + ); + + const updateUsedIn = useCallback( + (usedIn: string[]) => { + updateFilter('usedIn', usedIn); + }, + [updateFilter] + ); + + const updateProducts = useCallback( + (products: string[]) => { + updateFilter('products', products); + }, + [updateFilter] + ); + + const updateSortBy = useCallback( + (sortBy: string) => { + updateFilter('sortBy', sortBy); + }, + [updateFilter] + ); + + // Función para limpiar todos los filtros + const clearAllFilters = useCallback(() => { + setFilters(DEFAULT_FILTER_STATE); + }, []); + + // Función para obtener estadísticas de filtros + const getFilterStats = useCallback(() => { + const activeFiltersCount = + (filters.fileTypes.length > 0 ? 1 : 0) + + (filters.fileSizes.length > 0 ? 1 : 0) + + (filters.usedIn.length > 0 ? 1 : 0) + + (filters.products.length > 0 ? 1 : 0) + + (filters.searchTerm.length > 0 ? 1 : 0); + + return { + activeFiltersCount, + hasActiveFilters: activeFiltersCount > 0, + totalImages: images.length, + }; + }, [filters, images.length]); + + return { + filters, + filterOptions, + updateSearchTerm, + updateFileTypes, + updateFileSizes, + updateUsedIn, + updateProducts, + updateSortBy, + clearAllFilters, + getFilterStats, + }; +} diff --git a/app/store/components/images-selector/index.ts b/app/store/components/images-selector/index.ts index 9c400956..7a4d0149 100644 --- a/app/store/components/images-selector/index.ts +++ b/app/store/components/images-selector/index.ts @@ -4,14 +4,23 @@ export { default as ImageSelectorModal } from '@/app/store/components/images-sel // Hooks personalizados export { useImageSelection } from '@/app/store/components/images-selector/hooks/useImageSelection'; export { useImageUpload } from '@/app/store/components/images-selector/hooks/useImageUpload'; +export { useDebounce } from '@/app/store/components/images-selector/hooks/useDebounce'; +export { useImageFilters } from '@/app/store/components/images-selector/hooks/useImageFilters'; // Componentes modulares export { default as ImageGallery } from '@/app/store/components/images-selector/components/ImageGallery'; export { default as SearchAndFilters } from '@/app/store/components/images-selector/components/SearchAndFilters'; export { default as UploadDropZone } from '@/app/store/components/images-selector/components/UploadDropZone'; +export { default as FilterPopover } from '@/app/store/components/images-selector/components/FilterPopover'; +export { default as FilterCombobox } from '@/app/store/components/images-selector/components/FilterCombobox'; +export { default as SortPopover } from '@/app/store/components/images-selector/components/SortPopover'; // Re-exportar tipos del hook principal export type { S3Image } from '@/app/store/hooks/storage/useS3Images'; +// Utils +export { filterAndSortImages, getFilterStats } from '@/app/store/components/images-selector/utils/filterUtils'; + // Nuevos tipos para operaciones por lotes export type { BatchUploadResult } from '@/app/store/components/images-selector/hooks/useImageUpload'; +export type { FilterState } from '@/app/store/components/images-selector/hooks/useImageFilters'; diff --git a/app/store/components/images-selector/utils/filterUtils.ts b/app/store/components/images-selector/utils/filterUtils.ts new file mode 100644 index 00000000..c9f01f31 --- /dev/null +++ b/app/store/components/images-selector/utils/filterUtils.ts @@ -0,0 +1,178 @@ +import type { S3Image } from '@/app/store/hooks/storage/useS3Images'; +import type { FilterState } from '../hooks/useImageFilters'; + +/** + * Obtiene la extensión del archivo para determinar el tipo + */ +function getFileType(filename: string): string { + const extension = filename.split('.').pop()?.toLowerCase() || ''; + + // Imágenes + if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(extension)) { + return 'images'; + } + + // Videos + if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm'].includes(extension)) { + return 'videos'; + } + + // Videos externos (URLs de YouTube, Vimeo, etc.) + if (filename.includes('youtube') || filename.includes('vimeo') || filename.includes('youtu.be')) { + return 'external-videos'; + } + + // Modelos 3D + if (['obj', 'fbx', 'gltf', 'glb', '3ds', 'dae'].includes(extension)) { + return '3d'; + } + + return 'other'; +} + +/** + * Determina el tamaño del archivo basado en el tamaño en bytes + */ +function getFileSizeCategory(sizeInBytes: number): string { + const sizeInMB = sizeInBytes / (1024 * 1024); + + if (sizeInMB < 1) return 'small'; + if (sizeInMB <= 5) return 'medium'; + return 'large'; +} + +/** + * Filtra imágenes por término de búsqueda + */ +function filterBySearchTerm(images: S3Image[], searchTerm: string): S3Image[] { + if (!searchTerm.trim()) return images; + + const lowerSearchTerm = searchTerm.toLowerCase().trim(); + + return images.filter((image) => { + const filename = image.filename.toLowerCase(); + + // Búsqueda exacta + if (filename.includes(lowerSearchTerm)) return true; + + // Búsqueda por palabras + const searchWords = lowerSearchTerm.split(/\s+/); + return searchWords.every((word) => filename.includes(word)); + }); +} + +/** + * Filtra imágenes por tipos de archivo + */ +function filterByFileTypes(images: S3Image[], fileTypes: string[]): S3Image[] { + if (fileTypes.length === 0) return images; + + return images.filter((image) => { + const imageType = getFileType(image.filename); + return fileTypes.includes(imageType); + }); +} + +/** + * Filtra imágenes por tamaño de archivo + */ +function filterByFileSizes(images: S3Image[], fileSizes: string[]): S3Image[] { + if (fileSizes.length === 0) return images; + + return images.filter((image) => { + const sizeCategory = getFileSizeCategory(image.size || 0); + return fileSizes.includes(sizeCategory); + }); +} + +/** + * Filtra imágenes por dónde se usan (placeholder para futura implementación) + */ +function filterByUsedIn(images: S3Image[], _usedIn: string[]): S3Image[] { + // TODO: Implementar cuando tengamos la relación de uso + // Por ahora retorna todas las imágenes + return images; +} + +/** + * Filtra imágenes por productos (placeholder para futura implementación) + */ +function filterByProducts(images: S3Image[], _products: string[]): S3Image[] { + // TODO: Implementar cuando tengamos la relación con productos + // Por ahora retorna todas las imágenes + return images; +} + +/** + * Ordena imágenes según el criterio especificado + */ +function sortImages(images: S3Image[], sortBy: string): S3Image[] { + const sortedImages = [...images]; + + switch (sortBy) { + case 'date-asc': + return sortedImages.sort((a, b) => { + const dateA = new Date(a.lastModified || 0).getTime(); + const dateB = new Date(b.lastModified || 0).getTime(); + return dateA - dateB; + }); + + case 'date-desc': + return sortedImages.sort((a, b) => { + const dateA = new Date(a.lastModified || 0).getTime(); + const dateB = new Date(b.lastModified || 0).getTime(); + return dateB - dateA; + }); + + case 'name-asc': + return sortedImages.sort((a, b) => a.filename.localeCompare(b.filename)); + + case 'name-desc': + return sortedImages.sort((a, b) => b.filename.localeCompare(a.filename)); + + case 'size-asc': + return sortedImages.sort((a, b) => (a.size || 0) - (b.size || 0)); + + case 'size-desc': + return sortedImages.sort((a, b) => (b.size || 0) - (a.size || 0)); + + default: + return sortedImages; + } +} + +/** + * Función principal para filtrar y ordenar imágenes + */ +export function filterAndSortImages(images: S3Image[], filters: FilterState): S3Image[] { + let filteredImages = images; + + // Aplicar filtros en orden de eficiencia (menos restrictivos primero) + filteredImages = filterBySearchTerm(filteredImages, filters.searchTerm); + filteredImages = filterByFileTypes(filteredImages, filters.fileTypes); + filteredImages = filterByFileSizes(filteredImages, filters.fileSizes); + filteredImages = filterByUsedIn(filteredImages, filters.usedIn); + filteredImages = filterByProducts(filteredImages, filters.products); + + // Ordenar al final + filteredImages = sortImages(filteredImages, filters.sortBy); + + return filteredImages; +} + +/** + * Hook para obtener estadísticas de filtrado + */ +export function getFilterStats(images: S3Image[], filteredImages: S3Image[]) { + const totalImages = images.length; + const filteredCount = filteredImages.length; + const filteredOut = totalImages - filteredCount; + + return { + totalImages, + filteredCount, + filteredOut, + isFiltered: filteredCount < totalImages, + filterEfficiency: totalImages > 0 ? (filteredCount / totalImages) * 100 : 100, + }; +} diff --git a/app/store/components/payments/components/PaymentGatewayCard.tsx b/app/store/components/payments/components/PaymentGatewayCard.tsx index 45ebc985..e7624dac 100644 --- a/app/store/components/payments/components/PaymentGatewayCard.tsx +++ b/app/store/components/payments/components/PaymentGatewayCard.tsx @@ -1,6 +1,6 @@ import Image from 'next/image'; import { LegacyStack, Text, Button, Badge } from '@shopify/polaris'; -import { PaymentGatewayType } from '@/app/(setup-layout)/first-steps/hooks/useUserStoreData'; +import { PaymentGatewayType } from '@/app/(setup)/first-steps/hooks/useUserStoreData'; import { WompiPaymentIcons, MercadoPagoIcons } from '@/app/store/components/payments/components/PaymentMethodIcons'; interface PaymentGatewayCardProps { diff --git a/app/store/components/payments/components/PaymentMethodsSection.tsx b/app/store/components/payments/components/PaymentMethodsSection.tsx index eae7b387..c6882e09 100644 --- a/app/store/components/payments/components/PaymentMethodsSection.tsx +++ b/app/store/components/payments/components/PaymentMethodsSection.tsx @@ -1,5 +1,5 @@ import { Card, Text, LegacyStack, Box, BlockStack } from '@shopify/polaris'; -import { PaymentGatewayType } from '@/app/(setup-layout)/first-steps/hooks/useUserStoreData'; +import { PaymentGatewayType } from '@/app/(setup)/first-steps/hooks/useUserStoreData'; import { PaymentGatewayCard } from '@/app/store/components/payments/components/PaymentGatewayCard'; interface PaymentMethodsSectionProps { diff --git a/app/store/components/payments/hooks/usePaymentSettings.ts b/app/store/components/payments/hooks/usePaymentSettings.ts index a0bf665c..96d4e4f4 100644 --- a/app/store/components/payments/hooks/usePaymentSettings.ts +++ b/app/store/components/payments/hooks/usePaymentSettings.ts @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useParams } from 'next/navigation'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { useUserStoreData, PaymentGatewayType } from '@/app/(setup-layout)/first-steps/hooks/useUserStoreData'; +import { useUserStoreData, PaymentGatewayType } from '@/app/(setup)/first-steps/hooks/useUserStoreData'; import { useAuth } from '@/context/hooks/useAuth'; import { useToast } from '@/app/store/context/ToastContext'; diff --git a/app/store/components/sidebar/components/NavigationPolaris.tsx b/app/store/components/sidebar/components/NavigationPolaris.tsx index 848b1228..fb01ef69 100644 --- a/app/store/components/sidebar/components/NavigationPolaris.tsx +++ b/app/store/components/sidebar/components/NavigationPolaris.tsx @@ -101,14 +101,16 @@ export function NavigationPolaris({ storeId }: NavigationPolarisProps) { url: `/store/${storeId}/setup`, label: 'Tienda Online', displayActionsOnHover: true, - secondaryAction: { - accessibilityLabel: 'Ver tienda online', - icon: ViewIcon, - tooltip: { - content: 'Ver tienda online', - }, - onClick: handleViewStore, - }, + secondaryActions: [ + { + accessibilityLabel: 'Ver tienda online', + icon: ViewIcon, + tooltip: { + content: 'Ver tienda online', + }, + onClick: handleViewStore, + }, + ], icon: StoreIcon, selected: pathname.includes('/setup'), subNavigationItems: [ diff --git a/app/store/components/sidebar/components/TopBarPolaris.tsx b/app/store/components/sidebar/components/TopBarPolaris.tsx index a534de79..f3f26c50 100644 --- a/app/store/components/sidebar/components/TopBarPolaris.tsx +++ b/app/store/components/sidebar/components/TopBarPolaris.tsx @@ -121,8 +121,8 @@ export function TopBarPolaris({ storeId, onNavigationToggle }: TopBarPolarisProp { items: [ { content: 'Centro de Ayuda' }, + { content: 'Cambiar de Tienda', onAction: () => handleChangeStore() }, { content: 'Cerrar sesión', icon: ExitIcon, onAction: handleLogout }, - { content: 'Cambiar de Tienda', icon: ExitIcon, onAction: () => handleChangeStore() }, ], }, ]} diff --git a/app/store/components/store-config/components/LogoUploader.tsx b/app/store/components/store-config/components/LogoUploader.tsx index ffa3be34..23be1f35 100644 --- a/app/store/components/store-config/components/LogoUploader.tsx +++ b/app/store/components/store-config/components/LogoUploader.tsx @@ -13,7 +13,7 @@ import { InlineStack, } from '@shopify/polaris'; import { useLogoUpload } from '@/app/store/hooks/storage/useLogoUpload'; -import { useUserStoreData } from '@/app/(setup-layout)/first-steps/hooks/useUserStoreData'; +import { useUserStoreData } from '@/app/(setup)/first-steps/hooks/useUserStoreData'; import useStoreDataStore from '@/context/core/storeDataStore'; import { InfoIcon } from '@shopify/polaris-icons'; diff --git a/app/store/components/store-setup/components/EcommerceSetup.tsx b/app/store/components/store-setup/components/EcommerceSetup.tsx index 1d1c77da..4bfa665d 100644 --- a/app/store/components/store-setup/components/EcommerceSetup.tsx +++ b/app/store/components/store-setup/components/EcommerceSetup.tsx @@ -4,7 +4,7 @@ import { BlockStack, Box, Card, Loading, Page } from '@shopify/polaris'; import { useParams, usePathname } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { useUserStoreData } from '@/app/(setup-layout)/first-steps/hooks/useUserStoreData'; +import { useUserStoreData } from '@/app/(setup)/first-steps/hooks/useUserStoreData'; import { PricingDrawer } from '@/app/store/components/store-setup/components/PricingDrawer'; import useStoreDataStore from '@/context/core/storeDataStore'; import { getStoreId } from '@/utils/client/store-utils'; diff --git a/app/store/components/store-setup/components/PricingDrawer.tsx b/app/store/components/store-setup/components/PricingDrawer.tsx index 736796ba..48419600 100644 --- a/app/store/components/store-setup/components/PricingDrawer.tsx +++ b/app/store/components/store-setup/components/PricingDrawer.tsx @@ -3,8 +3,8 @@ import { Modal, Text, Button, Box, Card, Badge, BlockStack } from '@shopify/pola import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { useToast } from '@/app/store/context/ToastContext'; -import { plans } from '@/app/(main-layout)/pricing/components/plans'; -import { faqItems } from '@/app/(main-layout)/pricing/components/FAQItem'; +import { plans } from '@/app/(www)/pricing/components/plans'; +import { faqItems } from '@/app/(www)/pricing/components/FAQItem'; import { useIsClient } from '@/hooks/ui/useIsClient'; import { Amplify } from 'aws-amplify'; import { post } from 'aws-amplify/api'; diff --git a/app/store/hooks/storage/useProductImageUpload.ts b/app/store/hooks/storage/useProductImageUpload.ts index 9df35fbd..8a2066d8 100644 --- a/app/store/hooks/storage/useProductImageUpload.ts +++ b/app/store/hooks/storage/useProductImageUpload.ts @@ -1,12 +1,8 @@ -import outputs from '@/amplify_outputs.json'; import { getCdnUrlForKey } from '@/utils/client'; -import { Amplify } from 'aws-amplify'; import { uploadData } from 'aws-amplify/storage'; import { useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; -Amplify.configure(outputs); - export interface UploadedImage { url: string; alt: string; @@ -34,7 +30,7 @@ export function useProductImageUpload() { data: file, }).result; - // Construir la URL pública usando la utilidad de CDN + // Construir la URL pública usando la utilidad CDN const publicUrl = getCdnUrlForKey(result.path); return { diff --git a/app/store/hooks/storage/useS3Images.ts b/app/store/hooks/storage/useS3Images.ts index 884cd2df..c8c0ef5d 100644 --- a/app/store/hooks/storage/useS3Images.ts +++ b/app/store/hooks/storage/useS3Images.ts @@ -1,4 +1,5 @@ -import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import { useCallback, useMemo } from 'react'; +import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; import { post } from 'aws-amplify/api'; import useStoreDataStore from '@/context/core/storeDataStore'; @@ -22,16 +23,8 @@ interface S3ImagesResponse { } export function useS3Images(options: UseS3ImagesOptions = {}) { - const [images, setImages] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); const { storeId } = useStoreDataStore(); - const [nextContinuationToken, setNextContinuationToken] = useState(undefined); - const [loadingMore, setLoadingMore] = useState(false); - - // Ref para evitar peticiones duplicadas - const isFetching = useRef(false); - const lastFetchParams = useRef(''); + const queryClient = useQueryClient(); // Memoizar las opciones para evitar re-renders innecesarios const memoizedOptions = useMemo( @@ -42,107 +35,85 @@ export function useS3Images(options: UseS3ImagesOptions = {}) { [options.limit, options.prefix] ); - const fetchImages = useCallback( - async (token?: string) => { - // Crear una clave única para esta petición - const fetchKey = `${storeId}-${memoizedOptions.limit}-${memoizedOptions.prefix}-${token || 'initial'}`; - - // Si ya hay una petición en curso con los mismos parámetros, no hacer otra - if (isFetching.current && lastFetchParams.current === fetchKey) { - return; - } - + // Función para hacer la petición de imágenes + const fetchImagesPage = useCallback( + async ({ pageParam }: { pageParam?: string }) => { if (!storeId || memoizedOptions.limit <= 0) { - setLoading(false); - setImages([]); - setNextContinuationToken(undefined); - return; + return { + images: [], + nextContinuationToken: undefined, + }; } - // Marcar que estamos haciendo una petición - isFetching.current = true; - lastFetchParams.current = fetchKey; - - if (!token) { - setLoading(true); - setImages([]); - } else { - setLoadingMore(true); + const restOperation = post({ + apiName: 'StoreImagesApi', + path: 'store-images', + options: { + body: { + action: 'list', + storeId, + limit: memoizedOptions.limit, + prefix: memoizedOptions.prefix, + continuationToken: pageParam, + } as any, + }, + }); + + const { body } = await restOperation.response; + const response = (await body.json()) as S3ImagesResponse; + + if (!response.images) { + return { + images: [], + nextContinuationToken: undefined, + }; } - setError(null); - - try { - const restOperation = post({ - apiName: 'StoreImagesApi', - path: 'store-images', - options: { - body: { - action: 'list', - storeId, - limit: memoizedOptions.limit, - prefix: memoizedOptions.prefix, - continuationToken: token, - } as any, - }, - }); - - const { body } = await restOperation.response; - const response = (await body.json()) as S3ImagesResponse; - - if (!response.images) { - if (!token) { - setImages([]); - } - setNextContinuationToken(undefined); - return; - } - const processedImages = response.images.map((img) => ({ - ...img, - lastModified: img.lastModified ? new Date(img.lastModified) : undefined, - id: img.id || generateFallbackId(img.key, img.filename), - })); - - setImages((prev) => (token ? [...prev, ...processedImages] : processedImages)); - setNextContinuationToken(response.nextContinuationToken); - } catch (err) { - console.error(token ? 'Error fetching more S3 images:' : 'Error fetching S3 images:', err); - setError(err instanceof Error ? err : new Error('Unknown error occurred')); - setNextContinuationToken(undefined); - } finally { - // Resetear el flag de petición - isFetching.current = false; - - if (!token) { - setLoading(false); - } else { - setLoadingMore(false); - } - } + const processedImages = response.images.map((img) => ({ + ...img, + lastModified: img.lastModified ? new Date(img.lastModified) : undefined, + id: img.id || generateFallbackId(img.key, img.filename), + })); + + return { + images: processedImages, + nextContinuationToken: response.nextContinuationToken, + }; }, [storeId, memoizedOptions.limit, memoizedOptions.prefix] ); - // useEffect optimizado - solo se ejecuta cuando es necesario - useEffect(() => { - if (storeId && memoizedOptions.limit > 0) { - fetchImages(); - } - }, [storeId, memoizedOptions.limit, memoizedOptions.prefix, fetchImages]); - + // Configurar la query infinita de React Query + const { data, isLoading, error, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = useInfiniteQuery({ + queryKey: ['s3-images', storeId, memoizedOptions.limit, memoizedOptions.prefix], + queryFn: fetchImagesPage, + initialPageParam: undefined as string | undefined, + getNextPageParam: (lastPage) => lastPage.nextContinuationToken, + enabled: !!storeId && memoizedOptions.limit > 0, + staleTime: 5 * 60 * 1000, // 5 minutos + gcTime: 10 * 60 * 1000, // 10 minutos + }); + + // Combinar todas las páginas de imágenes en una sola lista + const images = useMemo(() => { + if (!data?.pages) return []; + return data.pages.flatMap((page) => page.images); + }, [data]); + + // Función para cargar más imágenes const fetchMoreImages = useCallback(() => { - if (nextContinuationToken && !loadingMore && !loading) { - fetchImages(nextContinuationToken); + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); } - }, [nextContinuationToken, loadingMore, loading, fetchImages]); + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); // Función para verificar si necesitamos cargar más imágenes automáticamente const checkAndLoadMoreIfNeeded = useCallback(() => { - // Si no hay imágenes pero hay nextToken disponible, cargar automáticamente - if (images.length === 0 && nextContinuationToken && !loadingMore && !loading) { + // Si no hay imágenes pero hay más páginas disponibles, cargar automáticamente + if (images.length === 0 && hasNextPage && !isFetchingNextPage) { fetchMoreImages(); } - }, [images.length, nextContinuationToken, loadingMore, loading, fetchMoreImages]); + }, [images.length, hasNextPage, isFetchingNextPage, fetchMoreImages]); // Función para actualizar imágenes después de operaciones exitosas const updateImages = useCallback( @@ -151,20 +122,54 @@ export function useS3Images(options: UseS3ImagesOptions = {}) { prev: import('@/app/store/components/images-selector/types/s3-types').S3Image[] ) => import('@/app/store/components/images-selector/types/s3-types').S3Image[] ) => { - setImages(updater); + // Actualizar el cache de React Query + queryClient.setQueryData( + ['s3-images', storeId, memoizedOptions.limit, memoizedOptions.prefix], + (oldData: any) => { + if (!oldData) return oldData; + + const allImages = oldData.pages.flatMap((page: any) => page.images); + const updatedImages = updater(allImages); + + // Reorganizar las imágenes actualizadas en páginas + const pageSize = memoizedOptions.limit; + const newPages = []; + + for (let i = 0; i < updatedImages.length; i += pageSize) { + newPages.push({ + images: updatedImages.slice(i, i + pageSize), + nextContinuationToken: i + pageSize < updatedImages.length ? 'continue' : undefined, + }); + } + + return { + ...oldData, + pages: newPages, + }; + } + ); }, - [] + [queryClient, storeId, memoizedOptions.limit, memoizedOptions.prefix] ); + // Función para invalidar y refrescar el cache + const refreshImages = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: ['s3-images', storeId, memoizedOptions.limit, memoizedOptions.prefix], + }); + }, [queryClient, storeId, memoizedOptions.limit, memoizedOptions.prefix]); + return { images, - loading, - error, + loading: isLoading, + error: error as Error | null, fetchMoreImages, - loadingMore, - nextContinuationToken, + loadingMore: isFetchingNextPage, + nextContinuationToken: hasNextPage ? 'available' : undefined, checkAndLoadMoreIfNeeded, updateImages, + refreshImages, + refetch, }; } diff --git a/lib/amplify-client.ts b/lib/amplify-client.ts index f843019c..9d033f42 100644 --- a/lib/amplify-client.ts +++ b/lib/amplify-client.ts @@ -17,6 +17,17 @@ import { generateClient } from 'aws-amplify/data'; import type { FullSchema, StoreSchema } from '@/data-schema'; +import outputs from '@/amplify_outputs.json'; +import { Amplify } from 'aws-amplify'; +Amplify.configure(outputs); +const existingConfig = Amplify.getConfig(); +Amplify.configure({ + ...existingConfig, + API: { + ...existingConfig.API, + REST: outputs.custom.APIs, + }, +}); // Alias de tipos explícitos para evitar tipos inferidos gigantes en la emisión export type FullClient = ReturnType>; diff --git a/middlewares/auth/auth.ts b/middlewares/auth/auth.ts index fc2aadfa..42cccd1a 100644 --- a/middlewares/auth/auth.ts +++ b/middlewares/auth/auth.ts @@ -90,7 +90,7 @@ export async function handleAuthenticationMiddleware(request: NextRequest, respo return NextResponse.redirect(new URL('/login', request.url)); } - return response; + return null; // Permitir que el middleware continúe } export async function handleAuthenticatedRedirectMiddleware(request: NextRequest, response: NextResponse) { diff --git a/next.config.ts b/next.config.ts index 5e25ae13..48764e60 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,11 @@ import type { NextConfig } from 'next'; /** @type {import('next').NextConfig} */ const nextConfig: NextConfig = { + // TypeScript optimizations - disable type checking in build + typescript: { + ignoreBuildErrors: true, // Disable type checking during build + }, + serverExternalPackages: [ '@aws-sdk/client-acm', '@aws-sdk/client-bedrock-runtime', @@ -21,6 +26,8 @@ const nextConfig: NextConfig = { unoptimized: process.env.NODE_ENV === 'development', + qualities: [25, 50, 75, 85, 90, 95, 100], + localPatterns: [ { pathname: '/icons/**', diff --git a/package.json b/package.json index 8d9ead30..8cdb8d95 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,9 @@ "test:coverage": "jest --coverage", "template-sync": "node scripts/template-sync.js", "type-check": "tsc --noEmit", + "type-check:fast": "tsc --noEmit --skipLibCheck", + "build:fast": "next build --turbopack", + "build:full": "pnpm run type-check && pnpm run build:fast", "workspace:install": "pnpm install --workspaces", "workspace:update": "pnpm update --workspaces", "workspace:audit": "pnpm audit --workspaces", @@ -109,7 +112,10 @@ "react-currency-input-field": "^3.10.0", "react-dom": "^18.3.1", "react-hook-form": "^7.60.0", + "react-markdown": "^10.1.0", "recharts": "^2.15.4", + "rehype-highlight": "^7.0.2", + "remark-gfm": "^4.0.1", "styled-components": "^6.1.19", "swiper": "^11.2.10", "tailwind-merge": "^3.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36a3c7cd..832635f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -182,9 +182,18 @@ importers: react-hook-form: specifier: ^7.60.0 version: 7.64.0(react@18.3.1) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@18.3.25)(react@18.3.1) recharts: specifier: ^2.15.4 version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rehype-highlight: + specifier: ^7.0.2 + version: 7.0.2 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 styled-components: specifier: ^6.1.19 version: 6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -4138,9 +4147,18 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hoist-non-react-statics@3.3.7': resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} peerDependencies: @@ -4173,6 +4191,9 @@ packages: '@types/lodash@4.17.20': resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -4211,6 +4232,12 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@8.3.4': resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} @@ -4670,6 +4697,9 @@ packages: babel-types@6.26.0: resolution: {integrity: sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -4780,6 +4810,9 @@ packages: capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + cdk-assets@3.3.1: resolution: {integrity: sha512-ZdvrDwbdc3buy3YPjsCFSAx1f1Z7e+i0asf5wG8lXyTnkrmbpDQb4O1x0Zf/qE3VlYfcbEVRrfVpoHsk5JiObQ==} engines: {node: '>= 18.0.0'} @@ -4817,6 +4850,18 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chardet@2.1.0: resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} @@ -4920,6 +4965,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -5184,6 +5232,9 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + dedent@1.7.0: resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} peerDependencies: @@ -5258,6 +5309,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -5428,6 +5482,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-config-next@15.5.4: resolution: {integrity: sha512-BzgVVuT3kfJes8i2GHenC1SRJ+W3BTML11lAOYFOOPzrk2xp66jBOAGEFRw+3LkYCln5UzvFsLhojrshb5Zfaw==} peerDependencies: @@ -5563,6 +5621,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -5608,6 +5669,9 @@ packages: resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@2.0.1: resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} @@ -5970,9 +6034,25 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + header-case@2.0.4: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + hjson@3.2.2: resolution: {integrity: sha512-MkUeB0cTIlppeSsndgESkfFD21T2nXPRaBStLtf3cAYA2bVEFdXlodZB0TukwZiobPD1Ksax5DK4RTZeaXCI3Q==} hasBin: true @@ -5991,6 +6071,9 @@ packages: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} @@ -6085,6 +6168,9 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + input-otp@1.4.2: resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} peerDependencies: @@ -6110,6 +6196,12 @@ packages: resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==} engines: {node: '>=0.10.0'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -6159,6 +6251,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6196,6 +6291,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -6809,6 +6907,9 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -6819,6 +6920,9 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lowlight@3.3.0: + resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -6863,6 +6967,9 @@ packages: resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} engines: {node: '>=0.10.0'} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@7.0.4: resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==} engines: {node: '>= 16'} @@ -6880,6 +6987,51 @@ packages: md5@2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.0.28: resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} @@ -6893,6 +7045,90 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -7222,6 +7458,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-filepath@1.0.2: resolution: {integrity: sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==} engines: {node: '>=0.8'} @@ -7653,6 +7892,9 @@ packages: property-expr@2.0.6: resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -7702,6 +7944,12 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-promise-suspense@0.3.4: resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} @@ -7809,9 +8057,24 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + rehype-highlight@7.0.2: + resolution: {integrity: sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==} + relay-runtime@12.0.0: resolution: {integrity: sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + repeating@2.0.1: resolution: {integrity: sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==} engines: {node: '>=0.10.0'} @@ -8029,6 +8292,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -8123,6 +8389,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -8161,6 +8430,12 @@ packages: strnum@2.1.1: resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} + style-to-js@1.1.18: + resolution: {integrity: sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==} + + style-to-object@1.0.11: + resolution: {integrity: sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==} + styled-components@6.1.19: resolution: {integrity: sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==} engines: {node: '>= 16'} @@ -8344,10 +8619,16 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + trim-right@1.0.1: resolution: {integrity: sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==} engines: {node: '>=0.10.0'} + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -8525,6 +8806,27 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -8615,6 +8917,12 @@ packages: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} @@ -8839,6 +9147,9 @@ packages: use-sync-external-store: optional: true + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@adobe/css-tools@4.4.4': {} @@ -15298,8 +15609,20 @@ snapshots: '@types/d3-timer@3.0.2': {} + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/hoist-non-react-statics@3.3.7(@types/react@18.3.25)': dependencies: '@types/react': 18.3.25 @@ -15337,6 +15660,10 @@ snapshots: '@types/lodash@4.17.20': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/ms@2.1.0': {} '@types/node-cache@4.2.5': @@ -15374,6 +15701,10 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/uuid@8.3.4': {} '@types/uuid@9.0.8': {} @@ -15925,6 +16256,8 @@ snapshots: lodash: 4.17.21 to-fast-properties: 1.0.3 + bail@2.0.2: {} + balanced-match@1.0.2: {} bare-events@2.7.0: {} @@ -16042,6 +16375,8 @@ snapshots: tslib: 2.8.1 upper-case-first: 2.0.2 + ccount@2.0.1: {} + cdk-assets@3.3.1: dependencies: '@aws-cdk/cloud-assembly-schema': 48.12.0 @@ -16126,6 +16461,14 @@ snapshots: char-regex@1.0.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chardet@2.1.0: {} charenc@0.0.2: {} @@ -16219,6 +16562,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} commander@11.1.0: {} @@ -16474,6 +16819,10 @@ snapshots: decimal.js@10.6.0: {} + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + dedent@1.7.0: {} deep-is@0.1.4: {} @@ -16525,6 +16874,10 @@ snapshots: detect-node-es@1.1.0: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + didyoumean@1.2.2: {} diff@4.0.2: @@ -16768,6 +17121,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-config-next@15.5.4(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 15.5.4 @@ -17021,6 +17376,8 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + esutils@2.0.3: {} event-target-shim@5.0.1: {} @@ -17079,6 +17436,8 @@ snapshots: jest-mock: 30.2.0 jest-util: 30.2.0 + extend@3.0.2: {} + fast-deep-equal@2.0.1: {} fast-deep-equal@3.1.3: {} @@ -17456,11 +17815,48 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.18 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + header-case@2.0.4: dependencies: capital-case: 1.0.4 tslib: 2.8.1 + highlight.js@11.11.1: {} + hjson@3.2.2: {} hoist-non-react-statics@3.3.2: @@ -17481,6 +17877,8 @@ snapshots: htmlparser2: 8.0.2 selderee: 0.11.0 + html-url-attributes@3.0.1: {} + htmlparser2@8.0.2: dependencies: domelementtype: 2.3.0 @@ -17561,6 +17959,8 @@ snapshots: ini@1.3.8: {} + inline-style-parser@0.2.4: {} + input-otp@1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -17585,6 +17985,13 @@ snapshots: is-relative: 1.0.0 is-windows: 1.0.2 + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -17641,6 +18048,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-docker@3.0.0: {} is-extglob@2.1.1: {} @@ -17671,6 +18080,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -18453,6 +18864,8 @@ snapshots: long@5.3.2: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -18465,6 +18878,12 @@ snapshots: dependencies: tslib: 2.8.1 + lowlight@3.3.0: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.11.1 + lru-cache@10.4.3: {} lru-cache@11.2.2: {} @@ -18500,6 +18919,8 @@ snapshots: map-cache@0.2.2: {} + markdown-table@3.0.4: {} + marked@7.0.4: {} math-intrinsics@1.1.0: {} @@ -18515,6 +18936,159 @@ snapshots: crypt: 0.0.2 is-buffer: 1.1.6 + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdn-data@2.0.28: {} mdn-data@2.12.2: {} @@ -18523,6 +19097,197 @@ snapshots: merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -18846,6 +19611,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.2.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-filepath@1.0.2: dependencies: is-absolute: 1.0.0 @@ -19227,6 +20002,8 @@ snapshots: property-expr@2.0.6: {} + property-information@7.1.0: {} + proto-list@1.2.4: {} proxy-from-env@1.1.0: {} @@ -19265,6 +20042,24 @@ snapshots: react-is@18.3.1: {} + react-markdown@10.1.0(@types/react@18.3.25)(react@18.3.1): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 18.3.25 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 18.3.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-promise-suspense@0.3.4: dependencies: fast-deep-equal: 2.0.1 @@ -19407,6 +20202,14 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + rehype-highlight@7.0.2: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-text: 4.0.2 + lowlight: 3.3.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + relay-runtime@12.0.0: dependencies: '@babel/runtime': 7.28.4 @@ -19415,6 +20218,40 @@ snapshots: transitivePeerDependencies: - encoding + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + repeating@2.0.1: dependencies: is-finite: 1.1.0 @@ -19666,6 +20503,8 @@ snapshots: source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + split2@4.2.0: {} sponge-case@1.0.1: @@ -19797,6 +20636,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -19823,6 +20667,14 @@ snapshots: strnum@2.1.1: {} + style-to-js@1.1.18: + dependencies: + style-to-object: 1.0.11 + + style-to-object@1.0.11: + dependencies: + inline-style-parser: 0.2.4 + styled-components@6.1.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@emotion/is-prop-valid': 1.2.2 @@ -20035,8 +20887,12 @@ snapshots: dependencies: punycode: 2.3.1 + trim-lines@3.0.1: {} + trim-right@1.0.1: {} + trough@2.2.0: {} + ts-api-utils@2.1.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -20199,6 +21055,44 @@ snapshots: unicorn-magic@0.3.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + universalify@0.1.2: {} universalify@2.0.1: {} @@ -20300,6 +21194,16 @@ snapshots: - '@types/react' - '@types/react-dom' + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + victory-vendor@36.9.2: dependencies: '@types/d3-array': 3.2.2 @@ -20544,3 +21448,5 @@ snapshots: immer: 9.0.6 react: 18.3.1 use-sync-external-store: 1.6.0(react@18.3.1) + + zwitch@2.0.4: {} diff --git a/scripts/add-license-header.js b/scripts/add-license-header.js index c54f0e23..e285c3c0 100644 --- a/scripts/add-license-header.js +++ b/scripts/add-license-header.js @@ -60,7 +60,7 @@ function addHeaderToFile(filePath) { // Si ya tiene header, saltar if (hasLicenseHeader(content)) { - console.log(`⏭️ Saltando ${filePath} (ya tiene header de licencia)`); + console.log(`Saltando ${filePath} (ya tiene header de licencia)`); return false; } @@ -68,10 +68,10 @@ function addHeaderToFile(filePath) { const newContent = LICENSE_HEADER + '\n\n' + content; fs.writeFileSync(filePath, newContent, 'utf8'); - console.log(`✅ Header agregado a ${filePath}`); + console.log(`Header agregado a ${filePath}`); return true; } catch (error) { - console.error(`❌ Error procesando ${filePath}:`, error.message); + console.error(`Error procesando ${filePath}:`, error.message); return false; } } @@ -82,7 +82,7 @@ async function main() { const dryRun = args.includes('--dry-run') || args.includes('-d'); const specificPath = args.find((arg) => !arg.startsWith('--')); - console.log('🔍 Buscando archivos para agregar header de licencia...\n'); + console.log('Buscando archivos para agregar header de licencia...\n'); let files = []; @@ -119,14 +119,14 @@ async function main() { // Remover duplicados y ordenar files = [...new Set(files)].sort(); - console.log(`📁 Encontrados ${files.length} archivos candidatos\n`); + console.log(`Encontrados ${files.length} archivos candidatos\n`); if (dryRun) { - console.log('🔍 MODO DRY-RUN - Solo mostrando archivos que serían modificados:\n'); + console.log('MODO DRY-RUN - Solo mostrando archivos que serían modificados:\n'); files.forEach((file) => { const content = fs.readFileSync(file, 'utf8'); if (!hasLicenseHeader(content)) { - console.log(` 📝 ${file}`); + console.log(` ${file}`); } }); return; @@ -143,10 +143,10 @@ async function main() { } } - console.log(`\n📊 Resumen:`); - console.log(` ✅ Archivos procesados: ${processedCount}`); - console.log(` ⏭️ Archivos saltados: ${skippedCount}`); - console.log(` 📁 Total de archivos: ${files.length}`); + console.log(`\nResumen:`); + console.log(` Archivos procesados: ${processedCount}`); + console.log(` Archivos saltados: ${skippedCount}`); + console.log(` Total de archivos: ${files.length}`); } // Mostrar ayuda diff --git a/scripts/check-license-header.js b/scripts/check-license-header.js index ab3e88a1..bbc2ee69 100644 --- a/scripts/check-license-header.js +++ b/scripts/check-license-header.js @@ -42,7 +42,7 @@ function checkHeaderInFile(filePath) { const content = fs.readFileSync(filePath, 'utf8'); return hasLicenseHeader(content); } catch (error) { - console.error(`❌ Error leyendo ${filePath}:`, error.message); + console.error(`Error leyendo ${filePath}:`, error.message); return false; } } @@ -53,7 +53,7 @@ async function main() { const specificPath = args.find((arg) => !arg.startsWith('--')); const exitOnMissing = args.includes('--exit-on-missing') || args.includes('-e'); - console.log('🔍 Verificando headers de licencia...\n'); + console.log('Verificando headers de licencia...\n'); let files = []; @@ -89,7 +89,7 @@ async function main() { // Remover duplicados y ordenar files = [...new Set(files)].sort(); - console.log(`📁 Verificando ${files.length} archivos...\n`); + console.log(`Verificando ${files.length} archivos...\n`); let withHeader = 0; let withoutHeader = 0; @@ -98,34 +98,34 @@ async function main() { for (const file of files) { if (checkHeaderInFile(file)) { withHeader++; - console.log(`✅ ${file}`); + console.log(`${file}`); } else { withoutHeader++; missingHeaderFiles.push(file); - console.log(`❌ ${file}`); + console.log(`${file}`); } } - console.log(`\n📊 Resumen:`); - console.log(` ✅ Con header: ${withHeader}`); - console.log(` ❌ Sin header: ${withoutHeader}`); - console.log(` 📁 Total archivos: ${files.length}`); + console.log(`\nResumen:`); + console.log(` Con header: ${withHeader}`); + console.log(` Sin header: ${withoutHeader}`); + console.log(` Total archivos: ${files.length}`); if (withoutHeader > 0) { - console.log(`\n📝 Archivos sin header de licencia:`); + console.log(`\nArchivos sin header de licencia:`); missingHeaderFiles.forEach((file) => { console.log(` - ${file}`); }); - console.log(`\n💡 Para agregar headers automáticamente:`); + console.log(`\nPara agregar headers automáticamente:`); console.log(` npm run license`); if (exitOnMissing) { - console.log(`\n❌ Exiting with error code due to missing license headers.`); + console.log(`\nExiting with error code due to missing license headers.`); process.exit(1); } } else { - console.log(`\n🎉 ¡Todos los archivos tienen header de licencia!`); + console.log(`\n¡Todos los archivos tienen header de licencia!`); } } diff --git a/scripts/test-analytics-webhook-jwt.js b/scripts/test-analytics-webhook-jwt.js index 32bf76a5..1417d380 100644 --- a/scripts/test-analytics-webhook-jwt.js +++ b/scripts/test-analytics-webhook-jwt.js @@ -288,22 +288,22 @@ function generateDateRange(startDate, endDate, storeId) { * Función principal de testing con datos mock completos */ async function main() { - console.log('📊 Poblando Base de Datos con Datos Mock de Analíticas\n'); + console.log('Poblando Base de Datos con Datos Mock de Analíticas\n'); // 1. Generar token de prueba console.log('1. Generando JWT token...'); const testToken = generateTestToken('9441866', 'ORDER_CREATED'); - console.log(`✅ Token generado: ${testToken.substring(0, 50)}...\n`); + console.log(`Token generado: ${testToken.substring(0, 50)}...\n`); // 2. Verificar token console.log('2. Verificando JWT token...'); const verification = verifyTestToken(testToken); if (verification.isValid) { - console.log('✅ Token válido'); + console.log('Token válido'); console.log(` Store ID: ${verification.payload.storeId}`); console.log(` Event Type: ${verification.payload.eventType}\n`); } else { - console.log(`❌ Token inválido: ${verification.error}\n`); + console.log(`Token inválido: ${verification.error}\n`); return; } @@ -342,11 +342,11 @@ async function main() { successCount++; if (successCount % 10 === 0) { // Mostrar progreso cada 10 eventos exitosos - console.log(` ✅ Procesados ${successCount} eventos...`); + console.log(` Procesados ${successCount} eventos...`); } } else { errorCount++; - console.log(` ❌ ${event.type}: Error - Status: ${webhookResult.status || 'N/A'}`); + console.log(` ${event.type}: Error - Status: ${webhookResult.status || 'N/A'}`); if (webhookResult.error) { console.log(` Error: ${webhookResult.error}`); } @@ -364,18 +364,18 @@ async function main() { await new Promise((resolve) => setTimeout(resolve, 50)); } - console.log(`\n📈 Resumen de inserción:`); - console.log(` ✅ Exitosos: ${successCount}`); - console.log(` ❌ Errores: ${errorCount}`); - console.log(` 📊 Total: ${allEvents.length}`); + console.log(`\nResumen de inserción:`); + console.log(` Exitosos: ${successCount}`); + console.log(` Errores: ${errorCount}`); + console.log(` Total: ${allEvents.length}`); - console.log(`\n📋 Eventos por tipo:`); + console.log(`\nEventos por tipo:`); Object.entries(eventTypeCounts).forEach(([type, count]) => { console.log(` ${type}: ${count} eventos`); }); if (successCount > 0) { - console.log(`\n🎉 ¡Base de datos poblada exitosamente!`); + console.log(`\n¡Base de datos poblada exitosamente!`); console.log(` Ahora puedes probar el hook useStoreAnalytics con datos reales.`); console.log(` Los eventos procesados generarán automáticamente las analíticas correspondientes.`); } @@ -398,11 +398,11 @@ async function main() { const expiredVerification = verifyTestToken(expiredToken); if (!expiredVerification.isValid) { - console.log(`✅ Token expirado correctamente rechazado: ${expiredVerification.error}`); + console.log(`Token expirado correctamente rechazado: ${expiredVerification.error}`); } // 6. Probar token con audiencia incorrecta - console.log('\n5. Probando token con audiencia incorrecta...'); + console.log('\nProbando token con audiencia incorrecta...'); const wrongAudienceToken = jwt.sign( { storeId: '9441866', @@ -418,10 +418,10 @@ async function main() { const wrongAudienceVerification = verifyTestToken(wrongAudienceToken); if (!wrongAudienceVerification.isValid) { - console.log(`✅ Token con audiencia incorrecta rechazado: ${wrongAudienceVerification.error}`); + console.log(`Token con audiencia incorrecta rechazado: ${wrongAudienceVerification.error}`); } - console.log('\n🎉 Test completado!'); + console.log('\nTest completado!'); } // Ejecutar si es llamado directamente diff --git a/scripts/test-email-system.js b/scripts/test-email-system.js index cb6b3d5f..e68e27f5 100644 --- a/scripts/test-email-system.js +++ b/scripts/test-email-system.js @@ -20,8 +20,8 @@ function getApiUrl() { return null; } catch (error) { - console.error('❌ Error leyendo amplify_outputs.json:', error.message); - console.log('💡 Asegúrate de que el sandbox esté ejecutándose: npx ampx sandbox'); + console.error('Error leyendo amplify_outputs.json:', error.message); + console.log('Asegúrate de que el sandbox esté ejecutándose: npx ampx sandbox'); process.exit(1); } } @@ -29,15 +29,15 @@ function getApiUrl() { const API_BASE_URL = getApiUrl(); if (!API_BASE_URL) { - console.error('❌ No se encontró la URL del API Gateway en amplify_outputs.json'); - console.log('💡 Asegúrate de que el sandbox esté ejecutándose: npx ampx sandbox'); + console.error('No se encontró la URL del API Gateway en amplify_outputs.json'); + console.log('Asegúrate de que el sandbox esté ejecutándose: npx ampx sandbox'); process.exit(1); } -console.log(`🌐 Usando API Gateway: ${API_BASE_URL}`); +console.log(`Usando API Gateway: ${API_BASE_URL}`); async function testEmailAPI() { - console.log('🧪 Probando sistema de emails con React Email...\n'); + console.log('Probando sistema de emails con React Email...\n'); // Test 1: Email de confirmación de pedido await testOrderConfirmation(); @@ -59,7 +59,7 @@ async function testEmailAPI() { } async function testOrderConfirmation() { - console.log('📧 Test 1: Confirmación de pedido'); + console.log('Test 1: Confirmación de pedido'); const testData = { templateId: 'order-confirmation', @@ -86,7 +86,7 @@ async function testOrderConfirmation() { } async function testShippingUpdate() { - console.log('📦 Test 2: Actualización de envío'); + console.log('Test 2: Actualización de envío'); const testData = { templateId: 'shipping-update', @@ -114,7 +114,7 @@ async function testShippingUpdate() { } async function testPromotion() { - console.log('🎉 Test 3: Email promocional'); + console.log('Test 3: Email promocional'); const testData = { templateId: 'promotion', @@ -145,7 +145,7 @@ async function testPromotion() { } async function testBulkEmail() { - console.log('📨 Test 4: Email masivo (SQS)'); + console.log('Test 4: Email masivo (SQS)'); const testData = { templateId: 'promotion', @@ -158,7 +158,7 @@ async function testBulkEmail() { ], templateVariables: { customerName: 'Cliente', - title: '🚀 React Email está funcionando!', + title: 'React Email está funcionando!', content: 'Este email fue generado con React Email + Tailwind y procesado por AWS Lambda.', discountCode: 'REACTEMAIL', discountPercentage: '25%', @@ -180,7 +180,7 @@ async function testBulkEmail() { async function sendTestEmail(data, testName) { try { console.log(` → Enviando ${testName}...`); - console.log(` 📡 URL: ${API_BASE_URL}/email/test-email`); + console.log(` URL: ${API_BASE_URL}/email/test-email`); const response = await fetch(`${API_BASE_URL}/email/test-email`, { method: 'POST', @@ -190,29 +190,29 @@ async function sendTestEmail(data, testName) { body: JSON.stringify(data), }); - console.log(` 📊 Status: ${response.status} ${response.statusText}`); + console.log(` Status: ${response.status} ${response.statusText}`); let result; try { result = await response.json(); - console.log(` 📄 Response:`, JSON.stringify(result, null, 2)); + console.log(` Response:`, JSON.stringify(result, null, 2)); } catch (parseError) { const text = await response.text(); - console.log(` 📄 Raw response:`, text); + console.log(` Raw response:`, text); result = { error: 'Invalid JSON response' }; } if (response.ok && result.success) { - console.log(` ✅ ${testName} enviado exitosamente`); + console.log(` ${testName} enviado exitosamente`); if (result.jobId) { - console.log(` 📝 Job ID: ${result.jobId}`); + console.log(` Job ID: ${result.jobId}`); } } else { - console.log(` ❌ Error en ${testName}:`, result.error || `HTTP ${response.status}`); + console.log(` Error en ${testName}:`, result.error || `HTTP ${response.status}`); } } catch (error) { - console.log(` ❌ Error de conexión en ${testName}:`, error.message); - console.log(` 🔍 Error details:`, error); + console.log(` Error de conexión en ${testName}:`, error.message); + console.log(` Error details:`, error); } console.log(''); // Línea en blanco @@ -221,7 +221,7 @@ async function sendTestEmail(data, testName) { async function sendBulkEmail(data, testName) { try { console.log(` → Enviando ${testName} a ${data.recipients.length} destinatarios...`); - console.log(` 📡 URL: ${API_BASE_URL}/email/send-bulk`); + console.log(` URL: ${API_BASE_URL}/email/send-bulk`); const response = await fetch(`${API_BASE_URL}/email/send-bulk`, { method: 'POST', @@ -231,33 +231,33 @@ async function sendBulkEmail(data, testName) { body: JSON.stringify(data), }); - console.log(` 📊 Status: ${response.status} ${response.statusText}`); + console.log(` Status: ${response.status} ${response.statusText}`); let result; try { result = await response.json(); - console.log(` 📄 Response:`, JSON.stringify(result, null, 2)); + console.log(` Response:`, JSON.stringify(result, null, 2)); } catch (parseError) { const text = await response.text(); - console.log(` 📄 Raw response:`, text); + console.log(` Raw response:`, text); result = { error: 'Invalid JSON response' }; } if (response.ok && result.success) { - console.log(` ✅ ${testName} encolado exitosamente`); - console.log(` 📊 ${data.recipients.length} emails en cola`); + console.log(` ${testName} encolado exitosamente`); + console.log(` ${data.recipients.length} emails en cola`); if (result.campaignId) { - console.log(` 🎯 Campaign ID: ${result.campaignId}`); + console.log(` Campaign ID: ${result.campaignId}`); } if (result.estimatedDelivery) { - console.log(` ⏱️ Entrega estimada: ${result.estimatedDelivery}`); + console.log(` Entrega estimada: ${result.estimatedDelivery}`); } } else { - console.log(` ❌ Error en ${testName}:`, result.error || `HTTP ${response.status}`); + console.log(` Error en ${testName}:`, result.error || `HTTP ${response.status}`); } } catch (error) { - console.log(` ❌ Error de conexión en ${testName}:`, error.message); - console.log(` 🔍 Error details:`, error); + console.log(` Error de conexión en ${testName}:`, error.message); + console.log(` Error details:`, error); } console.log(''); // Línea en blanco @@ -268,16 +268,16 @@ function delay(ms) { } // Ejecutar tests -console.log('🚀 Iniciando tests del sistema de emails...\n'); +console.log('Iniciando tests del sistema de emails...\n'); testEmailAPI() .then(() => { - console.log('✨ Tests completados!'); - console.log('\n📋 Qué revisar:'); + console.log('Tests completados!'); + console.log('\nQué revisar:'); console.log(' 1. Revisa tu bandeja de entrada'); console.log(' 2. Verifica que los emails se vean bien'); console.log(' 3. Comprueba que las variables se reemplazaron correctamente'); console.log(' 4. Confirma que los estilos de Tailwind funcionan'); }) .catch((error) => { - console.error('❌ Error en los tests:', error); + console.error('Error en los tests:', error); }); diff --git a/scripts/upload-base-template.js b/scripts/upload-base-template.js index f2c2b662..82cb6eec 100644 --- a/scripts/upload-base-template.js +++ b/scripts/upload-base-template.js @@ -142,10 +142,10 @@ async function readFilterModules() { } } } catch (cssError) { - console.warn('⚠️ No se encontró el directorio de CSS de filtros:', cssDir); + console.warn('No se encontró el directorio de CSS de filtros:', cssDir); } } catch (error) { - console.warn('⚠️ No se encontró el directorio de módulos de filtros:', FILTER_MODULES_DIR); + console.warn('No se encontró el directorio de módulos de filtros:', FILTER_MODULES_DIR); } return files; @@ -170,14 +170,14 @@ async function uploadTemplatesToS3(files) { try { await s3Client.send(command); - console.log(`✅ Subido: ${key}`); + console.log(`Subido: ${key}`); return { key, path: file.path, size: file.isBinaryFile ? file.content.length : Buffer.byteLength(file.content, 'utf-8'), }; } catch (error) { - console.error(`❌ Error al subir ${key}:`, error); + console.error(`Error al subir ${key}:`, error); throw error; } }); @@ -204,14 +204,14 @@ async function uploadFilterModulesToS3(files) { try { await s3Client.send(command); - console.log(`✅ Subido: ${key}`); + console.log(`Subido: ${key}`); return { key, path: file.path, size: Buffer.byteLength(file.content, 'utf-8'), }; } catch (error) { - console.error(`❌ Error al subir ${key}:`, error); + console.error(`Error al subir ${key}:`, error); throw error; } }); @@ -221,7 +221,7 @@ async function uploadFilterModulesToS3(files) { async function invalidateCloudFrontCache() { if (!CLOUDFRONT_DISTRIBUTION_ID) { - console.warn('⚠️ CLOUDFRONT_DISTRIBUTION_ID no configurado, saltando invalidación'); + console.warn('CLOUDFRONT_DISTRIBUTION_ID no configurado, saltando invalidación'); return; } @@ -238,9 +238,9 @@ async function invalidateCloudFrontCache() { }); await cloudFrontClient.send(command); - console.log('✅ Cache de CloudFront invalidado'); + console.log('Cache de CloudFront invalidado'); } catch (error) { - console.error('❌ Error al invalidar cache de CloudFront:', error); + console.error('Error al invalidar cache de CloudFront:', error); } } @@ -254,29 +254,29 @@ async function main() { console.log(`Directorio de plantilla: ${TEMPLATE_DIR}`); console.log(`Directorio de módulos: ${FILTER_MODULES_DIR}`); - console.log('\n📁 Leyendo archivos de plantilla...'); + console.log('\nLeyendo archivos de plantilla...'); const templateFiles = await readTemplateFiles(); console.log(`Se encontraron ${templateFiles.length} archivos de plantilla.`); - console.log('\n🔧 Leyendo módulos de filtros...'); + console.log('\nLeyendo módulos de filtros...'); const filterModules = await readFilterModules(); console.log(`Se encontraron ${filterModules.length} módulos de filtros.`); - console.log('\n📤 Subiendo plantillas...'); + console.log('\nSubiendo plantillas...'); const templateResults = await uploadTemplatesToS3(templateFiles); - console.log('\n📤 Subiendo módulos de filtros...'); + console.log('\nSubiendo módulos de filtros...'); const moduleResults = await uploadFilterModulesToS3(filterModules); - console.log('\n✅ Subida completada con éxito!'); + console.log('\nSubida completada con éxito!'); console.log(`Se subieron ${templateResults.length} archivos de plantilla.`); console.log(`Se subieron ${moduleResults.length} módulos de filtros.`); - console.log(`\n🌐 Los módulos están disponibles en: https://cdn.fasttify.com/assets/`); + console.log(`\nLos módulos están disponibles en: https://cdn.fasttify.com/assets/`); - console.log('\n🔄 Invalidando cache de CloudFront...'); + console.log('\nInvalidando cache de CloudFront...'); await invalidateCloudFrontCache(); } catch (error) { - console.error('❌ Error durante la subida:', error); + console.error('Error durante la subida:', error); process.exit(1); } } diff --git a/test/unit/components/StoreSelector.test.tsx b/test/unit/components/StoreSelector.test.tsx index 77dba552..06188ce3 100644 --- a/test/unit/components/StoreSelector.test.tsx +++ b/test/unit/components/StoreSelector.test.tsx @@ -1,14 +1,14 @@ import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { StoreSelector } from '@/app/(setup-layout)/my-store/components/StoreSelector'; +import { StoreSelector } from '@/app/(setup)/my-store/components/StoreSelector'; import { useAuth } from '@/context/hooks/useAuth'; -import { useUserStores } from '@/app/(setup-layout)/my-store/hooks/useUserStores'; +import { useUserStores } from '@/app/(setup)/my-store/hooks/useUserStores'; jest.mock('@/context/hooks/useAuth', () => ({ useAuth: jest.fn(), })); -jest.mock('@/app/(setup-layout)/my-store/hooks/useUserStores', () => ({ +jest.mock('@/app/(setup)/my-store/hooks/useUserStores', () => ({ useUserStores: jest.fn(), })); diff --git a/test/unit/hooks/SignIn.test.ts b/test/unit/hooks/SignIn.test.ts index 1496d69c..3b372b03 100644 --- a/test/unit/hooks/SignIn.test.ts +++ b/test/unit/hooks/SignIn.test.ts @@ -1,4 +1,4 @@ -import { useSignIn } from '@/app/(setup-layout)/login/hooks/SignIn'; +import { useSignIn } from '@/app/(setup)/login/hooks/SignIn'; import { act, renderHook } from '@testing-library/react'; import { resendSignUpCode, signIn } from 'aws-amplify/auth'; diff --git a/test/unit/hooks/signUp.test.ts b/test/unit/hooks/signUp.test.ts index 8e93c023..0ed2c579 100644 --- a/test/unit/hooks/signUp.test.ts +++ b/test/unit/hooks/signUp.test.ts @@ -1,4 +1,4 @@ -import { handleSignUp, handleConfirmSignUp } from '@/app/(setup-layout)/login/hooks/signUp'; +import { handleSignUp, handleConfirmSignUp } from '@/app/(setup)/login/hooks/signUp'; import { signUp, confirmSignUp } from 'aws-amplify/auth'; // Mock de los módulos de AWS Amplify diff --git a/test/unit/hooks/useResetPassword.test.ts b/test/unit/hooks/useResetPassword.test.ts index d07f4105..e7f5152a 100644 --- a/test/unit/hooks/useResetPassword.test.ts +++ b/test/unit/hooks/useResetPassword.test.ts @@ -1,5 +1,5 @@ import { renderHook, act } from '@testing-library/react'; -import { useResetPassword } from '@/app/(setup-layout)/login/hooks/useResetPassword'; +import { useResetPassword } from '@/app/(setup)/login/hooks/useResetPassword'; import { resetPassword } from 'aws-amplify/auth'; // Mock del módulo de AWS Amplify diff --git a/test/unit/integration/auth-flow.test.ts b/test/unit/integration/auth-flow.test.ts index fe26ddea..6620be2f 100644 --- a/test/unit/integration/auth-flow.test.ts +++ b/test/unit/integration/auth-flow.test.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getSession } from '@/middlewares/auth/auth'; +import { getSession, type AuthSession } from '@/middlewares/auth/auth'; import { runWithAmplifyServerContext } from '@/utils/client/AmplifyUtils'; import { fetchAuthSession } from 'aws-amplify/auth/server'; @@ -31,9 +31,8 @@ describe('Auth Flow Integration Tests', () => { let mockRequest: NextRequest; let mockResponse: NextResponse; - const mockSession = { + const mockSession: AuthSession = { tokens: { - accessToken: { toString: () => 'mock-access-token' }, idToken: { payload: { 'cognito:username': 'test-user-123', 'custom:plan': 'Royal' } }, }, }; @@ -152,9 +151,9 @@ describe('Auth Flow Integration Tests', () => { describe('Consistencia de datos entre middlewares', () => { it('debe mantener consistencia de datos entre middlewares', async () => { // Simular múltiples middlewares que acceden a la misma sesión - const session1 = await getSession(mockRequest, mockResponse, false); - const session2 = await getSession(mockRequest, mockResponse, false); - const session3 = await getSession(mockRequest, mockResponse, false); + const session1: AuthSession | null = await getSession(mockRequest, mockResponse, false); + const session2: AuthSession | null = await getSession(mockRequest, mockResponse, false); + const session3: AuthSession | null = await getSession(mockRequest, mockResponse, false); // Verificar que todos los middlewares ven los mismos datos expect(session1).toEqual(session2); diff --git a/test/unit/middlewares/auth.test.ts b/test/unit/middlewares/auth.test.ts index fc3d633a..ce7ccba3 100644 --- a/test/unit/middlewares/auth.test.ts +++ b/test/unit/middlewares/auth.test.ts @@ -135,7 +135,7 @@ describe('Auth Middleware', () => { const result = await handleAuthenticationMiddleware(mockRequest, mockResponse); - expect(result).toBe(mockResponse); + expect(result).toBeNull(); expect(NextResponse.redirect).not.toHaveBeenCalled(); }); diff --git a/tsconfig.base.json b/tsconfig.base.json index e9e4c7de..f93fd080 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -8,6 +8,7 @@ "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", + "tsBuildInfoFile": ".tsbuildinfo/tsbuildinfo", "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", diff --git a/tsconfig.json b/tsconfig.json index 6920ed4f..9725b4f1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,9 +3,15 @@ "compilerOptions": { "noEmit": true, "forceConsistentCasingInFileNames": false, - "jsx": "preserve" + "jsx": "preserve", + "tsBuildInfoFile": ".tsbuildinfo/tsbuildinfo", + "assumeChangesOnlyAffectDirectDependencies": true }, - "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], + "ts-node": { + "transpileOnly": true, + "files": false + }, + "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx", "**/*.test.ts"], "exclude": [ "node_modules", ".next/cache/**/*",