diff --git a/amplify.yml b/amplify.yml index 429d3f62..45e88eac 100644 --- a/amplify.yml +++ b/amplify.yml @@ -14,7 +14,6 @@ frontend: phases: preBuild: commands: - - nvm install 22 - nvm use 22 - npm ci build: diff --git a/amplify/functions/storeImages/handler.ts b/amplify/functions/storeImages/handler.ts index d2438e53..2c4f5a33 100644 --- a/amplify/functions/storeImages/handler.ts +++ b/amplify/functions/storeImages/handler.ts @@ -64,7 +64,7 @@ export const handler = async (event: any) => { // Manejar diferentes acciones switch (action) { case 'list': - return await listImages(storeId, body.limit, body.prefix) + return await listImages(storeId, body.limit, body.prefix, body.continuationToken) case 'upload': return await uploadImage(storeId, body.filename, body.contentType, body.fileContent) case 'delete': @@ -93,7 +93,12 @@ export const handler = async (event: any) => { } // Función para listar imágenes -async function listImages(storeId: string, limit: number = 1000, prefix: string = '') { +async function listImages( + storeId: string, + limit: number = 18, + prefix: string = '', + continuationToken?: string +) { try { // Configurar el prefijo para las imágenes de la tienda const storePrefix = prefix ? `products/${storeId}/${prefix}` : `products/${storeId}/` @@ -103,6 +108,7 @@ async function listImages(storeId: string, limit: number = 1000, prefix: string Bucket: bucketName, Prefix: storePrefix, MaxKeys: limit, + ContinuationToken: continuationToken, }) const listResponse = await s3Client.send(listCommand) @@ -163,7 +169,10 @@ async function listImages(storeId: string, limit: number = 1000, prefix: string return { statusCode: 200, - body: JSON.stringify({ images: validImages }), + body: JSON.stringify({ + images: validImages, + nextContinuationToken: listResponse.NextContinuationToken, + }), headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', diff --git a/app/(main-layout)/account-settings/page.tsx b/app/(main-layout)/account-settings/page.tsx index c7015672..ffb62e2c 100644 --- a/app/(main-layout)/account-settings/page.tsx +++ b/app/(main-layout)/account-settings/page.tsx @@ -20,7 +20,6 @@ Amplify.configure({ }, }) -// Client component that uses search params function AccountSettingsContent() { const searchParams = useSearchParams() const sectionParam = searchParams.get('section') @@ -32,7 +31,6 @@ function AccountSettingsContent() { ) useEffect(() => { - // Update view when URL parameter changes if (sectionParam && ['cuenta', 'pagos', 'sesiones'].includes(sectionParam)) { if (isGoogleUser && sectionParam === 'sesiones') { setCurrentView('cuenta') @@ -43,7 +41,7 @@ function AccountSettingsContent() { }, [sectionParam, isGoogleUser]) useEffect(() => { - document.title = 'Mi Perfil • Fasttify' + document.title = 'Mi Perfil | Fasttify' }, []) return ( diff --git a/app/(main-layout)/page.tsx b/app/(main-layout)/page.tsx index dfb1a3f1..d4fc4303 100644 --- a/app/(main-layout)/page.tsx +++ b/app/(main-layout)/page.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { getCurrentUser, fetchUserAttributes } from 'aws-amplify/auth' -import { ConsoleLogger, Hub } from 'aws-amplify/utils' +import { Hub } from 'aws-amplify/utils' import { Amplify } from 'aws-amplify' import 'aws-amplify/auth/enable-oauth-listener' import DocsLanding from '@/app/(main-layout)/landing/components/DocsLanding' diff --git a/app/(main-layout)/pricing/components/PricingCard.tsx b/app/(main-layout)/pricing/components/PricingCard.tsx index 05a6b2f9..213a1207 100644 --- a/app/(main-layout)/pricing/components/PricingCard.tsx +++ b/app/(main-layout)/pricing/components/PricingCard.tsx @@ -107,7 +107,6 @@ export function PricingCard({ plan }: PricingCardProps) { } }, [isSubmitting]) - // Render a skeleton or placeholder during SSR or when loading if (!isClient || loading) { return } diff --git a/app/(main-layout)/pricing/layout.tsx b/app/(main-layout)/pricing/layout.tsx new file mode 100644 index 00000000..8c14ecf2 --- /dev/null +++ b/app/(main-layout)/pricing/layout.tsx @@ -0,0 +1,7 @@ +export const metadata = { + title: 'Planes y precios', +} + +export default function PricingLayout({ children }: { children: React.ReactNode }) { + return <>{children} +} diff --git a/app/(main-layout)/pricing/page.tsx b/app/(main-layout)/pricing/page.tsx index ddac5bf0..e336ac05 100644 --- a/app/(main-layout)/pricing/page.tsx +++ b/app/(main-layout)/pricing/page.tsx @@ -1,6 +1,5 @@ 'use client' -import { useEffect } from 'react' import { motion } from 'framer-motion' import { PricingCard } from '@/app/(main-layout)/pricing/components/PricingCard' import { Footer } from '@/app/(main-layout)/landing/components/Footer' @@ -22,10 +21,6 @@ Amplify.configure({ }) export default function PricingPage() { - useEffect(() => { - document.title = 'Planes y Pagos • Fasttify' - }, []) - return ( <>
diff --git a/app/(setup-layout)/first-steps/hooks/useFirstStepsSetup.ts b/app/(setup-layout)/first-steps/hooks/useFirstStepsSetup.ts new file mode 100644 index 00000000..26ff2345 --- /dev/null +++ b/app/(setup-layout)/first-steps/hooks/useFirstStepsSetup.ts @@ -0,0 +1,210 @@ +import { useState } from 'react' +import { useUserStoreData } from '@/app/(setup-layout)/first-steps/hooks/useUserStoreData' +import { useApiKeyEncryption } from '@/app/(setup-layout)/first-steps/hooks/useApiKeyEncryption' +import { useAuthUser } from '@/hooks/auth/useAuthUser' +import { v4 as uuidv4 } from 'uuid' +import { routes } from '@/utils/routes' +import { + personalInfoSchema, + storeInfoSchema, + additionalSettingsSchema, +} from '@/lib/zod-schemas/first-step' +import sellingOptionsData from '@/app/(setup-layout)/first-steps/data/selling-options.json' + +export const useFirstStepsSetup = () => { + const [step, setStep] = useState(1) + const [isStepValid, setIsStepValid] = useState(false) + const [selectedOption, setSelectedOption] = useState(null) + const [formData, setFormData] = useState({ + fullName: '', + email: '', + phone: '', + documentType: '', + documentNumber: '', + + storeName: '', + description: '', + location: '', + category: '', + policies: '', + customDomain: '', + + wompiConfig: { + publicKey: '', + signature: '', + }, + }) + + const [validationErrors, setValidationErrors] = useState>({}) + const [saving, setSaving] = useState(false) + const { userData } = useAuthUser() + const { loading, createUserStore } = useUserStoreData() + const { encryptApiKey } = useApiKeyEncryption() + + const cognitoUsername = + userData && userData['cognito:username'] ? userData['cognito:username'] : null + + const updateFormData = (data: Partial) => { + setFormData(prev => ({ ...prev, ...data })) + } + const { options } = sellingOptionsData + + // Función para validar el paso actual + const validateStep = (): boolean => { + setValidationErrors({}) + let result + if (step === 2) { + result = personalInfoSchema.safeParse(formData) + } else if (step === 3) { + result = storeInfoSchema.safeParse(formData) + } else if (step === 4) { + result = additionalSettingsSchema.safeParse(formData) + } + if (result && !result.success) { + if (step === 4) { + setValidationErrors(result.error.format()) + } else { + setValidationErrors(result.error.flatten().fieldErrors) + } + return false + } + return true + } + + // Función para avanzar de paso, ejecutando la validación en cada cambio de paso + const nextStep = async () => { + if (step >= 2 && step <= 4) { + const valid = validateStep() + if (!valid) return + } + if (step === 1 && selectedOption) { + setStep(2) + } else if (step < 4) { + setStep(prev => prev + 1) + } else if (step === 4) { + setSaving(true) + + try { + // Cifrar las claves de Wompi usando la Lambda + let encryptedPublicKey = null + let encryptedSignature = null + + if (formData.wompiConfig.publicKey) { + encryptedPublicKey = await encryptApiKey( + formData.wompiConfig.publicKey, + 'wompi', + 'publicKey' + ) + } + + if (formData.wompiConfig.signature) { + encryptedSignature = await encryptApiKey( + formData.wompiConfig.signature, + 'wompi', + 'signature' + ) + } + + const storeInput = { + userId: cognitoUsername, + storeId: `${uuidv4().slice(0, 7)}`, + storeType: selectedOption || '', + storeName: formData.storeName, + storeDescription: formData.description, + storeCurrency: 'COP', + storeAdress: formData.location, + contactEmail: formData.email, + contactPhone: parseInt(formData.phone), + contactName: formData.fullName, + customDomain: + formData.customDomain || + `${formData.storeName.toLowerCase().replace(/\s+/g, '-')}.fasttify.com`, + conctactIdentification: formData.documentNumber, + contactIdentificationType: formData.documentType, + wompiConfig: JSON.stringify({ + isActive: true, + publicKey: encryptedPublicKey || formData.wompiConfig.publicKey, + signature: encryptedSignature || formData.wompiConfig.signature, + }), + onboardingCompleted: true, + } + + const result = await createUserStore(storeInput) + if (result) { + setTimeout(() => { + window.location.href = routes.store.dashboard.main(result.storeId) + }, 3000) + } else { + setSaving(false) + } + } catch (error) { + console.error('Error al cifrar las claves API:', error) + setSaving(false) + } + } + } + + const handleQuickSetup = async () => { + if (!cognitoUsername) return + + setSaving(true) + const quickStoreId = uuidv4() + const storeIdShort = quickStoreId.slice(0, 7) + const storeName = `Tienda ${storeIdShort}` + + const quickStoreInput = { + userId: cognitoUsername, + storeId: storeIdShort, + storeName: storeName, + customDomain: `${storeName.toLowerCase().replace(/\s+/g, '-')}.fasttify.com`, + storeType: 'quick-setup', + storeCurrency: 'COP', + onboardingCompleted: true, + } + + const result = await createUserStore(quickStoreInput) + + if (result) { + setTimeout(() => { + window.location.href = routes.store.dashboard.main(result.storeId) + }, 3000) + } else { + setSaving(false) + } + } + + const prevStep = () => { + if (step > 1) setStep(prev => prev - 1) + } + + const handleStepValidation = (isValid: boolean) => { + setIsStepValid(isValid) + } + + return { + step, + setStep, + isStepValid, + setIsStepValid, + selectedOption, + setSelectedOption, + formData, + setFormData, + validationErrors, + setValidationErrors, + saving, + setSaving, + userData, + loading, + createUserStore, + encryptApiKey, + cognitoUsername, + updateFormData, + options, + validateStep, + nextStep, + handleQuickSetup, + prevStep, + handleStepValidation, + } +} diff --git a/app/(setup-layout)/first-steps/layout.tsx b/app/(setup-layout)/first-steps/layout.tsx new file mode 100644 index 00000000..1812eefc --- /dev/null +++ b/app/(setup-layout)/first-steps/layout.tsx @@ -0,0 +1,7 @@ +export const metadata = { + title: 'Creando tu tienda', +} + +export default function FirstStepsLayout({ children }: { children: React.ReactNode }) { + return <>{children} +} diff --git a/app/(setup-layout)/first-steps/page.tsx b/app/(setup-layout)/first-steps/page.tsx index a2c37676..012d3148 100644 --- a/app/(setup-layout)/first-steps/page.tsx +++ b/app/(setup-layout)/first-steps/page.tsx @@ -1,196 +1,33 @@ 'use client' -import { useState, useEffect } from 'react' import { ArrowRight, Store, User, Settings, InfoIcon } from 'lucide-react' import { AnimatePresence, motion } from 'framer-motion' import { Button } from '@/components/ui/button' import { BackgroundGradientAnimation } from '@/app/(setup-layout)/first-steps/components/BackgroundGradientAnimation' -import { useUserStoreData } from '@/app/(setup-layout)/first-steps/hooks/useUserStoreData' import { MultiStepLoader } from '@/app/(setup-layout)/first-steps/components/MultiStepLoader' import Image from 'next/image' import PersonalInfo from '@/app/(setup-layout)/first-steps/components/PersonalInfo' import StoreInfo from '@/app/(setup-layout)/first-steps/components/StoreInfo' import AdditionalSettings from '@/app/(setup-layout)/first-steps/components/AdditionalSettings' -import { - personalInfoSchema, - storeInfoSchema, - additionalSettingsSchema, -} from '@/lib/zod-schemas/first-step' -import { useAuthUser } from '@/hooks/auth/useAuthUser' -import { v4 as uuidv4 } from 'uuid' -import { routes } from '@/utils/routes' -import { useApiKeyEncryption } from '@/app/(setup-layout)/first-steps/hooks/useApiKeyEncryption' -import sellingOptions from '@/app/(setup-layout)/first-steps/data/selling-options.json' +import { useFirstStepsSetup } from '@/app/(setup-layout)/first-steps/hooks/useFirstStepsSetup' export default function FirstStepsPage() { - const [step, setStep] = useState(1) - const [isStepValid, setIsStepValid] = useState(false) - const [selectedOption, setSelectedOption] = useState(null) - const [formData, setFormData] = useState({ - fullName: '', - email: '', - phone: '', - documentType: '', - documentNumber: '', - - storeName: '', - description: '', - location: '', - category: '', - policies: '', - customDomain: '', - - wompiConfig: { - publicKey: '', - signature: '', - }, - }) - - const [validationErrors, setValidationErrors] = useState>({}) - const [saving, setSaving] = useState(false) - const { userData } = useAuthUser() - const { loading, createUserStore } = useUserStoreData() - const { encryptApiKey } = useApiKeyEncryption() - - const cognitoUsername = - userData && userData['cognito:username'] ? userData['cognito:username'] : null - - const updateFormData = (data: Partial) => { - setFormData(prev => ({ ...prev, ...data })) - } - const { options } = sellingOptions - - // Función para validar el paso actual - const validateStep = (): boolean => { - setValidationErrors({}) - let result - if (step === 2) { - result = personalInfoSchema.safeParse(formData) - } else if (step === 3) { - result = storeInfoSchema.safeParse(formData) - } else if (step === 4) { - result = additionalSettingsSchema.safeParse(formData) - } - if (result && !result.success) { - if (step === 4) { - setValidationErrors(result.error.format()) - } else { - setValidationErrors(result.error.flatten().fieldErrors) - } - return false - } - return true - } - - // Función para avanzar de paso, ejecutando la validación en cada cambio de paso - const nextStep = async () => { - if (step >= 2 && step <= 4) { - const valid = validateStep() - if (!valid) return - } - if (step === 1 && selectedOption) { - setStep(2) - } else if (step < 4) { - setStep(prev => prev + 1) - } else if (step === 4) { - setSaving(true) - - try { - // Cifrar las claves de Wompi usando la Lambda - let encryptedPublicKey = null - let encryptedSignature = null - - if (formData.wompiConfig.publicKey) { - encryptedPublicKey = await encryptApiKey( - formData.wompiConfig.publicKey, - 'wompi', - 'publicKey' - ) - } - - if (formData.wompiConfig.signature) { - encryptedSignature = await encryptApiKey( - formData.wompiConfig.signature, - 'wompi', - 'signature' - ) - } - - const storeInput = { - userId: cognitoUsername, - storeId: `${uuidv4().slice(0, 7)}`, - storeType: selectedOption || '', - storeName: formData.storeName, - storeDescription: formData.description, - storeCurrency: 'COP', - storeAdress: formData.location, - contactEmail: formData.email, - contactPhone: parseInt(formData.phone), - contactName: formData.fullName, - customDomain: - formData.customDomain || - `${formData.storeName.toLowerCase().replace(/\s+/g, '-')}.fasttify.com`, - conctactIdentification: formData.documentNumber, - contactIdentificationType: formData.documentType, - wompiConfig: JSON.stringify({ - isActive: true, - publicKey: encryptedPublicKey || formData.wompiConfig.publicKey, - signature: encryptedSignature || formData.wompiConfig.signature, - }), - onboardingCompleted: true, - } - - const result = await createUserStore(storeInput) - if (result) { - setTimeout(() => { - window.location.href = routes.store.dashboard.main(result.storeId) - }, 3000) - } else { - setSaving(false) - } - } catch (error) { - console.error('Error al cifrar las claves API:', error) - setSaving(false) - } - } - } - - const handleQuickSetup = async () => { - if (!cognitoUsername) return - - setSaving(true) - const quickStoreId = uuidv4() - const storeIdShort = quickStoreId.slice(0, 7) - const storeName = `Tienda ${storeIdShort}` - - const quickStoreInput = { - userId: cognitoUsername, - storeId: storeIdShort, - storeName: storeName, - customDomain: `${storeName.toLowerCase().replace(/\s+/g, '-')}.fasttify.com`, - storeType: 'quick-setup', - storeCurrency: 'COP', - onboardingCompleted: true, - } - - const result = await createUserStore(quickStoreInput) - - if (result) { - setTimeout(() => { - window.location.href = routes.store.dashboard.main(result.storeId) - }, 3000) - } else { - setSaving(false) - } - } - - const prevStep = () => { - if (step > 1) setStep(prev => prev - 1) - } - - useEffect(() => { - document.title = 'Creando tu tienda • Fasttify' - }, []) + const { + step, + isStepValid, + selectedOption, + setSelectedOption, + formData, + validationErrors, + saving, + loading, + updateFormData, + options, + nextStep, + handleQuickSetup, + prevStep, + handleStepValidation, + } = useFirstStepsSetup() if (loading || saving) { return ( @@ -208,10 +45,6 @@ export default function FirstStepsPage() { ) } - const handleStepValidation = (isValid: boolean) => { - setIsStepValid(isValid) - } - const renderStep = () => { switch (step) { case 1: @@ -225,8 +58,6 @@ export default function FirstStepsPage() { Configuraremos todo para que puedas empezar a vender sin complicaciones en los canales que elijas.

- - {/* New information box about quick setup */}
diff --git a/app/(setup-layout)/login/components/AuthClient.tsx b/app/(setup-layout)/login/components/main-components/AuthClient.tsx similarity index 63% rename from app/(setup-layout)/login/components/AuthClient.tsx rename to app/(setup-layout)/login/components/main-components/AuthClient.tsx index a685df85..ca47583c 100644 --- a/app/(setup-layout)/login/components/AuthClient.tsx +++ b/app/(setup-layout)/login/components/main-components/AuthClient.tsx @@ -1,5 +1,5 @@ -import { AuthForm } from '@/app/(setup-layout)/login/AuthForm' -import ImageSlider from '@/app/(setup-layout)/login/components/ImageSlider' +import { AuthForm } from '@/app/(setup-layout)/login/components/main-components/AuthForm' +import ImageSlider from '@/app/(setup-layout)/login/components/main-components/ImageSlider' const LoginPage = () => { return ( diff --git a/app/(setup-layout)/login/AuthForm.tsx b/app/(setup-layout)/login/components/main-components/AuthForm.tsx similarity index 100% rename from app/(setup-layout)/login/AuthForm.tsx rename to app/(setup-layout)/login/components/main-components/AuthForm.tsx diff --git a/app/(setup-layout)/login/components/ImageSlider.tsx b/app/(setup-layout)/login/components/main-components/ImageSlider.tsx similarity index 100% rename from app/(setup-layout)/login/components/ImageSlider.tsx rename to app/(setup-layout)/login/components/main-components/ImageSlider.tsx diff --git a/app/(setup-layout)/login/page.tsx b/app/(setup-layout)/login/page.tsx index 06da97d6..1aadb3c8 100644 --- a/app/(setup-layout)/login/page.tsx +++ b/app/(setup-layout)/login/page.tsx @@ -1,6 +1,8 @@ -'use client' +import AuthClient from '@/app/(setup-layout)/login/components/main-components/AuthClient' -import AuthClient from '@/app/(setup-layout)/login/components/AuthClient' +export const metadata = { + title: 'Creando tu cuenta', +} export default function LoginPage() { return diff --git a/app/(setup-layout)/my-store/page.tsx b/app/(setup-layout)/my-store/page.tsx index ede6a0cb..dc225f88 100644 --- a/app/(setup-layout)/my-store/page.tsx +++ b/app/(setup-layout)/my-store/page.tsx @@ -1,14 +1,11 @@ -'use client' - -import { useEffect } from 'react' import { StoreSelector } from '@/app/(setup-layout)/my-store/components/StoreSelector' import { BackgroundGradientAnimation } from '@/app/(setup-layout)/first-steps/components/BackgroundGradientAnimation' -export default function MyStorePage() { - useEffect(() => { - document.title = 'Mis tiendas • Fasttify' - }, []) +export const metadata = { + title: 'Selecciona tu tienda ', +} +export default function MyStorePage() { return (
diff --git a/app/layout.tsx b/app/layout.tsx index 93d2f731..d4dd1e48 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata } from 'next' -import { Plus_Jakarta_Sans } from 'next/font/google' +import { plusJakartaSans } from '@/config/fonts' import { ReactQueryProvider } from '@/utils/ReactQueryProvider' import { Toaster } from '@/components/ui/sonner' import { Amplify } from 'aws-amplify' @@ -17,26 +17,18 @@ Amplify.configure({ }, }) -const plusJakartaSans = Plus_Jakarta_Sans({ - subsets: ['latin'], - weight: ['300', '400', '500', '700'], - display: 'swap', -}) - export const metadata: Metadata = { - // URL base para resolver rutas relativas en metadatos (por ejemplo, en imágenes) metadataBase: new URL('https://www.fasttify.com'), title: { - default: 'Fasttify - Ecommerce Dropshipping', - template: '%s | Fasttify Dropshipping', + default: 'Fasttify', + template: '%s | Fasttify', }, description: 'Fasttify es una plataforma ecommerce de dropshipping que te permite gestionar y escalar tu tienda online sin complicaciones, ofreciendo productos de calidad y una experiencia de compra excepcional.', keywords: ['ecommerce', 'dropshipping', 'tienda online', 'Fasttify', 'compras online'], openGraph: { title: 'Fasttify', - description: - 'Descubre Fasttify, la plataforma ecommerce de dropshipping que facilita la gestión de tu tienda online y te ayuda a escalar tus ventas sin complicaciones.', + description: 'Fasttify potencia tu tienda online de dropshipping.', url: 'https://www.fasttify.com', siteName: 'Fasttify', images: [ @@ -47,32 +39,17 @@ export const metadata: Metadata = { alt: 'Fasttify Dropshipping', }, ], - locale: 'es_ES', type: 'website', }, twitter: { card: 'summary_large_image', title: 'Fasttify - Ecommerce Dropshipping', - description: - 'Fasttify potencia tu tienda online de dropshipping, facilitando la gestión de productos y escalabilidad de ventas.', + description: 'Fasttify potencia tu tienda online de dropshipping.', images: ['https://www.fasttify.com/icons/fast@4x.webp'], }, icons: { icon: '/icons/fast@4x.webp', }, - robots: { - index: true, - follow: true, - nocache: false, - googleBot: { - index: true, - follow: true, - noimageindex: false, - 'max-video-preview': -1, - 'max-image-preview': 'large', - 'max-snippet': -1, - }, - }, } export default function RootLayout({ children }: { children: React.ReactNode }) { diff --git a/app/store/[slug]/products/[productId]/page.tsx b/app/store/[slug]/products/[productId]/page.tsx index 3927cec1..bfa7ad39 100644 --- a/app/store/[slug]/products/[productId]/page.tsx +++ b/app/store/[slug]/products/[productId]/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { ProductForm } from '@/app/store/components/product-management/ProductForm' +import { ProductForm } from '@/app/store/components/product-management/main-components/ProductForm' import { useParams, usePathname } from 'next/navigation' import { getStoreId } from '@/utils/store-utils' import { Amplify } from 'aws-amplify' diff --git a/app/store/[slug]/products/inventory/page.tsx b/app/store/[slug]/products/inventory/page.tsx index 893af607..269ff5b9 100644 --- a/app/store/[slug]/products/inventory/page.tsx +++ b/app/store/[slug]/products/inventory/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { InventoryManager } from '@/app/store/components/product-management/InventoryManager' +import { InventoryManager } from '@/app/store/components/product-management/main-components/InventoryManager' import { Amplify } from 'aws-amplify' import { getStoreId } from '@/utils/store-utils' import { useParams, usePathname } from 'next/navigation' diff --git a/app/store/[slug]/products/new/page.tsx b/app/store/[slug]/products/new/page.tsx index 208a2b61..136caaca 100644 --- a/app/store/[slug]/products/new/page.tsx +++ b/app/store/[slug]/products/new/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { ProductForm } from '@/app/store/components/product-management/ProductForm' +import { ProductForm } from '@/app/store/components/product-management/main-components/ProductForm' import { useParams, usePathname } from 'next/navigation' import { getStoreId } from '@/utils/store-utils' import { Amplify } from 'aws-amplify' diff --git a/app/store/[slug]/products/page.tsx b/app/store/[slug]/products/page.tsx index 97c67904..19da51cf 100644 --- a/app/store/[slug]/products/page.tsx +++ b/app/store/[slug]/products/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { ProductManager } from '@/app/store/components/product-management/ProductManager' +import { ProductManager } from '@/app/store/components/product-management/main-components/ProductManager' import { getStoreId } from '@/utils/store-utils' import { useParams, usePathname } from 'next/navigation' import { Amplify } from 'aws-amplify' diff --git a/app/store/components/images-selector/ImageGallery.tsx b/app/store/components/images-selector/ImageGallery.tsx new file mode 100644 index 00000000..a7a19015 --- /dev/null +++ b/app/store/components/images-selector/ImageGallery.tsx @@ -0,0 +1,176 @@ +import { Trash2 } from 'lucide-react' +import Image from 'next/image' +import { Button } from '@/components/ui/button' +import { Loader } from '@/components/ui/loader' +import { S3Image } from '@/app/store/hooks/useS3Images' + +interface ImageGalleryProps { + images: S3Image[] + viewMode: 'grid' | 'list' + selectedImage: string | string[] | null + allowMultipleSelection: boolean + loading: boolean + error: Error | null + searchTerm: string + onImageSelect: (image: S3Image) => void + onDeleteImage: (key: string) => Promise +} + +export default function ImageGallery({ + images, + viewMode, + selectedImage, + allowMultipleSelection, + loading, + error, + searchTerm, + onImageSelect, + onDeleteImage, +}: ImageGalleryProps) { + if (!loading && images.length === 0) { + return ( +
+ {searchTerm + ? 'No hay imágenes que coincidan con la búsqueda.' + : 'No hay imágenes disponibles. Sube algunas imágenes para comenzar.'} +
+ ) + } + + if (loading) { + return ( +
+ +
+ ) + } + + if (error) { + return ( +
+ Error al cargar las imágenes. Por favor, intenta de nuevo. +
+ ) + } + + return ( + <> + {viewMode === 'grid' && ( +
+ {images.map((image, index) => ( +
onImageSelect(image)} + > +
+ onImageSelect(image)} + onClick={e => e.stopPropagation()} + /> +
+
+ +
+
+ {image.filename} +
+ +
+
{image.filename}
+
+ {image.type?.split('/')[1]?.toUpperCase() || 'IMG'} +
+
+
+ ))} +
+ )} + + {/* Image gallery - List view */} + {viewMode === 'list' && ( +
+ {images.map((image, index) => ( +
onImageSelect(image)} + > +
+ onImageSelect(image)} + onClick={e => e.stopPropagation()} + /> +
+
+ {image.filename} +
+
+
{image.filename}
+
+ {image.type?.split('/')[1]?.toUpperCase() || 'IMG'} • + {image.size ? ` ${Math.round(image.size / 1024)} KB` : ''} +
+
+ +
+ ))} +
+ )} + + ) +} diff --git a/app/store/components/images-selector/image-selector-modal.tsx b/app/store/components/images-selector/image-selector-modal.tsx index b40aa1d7..37ce507e 100644 --- a/app/store/components/images-selector/image-selector-modal.tsx +++ b/app/store/components/images-selector/image-selector-modal.tsx @@ -12,12 +12,14 @@ import { } from '@/components/ui/dropdown-menu' import { useS3Images, type S3Image } from '@/app/store/hooks/useS3Images' import Image from 'next/image' +import ImageGallery from './ImageGallery' interface ImageSelectorModalProps { open: boolean onOpenChange: (open: boolean) => void - onSelect?: (image: S3Image | null) => void + onSelect?: (images: S3Image | S3Image[] | null) => void initialSelectedImage?: string | null + allowMultipleSelection?: boolean } export default function ImageSelectorModal({ @@ -25,61 +27,108 @@ export default function ImageSelectorModal({ onOpenChange, onSelect, initialSelectedImage = null, + allowMultipleSelection = false, }: ImageSelectorModalProps) { const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid') - const [selectedImage, setSelectedImage] = useState(initialSelectedImage) + const [selectedImage, setSelectedImage] = useState( + allowMultipleSelection + ? initialSelectedImage + ? [initialSelectedImage] + : [] + : initialSelectedImage + ) const [searchTerm, setSearchTerm] = useState('') const fileInputRef = useRef(null) const [isUploading, setIsUploading] = useState(false) const [uploadPreview, setUploadPreview] = useState(null) - // Usar el hook useS3Images para obtener, cargar y eliminar imágenes - const { images, loading, error, uploadImage, deleteImage } = useS3Images({ - limit: 100, + + const { + images, + loading, + error, + uploadImage, + deleteImage, + fetchMoreImages, + loadingMore, + nextContinuationToken, + } = useS3Images({ + limit: 18, }) - // Filtrar imágenes según el término de búsqueda const filteredImages = images.filter(img => img.filename.toLowerCase().includes(searchTerm.toLowerCase()) ) // Manejar la selección de imágenes const handleImageSelect = (image: S3Image) => { - const newSelectedKey = selectedImage === image.key ? null : image.key - setSelectedImage(newSelectedKey) + if (allowMultipleSelection) { + const selectedKeys = Array.isArray(selectedImage) ? selectedImage : [] + const isSelected = selectedKeys.includes(image.key) + if (isSelected) { + setSelectedImage(selectedKeys.filter(key => key !== image.key)) + } else { + setSelectedImage([...selectedKeys, image.key]) + } + } else { + const newSelectedKey = selectedImage === image.key ? null : image.key + setSelectedImage(newSelectedKey) + } } // Manejar la confirmación de selección const handleConfirm = () => { - const selected = images.find(img => img.key === selectedImage) || null - if (onSelect) { - onSelect(selected) + if (allowMultipleSelection) { + const selectedKeys = Array.isArray(selectedImage) ? selectedImage : [] + const selectedImages = images.filter(img => selectedKeys.includes(img.key)) + if (onSelect) { + onSelect(selectedImages.length > 0 ? selectedImages : null) + } + } else { + const selected = images.find(img => img.key === selectedImage) || null + if (onSelect) { + onSelect(selected) + } } onOpenChange(false) } - // Manejar la carga de archivos const handleFileUpload = async (event: React.ChangeEvent) => { const files = event.target.files if (!files || files.length === 0) return - const file = files[0] + const filesArray = Array.from(files) setIsUploading(true) - // Crear una vista previa de la imagen - const reader = new FileReader() - reader.onload = e => { - if (e.target?.result) { - setUploadPreview(e.target.result as string) + const previews: string[] = [] + for (const file of filesArray) { + const reader = new FileReader() + reader.onload = e => { + if (e.target?.result) { + previews.push(e.target.result as string) + + if (previews.length === filesArray.length) { + setUploadPreview(previews[0]) + } + } } + reader.readAsDataURL(file) } - reader.readAsDataURL(file) try { - const uploadedImage = await uploadImage(file) - if (uploadedImage) { - setSelectedImage(uploadedImage.key) + const uploadedImages = await uploadImage(filesArray) + if (uploadedImages && uploadedImages.length > 0) { + if (allowMultipleSelection) { + setSelectedImage(prev => { + const currentSelected = Array.isArray(prev) ? prev : prev ? [prev] : [] + const newKeys = uploadedImages.map(img => img.key) + const uniqueNewKeys = newKeys.filter(key => !currentSelected.includes(key)) + return [...currentSelected, ...uniqueNewKeys] + }) + } else { + setSelectedImage(uploadedImages[0].key) + } } } catch (error) { - console.error('Error uploading image:', error) + console.error('Error uploading image(s):', error) } finally { setIsUploading(false) setUploadPreview(null) @@ -108,33 +157,76 @@ export default function ImageSelectorModal({ e.preventDefault() if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { - const file = e.dataTransfer.files[0] + const filesArray = Array.from(e.dataTransfer.files) + + const imageFiles = filesArray.filter(file => file.type.startsWith('image/')) + + if (imageFiles.length === 0) { + console.warn('Dropped files are not images.') + return + } + + setIsUploading(true) + setUploadPreview(null) + + const reader = new FileReader() + reader.onload = e => { + if (e.target?.result) { + setUploadPreview(e.target.result as string) + } + } + reader.readAsDataURL(imageFiles[0]) try { - const uploadedImage = await uploadImage(file) - if (uploadedImage) { - setSelectedImage(uploadedImage.key) + const uploadedImages = await uploadImage(imageFiles) + if (uploadedImages && uploadedImages.length > 0) { + if (allowMultipleSelection) { + setSelectedImage(prev => { + const currentSelected = Array.isArray(prev) ? prev : prev ? [prev] : [] + const newKeys = uploadedImages.map(img => img.key) + const uniqueNewKeys = newKeys.filter(key => !currentSelected.includes(key)) + return [...currentSelected, ...uniqueNewKeys] + }) + } else { + setSelectedImage(uploadedImages[0].key) + } } - } catch (error) {} + } catch (error) { + console.error('Error uploading image(s) on drop:', error) + } finally { + setIsUploading(false) + setUploadPreview(null) + } } }, - [uploadImage] + [uploadImage, allowMultipleSelection, selectedImage] ) const onDragOver = useCallback((e: React.DragEvent) => { e.preventDefault() }, []) + // Handle scroll to fetch more images + const handleScroll = (e: React.UIEvent) => { + const target = e.target as HTMLDivElement + // Check if scrolled to the bottom (within a threshold) + const isAtBottom = target.scrollHeight - target.scrollTop <= target.clientHeight + 100 // 100px threshold + + if (isAtBottom && nextContinuationToken && !loadingMore && !loading) { + fetchMoreImages() + } + } + return ( - +
Seleccionar imagen
-
+
{/* Search and filters */}
@@ -183,6 +275,7 @@ export default function ImageSelectorModal({ onChange={handleFileUpload} accept="image/*" className="hidden" + multiple={allowMultipleSelection} /> -
-
- {image.filename} -
- -
-
{image.filename}
-
- {image.type?.split('/')[1]?.toUpperCase() || 'IMG'} -
-
-
- ))} -
- )} - - {/* Image gallery - List view */} - {viewMode === 'list' && ( -
- {filteredImages.map((image, index) => ( -
handleImageSelect(image)} - > -
- handleImageSelect(image)} - onClick={e => e.stopPropagation()} - /> -
-
- {image.filename} -
-
-
{image.filename}
-
- {image.type?.split('/')[1]?.toUpperCase() || 'IMG'} • - {image.size ? ` ${Math.round(image.size / 1024)} KB` : ''} -
-
- -
- ))} + {/* Loading indicator for more images */} + {loadingMore && ( +
+ + Cargando más imágenes...
)}
diff --git a/app/store/components/product-management/collection-form/image-section.tsx b/app/store/components/product-management/collection-form/image-section.tsx index b329a323..fbc71c43 100644 --- a/app/store/components/product-management/collection-form/image-section.tsx +++ b/app/store/components/product-management/collection-form/image-section.tsx @@ -17,7 +17,6 @@ Amplify.configure({ }, }) -// Añadir props para imageUrl y onImageChange export function ImageSection({ imageUrl = '', onImageChange, @@ -37,7 +36,7 @@ export function ImageSection({ key: imageUrl, url: imageUrl, filename: imageUrl.split('/').pop() || 'imagen', - type: 'image/jpeg', + type: 'image/', size: 0, lastModified: new Date(), }) @@ -123,8 +122,9 @@ export function ImageSection({ void} initialSelectedImage={selectedImage?.key} + allowMultipleSelection={false} />
) diff --git a/app/store/components/product-management/collection-form/publication-section.tsx b/app/store/components/product-management/collection-form/publication-section.tsx index ee499b80..3b7348d4 100644 --- a/app/store/components/product-management/collection-form/publication-section.tsx +++ b/app/store/components/product-management/collection-form/publication-section.tsx @@ -2,10 +2,7 @@ import { Button } from '@/components/ui/button' import { X } from 'lucide-react' // Añadir props para isActive y onActiveChange -export function PublicationSection({ - isActive = true, - onActiveChange, -}: { +export function PublicationSection({}: { isActive?: boolean onActiveChange: (isActive: boolean) => void }) { diff --git a/app/store/components/product-management/AttributesForm.tsx b/app/store/components/product-management/main-components/AttributesForm.tsx similarity index 100% rename from app/store/components/product-management/AttributesForm.tsx rename to app/store/components/product-management/main-components/AttributesForm.tsx diff --git a/app/store/components/product-management/ImageUpload.tsx b/app/store/components/product-management/main-components/ImageUpload.tsx similarity index 60% rename from app/store/components/product-management/ImageUpload.tsx rename to app/store/components/product-management/main-components/ImageUpload.tsx index 5ffc5869..8ab5c16b 100644 --- a/app/store/components/product-management/ImageUpload.tsx +++ b/app/store/components/product-management/main-components/ImageUpload.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from 'react' +import { useState } from 'react' import Image from 'next/image' import { X, Upload, Loader2 } from 'lucide-react' import { Button } from '@/components/ui/button' @@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { cn } from '@/lib/utils' import { Dialog, DialogContent } from '@/components/ui/dialog' -import { useProductImageUpload } from '@/app/store/hooks/useProductImageUpload' +import ImageSelectorModal from '@/app/store/components/images-selector/image-selector-modal' interface ImageFile { url: string @@ -19,74 +19,11 @@ interface ImageUploadProps { storeId: string } -export function ImageUpload({ value, onChange, storeId }: ImageUploadProps) { +export function ImageUpload({ value, onChange }: ImageUploadProps) { const [draggedIndex, setDraggedIndex] = useState(null) const [enlargedImage, setEnlargedImage] = useState(null) const [isDragging, setIsDragging] = useState(false) - const [isUploading, setIsUploading] = useState(false) - const [uploadingFiles, setUploadingFiles] = useState<{ file: File; preview: string }[]>([]) - const fileInputRef = useRef(null) - const { uploadMultipleProductImages, isLoading } = useProductImageUpload() - - useEffect(() => { - setIsUploading(isLoading) - }, [isLoading]) - - const handleFileSelect = async (files: FileList | null) => { - if (!files || files.length === 0) return - - setIsUploading(true) - - try { - const validFiles = Array.from(files).filter( - file => file.type.startsWith('image/') && file.size <= 5242880 - ) - - if (validFiles.length === 0) { - return - } - - // Crear previsualizaciones inmediatamente y mostrarlas - const uploading = validFiles.map(file => ({ - file, - preview: URL.createObjectURL(file), - })) - - setUploadingFiles(uploading) - - // Mostrar temporalmente las imágenes en la interfaz mientras se suben - const tempImages = uploading.map(item => ({ - url: item.preview, - alt: '', - isTemp: true, - })) - - // Añadir temporalmente las imágenes a la vista - onChange([...value, ...tempImages]) - - // Subir las imágenes en segundo plano - const uploadedImages = await uploadMultipleProductImages(validFiles, storeId) - - if (uploadedImages.length > 0) { - // Reemplazar las imágenes temporales con las reales - const newImages = [...value] - // Eliminar las imágenes temporales - const finalImages = newImages.filter(img => !(img as any).isTemp) - // Añadir las imágenes subidas - onChange([...finalImages, ...uploadedImages]) - } - } catch (error) { - console.error('Error uploading images:', error) - // En caso de error, eliminar las imágenes temporales - const newImages = [...value] - onChange(newImages.filter(img => !(img as any).isTemp)) - } finally { - // Clean up object URLs to avoid memory leaks - uploadingFiles.forEach(item => URL.revokeObjectURL(item.preview)) - setUploadingFiles([]) - setIsUploading(false) - } - } + const [isModalOpen, setIsModalOpen] = useState(false) const handleDragEnter = (e: React.DragEvent) => { e.preventDefault() @@ -112,11 +49,11 @@ export function ImageUpload({ value, onChange, storeId }: ImageUploadProps) { setIsDragging(false) const files = e.dataTransfer.files - handleFileSelect(files) + setIsModalOpen(true) } const handleButtonClick = () => { - fileInputRef.current?.click() + setIsModalOpen(true) } const removeImage = (index: number) => { @@ -131,7 +68,6 @@ export function ImageUpload({ value, onChange, storeId }: ImageUploadProps) { onChange(newImages) } - // Funcionalidad para reordenar imágenes const handleImageDragStart = (index: number) => { setDraggedIndex(index) } @@ -157,13 +93,6 @@ export function ImageUpload({ value, onChange, storeId }: ImageUploadProps) { setEnlargedImage(image) } - useEffect(() => { - // Clean up object URLs when component unmounts - return () => { - uploadingFiles.forEach(item => URL.revokeObjectURL(item.preview)) - } - }, [uploadingFiles]) - return (
- handleFileSelect(e.target.files)} - disabled={isUploading} - />

