Files
RecipesCam/docker/frontend/src/ui/NameModal.tsx
T
3dtours dcea6b926f web: name a photo on first save, and file looks from a SAVE RECENT tab
Two saves that never had a name of their own now ask for one, through a single
modal (ui/NameModal, shared by both flows).

The first filing of an upload asks what the folder keeps it as, and that name
rides along as the frame's title. A re-save keeps the name it already has, so
it never asks twice.

SAVE RECENT leaves CREATE RECIPES and becomes its own rail tab: it files the
look standing on the stage — sim, WB, light, FX and the frame — as a recipe of
this account's own, refusing a name the account has already spent. The frame
travels in the recipe's JSON, so applying the entry puts the whole look back.
The tab lists those files and is the one place they can be deleted from; the
API already scopes both by user. They also show up in PRESETS/RECIPES, deduped
against anything CREATE filed under the same name in this session.
2026-09-18 14:57:49 +07:00

79 lines
2.2 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { useI18n } from '../i18n/I18nProvider';
// One field, one rule, shared by the two save flows that have to ask for a name:
// the photo being filed and the look being filed as a recipe. `check` is where a
// caller says a name is already taken — the reply is shown under the field and
// nothing is saved until it comes back clean.
export function NameModal({
title,
label,
initial,
check,
onSubmit,
onClose,
}: {
title: string;
label: string;
initial: string;
check?: (name: string) => string | null;
onSubmit: (name: string) => void;
onClose: () => void;
}) {
const { t } = useI18n();
const [name, setName] = useState(initial);
const [problem, setProblem] = useState<string | null>(null);
const field = useRef<HTMLInputElement>(null);
useEffect(() => field.current?.select(), []);
const submit = (e: React.FormEvent) => {
e.preventDefault();
const trimmed = name.trim();
if (!trimmed) {
setProblem(t('name.required'));
return;
}
const clash = check?.(trimmed) ?? null;
if (clash) {
setProblem(clash);
return;
}
onSubmit(trimmed);
};
return (
<div className="modal-backdrop" onMouseDown={(e) => e.target === e.currentTarget && onClose()}>
<form className="modal" data-key="name-modal" onSubmit={submit}>
<h2>{title}</h2>
<label className="hint" htmlFor="name-modal-field">
{label}
</label>
<input
id="name-modal-field"
ref={field}
className="field"
data-key="name-input"
value={name}
maxLength={60}
onChange={(e) => {
setName(e.target.value);
setProblem(null);
}}
/>
{problem ? (
<p className="err" data-key="name-error">
{problem}
</p>
) : null}
<button type="submit" className="btn primary" data-key="name-ok">
{t('name.save')}
</button>
<button type="button" className="btn ghost" data-key="name-cancel" onClick={onClose}>
{t('name.cancel')}
</button>
</form>
</div>
);
}