Quick save

This commit is contained in:
Andras Schmelczer 2026-02-07 22:19:44 +00:00
parent e5d5819098
commit 2906b01734
25 changed files with 1070 additions and 237 deletions

View file

@ -0,0 +1,167 @@
import { useState, useCallback } from 'react';
import type { SavedSearch } from '../../hooks/useSavedSearches';
import { BookmarkIcon } from '../ui/icons/BookmarkIcon';
import { TrashIcon } from '../ui/icons/TrashIcon';
import { SpinnerIcon } from '../ui/icons/SpinnerIcon';
import { CloseIcon } from '../ui/icons/CloseIcon';
import { formatRelativeTime } from '../../lib/format';
import { summarizeParams } from '../../lib/url-state';
export default function SavedSearchesPage({
searches,
loading,
onDelete,
onOpen,
}: {
searches: SavedSearch[];
loading: boolean;
onDelete: (id: string) => Promise<void>;
onOpen: (params: string) => void;
}) {
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
const [copiedId, setCopiedId] = useState<string | null>(null);
const handleDeleteConfirm = useCallback(async () => {
if (!deleteConfirmId) return;
await onDelete(deleteConfirmId);
setDeleteConfirmId(null);
}, [deleteConfirmId, onDelete]);
const handleShare = useCallback((params: string, id: string) => {
const url = `${window.location.origin}/?${params}`;
const onSuccess = () => {
setCopiedId(id);
setTimeout(() => setCopiedId(null), 2000);
};
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(url).then(onSuccess);
} else {
const ta = document.createElement('textarea');
ta.value = url;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
onSuccess();
}
}, []);
return (
<div className="flex-1 overflow-auto bg-warm-50 dark:bg-warm-900">
<div className="max-w-5xl mx-auto px-6 py-8">
<h1 className="text-2xl font-bold text-navy-950 dark:text-warm-100 mb-6">Saved Searches</h1>
{loading ? (
<div className="flex items-center justify-center py-20">
<SpinnerIcon className="w-8 h-8 text-teal-600 dark:text-teal-400 animate-spin" />
</div>
) : searches.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-center">
<BookmarkIcon className="w-12 h-12 text-warm-300 dark:text-warm-600 mb-4" />
<p className="text-lg font-medium text-warm-600 dark:text-warm-400 mb-1">
No saved searches yet
</p>
<p className="text-sm text-warm-500 dark:text-warm-500">
Save your dashboard filters and view to quickly return to them later.
</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{searches.map((search) => (
<div
key={search.id}
className="bg-white dark:bg-warm-800 border border-warm-200 dark:border-warm-700 rounded-lg overflow-hidden"
>
{search.screenshotUrl ? (
<img
src={search.screenshotUrl}
alt={search.name}
className="w-full h-36 object-cover"
/>
) : (
<div className="w-full h-36 bg-gradient-to-br from-teal-600/20 to-navy-900/30 dark:from-teal-400/10 dark:to-navy-900/40 flex items-center justify-center">
<BookmarkIcon className="w-10 h-10 text-warm-300 dark:text-warm-600" />
</div>
)}
<div className="p-4">
<h3 className="font-medium text-navy-950 dark:text-warm-100 truncate mb-1">
{search.name}
</h3>
<p className="text-xs text-warm-500 dark:text-warm-400 mb-1">
{formatRelativeTime(search.created)}
</p>
<p className="text-xs text-warm-500 dark:text-warm-400 mb-3">
{summarizeParams(search.params)}
</p>
<div className="flex gap-2">
<button
onClick={() => onOpen(search.params)}
className="flex-1 px-3 py-1.5 text-sm font-medium rounded bg-teal-600 text-white hover:bg-teal-700"
>
Open
</button>
<button
onClick={() => handleShare(search.params, search.id)}
className="px-3 py-1.5 text-sm rounded border border-warm-200 dark:border-warm-700 text-warm-500 dark:text-warm-400 hover:text-warm-700 dark:hover:text-warm-300"
>
{copiedId === search.id ? 'Copied!' : 'Share'}
</button>
<button
onClick={() => setDeleteConfirmId(search.id)}
className="px-3 py-1.5 text-sm rounded border border-warm-200 dark:border-warm-700 text-warm-500 dark:text-warm-400 hover:text-warm-700 dark:hover:text-warm-300"
title="Delete"
>
<TrashIcon className="w-4 h-4" />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Delete confirmation dialog */}
{deleteConfirmId && (
<div className="fixed inset-0 z-50 flex items-center justify-center" onClick={() => setDeleteConfirmId(null)}>
<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-800 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">Delete search</h2>
<button
onClick={() => setDeleteConfirmId(null)}
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>
<p className="px-5 pb-4 text-sm text-warm-700 dark:text-warm-300">
Are you sure you want to delete this saved search? This cannot be undone.
</p>
<div className="flex gap-3 justify-end px-5 pb-5">
<button
onClick={() => setDeleteConfirmId(null)}
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"
>
Cancel
</button>
<button
onClick={handleDeleteConfirm}
className="px-4 py-2 text-sm rounded bg-red-600 text-white font-medium hover:bg-red-700"
>
Delete
</button>
</div>
</div>
</div>
)}
</div>
);
}