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
+6 -3
View File
@@ -9,15 +9,18 @@ import { Landing } from './Landing';
import { Workspace } from './App';
import { Admin } from './Admin';
import { ProfilePage } from './ProfilePage';
import { PhotosPage } from './PhotosPage';
// Four routes, no router: the landing page, the workspace, the strip moderation
// screen and the member's own profile. nginx serves index.html for all of them
// (SPA fallback), so this is just a pathname check.
// Five routes, no router: the landing page, the workspace, the strip moderation
// screen, the member's own profile and the member's photo folder. nginx serves
// index.html for all of them (SPA fallback), so this is a pathname check.
const path = window.location.pathname;
const page = path.startsWith('/app') ? (
<Workspace />
) : path.startsWith('/admin') ? (
<Admin />
) : path.startsWith('/photos') ? (
<PhotosPage />
) : path.startsWith('/profile') ? (
<ProfilePage />
) : (