image optimization. Image resizing client side, index added to db, pagination
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
import api from "./api";
|
||||
import {
|
||||
resizeImage,
|
||||
getVariantKey,
|
||||
getSizeSuffix,
|
||||
} from "../utils/imageResizer";
|
||||
|
||||
/**
|
||||
* Get the public URL for an image (S3 only)
|
||||
@@ -151,39 +156,6 @@ export async function uploadFile(
|
||||
return { key: presigned.key, publicUrl: presigned.publicUrl };
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload multiple files to S3 (complete flow)
|
||||
*/
|
||||
export async function uploadFiles(
|
||||
uploadType: UploadType,
|
||||
files: File[],
|
||||
options: UploadOptions = {}
|
||||
): Promise<{ key: string; publicUrl: string }[]> {
|
||||
if (files.length === 0) return [];
|
||||
|
||||
// Get presigned URLs for all files
|
||||
const presignedUrls = await getPresignedUrls(uploadType, files);
|
||||
|
||||
// Upload all files in parallel
|
||||
await Promise.all(
|
||||
files.map((file, i) =>
|
||||
uploadToS3(file, presignedUrls[i].uploadUrl, options)
|
||||
)
|
||||
);
|
||||
|
||||
// Confirm all uploads
|
||||
const keys = presignedUrls.map((p) => p.key);
|
||||
const { confirmed, total } = await confirmUploads(keys);
|
||||
|
||||
if (confirmed.length < total) {
|
||||
console.warn(`${total - confirmed.length} uploads failed verification`);
|
||||
}
|
||||
|
||||
return presignedUrls
|
||||
.filter((p) => confirmed.includes(p.key))
|
||||
.map((p) => ({ key: p.key, publicUrl: p.publicUrl }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a signed URL for accessing private content (messages, condition-checks)
|
||||
*/
|
||||
@@ -193,3 +165,119 @@ export async function getSignedUrl(key: string): Promise<string> {
|
||||
);
|
||||
return response.data.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a signed URL for a specific image size variant (private content)
|
||||
* Backend will validate ownership using the base key
|
||||
*/
|
||||
export async function getSignedImageUrl(
|
||||
baseKey: string,
|
||||
size: "thumbnail" | "medium" | "original" = "original"
|
||||
): Promise<string> {
|
||||
const suffix = getSizeSuffix(size);
|
||||
const variantKey = getVariantKey(baseKey, suffix);
|
||||
return getSignedUrl(variantKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL for a specific image size variant
|
||||
* Falls back to original if variant doesn't exist (backward compatibility)
|
||||
*/
|
||||
export function getImageUrl(
|
||||
baseKey: string | null | undefined,
|
||||
size: "thumbnail" | "medium" | "original" = "original"
|
||||
): string {
|
||||
if (!baseKey) return "";
|
||||
|
||||
const suffix = getSizeSuffix(size);
|
||||
const variantKey = getVariantKey(baseKey, suffix);
|
||||
|
||||
return getPublicImageUrl(variantKey);
|
||||
}
|
||||
|
||||
export interface UploadWithResizeOptions extends UploadOptions {
|
||||
skipResize?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a single image with all size variants (thumbnail, medium, original)
|
||||
* Returns the base key (original, without suffix) for database storage
|
||||
*/
|
||||
export async function uploadImageWithVariants(
|
||||
uploadType: UploadType,
|
||||
file: File,
|
||||
options: UploadWithResizeOptions = {}
|
||||
): Promise<{ baseKey: string; publicUrl: string; variants: string[] }> {
|
||||
const { onProgress, skipResize } = options;
|
||||
|
||||
// If skipping resize, use regular upload
|
||||
if (skipResize) {
|
||||
const result = await uploadFile(uploadType, file, { onProgress });
|
||||
return { baseKey: result.key, publicUrl: result.publicUrl, variants: [result.key] };
|
||||
}
|
||||
|
||||
// Generate resized variants
|
||||
const resizedImages = await resizeImage(file);
|
||||
|
||||
if (resizedImages.length === 0) {
|
||||
throw new Error("Failed to resize image");
|
||||
}
|
||||
|
||||
// Get presigned URLs for all variants
|
||||
const files = resizedImages.map((r) => r.file);
|
||||
const presignedUrls = await getPresignedUrls(uploadType, files);
|
||||
|
||||
// Upload all variants in parallel with combined progress
|
||||
const totalBytes = files.reduce((sum, f) => sum + f.size, 0);
|
||||
let uploadedBytes = 0;
|
||||
|
||||
await Promise.all(
|
||||
files.map((variantFile, i) =>
|
||||
uploadToS3(variantFile, presignedUrls[i].uploadUrl, {
|
||||
onProgress: (percent) => {
|
||||
if (onProgress) {
|
||||
const fileContribution = (variantFile.size / totalBytes) * percent;
|
||||
// Approximate combined progress
|
||||
onProgress(Math.min(99, Math.round(uploadedBytes / totalBytes * 100 + fileContribution)));
|
||||
}
|
||||
},
|
||||
}).then(() => {
|
||||
uploadedBytes += files[i].size;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Confirm all uploads
|
||||
const keys = presignedUrls.map((p) => p.key);
|
||||
await confirmUploads(keys);
|
||||
|
||||
// Find the original variant key (no suffix) for database storage
|
||||
const originalIdx = resizedImages.findIndex((r) => r.variant.size === "original");
|
||||
const baseKey = presignedUrls[originalIdx]?.key || presignedUrls[0].key;
|
||||
|
||||
if (onProgress) onProgress(100);
|
||||
|
||||
return {
|
||||
baseKey,
|
||||
publicUrl: getPublicImageUrl(baseKey),
|
||||
variants: keys,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload multiple images with all size variants
|
||||
* Returns array of base keys for database storage
|
||||
*/
|
||||
export async function uploadImagesWithVariants(
|
||||
uploadType: UploadType,
|
||||
files: File[],
|
||||
options: UploadWithResizeOptions = {}
|
||||
): Promise<{ baseKey: string; publicUrl: string }[]> {
|
||||
if (files.length === 0) return [];
|
||||
|
||||
const results = await Promise.all(
|
||||
files.map((file) => uploadImageWithVariants(uploadType, file, options))
|
||||
);
|
||||
|
||||
return results.map((r) => ({ baseKey: r.baseKey, publicUrl: r.publicUrl }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user