Accordion.jsx
1 month ago
Card.jsx
1 month ago
CardSkeleton.jsx
1 month ago
FileUploaderField.jsx
1 month ago
SearchComboBox.jsx
1 month ago
FileUploaderField.jsx
161 lines
| 1 | /** |
| 2 | * External Dependencies |
| 3 | */ |
| 4 | import { Upload } from 'lucide-react'; |
| 5 | |
| 6 | /** |
| 7 | * Internal Dependencies |
| 8 | */ |
| 9 | import { useFileUploader } from '../hooks/useFileUploader'; |
| 10 | |
| 11 | export function FileUploaderField( { |
| 12 | maxFiles = 5, |
| 13 | maxSizeMB = 5, |
| 14 | allowedExt = [ 'jpg', 'jpeg', 'png', 'gif', 'pdf', 'txt', 'docx', 'log' ], |
| 15 | label = 'Drag & drop files or <strong>click to browse</strong>', |
| 16 | uploaderRef = null, // pass a ref if parent needs { selectedFiles, reset } |
| 17 | } ) { |
| 18 | const { |
| 19 | selectedFiles, |
| 20 | errors, |
| 21 | atLimit, |
| 22 | processFiles, |
| 23 | deleteFile, |
| 24 | reset, |
| 25 | meta, |
| 26 | } = useFileUploader( { maxFiles, maxSizeMB, allowedExt } ); |
| 27 | |
| 28 | // Only expose selectedFiles + reset to the parent form via ref |
| 29 | // Do NOT destructure and store the whole hook return — it goes stale |
| 30 | if ( uploaderRef ) { |
| 31 | uploaderRef.current = { selectedFiles, reset }; |
| 32 | } |
| 33 | |
| 34 | function handleDragOver( e ) { |
| 35 | e.preventDefault(); |
| 36 | if ( ! atLimit ) { |
| 37 | e.currentTarget.classList.add( 'dragover' ); |
| 38 | } |
| 39 | } |
| 40 | function handleDragLeave( e ) { |
| 41 | e.currentTarget.classList.remove( 'dragover' ); |
| 42 | } |
| 43 | function handleDrop( e ) { |
| 44 | e.preventDefault(); |
| 45 | e.currentTarget.classList.remove( 'dragover' ); |
| 46 | processFiles( e.dataTransfer.files ); |
| 47 | } |
| 48 | function handleChange( e ) { |
| 49 | processFiles( e.target.files ); |
| 50 | e.target.value = ''; // allow re-selecting the same file |
| 51 | } |
| 52 | |
| 53 | const count = selectedFiles.length; |
| 54 | const acceptAttr = meta.allowedExt.map( ( e ) => '.' + e ).join( ',' ); |
| 55 | |
| 56 | return ( |
| 57 | <div |
| 58 | id="attachments-container" |
| 59 | className="advads-field-input advads-file-uploader" |
| 60 | > |
| 61 | { /* Drop zone */ } |
| 62 | <div |
| 63 | id="attachments-drop-zone" |
| 64 | className={ `dropzone${ atLimit ? ' limit-reached' : '' }` } |
| 65 | onDragOver={ handleDragOver } |
| 66 | onDragLeave={ handleDragLeave } |
| 67 | onDrop={ handleDrop } |
| 68 | > |
| 69 | <Upload className="size-8 mb-4" /> |
| 70 | |
| 71 | <p |
| 72 | className="text-sm mt-0 mb-2" |
| 73 | dangerouslySetInnerHTML={ { __html: label } } |
| 74 | /> |
| 75 | |
| 76 | <input |
| 77 | id="attachments" |
| 78 | type="file" |
| 79 | multiple |
| 80 | accept={ acceptAttr } |
| 81 | onChange={ handleChange } |
| 82 | /> |
| 83 | |
| 84 | { /* Hints */ } |
| 85 | <div className="upload-hints"> |
| 86 | <div> |
| 87 | <strong>Allowed:</strong>{ ' ' } |
| 88 | { meta.allowedExt.map( ( e ) => '.' + e ).join( ', ' ) } |
| 89 | </div> |
| 90 | <span> |
| 91 | <strong>Max size:</strong> { meta.maxSizeMB } MB per |
| 92 | file |
| 93 | </span> |
| 94 | <span> |
| 95 | <strong>Max files:</strong> { meta.maxFiles } |
| 96 | </span> |
| 97 | </div> |
| 98 | </div> |
| 99 | |
| 100 | { /* Count badge */ } |
| 101 | { count > 0 && ( |
| 102 | <div |
| 103 | className={ `file-count-badge${ |
| 104 | atLimit ? ' at-limit' : '' |
| 105 | }` } |
| 106 | > |
| 107 | { count } / { meta.maxFiles } file{ count !== 1 ? 's' : '' }{ ' ' } |
| 108 | selected |
| 109 | </div> |
| 110 | ) } |
| 111 | |
| 112 | { /* Errors */ } |
| 113 | { errors.length > 0 && ( |
| 114 | <div className="error-box"> |
| 115 | <p className="error-box-title">Some files were rejected:</p> |
| 116 | <ul className="error-list"> |
| 117 | { errors.map( ( r, i ) => ( |
| 118 | <li key={ i }> |
| 119 | <strong>{ r.name }</strong>:{ ' ' } |
| 120 | { r.reasons.join( '; ' ) } |
| 121 | </li> |
| 122 | ) ) } |
| 123 | </ul> |
| 124 | </div> |
| 125 | ) } |
| 126 | |
| 127 | { /* Selected files */ } |
| 128 | <div className="selected-files-container"> |
| 129 | { selectedFiles.map( ( f ) => ( |
| 130 | <div key={ f.id } className="file-item"> |
| 131 | <div className="file-image"> |
| 132 | { f.isImage ? ( |
| 133 | <img src={ f.fileimage } alt="" /> |
| 134 | ) : ( |
| 135 | <i className="far fa-file-alt" /> |
| 136 | ) } |
| 137 | </div> |
| 138 | <div className="file-detail"> |
| 139 | <h6>{ f.filename }</h6> |
| 140 | <p> |
| 141 | <span>Size: { f.filesize }</span> |
| 142 | <span style={ { marginLeft: 10 } }> |
| 143 | Modified: { f.datetime } |
| 144 | </span> |
| 145 | </p> |
| 146 | <div className="file-actions"> |
| 147 | <button |
| 148 | className="file-action-btn" |
| 149 | onClick={ () => deleteFile( f.id ) } |
| 150 | > |
| 151 | Delete |
| 152 | </button> |
| 153 | </div> |
| 154 | </div> |
| 155 | </div> |
| 156 | ) ) } |
| 157 | </div> |
| 158 | </div> |
| 159 | ); |
| 160 | } |
| 161 |