Skip to content

Commit 322fcb9

Browse files
AmirMohammad CheraghaliAmirMohammad Cheraghali
authored andcommitted
feat(history): Add Recent History dropdown to search bar
1 parent 8e40d8e commit 322fcb9

3 files changed

Lines changed: 106 additions & 3 deletions

File tree

src/App.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ import { ToastContainer } from './components/Toast';
2727
import { useToast } from './hooks/useToast';
2828
import { FavoritesPanel } from './components/FavoritesPanel';
2929
import { useFavorites } from './hooks/useFavorites';
30+
import { useHistory } from './hooks/useHistory';
3031

3132
function App() {
3233
const viewerRef = useRef<ProteinViewerRef>(null);
3334
const { toasts, removeToast, success, error, info } = useToast();
3435
const { favorites, toggleFavorite, removeFavorite, isFavorite } = useFavorites();
36+
const { history, addToHistory } = useHistory();
3537
// Parse Global URL State Once
3638
const initialUrlState = parseURLState();
3739

@@ -387,7 +389,13 @@ function App() {
387389
// Optional logic: if mixed, maybe prompt or simple notification?
388390
// For now, we leave it to user unless it's the *only* thing (handled above)
389391
}
390-
}, [showLigands, initialUrlState.showLigands]);
392+
// Add to Recent History
393+
if (dataSource === 'pdb' && pdbId) {
394+
addToHistory(pdbId, 'pdb');
395+
} else if (dataSource === 'pubchem' && pdbId) {
396+
addToHistory(pdbId, 'pubchem');
397+
}
398+
}, [showLigands, initialUrlState.showLigands, pdbId, dataSource, addToHistory]);
391399

392400
const [proteinTitle, setProteinTitle] = useState<string | null>(null);
393401

@@ -1357,6 +1365,7 @@ function App() {
13571365
onToggleFavorite={() => toggleFavorite(pdbId, dataSource, proteinTitle || undefined)}
13581366
isFavorite={isFavorite(pdbId, dataSource)}
13591367
onOpenFavorites={() => setIsFavoritesOpen(true)}
1368+
history={history}
13601369
/>
13611370
);
13621371
})()}

