shadimage
utilcn
componentupload-filesource: ok · hit

Upload A File To Storage

Upload a file to a storage provider using a presigned upload URL.

npx shadcn add "https://utilcn.dev/r/upload-file.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@tanstack/react-query

Files2 · source cached 10/14/2026, 3:02:00 PM

registry/default/storage/upload-file.tsx
'use client';

import { type ChangeEvent, useState } from 'react';
import { useUploadFile } from '@/registry/default/storage/use-upload-file';

export function UploadFile() {
  const [progress, setProgress] = useState(0);
  const [isUploading, setIsUploading] = useState(false);
  const { uploadFile } = useUploadFile();

  const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) {
      return;
    }

    setIsUploading(true);
    uploadFile({
      file,
      onProgress: (p: number) => setProgress(p),
      onSuccess: (url: string) => {
        console.log('Uploaded to:', url);
        setIsUploading(false);
        setProgress(0);
      },
      onError: (err: Error) => {
        console.error(`Upload failed: ${err.message}`);
        setIsUploading(false);
        setProgress(0);
      },
    });
  };

  return (
    <div className="flex flex-col gap-4">
      <input
        className="file:mr-4 file:rounded-md file:border-0 file:bg-secondary file:px-4 file:py-2 file:font-semibold file:text-secondary-foreground file:text-sm hover:file:bg-secondary/80"
        onChange={handleFileChange}
        type="file"
      />

      {isUploading && (
        <div className="mt-4 flex items-center gap-2">
          <div className="h-2 w-full rounded-full bg-secondary">
            <div
              className="h-2 rounded-full bg-primary transition-all duration-300"
              style={{ width: `${progress}%` }}
            />
          </div>
          <span className="text-muted-foreground text-sm">{progress}%</span>
        </div>
      )}
    </div>
  );
}
registry/default/storage/use-upload-file.ts
import { useCallback } from 'react';

type UploadArgs = {
  file: File;
  onProgress?: (percent: number) => void;
  onSuccess?: (fileUrl: string) => void;
  onError?: (error: Error) => void;
};

export function useUploadFile() {
  const uploadFile = useCallback(
    async ({ file, onProgress, onSuccess, onError }: UploadArgs) => {
      try {
        const presignRes = await fetch('http://localhost:8080/uploadFile', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            fileName: file.name,
            contentLength: file.size,
          }),
        });

        if (!presignRes.ok) {
          throw new Error('Failed to get presigned URL');
        }
        const presign = await presignRes.json();

        await new Promise<void>((resolve, reject) => {
          const xhr = new XMLHttpRequest();
          xhr.open('PUT', presign.uploadUrl);

          xhr.setRequestHeader('Content-Type', file.type);

          xhr.upload.onprogress = (evt) => {
            if (evt.lengthComputable && onProgress) {
              const PERCENTAGE_MULTIPLIER = 100;
              const percent = Math.round(
                (evt.loaded * PERCENTAGE_MULTIPLIER) / evt.total,
              );
              onProgress(percent);
            }
          };

          xhr.onload = () => {
            if (xhr.status >= 200 && xhr.status < 300) {
              resolve();
            } else {
              reject(new Error('Upload failed'));
            }
          };

          xhr.onerror = () => reject(new Error('Upload failed'));
          xhr.send(file);
        });

        const fileUrl = presign.fileUrl as string;
        onSuccess?.(fileUrl);
        return fileUrl;
      } catch (error) {
        const uploadError =
          error instanceof Error ? error : new Error('Upload failed');
        onError?.(uploadError);
        throw uploadError;
      }
    },
    [],
  );

  return { uploadFile };
}

More from utilcn

Browse all components in this library →