Resep Integrasi
Integrasi React Dropzone
Buat area unggah tarik-dan-lepas (drag-and-drop) yang intuitif dengan indikator progres animasi menggunakan react-dropzone dan `@cloudizero/sdk`.
Instalasi
bash
pnpm add react-dropzone @cloudizero/sdk1. Endpoint Backend (Presign & Complete)
API route sisi server untuk menghasilkan tiket presigned dan memvalidasi penyelesaian tanpa mengekspos API Key rahasia ke browser:
app/api/upload/route.ts
// app/api/upload/route.ts (Next.js App Router API Route)
import { NextResponse } from "next/server";
import { Cloudizero } from "@cloudizero/sdk";
const cz = new Cloudizero({
apiKey: process.env.CLOUDIZERO_API_KEY!,
});
// POST /api/upload - Generates presigned upload URL or finalizes metadata
export async function POST(req: Request) {
const body = await req.json();
if (body.action === "presign") {
const { fileName, fileSize, mimeType, projectId } = body;
const ticket = await cz.storage.createPresignedUpload({
projectId,
fileName,
fileSize,
mimeType,
});
return NextResponse.json(ticket);
}
if (body.action === "complete") {
const { fileId, projectId, fileName, mimeType, fileSize, storageKey } = body;
const recorded = await cz.storage.completeUpload({
fileId,
projectId,
fileName,
mimeType,
fileSize,
storageKey,
});
return NextResponse.json(recorded);
}
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
}2. Komponen Dropzone Klien
Komponen klien React Dropzone yang mengunggah biner langsung ke Cloudizero Storage Engine dengan bilah progres waktu-nyata:
components/dropzone-uploader.tsx
"use client";
import { useState } from "react";
import { useDropzone } from "react-dropzone";
import type { PresignedUploadResult, UploadedFile } from "@cloudizero/sdk";
export function DropzoneUploader({ projectId }: { projectId: string }) {
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const [uploadedUrl, setUploadedUrl] = useState<string | null>(null);
const onDrop = async (acceptedFiles: File[]) => {
const file = acceptedFiles[0];
if (!file) return;
setUploading(true);
setProgress(0);
setUploadedUrl(null);
try {
// 1. Dapatkan tiket upload presigned dari backend Anda
const presignRes = await fetch("/api/upload", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "presign",
fileName: file.name,
fileSize: file.size,
mimeType: file.type || "application/octet-stream",
projectId,
}),
});
const ticket: PresignedUploadResult = await presignRes.json();
// 2. Unggah biner langsung dari browser ke Cloudizero Storage Engine
await new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("PUT", ticket.uploadUrl);
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
xhr.upload.onprogress = (evt) => {
if (evt.lengthComputable) {
setProgress(Math.round((evt.loaded / evt.total) * 100));
}
};
xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new Error("PUT upload failed")));
xhr.onerror = () => reject(new Error("Network error during direct upload"));
xhr.send(file);
});
// 3. Rekam metadata dan finalisasi DAM asset di Cloudizero
const completeRes = await fetch("/api/upload", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "complete",
fileId: ticket.fileId,
projectId,
fileName: ticket.finalFileName || file.name,
mimeType: file.type || "application/octet-stream",
fileSize: file.size,
storageKey: ticket.storageKey,
}),
});
const recorded: UploadedFile = await completeRes.json();
setUploadedUrl(recorded.url || null);
} catch (err) {
console.error("Dropzone upload failed:", err);
} finally {
setUploading(false);
}
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: false,
});
return (
<div className="space-y-4">
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-2xl p-10 text-center cursor-pointer transition-colors ${
isDragActive ? "border-blue-500 bg-blue-500/10" : "border-border hover:border-blue-500/40"
}`}
>
<input {...getInputProps()} />
{uploading ? (
<div className="space-y-2">
<p className="text-sm font-medium">Uploading: {progress}%</p>
<div className="w-48 mx-auto bg-muted h-2 rounded-full overflow-hidden">
<div className="bg-blue-600 h-full transition-all" style={{ width: `${progress}%` }} />
</div>
</div>
) : isDragActive ? (
<p className="text-sm font-medium text-blue-600">Drop files here to upload...</p>
) : (
<p className="text-sm text-muted-foreground">
Drag & drop media here, or <span className="text-blue-600 font-medium">browse files</span>
</p>
)}
</div>
{uploadedUrl && (
<div className="p-4 bg-muted/40 rounded-xl text-sm flex items-center justify-between">
<span className="text-muted-foreground truncate max-w-xs">{uploadedUrl}</span>
<a href={uploadedUrl} target="_blank" rel="noreferrer" className="text-blue-600 font-medium shrink-0">
View on CDN →
</a>
</div>
)}
</div>
);
}