Arrastre y suelte imágenes aquí

o haga clic para buscar (máximo 5MB por imagen)

-
- {uploadingFiles.length > 0 && ( -
-

Subiendo imágenes ({uploadingFiles.length})

-
- {uploadingFiles.map((item, index) => ( -
-
- Imagen cargando -
- - Subiendo... -
-
-
{item.file.name}
-
- ))} -
-
- )} - {value.length > 0 && (

Imágenes del Producto ({value.length})

@@ -301,6 +196,31 @@ export function ImageUpload({ value, onChange, storeId }: ImageUploadProps) { )}
+ + { + if (selectedImage) { + let imagesToAdd: ImageFile[] = [] + if (Array.isArray(selectedImage)) { + imagesToAdd = selectedImage.map(img => ({ + url: img.url, + alt: '', + })) + } else { + imagesToAdd.push({ + url: selectedImage.url, + alt: '', + }) + } + onChange([...value, ...imagesToAdd]) + } + setIsModalOpen(false) + }} + initialSelectedImage={value.length > 0 ? value[0].url : null} + />
) } diff --git a/app/store/components/product-management/InventoryManager.tsx b/app/store/components/product-management/main-components/InventoryManager.tsx similarity index 84% rename from app/store/components/product-management/InventoryManager.tsx rename to app/store/components/product-management/main-components/InventoryManager.tsx index f6685bde..bcf6fbcd 100644 --- a/app/store/components/product-management/InventoryManager.tsx +++ b/app/store/components/product-management/main-components/InventoryManager.tsx @@ -1,8 +1,8 @@ import { useProducts } from '@/app/store/hooks/useProducts' -import { InventoryTracking } from './InventoryTracking' -import { InventoryPage } from './InventoryPage' +import { InventoryTracking } from '@/app/store/components/product-management/main-components/InventoryTracking' +import { InventoryPage } from '@/app/store/components/product-management/main-components/InventoryPage' import { Loader } from '@/components/ui/loader' -import { useProductPagination } from './hooks/useProductPagination' +import { useProductPagination } from '@/app/store/components/product-management/hooks/useProductPagination' interface InventoryManagerProps { storeId: string diff --git a/app/store/components/product-management/InventoryPage.tsx b/app/store/components/product-management/main-components/InventoryPage.tsx similarity index 100% rename from app/store/components/product-management/InventoryPage.tsx rename to app/store/components/product-management/main-components/InventoryPage.tsx diff --git a/app/store/components/product-management/InventoryTracking.tsx b/app/store/components/product-management/main-components/InventoryTracking.tsx similarity index 100% rename from app/store/components/product-management/InventoryTracking.tsx rename to app/store/components/product-management/main-components/InventoryTracking.tsx diff --git a/app/store/components/product-management/ProductForm.tsx b/app/store/components/product-management/main-components/ProductForm.tsx similarity index 100% rename from app/store/components/product-management/ProductForm.tsx rename to app/store/components/product-management/main-components/ProductForm.tsx diff --git a/app/store/components/product-management/ProductList.tsx b/app/store/components/product-management/main-components/ProductList.tsx similarity index 100% rename from app/store/components/product-management/ProductList.tsx rename to app/store/components/product-management/main-components/ProductList.tsx diff --git a/app/store/components/product-management/ProductManager.tsx b/app/store/components/product-management/main-components/ProductManager.tsx similarity index 92% rename from app/store/components/product-management/ProductManager.tsx rename to app/store/components/product-management/main-components/ProductManager.tsx index 8b5f449f..ef90fb4f 100644 --- a/app/store/components/product-management/ProductManager.tsx +++ b/app/store/components/product-management/main-components/ProductManager.tsx @@ -1,7 +1,7 @@ import { useProducts } from '@/app/store/hooks/useProducts' -import { ProductForm } from '@/app/store/components/product-management/ProductForm' -import { ProductList } from '@/app/store/components/product-management/ProductList' -import { ProductsPage } from '@/app/store/components/product-management/ProductPage' +import { ProductForm } from '@/app/store/components/product-management/main-components/ProductForm' +import { ProductList } from '@/app/store/components/product-management/main-components/ProductList' +import { ProductsPage } from '@/app/store/components/product-management/main-components/ProductPage' import { Loader } from '@/components/ui/loader' interface ProductManagerProps { diff --git a/app/store/components/product-management/ProductPage.tsx b/app/store/components/product-management/main-components/ProductPage.tsx similarity index 100% rename from app/store/components/product-management/ProductPage.tsx rename to app/store/components/product-management/main-components/ProductPage.tsx diff --git a/app/store/components/product-management/product-sections/attributes-section.tsx b/app/store/components/product-management/product-sections/attributes-section.tsx index 338db5db..1acafc43 100644 --- a/app/store/components/product-management/product-sections/attributes-section.tsx +++ b/app/store/components/product-management/product-sections/attributes-section.tsx @@ -1,5 +1,5 @@ import type { UseFormReturn } from 'react-hook-form' -import { AttributesForm } from '@/app/store/components/product-management/AttributesForm' +import { AttributesForm } from '@/app/store/components/product-management/main-components/AttributesForm' import type { ProductFormValues } from '@/lib/zod-schemas/product-schema' interface AttributesSectionProps { diff --git a/app/store/components/product-management/product-sections/images-section.tsx b/app/store/components/product-management/product-sections/images-section.tsx index 248d3dd4..04e8d248 100644 --- a/app/store/components/product-management/product-sections/images-section.tsx +++ b/app/store/components/product-management/product-sections/images-section.tsx @@ -1,5 +1,5 @@ import type { UseFormReturn } from 'react-hook-form' -import { ImageUpload } from '@/app/store/components/product-management/ImageUpload' +import { ImageUpload } from '@/app/store/components/product-management/main-components/ImageUpload' import type { ProductFormValues } from '@/lib/zod-schemas/product-schema' interface ImagesSectionProps { diff --git a/app/store/config/StoreLayoutClient.tsx b/app/store/config/StoreLayoutClient.tsx new file mode 100644 index 00000000..0cc992cf --- /dev/null +++ b/app/store/config/StoreLayoutClient.tsx @@ -0,0 +1,112 @@ +'use client' + +import { AppSidebar } from '@/app/store/components/sidebar/app-sidebar' +import { SidebarProvider, SidebarInset, SidebarTrigger } from '@/components/ui/sidebar' +import { Separator } from '@/components/ui/separator' +import { useEffect, useState } from 'react' +import { SearchNavigation } from '@/app/store/components/search-bar/SearchNavigation' +import { NotificationPopover } from '@/app/store/components/notifications/NotificationPopover' +import { PageTransition } from '@/components/ui/page-transition' +import { getStoreId } from '@/utils/store-utils' +import { useParams, usePathname } from 'next/navigation' +import { useStore } from '@/app/store/hooks/useStore' +import { ChatTrigger } from '@/app/store/components/ai-chat/ChatTrigger' +import { Amplify } from 'aws-amplify' +import outputs from '@/amplify_outputs.json' + +Amplify.configure(outputs) +const existingConfig = Amplify.getConfig() +Amplify.configure({ + ...existingConfig, + API: { + ...existingConfig.API, + REST: outputs.custom.APIs, + }, +}) + +export const StoreLayoutClient = ({ children }: { children: React.ReactNode }) => { + const pathname = usePathname() + const params = useParams() + const storeId = getStoreId(params, pathname) + useStore(storeId) + const [prefersReducedMotion, setPrefersReducedMotion] = useState(false) + + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)') + setPrefersReducedMotion(mediaQuery.matches) + + const handleChange = (e: MediaQueryListEvent) => { + setPrefersReducedMotion(e.matches) + } + + mediaQuery.addEventListener('change', handleChange) + return () => mediaQuery.removeEventListener('change', handleChange) + }, []) + + useEffect(() => { + const adjustViewport = () => { + if (window.innerWidth < 768) { + let viewportMeta = document.querySelector('meta[name="viewport"]') + if (!viewportMeta) { + viewportMeta = document.createElement('meta') + viewportMeta.setAttribute('name', 'viewport') + document.head.appendChild(viewportMeta) + } + + viewportMeta.setAttribute( + 'content', + 'width=device-width, initial-scale=0.95, maximum-scale=3, user-scalable=yes' + ) + } + } + + adjustViewport() + + window.addEventListener('resize', adjustViewport) + + return () => { + window.removeEventListener('resize', adjustViewport) + + const viewportMeta = document.querySelector('meta[name="viewport"]') + if (viewportMeta) { + viewportMeta.setAttribute( + 'content', + 'width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes' + ) + } + } + }, []) + + return ( + + + +
+
+ + +
+ +
+ +
+ +
+ + +
+
+ +
+ {children} +
+
+
+ ) +} diff --git a/app/store/hooks/useProductImageUpload.ts b/app/store/hooks/useProductImageUpload.ts index 1803223a..0215a191 100644 --- a/app/store/hooks/useProductImageUpload.ts +++ b/app/store/hooks/useProductImageUpload.ts @@ -2,7 +2,6 @@ import { useState } from 'react' import { uploadData } from 'aws-amplify/storage' import { Amplify } from 'aws-amplify' import { v4 as uuidv4 } from 'uuid' -import { getCurrentUser } from 'aws-amplify/auth' import outputs from '@/amplify_outputs.json' Amplify.configure(outputs) @@ -38,13 +37,10 @@ export function useProductImageUpload() { try { // Generar un UUID único para el archivo const uniqueFileName = `${uuidv4()}-${file.name.replace(/\s+/g, '-')}` - // Obtener el usuario actual para usar su ID en la ruta - const user = await getCurrentUser() - const userId = user.userId // Subir la imagen al bucket correcto const result = await uploadData({ - path: `products/${userId}/${uniqueFileName}`, + path: `products/${storeId}/${uniqueFileName}`, options: { bucket: 'fasttifyAssets', contentType: file.type, diff --git a/app/store/hooks/useS3Images.ts b/app/store/hooks/useS3Images.ts index 863422a0..2e31c114 100644 --- a/app/store/hooks/useS3Images.ts +++ b/app/store/hooks/useS3Images.ts @@ -21,6 +21,7 @@ interface S3ImagesResponse { images?: S3Image[] success?: boolean image?: S3Image + nextContinuationToken?: string } export function useS3Images(options: UseS3ImagesOptions = {}) { @@ -28,96 +29,132 @@ export function useS3Images(options: UseS3ImagesOptions = {}) { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const { storeId } = useStoreDataStore() + const [nextContinuationToken, setNextContinuationToken] = useState(undefined) + const [loadingMore, setLoadingMore] = useState(false) + + const fetchImages = async (token?: string) => { + if (!storeId) { + setLoading(false) + setImages([]) + setNextContinuationToken(undefined) + return + } - useEffect(() => { - const fetchImages = async () => { - if (!storeId) { - setLoading(false) - setImages([]) + if (!token) { + setLoading(true) + setImages([]) + } else { + setLoadingMore(true) + } + setError(null) + + try { + const restOperation = post({ + apiName: 'StoreImagesApi', + path: 'store-images', + options: { + body: { + action: 'list', + storeId, + limit: options.limit || 18, + prefix: options.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 } - setLoading(true) + const processedImages = response.images.map(img => ({ + ...img, + lastModified: img.lastModified ? new Date(img.lastModified) : undefined, + })) + + 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 { + if (!token) { + setLoading(false) + } else { + setLoadingMore(false) + } + } + } + + useEffect(() => { + fetchImages() + }, [storeId, options.prefix]) + + const fetchMoreImages = () => { + if (nextContinuationToken && !loadingMore && !loading) { + fetchImages(nextContinuationToken) + } + } + + const uploadImage = async (files: File[]): Promise => { + if (!storeId || files.length === 0) return null + + const uploadedImages: S3Image[] = [] + + for (const file of files) { try { + const base64File = await fileToBase64(file) + const restOperation = post({ apiName: 'StoreImagesApi', path: 'store-images', options: { body: { - action: 'list', + action: 'upload', storeId, - limit: options.limit || 1000, - prefix: options.prefix || '', - }, + filename: file.name, + contentType: file.type, + fileContent: base64File, + } as any, }, }) const { body } = await restOperation.response const response = (await body.json()) as S3ImagesResponse - if (!response.images) { - setImages([]) - return + if (!response.image) { + console.error('Failed to upload image:', file.name) + continue } - const processedImages = response.images.map(img => ({ - ...img, - lastModified: img.lastModified ? new Date(img.lastModified) : undefined, - })) + const newImage = { + ...response.image, + lastModified: response.image.lastModified + ? new Date(response.image.lastModified) + : new Date(), + } - setImages(processedImages) + uploadedImages.push(newImage) } catch (err) { - console.error('Error fetching S3 images:', err) - setError(err instanceof Error ? err : new Error('Unknown error occurred')) - } finally { - setLoading(false) - } - } - - fetchImages() - }, [storeId, options.prefix, options.limit]) - - const uploadImage = async (file: File): Promise => { - if (!storeId || !file) return null - - try { - const base64File = await fileToBase64(file) - - const restOperation = post({ - apiName: 'StoreImagesApi', - path: 'store-images', - options: { - body: { - action: 'upload', - storeId, - filename: file.name, - contentType: file.type, - fileContent: base64File, - }, - }, - }) - - const { body } = await restOperation.response - const response = (await body.json()) as S3ImagesResponse + console.error('Error uploading image:', file.name, err) - if (!response.image) { - throw new Error('Failed to upload image') + continue } + } - const newImage = { - ...response.image, - lastModified: response.image.lastModified - ? new Date(response.image.lastModified) - : new Date(), - } - - setImages(prev => [newImage, ...prev]) - - return newImage - } catch (err) { - console.error('Error uploading image:', err) - return null + if (uploadedImages.length > 0) { + setImages(prev => [...uploadedImages, ...prev]) } + + return uploadedImages.length > 0 ? uploadedImages : null } const deleteImage = async (key: string): Promise => { @@ -132,7 +169,7 @@ export function useS3Images(options: UseS3ImagesOptions = {}) { action: 'delete', storeId, key, - }, + } as any, }, }) @@ -168,5 +205,14 @@ export function useS3Images(options: UseS3ImagesOptions = {}) { }) } - return { images, loading, error, uploadImage, deleteImage } + return { + images, + loading, + error, + uploadImage, + deleteImage, + fetchMoreImages, + loadingMore, + nextContinuationToken, + } } diff --git a/app/store/layout.tsx b/app/store/layout.tsx index e7574e91..ffafa2be 100644 --- a/app/store/layout.tsx +++ b/app/store/layout.tsx @@ -1,124 +1,10 @@ -'use client' +import { StoreLayoutClient } from '@/app/store/config/StoreLayoutClient' -import { AppSidebar } from '@/app/store/components/sidebar/app-sidebar' -import { SidebarProvider, SidebarInset, SidebarTrigger } from '@/components/ui/sidebar' -import { Separator } from '@/components/ui/separator' -import { useEffect, useState } from 'react' -import { SearchNavigation } from '@/app/store/components/search-bar/SearchNavigation' -import { NotificationPopover } from '@/app/store/components/notifications/NotificationPopover' -import { PageTransition } from '@/components/ui/page-transition' -import { getStoreId } from '@/utils/store-utils' -import { useParams, usePathname } from 'next/navigation' -import { useStore } from '@/app/store/hooks/useStore' -import { ChatTrigger } from '@/app/store/components/ai-chat/ChatTrigger' -import { Amplify } from 'aws-amplify' -import outputs from '@/amplify_outputs.json' - -Amplify.configure(outputs) -const existingConfig = Amplify.getConfig() -Amplify.configure({ - ...existingConfig, - API: { - ...existingConfig.API, - REST: outputs.custom.APIs, - }, -}) +export const metadata = { + title: 'Mi tienda', + description: 'Dashboard de tu tienda en Fasttify', +} export default function StoreLayout({ children }: { children: React.ReactNode }) { - const pathname = usePathname() - const params = useParams() - const storeId = getStoreId(params, pathname) - useStore(storeId) - const [prefersReducedMotion, setPrefersReducedMotion] = useState(false) - - useEffect(() => { - document.title = 'Mi tienda • Fasttify' - - // Comprobar si el usuario prefiere reducir el movimiento - const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)') - setPrefersReducedMotion(mediaQuery.matches) - - const handleChange = (e: MediaQueryListEvent) => { - setPrefersReducedMotion(e.matches) - } - - mediaQuery.addEventListener('change', handleChange) - return () => mediaQuery.removeEventListener('change', handleChange) - }, []) - - // Efecto para ajustar el viewport en dispositivos móviles - useEffect(() => { - // Función para ajustar la escala de visualización - const adjustViewport = () => { - // Verificar si es un dispositivo móvil (ancho menor a 768px) - if (window.innerWidth < 768) { - // Crear o actualizar la meta tag de viewport - let viewportMeta = document.querySelector('meta[name="viewport"]') - if (!viewportMeta) { - viewportMeta = document.createElement('meta') - viewportMeta.setAttribute('name', 'viewport') - document.head.appendChild(viewportMeta) - } - - // Establecer una escala inicial más pequeña para las vistas de store - viewportMeta.setAttribute( - 'content', - 'width=device-width, initial-scale=0.95, maximum-scale=3, user-scalable=yes' - ) - } - } - - // Aplicar el ajuste al cargar la página - adjustViewport() - - // Aplicar el ajuste al cambiar el tamaño de la ventana - window.addEventListener('resize', adjustViewport) - - // Limpiar el event listener cuando el componente se desmonte - return () => { - window.removeEventListener('resize', adjustViewport) - - // Restaurar el viewport original al salir de las rutas /store - const viewportMeta = document.querySelector('meta[name="viewport"]') - if (viewportMeta) { - viewportMeta.setAttribute( - 'content', - 'width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes' - ) - } - } - }, []) - - return ( - - - -
-
- - -
- -
- -
- -
- - -
-
- -
- {children} -
-
-
- ) + return {children} } diff --git a/config/fonts.ts b/config/fonts.ts new file mode 100644 index 00000000..869e7362 --- /dev/null +++ b/config/fonts.ts @@ -0,0 +1,7 @@ +import { Plus_Jakarta_Sans } from 'next/font/google' + +export const plusJakartaSans = Plus_Jakarta_Sans({ + subsets: ['latin'], + weight: ['300', '400', '500', '700'], + display: 'swap', +}) diff --git a/hooks/ui/use-media-query.ts b/hooks/ui/use-media-query.ts index bc13fe34..afceb6a3 100644 --- a/hooks/ui/use-media-query.ts +++ b/hooks/ui/use-media-query.ts @@ -8,18 +8,14 @@ export function useMediaQuery(query: string): boolean { useEffect(() => { const mediaQuery = window.matchMedia(query) - // Set initial value setMatches(mediaQuery.matches) - // Create event listener function const handleChange = (event: MediaQueryListEvent) => { setMatches(event.matches) } - // Add event listener mediaQuery.addEventListener('change', handleChange) - // Clean up return () => { mediaQuery.removeEventListener('change', handleChange) } diff --git a/package-lock.json b/package-lock.json index e617026d..5bd9aef4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27313,9 +27313,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", - "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz", + "integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==", "cpu": [ "arm64" ], @@ -27335,9 +27335,9 @@ } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz", - "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz", + "integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==", "cpu": [ "x64" ], @@ -27501,9 +27501,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz", - "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz", + "integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==", "cpu": [ "arm" ], @@ -27523,9 +27523,9 @@ } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz", - "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz", + "integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==", "cpu": [ "arm64" ], @@ -27545,9 +27545,9 @@ } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz", - "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz", + "integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==", "cpu": [ "s390x" ], @@ -27567,9 +27567,9 @@ } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz", - "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz", + "integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==", "cpu": [ "x64" ], @@ -27589,9 +27589,9 @@ } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz", - "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz", + "integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==", "cpu": [ "arm64" ], @@ -27611,9 +27611,9 @@ } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz", - "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz", + "integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==", "cpu": [ "x64" ], @@ -27633,16 +27633,16 @@ } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz", - "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz", + "integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.4.0" + "@emnapi/runtime": "^1.4.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -27651,10 +27651,29 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz", + "integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz", - "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz", + "integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==", "cpu": [ "ia32" ], @@ -27671,9 +27690,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz", - "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz", + "integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==", "cpu": [ "x64" ], @@ -48047,9 +48066,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "devOptional": true, "license": "ISC", "bin": { @@ -48141,16 +48160,16 @@ "license": "MIT" }, "node_modules/sharp": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", - "integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==", + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz", + "integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==", "hasInstallScript": true, "license": "Apache-2.0", "optional": true, "dependencies": { "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.7.1" + "detect-libc": "^2.0.4", + "semver": "^7.7.2" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -48159,8 +48178,8 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.1", - "@img/sharp-darwin-x64": "0.34.1", + "@img/sharp-darwin-arm64": "0.34.2", + "@img/sharp-darwin-x64": "0.34.2", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", @@ -48170,15 +48189,16 @@ "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", - "@img/sharp-linux-arm": "0.34.1", - "@img/sharp-linux-arm64": "0.34.1", - "@img/sharp-linux-s390x": "0.34.1", - "@img/sharp-linux-x64": "0.34.1", - "@img/sharp-linuxmusl-arm64": "0.34.1", - "@img/sharp-linuxmusl-x64": "0.34.1", - "@img/sharp-wasm32": "0.34.1", - "@img/sharp-win32-ia32": "0.34.1", - "@img/sharp-win32-x64": "0.34.1" + "@img/sharp-linux-arm": "0.34.2", + "@img/sharp-linux-arm64": "0.34.2", + "@img/sharp-linux-s390x": "0.34.2", + "@img/sharp-linux-x64": "0.34.2", + "@img/sharp-linuxmusl-arm64": "0.34.2", + "@img/sharp-linuxmusl-x64": "0.34.2", + "@img/sharp-wasm32": "0.34.2", + "@img/sharp-win32-arm64": "0.34.2", + "@img/sharp-win32-ia32": "0.34.2", + "@img/sharp-win32-x64": "0.34.2" } }, "node_modules/sharp/node_modules/detect-libc": {