BillTracker/client/components/ui/input-dialog.jsx

100 lines
3.0 KiB
JavaScript

/**
* InputDialog — replaces window.prompt().
* Usage:
* <InputDialog
* open={open} onOpenChange={setOpen}
* title="Rename Category"
* description="Enter a new name."
* label="Name" defaultValue={current} placeholder="e.g. Utilities"
* onConfirm={(value) => handleSave(value)}
* />
*/
import { useState, useEffect, useRef } from 'react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
export function InputDialog({
open,
onOpenChange,
title,
description,
label,
defaultValue = '',
placeholder = '',
confirmLabel = 'Save',
cancelLabel = 'Cancel',
validate, // optional (value: string) => string | null (null = valid)
onConfirm,
loading = false,
}) {
const [value, setValue] = useState(defaultValue);
const [error, setError] = useState('');
const inputRef = useRef(null);
// Reset value when dialog opens
useEffect(() => {
if (open) {
setValue(defaultValue);
setError('');
// autofocus + select all after animation frame
requestAnimationFrame(() => {
inputRef.current?.focus();
inputRef.current?.select();
});
}
}, [open, defaultValue]);
const handleConfirm = () => {
const trimmed = value.trim();
if (!trimmed) { setError('This field is required.'); return; }
if (validate) {
const msg = validate(trimmed);
if (msg) { setError(msg); return; }
}
onConfirm?.(trimmed);
};
const handleKeyDown = (e) => {
if (e.key === 'Enter') { e.preventDefault(); handleConfirm(); }
if (e.key === 'Escape') onOpenChange?.(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
{description && <DialogDescription>{description}</DialogDescription>}
</DialogHeader>
<div className="space-y-1.5">
{label && <Label htmlFor="input-dialog-field" className="text-xs font-medium text-muted-foreground uppercase tracking-wide">{label}</Label>}
<Input
id="input-dialog-field"
ref={inputRef}
value={value}
onChange={(e) => { setValue(e.target.value); setError(''); }}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className={error ? 'border-destructive focus-visible:ring-destructive' : ''}
/>
{error && <p className="text-xs text-destructive">{error}</p>}
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => onOpenChange?.(false)} disabled={loading}>
{cancelLabel}
</Button>
<Button onClick={handleConfirm} disabled={loading || !value.trim()}>
{loading ? 'Saving…' : confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}