130 lines
4.8 KiB
TypeScript
130 lines
4.8 KiB
TypeScript
import { useState, useCallback, useEffect } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { CheckIcon } from './icons/CheckIcon';
|
|
import { CloseIcon } from './icons/CloseIcon';
|
|
import { SpinnerIcon } from './icons/SpinnerIcon';
|
|
|
|
export default function SaveSearchModal({
|
|
onClose,
|
|
onSave,
|
|
onViewSearches,
|
|
saving,
|
|
error,
|
|
}: {
|
|
onClose: () => void;
|
|
onSave: (name: string) => Promise<void>;
|
|
onViewSearches: () => void;
|
|
saving: boolean;
|
|
error: string | null;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const [name, setName] = useState('');
|
|
const [saved, setSaved] = useState(false);
|
|
|
|
const handleSubmit = useCallback(
|
|
async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!name.trim() || saving) return;
|
|
try {
|
|
await onSave(name.trim());
|
|
setSaved(true);
|
|
} catch {
|
|
// Error displayed in modal
|
|
}
|
|
},
|
|
[name, saving, onSave]
|
|
);
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose();
|
|
};
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
}, [onClose]);
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center" onClick={onClose}>
|
|
<div className="absolute inset-0 bg-black/50 dark:bg-black/70" />
|
|
<div
|
|
className="relative w-full max-w-sm mx-4 bg-white dark:bg-warm-900 rounded-lg shadow-xl border border-warm-200 dark:border-warm-700"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="flex items-center justify-between px-5 pt-5 pb-3">
|
|
<h2 className="text-lg font-semibold text-navy-950 dark:text-white">
|
|
{saved ? t('saveSearch.saved') : t('saveSearch.title')}
|
|
</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-warm-400 hover:text-warm-700 dark:text-warm-400 dark:hover:text-warm-200"
|
|
>
|
|
<CloseIcon className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{saved ? (
|
|
<div className="p-5 pt-2 space-y-4">
|
|
<div className="flex items-center gap-2 text-teal-600 dark:text-teal-400">
|
|
<CheckIcon className="w-5 h-5" />
|
|
<p className="text-sm text-warm-700 dark:text-warm-300">
|
|
{t('saveSearch.savedSuccess')}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-3 justify-end">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="px-4 py-2 text-sm rounded border border-warm-200 dark:border-warm-700 text-warm-700 dark:text-warm-300 hover:bg-warm-50 dark:hover:bg-warm-700"
|
|
>
|
|
{t('common.close')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={onViewSearches}
|
|
className="px-4 py-2 text-sm rounded bg-teal-600 text-white font-medium hover:bg-teal-700"
|
|
>
|
|
{t('saveSearch.viewSavedSearches')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleSubmit} className="p-5 pt-2 space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-warm-700 dark:text-warm-300 mb-1">
|
|
{t('saveSearch.name')}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
className="w-full px-3 py-2 text-sm rounded border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 text-navy-950 dark:text-white placeholder-warm-400 dark:placeholder-warm-500 outline-none focus:ring-2 ring-teal-400 dark:ring-teal-500"
|
|
placeholder={t('saveSearch.namePlaceholder')}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
{error && <p className="text-sm text-red-600 dark:text-red-300">{error}</p>}
|
|
|
|
<div className="flex gap-3 justify-end">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="px-4 py-2 text-sm rounded border border-warm-200 dark:border-warm-700 text-warm-700 dark:text-warm-300 hover:bg-warm-50 dark:hover:bg-warm-700"
|
|
>
|
|
{t('common.cancel')}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={!name.trim() || saving}
|
|
className="flex items-center gap-2 px-4 py-2 text-sm rounded bg-teal-600 text-white font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-wait"
|
|
>
|
|
{saving && <SpinnerIcon className="w-4 h-4 animate-spin" />}
|
|
{saving ? t('saveSearch.saving') : t('common.save')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|