feat: custom date range filter on calendar page
- Backend: /api/eco/calendar accepts date_from + date_to query params when period=custom; get_calendar() uses them directly without override Limit raised to 5000 for wide date ranges - Frontend: "Custom" tab at end of period list; shows two date inputs (from/to) with Apply button; displays day count; fetches on Apply click (not on every keystroke to avoid hammering the API) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -421,10 +421,12 @@ def ff_scrape_status_ep() -> Dict[str, Any]:
|
||||
|
||||
@router.get("/calendar")
|
||||
def ff_calendar(
|
||||
period: str = Query("recent", description="recent|today|tomorrow|yesterday|this_week|next_week|previous_week|this_month|next_month|previous_month"),
|
||||
period: str = Query("recent", description="recent|today|tomorrow|yesterday|this_week|next_week|previous_week|this_month|next_month|previous_month|custom"),
|
||||
date_from: Optional[str] = Query(None, description="YYYY-MM-DD (used when period=custom)"),
|
||||
date_to: Optional[str] = Query(None, description="YYYY-MM-DD (used when period=custom)"),
|
||||
currencies: Optional[str] = Query(None, description="Comma-separated: USD,EUR,GBP,..."),
|
||||
impacts: Optional[str] = Query(None, description="Comma-separated: high,medium,low"),
|
||||
limit: int = Query(300, ge=1, le=2000),
|
||||
limit: int = Query(500, ge=1, le=5000),
|
||||
offset: int = Query(0, ge=0),
|
||||
) -> Dict[str, Any]:
|
||||
"""Unified Forex Factory calendar — past releases + upcoming events."""
|
||||
@@ -433,6 +435,8 @@ def ff_calendar(
|
||||
imp_list = [i.strip().lower() for i in impacts.split(",") if i.strip()] if impacts else None
|
||||
return get_calendar(
|
||||
period=period,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
currencies=cur_list,
|
||||
impacts=imp_list,
|
||||
limit=limit,
|
||||
|
||||
@@ -232,6 +232,8 @@ def sync_live() -> Dict[str, Any]:
|
||||
|
||||
def get_calendar(
|
||||
period: str = "recent",
|
||||
date_from: Optional[str] = None,
|
||||
date_to: Optional[str] = None,
|
||||
currencies: Optional[List[str]] = None,
|
||||
impacts: Optional[List[str]] = None,
|
||||
limit: int = 300,
|
||||
@@ -243,6 +245,7 @@ def get_calendar(
|
||||
period options:
|
||||
recent | today | tomorrow | yesterday | this_week | next_week |
|
||||
previous_week | this_month | next_month | previous_month | custom
|
||||
When period=custom, date_from and date_to are used directly.
|
||||
"""
|
||||
from services.database import get_conn
|
||||
|
||||
@@ -250,10 +253,14 @@ def get_calendar(
|
||||
monday = today - timedelta(days=today.weekday())
|
||||
sunday = monday + timedelta(days=6)
|
||||
|
||||
date_from: Optional[str] = None
|
||||
date_to: Optional[str] = None
|
||||
# For custom period, date_from/date_to come from the caller — don't override
|
||||
if period != "custom":
|
||||
date_from = None
|
||||
date_to = None
|
||||
|
||||
if period == "today":
|
||||
if period == "custom":
|
||||
pass # date_from / date_to already set by caller
|
||||
elif period == "today":
|
||||
date_from = date_to = str(today)
|
||||
elif period == "tomorrow":
|
||||
t = today + timedelta(days=1)
|
||||
|
||||
@@ -41,16 +41,17 @@ interface ImportStatus {
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const PERIODS = [
|
||||
{ key: 'recent', label: 'Recent' },
|
||||
{ key: 'today', label: 'Today' },
|
||||
{ key: 'tomorrow', label: 'Tomorrow' },
|
||||
{ key: 'this_week', label: 'This Week' },
|
||||
{ key: 'next_week', label: 'Next Week' },
|
||||
{ key: 'this_month', label: 'This Month' },
|
||||
{ key: 'next_month', label: 'Next Month' },
|
||||
{ key: 'yesterday', label: 'Yesterday' },
|
||||
{ key: 'previous_week', label: 'Prev Week' },
|
||||
{ key: 'previous_month','label': 'Prev Month' },
|
||||
{ key: 'recent', label: 'Recent' },
|
||||
{ key: 'today', label: 'Today' },
|
||||
{ key: 'tomorrow', label: 'Tomorrow' },
|
||||
{ key: 'this_week', label: 'This Week' },
|
||||
{ key: 'next_week', label: 'Next Week' },
|
||||
{ key: 'this_month', label: 'This Month' },
|
||||
{ key: 'next_month', label: 'Next Month' },
|
||||
{ key: 'yesterday', label: 'Yesterday' },
|
||||
{ key: 'previous_week', label: 'Prev Week' },
|
||||
{ key: 'previous_month', label: 'Prev Month' },
|
||||
{ key: 'custom', label: '📅 Custom' },
|
||||
] as const
|
||||
|
||||
const ALL_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD', 'NZD', 'CHF', 'CNY']
|
||||
@@ -306,6 +307,8 @@ function ImpactFilter({ selected, onChange }: { selected: string[]; onChange: (v
|
||||
|
||||
export default function CalendarPage() {
|
||||
const [period, setPeriod] = useState<string>('recent')
|
||||
const [customFrom, setCustomFrom] = useState<string>('')
|
||||
const [customTo, setCustomTo] = useState<string>('')
|
||||
const [currencies, setCurrencies] = useState<string[]>(['USD'])
|
||||
const [impacts, setImpacts] = useState<string[]>(['high', 'medium'])
|
||||
const [data, setData] = useState<CalendarResponse | null>(null)
|
||||
@@ -323,10 +326,16 @@ export default function CalendarPage() {
|
||||
}, [])
|
||||
|
||||
const fetchEvents = useCallback(async () => {
|
||||
// For custom period, require both dates
|
||||
if (period === 'custom' && (!customFrom || !customTo)) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const params = new URLSearchParams({ period, limit: '500' })
|
||||
const params = new URLSearchParams({ period, limit: '2000' })
|
||||
if (period === 'custom') {
|
||||
params.set('date_from', customFrom)
|
||||
params.set('date_to', customTo)
|
||||
}
|
||||
if (currencies.length > 0) params.set('currencies', currencies.join(','))
|
||||
if (impacts.length > 0) params.set('impacts', impacts.join(','))
|
||||
const r: CalendarResponse = await fetch(`${API}/api/eco/calendar?${params}`).then(x => x.json())
|
||||
@@ -336,7 +345,7 @@ export default function CalendarPage() {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [period, currencies, impacts])
|
||||
}, [period, customFrom, customTo, currencies, impacts])
|
||||
|
||||
useEffect(() => { fetchStats() }, [fetchStats])
|
||||
useEffect(() => { fetchEvents() }, [fetchEvents])
|
||||
@@ -409,6 +418,38 @@ export default function CalendarPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Custom date range picker */}
|
||||
{period === 'custom' && (
|
||||
<div className="flex flex-wrap items-center gap-3 mb-3 p-3 bg-slate-800 border border-slate-700 rounded-lg">
|
||||
<span className="text-slate-400 text-sm">From</span>
|
||||
<input
|
||||
type="date"
|
||||
value={customFrom}
|
||||
onChange={e => setCustomFrom(e.target.value)}
|
||||
className="bg-slate-700 border border-slate-600 rounded px-2 py-1 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
<span className="text-slate-400 text-sm">To</span>
|
||||
<input
|
||||
type="date"
|
||||
value={customTo}
|
||||
onChange={e => setCustomTo(e.target.value)}
|
||||
className="bg-slate-700 border border-slate-600 rounded px-2 py-1 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
<button
|
||||
onClick={fetchEvents}
|
||||
disabled={!customFrom || !customTo}
|
||||
className="px-3 py-1.5 rounded bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-sm text-white"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
{customFrom && customTo && (
|
||||
<span className="text-slate-500 text-xs">
|
||||
{Math.round((new Date(customTo).getTime() - new Date(customFrom).getTime()) / 86400000) + 1} days
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filters toggle */}
|
||||
<div className="mb-3">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user