ca-grow-ops-manager/frontend/src/lib/uploadApi.ts
fullsizemalt 558e0ac1e2
Some checks failed
Deploy to Production / deploy (push) Failing after 0s
Test / backend-test (push) Failing after 0s
Test / frontend-test (push) Failing after 0s
feat: Full Spec Kit compliance implementation
Photo Management (per specs/photo-management.md):
- Sharp integration for 3-size compression (thumb/medium/full)
- WebP output with 80-90% quality
- Client-side compression with browser-image-compression
- PhotoUpload component with camera/drag-drop support
- Upload API with bulk support and stats endpoint

Testing:
- Backend: Jest tests for all major API endpoints
- Frontend: Vitest tests for utilities and API clients
- CI: Updated Forgejo workflow for test execution

Specs (100% coverage):
- visitor-management.md (Phase 8)
- messaging.md (Phase 9)
- audit-and-documents.md (Phase 10)
- accessibility-i18n.md (Phase 11)
- hardware-integration.md (Phase 12)
- advanced-features.md (Phase 13)

Documentation:
- OpenAPI 3.0 spec (docs/openapi.yaml)
- All endpoints documented with schemas
2025-12-11 09:53:32 -08:00

132 lines
3.8 KiB
TypeScript

/**
* Photo Upload API Client
*/
import api from './api';
import { compressPhoto, compressPhotos, CompressedPhoto } from './photoCompression';
export interface UploadedPhoto {
success: boolean;
photoId: string;
urls: {
thumb: string;
medium: string;
full: string;
};
metadata: {
originalSize: number;
originalFormat: string;
originalDimensions: { width: number; height: number };
compressedSizes: Record<string, number>;
savingsPercent: number;
format: string;
};
uploadedAt: string;
}
export interface BulkUploadResult {
success: boolean;
uploaded: number;
failed: number;
results: Array<{
photoId: string;
filename: string;
urls: Record<string, string>;
}>;
errors?: Array<{
filename: string;
error: string;
}>;
}
/**
* Upload a single photo with compression
*/
export async function uploadPhoto(
file: File,
onProgress?: (progress: number) => void
): Promise<UploadedPhoto> {
// Compress client-side first
const compressed = await compressPhoto(file);
const formData = new FormData();
formData.append('file', compressed.file, file.name);
const response = await api.post<UploadedPhoto>('/api/upload/photo', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
if (progressEvent.total && onProgress) {
const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
onProgress(progress);
}
}
});
return response.data;
}
/**
* Upload multiple photos with compression
*/
export async function uploadPhotos(
files: File[],
onProgress?: (completed: number, total: number) => void
): Promise<BulkUploadResult> {
// Compress all files first
const compressed = await compressPhotos(files, {}, (completed, total) => {
onProgress?.(completed, total * 2); // First half is compression
});
const formData = new FormData();
compressed.forEach((photo, index) => {
formData.append('files', photo.file, files[index].name);
});
const response = await api.post<BulkUploadResult>('/api/upload/photos', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
if (progressEvent.total && onProgress) {
const uploadProgress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
// Second half is upload (50-100)
onProgress?.(files.length + Math.round(files.length * uploadProgress / 100), files.length * 2);
}
}
});
return response.data;
}
/**
* Get photo URL (with auth token if needed)
*/
export function getPhotoUrl(path: string, size: 'thumb' | 'medium' | 'full' = 'medium'): string {
// If it's already a full URL, return as-is
if (path.startsWith('http')) return path;
// If path includes size, return with api prefix
if (path.includes('/thumb.webp') || path.includes('/medium.webp') || path.includes('/full.webp')) {
return `/api/upload/photo${path.replace('/photos', '')}`;
}
// Otherwise, assume it's a photoId and construct the URL
return `/api/upload/photo/${path}/${size}.webp`;
}
/**
* Delete a photo
*/
export async function deletePhoto(photoId: string): Promise<void> {
await api.delete(`/api/upload/photo/${photoId}`);
}
/**
* Get upload statistics
*/
export async function getUploadStats(): Promise<{
storagePath: string;
diskUsage: string;
sizes: Record<string, { width: number; height: number; quality: number }>;
}> {
const response = await api.get('/api/upload/stats');
return response.data;
}