web: star ratings on the reel, and PICTURES becomes an album browser
The landing strip now carries a score: each look and each contributed frame shows an average, five stars the visitor can press, and how many votes it has. Votes are keyed photo:<id> or look:<TAG> and one visitor has one vote per key, so pressing a second star moves a score instead of stacking one. The API is public and rate-limited; look: scores survive a cleared pool, photo: scores are pruned with their photo. PICTURES was four destination rows; it is now an album per uploader with a search box, a recipe/rating/newest sort, a minimum-star filter, a big preview and a filmstrip of thumbnails. Deleting and slot picking still live in the big box.
This commit is contained in:
@@ -75,6 +75,18 @@ export interface AdminPhoto extends Photo {
|
||||
email: string;
|
||||
mime: string;
|
||||
bytes: number;
|
||||
// The name of the look the photo was saved with, or null — the admin's A→Z
|
||||
// ordering key for the album strip.
|
||||
recipeName: string | null;
|
||||
}
|
||||
|
||||
// One film-strip frame's score. `avg` is the mean of every vote (0 when nobody
|
||||
// has voted), `n` how many there were, and `mine` this visitor's own vote (0
|
||||
// when they have not rated it) — what the landing's star row is drawn from.
|
||||
export interface Rating {
|
||||
avg: number;
|
||||
n: number;
|
||||
mine: number;
|
||||
}
|
||||
|
||||
// One account as /api/admin/users reports it. `avatar` is the ready-made URL
|
||||
@@ -198,6 +210,11 @@ export const api = {
|
||||
// labels ride the query string, since the body is the image itself.
|
||||
listPhotos: () => call<{ photos: Photo[] }>('/photos'),
|
||||
listMyPhotos: () => call<{ photos: Photo[] }>('/photos/mine'),
|
||||
// Film-strip ratings, keyed by subject (`photo:<id>` or `look:<TAG>`). Public:
|
||||
// any visitor may read the tallies and cast one vote per frame.
|
||||
ratings: () => call<{ ratings: Record<string, Rating> }>('/ratings'),
|
||||
rate: (key: string, stars: number) =>
|
||||
call<{ key: string; rating: Rating }>('/ratings', { method: 'POST', body: JSON.stringify({ key, stars }) }),
|
||||
uploadPhoto: async (file: File, labels?: PhotoLabels, opts?: PhotoMetaOpts) => {
|
||||
const res = await uploadBytes(`/api/photos${photoQuery(labels, opts)}`, file, MAX_PHOTO_DIM, MAX_PHOTO_UPLOAD);
|
||||
const body = await readJson(res);
|
||||
|
||||
Reference in New Issue
Block a user