web: moderate accounts, accept 12MB uploads, put SAVE under CREATE

- /admin User account rows gain BLOCK/UNBLOCK, REMOVE/RESTORE and DELETE.
  Blocked = cannot sign in (sessions swept), removed = hidden from the strip
  and cannot sign in, both reversible; DELETE drops the account with its
  photos and recipes and unlinks the files. An allowlisted account is never
  a target, so an admin cannot moderate or delete itself.
- Photo uploads move from a 3MB API cap / 4m nginx cap to 12MB / 16m, and
  the browser shrinks an oversized still before sending it (2048px JPEG,
  avatars 512px) so the declared type still matches the sniffed bytes.
- The studio SAVE leaves the top bar and sits under the CREATE RECIPES tab,
  labelled SAVE RECIPES.
This commit is contained in:
2026-09-18 10:33:35 +07:00
parent 8e6c1493e8
commit 43d86b4b6f
12 changed files with 386 additions and 45 deletions
+24 -11
View File
@@ -1023,8 +1023,9 @@ export function Workspace() {
setShowRecipes((v) => !v);
},
},
// ponytail: no CREATE chip — saving goes through the top bar. IMPORT
// is a chip inside the RECIPES strip.
// ponytail: no CREATE chip — the save button lives under the CREATE
// RECIPES tab (see the create column). IMPORT is a chip inside the
// RECIPES strip.
];
case 'favorited':
return saved.length === 0
@@ -1114,7 +1115,6 @@ export function Workspace() {
onUndo={undoLast}
onReset={reset}
onExport={exportPhoto}
onSave={saveRecipe}
onAuth={() => setAuthMode('login')}
onSignup={() => setAuthMode('signup')}
onLogout={async () => {
@@ -1131,14 +1131,27 @@ export function Workspace() {
<div className={`col col-main${tab === 'create' ? ' col-create' : ''}`} data-col="main">
<ChipRow chips={[resetChip()]} />
{tab === 'create' ? (
<RecipeCreatePanel
currentBase={recipe.baseFilter}
current={recipe.adjustments}
signedIn={!!user}
onPreview={previewCreatedRecipe}
onSave={(draft) => void addCreatedRecipe(draft)}
onExport={exportRecipeFile}
/>
<>
<RecipeCreatePanel
currentBase={recipe.baseFilter}
current={recipe.adjustments}
signedIn={!!user}
onPreview={previewCreatedRecipe}
onSave={(draft) => void addCreatedRecipe(draft)}
onExport={exportRecipeFile}
/>
{/* The studio's own save lives here now, under the CREATE tab:
it stores the recipe standing on the stage, where the panel
above stores the draft being built. */}
<button
type="button"
className="btn"
data-key="save-recipes"
onClick={() => void saveRecipe()}
>
{t('save.recipes')}
</button>
</>
) : (
<ChipRow chips={chips} />
)}