feat(Index.tsx): add flexible ISO8601 date parsing and fallback note fetching logic

This commit is contained in:
2025-08-11 11:52:42 +02:00
parent 3fcc9b02f7
commit d799df2dbd

View File

@@ -157,6 +157,94 @@ const Index = () => {
return trimmedLines.join('\n');
};
// Flexible ISO8601 date parsing (supports YYYY, YYYY-MM, YYYY-MM-DD, full ISO)
const parseFlexibleIsoDateToMs = (input: string): number | null => {
const trimmed = input.trim();
if (!trimmed) return null;
if (/^\d{4}$/.test(trimmed)) {
const year = Number(trimmed);
const start = Date.UTC(year, 0, 1, 0, 0, 0, 0);
const end = Date.UTC(year + 1, 0, 1, 0, 0, 0, 0) - 1;
return Math.floor((start + end) / 2);
}
const ym = trimmed.match(/^(\d{4})-(\d{2})$/);
if (ym) {
const year = Number(ym[1]);
const monthIndex = Number(ym[2]) - 1; // 0-based
if (monthIndex < 0 || monthIndex > 11) return null;
const start = Date.UTC(year, monthIndex, 1, 0, 0, 0, 0);
const nextMonthYear = monthIndex === 11 ? year + 1 : year;
const nextMonthIndex = (monthIndex + 1) % 12;
const end = Date.UTC(nextMonthYear, nextMonthIndex, 1, 0, 0, 0, 0) - 1;
return Math.floor((start + end) / 2);
}
const ymd = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (ymd) {
const year = Number(ymd[1]);
const monthIndex = Number(ymd[2]) - 1; // 0-based
const day = Number(ymd[3]);
if (monthIndex < 0 || monthIndex > 11) return null;
const start = Date.UTC(year, monthIndex, day, 0, 0, 0, 0);
const end = Date.UTC(year, monthIndex, day + 1, 0, 0, 0, 0) - 1;
return Math.floor((start + end) / 2);
}
// YYYY-MM-DD HH
const ymdh = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2})$/);
if (ymdh) {
const year = Number(ymdh[1]);
const monthIndex = Number(ymdh[2]) - 1;
const day = Number(ymdh[3]);
const hour = Number(ymdh[4]);
if (monthIndex < 0 || monthIndex > 11 || hour < 0 || hour > 23) return null;
const start = Date.UTC(year, monthIndex, day, hour, 0, 0, 0);
const end = Date.UTC(year, monthIndex, day, hour + 1, 0, 0, 0) - 1;
return Math.floor((start + end) / 2);
}
// YYYY-MM-DD HH:mm
const ymdhm = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})$/);
if (ymdhm) {
const year = Number(ymdhm[1]);
const monthIndex = Number(ymdhm[2]) - 1;
const day = Number(ymdhm[3]);
const hour = Number(ymdhm[4]);
const minute = Number(ymdhm[5]);
if (monthIndex < 0 || monthIndex > 11 || hour < 0 || hour > 23 || minute < 0 || minute > 59) return null;
const start = Date.UTC(year, monthIndex, day, hour, minute, 0, 0);
const end = Date.UTC(year, monthIndex, day, hour, minute + 1, 0, 0) - 1;
return Math.floor((start + end) / 2);
}
// YYYY-MM-DD HH:mm:ss
const ymdhms = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/);
if (ymdhms) {
const year = Number(ymdhms[1]);
const monthIndex = Number(ymdhms[2]) - 1;
const day = Number(ymdhms[3]);
const hour = Number(ymdhms[4]);
const minute = Number(ymdhms[5]);
const second = Number(ymdhms[6]);
if (
monthIndex < 0 || monthIndex > 11 ||
hour < 0 || hour > 23 ||
minute < 0 || minute > 59 ||
second < 0 || second > 59
) return null;
const start = Date.UTC(year, monthIndex, day, hour, minute, second, 0);
const end = Date.UTC(year, monthIndex, day, hour, minute, second + 1, 0) - 1;
return Math.floor((start + end) / 2);
}
const full = new Date(trimmed);
const ms = full.getTime();
if (Number.isNaN(ms)) return null;
return ms;
};
// Create a new note
const createNote = async (content: string) => {
try {
@@ -397,6 +485,53 @@ const Index = () => {
setNoteCache(notes);
setCurrentNoteIndex(targetIndex);
setPreviousNote(notes[targetIndex]);
} else {
// Fallback: fetch a date window around the target note
const targetMs = targetNote.epochTime;
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${MEILISEARCH_API_KEY}`,
} as const;
let windowMs = 1000 * 60 * 60 * 24 * 90; // 90 days
for (let i = 0; i < 3; i++) {
const start = targetMs - windowMs;
const end = targetMs + windowMs;
const resp = await fetch(`${MEILISEARCH_ENDPOINT}/indexes/${NOTE_INDEX}/search`, {
method: 'POST',
headers,
body: JSON.stringify({
q: '',
filter: `date >= ${start} AND date <= ${end}`,
sort: ['date:desc'],
limit: 1000,
}),
});
if (resp.ok) {
const d = await resp.json();
const aroundNotes: Note[] = d.hits.map((hit: any) => ({
id: hit.id,
epochTime: hit.date,
dateISO: hit.dateISO,
content: hit.content,
topLetter: hit.topLetter,
topLetterFrequency: hit.topLetterFrequency,
letterCount: hit.letterCount || 0,
}));
if (aroundNotes.length > 0) {
const exactIdx = aroundNotes.findIndex(n => n.id === targetNote.id);
const pickIdx = exactIdx >= 0
? exactIdx
: aroundNotes.reduce((bestIdx, n, idx) => (
Math.abs(n.epochTime - targetMs) < Math.abs(aroundNotes[bestIdx].epochTime - targetMs) ? idx : bestIdx
), 0);
setNoteCache(aroundNotes);
setCurrentNoteIndex(pickIdx);
setPreviousNote(aroundNotes[pickIdx]);
return;
}
}
windowMs *= 2;
}
}
} catch (error) {
console.error('Error loading notes around target:', error);
@@ -483,9 +618,8 @@ const Index = () => {
// 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)) {
const targetMs = parseFlexibleIsoDateToMs(iso8601);
if (targetMs === null) {
toast({
title: 'Invalid date',
description: 'Please enter a valid ISO8601 date.',