cm_bot_v2/web/components/create-account-dialog.tsx
yiekheng 3bfd35ef8d fix(web): PWA notch safe-area + skip autoFocus on touch devices
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.
2026-05-02 21:26:42 +08:00

123 lines
3.5 KiB
TypeScript

"use client";
import { useEffect, useRef, useState, useTransition } from "react";
import { createAccount } from "@/app/actions";
import FormDialogShell, { Field, inputClass } from "./form-dialog-shell";
type Props = {
open: boolean;
onClose: () => void;
onSuccess?: (username: string) => void;
prefixPattern?: string;
};
export default function CreateAccountDialog({ open, onClose, onSuccess, prefixPattern }: Props) {
const [pending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [status, setStatus] = useState("");
const [link, setLink] = useState("");
const firstFieldRef = useRef<HTMLInputElement>(null);
// Reset on open.
useEffect(() => {
if (open) {
setUsername("");
setPassword("");
setStatus("");
setLink("");
setError(null);
// Autofocus the first field only on devices with a fine pointer
// (desktop). Phones skip this — the soft keyboard popping the
// moment a dialog opens is jarring and reflows the page.
if (typeof window !== "undefined") {
const isPointerDevice = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
if (isPointerDevice) {
requestAnimationFrame(() => firstFieldRef.current?.focus());
}
}
}
}, [open]);
function submit() {
if (!username.trim() || !password) {
setError("Username and password are required");
return;
}
setError(null);
const trimmedUsername = username.trim();
startTransition(async () => {
const result = await createAccount({
username: trimmedUsername,
password,
status,
link,
});
if (result.ok) {
onSuccess?.(trimmedUsername);
onClose();
} else {
setError(result.error);
}
});
}
return (
<FormDialogShell
open={open}
onCancel={onClose}
onSubmit={submit}
title="New account"
submitLabel="Create"
pending={pending}
error={error}
>
<Field label="Username" required hint={prefixPattern ? `Suggested prefix: ${prefixPattern}` : undefined}>
<input
ref={firstFieldRef}
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={pending}
autoComplete="off"
spellCheck={false}
placeholder={prefixPattern ? `${prefixPattern}1234` : "username"}
className={inputClass}
/>
</Field>
<Field label="Password" required>
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={pending}
autoComplete="off"
spellCheck={false}
className={inputClass}
/>
</Field>
<Field label="Status" hint="Empty | wait | done">
<input
value={status}
onChange={(e) => setStatus(e.target.value)}
disabled={pending}
autoComplete="off"
spellCheck={false}
placeholder="(leave blank for available)"
className={inputClass}
/>
</Field>
<Field label="Link">
<input
value={link}
onChange={(e) => setLink(e.target.value)}
disabled={pending}
autoComplete="off"
spellCheck={false}
placeholder="https://..."
className={inputClass}
/>
</Field>
</FormDialogShell>
);
}