From 385bd2ca6fc92ede06a60f19cc5db807a6871692 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 11 Oct 2025 17:47:45 -0500 Subject: [PATCH 01/20] Add store limits validation API and integrate into backend This commit introduces a new API for validating store limits, enhancing the backend functionality. The `validateStoreLimits` function is imported and integrated into the backend configuration, allowing for the creation of a dedicated REST API endpoint. Additionally, the necessary permissions and outputs for the new API are established, ensuring proper access control and visibility within the system. Updates to various components reflect the integration of this new feature, improving overall store management capabilities. --- amplify/auth/resource.ts | 2 + amplify/backend.ts | 14 +- amplify/config/apis.ts | 23 +++ amplify/config/outputs.ts | 8 + amplify/data/resource.ts | 3 + amplify/functions/storeImages/resource.ts | 2 + .../storeImages/services/s3-service.ts | 12 +- .../functions/storeImages/services/utils.ts | 10 +- .../functions/validateStoreLimits/handler.ts | 186 ++++++++++++++++++ .../functions/validateStoreLimits/resource.ts | 12 ++ .../first-steps/hooks/useStoreCreation.ts | 8 + .../hooks/useStoreLimitsValidator.ts | 83 ++++++++ .../sidebar/components/NavigationPolaris.tsx | 18 +- .../sidebar/components/TopBarPolaris.tsx | 2 +- lib/amplify-client.ts | 11 ++ middlewares/auth/auth.ts | 2 +- scripts/add-license-header.js | 22 +-- scripts/check-license-header.js | 26 +-- scripts/test-analytics-webhook-jwt.js | 32 +-- scripts/test-email-system.js | 70 +++---- scripts/upload-base-template.js | 34 ++-- 21 files changed, 460 insertions(+), 120 deletions(-) create mode 100644 amplify/functions/validateStoreLimits/handler.ts create mode 100644 amplify/functions/validateStoreLimits/resource.ts create mode 100644 app/(setup-layout)/first-steps/hooks/useStoreLimitsValidator.ts 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/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/app/(setup-layout)/first-steps/hooks/useStoreCreation.ts b/app/(setup-layout)/first-steps/hooks/useStoreCreation.ts index f0a98dad..8cbf8c0a 100644 --- a/app/(setup-layout)/first-steps/hooks/useStoreCreation.ts +++ b/app/(setup-layout)/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-layout)/first-steps/hooks/useStoreLimitsValidator.ts b/app/(setup-layout)/first-steps/hooks/useStoreLimitsValidator.ts new file mode 100644 index 00000000..229980a5 --- /dev/null +++ b/app/(setup-layout)/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/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/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/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); } } From 331866ee86ba6d8eed94df468f9afa06f1b5ec11 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 11 Oct 2025 17:50:43 -0500 Subject: [PATCH 02/20] Update auth middleware test to expect null response on authentication failure This commit modifies the authentication middleware test to assert that the result is null when authentication fails, ensuring that the test accurately reflects the expected behavior of the middleware. This change enhances the reliability of the test suite by aligning it with the intended functionality of the authentication process. --- test/unit/middlewares/auth.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); }); From 34b0f79ebfe1a61ad528143e0f61ebc474a49860 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 11 Oct 2025 20:20:16 -0500 Subject: [PATCH 03/20] Enhance image selector functionality with filters and sorting options This commit introduces several improvements to the image selector component, including the addition of new hooks for image filtering and debouncing search input. The `SearchAndFilters` component has been updated to support multiple filter options, and the `ImageSelectorModal` now reflects the filtered images based on user input. Additionally, the `ImageGallery` component has been refactored to utilize a new `ImageCard` component for better code organization. Overall, these changes enhance the user experience by providing more robust image management capabilities. --- .../components/FilterCombobox.tsx | 118 ++++++++++ .../components/FilterPopover.tsx | 130 +++++++++++ .../images-selector/components/ImageCard.tsx | 216 ++++++++++++++++++ .../components/ImageGallery.tsx | 104 ++------- .../components/ImageSelectorModal.tsx | 52 ++++- .../components/SearchAndFilters.tsx | 152 ++++++++++-- .../components/SortPopover.tsx | 58 +++++ .../images-selector/hooks/useDebounce.ts | 23 ++ .../images-selector/hooks/useImageFilters.ts | 148 ++++++++++++ app/store/components/images-selector/index.ts | 9 + .../images-selector/utils/filterUtils.ts | 178 +++++++++++++++ next.config.ts | 2 + 12 files changed, 1085 insertions(+), 105 deletions(-) create mode 100644 app/store/components/images-selector/components/FilterCombobox.tsx create mode 100644 app/store/components/images-selector/components/FilterPopover.tsx create mode 100644 app/store/components/images-selector/components/ImageCard.tsx create mode 100644 app/store/components/images-selector/components/SortPopover.tsx create mode 100644 app/store/components/images-selector/hooks/useDebounce.ts create mode 100644 app/store/components/images-selector/hooks/useImageFilters.ts create mode 100644 app/store/components/images-selector/utils/filterUtils.ts 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/next.config.ts b/next.config.ts index 5e25ae13..487121ef 100644 --- a/next.config.ts +++ b/next.config.ts @@ -21,6 +21,8 @@ const nextConfig: NextConfig = { unoptimized: process.env.NODE_ENV === 'development', + qualities: [25, 50, 75, 85, 90, 95, 100], + localPatterns: [ { pathname: '/icons/**', From 051c2b988954ead9a973143bf0f5b425decb7d58 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 11 Oct 2025 23:53:06 -0500 Subject: [PATCH 04/20] Enhance AI chat functionality with new features and improvements This commit introduces several enhancements to the AI chat components, including the addition of new libraries for Markdown rendering and syntax highlighting. The `TypingMessage` component now supports formatted messages using `react-markdown`, allowing for better presentation of AI responses. The `AIInputWithSearch` component has been updated to include a stop button for ongoing responses, improving user control during interactions. Additionally, various UI adjustments have been made to enhance the overall user experience, including updated empty state messages and improved loading indicators. These changes collectively enhance the functionality and usability of the AI chat interface. --- .../functions/chat-generate/systemPrompt.ts | 147 ++- .../description-generate/systemPrompt.ts | 157 +-- .../components/ai-chat/components/AiInput.tsx | 30 +- .../ai-chat/components/ChatHeader.tsx | 1 + .../ai-chat/components/EmptyState.tsx | 20 +- .../ai-chat/components/MessageList.tsx | 2 +- .../ai-chat/components/MessageLoading.tsx | 48 +- .../ai-chat/components/RefinedAiAssistant.tsx | 8 +- .../ai-chat/components/TypingMessage.tsx | 106 +- .../ai-chat/constants/chat-constants.ts | 7 +- .../ai-chat/hooks/useConversation.ts | 42 + .../components/ai-chat/hooks/useSimpleChat.ts | 10 + .../ai-chat/styles/chat-markdown.css | 209 ++++ .../hooks/storage/useProductImageUpload.ts | 6 +- app/store/hooks/storage/useS3Images.ts | 207 ++-- package.json | 3 + pnpm-lock.yaml | 906 ++++++++++++++++++ 17 files changed, 1657 insertions(+), 252 deletions(-) create mode 100644 app/store/components/ai-chat/styles/chat-markdown.css 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/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/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/package.json b/package.json index 8d9ead30..1936050f 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,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: {} From fa174a6f0dccb1833249a2dc135a25e92d63b2a2 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 14:00:01 -0500 Subject: [PATCH 05/20] Remove deprecated layout and page components from main layout and pricing sections This commit deletes several unused layout and page components from the main layout and pricing sections, including `layout.tsx`, `page.tsx`, and various landing components. The removal of these files streamlines the codebase and eliminates unnecessary dependencies, contributing to improved maintainability and performance of the application. --- .../landing/components/DocsLanding.tsx | 41 ------------------- app/(main-layout)/page.tsx | 7 ---- .../components/MultiStepLoader.tsx | 0 .../first-steps/components/PersonalInfo.tsx | 0 .../first-steps/components/StoreInfo.tsx | 0 .../first-steps/data/selling-options.json | 0 .../first-steps/hooks/useFirstStepsSetup.ts | 12 +++--- .../first-steps/hooks/useFormManager.ts | 0 .../first-steps/hooks/useStepManager.ts | 0 .../first-steps/hooks/useStoreCreation.ts | 0 .../hooks/useStoreLimitsValidator.ts | 0 .../first-steps/hooks/useTemplateUpload.ts | 0 .../first-steps/hooks/useUserStoreData.ts | 0 .../first-steps/layout.tsx | 0 .../first-steps/page.tsx | 10 ++--- app/{(setup-layout) => (setup)}/layout.tsx | 0 app/(setup)/login/LoginClient.tsx | 7 ++++ .../forgot-password/ForgotPasswordForm.tsx | 0 .../components/main-components/AuthClient.tsx | 4 +- .../components/main-components/AuthForm.tsx | 8 ++-- .../main-components/ImageSlider.tsx | 2 +- .../login/components/sing-in/SignInForm.tsx | 2 +- .../login/components/sing-up/SignUpForm.tsx | 2 +- .../verification-form/VerificationForm.tsx | 2 +- .../login/hooks/SignIn.ts | 0 .../login/hooks/signUp.ts | 0 .../login/hooks/useResetPassword.ts | 0 .../login/page.tsx | 0 .../components/AnimatedBackground.tsx | 0 .../my-store/components/StoreAvatar.tsx | 0 .../my-store/components/StoreSelector.tsx | 4 +- .../my-store/hooks/useUserStores.ts | 0 .../my-store/page.tsx | 4 +- app/(setup-layout)/login/LoginClient.tsx | 7 ---- .../landing/components/AboutUs.tsx | 0 app/(www)/landing/components/DocsLanding.tsx | 41 +++++++++++++++++++ .../landing/components/DropshippingIntro.tsx | 0 .../landing/components/FashionSlider.tsx | 0 .../landing/components/Feature.tsx | 0 .../landing/components/FirstView.tsx | 0 .../landing/components/Footer.tsx | 0 .../landing/components/LogoCarousell.tsx | 0 .../landing/components/MarqueeLogos.tsx | 0 .../landing/components/NavBar.tsx | 2 +- .../landing/components/Platform.tsx | 0 .../landing/components/StepGuide.tsx | 0 .../landing/components/Testimonials.tsx | 0 .../landing/components/navigation.ts | 0 app/{(main-layout) => (www)}/layout.tsx | 2 +- app/(www)/page.tsx | 7 ++++ .../pricing/components/FAQItem.tsx | 0 .../pricing/components/FAQSection.tsx | 0 .../pricing/components/FeatureComparison.tsx | 0 .../pricing/components/PricingCard.tsx | 0 .../pricing/components/plans.ts | 0 .../pricing/layout.tsx | 0 app/{(main-layout) => (www)}/pricing/page.tsx | 12 +++--- .../terms/components/LegalDocuments.tsx | 2 +- .../terms/components/legal-content.json | 0 app/{(main-layout) => (www)}/terms/page.tsx | 2 +- .../domains/components/ChangeDomainDialog.tsx | 2 +- .../components/EditStoreProfileDialog.tsx | 2 +- .../components/PaymentGatewayCard.tsx | 2 +- .../components/PaymentMethodsSection.tsx | 2 +- .../payments/hooks/usePaymentSettings.ts | 2 +- .../store-config/components/LogoUploader.tsx | 2 +- .../store-setup/components/EcommerceSetup.tsx | 2 +- .../store-setup/components/PricingDrawer.tsx | 4 +- 68 files changed, 99 insertions(+), 99 deletions(-) delete mode 100644 app/(main-layout)/landing/components/DocsLanding.tsx delete mode 100644 app/(main-layout)/page.tsx rename app/{(setup-layout) => (setup)}/first-steps/components/MultiStepLoader.tsx (100%) rename app/{(setup-layout) => (setup)}/first-steps/components/PersonalInfo.tsx (100%) rename app/{(setup-layout) => (setup)}/first-steps/components/StoreInfo.tsx (100%) rename app/{(setup-layout) => (setup)}/first-steps/data/selling-options.json (100%) rename app/{(setup-layout) => (setup)}/first-steps/hooks/useFirstStepsSetup.ts (88%) rename app/{(setup-layout) => (setup)}/first-steps/hooks/useFormManager.ts (100%) rename app/{(setup-layout) => (setup)}/first-steps/hooks/useStepManager.ts (100%) rename app/{(setup-layout) => (setup)}/first-steps/hooks/useStoreCreation.ts (100%) rename app/{(setup-layout) => (setup)}/first-steps/hooks/useStoreLimitsValidator.ts (100%) rename app/{(setup-layout) => (setup)}/first-steps/hooks/useTemplateUpload.ts (100%) rename app/{(setup-layout) => (setup)}/first-steps/hooks/useUserStoreData.ts (100%) rename app/{(setup-layout) => (setup)}/first-steps/layout.tsx (100%) rename app/{(setup-layout) => (setup)}/first-steps/page.tsx (94%) rename app/{(setup-layout) => (setup)}/layout.tsx (100%) create mode 100644 app/(setup)/login/LoginClient.tsx rename app/{(setup-layout) => (setup)}/login/components/forgot-password/ForgotPasswordForm.tsx (100%) rename app/{(setup-layout) => (setup)}/login/components/main-components/AuthClient.tsx (64%) rename app/{(setup-layout) => (setup)}/login/components/main-components/AuthForm.tsx (94%) rename app/{(setup-layout) => (setup)}/login/components/main-components/ImageSlider.tsx (98%) rename app/{(setup-layout) => (setup)}/login/components/sing-in/SignInForm.tsx (98%) rename app/{(setup-layout) => (setup)}/login/components/sing-up/SignUpForm.tsx (98%) rename app/{(setup-layout) => (setup)}/login/components/verification-form/VerificationForm.tsx (97%) rename app/{(setup-layout) => (setup)}/login/hooks/SignIn.ts (100%) rename app/{(setup-layout) => (setup)}/login/hooks/signUp.ts (100%) rename app/{(setup-layout) => (setup)}/login/hooks/useResetPassword.ts (100%) rename app/{(setup-layout) => (setup)}/login/page.tsx (100%) rename app/{(setup-layout) => (setup)}/my-store/components/AnimatedBackground.tsx (100%) rename app/{(setup-layout) => (setup)}/my-store/components/StoreAvatar.tsx (100%) rename app/{(setup-layout) => (setup)}/my-store/components/StoreSelector.tsx (97%) rename app/{(setup-layout) => (setup)}/my-store/hooks/useUserStores.ts (100%) rename app/{(setup-layout) => (setup)}/my-store/page.tsx (66%) delete mode 100644 app/(setup-layout)/login/LoginClient.tsx rename app/{(main-layout) => (www)}/landing/components/AboutUs.tsx (100%) create mode 100644 app/(www)/landing/components/DocsLanding.tsx rename app/{(main-layout) => (www)}/landing/components/DropshippingIntro.tsx (100%) rename app/{(main-layout) => (www)}/landing/components/FashionSlider.tsx (100%) rename app/{(main-layout) => (www)}/landing/components/Feature.tsx (100%) rename app/{(main-layout) => (www)}/landing/components/FirstView.tsx (100%) rename app/{(main-layout) => (www)}/landing/components/Footer.tsx (100%) rename app/{(main-layout) => (www)}/landing/components/LogoCarousell.tsx (100%) rename app/{(main-layout) => (www)}/landing/components/MarqueeLogos.tsx (100%) rename app/{(main-layout) => (www)}/landing/components/NavBar.tsx (99%) rename app/{(main-layout) => (www)}/landing/components/Platform.tsx (100%) rename app/{(main-layout) => (www)}/landing/components/StepGuide.tsx (100%) rename app/{(main-layout) => (www)}/landing/components/Testimonials.tsx (100%) rename app/{(main-layout) => (www)}/landing/components/navigation.ts (100%) rename app/{(main-layout) => (www)}/layout.tsx (81%) create mode 100644 app/(www)/page.tsx rename app/{(main-layout) => (www)}/pricing/components/FAQItem.tsx (100%) rename app/{(main-layout) => (www)}/pricing/components/FAQSection.tsx (100%) rename app/{(main-layout) => (www)}/pricing/components/FeatureComparison.tsx (100%) rename app/{(main-layout) => (www)}/pricing/components/PricingCard.tsx (100%) rename app/{(main-layout) => (www)}/pricing/components/plans.ts (100%) rename app/{(main-layout) => (www)}/pricing/layout.tsx (100%) rename app/{(main-layout) => (www)}/pricing/page.tsx (71%) rename app/{(main-layout) => (www)}/terms/components/LegalDocuments.tsx (97%) rename app/{(main-layout) => (www)}/terms/components/legal-content.json (100%) rename app/{(main-layout) => (www)}/terms/page.tsx (80%) 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 100% rename from app/(setup-layout)/first-steps/hooks/useStoreCreation.ts rename to app/(setup)/first-steps/hooks/useStoreCreation.ts diff --git a/app/(setup-layout)/first-steps/hooks/useStoreLimitsValidator.ts b/app/(setup)/first-steps/hooks/useStoreLimitsValidator.ts similarity index 100% rename from app/(setup-layout)/first-steps/hooks/useStoreLimitsValidator.ts rename to app/(setup)/first-steps/hooks/useStoreLimitsValidator.ts 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 100% rename from app/(main-layout)/landing/components/FashionSlider.tsx rename to app/(www)/landing/components/FashionSlider.tsx 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 100% rename from app/(main-layout)/landing/components/MarqueeLogos.tsx rename to app/(www)/landing/components/MarqueeLogos.tsx 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/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/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/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'; From dbae8b509d702fae2e0b56ae3f833603fd66c97a Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 14:06:30 -0500 Subject: [PATCH 06/20] Refactor import paths in test files to remove deprecated layout references This commit updates the import paths in several test files to reflect the removal of the deprecated layout structure. The changes affect the `StoreSelector`, `SignIn`, `signUp`, and `useResetPassword` test files, ensuring that they now point to the correct locations within the new application structure. This refactor improves code organization and maintains consistency across the test suite. --- test/unit/components/StoreSelector.test.tsx | 6 +++--- test/unit/hooks/SignIn.test.ts | 2 +- test/unit/hooks/signUp.test.ts | 2 +- test/unit/hooks/useResetPassword.test.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) 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 From 190b3e380da07a38b56fcb9a28aac30fb3769f52 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 15:25:50 -0500 Subject: [PATCH 07/20] Refactor auth flow tests to use typed AuthSession This commit updates the integration tests for the authentication flow by introducing the AuthSession type in the mock session and the getSession calls. This change enhances type safety and improves code clarity, ensuring that the tests accurately reflect the expected session structure. Additionally, it maintains consistency across the test suite by aligning with the updated session management logic. --- test/unit/integration/auth-flow.test.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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); From 7f3175f5dbdf8f6482280db0f2a4a7578a9abbb5 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 17:24:30 -0500 Subject: [PATCH 08/20] Update TypeScript configuration and GitHub Actions for improved build and type checking This commit enhances the TypeScript configuration by adding `tsBuildInfoFile` options for incremental builds and optimizing type checking. The GitHub Actions workflow is updated to include caching for the `.tsbuildinfo` directory and introduces environment variables for optimized type checking. Additionally, the `FashionSlider` component's description is updated to reflect a change in payment integration. These changes collectively improve build performance and maintainability. --- .github/workflows/build.yml | 15 ++++++++++++++- amplify/tsconfig.json | 3 +++ app/(www)/landing/components/FashionSlider.tsx | 2 +- tsconfig.base.json | 1 + tsconfig.json | 10 ++++++++-- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 07d7bc55..04a00354 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,13 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: "20" + cache: "pnpm" + + - 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 and build uses: actions/cache@v4 @@ -33,6 +39,7 @@ jobs: ~/.pnpm-store ${{ github.workspace }}/node_modules ${{ github.workspace }}/.next/cache + ${{ github.workspace }}/.tsbuildinfo key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} restore-keys: | ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}- @@ -74,5 +81,11 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Type check (optimized) + run: | + echo "🚀 Running optimized type check..." + time pnpm run type-check + echo "✅ Type check completed" + - name: Build project run: pnpm run build 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/(www)/landing/components/FashionSlider.tsx b/app/(www)/landing/components/FashionSlider.tsx index d9d448ce..2703f69f 100644 --- a/app/(www)/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/tsconfig.base.json b/tsconfig.base.json index e9e4c7de..bf61f95e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -8,6 +8,7 @@ "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", + "tsBuildInfoFile": ".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/**/*", From 9e9fca5217f37bb9377ff317543881ada7d2ec6d Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 17:24:55 -0500 Subject: [PATCH 09/20] Update GitHub Actions workflow to standardize Node.js setup syntax This commit modifies the Node.js setup section in the GitHub Actions workflow by changing the quotes from double to single for consistency. This minor adjustment enhances readability and maintains uniformity in the configuration file. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 04a00354..85506406 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,8 +24,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" - cache: "pnpm" + node-version: '20' + cache: 'pnpm' - name: Set Node.js optimization environment variables run: | From c76987b4ad2153a420631a49f20fa518851c5ea7 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 17:42:35 -0500 Subject: [PATCH 10/20] Enhance Next.js configuration and GitHub Actions for improved TypeScript support This commit adds TypeScript optimizations to the Next.js configuration by specifying the `tsconfigPath`. Additionally, the GitHub Actions workflow is updated to streamline the build process, incorporating type checking directly into the build step for better clarity and efficiency. These changes collectively enhance the development experience and maintainability of the project. --- .github/workflows/build.yml | 15 ++++++--------- next.config.ts | 5 +++++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85506406..a3f4e789 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,8 +24,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'pnpm' + node-version: "20" + cache: "pnpm" - name: Set Node.js optimization environment variables run: | @@ -81,11 +81,8 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Type check (optimized) + - name: Build project (includes type checking) run: | - echo "🚀 Running optimized type check..." - time pnpm run type-check - echo "✅ Type check completed" - - - name: Build project - run: pnpm run build + echo "Running Next.js build with type checking..." + time pnpm run build + echo "Build completed" diff --git a/next.config.ts b/next.config.ts index 487121ef..663ffa7b 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 + typescript: { + tsconfigPath: './tsconfig.json', + }, + serverExternalPackages: [ '@aws-sdk/client-acm', '@aws-sdk/client-bedrock-runtime', From 27f34bc65c9ec24d3f4d791afef2725eed41b000 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 17:43:02 -0500 Subject: [PATCH 11/20] Standardize quote usage in GitHub Actions workflow for Node.js setup This commit updates the Node.js setup section in the GitHub Actions workflow by changing the quotes from double to single. This minor adjustment enhances consistency and readability in the configuration file. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a3f4e789..4dcd841b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,8 +24,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" - cache: "pnpm" + node-version: '20' + cache: 'pnpm' - name: Set Node.js optimization environment variables run: | From 0fa3c889769070b25420503f2f00fe4ed100eea5 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 19:12:13 -0500 Subject: [PATCH 12/20] Refactor TypeScript build process and GitHub Actions for improved efficiency This commit updates the Next.js configuration to disable type checking during the build process, enhancing build speed. Additionally, it introduces new scripts in the package.json for faster type checking and building, and modifies the GitHub Actions workflow to separate type checking from the build step, improving clarity and efficiency in the CI/CD pipeline. --- .github/workflows/build.yml | 16 +++++++++++----- next.config.ts | 4 ++-- package.json | 3 +++ tsconfig.base.json | 2 +- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4dcd841b..2dbdf558 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,8 +24,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'pnpm' + node-version: "20" + cache: "pnpm" - name: Set Node.js optimization environment variables run: | @@ -81,8 +81,14 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Build project (includes type checking) + - name: Type check (separate) run: | - echo "Running Next.js build with type checking..." - time pnpm run build + 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/next.config.ts b/next.config.ts index 663ffa7b..48764e60 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,9 +2,9 @@ import type { NextConfig } from 'next'; /** @type {import('next').NextConfig} */ const nextConfig: NextConfig = { - // TypeScript optimizations + // TypeScript optimizations - disable type checking in build typescript: { - tsconfigPath: './tsconfig.json', + ignoreBuildErrors: true, // Disable type checking during build }, serverExternalPackages: [ diff --git a/package.json b/package.json index 1936050f..a8e02c5a 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 --incremental false", + "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", diff --git a/tsconfig.base.json b/tsconfig.base.json index bf61f95e..f93fd080 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -8,7 +8,7 @@ "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", - "tsBuildInfoFile": ".tsbuildinfo", + "tsBuildInfoFile": ".tsbuildinfo/tsbuildinfo", "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", From 5e4afc2b94c2ea0bc55dc2dc12ad9b5b20839cd6 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 19:12:45 -0500 Subject: [PATCH 13/20] Standardize quote usage in GitHub Actions workflow for Node.js setup This commit updates the Node.js setup section in the GitHub Actions workflow by changing the quotes from double to single, ensuring consistency and improving readability in the configuration file. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2dbdf558..fe06b70e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,8 +24,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" - cache: "pnpm" + node-version: '20' + cache: 'pnpm' - name: Set Node.js optimization environment variables run: | From a6321c76c7f0991c50aecde0a29aaa32b7edcfec Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 19:27:38 -0500 Subject: [PATCH 14/20] Refactor import statement in MarqueeLogos component for consistency This commit updates the import statement for the Image component in MarqueeLogos.tsx by removing the unnecessary line break, enhancing code readability and maintaining consistency in import formatting. --- app/(www)/landing/components/MarqueeLogos.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/(www)/landing/components/MarqueeLogos.tsx b/app/(www)/landing/components/MarqueeLogos.tsx index 41969194..13f986ef 100644 --- a/app/(www)/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'; From 2caceea5c268903a0701b0bc71a0b238c62e5403 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 19:51:08 -0500 Subject: [PATCH 15/20] Update type-check script in package.json and standardize quotes in GitHub Actions workflow This commit modifies the type-check script in package.json by removing the unnecessary `--incremental false` flag, simplifying the command. Additionally, it standardizes the quote usage in the GitHub Actions workflow for Node.js setup, enhancing consistency and readability across configuration files. --- .github/workflows/build.yml | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe06b70e..2dbdf558 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,8 +24,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'pnpm' + node-version: "20" + cache: "pnpm" - name: Set Node.js optimization environment variables run: | diff --git a/package.json b/package.json index a8e02c5a..8cdb8d95 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "test:coverage": "jest --coverage", "template-sync": "node scripts/template-sync.js", "type-check": "tsc --noEmit", - "type-check:fast": "tsc --noEmit --skipLibCheck --incremental false", + "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", From c94ea75bae588726d68815d3cd0c7d3c4fdb4e3e Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 19:51:37 -0500 Subject: [PATCH 16/20] Standardize quote usage in GitHub Actions workflow for Node.js setup This commit updates the Node.js setup section in the GitHub Actions workflow by changing the quotes from double to single, ensuring consistency and improving readability in the configuration file. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2dbdf558..fe06b70e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,8 +24,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" - cache: "pnpm" + node-version: '20' + cache: 'pnpm' - name: Set Node.js optimization environment variables run: | From 785284956c73106d8de7546a6a4b66105469e464 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 19:58:50 -0500 Subject: [PATCH 17/20] Enhance Amplify build process with TypeScript optimizations and improved logging This commit updates the Amplify configuration by adding environment variables for TypeScript optimization, changing the build command to a faster variant, and including logging statements to indicate the build process stages. Additionally, it includes the `.tsbuildinfo` directory in the artifacts paths for better caching and build performance. --- amplify.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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/**/* From bf57f46d99d8ca3d4761c2535c7b2572dcb217c8 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 20:10:41 -0500 Subject: [PATCH 18/20] Refactor GitHub Actions workflow to enhance caching strategy and improve build performance This commit updates the GitHub Actions workflow by separating caching for dependencies, TypeScript build info, and Next.js build outputs. It introduces distinct cache keys for each component, optimizing the caching process and improving build efficiency. Additionally, it standardizes quote usage for better consistency in the configuration file. --- .github/workflows/build.yml | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe06b70e..9fe65e7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,25 +24,43 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'pnpm' + node-version: "20" + cache: "pnpm" - 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 and build + - name: Cache dependencies uses: actions/cache@v4 with: path: | ~/.pnpm-store ${{ github.workspace }}/node_modules - ${{ github.workspace }}/.next/cache + 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 }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} + 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 }}-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: | From c0a08531640493e3eeef3123ed2f1853c0fcfa48 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 20:14:13 -0500 Subject: [PATCH 19/20] Add test comment for cache incremental verification in products page --- app/store/[slug]/products/page.tsx | 1 + 1 file changed, 1 insertion(+) 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 }> }) { From 717d093e1be01700ce90d35e9a4a8f57f9c9c6ce Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 12 Oct 2025 20:14:50 -0500 Subject: [PATCH 20/20] Standardize quote usage in GitHub Actions workflow for Node.js setup --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9fe65e7f..cde906f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,8 +24,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" - cache: "pnpm" + node-version: '20' + cache: 'pnpm' - name: Set Node.js optimization environment variables run: |