web: give each member a photo folder and burn the strip into the export

Every member gets /photos — their own uploads, counted against a 12-photo
cap, each card showing the tagline and the technical line the studio would
print. The studio gains SAVE PHOTO n/12 in the top bar: it renders the full
resolution look, stores the strip (tag/title/meta) with the upload so the
landing reel frames it the same way, and refuses past the cap.

EXPORT now burns that strip into the file: the amber #TAG over the photo's
top-left plus a dark caption band below carrying the recipe name and the
ISO / grain / warmth line. The live preview stays clean, and the saved
upload stays clean too — the reel draws its own frame from the stored
labels, so a burned band would tag the tag twice.

Admins manage any photo through DELETE /api/photos/:id; members only their
own. The users table's photo counts stay in step with the folder.
This commit is contained in:
2026-09-18 10:56:26 +07:00
parent 43d86b4b6f
commit b4d5d2926b
14 changed files with 576 additions and 22 deletions
+18
View File
@@ -106,13 +106,31 @@ export const en: Dict = {
'save.recipes': 'SAVE RECIPES',
'save.saved': 'Saved "{name}"',
'save.needLogin': 'Log in to save a recipe.',
'save.photo': 'SAVE PHOTO {n}/{max}',
'save.photoNeedLogin': 'Log in to save a photo.',
'save.photoSaved': 'Photo saved {n}/{max}.',
'save.photoFull': 'That is all {max} — remove one in My photos.',
'save.photoNoSource': 'Load a photo first.',
'save.photoFailed': 'Could not save the photo: {msg}',
'photo.contribute': 'CONTRIBUTE A PHOTO',
'photo.uploading': 'UPLOADING…',
'photo.thanks': 'Your frame is in the strip.',
'photo.failed': 'Upload failed: {msg}',
'photos.title': 'My photos',
'photos.hint': 'Each account keeps up to {max} photos. What the studio saves lives here and shows up on the community reel.',
'photos.count': '{n}/{max} photos',
'photos.add': 'ADD A PHOTO',
'photos.empty': 'No photos yet. Open the studio and hit SAVE PHOTO, or add one right here.',
'photos.full': 'That is all {max}. Remove one before saving again.',
'photos.delete': 'DELETE',
'photos.deleted': 'Photo removed.',
'photos.failed': 'That did not work: {msg}',
'photos.noLabels': 'No strip labels yet',
'nav.admin': 'Admin',
'nav.photos': 'My photos',
'nav.profile': 'Profile',
'nav.studio': 'Studio',
'adm.title': 'Strip moderation',
+18
View File
@@ -15,6 +15,7 @@ export const vi = {
'nav.language': 'Ngôn ngữ',
'nav.back': 'Về trang chủ',
'nav.admin': 'Quản trị',
'nav.photos': 'Ảnh của tôi',
'nav.profile': 'Hồ sơ',
'nav.studio': 'Studio',
'nav.guest': 'Khách',
@@ -112,12 +113,29 @@ export const vi = {
'save.recipes': 'LƯU CÔNG THỨC',
'save.saved': 'Đã lưu "{name}"',
'save.needLogin': 'Đăng nhập để lưu công thức.',
'save.photo': 'LƯU ẢNH {n}/{max}',
'save.photoNeedLogin': 'Đăng nhập để lưu ảnh.',
'save.photoSaved': 'Đã lưu ảnh {n}/{max}.',
'save.photoFull': 'Đã đủ {max} ảnh — xoá bớt trong Ảnh của tôi.',
'save.photoNoSource': 'Chưa có ảnh nào để lưu.',
'save.photoFailed': 'Không lưu được ảnh: {msg}',
'photo.contribute': 'ĐÓNG GÓP ẢNH',
'photo.uploading': 'ĐANG TẢI…',
'photo.thanks': 'Ảnh của bạn đã vào dải phim.',
'photo.failed': 'Không tải được ảnh: {msg}',
'photos.title': 'Ảnh của tôi',
'photos.hint': 'Mỗi tài khoản giữ tối đa {max} ảnh. Ảnh lưu từ studio nằm ở đây và xuất hiện trên dải phim cộng đồng.',
'photos.count': '{n}/{max} ảnh',
'photos.add': 'THÊM ẢNH',
'photos.empty': 'Chưa có ảnh nào. Mở studio rồi bấm LƯU ẢNH, hoặc thêm ảnh ngay ở đây.',
'photos.full': 'Đã đủ {max} ảnh. Xoá bớt trước khi lưu tiếp.',
'photos.delete': 'XOÁ',
'photos.deleted': 'Đã xoá ảnh.',
'photos.failed': 'Không thực hiện được: {msg}',
'photos.noLabels': 'Chưa có nhãn dải phim',
'adm.title': 'Quản trị dải phim',
'adm.subtitle': 'Ảnh do người dùng đóng góp. Xoá một ảnh để gỡ nó khỏi trang chủ.',
'adm.upload': 'THÊM ẢNH',