Adds viewportFit: 'cover' so the PWA can draw under the notch / Dynamic Island when installed. Nav and Toast read env(safe-area-inset-*) to keep their content out of the hardware cutouts (no-op on browsers without a notch — env() resolves to 0). Replaces autoFocus on the first field of CreateAccountDialog and CreateUserDialog with a useEffect that only focuses on pointer devices (matchMedia '(hover: hover) and (pointer: fine)'). Phones no longer get the soft keyboard popping the instant a dialog opens.
116 lines
3.2 KiB
TypeScript
116 lines
3.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState, useTransition } from "react";
|
|
import { createUser } from "@/app/actions";
|
|
import FormDialogShell, { Field, inputClass } from "./form-dialog-shell";
|
|
|
|
type Props = {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSuccess?: (fUsername: string) => void;
|
|
};
|
|
|
|
export default function CreateUserDialog({ open, onClose, onSuccess }: Props) {
|
|
const [pending, startTransition] = useTransition();
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [fUsername, setFUsername] = useState("");
|
|
const [fPassword, setFPassword] = useState("");
|
|
const [tUsername, setTUsername] = useState("");
|
|
const [tPassword, setTPassword] = useState("");
|
|
const firstFieldRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setFUsername("");
|
|
setFPassword("");
|
|
setTUsername("");
|
|
setTPassword("");
|
|
setError(null);
|
|
// Autofocus first field only on pointer devices — see CreateAccountDialog.
|
|
if (typeof window !== "undefined") {
|
|
const isPointerDevice = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
|
|
if (isPointerDevice) {
|
|
requestAnimationFrame(() => firstFieldRef.current?.focus());
|
|
}
|
|
}
|
|
}
|
|
}, [open]);
|
|
|
|
function submit() {
|
|
if (!fUsername.trim() || !fPassword || !tUsername.trim() || !tPassword) {
|
|
setError("All fields are required");
|
|
return;
|
|
}
|
|
setError(null);
|
|
const trimmedFUsername = fUsername.trim();
|
|
startTransition(async () => {
|
|
const result = await createUser({
|
|
f_username: trimmedFUsername,
|
|
f_password: fPassword,
|
|
t_username: tUsername.trim(),
|
|
t_password: tPassword,
|
|
});
|
|
if (result.ok) {
|
|
onSuccess?.(trimmedFUsername);
|
|
onClose();
|
|
} else {
|
|
setError(result.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
return (
|
|
<FormDialogShell
|
|
open={open}
|
|
onCancel={onClose}
|
|
onSubmit={submit}
|
|
title="New user pairing"
|
|
submitLabel="Create"
|
|
pending={pending}
|
|
error={error}
|
|
>
|
|
<Field label="From username" required>
|
|
<input
|
|
ref={firstFieldRef}
|
|
value={fUsername}
|
|
onChange={(e) => setFUsername(e.target.value)}
|
|
disabled={pending}
|
|
autoComplete="off"
|
|
spellCheck={false}
|
|
className={inputClass}
|
|
/>
|
|
</Field>
|
|
<Field label="From password" required>
|
|
<input
|
|
value={fPassword}
|
|
onChange={(e) => setFPassword(e.target.value)}
|
|
disabled={pending}
|
|
autoComplete="off"
|
|
spellCheck={false}
|
|
className={inputClass}
|
|
/>
|
|
</Field>
|
|
<Field label="To username" required>
|
|
<input
|
|
value={tUsername}
|
|
onChange={(e) => setTUsername(e.target.value)}
|
|
disabled={pending}
|
|
autoComplete="off"
|
|
spellCheck={false}
|
|
className={inputClass}
|
|
/>
|
|
</Field>
|
|
<Field label="To password (security PIN)" required>
|
|
<input
|
|
value={tPassword}
|
|
onChange={(e) => setTPassword(e.target.value)}
|
|
disabled={pending}
|
|
autoComplete="off"
|
|
spellCheck={false}
|
|
className={inputClass}
|
|
/>
|
|
</Field>
|
|
</FormDialogShell>
|
|
);
|
|
}
|