Skip to content

Commit ae0bbce

Browse files
committed
ui: add prompts
1 parent 3cd4039 commit ae0bbce

File tree

3 files changed

+42
-5
lines changed

3 files changed

+42
-5
lines changed

src/app/claim/[id]/ClientPage.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default function ClientPage() {
1919
const [loading, setLoading] = useState(true);
2020
const [error, setError] = useState<string | undefined>();
2121
const [effectivePassword, setEffectivePassword] = useState<string | undefined>(undefined);
22+
const [passwordChecked, setPasswordChecked] = useState(false);
2223
const [askPassword, setAskPassword] = useState(false);
2324
const [passwordError, setPasswordError] = useState(false);
2425
type DownUrl = { url: string; expiresAt?: string };
@@ -29,11 +30,12 @@ export default function ClientPage() {
2930
if (!id) return;
3031
const saved = getClaimPassword(id);
3132
setEffectivePassword(saved);
33+
setPasswordChecked(true);
3234
}, [id]);
3335

3436
const lastLoadIdRef = useRef(0);
3537
const load = useCallback(async () => {
36-
if (!id) return;
38+
if (!id || !passwordChecked) return;
3739
const thisLoadId = ++lastLoadIdRef.current;
3840
setLoading(true);
3941
setError(undefined);
@@ -72,7 +74,7 @@ export default function ClientPage() {
7274
} finally {
7375
if (lastLoadIdRef.current === thisLoadId) setLoading(false);
7476
}
75-
}, [id, effectivePassword]);
77+
}, [id, effectivePassword, passwordChecked]);
7678

