Add full photo capture and management functionality using standard Web APIs: - Photo capture via getUserMedia (camera) or file upload - Automatic thumbnail generation (max 200px width) - IndexedDB storage for photos with Blob support - PhotoCapture component with camera preview and capture controls - PhotoGallery component with grid view and fullscreen modal - Integration into item detail page - 9 new unit tests (all passing) PWA-friendly implementation: - No native dependencies required - Works in mobile browsers - Falls back to file upload if camera unavailable - Stores photos locally in IndexedDB Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
119 lines
2.7 KiB
TypeScript
119 lines
2.7 KiB
TypeScript
import { getDB } from './db';
|
|
import type { Photo } from '$lib/types';
|
|
import { nanoid } from 'nanoid';
|
|
|
|
/**
|
|
* Get all photos for a specific item
|
|
*/
|
|
export async function getPhotosByItemId(itemId: string): Promise<Photo[]> {
|
|
const db = await getDB();
|
|
return db.getAllFromIndex('photos', 'by-item', itemId);
|
|
}
|
|
|
|
/**
|
|
* Get a specific photo by ID
|
|
*/
|
|
export async function getPhotoById(id: string): Promise<Photo | undefined> {
|
|
const db = await getDB();
|
|
return db.get('photos', id);
|
|
}
|
|
|
|
/**
|
|
* Create a new photo
|
|
* Automatically generates thumbnail (max 200px width)
|
|
*/
|
|
export async function createPhoto(itemId: string, blob: Blob): Promise<Photo> {
|
|
const thumbnail = await generateThumbnail(blob, 200);
|
|
|
|
const photo: Photo = {
|
|
id: `photo_${nanoid(10)}`,
|
|
itemId,
|
|
blob,
|
|
thumbnail,
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
|
|
const db = await getDB();
|
|
await db.put('photos', photo);
|
|
return photo;
|
|
}
|
|
|
|
/**
|
|
* Delete a photo
|
|
*/
|
|
export async function deletePhoto(id: string): Promise<void> {
|
|
const db = await getDB();
|
|
await db.delete('photos', id);
|
|
}
|
|
|
|
/**
|
|
* Delete all photos for an item
|
|
*/
|
|
export async function deletePhotosByItemId(itemId: string): Promise<void> {
|
|
const photos = await getPhotosByItemId(itemId);
|
|
const db = await getDB();
|
|
await Promise.all(photos.map((photo) => db.delete('photos', photo.id)));
|
|
}
|
|
|
|
/**
|
|
* Generate a thumbnail from a blob
|
|
*/
|
|
async function generateThumbnail(blob: Blob, maxWidth: number): Promise<Blob> {
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
img.onload = () => {
|
|
// Calculate dimensions
|
|
const scale = maxWidth / img.width;
|
|
const width = maxWidth;
|
|
const height = img.height * scale;
|
|
|
|
// Create canvas
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
|
|
// Draw scaled image
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) {
|
|
reject(new Error('Could not get canvas context'));
|
|
return;
|
|
}
|
|
ctx.drawImage(img, 0, 0, width, height);
|
|
|
|
// Convert to blob
|
|
canvas.toBlob(
|
|
(thumbnailBlob) => {
|
|
URL.revokeObjectURL(url);
|
|
if (thumbnailBlob) {
|
|
resolve(thumbnailBlob);
|
|
} else {
|
|
reject(new Error('Could not create thumbnail'));
|
|
}
|
|
},
|
|
'image/jpeg',
|
|
0.8
|
|
);
|
|
};
|
|
|
|
img.onerror = () => {
|
|
URL.revokeObjectURL(url);
|
|
reject(new Error('Could not load image'));
|
|
};
|
|
|
|
img.src = url;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Convert a blob to a data URL for display
|
|
*/
|
|
export function blobToDataURL(blob: Blob): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(reader.result as string);
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
}
|