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:
2026-09-18 19:17:38 +07:00
parent 4057566a14
commit 07fbadcdc5
10 changed files with 558 additions and 150 deletions
+70 -10
View File
@@ -475,14 +475,78 @@ input[type="range"] { width: 100%; accent-color: var(--accent); }
.adm-brand { display: flex; align-items: center; gap: 6px; font-weight: 650; letter-spacing: -0.01em; color: var(--text); text-decoration: none; }
.adm-brand em { font-style: normal; font-weight: 400; color: var(--accent); }
.adm-brand img { border-radius: 8px; }
/* Curating pictures: the pool of uploads on the left, the three landing
sections on the right. One picker button per destination under each photo. */
.adm-cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; align-items: start; }
/* Curating pictures: the albums on the left, the chosen frame and its strip of
thumbnails on the right. The album column is the narrow one — it is a list of
names — and scrolls on its own so the strip beside it never moves. */
.adm-cols { display: grid; grid-template-columns: minmax(200px, 280px) minmax(0, 1fr); gap: 16px; align-items: start; }
@media (max-width: 1100px) { .adm-cols { grid-template-columns: minmax(0, 1fr); } }
.adm-col { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
/* Wide enough for the three chips to sit on one line: the picker is the point
of the card, so the grid gives it the width it needs. */
.adm-cols .adm-grid { grid-template-columns: repeat(auto-fill, minmax(450px, 1fr)); }
/* The filters over the album list: one name box, two pickers. */
.adm-input {
width: 100%;
font: inherit;
font-size: 12px;
color: var(--text);
background: var(--bg-sunken);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 6px 8px;
}
.adm-filters { display: flex; gap: 6px; }
.adm-filters .adm-input { min-width: 0; }
.adm-album-list { display: flex; flex-direction: column; gap: 4px; max-height: 62vh; overflow-y: auto; padding-right: 2px; }
.adm-album {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font: inherit;
font-size: 12px;
text-align: left;
color: var(--text-dim);
background: var(--bg-sunken);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 7px 9px;
cursor: pointer;
}
.adm-album:hover { border-color: var(--accent-line); }
.adm-album.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.adm-album-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.adm-album-count { font-family: var(--mono); font-size: 11px; }
/* The frame that is up: the picture on the left, everything about it on the
right — including the four landing sections it may appear in. */
.adm-big {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(220px, 300px);
gap: 12px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg);
padding: 10px;
}
.adm-big-img { width: 100%; aspect-ratio: 4 / 3; object-fit: contain; border-radius: var(--radius-sm); background: var(--bg-sunken); }
.adm-big-side { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.adm-qr-inline { border-radius: var(--radius-sm); }
/* The album's strip: thumbnails in one row, scrolled sideways when they
overflow the pane rather than wrapping into a wall of pictures. */
.adm-filmstrip { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; }
.adm-thumb {
flex: 0 0 132px;
margin: 0;
padding: 0;
font: inherit;
cursor: pointer;
border: 1px solid var(--border-soft);
border-radius: var(--radius-sm);
overflow: hidden;
background: var(--bg-sunken);
color: var(--text-dim);
}
.adm-thumb:hover { border-color: var(--accent-line); }
.adm-thumb.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.adm-thumb img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
.adm-thumb-cap { display: block; padding: 3px 6px; font-family: var(--mono); font-size: 10.5px; }
/* The three destinations sit on one line under the photo, each an independent
checkbox — tick all three and the photo shows in all three sections. */
.adm-choices { display: flex; flex-wrap: wrap; gap: 5px; }
@@ -518,9 +582,6 @@ input[type="range"] { width: 100%; accent-color: var(--accent); }
}
.adm-row-count { font-family: var(--mono); color: var(--text); }
.adm-row-empty { margin: 0; }
.adm-thumb { margin: 0; border: 1px solid var(--border-soft); border-radius: var(--radius-sm); overflow: hidden; background: var(--bg-sunken); }
.adm-thumb img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
.adm-thumb figcaption { padding: 3px 6px; font-family: var(--mono); font-size: 10.5px; color: var(--text-dim); }
/* The QR section's real artefact is the code, not the photo. Drawn on white:
a scanner needs the quiet zone, and the dark theme has none. */
.adm-qr { display: flex; align-items: center; justify-content: center; min-height: 88px; padding: 6px; background: #fff; }
@@ -610,7 +671,6 @@ input[type="range"] { width: 100%; accent-color: var(--accent); }
font-weight: 600;
}
.adm-face img { display: block; width: 100%; height: 100%; object-fit: cover; }
.adm-filter { display: flex; align-items: center; gap: 8px; }
/* Moderation state next to the account name, and the row's own actions: the
three controls sit side by side and the pane scrolls when it runs out. */
.adm-badge-warn { margin-left: 6px; color: var(--danger); font-size: 11px; }
+18
View File
@@ -308,6 +308,24 @@ html { scroll-behavior: smooth; scroll-padding-top: 116px; }
.lp-frame-meta { display: flex; flex-direction: column; gap: 3px; }
.lp-frame-meta b { font-size: 13px; font-weight: 600; }
.lp-frame-meta span { font-family: var(--lp-mono); font-size: 9.5px; color: var(--lp-muted); letter-spacing: 0.06em; }
/* The score line: the tally reads first, the five clickable stars after it, the
number of votes last. One vote per visitor — a second click replaces it. */
.lp-rate { display: flex; align-items: center; gap: 6px; margin-top: 2px; }
.lp-rate-index { font-family: var(--lp-mono); font-size: 12px; font-weight: 600; color: var(--lp-amber); }
.lp-stars { display: inline-flex; gap: 1px; }
.lp-star {
border: 0;
background: none;
padding: 0;
line-height: 1;
font-size: 13px;
cursor: pointer;
color: var(--lp-line);
transition: color 0.12s ease, transform 0.12s ease;
}
.lp-star:hover { color: var(--lp-amber); transform: scale(1.18); }
.lp-star.on { color: var(--lp-amber); }
.lp-rate-count { font-family: var(--lp-mono); font-size: 9.5px; color: var(--lp-muted); }
/* --- 4. preset tester --------------------------------------------------- */
.lp-tester { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 26px; align-items: start; }