@@ -7,7 +7,6 @@ import { Textarea } from '@comp/ui/textarea';
77import type { Attachment } from '@db' ;
88import { AttachmentEntityType } from '@db' ;
99import { Loader2 , Paperclip , Plus } from 'lucide-react' ;
10- import { useAction } from 'next-safe-action/hooks' ;
1110import { useRouter } from 'next/navigation' ;
1211import type React from 'react' ;
1312import { useCallback , useRef , useState } from 'react' ;
@@ -38,60 +37,127 @@ export function TaskBody({
3837 onAttachmentsChange,
3938} : TaskBodyProps ) {
4039 const fileInputRef = useRef < HTMLInputElement > ( null ) ;
40+ const [ isUploading , setIsUploading ] = useState ( false ) ;
4141 const [ busyAttachmentId , setBusyAttachmentId ] = useState < string | null > ( null ) ;
4242 const router = useRouter ( ) ;
43- const { execute, isExecuting } = useAction ( uploadFile , {
44- onSuccess : ( ) => {
45- onAttachmentsChange ?.( ) ;
46- router . refresh ( ) ;
47- toast . success ( 'File uploaded successfully' ) ;
48- } ,
49- onError : ( { error } ) => {
50- console . error ( 'File upload failed:' , error ) ;
51- toast . error ( error . serverError || 'Failed to upload file. Check console for details.' ) ;
52- } ,
53- onSettled : ( ) => {
54- if ( fileInputRef . current ) {
55- fileInputRef . current . value = '' ;
56- }
57- } ,
58- } ) ;
43+
44+ const resetState = ( ) => {
45+ setIsUploading ( false ) ;
46+ if ( fileInputRef . current ) {
47+ fileInputRef . current . value = '' ;
48+ }
49+ } ;
5950
6051 const handleFileSelect = useCallback (
6152 async ( event : React . ChangeEvent < HTMLInputElement > ) => {
6253 const files = event . target . files ;
6354 if ( ! files || files . length === 0 ) return ;
55+ setIsUploading ( true ) ;
56+ try {
57+ for ( const file of Array . from ( files ) ) {
58+ const MAX_FILE_SIZE_MB = 10 ;
59+ const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024 ;
60+ if ( file . size > MAX_FILE_SIZE_BYTES ) {
61+ toast . error ( `File "${ file . name } " exceeds the ${ MAX_FILE_SIZE_MB } MB limit.` ) ;
62+ continue ;
63+ }
6464
65- for ( const file of Array . from ( files ) ) {
66- const MAX_FILE_SIZE_MB = 10 ;
67- const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024 ;
68- if ( file . size > MAX_FILE_SIZE_BYTES ) {
69- toast . error ( `File "${ file . name } " exceeds the ${ MAX_FILE_SIZE_MB } MB limit.` ) ;
70- continue ;
65+ const reader = new FileReader ( ) ;
66+ reader . onloadend = async ( ) => {
67+ const base64Data = ( reader . result as string ) ?. split ( ',' ) [ 1 ] ;
68+ if ( ! base64Data ) {
69+ toast . error ( 'Failed to read file data.' ) ;
70+ resetState ( ) ;
71+ return ;
72+ }
73+
74+ const result = await uploadFile ( {
75+ fileName : file . name ,
76+ fileType : file . type ,
77+ fileData : base64Data ,
78+ entityId : taskId ,
79+ entityType : AttachmentEntityType . task ,
80+ } ) ;
81+
82+ if ( result . success ) {
83+ toast . success ( 'File uploaded successfully.' ) ;
84+ onAttachmentsChange ?.( ) ;
85+ router . refresh ( ) ;
86+ } else {
87+ console . error ( 'File upload failed:' , result . error ) ;
88+ toast . error ( result . error || 'Failed to upload file. Check console for details.' ) ;
89+ }
90+ } ;
91+ reader . onerror = ( ) => {
92+ toast . error ( 'Error reading file.' ) ;
93+ resetState ( ) ;
94+ } ;
95+ reader . readAsDataURL ( file ) ;
7196 }
97+ } finally {
98+ // This finally block might run before all file readers are done.
99+ // It's better to manage the loading state inside the onloadend.
100+ }
101+ } ,
102+ [ taskId , onAttachmentsChange , router ] ,
103+ ) ;
104+
105+ // A better way to handle multiple file uploads
106+ const handleFileSelectMultiple = useCallback (
107+ async ( event : React . ChangeEvent < HTMLInputElement > ) => {
108+ const files = event . target . files ;
109+ if ( ! files || files . length === 0 ) return ;
110+ setIsUploading ( true ) ;
72111
73- const reader = new FileReader ( ) ;
74- reader . onloadend = async ( ) => {
75- const base64Data = ( reader . result as string ) ?. split ( ',' ) [ 1 ] ;
76- if ( ! base64Data ) {
77- toast . error ( 'Failed to read file data.' ) ;
78- return ;
112+ const uploadPromises = Array . from ( files ) . map ( ( file ) => {
113+ return new Promise ( ( resolve , reject ) => {
114+ const MAX_FILE_SIZE_MB = 10 ;
115+ const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024 ;
116+ if ( file . size > MAX_FILE_SIZE_BYTES ) {
117+ toast . error ( `File "${ file . name } " exceeds the ${ MAX_FILE_SIZE_MB } MB limit.` ) ;
118+ return resolve ( null ) ; // Resolve to skip this file
79119 }
80- execute ( {
81- fileName : file . name ,
82- fileType : file . type ,
83- fileData : base64Data ,
84- entityId : taskId ,
85- entityType : AttachmentEntityType . task ,
86- } ) ;
87- } ;
88- reader . onerror = ( ) => {
89- toast . error ( 'Error reading file.' ) ;
90- } ;
91- reader . readAsDataURL ( file ) ;
92- }
120+ const reader = new FileReader ( ) ;
121+ reader . onloadend = async ( ) => {
122+ try {
123+ const base64Data = ( reader . result as string ) ?. split ( ',' ) [ 1 ] ;
124+ if ( ! base64Data ) {
125+ throw new Error ( 'Failed to read file data.' ) ;
126+ }
127+ const result = await uploadFile ( {
128+ fileName : file . name ,
129+ fileType : file . type ,
130+ fileData : base64Data ,
131+ entityId : taskId ,
132+ entityType : AttachmentEntityType . task ,
133+ } ) ;
134+ if ( result . success ) {
135+ toast . success ( `File "${ file . name } " uploaded successfully.` ) ;
136+ resolve ( result ) ;
137+ } else {
138+ throw new Error ( result . error ) ;
139+ }
140+ } catch ( error ) {
141+ console . error ( `Failed to upload ${ file . name } :` , error ) ;
142+ toast . error ( `Failed to upload ${ file . name } .` ) ;
143+ resolve ( null ) ; // Resolve even if there's an error to not break Promise.all
144+ }
145+ } ;
146+ reader . onerror = ( ) => {
147+ toast . error ( `Error reading file "${ file . name } ".` ) ;
148+ resolve ( null ) ;
149+ } ;
150+ reader . readAsDataURL ( file ) ;
151+ } ) ;
152+ } ) ;
153+
154+ await Promise . all ( uploadPromises ) ;
155+
156+ onAttachmentsChange ?.( ) ;
157+ router . refresh ( ) ;
158+ resetState ( ) ;
93159 } ,
94- [ execute , taskId , onAttachmentsChange , router ] ,
160+ [ taskId , onAttachmentsChange , router ] ,
95161 ) ;
96162
97163 const triggerFileInput = ( ) => {
@@ -136,21 +202,21 @@ export function TaskBody({
136202 onChange = { onTitleChange }
137203 className = "h-auto shrink-0 border-none bg-transparent p-0 md:text-lg font-semibold tracking-tight shadow-none focus-visible:ring-0"
138204 placeholder = "Task Title"
139- disabled = { disabled || isExecuting || ! ! busyAttachmentId }
205+ disabled = { disabled || isUploading || ! ! busyAttachmentId }
140206 />
141207 < Textarea
142208 value = { description }
143209 onChange = { onDescriptionChange }
144210 placeholder = "Add description..."
145211 className = "text-muted-foreground text-md min-h-[80px] resize-none border-none p-0 shadow-none focus-visible:ring-0"
146- disabled = { disabled || isExecuting || ! ! busyAttachmentId }
212+ disabled = { disabled || isUploading || ! ! busyAttachmentId }
147213 />
148214 < input
149215 type = "file"
150216 ref = { fileInputRef }
151- onChange = { handleFileSelect }
217+ onChange = { handleFileSelectMultiple }
152218 className = "hidden"
153- disabled = { isExecuting || ! ! busyAttachmentId }
219+ disabled = { isUploading || ! ! busyAttachmentId }
154220 multiple
155221 />
156222 < div className = "space-y-3" >
@@ -161,11 +227,11 @@ export function TaskBody({
161227 variant = "ghost"
162228 size = "icon"
163229 onClick = { triggerFileInput }
164- disabled = { isExecuting || ! ! busyAttachmentId }
230+ disabled = { isUploading || ! ! busyAttachmentId }
165231 className = "text-muted-foreground hover:text-foreground flex h-7 w-7 items-center justify-center"
166232 aria-label = "Add attachment"
167233 >
168- { isExecuting ? (
234+ { isUploading ? (
169235 < Loader2 className = "h-4 w-4 animate-spin" />
170236 ) : (
171237 < Paperclip className = "h-4 w-4" />
@@ -185,13 +251,13 @@ export function TaskBody({
185251 onClickFilename = { handleDownloadClick }
186252 onDelete = { handleDeleteAttachment }
187253 isBusy = { isBusy }
188- isParentBusy = { isExecuting }
254+ isParentBusy = { isUploading }
189255 />
190256 ) ;
191257 } ) }
192258 </ div >
193259 ) : (
194- ! isExecuting && (
260+ ! isUploading && (
195261 < p className = "text-muted-foreground pt-1 text-sm italic" >
196262 No attachments yet. Click the < Paperclip className = "inline h-4 w-4" /> icon above to
197263 add one.
@@ -203,11 +269,11 @@ export function TaskBody({
203269 < Button
204270 variant = "outline"
205271 onClick = { triggerFileInput }
206- disabled = { isExecuting || ! ! busyAttachmentId }
272+ disabled = { isUploading || ! ! busyAttachmentId }
207273 className = "mt-2 w-full justify-center gap-2"
208274 aria-label = "Add attachment"
209275 >
210- { isExecuting ? (
276+ { isUploading ? (
211277 < Loader2 className = "h-4 w-4 animate-spin" />
212278 ) : (
213279 < Plus className = "h-4 w-4" />
0 commit comments