feat: Purchase Order Add
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import type { BoxProps } from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
|
||||
// Component Imports
|
||||
import Link from '@components/Link'
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
|
||||
// Styled Component Imports
|
||||
import AppReactDropzone from '@/libs/styles/AppReactDropzone'
|
||||
|
||||
type FileProp = {
|
||||
name: string
|
||||
type: string
|
||||
size: number
|
||||
}
|
||||
|
||||
interface ImageUploadProps {
|
||||
// Required props
|
||||
onUpload: (file: File) => Promise<string> | string // Returns image URL
|
||||
|
||||
// Optional customization props
|
||||
title?: string | null // Made nullable
|
||||
currentImageUrl?: string
|
||||
onImageRemove?: () => void
|
||||
onImageChange?: (url: string) => void
|
||||
|
||||
// Upload state
|
||||
isUploading?: boolean
|
||||
|
||||
// UI customization
|
||||
maxFileSize?: number // in bytes
|
||||
acceptedFileTypes?: string[]
|
||||
showUrlOption?: boolean
|
||||
uploadButtonText?: string
|
||||
browseButtonText?: string
|
||||
dragDropText?: string
|
||||
replaceText?: string
|
||||
|
||||
// Style customization
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
// Styled Dropzone Component
|
||||
const Dropzone = styled(AppReactDropzone)<BoxProps>(({ theme }) => ({
|
||||
'& .dropzone': {
|
||||
minHeight: 'unset',
|
||||
padding: theme.spacing(12),
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
paddingInline: theme.spacing(5)
|
||||
},
|
||||
'&+.MuiList-root .MuiListItem-root .file-name': {
|
||||
fontWeight: theme.typography.body1.fontWeight
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const ImageUpload: React.FC<ImageUploadProps> = ({
|
||||
onUpload,
|
||||
title = null, // Default to null
|
||||
currentImageUrl = '',
|
||||
onImageRemove,
|
||||
onImageChange,
|
||||
isUploading = false,
|
||||
maxFileSize = 5 * 1024 * 1024, // 5MB default
|
||||
acceptedFileTypes = ['image/*'],
|
||||
showUrlOption = true,
|
||||
uploadButtonText = 'Upload',
|
||||
browseButtonText = 'Browse Image',
|
||||
dragDropText = 'Drag and Drop Your Image Here.',
|
||||
replaceText = 'Drop New Image to Replace',
|
||||
className = '',
|
||||
disabled = false
|
||||
}) => {
|
||||
// States
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const [error, setError] = useState<string>('')
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!files.length) return
|
||||
|
||||
try {
|
||||
setError('')
|
||||
const imageUrl = await onUpload(files[0])
|
||||
|
||||
if (typeof imageUrl === 'string') {
|
||||
onImageChange?.(imageUrl)
|
||||
setFiles([]) // Clear files after successful upload
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Upload failed')
|
||||
}
|
||||
}
|
||||
|
||||
// Hooks
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
onDrop: (acceptedFiles: File[]) => {
|
||||
setError('')
|
||||
|
||||
if (acceptedFiles.length === 0) return
|
||||
|
||||
const file = acceptedFiles[0]
|
||||
|
||||
// Validate file size
|
||||
if (file.size > maxFileSize) {
|
||||
setError(`File size should be less than ${formatFileSize(maxFileSize)}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Replace files instead of adding to them
|
||||
setFiles([file])
|
||||
},
|
||||
accept: acceptedFileTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}),
|
||||
disabled: disabled || isUploading
|
||||
})
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
const renderFilePreview = (file: FileProp) => {
|
||||
if (file.type.startsWith('image')) {
|
||||
return <img width={38} height={38} alt={file.name} src={URL.createObjectURL(file as any)} />
|
||||
} else {
|
||||
return <i className='tabler-file-description' />
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveFile = (file: FileProp) => {
|
||||
const filtered = files.filter((i: FileProp) => i.name !== file.name)
|
||||
setFiles(filtered)
|
||||
setError('')
|
||||
}
|
||||
|
||||
const handleRemoveCurrentImage = () => {
|
||||
onImageRemove?.()
|
||||
}
|
||||
|
||||
const handleRemoveAllFiles = () => {
|
||||
setFiles([])
|
||||
setError('')
|
||||
}
|
||||
|
||||
const fileList = files.map((file: FileProp) => (
|
||||
<ListItem key={file.name} className='pis-4 plb-3'>
|
||||
<div className='file-details'>
|
||||
<div className='file-preview'>{renderFilePreview(file)}</div>
|
||||
<div>
|
||||
<Typography className='file-name font-medium' color='text.primary'>
|
||||
{file.name}
|
||||
</Typography>
|
||||
<Typography className='file-size' variant='body2'>
|
||||
{formatFileSize(file.size)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<IconButton onClick={() => handleRemoveFile(file)} disabled={isUploading}>
|
||||
<i className='tabler-x text-xl' />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
))
|
||||
|
||||
return (
|
||||
<Dropzone className={className}>
|
||||
{/* Conditional title and URL option header */}
|
||||
{title && (
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<Typography variant='h6' component='h2'>
|
||||
{title}
|
||||
</Typography>
|
||||
{showUrlOption && (
|
||||
<Typography component={Link} color='primary.main' className='font-medium'>
|
||||
Add media from URL
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div {...getRootProps({ className: 'dropzone' })}>
|
||||
<input {...getInputProps()} />
|
||||
<div className='flex items-center flex-col gap-2 text-center'>
|
||||
<CustomAvatar variant='rounded' skin='light' color='secondary'>
|
||||
<i className='tabler-upload' />
|
||||
</CustomAvatar>
|
||||
<Typography variant='h4'>{currentImageUrl && !files.length ? replaceText : dragDropText}</Typography>
|
||||
<Typography color='text.disabled'>or</Typography>
|
||||
<Button variant='tonal' size='small' disabled={disabled || isUploading}>
|
||||
{browseButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<Typography color='error' variant='body2' className='mt-2 text-center'>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Show current image if it exists */}
|
||||
{currentImageUrl && !files.length && (
|
||||
<div className='current-image mb-4'>
|
||||
<Typography variant='subtitle2' className='mb-2'>
|
||||
Current Image:
|
||||
</Typography>
|
||||
<div className='flex items-center justify-between p-3 border border-gray-200 rounded'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<img width={60} height={60} alt='Current image' src={currentImageUrl} className='rounded object-cover' />
|
||||
<div>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
Current image
|
||||
</Typography>
|
||||
<Typography variant='body2' color='text.secondary'>
|
||||
Uploaded image
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
{onImageRemove && (
|
||||
<IconButton onClick={handleRemoveCurrentImage} color='error' disabled={isUploading}>
|
||||
<i className='tabler-x text-xl' />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File list and upload buttons */}
|
||||
{files.length > 0 && (
|
||||
<>
|
||||
<List>{fileList}</List>
|
||||
<div className='buttons'>
|
||||
<Button color='error' variant='tonal' onClick={handleRemoveAllFiles} disabled={isUploading}>
|
||||
Remove All
|
||||
</Button>
|
||||
<Button variant='contained' onClick={handleUpload} disabled={isUploading}>
|
||||
{isUploading ? 'Uploading...' : uploadButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Dropzone>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageUpload
|
||||
|
||||
// ===== USAGE EXAMPLES =====
|
||||
|
||||
// 1. Without title
|
||||
// <ImageUpload
|
||||
// onUpload={handleUpload}
|
||||
// currentImageUrl={imageUrl}
|
||||
// onImageChange={setImageUrl}
|
||||
// onImageRemove={() => setImageUrl('')}
|
||||
// />
|
||||
|
||||
// 2. With title
|
||||
// <ImageUpload
|
||||
// title="Product Image"
|
||||
// onUpload={handleUpload}
|
||||
// currentImageUrl={imageUrl}
|
||||
// onImageChange={setImageUrl}
|
||||
// onImageRemove={() => setImageUrl('')}
|
||||
// />
|
||||
|
||||
// 3. Explicitly set title to null
|
||||
// <ImageUpload
|
||||
// title={null}
|
||||
// onUpload={handleUpload}
|
||||
// currentImageUrl={imageUrl}
|
||||
// onImageChange={setImageUrl}
|
||||
// onImageRemove={() => setImageUrl('')}
|
||||
// />
|
||||
Reference in New Issue
Block a user