@@ -79,6 +79,9 @@ export default function Page() {
7979 < GridItem >
8080 < Comp547 />
8181 </ GridItem >
82+ < GridItem >
83+ < Comp552 />
84+ </ GridItem >
8285 < GridItem >
8386 < Comp553 />
8487 </ GridItem >
@@ -463,6 +466,181 @@ const getFilePreview = (file: AnyFile<AnyFileRoute> & { preview?: string }) => {
463466 ) ;
464467} ;
465468
469+ function Comp552 ( ) {
470+ const [ isPending , startTransition ] = React . useTransition ( ) ;
471+ const {
472+ getRootProps,
473+ getInputProps,
474+ isDragActive,
475+ routeConfig,
476+ files,
477+ clearFiles,
478+ removeFile,
479+ setFiles,
480+ } = useUploadThingDropzone ( {
481+ route : routeRegistry . anyPublic ,
482+ disabled : isPending ,
483+ } ) ;
484+
485+ const uploadFiles = ( ) => {
486+ startTransition ( async ( ) => {
487+ await future_uploadFiles ( ( rr ) => rr . anyPublic , {
488+ files : files . filter ( ( f ) => f . status === "pending" ) ,
489+ input : { } ,
490+ onEvent : ( event ) => {
491+ setFiles ( ( prev ) =>
492+ prev . map ( ( f ) =>
493+ "file" in event && f === event . file ? event . file : f ,
494+ ) ,
495+ ) ;
496+ } ,
497+ } ) ;
498+ } ) ;
499+ } ;
500+
501+ return (
502+ < div className = "flex flex-col gap-2" >
503+ { /* Drop area */ }
504+ < div
505+ { ...getRootProps ( ) }
506+ data-dragging = { isDragActive || undefined }
507+ data-files = { files . length > 0 || undefined }
508+ className = "border-input data-[dragging=true]:bg-accent/50 has-[input:focus]:border-ring has-[input:focus]:ring-ring/50 not-data-[files]:justify-center relative flex min-h-52 flex-col items-center overflow-hidden rounded-xl border border-dashed p-4 transition-colors has-[input:focus]:ring-[3px]"
509+ >
510+ { files . length > 0 ? (
511+ < div className = "flex w-full flex-col gap-3" >
512+ < div className = "flex items-center justify-between gap-2" >
513+ < h3 className = "truncate text-sm font-medium" >
514+ Files ({ files . length } )
515+ </ h3 >
516+ < div className = "flex gap-2" >
517+ < label
518+ className = { twMerge (
519+ buttonVariants ( {
520+ variant : "outline" ,
521+ size : "sm" ,
522+ } ) ,
523+ "size-8" ,
524+ ) }
525+ >
526+ < input
527+ { ...getInputProps ( ) }
528+ className = "sr-only"
529+ aria-label = "Upload files"
530+ />
531+ < PlusIcon
532+ className = "size-3.5 opacity-60"
533+ aria-hidden = "true"
534+ />
535+ </ label >
536+ < Button
537+ variant = "outline"
538+ size = "sm"
539+ onClick = { ( e ) => {
540+ e . stopPropagation ( ) ;
541+ clearFiles ( ) ;
542+ } }
543+ >
544+ < Trash2Icon
545+ className = "size-3.5 opacity-60"
546+ aria-hidden = "true"
547+ />
548+ </ Button >
549+ < Button
550+ size = "sm"
551+ disabled = {
552+ isPending ||
553+ files . filter ( ( f ) => f . status === "pending" ) . length === 0
554+ }
555+ onClick = { ( e ) => {
556+ e . stopPropagation ( ) ;
557+ uploadFiles ( ) ;
558+ } }
559+ >
560+ Upload
561+ </ Button >
562+ </ div >
563+ </ div >
564+
565+ < div className = "grid grid-cols-2 gap-4 md:grid-cols-3" >
566+ { files . map ( ( file , index ) => (
567+ < div
568+ key = { file . key ?? index }
569+ className = "bg-background relative flex flex-col rounded-md border"
570+ >
571+ { getFilePreview ( file ) }
572+ < div className = "absolute -right-2 -top-2" >
573+ { file . status === "uploaded" ? (
574+ < div className = "border-background flex size-6 items-center justify-center rounded-full border-2 bg-green-500" >
575+ < CheckIcon className = "size-3.5 text-white" />
576+ </ div >
577+ ) : file . status === "uploading" ||
578+ ( file . status === "pending" && file . key ) ? (
579+ < div className = "text-muted-foreground/80 bg-background flex size-6 items-center justify-center rounded-full" >
580+ < Loader2Icon
581+ aria-hidden = "true"
582+ className = "size-3.5 animate-spin"
583+ />
584+ </ div >
585+ ) : (
586+ < Button
587+ onClick = { ( e ) => {
588+ e . stopPropagation ( ) ;
589+ removeFile ( file ) ;
590+ } }
591+ size = "icon"
592+ className = "border-background focus-visible:border-background size-6 rounded-full border-2 shadow-none"
593+ aria-label = "Remove image"
594+ >
595+ < XIcon className = "size-3.5" />
596+ </ Button >
597+ ) }
598+ </ div >
599+ < div className = "flex min-w-0 flex-col gap-0.5 border-t p-3" >
600+ < p className = "truncate text-[13px] font-medium" >
601+ { file . name }
602+ </ p >
603+ < p className = "text-muted-foreground truncate text-xs" >
604+ { bytesToFileSize ( file . size ) }
605+ </ p >
606+ </ div >
607+ </ div >
608+ ) ) }
609+ </ div >
610+ </ div >
611+ ) : (
612+ < div className = "flex flex-col items-center justify-center px-4 py-3 text-center" >
613+ < div
614+ className = "bg-background mb-2 flex size-11 shrink-0 items-center justify-center rounded-full border"
615+ aria-hidden = "true"
616+ >
617+ < ImageIcon className = "size-4 opacity-60" />
618+ </ div >
619+ < p className = "mb-1.5 text-sm font-medium" > Drop your files here</ p >
620+ < p className = "text-muted-foreground text-xs" >
621+ { allowedContentTextLabelGenerator ( routeConfig ) }
622+ </ p >
623+ < label
624+ className = { buttonVariants ( {
625+ variant : "outline" ,
626+ className : "mt-4" ,
627+ } ) }
628+ >
629+ < input
630+ { ...getInputProps ( ) }
631+ className = "sr-only"
632+ aria-label = "Upload files"
633+ />
634+ < UploadIcon className = "-ms-1 opacity-60" aria-hidden = "true" />
635+ Select files
636+ </ label >
637+ </ div >
638+ ) }
639+ </ div >
640+ </ div >
641+ ) ;
642+ }
643+
466644function Comp553 ( ) {
467645 const [ isPending , startTransition ] = React . useTransition ( ) ;
468646
0 commit comments