src/components/Controls.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ import {
3131
Wrench,
3232
Share2,
3333
ScanSearch, // Added Icon
34-
Star
34+
Star,
35+
Clock
3536
} from 'lucide-react';
3637
import type { RepresentationType, ColoringType, ChainInfo, CustomColorRule, Snapshot, Movie, ColorPalette, PDBMetadata } from '../types';
3738
import type { DataSource } from '../utils/pdbUtils';
39+
import type { HistoryItem } from '../hooks/useHistory';
3840
import { formatChemicalId } from '../utils/pdbUtils';
3941
import { findMotifs } from '../utils/searchUtils';
4042
import type { MotifMatch } from '../utils/searchUtils';
@@ -323,6 +325,9 @@ interface ControlsProps {
323325
setShowIons?: (show: boolean) => void;
324326
onStartTour?: () => void;
325327

328+
// History
329+
history?: HistoryItem[];
330+
326331
// UI State Lifted Info
327332
openSections?: Record<string, boolean>;
328333
onToggleSection?: (section: string) => void;
@@ -405,12 +410,14 @@ export const Controls: React.FC<ControlsProps> = ({
405410
onToggleMobileSidebar,
406411
onToggleFavorite,
407412
isFavorite,
408-
onOpenFavorites
413+
onOpenFavorites,
414+
history = []
409415
}) => {
410416
// Motif Search State
411417
const [searchPattern, setSearchPattern] = useState('');
412418
const [searchResults, setSearchResults] = useState<MotifMatch[]>([]);
413419
const [isSearching, setIsSearching] = useState(false);
420+
const [isSearchFocused, setIsSearchFocused] = useState(false);
414421

415422

416423
const handleSearch = () => {
@@ -688,12 +695,42 @@ export const Controls: React.FC<ControlsProps> = ({
688695
type="text"
689696
value={localPdbId}
690697
onChange={(e) => setLocalPdbId(e.target.value)}
698+
onFocus={() => setIsSearchFocused(true)}
699+
onBlur={() => setTimeout(() => setIsSearchFocused(false), 200)}
691700
placeholder={
692701
dataSource === 'pubchem' ? "Search PubChem CID (e.g. 2244)" :
693702
"Search PDB ID (e.g. 1crn)"
694703
}
695704
className={`w-full rounded-lg pl-9 pr-3 py-2 border outline-none transition-all ${inputBg}`}
696705
/>
706+
{isSearchFocused && history && history.length > 0 && !localPdbId && (
707+
<div className="absolute top-full left-0 right-0 mt-2 bg-white dark:bg-neutral-800 rounded-lg shadow-xl border border-neutral-200 dark:border-neutral-700 z-50 overflow-hidden">
708+
<div className="px-3 py-2 text-xs font-semibold text-neutral-500 uppercase tracking-wider bg-neutral-50 dark:bg-neutral-900 border-b border-neutral-200 dark:border-neutral-700 flex items-center gap-2">
709+
<Clock className="w-3 h-3" />
710+
Recent History
711+
</div>
712+
<div className="max-h-60 overflow-y-auto">
713+
{history.slice(0, 10).map((item) => (
714+
<button
715+
key={`${item.id}-${item.dataSource}-${item.timestamp}`}
716+
type="button"
717+
onClick={() => {
718+
setLocalPdbId(item.id);
719+
setDataSource(item.dataSource);
720+
setPdbId(item.id);
721+
setIsSearchFocused(false);
722+
}}
723+
className="w-full text-left px-3 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-neutral-700 flex items-center justify-between group border-b border-neutral-100 dark:border-neutral-800 last:border-0"
724+
>
725+
<span className="font-mono font-medium">{item.id}</span>
726+
<span className="text-[10px] text-neutral-400 group-hover:text-neutral-500 bg-neutral-100 dark:bg-neutral-900 px-1.5 py-0.5 rounded border border-neutral-200 dark:border-neutral-700">
727+
{item.dataSource === 'pdb' ? 'PDB' : 'CHEM'}
728+
</span>
729+
</button>
730+
))}
731+
</div>
732+
</div>
733+
)}
697734
</div>
698735
{onToggleFavorite && (
699736
<button

src/hooks/useHistory.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { useState, useEffect, useCallback } from 'react';
2+
3+
export interface HistoryItem {
4+
id: string;
5+
dataSource: 'pdb' | 'pubchem';
6+
timestamp: number;
7+
}
8+
9+
const STORAGE_KEY = 'protein-viewer-history';
10+
const MAX_HISTORY = 10;
11+
12+
export function useHistory() {
13+
const [history, setHistory] = useState<HistoryItem[]>(() => {
14+
try {
15+
const stored = localStorage.getItem(STORAGE_KEY);
16+
return stored ? JSON.parse(stored) : [];
17+
} catch {
18+
return [];
19+
}
20+
});
21+
22+
// Persist to localStorage whenever history changes
23+
useEffect(() => {
24+
try {
25+
localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
26+
} catch (e) {
27+
console.error('Failed to save history:', e);
28+
}
29+
}, [history]);
30+
31+
const addToHistory = useCallback((id: string, dataSource: 'pdb' | 'pubchem') => {
32+
setHistory(prev => {
33+
// Remove existing entry for this ID/Source to prevent duplicates
34+
const filtered = prev.filter(item => !(item.id === id && item.dataSource === dataSource));
35+
36+
// Add new item to the top
37+
const newItem: HistoryItem = {
38+
id,
39+
dataSource,
40+
timestamp: Date.now()
41+
};
42+
43+
// Combine and slice to max length
44+
return [newItem, ...filtered].slice(0, MAX_HISTORY);
45+
});
46+
}, []);
47+
48+
const clearHistory = useCallback(() => {
49+
setHistory([]);
50+
}, []);
51+
52+
return {
53+
history,
54+
addToHistory,
55+
clearHistory
56+
};
57+
}

0 commit comments

Comments
 (0)