7779
useEffect(() => {
7880
void load();
@@ -149,7 +151,7 @@ export default function ClientPage() {
149151
}
150152

151153
// Require password: render only the password prompt, hide page content
152-
if (askPassword) {
154+
if (askPassword && passwordChecked) {
153155
return (
154156
<div className="w-full min-h-[50vh] flex items-center justify-center">
155157
<div className="w-full max-w-sm rounded bg-background p-4 border">
@@ -175,7 +177,7 @@ export default function ClientPage() {
175177
<div className="flex items-center justify-between gap-3">
176178
<h1 className="text-2xl font-semibold">{t('detailTitle')}</h1>
177179
<div className="flex items-center gap-2">
178-
<button onClick={copyLink} className="px-3 py-1.5 rounded border text-sm hover:bg-black/5 dark:hover:bg-white/10">{t('copy')}</button>
180+
<CopyButton onCopy={copyLink} label={t('copy')} doneLabel={t('copied')} />
179181
<Link href="/claim/new" className="px-3 py-1.5 rounded border text-sm hover:bg-black/5 dark:hover:bg-white/10">{t('newClaim')}</Link>
180182
</div>
181183
</div>
@@ -208,7 +210,7 @@ export default function ClientPage() {
208210
<div className="font-mono text-sm break-all">{claim.id}</div>
209211
<div className="text-xs text-black/60 dark:text-white/60">{t('claimIdNote')}</div>
210212
</div>
211-
<button onClick={() => navigator.clipboard?.writeText(claim.id)} className="shrink-0 px-3 py-1.5 rounded border text-sm hover:bg-black/5 dark:hover:bg-white/10">{t('copy')}</button>
213+
<CopyButton onCopy={() => navigator.clipboard?.writeText(claim.id)} label={t('copy')} doneLabel={t('copied')} className="shrink-0" />
212214
</div>
213215
{claim.description && <p className="mt-2 text-sm whitespace-pre-wrap">{claim.description}</p>}
214216
<div className="mt-3 grid gap-2 sm:grid-cols-2 text-sm">
@@ -328,3 +330,19 @@ function PasswordPrompt({ onSubmit, onCancel, error }: { onSubmit: (pwd: string)
328330
</form>
329331
);
330332
}
333+
334+
function CopyButton({ onCopy, label, doneLabel, className }: { onCopy: () => void | Promise<void>; label: string; doneLabel: string; className?: string }) {
335+
const [copied, setCopied] = useState(false);
336+
return (
337+
<button
338+
onClick={async () => {
339+
try { await onCopy(); } catch {}
340+
setCopied(true);
341+
setTimeout(() => setCopied(false), 1200);
342+
}}
343+
className={(className ? className + ' ' : '') + "px-3 py-1.5 rounded border text-sm hover:bg-black/5 dark:hover:bg-white/10"}
344+
>
345+
{copied ? doneLabel : label}
346+
</button>
347+
);
348+
}

src/app/claim/new/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export default function NewClaimPage() {
5252
const [expenseAt, setExpenseAt] = useState<string>(""); // yyyy-MM-ddTHH:mm
5353
const [recipient, setRecipient] = useState<string>("");
5454
const [password, setPassword] = useState<string>("");
55+
const [password2, setPassword2] = useState<string>("");
5556
const [payout, setPayout] = useState<PayoutInfo>({});
5657

5758
useEffect(() => {
@@ -88,6 +89,8 @@ export default function NewClaimPage() {
8889
if (!expenseAt) return "请选择消费发生时间";
8990
// minimal payout validation
9091
if (!payout.iban && !payout.accountNumber) return "请至少填写 IBAN 或 账户号";
92+
// If password set, require confirmation match
93+
if (password.trim() && password.trim() !== password2.trim()) return t('passwordMismatch');
9194
}
9295

9396
async function handleSubmit(e: React.FormEvent) {
@@ -253,6 +256,7 @@ export default function NewClaimPage() {
253256
<h2 className="text-lg font-medium">{t('password')}</h2>
254257
<p className="text-sm text-black/60 dark:text-white/60">{t('passwordHint')}</p>
255258
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className="w-full rounded border px-3 py-2 bg-transparent" placeholder={t('password')} />
259+
<input type="password" value={password2} onChange={(e) => setPassword2(e.target.value)} className="w-full rounded border px-3 py-2 bg-transparent" placeholder={t('passwordConfirm')} />
256260
</section>
257261

258262
{error && <div className="text-sm text-red-600">{error}</div>}

src/lib/i18n.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const dict: Record<Lang, Record<string, string>> = {
4141
attachments: "附件(发票/小票照片)",
4242
password: "访问密码(可选)",
4343
passwordHint: "可为该报销单设置访问密码。设置后,查看或更新需要输入此密码。",
44+
passwordConfirm: "再次输入密码",
45+
passwordMismatch: "两次输入的密码不一致",
4446
submit: "提交报销单",
4547
detailTitle: "报销单详情",
4648
refresh: "刷新",
@@ -63,6 +65,7 @@ const dict: Record<Lang, Record<string, string>> = {
6365
status_WITHDRAW: "已撤回",
6466
claimIdNote: "此 ID 类似密码,请妥善保管,不要分享。",
6567
copy: "复制",
68+
copied: "已复制",
6669
enterPassword: "请输入访问密码",
6770
passwordWrong: "密码错误,请重试",
6871
cancel: "取消",
@@ -96,6 +99,8 @@ const dict: Record<Lang, Record<string, string>> = {
9699
attachments: "Attachments (receipt photos)",
97100
password: "Password (optional)",
98101
passwordHint: "You can set a password. Viewing or updating later requires it.",
102+
passwordConfirm: "Confirm password",
103+
passwordMismatch: "Passwords do not match",
99104
submit: "Submit Claim",
100105
detailTitle: "Claim Detail",
101106
refresh: "Refresh",
@@ -118,6 +123,7 @@ const dict: Record<Lang, Record<string, string>> = {
118123
status_WITHDRAW: "Withdrawn",
119124
claimIdNote: "Treat this ID like a password. Keep it private.",
120125
copy: "Copy",
126+
copied: "Copied",
121127
enterPassword: "Enter password",
122128
passwordWrong: "Incorrect password. Please try again.",
123129
cancel: "Cancel",
@@ -151,6 +157,8 @@ const dict: Record<Lang, Record<string, string>> = {
151157
attachments: "Beilagen (Belege/Fotos)",
152158
password: "Passwort (optional)",
153159
passwordHint: "Sie können ein Passwort setzen. Ansicht/Änderung erfordert es.",
160+
passwordConfirm: "Passwort bestätigen",
161+
passwordMismatch: "Passwörter stimmen nicht überein",
154162
submit: "Spesen einreichen",
155163
detailTitle: "Spesendetails",
156164
refresh: "Aktualisieren",
@@ -173,6 +181,7 @@ const dict: Record<Lang, Record<string, string>> = {
173181
status_WITHDRAW: "Zurückgezogen",
174182
claimIdNote: "Behandeln Sie diese ID wie ein Passwort. Nicht teilen.",
175183
copy: "Kopieren",
184+
copied: "Kopiert",
176185
enterPassword: "Passwort eingeben",
177186
passwordWrong: "Falsches Passwort. Bitte erneut versuchen.",
178187
cancel: "Abbrechen",
@@ -206,6 +215,8 @@ const dict: Record<Lang, Record<string, string>> = {
206215
attachments: "Pièces jointes (reçus)",
207216
password: "Mot de passe (optionnel)",
208217
passwordHint: "Vous pouvez définir un mot de passe. Requis pour consulter/modifier.",
218+
passwordConfirm: "Confirmer le mot de passe",
219+
passwordMismatch: "Les mots de passe ne correspondent pas",
209220
submit: "Soumettre la note",
210221
detailTitle: "Détails de la note",
211222
refresh: "Actualiser",
@@ -228,6 +239,7 @@ const dict: Record<Lang, Record<string, string>> = {
228239
status_WITHDRAW: "Retiré",
229240
claimIdNote: "Considérez cet ID comme un mot de passe. Ne le partagez pas.",
230241
copy: "Copier",
242+
copied: "Copié",
231243
enterPassword: "Saisir le mot de passe",
232244
passwordWrong: "Mot de passe incorrect. Veuillez réessayer.",
233245
cancel: "Annuler",
@@ -261,6 +273,8 @@ const dict: Record<Lang, Record<string, string>> = {
261273
attachments: "Allegati (ricevute/foto)",
262274
password: "Password (opzionale)",
263275
passwordHint: "Puoi impostare una password. Sarà richiesta per vedere/modificare in seguito.",
276+
passwordConfirm: "Conferma password",
277+
passwordMismatch: "Le password non corrispondono",
264278
submit: "Invia nota",
265279
detailTitle: "Dettagli della nota",
266280
refresh: "Aggiorna",
@@ -283,6 +297,7 @@ const dict: Record<Lang, Record<string, string>> = {
283297
status_WITHDRAW: "Ritirata",
284298
claimIdNote: "Tratta questo ID come una password. Mantienilo privato.",
285299
copy: "Copia",
300+
copied: "Copiato",
286301
enterPassword: "Inserisci la password",
287302
passwordWrong: "Password errata. Riprova.",
288303
cancel: "Annulla",

0 commit comments

Comments
 (0)