useFetch.js
2 days ago
useFileUploader.js
2 days ago
useModalState.js
2 days ago
useSearchCombobox.js
2 days ago
useFileUploader.js
166 lines
| 1 | import { useState, useCallback, useRef } from 'react'; |
| 2 | |
| 3 | function generateId() { |
| 4 | return ( |
| 5 | Math.random().toString( 36 ).slice( 2, 9 ) + Date.now().toString( 36 ) |
| 6 | ); |
| 7 | } |
| 8 | |
| 9 | function formatSize( bytes, decimals = 2 ) { |
| 10 | if ( bytes === 0 ) { |
| 11 | return '0 Bytes'; |
| 12 | } |
| 13 | const k = 1024; |
| 14 | const sizes = [ 'Bytes', 'KB', 'MB', 'GB', 'TB' ]; |
| 15 | const i = Math.floor( Math.log( bytes ) / Math.log( k ) ); |
| 16 | return ( |
| 17 | parseFloat( |
| 18 | ( bytes / Math.pow( k, i ) ).toFixed( Math.max( 0, decimals ) ) |
| 19 | ) + |
| 20 | ' ' + |
| 21 | sizes[ i ] |
| 22 | ); |
| 23 | } |
| 24 | |
| 25 | function isImageFile( filename ) { |
| 26 | return /\.(jpg|jpeg|png|gif|svg)$/i.test( filename ); |
| 27 | } |
| 28 | |
| 29 | function getExt( filename ) { |
| 30 | return filename.split( '.' ).pop().toLowerCase(); |
| 31 | } |
| 32 | |
| 33 | export function useFileUploader( { |
| 34 | maxFiles = 10, |
| 35 | maxSizeMB = 5, |
| 36 | allowedExt = [ 'jpg', 'jpeg', 'png', 'gif', 'svg', 'pdf', 'docx', 'txt' ], |
| 37 | } = {} ) { |
| 38 | const [ selectedFiles, setSelectedFiles ] = useState( [] ); |
| 39 | const [ errors, setErrors ] = useState( [] ); |
| 40 | |
| 41 | // Tracks committed + in-flight count so slots never goes stale between async reads |
| 42 | const totalCountRef = useRef( 0 ); |
| 43 | |
| 44 | const syncFiles = ( updater ) => { |
| 45 | setSelectedFiles( ( prev ) => { |
| 46 | const next = |
| 47 | typeof updater === 'function' ? updater( prev ) : updater; |
| 48 | totalCountRef.current = next.length; |
| 49 | return next; |
| 50 | } ); |
| 51 | }; |
| 52 | |
| 53 | const maxSizeBytes = maxSizeMB * 1024 * 1024; |
| 54 | |
| 55 | const validateFile = useCallback( |
| 56 | ( file ) => { |
| 57 | const errs = []; |
| 58 | const ext = getExt( file.name ); |
| 59 | if ( ! allowedExt.includes( ext ) ) { |
| 60 | errs.push( `".${ ext }" is not an allowed extension` ); |
| 61 | } |
| 62 | if ( file.size > maxSizeBytes ) { |
| 63 | errs.push( |
| 64 | `exceeds max size of ${ maxSizeMB } MB (is ${ formatSize( |
| 65 | file.size |
| 66 | ) })` |
| 67 | ); |
| 68 | } |
| 69 | return errs; |
| 70 | }, |
| 71 | [ allowedExt, maxSizeBytes, maxSizeMB ] |
| 72 | ); |
| 73 | |
| 74 | const processFiles = useCallback( |
| 75 | ( fileList ) => { |
| 76 | const slots = maxFiles - totalCountRef.current; |
| 77 | |
| 78 | if ( slots <= 0 ) { |
| 79 | setErrors( [ |
| 80 | { |
| 81 | name: 'Selection blocked', |
| 82 | reasons: [ |
| 83 | `you have already reached the limit of ${ maxFiles } files`, |
| 84 | ], |
| 85 | }, |
| 86 | ] ); |
| 87 | return; |
| 88 | } |
| 89 | |
| 90 | const rejections = []; |
| 91 | let accepted = 0; |
| 92 | |
| 93 | for ( const file of fileList ) { |
| 94 | if ( accepted >= slots ) { |
| 95 | rejections.push( { |
| 96 | name: file.name, |
| 97 | reasons: [ `max file limit of ${ maxFiles } reached` ], |
| 98 | } ); |
| 99 | continue; |
| 100 | } |
| 101 | |
| 102 | const errs = validateFile( file ); |
| 103 | if ( errs.length ) { |
| 104 | rejections.push( { name: file.name, reasons: errs } ); |
| 105 | continue; |
| 106 | } |
| 107 | |
| 108 | // Increment ref immediately — before the async read completes — |
| 109 | // so the next processFiles call sees the correct count right away |
| 110 | totalCountRef.current += 1; |
| 111 | |
| 112 | const reader = new FileReader(); |
| 113 | reader.onloadend = () => { |
| 114 | syncFiles( ( prev ) => [ |
| 115 | ...prev, |
| 116 | { |
| 117 | id: generateId(), |
| 118 | filename: file.name, |
| 119 | filetype: file.type, |
| 120 | fileimage: reader.result, |
| 121 | datetime: file.lastModified |
| 122 | ? new Date( file.lastModified ).toLocaleString( |
| 123 | 'en-IN' |
| 124 | ) |
| 125 | : 'Unknown', |
| 126 | filesize: formatSize( file.size ), |
| 127 | isImage: isImageFile( file.name ), |
| 128 | }, |
| 129 | ] ); |
| 130 | }; |
| 131 | reader.readAsDataURL( file ); |
| 132 | accepted++; |
| 133 | } |
| 134 | |
| 135 | setErrors( rejections ); |
| 136 | }, |
| 137 | [ maxFiles, validateFile ] |
| 138 | ); |
| 139 | |
| 140 | const deleteFile = useCallback( ( id ) => { |
| 141 | if ( |
| 142 | globalThis.confirm( 'Are you sure you want to delete this file?' ) |
| 143 | ) { |
| 144 | syncFiles( ( prev ) => prev.filter( ( f ) => f.id !== id ) ); |
| 145 | } |
| 146 | }, [] ); |
| 147 | |
| 148 | const reset = useCallback( () => { |
| 149 | totalCountRef.current = 0; |
| 150 | syncFiles( [] ); |
| 151 | setErrors( [] ); |
| 152 | }, [] ); |
| 153 | |
| 154 | const atLimit = selectedFiles.length >= maxFiles; |
| 155 | |
| 156 | return { |
| 157 | selectedFiles, |
| 158 | errors, |
| 159 | atLimit, |
| 160 | processFiles, |
| 161 | deleteFile, |
| 162 | reset, |
| 163 | meta: { maxFiles, maxSizeMB, allowedExt }, |
| 164 | }; |
| 165 | } |
| 166 |