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.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.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 (