0612ce66a6
Bug #1: the first/Primary (calibration) photo rendered broken because its in-memory capture Blob was assigned to <img src> directly (String(Blob)= "object Blob"). resolvePhotoUrl() now mints a blob: object URL (revoked on unload) and falls back to the stored photo endpoint when there is no blob. Bug #2: strip thumbnails used a flat dark bg; now carry the gray checkerboard from the Capture lineup (MeasurePanel) so transparent inventory PNGs read the same on both screens. Also harden missingMinimums to filter kept internally (pre-existing test 7 failed on the clean base because it passed un-kept items).
83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
// #90 Review photo strip (REVIEW-5 / PUBLISH-45). Pure logic split out so the
|
|
// keep/remove + reorder + minimum enforcement is unit-testable without a browser.
|
|
//
|
|
// Model (approved in the wireframe): the owner taps a photo to keep/remove it,
|
|
// drags to reorder, and the LEFTMOST kept photo is the primary (Woo cover). The
|
|
// submission-ready minimum is at least one clean (calibration) photo AND one
|
|
// dimension-overlay copy (REVIEW-5 "clean first pic plus its dimension-overlay
|
|
// copy"). Batch photos are optional.
|
|
|
|
export type PhotoRole = 'calibration' | 'overlay' | 'batch';
|
|
|
|
export interface LineupItem {
|
|
id: string;
|
|
role: PhotoRole;
|
|
isCalibration: boolean;
|
|
/** True = the owner keeps this photo in the submission. */
|
|
kept: boolean;
|
|
}
|
|
|
|
/**
|
|
* The kept photos in their current (left→right) order. The first kept photo is
|
|
* the primary (Woo cover). Reordering is a plain array move; this helper just
|
|
* exposes the kept subset so the caller can re-seq on persist.
|
|
*/
|
|
export function keptInOrder(items: LineupItem[]): LineupItem[] {
|
|
return items.filter((it) => it.kept);
|
|
}
|
|
|
|
/** Move an item from one index to another (drag-reorder). Returns a new array. */
|
|
export function moveItem<T>(arr: T[], from: number, to: number): T[] {
|
|
if (from === to || from < 0 || to < 0 || from >= arr.length || to >= arr.length) {
|
|
return arr;
|
|
}
|
|
const next = arr.slice();
|
|
const [item] = next.splice(from, 1);
|
|
next.splice(to, 0, item);
|
|
return next;
|
|
}
|
|
|
|
/**
|
|
* Whether the kept lineup meets the submission minimum (REVIEW-5): at least one
|
|
* clean calibration photo and one dimension-overlay copy. Returns the missing
|
|
* role(s) so the UI can name them in a nudge.
|
|
*/
|
|
export function missingMinimums(items: LineupItem[]): PhotoRole[] {
|
|
const kept = items.filter((it) => it.kept);
|
|
const missing: PhotoRole[] = [];
|
|
if (!kept.some((it) => it.role === 'calibration')) missing.push('calibration');
|
|
if (!kept.some((it) => it.role === 'overlay')) missing.push('overlay');
|
|
return missing;
|
|
}
|
|
|
|
/** True when the kept lineup is publishable from a photo standpoint. */
|
|
export function lineupValid(kept: LineupItem[]): boolean {
|
|
return missingMinimums(kept).length === 0;
|
|
}
|
|
|
|
/** A short owner-facing label for a role (used in the minimum nudge). */
|
|
export function roleLabel(role: PhotoRole): string {
|
|
if (role === 'calibration') return 'a clean photo';
|
|
if (role === 'overlay') return 'its dimension-overlay copy';
|
|
return 'a batch photo';
|
|
}
|
|
|
|
/**
|
|
* Resolve the <img> src for a strip photo. The in-memory capture blob (fresh
|
|
* photo) must become a `blob:` object URL — assigning a raw Blob (or
|
|
* `String(blob)` = "object Blob") to `src` renders a broken image. That was
|
|
* bug #1: the first/Primary (calibration) photo is the one carrying a blob.
|
|
* When there is no usable blob, fall back to the stored photo endpoint.
|
|
*
|
|
* Pure + injectable: `createObjectUrl` defaults to `URL.createObjectURL` so it
|
|
* is unit-testable without a real DOM/Blob.
|
|
*/
|
|
export function resolvePhotoUrl(
|
|
blob: Blob | null,
|
|
endpoint: string,
|
|
createObjectUrl: (b: Blob) => string = (b) => URL.createObjectURL(b),
|
|
): string {
|
|
if (blob && blob.size > 0) return createObjectUrl(blob);
|
|
return endpoint;
|
|
}
|