mike/frontend/src/app/components/shared/AddUserInput.tsx
2026-07-16 20:07:12 +08:00

126 lines
4.1 KiB
TypeScript

"use client";
import { useState } from "react";
import type { KeyboardEvent } from "react";
import { Loader2, UserPlus } from "lucide-react";
import {
lookupUserByEmail,
type UserLookupResult,
} from "@/app/lib/mikeApi";
import { PillButton } from "@/app/components/ui/pill-button";
import { cn } from "@/app/lib/utils";
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
interface AddUserInputProps {
onAdd: (user: UserLookupResult) => Promise<void> | void;
validateEmail?: (email: string) => Promise<string | null> | string | null;
busy?: boolean;
placeholder?: string;
autoFocus?: boolean;
submitLabel?: string;
className?: string;
}
export function AddUserInput({
onAdd,
validateEmail,
busy = false,
placeholder = "Add by email...",
autoFocus = false,
submitLabel = "Add user",
className,
}: AddUserInputProps) {
const [input, setInput] = useState("");
const [checking, setChecking] = useState(false);
const [error, setError] = useState<string | null>(null);
const trimmedEmail = input.trim().toLowerCase();
const showAddButton = trimmedEmail.length > 0;
async function commitUser() {
const email = trimmedEmail;
if (!email || busy || checking) return;
if (!EMAIL_RE.test(email)) {
setError("Enter a valid email.");
return;
}
setError(null);
setChecking(true);
try {
const validationError = await validateEmail?.(email);
if (validationError) {
setError(validationError);
return;
}
const user = await lookupUserByEmail(email);
if (!user.exists) {
setError(`${email} does not belong to a Mike user.`);
return;
}
await onAdd(user);
setInput("");
} catch (err) {
setError(
err instanceof Error
? err.message
: "Could not add this user. Try again.",
);
} finally {
setChecking(false);
}
}
function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (event.key === "Enter" || event.key === ",") {
event.preventDefault();
void commitUser();
}
}
return (
<div>
<div
className={cn(
"flex min-h-10 items-center gap-2 rounded-xl border border-white/70 bg-white/55 px-3 py-1.5 shadow-[0_3px_9px_rgba(15,23,42,0.04),inset_0_1px_0_rgba(255,255,255,0.86),inset_0_-1px_0_rgba(255,255,255,0.58)] backdrop-blur-xl transition-colors focus-within:bg-white/70",
className,
)}
>
<UserPlus className="h-3.5 w-3.5 shrink-0 text-gray-400" />
<input
type="email"
value={input}
onChange={(event) => {
setInput(event.target.value);
setError(null);
}}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className="min-w-0 flex-1 bg-transparent text-sm text-gray-700 outline-none placeholder:text-gray-400"
autoFocus={autoFocus}
/>
{showAddButton && (
<PillButton
tone="blue"
size="sm"
type="button"
onMouseDown={(event) => event.preventDefault()}
onClick={() => void commitUser()}
disabled={busy || checking}
title={submitLabel}
className="h-6 shrink-0 px-2.5 text-[11px] leading-none"
>
{(busy || checking) && (
<Loader2 className="h-3 w-3 animate-spin" />
)}
Add
</PillButton>
)}
</div>
{error && <p className="mt-1.5 text-xs text-red-500">{error}</p>}
</div>
);
}