From c058b072a12a08791360b2f0e6283b08e43c1f18 Mon Sep 17 00:00:00 2001 From: Stivenjs Date: Tue, 10 Jun 2025 19:50:52 -0500 Subject: [PATCH] refactor(images-selector): integrate Shopify Polaris components; remove unused components, enhance image selection modal, and improve upload functionality --- .../components/ImageGallery.tsx | 217 +++------ .../components/ModalFooter.tsx | 24 - .../components/SearchAndFilters.tsx | 57 +-- .../components/UploadDropZone.tsx | 82 ++-- .../components/UploadPreview.tsx | 25 - .../components/image-selector-modal.tsx | 153 +++--- .../hooks/useImageSelection.ts | 21 +- .../images-selector/hooks/useImageUpload.ts | 84 +--- app/store/components/images-selector/index.ts | 2 - .../components/CollectionContent.tsx | 67 +-- .../components/CollectionFooter.tsx | 45 -- .../components/CollectionHeader.tsx | 28 -- .../components/CollectionSidebar.tsx | 38 +- .../components/ProductControls.tsx | 73 +-- .../components/ProductItem.tsx | 40 -- .../components/ProductSelectionDialog.tsx | 165 +++---- .../components/SelectedProductsList.tsx | 60 ++- .../collection-form/description-editor.tsx | 146 +++--- .../collection-form/form-page.tsx | 79 +-- .../hooks/useProductSelection.ts | 10 +- .../collection-form/image-section.tsx | 168 +++---- .../collection-form/index.ts | 28 +- .../collection-form/product-section.tsx | 17 +- .../collection-form/publication-section.tsx | 111 ++--- .../collection-form/search-input.tsx | 27 -- .../hooks/useProductFilters.ts | 4 +- .../main-components/AttributesForm.tsx | 340 ++++++------- .../main-components/ImageUpload.tsx | 241 +++------- .../main-components/InventoryManager.tsx | 1 - .../main-components/ProductForm.tsx | 409 +++++++--------- .../product-sections/ai-generate-button.tsx | 91 +--- .../product-sections/basic-info-section.tsx | 452 +++++++----------- .../price-suggestion-panel.tsx | 218 +++++---- .../pricing-inventory-section.tsx | 325 ++++++------- .../product-sections/pricing-section.tsx | 158 ------ .../product-sections/publication-section.tsx | 97 ++-- .../product-sections/text-shimmer.tsx | 54 --- .../utils/collection-form-utils.ts | 25 +- components/ui/unsaved-changes-alert.tsx | 96 ---- 39 files changed, 1507 insertions(+), 2771 deletions(-) delete mode 100644 app/store/components/images-selector/components/ModalFooter.tsx delete mode 100644 app/store/components/images-selector/components/UploadPreview.tsx delete mode 100644 app/store/components/product-management/collection-form/components/CollectionFooter.tsx delete mode 100644 app/store/components/product-management/collection-form/components/CollectionHeader.tsx delete mode 100644 app/store/components/product-management/collection-form/components/ProductItem.tsx delete mode 100644 app/store/components/product-management/collection-form/search-input.tsx delete mode 100644 app/store/components/product-management/product-sections/text-shimmer.tsx delete mode 100644 components/ui/unsaved-changes-alert.tsx diff --git a/app/store/components/images-selector/components/ImageGallery.tsx b/app/store/components/images-selector/components/ImageGallery.tsx index 7e01f071..ab272e53 100644 --- a/app/store/components/images-selector/components/ImageGallery.tsx +++ b/app/store/components/images-selector/components/ImageGallery.tsx @@ -1,12 +1,9 @@ -import { Trash2 } from 'lucide-react' -import Image from 'next/image' -import { Button } from '@/components/ui/button' -import { Loader } from '@/components/ui/loader' +import { Grid, Card, Thumbnail, Text, Spinner, EmptyState, Button, Box } from '@shopify/polaris' +import { DeleteIcon } from '@shopify/polaris-icons' import { S3Image } from '@/app/store/hooks/useS3Images' interface ImageGalleryProps { images: S3Image[] - viewMode: 'grid' | 'list' selectedImage: string | string[] | null allowMultipleSelection: boolean loading: boolean @@ -18,7 +15,6 @@ interface ImageGalleryProps { export default function ImageGallery({ images, - viewMode, selectedImage, allowMultipleSelection, loading, @@ -27,159 +23,90 @@ export default function ImageGallery({ onImageSelect, onDeleteImage, }: ImageGalleryProps) { - // Función para generar un ID único si no existe - const getImageKey = (image: S3Image, index: number): string => { - if (image.id) { - return image.id - } - // Fallback: generar clave única combinando key, index y filename - return `${image.key}-${index}-${image.filename || 'unknown'}` - } - - if (!loading && images.length === 0) { + if (loading) { 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) { + if (error) { return ( -
- -
+ +

Hubo un problema al recuperar tus imágenes. Por favor, intenta de nuevo.

+
) } - if (error) { + if (images.length === 0) { return ( -
- Error al cargar las imágenes. Por favor, intenta de nuevo. -
+ {}, // Se podría conectar a un trigger de subida + }} + image="https://cdn.shopify.com/s/files/1/0262/4071/2726/files/emptystate-files.png" + > +

+ {searchTerm + ? 'Prueba con un término de búsqueda diferente.' + : 'Arrastra y suelta archivos para subirlos.'} +

+
) } + const isSelected = (image: S3Image) => { + if (allowMultipleSelection && Array.isArray(selectedImage)) { + return selectedImage.includes(image.key) + } + return selectedImage === image.key + } + 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` : ''} +
-
- -
- ))} -
- )} - + + + + + {image.filename} + + + + + + ))} + ) } diff --git a/app/store/components/images-selector/components/ModalFooter.tsx b/app/store/components/images-selector/components/ModalFooter.tsx deleted file mode 100644 index 8b1352ab..00000000 --- a/app/store/components/images-selector/components/ModalFooter.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Button } from '@/components/ui/button' - -interface ModalFooterProps { - onCancel: () => void - onConfirm: () => void - hasSelection: boolean -} - -export default function ModalFooter({ onCancel, onConfirm, hasSelection }: ModalFooterProps) { - return ( -
- - -
- ) -} diff --git a/app/store/components/images-selector/components/SearchAndFilters.tsx b/app/store/components/images-selector/components/SearchAndFilters.tsx index 11a561db..b40d1ea0 100644 --- a/app/store/components/images-selector/components/SearchAndFilters.tsx +++ b/app/store/components/images-selector/components/SearchAndFilters.tsx @@ -1,56 +1,25 @@ -import { Search, Grid, List } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, -} from '@/components/ui/dropdown-menu' +import { TextField, InlineStack } from '@shopify/polaris' +import { SearchIcon } from '@shopify/polaris-icons' interface SearchAndFiltersProps { searchTerm: string onSearchChange: (value: string) => void - viewMode: 'grid' | 'list' - onViewModeChange: (mode: 'grid' | 'list') => void } -export default function SearchAndFilters({ - searchTerm, - onSearchChange, - viewMode, - onViewModeChange, -}: SearchAndFiltersProps) { +export default function SearchAndFilters({ searchTerm, onSearchChange }: SearchAndFiltersProps) { return ( -
-
- - +
+ onSearchChange(e.target.value)} + onChange={onSearchChange} + prefix={} + autoComplete="off" />
-
- - - - - - onViewModeChange('grid')}> - - Cuadrícula - - onViewModeChange('list')}> - - Lista - - - -
-
+ ) } diff --git a/app/store/components/images-selector/components/UploadDropZone.tsx b/app/store/components/images-selector/components/UploadDropZone.tsx index f0afd56a..77b69a19 100644 --- a/app/store/components/images-selector/components/UploadDropZone.tsx +++ b/app/store/components/images-selector/components/UploadDropZone.tsx @@ -1,42 +1,56 @@ -import { Upload } from 'lucide-react' -import { Button } from '@/components/ui/button' +import { DropZone, LegacyStack, Thumbnail, Text, BlockStack } from '@shopify/polaris' +import { NoteIcon } from '@shopify/polaris-icons' +import { useCallback, useState } from 'react' interface UploadDropZoneProps { - onDrop: (e: React.DragEvent) => void - onDragOver: (e: React.DragEvent) => void - onFileUpload: (e: React.ChangeEvent) => void - triggerFileSelect: () => void - fileInputRef: React.RefObject + onDrop: (files: File[]) => void allowMultipleSelection: boolean } -export default function UploadDropZone({ - onDrop, - onDragOver, - onFileUpload, - triggerFileSelect, - fileInputRef, - allowMultipleSelection, -}: UploadDropZoneProps) { +export default function UploadDropZone({ onDrop, allowMultipleSelection }: UploadDropZoneProps) { + const [files, setFiles] = useState([]) + + const handleDropZoneDrop = useCallback( + (_dropFiles: File[], acceptedFiles: File[], _rejectedFiles: File[]) => { + setFiles(prevFiles => [...prevFiles, ...acceptedFiles]) + onDrop(acceptedFiles) + }, + [onDrop] + ) + + const validImageTypes = ['image/gif', 'image/jpeg', 'image/png'] + + const uploadedFiles = + files.length > 0 ? ( +
+ + {files.map((file, index) => ( + + +
+ {file.name}{' '} + + {file.size} bytes + +
+
+ ))} +
+
+ ) : null + return ( -
- - -

Arrastrar y soltar imágenes aquí

-
+ + + + + {uploadedFiles} + ) } diff --git a/app/store/components/images-selector/components/UploadPreview.tsx b/app/store/components/images-selector/components/UploadPreview.tsx deleted file mode 100644 index 170e23d2..00000000 --- a/app/store/components/images-selector/components/UploadPreview.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Image from 'next/image' -import { Loader } from '@/components/ui/loader' - -interface UploadPreviewProps { - uploadPreview: string - isUploading: boolean -} - -export default function UploadPreview({ uploadPreview, isUploading }: UploadPreviewProps) { - if (!isUploading || !uploadPreview) { - return null - } - - return ( -
-
- Preview -
-
- - Subiendo imagen... -
-
- ) -} diff --git a/app/store/components/images-selector/components/image-selector-modal.tsx b/app/store/components/images-selector/components/image-selector-modal.tsx index e075ad7b..c0ff0bb5 100644 --- a/app/store/components/images-selector/components/image-selector-modal.tsx +++ b/app/store/components/images-selector/components/image-selector-modal.tsx @@ -1,6 +1,5 @@ import { useState, useCallback, useMemo } from 'react' -import { Loader } from '@/components/ui/loader' -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Modal, BlockStack, Spinner, Box } from '@shopify/polaris' import { useS3Images, type S3Image } from '@/app/store/hooks/useS3Images' import ImageGallery from '@/app/store/components/images-selector/components/ImageGallery' @@ -11,14 +10,12 @@ import { useImageUpload } from '@/app/store/components/images-selector/hooks/use // Componentes modulares import SearchAndFilters from '@/app/store/components/images-selector/components/SearchAndFilters' import UploadDropZone from '@/app/store/components/images-selector/components/UploadDropZone' -import UploadPreview from '@/app/store/components/images-selector/components/UploadPreview' -import ModalFooter from '@/app/store/components/images-selector/components/ModalFooter' interface ImageSelectorModalProps { open: boolean onOpenChange: (open: boolean) => void onSelect?: (images: S3Image | S3Image[] | null) => void - initialSelectedImage?: string | null + initialSelectedImage?: string | string[] | null allowMultipleSelection?: boolean } @@ -29,13 +26,9 @@ export default function ImageSelectorModal({ initialSelectedImage = null, allowMultipleSelection = false, }: ImageSelectorModalProps) { - const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid') const [searchTerm, setSearchTerm] = useState('') - - // Memoizar las opciones para evitar re-renders del hook const s3Options = useMemo(() => ({ limit: 18 }), []) - // Hook principal de S3 const { images, loading, @@ -47,7 +40,6 @@ export default function ImageSelectorModal({ nextContinuationToken, } = useS3Images(s3Options) - // Hook para manejo de selección const { selectedImage, handleImageSelect, @@ -60,44 +52,35 @@ export default function ImageSelectorModal({ images, }) - // Hook para manejo de upload - const { - isUploading, - uploadPreview, - fileInputRef, - handleFileUpload, - handleDrop, - handleDragOver, - triggerFileSelect, - } = useImageUpload({ + const { isUploading, handleDrop } = useImageUpload({ uploadImage, - allowMultipleSelection, onImagesUploaded: uploadedImages => { const keys = uploadedImages.map(img => img.key) - addToSelection(keys) + if (allowMultipleSelection) { + addToSelection(keys) + } else { + handleImageSelect(uploadedImages[0]) + } }, }) - // Filtrado de imágenes const filteredImages = useMemo( () => images.filter(img => img.filename.toLowerCase().includes(searchTerm.toLowerCase())), [images, searchTerm] ) - // Manejo de confirmación const handleConfirm = useCallback(() => { const selectedImages = getSelectedImages() - - if (allowMultipleSelection) { - onSelect?.(selectedImages.length > 0 ? selectedImages : null) - } else { - onSelect?.(selectedImages[0] || null) + if (onSelect) { + if (allowMultipleSelection) { + onSelect(selectedImages.length > 0 ? selectedImages : null) + } else { + onSelect(selectedImages[0] || null) + } } - onOpenChange(false) }, [allowMultipleSelection, getSelectedImages, onSelect, onOpenChange]) - // Manejo de eliminación de imágenes const handleDeleteImage = useCallback( async (key: string) => { try { @@ -112,7 +95,6 @@ export default function ImageSelectorModal({ [deleteImage, removeFromSelection] ) - // Manejo del scroll infinito const handleScroll = useCallback( (e: React.UIEvent) => { const target = e.target as HTMLDivElement @@ -125,7 +107,6 @@ export default function ImageSelectorModal({ [nextContinuationToken, loadingMore, loading, fetchMoreImages] ) - // Verificar si hay selección const hasSelection = useMemo(() => { if (allowMultipleSelection) { return Array.isArray(selectedImage) && selectedImage.length > 0 @@ -134,65 +115,51 @@ export default function ImageSelectorModal({ }, [selectedImage, allowMultipleSelection]) return ( - - - -
- Seleccionar imagen -
-
- -
- {/* Search and filters */} - - - {/* Upload drop zone */} - - - {/* Upload preview */} - - - {/* Image gallery */} - - - {/* Loading indicator for more images */} - {loadingMore && ( -
- - Cargando más imágenes... -
- )} -
- - {/* Footer */} - onOpenChange(false)} - onConfirm={handleConfirm} - hasSelection={hasSelection} - /> -
-
+ onOpenChange(false)} + title="Seleccionar imagen" + primaryAction={{ + content: 'Confirmar', + onAction: handleConfirm, + disabled: !hasSelection, + }} + secondaryActions={[ + { + content: 'Cancelar', + onAction: () => onOpenChange(false), + }, + ]} + > +
+ + + + + {isUploading && ( + + + + )} + + {loadingMore && ( + + + + )} + + +
+
) } diff --git a/app/store/components/images-selector/hooks/useImageSelection.ts b/app/store/components/images-selector/hooks/useImageSelection.ts index 9e71c13c..8663096c 100644 --- a/app/store/components/images-selector/hooks/useImageSelection.ts +++ b/app/store/components/images-selector/hooks/useImageSelection.ts @@ -2,7 +2,7 @@ import { useState, useCallback } from 'react' import type { S3Image } from '@/app/store/hooks/useS3Images' interface UseImageSelectionProps { - initialSelectedImage?: string | null + initialSelectedImage?: string | string[] | null allowMultipleSelection?: boolean images: S3Image[] } @@ -12,13 +12,18 @@ export function useImageSelection({ allowMultipleSelection = false, images, }: UseImageSelectionProps) { - const [selectedImage, setSelectedImage] = useState( - allowMultipleSelection - ? initialSelectedImage - ? [initialSelectedImage] - : [] - : initialSelectedImage - ) + const [selectedImage, setSelectedImage] = useState(() => { + if (allowMultipleSelection) { + if (Array.isArray(initialSelectedImage)) { + return initialSelectedImage + } + if (typeof initialSelectedImage === 'string') { + return [initialSelectedImage] + } + return [] + } + return initialSelectedImage + }) const handleImageSelect = useCallback( (image: S3Image) => { diff --git a/app/store/components/images-selector/hooks/useImageUpload.ts b/app/store/components/images-selector/hooks/useImageUpload.ts index 34219c8f..243491b8 100644 --- a/app/store/components/images-selector/hooks/useImageUpload.ts +++ b/app/store/components/images-selector/hooks/useImageUpload.ts @@ -1,38 +1,15 @@ -import { useState, useCallback, useRef } from 'react' +import { useState, useCallback } from 'react' import type { S3Image } from '@/app/store/hooks/useS3Images' interface UseImageUploadProps { uploadImage: (files: File[]) => Promise - allowMultipleSelection?: boolean onImagesUploaded?: (images: S3Image[]) => void } -export function useImageUpload({ - uploadImage, - allowMultipleSelection = false, - onImagesUploaded, -}: UseImageUploadProps) { +export function useImageUpload({ uploadImage, onImagesUploaded }: UseImageUploadProps) { const [isUploading, setIsUploading] = useState(false) - const [uploadPreview, setUploadPreview] = useState(null) - const fileInputRef = useRef(null) - const fileToBase64 = useCallback((file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.readAsDataURL(file) - reader.onload = () => { - if (typeof reader.result === 'string') { - const base64 = reader.result.split(',')[1] - resolve(base64) - } else { - reject(new Error('Failed to convert file to base64')) - } - } - reader.onerror = error => reject(error) - }) - }, []) - - const processFiles = useCallback( + const handleDrop = useCallback( async (files: File[]) => { const imageFiles = files.filter(file => file.type.startsWith('image/')) @@ -42,20 +19,6 @@ export function useImageUpload({ } setIsUploading(true) - setUploadPreview(null) - - // Set preview for first image - try { - const reader = new FileReader() - reader.onload = e => { - if (e.target?.result) { - setUploadPreview(e.target.result as string) - } - } - reader.readAsDataURL(imageFiles[0]) - } catch (error) { - console.error('Error setting preview:', error) - } try { const uploadedImages = await uploadImage(imageFiles) @@ -69,54 +32,13 @@ export function useImageUpload({ return null } finally { setIsUploading(false) - setUploadPreview(null) - if (fileInputRef.current) { - fileInputRef.current.value = '' - } } }, [uploadImage, onImagesUploaded] ) - const handleFileUpload = useCallback( - async (event: React.ChangeEvent) => { - const files = event.target.files - if (!files || files.length === 0) return - - const filesArray = Array.from(files) - await processFiles(filesArray) - }, - [processFiles] - ) - - const handleDrop = useCallback( - async (e: React.DragEvent) => { - e.preventDefault() - - if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { - const filesArray = Array.from(e.dataTransfer.files) - await processFiles(filesArray) - } - }, - [processFiles] - ) - - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault() - }, []) - - const triggerFileSelect = useCallback(() => { - fileInputRef.current?.click() - }, []) - return { isUploading, - uploadPreview, - fileInputRef, - handleFileUpload, handleDrop, - handleDragOver, - triggerFileSelect, - allowMultipleSelection, } } diff --git a/app/store/components/images-selector/index.ts b/app/store/components/images-selector/index.ts index cab2c3f7..d94b10de 100644 --- a/app/store/components/images-selector/index.ts +++ b/app/store/components/images-selector/index.ts @@ -8,8 +8,6 @@ export { useImageUpload } from '@/app/store/components/images-selector/hooks/use // Componentes modulares export { default as SearchAndFilters } from '@/app/store/components/images-selector/components/SearchAndFilters' export { default as UploadDropZone } from '@/app/store/components/images-selector/components/UploadDropZone' -export { default as UploadPreview } from '@/app/store/components/images-selector/components/UploadPreview' -export { default as ModalFooter } from '@/app/store/components/images-selector/components/ModalFooter' export { default as ImageGallery } from '@/app/store/components/images-selector/components/ImageGallery' // Re-exportar tipos del hook principal diff --git a/app/store/components/product-management/collection-form/components/CollectionContent.tsx b/app/store/components/product-management/collection-form/components/CollectionContent.tsx index e8824b85..7afe0f5b 100644 --- a/app/store/components/product-management/collection-form/components/CollectionContent.tsx +++ b/app/store/components/product-management/collection-form/components/CollectionContent.tsx @@ -1,6 +1,4 @@ -import { Edit } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' +import { Card, TextField, BlockStack, Text } from '@shopify/polaris' import { DescriptionEditor } from '@/app/store/components/product-management/collection-form/description-editor' import { ProductSection } from '@/app/store/components/product-management/collection-form/product-section' import { IProduct } from '@/app/store/components/product-management/collection-form/types/productTypes' @@ -20,63 +18,42 @@ interface CollectionContentProps { export function CollectionContent({ title, description, - slug, selectedProducts, currentStoreCustomDomain, + slug, onTitleChange, onDescriptionChange, onAddProduct, onRemoveProduct, }: CollectionContentProps) { return ( -
- {/* Title Section */} -
-
-
- - onTitleChange(e.target.value)} - className="border-gray-300" - /> -
- -
- - -
-
-
+ + + + + + + - {/* Products Section */} - {/* SEO Section */} -
-
-

Publicación del motor de búsqueda

- -
-
-
Mi tienda
-
+ + + + Publicación del motor de búsqueda + + {currentStoreCustomDomain} › collections › {slug || 'frontpage'} -
-
{title}
-
-
-
+ + + {title} + + + + ) } diff --git a/app/store/components/product-management/collection-form/components/CollectionFooter.tsx b/app/store/components/product-management/collection-form/components/CollectionFooter.tsx deleted file mode 100644 index 1433e893..00000000 --- a/app/store/components/product-management/collection-form/components/CollectionFooter.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Button } from '@/components/ui/button' -import { Loader } from '@/components/ui/loader' - -interface CollectionFooterProps { - isEditing: boolean - isSubmitting: boolean - onSave: () => void - onDelete?: () => void -} - -export function CollectionFooter({ - isEditing, - isSubmitting, - onSave, - onDelete, -}: CollectionFooterProps) { - return ( -
- {isEditing && onDelete && ( - - )} - -
- ) -} diff --git a/app/store/components/product-management/collection-form/components/CollectionHeader.tsx b/app/store/components/product-management/collection-form/components/CollectionHeader.tsx deleted file mode 100644 index b5d20ec0..00000000 --- a/app/store/components/product-management/collection-form/components/CollectionHeader.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { ArrowLeft } from 'lucide-react' -import { Button } from '@/components/ui/button' - -interface CollectionHeaderProps { - isEditing: boolean - onBack: () => void -} - -export function CollectionHeader({ isEditing, onBack }: CollectionHeaderProps) { - return ( -
-
- -

- {isEditing ? 'Editar colección' : 'Nueva colección'} -

-
- -
- ) -} diff --git a/app/store/components/product-management/collection-form/components/CollectionSidebar.tsx b/app/store/components/product-management/collection-form/components/CollectionSidebar.tsx index 79b4f6a5..828cdfa1 100644 --- a/app/store/components/product-management/collection-form/components/CollectionSidebar.tsx +++ b/app/store/components/product-management/collection-form/components/CollectionSidebar.tsx @@ -1,10 +1,4 @@ -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' +import { Card, Select, BlockStack } from '@shopify/polaris' import { PublicationSection } from '@/app/store/components/product-management/collection-form/publication-section' import { ImageSection } from '@/app/store/components/product-management/collection-form/image-section' @@ -21,26 +15,24 @@ export function CollectionSidebar({ onActiveChange, onImageChange, }: CollectionSidebarProps) { + const templateOptions = [{ label: 'Colección predeterminada', value: 'default' }] + return ( -
- {/* Publication Section */} + - {/* Image Section */} - {/* Theme Template Section */} -
-

Plantilla de tema

- -
-
+ + - - - - - Más recientes - Más antiguos - Mayor precio - Menor precio - - -
+
+ +
+ } + autoComplete="off" + /> +
+ - - - - - Párrafo - Encabezado - - -
- - - - -
+ const handleFormat = (command: 'bold' | 'italic' | 'underline') => { + handleExecCommand(command) + } - {/* Resto de los botones de la barra de herramientas */} -
+ return ( + + + + - - ) -} diff --git a/app/store/components/product-management/hooks/useProductFilters.ts b/app/store/components/product-management/hooks/useProductFilters.ts index 60955beb..25066ac6 100644 --- a/app/store/components/product-management/hooks/useProductFilters.ts +++ b/app/store/components/product-management/hooks/useProductFilters.ts @@ -8,8 +8,8 @@ import type { export function useProductFilters(products: IProduct[]) { const [activeTab, setActiveTab] = useState('all') const [searchQuery, setSearchQuery] = useState('') - const [sortField, setSortField] = useState(null) - const [sortDirection, setSortDirection] = useState(null) + const [sortField, setSortField] = useState(null) + const [sortDirection, setSortDirection] = useState(null) // Filtrar productos según la pestaña activa y búsqueda const filteredProducts = products diff --git a/app/store/components/product-management/main-components/AttributesForm.tsx b/app/store/components/product-management/main-components/AttributesForm.tsx index 46301ec9..4dd5ad26 100644 --- a/app/store/components/product-management/main-components/AttributesForm.tsx +++ b/app/store/components/product-management/main-components/AttributesForm.tsx @@ -1,14 +1,21 @@ -import { useState } from 'react' -import { Plus, X } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Badge } from '@/components/ui/badge' -import { Separator } from '@/components/ui/separator' +import { useState, useCallback } from 'react' +import { + Card, + Text, + BlockStack, + TextField, + Button, + ResourceList, + ResourceItem, + LegacyStack, + Tag, + EmptyState, + Banner, +} from '@shopify/polaris' interface Attribute { - name: string - values: string[] + name?: string + values?: string[] } interface AttributesFormProps { @@ -16,205 +23,168 @@ interface AttributesFormProps { onChange: (value: Attribute[]) => void } -export function AttributesForm({ value, onChange }: AttributesFormProps) { +export function AttributesForm({ value: attributes, onChange }: AttributesFormProps) { const [newAttributeName, setNewAttributeName] = useState('') const [newAttributeValue, setNewAttributeValue] = useState('') - const [selectedAttributeIndex, setSelectedAttributeIndex] = useState(null) + const [selectedAttributeIndex, setSelectedAttributeIndex] = useState( + attributes.length > 0 ? '0' : undefined + ) - const addAttribute = () => { + const handleAddAttribute = useCallback(() => { if (!newAttributeName.trim()) return - - onChange([...value, { name: newAttributeName, values: [] }]) + const newAttributes = [...attributes, { name: newAttributeName.trim(), values: [] }] + onChange(newAttributes) setNewAttributeName('') - setSelectedAttributeIndex(value.length) - } + setSelectedAttributeIndex(String(newAttributes.length - 1)) + }, [newAttributeName, attributes, onChange]) - const removeAttribute = (index: number) => { - const newAttributes = [...value] - newAttributes.splice(index, 1) - onChange(newAttributes) + const handleRemoveAttribute = useCallback( + (indexToRemove: number) => { + const newAttributes = attributes.filter((_, i) => i !== indexToRemove) + onChange(newAttributes) - if (selectedAttributeIndex === index) { - setSelectedAttributeIndex(null) - } else if (selectedAttributeIndex !== null && selectedAttributeIndex > index) { - setSelectedAttributeIndex(selectedAttributeIndex - 1) - } - } + const currentSelected = selectedAttributeIndex ? parseInt(selectedAttributeIndex, 10) : -1 - const addAttributeValue = () => { - if (selectedAttributeIndex === null || !newAttributeValue.trim()) return + if (currentSelected === indexToRemove) { + setSelectedAttributeIndex(newAttributes.length > 0 ? '0' : undefined) + } else if (currentSelected > indexToRemove) { + setSelectedAttributeIndex(String(currentSelected - 1)) + } + }, + [attributes, onChange, selectedAttributeIndex] + ) + + const handleAddAttributeValue = useCallback(() => { + const index = selectedAttributeIndex ? parseInt(selectedAttributeIndex, 10) : -1 + if (index === -1 || !newAttributeValue.trim()) return - const newAttributes = [...value] - if (!newAttributes[selectedAttributeIndex].values.includes(newAttributeValue)) { - newAttributes[selectedAttributeIndex].values.push(newAttributeValue) + const newAttributes = [...attributes] + const currentAttribute = newAttributes[index] + if (!currentAttribute.values?.includes(newAttributeValue.trim())) { + currentAttribute.values = [...(currentAttribute.values || []), newAttributeValue.trim()] onChange(newAttributes) } setNewAttributeValue('') - } + }, [newAttributeValue, selectedAttributeIndex, attributes, onChange]) + + const handleRemoveAttributeValue = useCallback( + (valueIndex: number) => { + const index = selectedAttributeIndex ? parseInt(selectedAttributeIndex, 10) : -1 + if (index === -1) return + const newAttributes = [...attributes] + newAttributes[index].values?.splice(valueIndex, 1) + onChange(newAttributes) + }, + [selectedAttributeIndex, attributes, onChange] + ) - const removeAttributeValue = (attrIndex: number, valueIndex: number) => { - const newAttributes = [...value] - newAttributes[attrIndex].values.splice(valueIndex, 1) - onChange(newAttributes) - } + const selectedIndex = + selectedAttributeIndex !== undefined ? parseInt(selectedAttributeIndex, 10) : -1 + const selectedAttribute = selectedIndex !== -1 ? attributes[selectedIndex] : null - const handleKeyDown = (e: React.KeyboardEvent, action: () => void) => { - if (e.key === 'Enter') { - e.preventDefault() - action() + const resourceListItems = attributes.map((attr, index) => { + return { + id: String(index), + name: attr.name || '', + actions: [{ content: 'Eliminar', onAction: () => handleRemoveAttribute(index) }], } - } + }) return ( -
-
- -

- Agregue atributos como talla, color, material, etc. para crear variantes del producto. -

-
- -
-
-
-
- - setNewAttributeName(e.target.value)} - placeholder="ej. Talla, Color, Material" - onKeyDown={e => handleKeyDown(e, addAttribute)} - /> -
- -
- -
- {value.length > 0 ? ( -
- {value.map((attr, index) => ( -
setSelectedAttributeIndex(index)} + } + /> + + {attributes.length > 0 ? ( + + { + setSelectedAttributeIndex(selected[0]) + }} + renderItem={item => { + const { id, name, actions } = item + return ( + setSelectedAttributeIndex(id)} + shortcutActions={actions} + persistActions > -
{attr.name}
-
- - {attr.values.length} valores - + + {name} + + + ) + }} + /> + + {selectedAttribute ? ( + + + + Valores de {selectedAttribute.name} + + { - e.stopPropagation() - removeAttribute(index) - }} + onClick={handleAddAttributeValue} + disabled={!newAttributeValue.trim()} > - - Eliminar + Añadir -
-
- ))} -
+ } + /> + + {(selectedAttribute.values || []).map((val, index) => ( + handleRemoveAttributeValue(index)}> + {val} + + ))} + + + ) : ( -
- No se han agregado atributos aún -
+ +

Seleccione un atributo de la lista para agregarle valores.

+
)} -
-
- -
- {selectedAttributeIndex !== null && value[selectedAttributeIndex] ? ( -
-
-

- Valores de {value[selectedAttributeIndex].name} -

-
- - - -
-
- - setNewAttributeValue(e.target.value)} - placeholder={`Agregar valor de ${value[selectedAttributeIndex].name.toLowerCase()}`} - onKeyDown={e => handleKeyDown(e, addAttributeValue)} - /> -
- -
- -
- {value[selectedAttributeIndex].values.length > 0 ? ( - value[selectedAttributeIndex].values.map((val, valueIndex) => ( - - {val} - - - )) - ) : ( -
- No se han agregado valores aún -
- )} -
-
- ) : ( -
-
-

Seleccione un atributo

-

- Seleccione un atributo de la izquierda o agregue uno nuevo para gestionar sus - valores -

-
-
- )} -
-
-
+ + ) : ( + +

Agregue un atributo para empezar a crear variantes de producto.

+
+ )} + + ) } diff --git a/app/store/components/product-management/main-components/ImageUpload.tsx b/app/store/components/product-management/main-components/ImageUpload.tsx index 34200c42..7285dc16 100644 --- a/app/store/components/product-management/main-components/ImageUpload.tsx +++ b/app/store/components/product-management/main-components/ImageUpload.tsx @@ -1,16 +1,19 @@ import { useState } from 'react' -import Image from 'next/image' -import { X, Upload, Loader2 } from 'lucide-react' -import { Button } from '@/components/ui/button' -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 { + Card, + BlockStack, + Text, + Button, + DropZone, + Thumbnail, + LegacyStack, + TextField, +} from '@shopify/polaris' import ImageSelectorModal from '@/app/store/components/images-selector/components/image-selector-modal' interface ImageFile { url: string - alt: string + alt?: string } interface ImageUploadProps { @@ -19,40 +22,10 @@ interface ImageUploadProps { storeId: string } -export function ImageUpload({ value, onChange }: ImageUploadProps) { - const [draggedIndex, setDraggedIndex] = useState(null) - const [enlargedImage, setEnlargedImage] = useState(null) - const [isDragging, setIsDragging] = useState(false) +export function ImageUpload({ value, onChange, storeId }: ImageUploadProps) { const [isModalOpen, setIsModalOpen] = useState(false) - const handleDragEnter = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragging(true) - } - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - if (!isDragging) setIsDragging(true) - } - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragging(false) - } - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragging(false) - - const files = e.dataTransfer.files - setIsModalOpen(true) - } - - const handleButtonClick = () => { + const handleDropZoneDrop = () => { setIsModalOpen(true) } @@ -68,159 +41,65 @@ export function ImageUpload({ value, onChange }: ImageUploadProps) { onChange(newImages) } - const handleImageDragStart = (index: number) => { - setDraggedIndex(index) - } - - const handleImageDragOver = (e: React.DragEvent, index: number) => { - e.preventDefault() - if (draggedIndex === null || draggedIndex === index) return - - const newImages = [...value] - const draggedImage = newImages[draggedIndex] - newImages.splice(draggedIndex, 1) - newImages.splice(index, 0, draggedImage) - - onChange(newImages) - setDraggedIndex(index) - } - - const handleImageDragEnd = () => { - setDraggedIndex(null) - } - - const openEnlargedView = (image: ImageFile) => { - setEnlargedImage(image) + const handleSelectImages = (selectedImages: any) => { + const imagesToAdd: ImageFile[] = ( + Array.isArray(selectedImages) ? selectedImages : [selectedImages] + ).map(img => ({ + url: img.url, + alt: img.filename || '', + })) + onChange([...value, ...imagesToAdd]) + setIsModalOpen(false) } return ( -
-
-
- -

Arrastre y suelte imágenes aquí

-

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

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

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

-
+ <> + + + Imágenes + + + + + + + + {value.length > 0 && ( + {value.map((image, index) => ( -
handleImageDragStart(index)} - onDragOver={e => handleImageDragOver(e, index)} - onDragEnd={handleImageDragEnd} - > -
- {image.alt openEnlargedView(image)} + + + - {(image as any).isTemp && ( -
- -
- )} - -
-
- - updateAltText(index, e.target.value)} - placeholder="Describa esta imagen" - className="h-8 text-xs" - /> -
-
- {index === 0 ? 'Imagen Principal' : `Imagen ${index + 1}`} -
-
+
+ ))} -
-
- )} - - !open && setEnlargedImage(null)}> - - {enlargedImage && ( -
- {enlargedImage.alt -
- )} -
-
+ + )} + { - 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) - }} + onSelect={handleSelectImages} initialSelectedImage={value.length > 0 ? value[0].url : null} /> -
+ ) } diff --git a/app/store/components/product-management/main-components/InventoryManager.tsx b/app/store/components/product-management/main-components/InventoryManager.tsx index ded5e8b1..44957552 100644 --- a/app/store/components/product-management/main-components/InventoryManager.tsx +++ b/app/store/components/product-management/main-components/InventoryManager.tsx @@ -1,7 +1,6 @@ import { useProducts } from '@/app/store/hooks/useProducts' 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 { ProductPageSkeleton } from '@/app/store/components/product-management/main-components/ProductPageSkeleton' interface InventoryManagerProps { diff --git a/app/store/components/product-management/main-components/ProductForm.tsx b/app/store/components/product-management/main-components/ProductForm.tsx index 1fc4eff8..530a55b5 100644 --- a/app/store/components/product-management/main-components/ProductForm.tsx +++ b/app/store/components/product-management/main-components/ProductForm.tsx @@ -3,12 +3,21 @@ import { useState, useEffect, useCallback } from 'react' import { useRouter } from 'next/navigation' import { zodResolver } from '@hookform/resolvers/zod' -import { useForm } from 'react-hook-form' -import { Loader } from '@/components/ui/loader' -import { Button } from '@/components/ui/button' -import { Form } from '@/components/ui/form' -import { Card, CardContent } from '@/components/ui/card' -import { toast } from 'sonner' +import { useForm, Controller } from 'react-hook-form' +import { + Page, + Layout, + Card, + Button, + Form, + FormLayout, + ContextualSaveBar, + BlockStack, + Spinner, + Select, + Text, +} from '@shopify/polaris' +import { useToast } from '@/app/store/context/ToastContext' import { productFormSchema, type ProductFormValues, @@ -21,12 +30,10 @@ import { handleProductUpdate, handleProductCreate, } from '@/app/store/components/product-management/utils/productUtils' -import { useUnsavedChangesWarning } from '@/hooks/ui/use-unsaved-changes-warning' -import { UnsavedChangesAlert } from '@/components/ui/unsaved-changes-alert' import { BasicInfoSection } from '@/app/store/components/product-management/product-sections/basic-info-section' import { PricingInventorySection } from '@/app/store/components/product-management/product-sections/pricing-inventory-section' -import { ImagesSection } from '@/app/store/components/product-management/product-sections/images-section' -import { AttributesSection } from '@/app/store/components/product-management/product-sections/attributes-section' +import { ImageUpload } from '@/app/store/components/product-management/main-components/ImageUpload' +import { AttributesForm } from '@/app/store/components/product-management/main-components/AttributesForm' import { PublicationSection } from '@/app/store/components/product-management/product-sections/publication-section' interface ProductFormProps { @@ -34,59 +41,30 @@ interface ProductFormProps { productId?: string } -// Función helper para validar el status const normalizeStatus = (status: any): 'draft' | 'pending' | 'active' | 'inactive' => { const validStatuses = ['draft', 'pending', 'active', 'inactive'] as const - - // Si el status es undefined, null o string vacío, retornar 'draft' - if (!status || status === '') { - return 'draft' - } - - // Si el status es válido, retornarlo; sino, retornar 'draft' + if (!status || status === '') return 'draft' return validStatuses.includes(status) ? status : 'draft' } -// Componente de Loading reutilizable function ProductLoadingState() { return ( -
- -
- ) -} - -// Componente del botón de volver -function BackButton({ onClick }: { onClick: () => void }) { - return ( - + + ) } export function ProductForm({ storeId, productId }: ProductFormProps) { const router = useRouter() + const { showToast } = useToast() const [isSubmitting, setIsSubmitting] = useState(false) const [isLoadingProduct, setIsLoadingProduct] = useState(!!productId) @@ -96,22 +74,14 @@ export function ProductForm({ storeId, productId }: ProductFormProps) { const form = useForm({ resolver: zodResolver(productFormSchema), - defaultValues: defaultValues, + defaultValues, mode: 'onChange', }) const { - hasUnsavedChanges, - resetUnsavedChanges, - confirmNavigation, - pendingNavigation, - discardChanges, - } = useUnsavedChangesWarning({ - form, - isSubmitting, - }) + formState: { isDirty }, + } = form - // Cargar producto para edición useEffect(() => { if (!productId) { setIsLoadingProduct(false) @@ -123,203 +93,180 @@ export function ProductForm({ storeId, productId }: ProductFormProps) { const product = await fetchProduct(productId) if (product) { const formValues = mapProductToFormValues(product) - - // Normalizar valores antes de establecerlos en el formulario formValues.status = normalizeStatus(formValues.status) formValues.category = formValues.category || '' - - // Establecer los valores en el formulario form.reset(formValues) } } catch (error) { console.error('Error loading product:', error) - toast.error('Error', { - description: 'No se pudo cargar el producto. Por favor, inténtelo de nuevo.', - }) + showToast('No se pudo cargar el producto. Por favor, inténtelo de nuevo.', true) } finally { setIsLoadingProduct(false) } } loadProduct() - }, [productId, fetchProduct, form]) + }, [productId, fetchProduct, form, showToast]) - // Función optimizada para manejar el guardado - const handleSave = useCallback( - async (isNavigating = false) => { - try { - const isValid = await form.trigger() - if (!isValid) { - throw new Error('Validation failed') - } - - const data = form.getValues() - const basicProductData = prepareProductData(data, storeId) - - const result = productId - ? await handleProductUpdate(basicProductData, productId, storeId, updateProduct) - : await handleProductCreate(basicProductData, createProduct) - - if (result) { - resetUnsavedChanges() - // Mantener isSubmitting true durante la redirección - if (isNavigating && pendingNavigation) { - pendingNavigation() - } else { - router.push(`/store/${storeId}/products`) - } - // No resetear isSubmitting aquí, se hará cuando se complete la navegación - } else { - throw new Error( - productId ? 'Error al actualizar el producto' : 'Error al crear el producto' - ) - } - } catch (error) { - console.error('Error al guardar producto:', error) - - if (!(error instanceof Error && error.message === 'Validation failed')) { - toast.error('Error', { - description: - 'Ha ocurrido un error al guardar el producto. Por favor, inténtelo de nuevo.', - }) - } - throw error - } - }, - [ - form, - storeId, - productId, - updateProduct, - createProduct, - resetUnsavedChanges, - pendingNavigation, - router, - ] - ) - - // Función para manejar el submit del formulario - const onSubmit = async (data: ProductFormValues) => { - if (isSubmitting) return + const handleSave = useCallback(async () => { setIsSubmitting(true) - try { - await handleSave() - } catch (error) { - setIsSubmitting(false) - } - } - - // Función para manejar el guardado desde UnsavedChangesAlert - const handleUnsavedSave = useCallback(async () => { - setIsSubmitting(true) - try { - await handleSave(true) - // Mantener isSubmitting true hasta la redirección + const isValid = await form.trigger() + if (!isValid) { + showToast('Por favor, complete todos los campos requeridos.', true) + throw new Error('Validation failed') + } + const data = form.getValues() + const basicProductData = prepareProductData(data, storeId) + const result = productId + ? await handleProductUpdate(basicProductData, productId, storeId, updateProduct) + : await handleProductCreate(basicProductData, createProduct) + + if (result) { + form.reset(form.getValues()) // Resets dirty state + showToast(`Producto ${productId ? 'actualizado' : 'creado'} con éxito.`) + router.push(`/store/${storeId}/products`) + } else { + throw new Error(productId ? 'Error al actualizar producto' : 'Error al crear producto') + } } catch (error) { + if (!(error instanceof Error && error.message === 'Validation failed')) { + showToast('Ha ocurrido un error al guardar el producto.', true) + } + } finally { setIsSubmitting(false) - throw error } - }, [handleSave]) + }, [form, storeId, productId, updateProduct, createProduct, router, showToast]) - // Función para manejar la navegación con confirmación - const handleNavigation = useCallback( - (destination: () => void) => { - confirmNavigation(destination) - }, - [confirmNavigation] - ) - - // Si está cargando, mostrar el loader if (isLoadingProduct) { return } - return ( - <> - {hasUnsavedChanges && ( - - )} + const categoryOptions = [ + { label: 'Ropa', value: 'Ropa' }, + { label: 'Electrónica', value: 'Electronicos' }, + { label: 'Hogar y Cocina', value: 'Hogar' }, + { label: 'Belleza y Cuidado Personal', value: 'Belleza' }, + { label: 'Deportes y Aire Libre', value: 'Deporte' }, + ] + + const statusOptions = [ + { label: 'Borrador', value: 'draft' }, + { label: 'Pendiente', value: 'pending' }, + { label: 'Activo', value: 'active' }, + { label: 'Inactivo', value: 'inactive' }, + ] -
-
- handleNavigation(() => router.back())} /> -
- -
- - {/* Main content column */} -
- - - - - - - - - - + return ( + router.push(`/store/${storeId}/products`), + }} + title={productId ? 'Editar producto' : 'Añadir producto'} + primaryAction={{ + content: 'Guardar', + onAction: handleSave, + loading: isSubmitting, + disabled: !isDirty, + }} + > + + {isDirty && ( + form.reset(), + content: 'Descartar', + }} + /> + )} + + + + + + + + Imágenes + + ( + + )} + /> + - - - - - + + + ( + + )} + /> - - - - - + + + + + + + + Estado + + ( + + )} + /> + -
-
- -
- + + + + + + ) } diff --git a/app/store/components/product-management/product-sections/ai-generate-button.tsx b/app/store/components/product-management/product-sections/ai-generate-button.tsx index 6e0ea44a..0d03c705 100644 --- a/app/store/components/product-management/product-sections/ai-generate-button.tsx +++ b/app/store/components/product-management/product-sections/ai-generate-button.tsx @@ -1,18 +1,13 @@ 'use client' -import { useState } from 'react' -import { motion } from 'framer-motion' -import { Sparkles } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { cn } from '@/lib/utils' -import { TextShimmer } from '@/app/store/components/product-management/product-sections/text-shimmer' +import { Button } from '@shopify/polaris' +import { MagicIcon } from '@shopify/polaris-icons' interface AIGenerateButtonProps { - onClick: () => Promise + onClick: () => void isLoading: boolean isDisabled?: boolean label?: string - loadingLabel?: string } export function AIGenerateButton({ @@ -20,84 +15,10 @@ export function AIGenerateButton({ isLoading, isDisabled = false, label = 'Generar con IA', - loadingLabel = 'Generando', }: AIGenerateButtonProps) { - const [isHovered, setIsHovered] = useState(false) - return ( -
- {/* Glow effect behind button */} - {!isLoading && !isDisabled && ( - - )} - - -
+ ) } diff --git a/app/store/components/product-management/product-sections/basic-info-section.tsx b/app/store/components/product-management/product-sections/basic-info-section.tsx index ad9a6b18..a476a9f4 100644 --- a/app/store/components/product-management/product-sections/basic-info-section.tsx +++ b/app/store/components/product-management/product-sections/basic-info-section.tsx @@ -1,35 +1,20 @@ import type { UseFormReturn } from 'react-hook-form' -import { format } from 'date-fns' -import { es } from 'date-fns/locale' -import { CalendarIcon, Check, HelpCircle, RefreshCw, X } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form' -import { Input } from '@/components/ui/input' -import { Textarea } from '@/components/ui/textarea' import { + FormLayout, + TextField, Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import { Calendar } from '@/components/ui/calendar' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { cn } from '@/lib/utils' + BlockStack, + ButtonGroup, + Button, + Banner, + Text, +} from '@shopify/polaris' import { useState } from 'react' -import { toast } from 'sonner' import type { ProductFormValues } from '@/lib/zod-schemas/product-schema' import { useProductDescription } from '@/app/store/components/product-management/hooks/useProductDescription' -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { AIGenerateButton } from '@/app/store/components/product-management/product-sections/ai-generate-button' -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +import { Controller } from 'react-hook-form' +import { useToast } from '@/app/store/context/ToastContext' interface BasicInfoSectionProps { form: UseFormReturn @@ -38,15 +23,14 @@ interface BasicInfoSectionProps { export function BasicInfoSection({ form }: BasicInfoSectionProps) { const { generateDescription, loading: isGeneratingDescription } = useProductDescription() const [previewDescription, setPreviewDescription] = useState(null) + const { showToast } = useToast() const handleGenerateDescription = async () => { const productName = form.getValues('name') const category = form.getValues('category') if (!productName) { - toast.error('Error', { - description: 'Por favor, ingrese un nombre de producto primero.', - }) + showToast('Por favor, ingrese un nombre de producto primero.', true) return } @@ -55,298 +39,184 @@ export function BasicInfoSection({ form }: BasicInfoSectionProps) { productName, category: category || undefined, }) - setPreviewDescription(description) } catch (error) { console.error('Error al generar descripción:', error) - toast.error('Error', { - description: 'No se pudo generar la descripción. Inténtelo de nuevo más tarde.', - }) + showToast('No se pudo generar la descripción. Inténtelo de nuevo más tarde.', true) } } const acceptDescription = () => { if (previewDescription) { form.setValue('description', previewDescription, { shouldDirty: true, shouldTouch: true }) - toast.success('Descripción aplicada', { - description: 'La descripción generada ha sido aplicada al producto.', - }) + showToast('La descripción generada ha sido aplicada al producto.') setPreviewDescription(null) } } const rejectDescription = () => { setPreviewDescription(null) - toast.info('Descripción descartada', { - description: 'La descripción generada ha sido descartada.', - }) + showToast('La descripción generada ha sido descartada.') } - return ( -
- ( - -
- Nombre del Producto - - - - - - -

- Un nombre descriptivo y específico mejorará la calidad de las descripciones - generadas con IA. -

-
-
-
-
- - - - - El nombre de su producto como aparecerá a los clientes. - - -
- )} - /> - - ( - -
-
- Descripción - - - - - - -

Para obtener mejores resultados:

-
    -
  • Use un nombre de producto descriptivo
  • -
  • Seleccione una categoría adecuada
  • -
  • Incluya características clave y beneficios
  • -
-
-
-
-
- -
+ const categoryOptions = [ + { label: 'Ropa', value: 'Ropa' }, + { label: 'Electrónica', value: 'Electronicos' }, + { label: 'Hogar y Cocina', value: 'Hogar' }, + { label: 'Belleza y Cuidado Personal', value: 'Belleza' }, + { label: 'Deportes y Aire Libre', value: 'Deporte' }, + ] + + const statusOptions = [ + { label: 'Borrador', value: 'draft' }, + { label: 'Pendiente', value: 'pending' }, + { label: 'Activo', value: 'active' }, + { label: 'Inactivo', value: 'inactive' }, + ] - {previewDescription ? ( - - - - Vista previa de descripción generada - - - - {previewDescription} - - - - - + - - - ) : null} - - -