Back to blog
Here is the
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 componentsS3FileUploadForm.tsx
=> This has the upload form
FileInputComponent.tsx
Here is the "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>
);
}