feat: add progress bar to collage processing

Uses XMLHttpRequest upload progress to track image upload (0-80%),
then shows "Processing collage..." for the server-side compositing
phase (80-100%). Progress bar with percentage shown in the preview area.
This commit is contained in:
ashim-hq
2026-04-19 17:12:12 +08:00
parent 889ce1dd26
commit 8495a0de2d
3 changed files with 72 additions and 17 deletions
@@ -61,12 +61,7 @@ export function CollagePreview() {
}
if (phase === "processing") {
return (
<div className="flex flex-col items-center justify-center h-full gap-3">
<Loader2 className="h-8 w-8 text-primary animate-spin" />
<p className="text-sm text-muted-foreground">Creating your collage...</p>
</div>
);
return <ProcessingView />;
}
if (phase === "result" && resultUrl) {
@@ -150,6 +145,29 @@ function UploadArea() {
);
}
function ProcessingView() {
const progress = useCollageStore((s) => s.progress);
const label = progress < 80 ? "Uploading images..." : "Processing collage...";
return (
<div className="flex flex-col items-center justify-center h-full gap-4 px-8">
<Loader2 className="h-8 w-8 text-primary animate-spin" />
<div className="w-full max-w-xs space-y-2">
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>{label}</span>
<span className="font-mono">{progress}%</span>
</div>
<div className="h-2 w-full rounded-full bg-muted overflow-hidden">
<div
className="h-full rounded-full bg-primary transition-all duration-300 ease-out"
style={{ width: `${progress}%` }}
/>
</div>
</div>
</div>
);
}
/** The live CSS Grid collage canvas. */
function CollageCanvas({ template }: { template: CollageTemplate }) {
const store = useCollageStore();
@@ -63,11 +63,11 @@ export function CollageSettings() {
if (!hasImages || !template) return;
store.setPhase("processing");
store.setProgress(0);
store.setError(null);
try {
const formData = new FormData();
// Send images in cell-assignment order
for (let i = 0; i < template.cells.length; i++) {
const imgIdx = cellAssignments[i] ?? -1;
if (imgIdx >= 0 && images[imgIdx]) {
@@ -94,18 +94,50 @@ export function CollageSettings() {
}),
);
const res = await fetch("/api/v1/tools/collage", {
method: "POST",
headers: formatHeaders(),
body: formData,
const result = await new Promise<{
downloadUrl: string;
processedSize: number;
originalSize: number;
jobId: string;
}>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/v1/tools/collage");
const headers = formatHeaders();
headers.forEach((value, key) => {
xhr.setRequestHeader(key, value);
});
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
store.setProgress(Math.round((e.loaded / e.total) * 80));
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
store.setProgress(100);
try {
resolve(JSON.parse(xhr.responseText));
} catch {
reject(new Error("Invalid response"));
}
} else {
try {
const body = JSON.parse(xhr.responseText);
reject(new Error(body.error || `Failed: ${xhr.status}`));
} catch {
reject(new Error(`Failed: ${xhr.status}`));
}
}
};
xhr.onerror = () => reject(new Error("Network error"));
xhr.send(formData);
store.setProgress(5);
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || `Failed: ${res.status}`);
}
const result = await res.json();
store.setResult(result.downloadUrl, result.processedSize, result.originalSize, result.jobId);
} catch (err) {
store.setError(err instanceof Error ? err.message : "Collage failed");
+5
View File
@@ -50,6 +50,7 @@ interface CollageState {
// UI state
selectedCell: number | null;
phase: Phase;
progress: number;
resultUrl: string | null;
resultSize: number | null;
originalSize: number | null;
@@ -74,6 +75,7 @@ interface CollageState {
setQuality: (v: number) => void;
setSelectedCell: (v: number | null) => void;
setPhase: (v: Phase) => void;
setProgress: (v: number) => void;
setResult: (url: string, size: number, originalSize: number, jobId: string) => void;
setError: (e: string | null) => void;
reset: () => void;
@@ -105,6 +107,7 @@ export const useCollageStore = create<CollageState>((set, get) => ({
quality: 90,
selectedCell: null,
phase: "upload",
progress: 0,
resultUrl: null,
resultSize: null,
originalSize: null,
@@ -263,6 +266,7 @@ export const useCollageStore = create<CollageState>((set, get) => ({
setQuality: (v) => set({ quality: v, resultUrl: null }),
setSelectedCell: (v) => set({ selectedCell: v }),
setPhase: (v) => set({ phase: v }),
setProgress: (v) => set({ progress: v }),
setResult: (url, size, originalSize, jobId) =>
set({ resultUrl: url, resultSize: size, originalSize, jobId, phase: "result", error: null }),
setError: (e) => set({ error: e, phase: "editing" }),
@@ -287,6 +291,7 @@ export const useCollageStore = create<CollageState>((set, get) => ({
quality: 90,
selectedCell: null,
phase: "upload",
progress: 0,
resultUrl: null,
resultSize: null,
originalSize: null,