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:
@@ -36,6 +36,8 @@ import {
|
||||
photoFile,
|
||||
photoPath,
|
||||
photoPreset,
|
||||
rateLook,
|
||||
ratingsFor,
|
||||
sessionUser,
|
||||
setPhotoSlots,
|
||||
setPhotoConsent,
|
||||
@@ -346,6 +348,28 @@ app.post('/api/events', async (req, reply) => {
|
||||
return reply.status(204).send();
|
||||
});
|
||||
|
||||
// The film strip's ratings. Public and unauthenticated for the same reason the
|
||||
// counter is: any visitor may score a frame once, and the vote is held against
|
||||
// the salted-address hash rather than an account. The subject is a photo id or
|
||||
// a built-in look's tag, so both kinds of frame are rated through one route.
|
||||
const allowRate = limiter(120, 60_000);
|
||||
const RATING_KEY = /^[A-Za-z0-9:_-]{1,64}$/;
|
||||
|
||||
app.get('/api/ratings', async (req) => ({ ratings: ratingsFor(visitorOf(clientIp(req) || 'unknown')) }));
|
||||
|
||||
app.post('/api/ratings', async (req, reply) => {
|
||||
const b = bodyOf(req);
|
||||
const ip = clientIp(req);
|
||||
const key = typeof b?.key === 'string' ? b.key.trim() : '';
|
||||
const stars = Math.round(Number(b?.stars));
|
||||
if (!RATING_KEY.test(key) || !Number.isFinite(stars) || stars < 1 || stars > 5)
|
||||
return reply.status(400).send({ error: 'invalid rating' });
|
||||
if (!allowRate(ip || 'unknown')) return tooMany(reply);
|
||||
const visitor = visitorOf(ip || 'unknown');
|
||||
rateLook(key, visitor, stars);
|
||||
return reply.status(200).send({ key, rating: ratingsFor(visitor)[key] });
|
||||
});
|
||||
|
||||
app.post('/api/auth/signup', async (req, reply) => {
|
||||
const b = bodyOf(req);
|
||||
if (!b) return reply.status(400).send({ error: 'invalid body' });
|
||||
|
||||
Reference in New Issue
Block a user