diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 699c6d5..afcda49 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -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(null); const currentNoteRef = useRef(null); const scratchRef = useRef(null); const searchInputRef = useRef(null); const searchTimeoutRef = useRef(); + const gotoInputRef = useRef(null); // Add a ref to track the last saved content const lastSavedContentRef = useRef(''); @@ -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 = () => { + {/* Go To Date Modal */} + + + + Go to date + +
+ setGotoDateInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + goToClosestNoteToDate(gotoDateInput); + } + }} + className={`${getTextClass('2xl')} bg-slate-700 border-slate-600 text-slate-200`} + /> +
+ + +
+
+
+
+ {/* Cleanup Modal */}