feat(Index.tsx): add functionality to navigate to notes by date using MeiliSearch

This commit is contained in:
2025-08-11 10:45:40 +02:00
parent 320432bd8c
commit 3fcc9b02f7

View File

@@ -53,12 +53,15 @@ const Index = () => {
const [isInitialized, setIsInitialized] = useState(false); const [isInitialized, setIsInitialized] = useState(false);
const [fontSize, setFontSize] = useState('medium'); const [fontSize, setFontSize] = useState('medium');
const [isPreviousNoteModified, setIsPreviousNoteModified] = useState(false); const [isPreviousNoteModified, setIsPreviousNoteModified] = useState(false);
const [isGotoOpen, setIsGotoOpen] = useState(false);
const [gotoDateInput, setGotoDateInput] = useState('');
const previousNoteRef = useRef<HTMLDivElement>(null); const previousNoteRef = useRef<HTMLDivElement>(null);
const currentNoteRef = useRef<HTMLTextAreaElement>(null); const currentNoteRef = useRef<HTMLTextAreaElement>(null);
const scratchRef = useRef<HTMLTextAreaElement>(null); const scratchRef = useRef<HTMLTextAreaElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
const searchTimeoutRef = useRef<NodeJS.Timeout>(); const searchTimeoutRef = useRef<NodeJS.Timeout>();
const gotoInputRef = useRef<HTMLInputElement>(null);
// Add a ref to track the last saved content // Add a ref to track the last saved content
const lastSavedContentRef = useRef<string>(''); const lastSavedContentRef = useRef<string>('');
@@ -423,6 +426,7 @@ const Index = () => {
}, },
body: JSON.stringify({ body: JSON.stringify({
q: query, q: query,
matchingStrategy: 'all',
limit: 50, limit: 50,
attributesToHighlight: ['content'], attributesToHighlight: ['content'],
showRankingScore: true, showRankingScore: true,
@@ -476,6 +480,84 @@ const Index = () => {
}; };
}, [searchQuery]); }, [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 // Get problematic notes
const getProblematicNotes = async () => { const getProblematicNotes = async () => {
try { try {
@@ -634,6 +716,10 @@ const Index = () => {
e.preventDefault(); e.preventDefault();
setIsSearchOpen(true); setIsSearchOpen(true);
searchInputRef.current?.focus(); 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') { } else if (e.key === 'Escape') {
e.preventDefault(); e.preventDefault();
// Save current note if it has content // Save current note if it has content
@@ -1155,6 +1241,45 @@ const Index = () => {
</DialogContent> </DialogContent>
</Dialog> </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 */} {/* Cleanup Modal */}
<Dialog open={isCleanupOpen} onOpenChange={setIsCleanupOpen}> <Dialog open={isCleanupOpen} onOpenChange={setIsCleanupOpen}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-hidden bg-slate-800 border-slate-700"> <DialogContent className="max-w-6xl max-h-[90vh] overflow-hidden bg-slate-800 border-slate-700">