Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | 37x 68x 68x 222x 222x 222x 222x 222x 225x 225x 225x 225x 225x 10x 1x 9x 222x | import type { Photo } from "@/types";
export function getBlobAccess(url: string): "private" | "public" {
return url.includes(".private.blob.vercel-storage.com/") ? "private" : "public";
}
/** Optional rendering hints for a photo asset URL. `w` selects a server-resized
* variant; `v` is an opaque cache-bust (the server ignores it for content) so a
* changed photo (e.g. rotation bumps updatedAt) yields a new URL within A's
* max-age window. */
export interface PhotoUrlOptions {
w?: number;
v?: number;
}
/** Allowed clockwise rotation values (degrees) for the rotate PATCH endpoints. */
export const ALLOWED_ROTATIONS = [0, 90, 180, 270] as const;
/** Per-surface request widths (one per surface — `unoptimized` precludes srcset).
* Must be members of RESIZE_WIDTHS in photo-serving.ts. */
export const PHOTO_WIDTH = {
/** Cards/grids: ItemCard, CollectionCard, CompareChoice, MostLovedHero, triage. */
card: 640,
/** Dense pool grid (up to 6 cols). */
poolTile: 320,
/** Item-detail hero. */
hero: 1280,
/** Item-detail / photo-manager gallery thumbnails. */
galleryTile: 640,
} as const;
function withQuery(base: string, opts?: PhotoUrlOptions): string {
const params = new URLSearchParams();
if (opts?.w) params.set("w", String(opts.w));
if (opts?.v) params.set("v", String(opts.v));
const qs = params.toString();
return qs ? `${base}?${qs}` : base;
}
function withItemPhotoQuery(
photoId: string,
collectionId: string,
itemId: string,
opts?: PhotoUrlOptions,
): string {
const params = new URLSearchParams({ collectionId, itemId });
if (opts?.w) params.set("w", String(opts.w));
if (opts?.v) params.set("v", String(opts.v));
return `/api/photo-files/${encodeURIComponent(photoId)}?${params.toString()}`;
}
export function getPhotoAssetUrl(
collectionId: string,
itemId: string,
photoId: string,
opts?: PhotoUrlOptions
): string {
return withItemPhotoQuery(photoId, collectionId, itemId, opts);
}
export function getPhotoAssetUrlForPhoto(
photo: Pick<Photo, "id" | "itemId" | "updatedAt">,
collectionId: string,
opts?: PhotoUrlOptions
): string {
if (!photo.itemId) {
// Pool photos (no item yet) have no item-scoped asset URL. Pool/catalog-number
// URLs arrive with the upload-to-pool work (purses-b1k, plans 2–3).
throw new Error("Photo is not assigned to an item");
}
// Auto-bust the cache on any photo change (rotation bumps updatedAt) so every
// item-photo surface refreshes within A's max-age window after a rotate.
return getPhotoAssetUrl(collectionId, photo.itemId, photo.id, {
...opts,
v: opts?.v ?? photo.updatedAt?.getTime(),
});
}
export function getPoolPhotoAssetUrl(
collectionId: string,
photoId: string,
opts?: PhotoUrlOptions
): string {
return withQuery(`/api/collections/${collectionId}/pool/photos/${photoId}/file`, opts);
}
/** The token-scoped, public photo byte route for an anonymous shared-results
* viewer — authorized by the share token, not a session (purses-21x5.1). */
export function getSharedResultPhotoAssetUrl(
token: string,
photo: Pick<Photo, "id" | "updatedAt">,
opts?: PhotoUrlOptions
): string {
return withQuery(
`/api/shared-results/${encodeURIComponent(token)}/photos/${encodeURIComponent(photo.id)}`,
{ ...opts, v: opts?.v ?? photo.updatedAt?.getTime() },
);
}
|