Files
2026FIFAWorldCup/platform/web/app/daily-card/page.tsx

152 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect, useMemo, useState } from 'react';
import { formatToTaipeiTime } from '@/lib/timezone';
import { getDailyCard, type DailyCardItem } from '@/lib/analytics-api';
import { ActionableBetCard } from '@/components/ActionableBetCard';
const TAB_MAP: Record<'safe' | 'risk' | 'parlay' | 'sgp', string> = {
safe: 'SAFE_SINGLE',
risk: 'HIGH_RISK_SINGLE',
parlay: 'SAFE_PARLAY',
sgp: 'SGP_LOTTERY',
};
const sampleDate = formatToTaipeiTime(new Date().toISOString(), 'yyyy-MM-dd');
export default function DailyCardPage() {
const [targetDate] = useState(sampleDate);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [activeTab, setActiveTab] = useState<'safe' | 'risk' | 'parlay' | 'sgp'>('safe');
const [selectedCount, setSelectedCount] = useState(0);
const [data, setData] = useState<Awaited<ReturnType<typeof getDailyCard>> | null>(null);
const tabCards: Record<'safe' | 'risk' | 'parlay' | 'sgp', DailyCardItem[]> = useMemo(() => {
if (!data) {
return { safe: [], risk: [], parlay: [], sgp: [] };
}
return {
safe: data.safe_singles,
risk: data.high_risk_singles,
parlay: data.safe_parlays,
sgp: data.sgp_lotteries,
};
}, [data]);
const briefing = useMemo(() => {
if (!data) {
return '系統載入中,將於短時間內給出當日全場賽前簡報。';
}
return `AI 總結:今日比賽共 ${data.matched_matches} 場,策略核心為 ${
data.total_daily_unit_recommendation
} Units。${
data.safe_singles.length > 0
? `檢測到 ${data.safe_singles.length} 組高穩定單關機會,重點偏向亞盤與低風險連續進位。`
: '低風險盤口偏弱,建議保守減碼。'
} ${
data.high_risk_singles.length > 0
? `另有 ${data.high_risk_singles.length} 組高賠搏冷訊號可做小額槓桿。`
: ''
}`;
}, [data]);
useEffect(() => {
const load = async () => {
setLoading(true);
setError('');
try {
const response = await getDailyCard(targetDate);
setData(response);
} catch (payloadError) {
setError(payloadError instanceof Error ? payloadError.message : '每日作戰卡暫時無法抓取');
} finally {
setLoading(false);
}
};
load().catch(() => undefined);
}, [targetDate]);
function handleAddToSlip(item: DailyCardItem) {
setSelectedCount((count) => count + 1);
// 未建立後續注單 API先保留為前端選單暫存
window.alert(`已加入注單追蹤:${item.match_label} ${item.selection}`);
}
return (
<div className="space-y-4">
<section className="panel-glow rounded-2xl p-5">
<h2 className="dot-matrix text-2xl text-[#7d2a15]"></h2>
<p className="mt-1 text-xs text-[#8a6b58]">{data ? data.total_daily_unit_recommendation.toFixed(2) : '-'}</p>
<p className="mt-2 text-sm text-[#7a5b46]">{targetDate}</p>
<p className="mt-2 text-sm text-[#6f4f3c]">{briefing}</p>
<p className="mt-2 text-xs text-[#8c2f2f]">{selectedCount} </p>
</section>
<section className="panel-glow rounded-2xl p-4">
<p className="dot-matrix text-lg text-[#7d2a15]">Hard Filters</p>
<div className="mt-2 flex flex-wrap gap-2">
<button
type="button"
className={`rounded-full px-3 py-2 text-sm ${
activeTab === 'safe' ? 'bg-[#7d2a15] text-white' : 'bg-white/70 text-[#5f4330]'
}`}
onClick={() => setActiveTab('safe')}
>
Safe Singles
</button>
<button
type="button"
className={`rounded-full px-3 py-2 text-sm ${
activeTab === 'risk' ? 'bg-[#7d2a15] text-white' : 'bg-white/70 text-[#5f4330]'
}`}
onClick={() => setActiveTab('risk')}
>
High-Risk Underdog
</button>
<button
type="button"
className={`rounded-full px-3 py-2 text-sm ${
activeTab === 'parlay' ? 'bg-[#7d2a15] text-white' : 'bg-white/70 text-[#5f4330]'
}`}
onClick={() => setActiveTab('parlay')}
>
Safe Parlays
</button>
<button
type="button"
className={`rounded-full px-3 py-2 text-sm ${
activeTab === 'sgp' ? 'bg-[#7d2a15] text-white' : 'bg-white/70 text-[#5f4330]'
}`}
onClick={() => setActiveTab('sgp')}
>
SGP
</button>
</div>
</section>
{loading ? <p className="text-sm text-[#8a6b58]">...</p> : null}
{error ? <p className="text-sm text-[#8c2f2f]">{error}</p> : null}
<section className="grid gap-4 md:grid-cols-2">
{tabCards[activeTab].map((item) => (
<ActionableBetCard
key={`${item.match_id}-${item.selection}-${item.market_type}`}
item={item}
onAddToSlip={handleAddToSlip}
className="panel-glow"
/>
))}
{!loading && tabCards[activeTab].length === 0 ? (
<p className="panel-glow rounded-2xl p-4 text-sm text-[#7a5b46]"></p>
) : null}
</section>
</div>
);
}