Back to blog

Friday, May 9, 2025

Learning AWS

cover

Login to your Amazon account and search for S3 and go ahead and create an s3 bucket and give it policies

S3 Bucket Policies for Allowing the media to be publically accessed

{
"Version": "2012-10-17",
"Statement": [{
  "Sid": "PublicReadGetObject",
  "Principal": "*",
  "Effect": "Allow",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::desishub-web-storage/*"
}]
}

IAM (Identity Access Management)

It helps to manage access to AWS Resources

Create IAM User for the S3 Bucket and add him to a group and then give the group the Awss3FullAccess previledges

An IAM user is an identity with long-term credentials that is used to interact with AWS in an account.

  • Give a user a name eg desishub-storage-user and add him to a group, you can create a group eg amazon-s3-users

  • After creating a user, then click in the user and create the access key

  • Download the Access key in the excel file, and also navigate to ur s3 bucket and get the region eg europe-1 and also add it your excel

Add theses environmental variables

NEXT_PUBLIC_AWS_S3_REGION = "";
NEXT_PUBLIC_AWS_S3_ACCESS_KEY_ID = "";
NEXT_PUBLIC_AWS_S3_SECRET_ACCESS_KEY = "";
NEXT_PUBLIC_AWS_S3_BUCKET_NAME = "";
NEXT_PUBLIC_AWS_CLOUDFRONT_URL = "";
  • Create your Next js app and then lets install s3 client sdk

  • Navigate to S3 Client SDK

  • Install the sdk

pnpm add @aws-sdk/client-s3

Create a next endpoint to handle the image Uploads '/s3-uploads'

import {
  S3Client,
  PutObjectCommand,
  PutObjectCommandOutput,
} from "@aws-sdk/client-s3";

const client = new S3Client({
  region: process.env.NEXT_PUBLIC_AWS_S3_REGION ?? "",
  credentials: {
    secretAccessKey: process.env.NEXT_PUBLIC_AWS_S3_SECRET_ACCESS_KEY ?? "",
    accessKeyId: process.env.NEXT_PUBLIC_AWS_S3_ACCESS_KEY_ID ?? "",
  },
});

// Configure allowed file types and their corresponding MIME types
const ALLOWED_FILE_TYPES: Record<string, string[]> = {
  // Images
  jpg: ["image/jpeg"],
  jpeg: ["image/jpeg"],
  png: ["image/png"],
  gif: ["image/gif"],
  webp: ["image/webp"],
  svg: ["image/svg+xml"],

  // Videos
  mp4: ["video/mp4"],
  webm: ["video/webm"],
  mov: ["video/quicktime"],
  avi: ["video/x-msvideo"],
  mkv: ["video/x-matroska"],

  // Documents
  pdf: ["application/pdf"],
  doc: ["application/msword"],
  docx: [
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  ],
  xls: ["application/vnd.ms-excel"],
  xlsx: ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
  ppt: ["application/vnd.ms-powerpoint"],
  pptx: [
    "application/vnd.openxmlformats-officedocument.presentationml.presentation",
  ],

  // Others
  txt: ["text/plain"],
  csv: ["text/csv"],
  json: ["application/json"],
  zip: ["application/zip"],
};

// Maximum file size in bytes (default: 10MB)
const MAX_FILE_SIZE =
  parseInt(process.env.MAX_FILE_SIZE || "") || 10 * 1024 * 1024;

async function uploadFileToS3(
  file: Buffer,
  filename: string,
  contentType: string
): Promise<{
  fileName: string;
  fileUrl: string;
}> {
  // Generate a cleaned filename to avoid S3 issues
  const cleanedOriginalName = filename.replace(/[^a-zA-Z0-9.-]/g, "-");

  // Get file extension
  const fileExt = cleanedOriginalName.split(".").pop()?.toLowerCase() || "";

  // Create unique filename with timestamp to prevent overwriting
  const timestamp = Date.now();
  const uniqueFileName = `${timestamp}-${cleanedOriginalName}`;

  // Define the folder structure in S3 based on content type
  let folder = "others";
  if (contentType.startsWith("image/")) {
    folder = "images";
  } else if (contentType.startsWith("video/")) {
    folder = "videos";
  } else if (contentType.startsWith("application/")) {
    folder = "documents";
  }

  // Create the complete file path in S3
  const key = `desishub-assets/${folder}/${uniqueFileName}`;

  // Set the ACL based on environment
  const isPublic = process.env.S3_PUBLIC_ACCESS === "true";

  // Configure upload parameters
  const params = {
    Bucket: process.env.NEXT_PUBLIC_AWS_S3_BUCKET_NAME,
    Key: key,
    Body: file,
    ContentType: contentType,
  };

  try {
    const command = new PutObjectCommand(params);
    const result: PutObjectCommandOutput = await client.send(command);

    console.log("S3 upload successful:", result);

    // Construct the public URL for the uploaded file
    const fileUrl = `${process.env.NEXT_PUBLIC_AWS_CLOUDFRONT_URL}/${key}`;

    return {
      fileName: uniqueFileName,
      fileUrl,
    };
  } catch (error) {
    console.error("S3 upload error:", error);
    throw error;
  }
}

// Helper function to determine ContentType based on file extension
function getContentType(filename: string): string {
  const extension = filename.split(".").pop()?.toLowerCase() || "";

  for (const [ext, mimeTypes] of Object.entries(ALLOWED_FILE_TYPES)) {
    if (ext === extension) {
      return mimeTypes[0]; // Return the first MIME type associated with the extension
    }
  }

  // If extension not found in the allowed types, return octet-stream as default
  return "application/octet-stream";
}

// Validate file extensions and types
function validateFile(file: File): { valid: boolean; message?: string } {
  // Validate file size
  if (file.size > MAX_FILE_SIZE) {
    return {
      valid: false,
      message: `File size exceeds the maximum allowed size of ${
        MAX_FILE_SIZE / (1024 * 1024)
      }MB`,
    };
  }

  const extension = file.name.split(".").pop()?.toLowerCase() || "";

  // Check if the extension is in our allowed types
  if (!Object.keys(ALLOWED_FILE_TYPES).includes(extension)) {
    return {
      valid: false,
      message: `File type .${extension} is not allowed`,
    };
  }

  // Check if the file's MIME type matches what we expect for this extension
  const expectedMimeTypes = ALLOWED_FILE_TYPES[extension];
  if (!expectedMimeTypes.includes(file.type)) {
    // Either the file type doesn't match the extension, or it's not in our list
    return {
      valid: false,
      message: `Invalid file type. Expected ${expectedMimeTypes.join(
        " or "
      )} for .${extension} files`,
    };
  }

  return { valid: true };
}

export async function POST(request: Request): Promise<Response> {
  try {
    const formData = await request.formData();
    const file = formData.get("file") as File | null;

    if (!file) {
      return new Response(
        JSON.stringify({
          error: "File is required",
        }),
        {
          status: 400,
          headers: { "Content-Type": "application/json" },
        }
      );
    }

    // Validate the file
    const validation = validateFile(file);
    if (!validation.valid) {
      return new Response(
        JSON.stringify({
          error: validation.message,
        }),
        {
          status: 400,
          headers: { "Content-Type": "application/json" },
        }
      );
    }

    // Convert File to Buffer
    const buffer = Buffer.from(await file.arrayBuffer());

    // Get content type
    const contentType = getContentType(file.name);

    // Upload to S3
    const uploadResult = await uploadFileToS3(buffer, file.name, contentType);

    return new Response(
      JSON.stringify({
        success: true,
        fileName: uploadResult.fileName,
        fileUrl: uploadResult.fileUrl,
        contentType,
      }),
      {
        status: 200,
        headers: { "Content-Type": "application/json" },
      }
    );
  } catch (error) {
    console.error("S3 upload error:", error);

    // Determine if the error is related to access permissions
    const isAccessDenied =
      error instanceof Error &&
      (error.name === "AccessDenied" ||
        error.message.includes("Access Denied"));

    // Return appropriate error response
    return new Response(
      JSON.stringify({
        error: isAccessDenied
          ? "Access denied to S3 bucket. Please check your IAM permissions."
          : error instanceof Error
          ? error.message
          : "An unknown error occurred",
      }),
      {
        status: isAccessDenied ? 403 : 500,
        headers: { "Content-Type": "application/json" },
      }
    );
  }
}

Create the Upload Components

Create a folder called s3 in your components Then inside lets create 2 files

  • FileInputComponent.tsx => This holds the upload components
  • S3FileUploadForm.tsx => This has the upload form

Here is the FileInputComponent.tsx

"use client";
import React, { useState } from "react";

// Shared types
export type MediaFile = {
  id: string;
  file: File;
  preview: string;
  type: "image" | "video" | "document" | "other";
};

export type FileInputProps = {
  multiple?: boolean;
  maxSizeMB?: number;
  onChange?: (files: File | File[]) => void;
  acceptedFileTypes?: string;
  value?: File | File[];
  label?: string;
};

// Helper function to generate file previews
const getFilePreview = (
  file: File
): { preview: string; type: "image" | "video" | "document" | "other" } => {
  if (file.type.startsWith("image/")) {
    return {
      preview: URL.createObjectURL(file),
      type: "image",
    };
  } else if (file.type.startsWith("video/")) {
    return {
      preview: URL.createObjectURL(file),
      type: "video",
    };
  } else if (
    file.type.startsWith("application/pdf") ||
    file.type.includes("document") ||
    file.type.includes("sheet")
  ) {
    return {
      preview: "/document-icon.png", // You'd need to add this static image
      type: "document",
    };
  } else {
    return {
      preview: "/file-icon.png", // You'd need to add this static image
      type: "other",
    };
  }
};

// File Preview Component
const FilePreview: React.FC<{
  file: MediaFile;
  onRemove: (id: string) => void;
  previewSize?: "small" | "large";
}> = ({ file, onRemove, previewSize = "small" }) => {
  const isSmall = previewSize === "small";

  return (
    <div
      className={`preview-item relative rounded-lg overflow-hidden border ${
        isSmall ? "w-16 h-16" : "aspect-square w-full"
      }`}
    >
      {file.type === "image" ? (
        <img
          src={file.preview}
          alt={file.file.name}
          className="w-full h-full object-cover"
        />
      ) : file.type === "video" ? (
        <div className="w-full h-full relative bg-black flex items-center justify-center">
          <video
            src={file.preview}
            className="max-w-full max-h-full"
            controls={!isSmall}
          />
          {isSmall && (
            <div className="absolute inset-0 flex items-center justify-center">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                width="24"
                height="24"
                fill="white"
                viewBox="0 0 16 16"
              >
                <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
                <path d="M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445z" />
              </svg>
            </div>
          )}
        </div>
      ) : (
        <div className="w-full h-full flex items-center justify-center bg-gray-100">
          <img src={file.preview} alt={file.file.name} className="w-8 h-8" />
        </div>
      )}
      {!isSmall && (
        <div className="p-2 bg-white text-xs truncate">{file.file.name}</div>
      )}
      <button
        type="button"
        onClick={(e) => {
          e.stopPropagation();
          onRemove(file.id);
        }}
        className={`absolute top-1 right-1 bg-red-500 text-white rounded-full flex items-center justify-center 
                   ${isSmall ? "w-5 h-5 text-xs" : "w-6 h-6 text-sm"}`}
      >
        ×
      </button>
    </div>
  );
};

// Compact File Input Component
export const CompactFileInput: React.FC<FileInputProps> = ({
  multiple = false,
  maxSizeMB = 5,
  onChange,
  acceptedFileTypes = "image/*,video/*",
  value,
  label,
}) => {
  const [media, setMedia] = useState<MediaFile[]>([]);
  const [error, setError] = useState<string | null>(null);
  const fileInputRef = React.useRef<HTMLInputElement>(null);

  // Initialize from value prop if provided
  React.useEffect(() => {
    if (value && media.length === 0) {
      if (!multiple && !Array.isArray(value)) {
        const file = value;
        const { preview, type } = getFilePreview(file);
        setMedia([
          {
            id: Math.random().toString(36).substr(2, 9),
            file,
            preview,
            type,
          },
        ]);
      } else if (multiple && Array.isArray(value)) {
        const newMedia = value.map((file) => {
          const { preview, type } = getFilePreview(file);
          return {
            id: Math.random().toString(36).substr(2, 9),
            file,
            preview,
            type,
          };
        });
        setMedia(newMedia);
      }
    }
  }, [value, multiple, media.length]);

  const maxSizeBytes = maxSizeMB * 1024 * 1024; // Convert MB to bytes

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const selectedFiles = Array.from(e.target.files || []);

    // Reset error
    setError(null);

    // Validate file size
    const oversizedFiles = selectedFiles.filter(
      (file) => file.size > maxSizeBytes
    );
    if (oversizedFiles.length > 0) {
      setError(`File(s) exceed the ${maxSizeMB}MB limit`);
      return;
    }

    // Handle single file mode
    if (!multiple && selectedFiles.length > 0) {
      // Remove previous media and URL
      media.forEach((item) => URL.revokeObjectURL(item.preview));

      const file = selectedFiles[0];
      const { preview, type } = getFilePreview(file);
      const newMedia = {
        id: Math.random().toString(36).substr(2, 9),
        file,
        preview,
        type,
      };

      setMedia([newMedia]);
      onChange?.(file);
      return;
    }

    // Handle multiple files
    const newMedia = selectedFiles.map((file) => {
      const { preview, type } = getFilePreview(file);
      return {
        id: Math.random().toString(36).substr(2, 9),
        file,
        preview,
        type,
      };
    });

    setMedia((prev) => [...prev, ...newMedia]);
    onChange?.(selectedFiles);
  };

  const removeMedia = (id: string) => {
    setMedia((prev) => {
      const updatedMedia = prev.filter((item) => {
        if (item.id === id) {
          URL.revokeObjectURL(item.preview);
          return false;
        }
        return true;
      });

      // Notify parent component
      if (multiple) {
        onChange?.(updatedMedia.map((item) => item.file));
      } else if (updatedMedia.length > 0) {
        onChange?.(updatedMedia[0].file);
      } else {
        onChange?.(multiple ? [] : (null as any));
      }

      return updatedMedia;
    });
  };

  const handleBrowseClick = () => {
    fileInputRef.current?.click();
  };

  return (
    <div className="compact-file-input">
      <div className="input-container flex items-center gap-2 p-2 border rounded border-gray-300">
        <button
          type="button"
          onClick={handleBrowseClick}
          className="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600"
        >
          Browse
        </button>
        <span className="text-sm text-gray-500">
          {label || (multiple ? "Choose files" : "Choose a file")}
        </span>
        <input
          ref={fileInputRef}
          type="file"
          accept={acceptedFileTypes}
          multiple={multiple}
          onChange={handleFileChange}
          className="hidden"
        />
      </div>

      {error && <div className="text-red-500 text-xs mt-1">{error}</div>}

      {media.length > 0 && (
        <div className="media-previews mt-2 flex flex-wrap gap-2">
          {media.map((item) => (
            <FilePreview
              key={item.id}
              file={item}
              onRemove={removeMedia}
              previewSize="small"
            />
          ))}
        </div>
      )}
    </div>
  );
};

// Dropzone File Input Component
export const DropzoneFileInput: React.FC<FileInputProps> = ({
  multiple = false,
  maxSizeMB = 5,
  onChange,
  acceptedFileTypes = "image/*,video/*",
  value,
  label,
}) => {
  const [media, setMedia] = useState<MediaFile[]>([]);
  const [error, setError] = useState<string | null>(null);
  const [isDragging, setIsDragging] = useState(false);
  const fileInputRef = React.useRef<HTMLInputElement>(null);

  // Initialize from value prop if provided
  React.useEffect(() => {
    if (value && media.length === 0) {
      if (!multiple && !Array.isArray(value)) {
        const file = value;
        const { preview, type } = getFilePreview(file);
        setMedia([
          {
            id: Math.random().toString(36).substr(2, 9),
            file,
            preview,
            type,
          },
        ]);
      } else if (multiple && Array.isArray(value)) {
        const newMedia = value.map((file) => {
          const { preview, type } = getFilePreview(file);
          return {
            id: Math.random().toString(36).substr(2, 9),
            file,
            preview,
            type,
          };
        });
        setMedia(newMedia);
      }
    }
  }, [value, multiple, media.length]);

  const maxSizeBytes = maxSizeMB * 1024 * 1024; // Convert MB to bytes

  const handleFiles = React.useCallback(
    (files: File[]) => {
      // Reset error
      setError(null);

      // Validate file size
      const oversizedFiles = files.filter((file) => file.size > maxSizeBytes);
      if (oversizedFiles.length > 0) {
        setError(`File(s) exceed the ${maxSizeMB}MB limit`);
        return;
      }

      // Filter files based on acceptedFileTypes
      const acceptedTypesArray = acceptedFileTypes.split(",").map((type) => {
        if (type.includes("*")) {
          return type.replace("*", "");
        }
        return type;
      });

      const validFiles = files.filter((file) => {
        // If no specific types are specified or if "*/*" is specified, accept all files
        if (!acceptedFileTypes || acceptedFileTypes === "*/*") return true;

        return acceptedTypesArray.some((acceptedType) =>
          file.type.startsWith(acceptedType.trim())
        );
      });

      if (validFiles.length === 0) {
        setError(`Please select valid file types: ${acceptedFileTypes}`);
        return;
      }

      // Handle single file mode
      if (!multiple && validFiles.length > 0) {
        // Remove previous media and URLs
        media.forEach((item) => URL.revokeObjectURL(item.preview));

        const file = validFiles[0];
        const { preview, type } = getFilePreview(file);
        const newMedia = {
          id: Math.random().toString(36).substr(2, 9),
          file,
          preview,
          type,
        };

        setMedia([newMedia]);
        onChange?.(file);
        return;
      }

      // Handle multiple files
      const newMedia = validFiles.map((file) => {
        const { preview, type } = getFilePreview(file);
        return {
          id: Math.random().toString(36).substr(2, 9),
          file,
          preview,
          type,
        };
      });

      setMedia((prev) => [...prev, ...newMedia]);

      // If multiple is true, we'll return an array of files, otherwise we'll return the first file
      if (multiple) {
        const allFiles = [...media.map((m) => m.file), ...validFiles];
        onChange?.(allFiles);
      } else {
        onChange?.(validFiles[0]);
      }
    },
    [media, maxSizeBytes, multiple, onChange, acceptedFileTypes]
  );

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const selectedFiles = Array.from(e.target.files || []);
    handleFiles(selectedFiles);
  };

  const removeMedia = (id: string) => {
    setMedia((prev) => {
      const updatedMedia = prev.filter((item) => {
        if (item.id === id) {
          URL.revokeObjectURL(item.preview);
          return false;
        }
        return true;
      });

      // Notify parent component
      if (multiple) {
        onChange?.(updatedMedia.map((item) => item.file));
      } else if (updatedMedia.length > 0) {
        onChange?.(updatedMedia[0].file);
      } else {
        onChange?.(multiple ? [] : (null as any));
      }

      return updatedMedia;
    });
  };

  const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault();
    setIsDragging(true);
  };

  const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault();
    setIsDragging(false);
  };

  const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault();
    setIsDragging(false);

    if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
      const droppedFiles = Array.from(e.dataTransfer.files);
      handleFiles(droppedFiles);
    }
  };

  const handleBrowseClick = () => {
    fileInputRef.current?.click();
  };

  // Get file type label
  const getFileTypeLabel = () => {
    if (
      acceptedFileTypes.includes("image/") &&
      acceptedFileTypes.includes("video/")
    ) {
      return "Images & Videos";
    } else if (acceptedFileTypes.includes("image/")) {
      return "Images";
    } else if (acceptedFileTypes.includes("video/")) {
      return "Videos";
    } else {
      return "Files";
    }
  };

  return (
    <div className="dropzone-file-input">
      <div
        className={`dropzone p-8 border-2 border-dashed rounded-lg flex flex-col items-center justify-center cursor-pointer
          ${
            isDragging
              ? "border-blue-500 bg-blue-50"
              : "border-gray-300 hover:border-gray-400"
          }`}
        onDragOver={handleDragOver}
        onDragLeave={handleDragLeave}
        onDrop={handleDrop}
        onClick={handleBrowseClick}
      >
        <div className="icon mb-4 text-blue-500">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            width="48"
            height="48"
            fill="currentColor"
            viewBox="0 0 16 16"
          >
            <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z" />
            <path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z" />
          </svg>
        </div>
        <div className="text-center">
          <p className="text-lg font-medium mb-1">
            {label ||
              `Drag & Drop ${
                multiple
                  ? getFileTypeLabel()
                  : `a ${getFileTypeLabel().slice(0, -1)}`
              } Here`}
          </p>
          <p className="text-sm text-gray-500 mb-2">or</p>
          <button
            type="button"
            className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
          >
            Browse Files
          </button>
          <p className="text-xs text-gray-400 mt-2">
            Max size: {maxSizeMB}MB
            {acceptedFileTypes !== "*/*" &&
              ` • Accepted: ${acceptedFileTypes.replace(/\*/g, "All")}`}
          </p>
        </div>
        <input
          ref={fileInputRef}
          type="file"
          accept={acceptedFileTypes}
          multiple={multiple}
          onChange={handleFileChange}
          className="hidden"
        />
      </div>

      {error && <div className="text-red-500 text-sm mt-2">{error}</div>}

      {media.length > 0 && (
        <div className="media-previews mt-4">
          <h3 className="text-sm font-medium mb-2">
            {media.length} {media.length === 1 ? "File" : "Files"} Selected
          </h3>
          <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
            {media.map((item) => (
              <FilePreview
                key={item.id}
                file={item}
                onRemove={removeMedia}
                previewSize="large"
              />
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

// Export both components
export default {
  Compact: CompactFileInput,
  Dropzone: DropzoneFileInput,
};

the Form Component

"use client";

import React, { useState } from "react";
import { CompactFileInput, DropzoneFileInput } from "./FileInputComponent";

interface UploadedFile {
  fileName: string;
  fileUrl: string;
  fileType: string;
  fileSize: number;
}

interface S3UploadFormProps {
  variant?: "compact" | "dropzone";
  multiple?: boolean;
  maxSizeMB?: number;
  acceptedFileTypes?: string;
  onUploadComplete?: (files: UploadedFile | UploadedFile[]) => void;
  onUploadError?: (error: string) => void;
}

const S3UploadForm: React.FC<S3UploadFormProps> = ({
  variant = "dropzone",
  multiple = false,
  maxSizeMB = 5,
  acceptedFileTypes = "image/*,video/*",
  onUploadComplete,
  onUploadError,
}) => {
  const [files, setFiles] = useState<File | File[]>(
    multiple ? [] : (null as any)
  );
  const [isUploading, setIsUploading] = useState(false);
  const [uploadProgress, setUploadProgress] = useState(0);
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
  const [error, setError] = useState<string | null>(null);

  const handleFileChange = (selectedFiles: File | File[]) => {
    setFiles(selectedFiles);
    // Reset states when files change
    setUploadedFiles([]);
    setError(null);
    setUploadProgress(0);
  };

  const handleUpload = async () => {
    if (!files || (Array.isArray(files) && files.length === 0)) {
      setError("Please select file(s) to upload");
      return;
    }

    setIsUploading(true);
    setUploadProgress(0);
    setError(null);

    try {
      if (multiple && Array.isArray(files)) {
        // Handle multiple files upload
        const uploadedResults: UploadedFile[] = [];

        for (let i = 0; i < files.length; i++) {
          const file = files[i];
          const formData = new FormData();
          formData.append("file", file);

          const response = await fetch("/api/s3-upload", {
            method: "POST",
            body: formData,
          });

          if (!response.ok) {
            const error = await response.json();
            throw new Error(error.message || `Failed to upload ${file.name}`);
          }

          const result = await response.json();
          uploadedResults.push({
            fileName: result.fileName,
            fileUrl: result.fileUrl,
            fileType: file.type,
            fileSize: file.size,
          });

          // Update progress
          setUploadProgress(Math.round(((i + 1) / files.length) * 100));
        }

        setUploadedFiles(uploadedResults);
        onUploadComplete?.(uploadedResults);
      } else {
        // Handle single file upload
        const file = Array.isArray(files) ? files[0] : files;
        const formData = new FormData();
        formData.append("file", file);

        const response = await fetch("/api/s3-upload", {
          method: "POST",
          body: formData,
        });

        if (!response.ok) {
          const error = await response.json();
          throw new Error(error.message || "Failed to upload file");
        }

        const result = await response.json();
        const uploadedFile = {
          fileName: result.fileName,
          fileUrl: result.fileUrl,
          fileType: file.type,
          fileSize: file.size,
        };

        setUploadedFiles([uploadedFile]);
        setUploadProgress(100);
        onUploadComplete?.(uploadedFile);
      }
    } catch (err) {
      console.error("Upload error:", err);
      const errorMessage =
        err instanceof Error ? err.message : "An unknown error occurred";
      setError(errorMessage);
      onUploadError?.(errorMessage);
    } finally {
      setIsUploading(false);
    }
  };

  const renderFileInput = () => {
    const props = {
      multiple,
      maxSizeMB,
      acceptedFileTypes,
      onChange: handleFileChange,
      value: files,
    };

    if (variant === "compact") {
      return <CompactFileInput {...props} />;
    } else {
      return <DropzoneFileInput {...props} />;
    }
  };

  return (
    <div className="s3-upload-form max-w-2xl mx-auto p-4 bg-white rounded-lg shadow-sm">
      <h2 className="text-xl font-semibold mb-4">
        Upload {multiple ? "Files" : "File"} to S3
      </h2>

      {renderFileInput()}

      <div className="mt-4">
        <button
          onClick={handleUpload}
          disabled={
            isUploading ||
            !files ||
            (Array.isArray(files) && files.length === 0)
          }
          className={`w-full py-2 px-4 rounded-md text-white font-medium ${
            isUploading ||
            !files ||
            (Array.isArray(files) && files.length === 0)
              ? "bg-gray-400 cursor-not-allowed"
              : "bg-blue-500 hover:bg-blue-600"
          }`}
        >
          {isUploading ? "Uploading..." : "Upload to S3"}
        </button>
      </div>

      {isUploading && (
        <div className="mt-4">
          <div className="w-full bg-gray-200 rounded-full h-2.5">
            <div
              className="bg-blue-500 h-2.5 rounded-full"
              style={{ width: `${uploadProgress}%` }}
            ></div>
          </div>
          <p className="text-sm text-center mt-1">{uploadProgress}% Complete</p>
        </div>
      )}

      {error && (
        <div className="mt-4 p-3 bg-red-100 text-red-700 rounded-md">
          {error}
        </div>
      )}

      {uploadedFiles.length > 0 && (
        <div className="mt-6 border-t pt-4">
          <h3 className="font-medium text-lg mb-2">
            Uploaded {uploadedFiles.length > 1 ? "Files" : "File"}
          </h3>
          <div className="space-y-3">
            {uploadedFiles.map((file, index) => (
              <div key={index} className="p-3 bg-gray-50 rounded-md">
                <div className="flex justify-between items-start">
                  <div>
                    <p className="font-medium break-all">{file.fileName}</p>
                    <p className="text-sm text-gray-500">
                      {file.fileType} • {formatFileSize(file.fileSize)}
                    </p>
                  </div>
                  <a
                    href={file.fileUrl}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="text-blue-500 hover:text-blue-700"
                  >
                    <svg
                      xmlns="http://www.w3.org/2000/svg"
                      className="h-5 w-5"
                      viewBox="0 0 20 20"
                      fill="currentColor"
                    >
                      <path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
                      <path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
                    </svg>
                  </a>
                </div>
                <div className="mt-2">
                  <input
                    type="text"
                    value={file.fileUrl}
                    readOnly
                    className="w-full p-2 text-sm bg-white border rounded"
                    onClick={(e) => (e.target as HTMLInputElement).select()}
                  />
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

// Helper function to format file size
const formatFileSize = (bytes: number): string => {
  if (bytes === 0) return "0 Bytes";

  const k = 1024;
  const sizes = ["Bytes", "KB", "MB", "GB"];

  const i = Math.floor(Math.log(bytes) / Math.log(k));

  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};

export default S3UploadForm;

Create the Upload Page the uses the form

"use client";

import {
  CompactFileInput,
  DropzoneFileInput,
} from "@/components/s3/FileInputComponent";
import S3UploadForm from "@/components/s3/S3FileUploadForm";
import React, { useState } from "react";

interface UploadedFile {
  fileName: string;
  fileUrl: string;
  fileType: string;
  fileSize: number;
}

export default function UploadDemoPage() {
  const [variant, setVariant] = useState<"compact" | "dropzone">("dropzone");
  const [multiple, setMultiple] = useState(false);
  const [uploadedFiles, setUploadedFiles] = useState<
    UploadedFile | UploadedFile[]
  >([]);
  const [uploadError, setUploadError] = useState<string | null>(null);

  const handleUploadComplete = (files: UploadedFile | UploadedFile[]) => {
    setUploadedFiles(files);
    setUploadError(null);
  };

  const handleUploadError = (error: string) => {
    setUploadError(error);
  };

  return (
    <div className="container mx-auto py-8 px-4">
      <h1 className="text-3xl font-bold mb-8 text-center">
        S3 File Upload Demo
      </h1>

      <div className="mb-8 p-4 bg-gray-50 rounded-lg">
        <h2 className="text-xl font-semibold mb-4">Configuration</h2>
        <div className="flex flex-wrap gap-4">
          <div>
            <label className="block mb-2 text-sm font-medium">
              Component Style
            </label>
            <div className="flex gap-2">
              <button
                onClick={() => setVariant("dropzone")}
                className={`px-4 py-2 rounded ${
                  variant === "dropzone"
                    ? "bg-blue-500 text-white"
                    : "bg-gray-200 text-gray-700"
                }`}
              >
                Dropzone
              </button>
              <button
                onClick={() => setVariant("compact")}
                className={`px-4 py-2 rounded ${
                  variant === "compact"
                    ? "bg-blue-500 text-white"
                    : "bg-gray-200 text-gray-700"
                }`}
              >
                Compact
              </button>
            </div>
          </div>

          <div>
            <label className="block mb-2 text-sm font-medium">
              Upload Mode
            </label>
            <div className="flex gap-2">
              <button
                onClick={() => setMultiple(false)}
                className={`px-4 py-2 rounded ${
                  !multiple
                    ? "bg-blue-500 text-white"
                    : "bg-gray-200 text-gray-700"
                }`}
              >
                Single File
              </button>
              <button
                onClick={() => setMultiple(true)}
                className={`px-4 py-2 rounded ${
                  multiple
                    ? "bg-blue-500 text-white"
                    : "bg-gray-200 text-gray-700"
                }`}
              >
                Multiple Files
              </button>
            </div>
          </div>
        </div>
      </div>

      <S3UploadForm
        variant={variant}
        multiple={multiple}
        maxSizeMB={10}
        acceptedFileTypes="image/*,video/*,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
        onUploadComplete={handleUploadComplete}
        onUploadError={handleUploadError}
      />

      <div className="mt-12 border-t pt-8">
        <h2 className="text-2xl font-semibold mb-6">
          Component Usage Examples
        </h2>

        <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
          <div className="p-6 border rounded-lg">
            <h3 className="text-lg font-medium mb-4">Individual Components</h3>
            <p className="text-sm text-gray-600 mb-4">
              You can use the file input components separately from the S3
              upload logic:
            </p>

            <div className="mb-6">
              <h4 className="font-medium mb-2">Compact File Input</h4>
              <CompactFileInput
                multiple={false}
                acceptedFileTypes="image/*,video/*"
                onChange={(files) => console.log("Files selected:", files)}
              />
            </div>

            <div>
              <h4 className="font-medium mb-2">Dropzone File Input</h4>
              <DropzoneFileInput
                multiple={true}
                maxSizeMB={5}
                acceptedFileTypes="image/*"
                onChange={(files) => console.log("Files selected:", files)}
              />
            </div>
          </div>

          <div className="p-6 border rounded-lg">
            <h3 className="text-lg font-medium mb-4">Code Examples</h3>

            <div className="bg-gray-100 p-4 rounded-md overflow-auto text-sm">
              <pre className="text-xs md:text-sm">
                {`// Single file upload example
<S3UploadForm
  variant="dropzone"
  multiple={false}
  maxSizeMB={5}
  acceptedFileTypes="image/*,video/*"
  onUploadComplete={(file) => {
    // file is a single UploadedFile object
    console.log('File URL:', file.fileUrl);
  }}
/>

// Multiple files upload example
<S3UploadForm
  variant="compact"
  multiple={true}
  maxSizeMB={10}
  acceptedFileTypes="image/*,video/*,application/pdf"
  onUploadComplete={(files) => {
    // files is an array of UploadedFile objects
    files.forEach(file => {
      console.log('File URL:', file.fileUrl);
    });
  }}
/>
`}
              </pre>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}