feat(Index.tsx): add functionality to navigate to notes by date using MeiliSearch
This commit is contained in:
@@ -53,12 +53,15 @@ const Index = () => {
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [fontSize, setFontSize] = useState('medium');
|
||||
const [isPreviousNoteModified, setIsPreviousNoteModified] = useState(false);
|
||||
const [isGotoOpen, setIsGotoOpen] = useState(false);
|
||||
const [gotoDateInput, setGotoDateInput] = useState('');
|
||||
|
||||
const previousNoteRef = useRef<HTMLDivElement>(null);
|
||||
const currentNoteRef = useRef<HTMLTextAreaElement>(null);
|
||||
const scratchRef = useRef<HTMLTextAreaElement>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const searchTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const gotoInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Add a ref to track the last saved content
|
||||
const lastSavedContentRef = useRef<string>('');
|
||||
@@ -423,6 +426,7 @@ const Index = () => {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
q: query,
|
||||
matchingStrategy: 'all',
|
||||
limit: 50,
|
||||
attributesToHighlight: ['content'],
|
||||
showRankingScore: true,
|
||||
@@ -476,6 +480,84 @@ const Index = () => {
|
||||
};
|
||||
}, [searchQuery]);
|
||||
|
||||
// Go to closest note by ISO8601 date
|
||||
const goToClosestNoteToDate = async (iso8601: string) => {
|
||||
try {
|
||||
const target = new Date(iso8601);
|
||||
const targetMs = target.getTime();
|
||||
if (Number.isNaN(targetMs)) {
|
||||
toast({
|
||||
title: 'Invalid date',
|
||||
description: 'Please enter a valid ISO8601 date.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${MEILISEARCH_API_KEY}`,
|
||||
} as const;
|
||||
|
||||
const [prevRes, nextRes] = await Promise.all([
|
||||
fetch(`${MEILISEARCH_ENDPOINT}/indexes/${NOTE_INDEX}/search`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ q: '', filter: `date <= ${targetMs}`, sort: ['date:desc'], limit: 1 }),
|
||||
}),
|
||||
fetch(`${MEILISEARCH_ENDPOINT}/indexes/${NOTE_INDEX}/search`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ q: '', filter: `date >= ${targetMs}`, sort: ['date:asc'], limit: 1 }),
|
||||
}),
|
||||
]);
|
||||
|
||||
const [prevData, nextData] = await Promise.all([
|
||||
prevRes.ok ? prevRes.json() : Promise.resolve({ hits: [] }),
|
||||
nextRes.ok ? nextRes.json() : Promise.resolve({ hits: [] }),
|
||||
]);
|
||||
|
||||
const candidates: any[] = [];
|
||||
if (prevData.hits && prevData.hits.length > 0) candidates.push(prevData.hits[0]);
|
||||
if (nextData.hits && nextData.hits.length > 0) candidates.push(nextData.hits[0]);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
toast({
|
||||
title: 'No notes found',
|
||||
description: 'There are no notes near that date.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const closest = candidates.reduce((best, hit) => {
|
||||
const bestDiff = Math.abs((best?.date ?? best?.epochTime ?? 0) - targetMs);
|
||||
const hitDiff = Math.abs((hit.date ?? 0) - targetMs);
|
||||
return hitDiff < bestDiff ? hit : best;
|
||||
}, candidates[0]);
|
||||
|
||||
const targetNote: Note = {
|
||||
id: closest.id,
|
||||
epochTime: closest.date,
|
||||
dateISO: closest.dateISO,
|
||||
content: closest.content,
|
||||
topLetter: closest.topLetter,
|
||||
topLetterFrequency: closest.topLetterFrequency,
|
||||
letterCount: closest.letterCount || 0,
|
||||
};
|
||||
|
||||
setIsGotoOpen(false);
|
||||
setGotoDateInput('');
|
||||
await loadNotesAroundNote(targetNote);
|
||||
} catch (error) {
|
||||
console.error('Error going to date:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to locate a note near that date.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get problematic notes
|
||||
const getProblematicNotes = async () => {
|
||||
try {
|
||||
@@ -634,6 +716,10 @@ const Index = () => {
|
||||
e.preventDefault();
|
||||
setIsSearchOpen(true);
|
||||
searchInputRef.current?.focus();
|
||||
} else if (e.ctrlKey && (e.key === 'g' || e.key === 'G')) {
|
||||
e.preventDefault();
|
||||
setIsGotoOpen(true);
|
||||
setTimeout(() => gotoInputRef.current?.focus(), 0);
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
// Save current note if it has content
|
||||
@@ -1155,6 +1241,45 @@ const Index = () => {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Go To Date Modal */}
|
||||
<Dialog open={isGotoOpen} onOpenChange={setIsGotoOpen}>
|
||||
<DialogContent className="max-w-xl bg-slate-800 border-slate-700">
|
||||
<DialogHeader>
|
||||
<DialogTitle className={`${getTextClass('4xl')} text-slate-100`}>Go to date</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
ref={gotoInputRef}
|
||||
type="text"
|
||||
placeholder="Enter ISO8601 date (e.g. 2024-12-31T23:59:59Z)"
|
||||
value={gotoDateInput}
|
||||
onChange={(e) => setGotoDateInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
goToClosestNoteToDate(gotoDateInput);
|
||||
}
|
||||
}}
|
||||
className={`${getTextClass('2xl')} bg-slate-700 border-slate-600 text-slate-200`}
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setIsGotoOpen(false)}
|
||||
className={getTextClass('base')}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => goToClosestNoteToDate(gotoDateInput)}
|
||||
className={getTextClass('base')}
|
||||
>
|
||||
Go
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Cleanup Modal */}
|
||||
<Dialog open={isCleanupOpen} onOpenChange={setIsCleanupOpen}>
|
||||
<DialogContent className="max-w-6xl max-h-[90vh] overflow-hidden bg-slate-800 border-slate-700">
|
||||
|
Reference in New Issue
Block a user