diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 1d9480a..e1e0c2b 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -88,6 +88,16 @@ jobs: cd platform/web npm run lint + - name: Run Frontend Production Build + env: + DATABASE_URL: postgresql://fifa_user:ci-placeholder-db-password@127.0.0.1:5432/fifa2026 + NEXTAUTH_SECRET: ci-placeholder-nextauth-secret + NEXTAUTH_URL: https://2026fifa.wooo.work + ANALYTICS_BACKEND_URL: http://127.0.0.1:8000 + run: | + cd platform/web + npm run build + - name: Validate Docker Compose env: DB_PASSWORD: ci-placeholder-db-password diff --git a/platform/web/Dockerfile b/platform/web/Dockerfile index 094fc83..b51e920 100644 --- a/platform/web/Dockerfile +++ b/platform/web/Dockerfile @@ -3,8 +3,8 @@ FROM node:22-alpine AS deps WORKDIR /app RUN apk add --no-cache openssl -COPY package.json ./ -RUN npm install --legacy-peer-deps +COPY package.json package-lock.json ./ +RUN npm ci --legacy-peer-deps # ── stage 2: build ───────────────────────────────────────────────────────────── FROM node:22-alpine AS builder diff --git a/platform/web/components/HeaderNav.tsx b/platform/web/components/HeaderNav.tsx new file mode 100644 index 0000000..c25b309 --- /dev/null +++ b/platform/web/components/HeaderNav.tsx @@ -0,0 +1,93 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +type NavItem = { + href: string; + label: string; + shortLabel: string; + description: string; +}; + +const PRIMARY_NAV_ITEMS: NavItem[] = [ + { href: '/', label: '主控總覽', shortLabel: '總覽', description: '首頁先看今日推薦與資料健康' }, + { href: '/daily-card', label: '每日作戰室', shortLabel: '作戰室', description: '依日期檢查賽事與推薦' }, + { href: '/matches', label: '完整賽程', shortLabel: '賽程', description: '所有賽事、比分與狀態' }, + { href: '/deep-bet', label: '投注研究', shortLabel: '投注', description: '單關、串關與組合評估' }, + { href: '/odds', label: '盤口監控', shortLabel: '盤口', description: '賠率覆蓋與即時變化' }, + { href: '/sharp-money', label: '聰明錢追蹤', shortLabel: '聰明錢', description: '資金流向與市場偏差' }, +]; + +const RESEARCH_NAV_ITEMS: NavItem[] = [ + { href: '/rlm', label: '反向盤口雷達', shortLabel: '反向盤', description: '少數派資金與賠率逆行' }, + { href: '/models', label: '量化模型', shortLabel: '模型', description: '泊松、EV 與風險校準' }, + { href: '/ml-edge', label: '機器學習邊緣', shortLabel: 'ML', description: '模型機率與市場機率差' }, + { href: '/match-conditions', label: '裁判/天候模型', shortLabel: '天候', description: '場地、熱度與判罰風險' }, + { href: '/props', label: '球員道具盤', shortLabel: '道具', description: '射門、傳球與球員線' }, + { href: '/kelly', label: '凱利配置', shortLabel: '凱利', description: '注碼上限與風險控管' }, + { href: '/backtesting', label: '策略回測', shortLabel: '回測', description: '歷史命中率與收益曲線' }, + { href: '/proof-of-yield', label: '公開收益帳本', shortLabel: '帳本', description: '推薦賽後校準與命中率' }, + { href: '/portfolio', label: '個人組合', shortLabel: '組合', description: '追蹤清單與部位管理' }, +]; + +function isActivePath(pathname: string, href: string): boolean { + if (href === '/') { + return pathname === '/'; + } + return pathname === href || pathname.startsWith(`${href}/`); +} + +function NavPill({ item, active }: { item: NavItem; active: boolean }) { + return ( + + {item.label} + {item.shortLabel} + + ); +} + +export function HeaderNav() { + const pathname = usePathname() || '/'; + const allItems = [...PRIMARY_NAV_ITEMS, ...RESEARCH_NAV_ITEMS]; + const activeItem = allItems.find((item) => isActivePath(pathname, item.href)) ?? PRIMARY_NAV_ITEMS[0]; + + return ( +
+ 2026 世界盃
量化投注研究中心
+
+ 台北時間 (UTC+8) | 台灣量化實戰研究版 +
++ 目前頁面:{activeItem.label} +
+ + + +