From f18c40b12c65a1939d233b38df7587c778c14a6b Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Wed, 21 May 2025 20:31:20 -0500 Subject: [PATCH 1/4] feat(product-management): refactor product management components and update image handling This commit introduces several changes to the product management section, including the restructuring of component imports to a new main-components directory. It enhances the ImageSelectorModal to support multiple image selection and updates the ImageUpload component for better image handling. Additionally, new components for inventory management and product attributes are added, improving the overall organization and functionality of the product management features. --- .../[slug]/products/[productId]/page.tsx | 2 +- app/store/[slug]/products/new/page.tsx | 2 +- app/store/[slug]/products/page.tsx | 2 +- .../images-selector/image-selector-modal.tsx | 100 +++++++++--- .../collection-form/image-section.tsx | 6 +- .../collection-form/publication-section.tsx | 5 +- .../{ => main-components}/AttributesForm.tsx | 0 .../{ => main-components}/ImageUpload.tsx | 146 ++++-------------- .../InventoryManager.tsx | 6 +- .../{ => main-components}/InventoryPage.tsx | 0 .../InventoryTracking.tsx | 0 .../{ => main-components}/ProductForm.tsx | 0 .../{ => main-components}/ProductList.tsx | 0 .../{ => main-components}/ProductManager.tsx | 6 +- .../{ => main-components}/ProductPage.tsx | 0 .../product-sections/attributes-section.tsx | 2 +- .../product-sections/images-section.tsx | 2 +- app/store/hooks/useProductImageUpload.ts | 6 +- hooks/ui/use-media-query.ts | 4 - package-lock.json | 126 ++++++++------- 20 files changed, 201 insertions(+), 214 deletions(-) rename app/store/components/product-management/{ => main-components}/AttributesForm.tsx (100%) rename app/store/components/product-management/{ => main-components}/ImageUpload.tsx (60%) rename app/store/components/product-management/{ => main-components}/InventoryManager.tsx (84%) rename app/store/components/product-management/{ => main-components}/InventoryPage.tsx (100%) rename app/store/components/product-management/{ => main-components}/InventoryTracking.tsx (100%) rename app/store/components/product-management/{ => main-components}/ProductForm.tsx (100%) rename app/store/components/product-management/{ => main-components}/ProductList.tsx (100%) rename app/store/components/product-management/{ => main-components}/ProductManager.tsx (92%) rename app/store/components/product-management/{ => main-components}/ProductPage.tsx (100%) 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/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/image-selector-modal.tsx b/app/store/components/images-selector/image-selector-modal.tsx index b40aa1d7..a3625228 100644 --- a/app/store/components/images-selector/image-selector-modal.tsx +++ b/app/store/components/images-selector/image-selector-modal.tsx @@ -16,8 +16,9 @@ import Image from 'next/image' 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,33 +26,57 @@ 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, }) - // 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) } @@ -110,6 +135,11 @@ export default function ImageSelectorModal({ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { const file = e.dataTransfer.files[0] + if (!file.type.startsWith('image/')) { + console.warn('Dropped file is not an image:', file.type) + return + } + try { const uploadedImage = await uploadImage(file) if (uploadedImage) { @@ -241,16 +271,18 @@ export default function ImageSelectorModal({ {filteredImages.map((image, index) => (
handleImageSelect(image)} >
handleImageSelect(image)} onClick={e => e.stopPropagation()} /> @@ -275,10 +307,22 @@ export default function ImageSelectorModal({ width={300} height={300} quality={75} - priority={selectedImage === image.key || index < 12} + priority={ + allowMultipleSelection + ? Array.isArray(selectedImage) && selectedImage.includes(image.key) + : selectedImage === image.key || index < 12 + } className="object-cover w-full h-full hover:scale-105 transition-transform duration-200" style={{ objectFit: 'cover' }} - loading={selectedImage === image.key || index < 12 ? undefined : 'lazy'} + loading={ + ( + allowMultipleSelection + ? Array.isArray(selectedImage) && selectedImage.includes(image.key) + : selectedImage === image.key || index < 12 + ) + ? undefined + : 'lazy' + } placeholder="blur" blurDataURL="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMGYwIi8+PC9zdmc+" /> @@ -301,16 +345,18 @@ export default function ImageSelectorModal({ {filteredImages.map((image, index) => (
handleImageSelect(image)} >
handleImageSelect(image)} onClick={e => e.stopPropagation()} /> @@ -322,10 +368,22 @@ export default function ImageSelectorModal({ width={96} height={96} quality={75} - priority={selectedImage === image.key || index < 20} + priority={ + allowMultipleSelection + ? Array.isArray(selectedImage) && selectedImage.includes(image.key) + : selectedImage === image.key || index < 20 + } className="object-cover w-full h-full rounded" style={{ objectFit: 'cover' }} - loading={selectedImage === image.key || index < 20 ? undefined : 'lazy'} + loading={ + ( + allowMultipleSelection + ? Array.isArray(selectedImage) && selectedImage.includes(image.key) + : selectedImage === image.key || index < 20 + ) + ? undefined + : 'lazy' + } placeholder="blur" blurDataURL="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTYiIGhlaWdodD0iOTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iI2YwZjBmMCIvPjwvc3ZnPg==" /> 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/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/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": { From 4422ee89c7e11a712ff269dc1bb9c1fc4a724be4 Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Wed, 21 May 2025 21:16:36 -0500 Subject: [PATCH 2/4] feat(image-management): enhance image listing and upload functionality This commit updates the image handling in the storeImages function to support pagination with a continuation token, allowing for more efficient image retrieval. The ImageSelectorModal component is modified to handle multiple image uploads and includes a new ImageGallery component for better organization of displayed images. Additionally, the useS3Images hook is enhanced to manage loading states and fetch more images as needed, improving the overall user experience in image management. --- amplify/functions/storeImages/handler.ts | 15 +- .../images-selector/ImageGallery.tsx | 176 +++++++++++ .../images-selector/image-selector-modal.tsx | 292 +++++++----------- app/store/hooks/useS3Images.ts | 180 +++++++---- 4 files changed, 405 insertions(+), 258 deletions(-) create mode 100644 app/store/components/images-selector/ImageGallery.tsx 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/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 a3625228..37ce507e 100644 --- a/app/store/components/images-selector/image-selector-modal.tsx +++ b/app/store/components/images-selector/image-selector-modal.tsx @@ -12,6 +12,7 @@ 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 @@ -41,8 +42,17 @@ export default function ImageSelectorModal({ const [isUploading, setIsUploading] = useState(false) const [uploadPreview, setUploadPreview] = useState(null) - const { images, loading, error, uploadImage, deleteImage } = useS3Images({ - limit: 100, + const { + images, + loading, + error, + uploadImage, + deleteImage, + fetchMoreImages, + loadingMore, + nextContinuationToken, + } = useS3Images({ + limit: 18, }) const filteredImages = images.filter(img => img.filename.toLowerCase().includes(searchTerm.toLowerCase()) @@ -81,30 +91,44 @@ export default function ImageSelectorModal({ 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) @@ -133,38 +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) - if (!file.type.startsWith('image/')) { - console.warn('Dropped file is not an image:', file.type) + 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 */}
@@ -213,6 +275,7 @@ export default function ImageSelectorModal({ onChange={handleFileUpload} accept="image/*" className="hidden" + multiple={allowMultipleSelection} /> -
-
- {image.filename} -
+ {/* Render the extracted ImageGallery component */} + -
-
{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/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, + } } From e80e2bc4cd432c27c2f2a8d4b8d6f81443edb6b4 Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Thu, 22 May 2025 15:10:50 -0500 Subject: [PATCH 3/4] refactor(layout): update font import and metadata, clean up unused code This commit refactors the layout components by changing the font import to use a custom configuration. It also updates the metadata for various pages, enhancing the title and description for better SEO. Additionally, it removes unused code and comments across several files, improving overall code cleanliness and maintainability. --- app/(main-layout)/account-settings/page.tsx | 4 +- app/(main-layout)/page.tsx | 2 +- .../pricing/components/PricingCard.tsx | 1 - app/(main-layout)/pricing/layout.tsx | 7 + app/(main-layout)/pricing/page.tsx | 5 - .../first-steps/hooks/useFirstStepsSetup.ts | 210 ++++++++++++++++++ app/(setup-layout)/first-steps/layout.tsx | 7 + app/(setup-layout)/first-steps/page.tsx | 203 ++--------------- .../{ => main-components}/AuthClient.tsx | 4 +- .../main-components}/AuthForm.tsx | 0 .../{ => main-components}/ImageSlider.tsx | 0 app/(setup-layout)/login/page.tsx | 6 +- app/(setup-layout)/my-store/page.tsx | 11 +- app/layout.tsx | 33 +-- app/store/config/StoreLayoutClient.tsx | 112 ++++++++++ app/store/layout.tsx | 126 +---------- config/fonts.ts | 7 + 17 files changed, 383 insertions(+), 355 deletions(-) create mode 100644 app/(main-layout)/pricing/layout.tsx create mode 100644 app/(setup-layout)/first-steps/hooks/useFirstStepsSetup.ts create mode 100644 app/(setup-layout)/first-steps/layout.tsx rename app/(setup-layout)/login/components/{ => main-components}/AuthClient.tsx (63%) rename app/(setup-layout)/login/{ => components/main-components}/AuthForm.tsx (100%) rename app/(setup-layout)/login/components/{ => main-components}/ImageSlider.tsx (100%) create mode 100644 app/store/config/StoreLayoutClient.tsx create mode 100644 config/fonts.ts 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/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/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', +}) From e9cc0bc136697cf891441e33be07255a133a2cbb Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Thu, 22 May 2025 15:54:20 -0500 Subject: [PATCH 4/4] chore(amplify): remove Node.js installation command from preBuild phase This commit simplifies the amplify.yml file by removing the explicit Node.js installation command, relying instead on the nvm use command to ensure the correct version is utilized during the build process. Additionally, it updates the import path for the InventoryManager component to reflect its new location in the main-components directory, improving code organization. --- amplify.yml | 1 - app/store/[slug]/products/inventory/page.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) 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/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'