@@ -12,14 +12,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
12
12
import { PageHeader } from "../commonComponents/PageHeader/PageHeader" ;
13
13
import { DnDFileInput } from "@thunderstore/react-dnd" ;
14
14
import { useCallback , useEffect , useState } from "react" ;
15
- import { initMultipartUpload , IUploadHandle } from "@thunderstore/ts-uploader" ;
16
- import { useUploadProgress } from "@thunderstore/ts-uploader-react" ;
15
+ import { MultipartUpload , IBaseUploadHandle } from "@thunderstore/ts-uploader" ;
16
+ import {
17
+ useUploadProgress ,
18
+ useUploadStatus ,
19
+ useUploadError ,
20
+ useUploadControls ,
21
+ } from "@thunderstore/ts-uploader-react" ;
17
22
// import { useOutletContext } from "@remix-run/react";
18
23
// import { OutletContextShape } from "../../root";
19
24
import { useSession } from "@thunderstore/ts-api-react" ;
20
25
import { faFileZip , faTreasureChest } from "@fortawesome/pro-solid-svg-icons" ;
21
26
import { UserMedia } from "@thunderstore/ts-uploader/src/client/types" ;
22
- import { DapperTs } from "@thunderstore/dapper-ts" ;
27
+ import { DapperTs , PackageSubmissionResponse } from "@thunderstore/dapper-ts" ;
23
28
import { MetaFunction } from "@remix-run/node" ;
24
29
import { useLoaderData } from "@remix-run/react" ;
25
30
@@ -106,48 +111,108 @@ export default function Upload() {
106
111
) ;
107
112
108
113
const [ file , setFile ] = useState < File | null > ( null ) ;
109
- const [ handle , setHandle ] = useState < IUploadHandle > ( ) ;
114
+ const [ handle , setHandle ] = useState < IBaseUploadHandle > ( ) ;
110
115
const [ lock , setLock ] = useState < boolean > ( false ) ;
111
116
const [ isDone , setIsDone ] = useState < boolean > ( false ) ;
112
117
118
+ const error = useUploadError ( handle ) ;
119
+ const controls = useUploadControls ( handle ) ;
120
+
113
121
const [ usermedia , setUsermedia ] = useState < UserMedia > ( ) ;
114
122
115
- const startUpload = useCallback ( ( ) => {
123
+ const [ submissionError , setSubmissionError ] =
124
+ useState < PackageSubmissionResponse > ( { } ) ;
125
+
126
+ const startUpload = useCallback ( async ( ) => {
116
127
if ( ! file ) return ;
117
- const config = session . getConfig ( ) ;
118
- setLock ( true ) ;
119
- initMultipartUpload ( file , {
120
- api : {
121
- domain : config . apiHost ?? "https://thunderstore.io" ,
122
- authorization : `Session ${ config . sessionId } ` ,
123
- } ,
124
- } ) . then ( ( handle ) => {
125
- setHandle ( handle ) ;
126
- handle . startUpload ( ) . then (
127
- ( value ) => {
128
- setUsermedia ( value ) ;
129
- setIsDone ( true ) ;
128
+
129
+ try {
130
+ const config = session . getConfig ( ) ;
131
+ if ( ! config . apiHost ) {
132
+ throw new Error ( "API host is not configured" ) ;
133
+ }
134
+ const upload = new MultipartUpload ( {
135
+ file ,
136
+ api : {
137
+ domain : config . apiHost ,
138
+ authorization : config . sessionId
139
+ ? `Session ${ config . sessionId } `
140
+ : undefined ,
130
141
} ,
131
- ( reason ) => {
132
- console . log ( reason ) ;
133
- }
134
- ) ;
135
- } ) ;
142
+ } ) ;
143
+
144
+ setLock ( true ) ;
145
+ await upload . start ( ) ;
146
+ setHandle ( upload ) ;
147
+ setUsermedia ( upload . uploadHandle ) ;
148
+ setIsDone ( true ) ;
149
+ } catch ( error ) {
150
+ console . error ( "Upload failed:" , error ) ;
151
+ if ( error instanceof Error ) {
152
+ alert ( `Upload failed: ${ error . message } ` ) ;
153
+ }
154
+ } finally {
155
+ setLock ( false ) ;
156
+ }
136
157
} , [ file , session ] ) ;
137
158
138
- const submit = useCallback ( ( ) => {
139
- const config = session . getConfig ( ) ;
140
- const dapper = new DapperTs ( ( ) => config ) ;
141
- dapper . postPackageSubmissionMetadata (
142
- team ?? "" ,
143
- selectedCategories . map ( ( cat ) => cat . categoryId ) ,
144
- selectedCommunities . map (
145
- ( community ) => ( community as NewSelectOption ) . value
146
- ) ,
147
- NSFW ,
148
- usermedia ?. uuid ?? "" ,
149
- [ ] // TODO: wth are community categories??
150
- ) ;
159
+ const submit = useCallback ( async ( ) => {
160
+ if ( ! usermedia ?. uuid ) {
161
+ setSubmissionError ( {
162
+ __all__ : [ "Upload not completed" ] ,
163
+ } ) ;
164
+ return ;
165
+ }
166
+
167
+ try {
168
+ setSubmissionError ( { } ) ;
169
+ const config = session . getConfig ( ) ;
170
+ if ( ! config . apiHost ) {
171
+ throw new Error ( "API host is not configured" ) ;
172
+ }
173
+ const dapper = new DapperTs ( ( ) => config ) ;
174
+ const result = await dapper . postPackageSubmissionMetadata (
175
+ team ?? "" ,
176
+ selectedCommunities . map (
177
+ ( community ) => ( community as NewSelectOption ) . value
178
+ ) ,
179
+ NSFW ,
180
+ usermedia . uuid ,
181
+ selectedCategories . map ( ( cat ) => cat . categoryId ) ,
182
+ { }
183
+ ) ;
184
+
185
+ // Check if the result is a SubmissionError
186
+ if (
187
+ "__all__" in result ||
188
+ "author_name" in result ||
189
+ "communities" in result
190
+ ) {
191
+ setSubmissionError ( result ) ;
192
+ return ;
193
+ }
194
+
195
+ // Handle successful submission
196
+ if ( "task_error" in result && result . task_error ) {
197
+ setSubmissionError ( {
198
+ __all__ : [ `Submission failed: ${ result . result } ` ] ,
199
+ } ) ;
200
+ return ;
201
+ }
202
+
203
+ alert ( "Package submitted successfully!" ) ;
204
+ } catch ( error ) {
205
+ console . error ( "Submission failed:" , error ) ;
206
+ if ( error instanceof Error ) {
207
+ setSubmissionError ( {
208
+ __all__ : [ error . message ] ,
209
+ } ) ;
210
+ } else {
211
+ setSubmissionError ( {
212
+ __all__ : [ "An unexpected error occurred during submission" ] ,
213
+ } ) ;
214
+ }
215
+ }
151
216
} , [ usermedia , NSFW , team , selectedCommunities , selectedCategories , session ] ) ;
152
217
153
218
useEffect ( ( ) => {
@@ -175,6 +240,16 @@ export default function Upload() {
175
240
}
176
241
} , [ selectedCommunities ] ) ;
177
242
243
+ // Helper function to format field names for display
244
+ const formatFieldName = ( field : string ) => {
245
+ return field
246
+ . split ( "_" )
247
+ . map ( ( word ) => word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 ) )
248
+ . join ( " " ) ;
249
+ } ;
250
+
251
+ // console.log(submissionError);
252
+
178
253
return (
179
254
< div className = "container container--y container--full layout__content" >
180
255
< NewBreadCrumbs >
@@ -252,8 +327,17 @@ export default function Upload() {
252
327
< div className = "upload__content" >
253
328
< NewSelectSearch
254
329
options = { [
255
- { value : "team1" , label : "Team 1" } ,
256
- { value : "team2" , label : "Team 2" } ,
330
+ { value : "Test_Team_0" , label : "Test_Team_0" } ,
331
+ { value : "Test_Team_1" , label : "Test_Team_1" } ,
332
+ { value : "Test_Team_2" , label : "Test_Team_2" } ,
333
+ { value : "Test_Team_3" , label : "Test_Team_3" } ,
334
+ { value : "Test_Team_4" , label : "Test_Team_4" } ,
335
+ { value : "Test_Team_5" , label : "Test_Team_5" } ,
336
+ { value : "Test_Team_6" , label : "Test_Team_6" } ,
337
+ { value : "Test_Team_7" , label : "Test_Team_7" } ,
338
+ { value : "Test_Team_8" , label : "Test_Team_8" } ,
339
+ { value : "Test_Team_9" , label : "Test_Team_9" } ,
340
+ { value : "Test_Team_10" , label : "Test_Team_10" } ,
257
341
] }
258
342
onChange = { ( val ) => setTeam ( val ?. value ) }
259
343
value = { team ? { value : team , label : team } : undefined }
@@ -374,21 +458,30 @@ export default function Upload() {
374
458
< div className = "upload__content" >
375
459
< div className = "upload__buttons" >
376
460
< NewButton
377
- // onClick={startUpload}
461
+ onClick = { ( ) => {
462
+ setFile ( null ) ;
463
+ setHandle ( undefined ) ;
464
+ setUsermedia ( undefined ) ;
465
+ setIsDone ( false ) ;
466
+ setSelectedCommunities ( [ ] ) ;
467
+ setSelectedCategories ( [ ] ) ;
468
+ setTeam ( undefined ) ;
469
+ setNSFW ( false ) ;
470
+ } }
378
471
csVariant = "secondary"
379
472
csSize = "big"
380
473
>
381
474
Reset
382
475
</ NewButton >
383
476
{ isDone ? (
384
477
< NewButton
385
- // disabled={!file || !!handle || lock }
478
+ disabled = { ! usermedia ?. uuid || ! selectedCommunities . length }
386
479
onClick = { submit }
387
480
csVariant = "accent"
388
481
csSize = "big"
389
482
rootClasses = "upload__submit"
390
483
>
391
- Submit
484
+ Submit Package
392
485
</ NewButton >
393
486
) : (
394
487
< NewButton
@@ -398,12 +491,43 @@ export default function Upload() {
398
491
csSize = "big"
399
492
rootClasses = "upload__submit"
400
493
>
401
- Start upload
494
+ Upload File
402
495
</ NewButton >
403
496
) }
404
497
</ div >
405
498
< UploadProgressDisplay handle = { handle } />
406
- < p > Upload success: { isDone . toString ( ) } </ p >
499
+ { error && (
500
+ < div className = "upload__error" >
501
+ < p > { error . message } </ p >
502
+ { error . retryable && (
503
+ < NewButton onClick = { controls . retry } > Retry</ NewButton >
504
+ ) }
505
+ </ div >
506
+ ) }
507
+ { isDone && ! usermedia ?. uuid && (
508
+ < div className = "upload__error" >
509
+ < p >
510
+ Upload completed but no UUID was received. Please try again.
511
+ </ p >
512
+ </ div >
513
+ ) }
514
+ { isDone && usermedia ?. uuid && ! selectedCommunities . length && (
515
+ < div className = "upload__error" >
516
+ < p > Please select at least one community.</ p >
517
+ </ div >
518
+ ) }
519
+ { Object . keys ( submissionError ) . length > 0 && (
520
+ < div className = "upload__error" >
521
+ { Object . entries ( submissionError ) . map ( ( [ field , errors ] ) => (
522
+ < div key = { field } >
523
+ { field !== "__all__" && (
524
+ < strong > { formatFieldName ( field ) } : </ strong >
525
+ ) }
526
+ { Array . isArray ( errors ) ? errors . join ( ", " ) : String ( errors ) }
527
+ </ div >
528
+ ) ) }
529
+ </ div >
530
+ ) }
407
531
</ div >
408
532
</ div >
409
533
{ /* <div className="upload-form">
@@ -433,12 +557,47 @@ export default function Upload() {
433
557
) ;
434
558
}
435
559
436
- const UploadProgressDisplay = ( props : { handle ?: IUploadHandle } ) => {
560
+ const UploadProgressDisplay = ( props : { handle ?: IBaseUploadHandle } ) => {
437
561
const progress = useUploadProgress ( props . handle ) ;
562
+ const status = useUploadStatus ( props . handle ) ;
563
+ const error = useUploadError ( props . handle ) ;
564
+ const controls = useUploadControls ( props . handle ) ;
565
+
438
566
if ( ! progress ) return < p > Upload not started</ p > ;
567
+
568
+ const percent = Math . round ( ( progress . complete / progress . total ) * 100 ) ;
569
+ const speed = Math . round ( progress . metrics . bytesPerSecond / 1024 / 1024 ) ; // MB/s
570
+
439
571
return (
440
- < p >
441
- Progress: { progress . complete } / { progress . total }
442
- </ p >
572
+ < div className = "upload__progress" >
573
+ < div className = "upload__progress-bar" >
574
+ < div
575
+ className = "upload__progress-bar-fill"
576
+ style = { { width : `${ percent } %` } }
577
+ />
578
+ </ div >
579
+ < div className = "upload__progress-info" >
580
+ < span > { percent } %</ span >
581
+ < span > { speed } MB/s</ span >
582
+ < span > { status } </ span >
583
+ </ div >
584
+ { error && (
585
+ < div className = "upload__error" >
586
+ < p > { error . message } </ p >
587
+ { error . retryable && (
588
+ < NewButton onClick = { controls . retry } > Retry</ NewButton >
589
+ ) }
590
+ </ div >
591
+ ) }
592
+ { status === "running" && (
593
+ < NewButton onClick = { controls . pause } > Pause</ NewButton >
594
+ ) }
595
+ { status === "paused" && (
596
+ < NewButton onClick = { controls . resume } > Resume</ NewButton >
597
+ ) }
598
+ { ( status === "running" || status === "paused" ) && (
599
+ < NewButton onClick = { controls . abort } > Cancel</ NewButton >
600
+ ) }
601
+ </ div >
443
602
) ;
444
603
} ;
0 commit comments