feat: calendar

This commit is contained in:
OpenSquared
2026-07-02 11:27:45 +02:00
parent 8098104f5b
commit 5081514898
6 changed files with 286 additions and 118 deletions

View File

@@ -167,9 +167,9 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
const [syncMsg, setSyncMsg] = useState<string | null>(null)
const calPollRef = useRef<ReturnType<typeof setInterval> | null>(null)
const [teHtmlMsg, setTeHtmlMsg] = useState<string | null>(null)
const [teHtmlBusy, setTeHtmlBusy] = useState(false)
const teHtmlPollRef = useRef<ReturnType<typeof setInterval> | null>(null)
const [ffPageMsg, setFfPageMsg] = useState<string | null>(null)
const [ffPageBusy, setFfPageBusy] = useState(false)
const ffPagePollRef = useRef<ReturnType<typeof setInterval> | null>(null)
const fileInputId = useId()
useEffect(() => {
@@ -193,7 +193,7 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
return () => {
if (pollRef.current) clearInterval(pollRef.current)
if (calPollRef.current) clearInterval(calPollRef.current)
if (teHtmlPollRef.current) clearInterval(teHtmlPollRef.current)
if (ffPagePollRef.current) clearInterval(ffPagePollRef.current)
}
}, [onImported])
@@ -210,12 +210,11 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
const r = s.last_result || {}
if (r.error) {
setSyncMsg(`Erreur: ${r.error}`)
} else if (s.source === 'ff_fallback') {
const tw = r.thisweek?.events ?? '?'
const nw = r.nextweek?.events ?? '?'
setSyncMsg(`✓ FF fallback: ${tw}+${nw} events`)
} else {
setSyncMsg(`✓ FXStreet: ${r.total_upserted} events`)
const tw = r.live?.thisweek?.events ?? r.live?.events ?? '?'
const nw = r.live?.nextweek?.events ?? '?'
const sc = r.scrape?.total_upserted ?? 0
setSyncMsg(`✓ FF: ${tw}+${nw} live, ${sc} upcoming`)
}
onImported()
}
@@ -223,35 +222,35 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
} catch { setSyncMsg('sync error'); setCalStatus(s => ({ ...s, running: false })) }
}
const uploadAndImportTeHtml = async (file: File) => {
setTeHtmlBusy(true)
setTeHtmlMsg('uploading…')
const uploadAndImportFfPage = async (file: File) => {
setFfPageBusy(true)
setFfPageMsg('uploading…')
try {
const form = new FormData()
form.append('file', file)
const up = await fetch(`${API}/api/eco/te-html-upload`, { method: 'POST', body: form }).then(x => x.json())
if (up.error) { setTeHtmlMsg(`Upload error: ${up.error}`); setTeHtmlBusy(false); return }
setTeHtmlMsg(`uploaded (${up.size_mb} MB) — importing…`)
await fetch(`${API}/api/eco/te-html-import`, { method: 'POST' })
teHtmlPollRef.current = setInterval(async () => {
const s = await fetch(`${API}/api/eco/te-html-import/status`).then(x => x.json())
const up = await fetch(`${API}/api/eco/ff-page-upload`, { method: 'POST', body: form }).then(x => x.json())
if (up.error) { setFfPageMsg(`Upload error: ${up.error}`); setFfPageBusy(false); return }
setFfPageMsg(`uploaded (${up.size_mb} MB) — importing…`)
await fetch(`${API}/api/eco/ff-page-import`, { method: 'POST' })
ffPagePollRef.current = setInterval(async () => {
const s = await fetch(`${API}/api/eco/ff-page-import/status`).then(x => x.json())
if (!s.running) {
clearInterval(teHtmlPollRef.current!)
setTeHtmlBusy(false)
clearInterval(ffPagePollRef.current!)
setFfPageBusy(false)
const r = s.last_result || {}
if (r.error) { setTeHtmlMsg(`Error: ${r.error}`); return }
setTeHtmlMsg(`TE HTML: ${r.total} events (${r.date_from}${r.date_to})`)
if (r.error) { setFfPageMsg(`Error: ${r.error}`); return }
setFfPageMsg(`FF Page: ${r.inserted} events (${r.date_from}${r.date_to})`)
onImported()
}
}, 2000)
} catch { setTeHtmlBusy(false); setTeHtmlMsg('error') }
} catch { setFfPageBusy(false); setFfPageMsg('error') }
}
useEffect(() => () => { if (teHtmlPollRef.current) clearInterval(teHtmlPollRef.current) }, [])
useEffect(() => () => { if (ffPagePollRef.current) clearInterval(ffPagePollRef.current) }, [])
const hasData = stats.total > 0
const res = importStatus.last_result
const srcLabel = calStatus.source === 'ff_fallback' ? 'FF fallback' : calStatus.source === 'fxstreet' ? 'FXStreet' : null
const srcLabel = calStatus.source === 'ff' ? 'ForexFactory' : calStatus.source ?? null
return (
<div className="bg-slate-800 border border-slate-700 rounded-lg p-4 mb-4 flex flex-wrap items-center gap-4 text-sm">
@@ -284,27 +283,27 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
{calStatus.running ? 'Syncing…' : 'Sync Now'}
</button>
{/* TE HTML import */}
{/* FF Page import — save forexfactory.com/calendar as HTML (Ctrl+U) and upload */}
<label
htmlFor={fileInputId}
className={clsx(
'px-3 py-1.5 rounded flex items-center gap-1.5 text-white cursor-pointer',
teHtmlBusy
ffPageBusy
? 'bg-amber-800 opacity-50 cursor-not-allowed'
: 'bg-amber-700 hover:bg-amber-600'
)}
title="Import Trading Economics HTML page (calendar_data.txt) — 1000 events with forecasts"
>
<Upload size={13} />
Import TE HTML
Import FF Page
</label>
<input
id={fileInputId}
type="file"
accept=".txt,.html"
className="hidden"
disabled={teHtmlBusy}
onChange={e => { const f = e.target.files?.[0]; if (f) uploadAndImportTeHtml(f); e.target.value = '' }}
disabled={ffPageBusy}
onChange={e => { const f = e.target.files?.[0]; if (f) uploadAndImportFfPage(f); e.target.value = '' }}
/>
{res && !importStatus.running && (
@@ -317,9 +316,9 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
{syncMsg}
</span>
)}
{teHtmlMsg && (
<span className={clsx('text-xs', teHtmlMsg.startsWith('✓') ? 'text-emerald-400' : teHtmlMsg.startsWith('Error') ? 'text-red-400' : 'text-yellow-400')}>
{teHtmlMsg}
{ffPageMsg && (
<span className={clsx('text-xs', ffPageMsg.startsWith('✓') ? 'text-emerald-400' : ffPageMsg.startsWith('Error') ? 'text-red-400' : 'text-yellow-400')}>
{ffPageMsg}
</span>
)}
</div>