From 858d07786304b6dfac68ea012d7ed6e7f18f99d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=87=E6=BA=AF?= Date: Mon, 26 Jan 2026 20:28:41 -0800 Subject: [PATCH 1/3] add qwen stock visualization --- apps/README.md | 20 + .../.env.local.example | 3 + apps/qwen-stock-visualization/.eslintrc.js | 3 + apps/qwen-stock-visualization/.eslintrc.json | 3 + apps/qwen-stock-visualization/README.md | 106 + .../app/about/page.tsx | 69 + .../app/contact/page.tsx | 151 ++ apps/qwen-stock-visualization/app/globals.css | 27 + apps/qwen-stock-visualization/app/layout.tsx | 39 + .../app/login/page.tsx | 50 + .../app/markets/page.tsx | 181 ++ apps/qwen-stock-visualization/app/page.tsx | 27 + .../app/signup/page.tsx | 75 + .../components/AddToWatchlistButton.tsx | 72 + .../components/BootstrapClient.tsx | 12 + .../components/EnhancedDashboard.tsx | 866 ++++++ .../components/Navbar.tsx | 108 + .../components/ProtectedRoute.tsx | 29 + .../components/SimpleAuthForm.tsx | 156 ++ .../components/SimpleAuthProvider.tsx | 87 + .../components/StockChart.tsx | 108 + .../lib/simpleAuth.ts | 190 ++ .../lib/stockDataLayer.ts | 336 +++ .../lib/supabaseClient.ts | 18 + .../mock_server/package-lock.json | 736 ++++++ .../mock_server/package.json | 22 + .../mock_server/server.ts | 164 ++ apps/qwen-stock-visualization/next-env.d.ts | 6 + apps/qwen-stock-visualization/next.config.js | 4 + .../package-lock.json | 2327 +++++++++++++++++ apps/qwen-stock-visualization/package.json | 39 + .../scripts/insertUserData.js | 224 ++ .../scripts/populateMockData.ts | 79 + .../supabase/config.toml | 39 + .../migrations/000001_init_simple_schema.sql | 114 + .../tailwind.config.js | 15 + .../test-db-connection.ts | 60 + apps/qwen-stock-visualization/tsconfig.json | 41 + 38 files changed, 6606 insertions(+) create mode 100644 apps/qwen-stock-visualization/.env.local.example create mode 100644 apps/qwen-stock-visualization/.eslintrc.js create mode 100644 apps/qwen-stock-visualization/.eslintrc.json create mode 100644 apps/qwen-stock-visualization/README.md create mode 100644 apps/qwen-stock-visualization/app/about/page.tsx create mode 100644 apps/qwen-stock-visualization/app/contact/page.tsx create mode 100644 apps/qwen-stock-visualization/app/globals.css create mode 100644 apps/qwen-stock-visualization/app/layout.tsx create mode 100644 apps/qwen-stock-visualization/app/login/page.tsx create mode 100644 apps/qwen-stock-visualization/app/markets/page.tsx create mode 100644 apps/qwen-stock-visualization/app/page.tsx create mode 100644 apps/qwen-stock-visualization/app/signup/page.tsx create mode 100644 apps/qwen-stock-visualization/components/AddToWatchlistButton.tsx create mode 100644 apps/qwen-stock-visualization/components/BootstrapClient.tsx create mode 100644 apps/qwen-stock-visualization/components/EnhancedDashboard.tsx create mode 100644 apps/qwen-stock-visualization/components/Navbar.tsx create mode 100644 apps/qwen-stock-visualization/components/ProtectedRoute.tsx create mode 100644 apps/qwen-stock-visualization/components/SimpleAuthForm.tsx create mode 100644 apps/qwen-stock-visualization/components/SimpleAuthProvider.tsx create mode 100644 apps/qwen-stock-visualization/components/StockChart.tsx create mode 100644 apps/qwen-stock-visualization/lib/simpleAuth.ts create mode 100644 apps/qwen-stock-visualization/lib/stockDataLayer.ts create mode 100644 apps/qwen-stock-visualization/lib/supabaseClient.ts create mode 100644 apps/qwen-stock-visualization/mock_server/package-lock.json create mode 100644 apps/qwen-stock-visualization/mock_server/package.json create mode 100644 apps/qwen-stock-visualization/mock_server/server.ts create mode 100644 apps/qwen-stock-visualization/next-env.d.ts create mode 100644 apps/qwen-stock-visualization/next.config.js create mode 100644 apps/qwen-stock-visualization/package-lock.json create mode 100644 apps/qwen-stock-visualization/package.json create mode 100644 apps/qwen-stock-visualization/scripts/insertUserData.js create mode 100644 apps/qwen-stock-visualization/scripts/populateMockData.ts create mode 100644 apps/qwen-stock-visualization/supabase/config.toml create mode 100644 apps/qwen-stock-visualization/supabase/migrations/000001_init_simple_schema.sql create mode 100644 apps/qwen-stock-visualization/tailwind.config.js create mode 100644 apps/qwen-stock-visualization/test-db-connection.ts create mode 100644 apps/qwen-stock-visualization/tsconfig.json diff --git a/apps/README.md b/apps/README.md index a551897..ec12636 100644 --- a/apps/README.md +++ b/apps/README.md @@ -126,6 +126,25 @@ A product promotional video creation tool that combines the power of Remotion (p --- +### 6. StockVisualization — For Stock Traders + +> **Tagline**: *Visualize stock market trends* + +An AI-powered stock visualization tool built on RDS Supabase, featuring interactive charts and a paper trading simulator to track real-time portfolio performance. + +**Key Features**: +- Data-Driven Stock Visualization +- Interactive Portfolio Simulator +- Real-time Portfolio Performance +- Supabase-Powered Data Backend + +**Use Cases**: +- Visualize stock market trends +- Track portfolio performance in real-time +- Check stock analysis reports + +**Tech Stack**: Next.js, RDS Supabase, Build-in Authentication + ## Getting Started ### Prerequisites @@ -207,6 +226,7 @@ We welcome contributions! Each app has its own development guidelines in its res - [ ] **CoWork** v1.0 - Email assistant - [ ] **NoteGenius** v1.0 - Blog writing mode - [ ] **PromoStudio** v1.0 - Basic video generation +- [ ] **StockVisualization** v1.0 - Visualize stock market trends ## License diff --git a/apps/qwen-stock-visualization/.env.local.example b/apps/qwen-stock-visualization/.env.local.example new file mode 100644 index 0000000..abb7663 --- /dev/null +++ b/apps/qwen-stock-visualization/.env.local.example @@ -0,0 +1,3 @@ +# Supabase Environment Variables +NEXT_PUBLIC_SUPABASE_URL="" +NEXT_PUBLIC_SUPABASE_ANON_KEY="" \ No newline at end of file diff --git a/apps/qwen-stock-visualization/.eslintrc.js b/apps/qwen-stock-visualization/.eslintrc.js new file mode 100644 index 0000000..f117e74 --- /dev/null +++ b/apps/qwen-stock-visualization/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['next/core-web-vitals'], +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/.eslintrc.json b/apps/qwen-stock-visualization/.eslintrc.json new file mode 100644 index 0000000..0e81f9b --- /dev/null +++ b/apps/qwen-stock-visualization/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/README.md b/apps/qwen-stock-visualization/README.md new file mode 100644 index 0000000..b828527 --- /dev/null +++ b/apps/qwen-stock-visualization/README.md @@ -0,0 +1,106 @@ +# 股票数据实时可视化系统 + +这是一个直观、交互性强且个性化的股票数据可视化与模拟投资平台。 + +## 功能特性 + +- 用户身份认证(注册、登录) +- 个性化股票仪表盘 +- 实时股票价格可视化 +- 自选股管理 +- 模拟交易功能 +- 投资组合管理 +- 收益追踪与分析 + +## 技术栈 + +- Next.js 14+ +- React +- Supabase (PostgreSQL数据库、身份验证) +- Tailwind CSS +- Recharts (数据可视化) + +## 快速开始 + +### 1. 环境配置 + +首先,您需要在 Supabase 上创建一个项目并获取以下凭据: + +- `NEXT_PUBLIC_SUPABASE_URL` - 你的 Supabase 项目 URL +- `NEXT_PUBLIC_SUPABASE_ANON_KEY` - 你的 Supabase 匿名密钥 + +### 2. 安装依赖 + +```bash +npm install +``` + +### 3. 环境变量配置 + +复制 `.env.local.example` 文件并填入您的 Supabase 凭据: + +```bash +cp .env.local.example .env.local +# 编辑 .env.local 文件并添加您的 Supabase 凭据 +``` + +### 4. 运行开发服务器 + +```bash +npm run dev +``` + +应用将在 http://localhost:3000 上启动。 + +### 5. 填充模拟数据(可选) + +如果您想用模拟数据测试应用,请运行: + +```bash +npm run populate-mock-data +``` + +注意:运行此命令前,请确保您已在 `.env.local` 中设置了正确的 Supabase 凭据。 + +## 项目结构 + +``` +supabase-demo-stock/ +├── app/ # Next.js 14+ App Router 页面 +├── components/ # React 组件 +├── lib/ # 工具函数和数据访问层 +├── scripts/ # 实用脚本 +├── supabase/ # Supabase 配置和迁移 +└── ... +``` + +## 数据库模式 + +系统包含以下核心表: + +- `stocks` - 股票基础信息 +- `stock_prices` - 股票实时价格 +- `profiles` - 用户资料 +- `watchlists` - 自选股列表 +- `portfolios` - 模拟投资组合 +- `portfolio_positions` - 持仓信息 +- `portfolio_transactions` - 交易记录 + +## 安全特性 + +- 使用 Supabase 行级安全 (RLS) 保护用户数据 +- 用户只能访问自己的数据 +- 所有数据库操作都经过适当验证 + +## 部署 + +要构建生产版本: + +```bash +npm run build +npm start +``` + +或者使用 Vercel 一键部署: + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/your-repo/stock-visualization-demo) \ No newline at end of file diff --git a/apps/qwen-stock-visualization/app/about/page.tsx b/apps/qwen-stock-visualization/app/about/page.tsx new file mode 100644 index 0000000..09db93c --- /dev/null +++ b/apps/qwen-stock-visualization/app/about/page.tsx @@ -0,0 +1,69 @@ +import Link from 'next/link'; + +export default function AboutPage() { + return ( +
+
+
+

关于我们

+

+ 股票数据实时可视化系统是一个直观、交互性强且个性化的股票数据可视化与模拟投资平台。 +

+

+ 我们的平台提供以下功能: +

+
    +
  • +
    + + + +
    +
    +
    实时股票数据
    +

    获取最新的股票价格和市场数据

    +
    +
  • +
  • +
    + + + +
    +
    +
    交互式图表
    +

    通过交互式图表分析股票趋势

    +
    +
  • +
  • +
    + + + +
    +
    +
    投资组合管理
    +

    创建和管理多个投资组合

    +
    +
  • +
  • +
    + + + +
    +
    +
    自选股跟踪
    +

    跟踪您关注的股票

    +
    +
  • +
+
+ 返回首页 + 联系我们 +
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/app/contact/page.tsx b/apps/qwen-stock-visualization/app/contact/page.tsx new file mode 100644 index 0000000..e0b0ed7 --- /dev/null +++ b/apps/qwen-stock-visualization/app/contact/page.tsx @@ -0,0 +1,151 @@ +'use client'; + +import { useState } from 'react'; + +export default function ContactPage() { + const [formData, setFormData] = useState({ + name: '', + email: '', + subject: '', + message: '' + }); + const [submitted, setSubmitted] = useState(false); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + console.log('提交的表单数据:', formData); + setSubmitted(true); + + // 重置表单 + setTimeout(() => { + setFormData({ + name: '', + email: '', + subject: '', + message: '' + }); + setSubmitted(false); + }, 3000); + }; + + return ( +
+
+
+

联系我们

+

+ 如果您有任何问题或建议,请随时与我们联系 +

+ + {submitted && ( +
+ 感谢您的消息! 我们已收到您的信息,会尽快回复您。 + +
+ )} + +
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ +
+
+
+ + + +
+
邮箱
+

support@stockdashboard.com

+
+
+
+ + + +
+
地址
+

中国上海市浦东新区
世纪大道1001号

+
+
+
+ + + + +
+
工作时间
+

周一至周五: 9:00 AM - 6:00 PM
周末: 关闭

+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/app/globals.css b/apps/qwen-stock-visualization/app/globals.css new file mode 100644 index 0000000..8bf6eac --- /dev/null +++ b/apps/qwen-stock-visualization/app/globals.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + transparent + ) + rgb(var(--background-start-rgb)); +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/app/layout.tsx b/apps/qwen-stock-visualization/app/layout.tsx new file mode 100644 index 0000000..51b88b2 --- /dev/null +++ b/apps/qwen-stock-visualization/app/layout.tsx @@ -0,0 +1,39 @@ +import './globals.css'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import { SimpleAuthProvider } from '@/components/SimpleAuthProvider'; +import Navbar from '@/components/Navbar'; +import BootstrapClient from '@/components/BootstrapClient'; + +const inter = Inter({ subsets: ['latin'] }); + +export const metadata: Metadata = { + title: '股票数据实时可视化系统', + description: '一个直观、交互性强且个性化的股票数据可视化与模拟投资平台', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + +
+ {children} +
+
+
+

© {new Date().getFullYear()} 股票数据实时可视化系统 - 专业的投资分析工具

+
+
+
+ + + + ); +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/app/login/page.tsx b/apps/qwen-stock-visualization/app/login/page.tsx new file mode 100644 index 0000000..335f2df --- /dev/null +++ b/apps/qwen-stock-visualization/app/login/page.tsx @@ -0,0 +1,50 @@ +import Link from 'next/link'; + +export default function LoginPage() { + return ( +
+
+
+
+
+

登录到您的账户

+
+
+
+
+ + +
+
+ + +
+
+ +
+
+

+ 还没有账户? 立即注册 +

+

+ 忘记密码? +

+
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/app/markets/page.tsx b/apps/qwen-stock-visualization/app/markets/page.tsx new file mode 100644 index 0000000..bd19a00 --- /dev/null +++ b/apps/qwen-stock-visualization/app/markets/page.tsx @@ -0,0 +1,181 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useSimpleAuth } from '@/components/SimpleAuthProvider'; +import { getAllStocks, getLatestPrice, getUserWatchlist } from '@/lib/stockDataLayer'; +import AddToWatchlistButton from '@/components/AddToWatchlistButton'; + +export default function MarketsPage() { + const { user } = useSimpleAuth(); + const [marketData, setMarketData] = useState([]); + const [watchlistStockIds, setWatchlistStockIds] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchMarketData = async () => { + try { + // 获取所有股票 - 使用匿名令牌也可以获取公开的股票信息 + const stocks = await getAllStocks(); + + // 获取每只股票的最新价格 + const stocksWithLatestPrices = await Promise.all( + stocks.map(async (stock: any) => { + const latestPrice = await getLatestPrice(stock.id); + return { + ...stock, + latestPrice: latestPrice || null + }; + }) + ); + + setMarketData(stocksWithLatestPrices); + } catch (error) { + console.error('Error fetching market data:', error); + } finally { + setLoading(false); + } + }; + + // 首次加载数据 + fetchMarketData(); + + // 设置定时器,每0.5秒刷新一次市场数据 + const interval = setInterval(fetchMarketData, 500); + + // 清理定时器 + return () => clearInterval(interval); + }, []); + + useEffect(() => { + // 获取用户的自选股列表 + const fetchWatchlist = async () => { + if (user) { + try { + const watchlist = await getUserWatchlist(user.id, localStorage.getItem('auth_token')); + const stockIds = watchlist.map((item: any) => item.stock_id); + setWatchlistStockIds(stockIds); + } catch (error) { + console.error('Error fetching watchlist:', error); + } + } + }; + + fetchWatchlist(); + }, [user]); + + const handleWatchlistAdded = () => { + // 重新获取自选股列表以更新状态 + if (user) { + getUserWatchlist(user.id, localStorage.getItem('auth_token')) + .then(watchlist => { + const stockIds = watchlist.map((item: any) => item.stock_id); + setWatchlistStockIds(stockIds); + }) + .catch(error => { + console.error('Error updating watchlist after add:', error); + }); + } + }; + + const handleWatchlistRemoved = () => { + // 重新获取自选股列表以更新状态 + if (user) { + getUserWatchlist(user.id, localStorage.getItem('auth_token')) + .then(watchlist => { + const stockIds = watchlist.map((item: any) => item.stock_id); + setWatchlistStockIds(stockIds); + }) + .catch(error => { + console.error('Error updating watchlist after remove:', error); + }); + } + }; + + return ( +
+
+
+

市场数据 - 实时股票价格

+ + {loading ? ( +
+
+ Loading... +
+
+ ) : marketData.length === 0 ? ( +
+

暂无市场数据

+
+ ) : ( +
+
+
+ + + + + + + + + + + + + + + {marketData.map((stock: any) => { + const isInWatchlist = watchlistStockIds.includes(stock.id); + return ( + + + + + + + + + + + ); + })} + +
股票代码公司名称最新价格涨跌额涨跌幅交易所时间操作
{stock.symbol}{stock.name} + {stock.latestPrice + ? `$${parseFloat(stock.latestPrice.price).toFixed(2)}` + : 'N/A'} + = 0 ? 'text-success' : 'text-danger'}> + {stock.latestPrice + ? `${stock.latestPrice.change_amount >= 0 ? '+' : ''}${parseFloat(stock.latestPrice.change_amount).toFixed(2)}` + : 'N/A'} + = 0 ? 'text-success' : 'text-danger'}> + {stock.latestPrice + ? `${stock.latestPrice.change_percent >= 0 ? '+' : ''}${parseFloat(stock.latestPrice.change_percent).toFixed(2)}%` + : 'N/A'} + {stock.exchange} + {stock.latestPrice + ? new Date(stock.latestPrice.timestamp).toLocaleString('zh-CN') + : 'N/A'} + + {user ? ( + + ) : ( + - + )} +
+
+
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/app/page.tsx b/apps/qwen-stock-visualization/app/page.tsx new file mode 100644 index 0000000..4a1c34a --- /dev/null +++ b/apps/qwen-stock-visualization/app/page.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import SimpleAuthForm from '@/components/SimpleAuthForm'; +import EnhancedDashboard from '@/components/EnhancedDashboard'; +import { useSimpleAuth } from '@/components/SimpleAuthProvider'; + +export default function HomePage() { + const { user, isLoading } = useSimpleAuth(); + + if (isLoading) { + // Loading state while checking auth + return ( +
+
+ Loading... +
+
+ ); + } + + return ( +
+ {user ? : } +
+ ); +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/app/signup/page.tsx b/apps/qwen-stock-visualization/app/signup/page.tsx new file mode 100644 index 0000000..1013971 --- /dev/null +++ b/apps/qwen-stock-visualization/app/signup/page.tsx @@ -0,0 +1,75 @@ +import Link from 'next/link'; + +export default function SignupPage() { + return ( +
+
+
+
+
+

创建新账户

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+

+ 已有账户? 立即登录 +

+
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/components/AddToWatchlistButton.tsx b/apps/qwen-stock-visualization/components/AddToWatchlistButton.tsx new file mode 100644 index 0000000..e123514 --- /dev/null +++ b/apps/qwen-stock-visualization/components/AddToWatchlistButton.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { addToWatchlist, removeFromWatchlist } from '@/lib/stockDataLayer'; + +export default function AddToWatchlistButton({ + userId, + stockId, + isInWatchlist, + onAdded, + onRemoved +}: { + userId: number; + stockId: number; + isInWatchlist: boolean; + onAdded: () => void; + onRemoved: () => void; +}) { + const [loading, setLoading] = useState(false); + const [isAdded, setIsAdded] = useState(isInWatchlist); + + useEffect(() => { + setIsAdded(isInWatchlist); + }, [isInWatchlist]); + + const handleClick = async () => { + if (loading) return; // 防止重复点击 + + setLoading(true); + try { + let success = false; + if (isAdded) { + // 如果已在自选股中,则移除 + success = await removeFromWatchlist(stockId, localStorage.getItem('auth_token')); + if (success) { + setIsAdded(false); + onRemoved(); // 触发父组件的更新回调 + } + } else { + // 如果不在自选股中,则添加 + success = await addToWatchlist(userId, stockId, localStorage.getItem('auth_token')); + if (success) { + setIsAdded(true); + onAdded(); // 触发父组件的更新回调 + } + } + } catch (error) { + console.error('Error updating watchlist:', error); + } finally { + setLoading(false); + } + }; + + return ( + + ); +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/components/BootstrapClient.tsx b/apps/qwen-stock-visualization/components/BootstrapClient.tsx new file mode 100644 index 0000000..95b107a --- /dev/null +++ b/apps/qwen-stock-visualization/components/BootstrapClient.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { useEffect } from 'react'; + +export default function BootstrapClient() { + useEffect(() => { + // 动态导入Bootstrap JS以避免服务端渲染错误 + import('bootstrap/dist/js/bootstrap.bundle.min.js'); + }, []); + + return null; +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/components/EnhancedDashboard.tsx b/apps/qwen-stock-visualization/components/EnhancedDashboard.tsx new file mode 100644 index 0000000..a4001e8 --- /dev/null +++ b/apps/qwen-stock-visualization/components/EnhancedDashboard.tsx @@ -0,0 +1,866 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useSimpleAuth } from '@/components/SimpleAuthProvider'; +import { getAllStocks, getUserWatchlist, addToWatchlist, removeFromWatchlist, getUserPortfolios, createPortfolio, deletePortfolio, getPortfolioPositions, getPortfolioTransactions, executeTrade, getHistoricalPrices, getLatestPrice } from '@/lib/stockDataLayer'; +import StockChart from '@/components/StockChart'; + +export default function EnhancedDashboard() { + const { user, signOut } = useSimpleAuth(); + const [profile, setProfile] = useState(null); + const [watchlist, setWatchlist] = useState([]); + const [stocks, setStocks] = useState([]); + const [portfolios, setPortfolios] = useState([]); + const [selectedPortfolio, setSelectedPortfolio] = useState(null); + const [positions, setPositions] = useState([]); + const [transactions, setTransactions] = useState([]); + const [selectedStock, setSelectedStock] = useState(null); + const [historicalData, setHistoricalData] = useState([]); + const [tradeForm, setTradeForm] = useState({ + quantity: 1, + price: 0, // 默认价格为0,稍后会根据选中的股票更新 + type: 'buy' as 'buy' | 'sell' + }); + const [showTradePanel, setShowTradePanel] = useState(true); // 控制交易面板显示状态 + const [newPortfolioName, setNewPortfolioName] = useState(''); + const [initialBalance, setInitialBalance] = useState(100000); + const [loading, setLoading] = useState(true); + + // 添加定时刷新功能 + useEffect(() => { + if (user) { + fetchData(); + + // 设置定时器,每0.5秒刷新一次数据 + const interval = setInterval(() => { + // 只刷新持仓数据和相关价格,避免刷新全部数据影响性能 + if (selectedPortfolio) { + loadPortfolioData({}); // 正常刷新,不使用价格覆盖 + } + if (selectedStock) { + loadHistoricalData(); + } + }, 500); // 每500毫秒(0.5秒)刷新一次 + + // 清理定时器 + return () => clearInterval(interval); + } + }, [user]); // 只监听user,避免依赖数组长度变化 + + useEffect(() => { + if (selectedPortfolio) { + loadPortfolioData(); + } + }, [selectedPortfolio]); + + useEffect(() => { + if (selectedStock) { + loadHistoricalData(); + // 当选中股票时,获取最新价格并更新交易表单 + const fetchCurrentPrice = async () => { + try { + const latestPrice = await getLatestPrice(selectedStock.id, localStorage.getItem('auth_token')); + if (latestPrice) { + setTradeForm(prev => ({ + ...prev, + price: parseFloat(latestPrice.price) + })); + } + } catch (error) { + console.error('Error fetching current price:', error); + } + }; + fetchCurrentPrice(); + } + }, [selectedStock]); + + const fetchData = async () => { + setLoading(true); + + try { + // 设置用户资料为简单用户信息 + setProfile(user); + + // 获取所有数据 + await Promise.all([ + fetchWatchlist(), + fetchStocks(), + fetchPortfolios() + ]); + } catch (error) { + console.error('Error fetching data:', error); + } finally { + setLoading(false); + } + }; + + const fetchWatchlist = async () => { + try { + // 使用简单用户ID和访问令牌获取自选股 + const watchlistData = await getUserWatchlist(user?.id, localStorage.getItem('auth_token')); + setWatchlist(watchlistData); + } catch (error) { + console.error('Error fetching watchlist:', error); + } + }; + + const fetchStocks = async () => { + try { + // 使用访问令牌获取股票数据 + const stocksData = await getAllStocks(localStorage.getItem('auth_token')); + setStocks(stocksData); + } catch (error) { + console.error('Error fetching stocks:', error); + } + }; + + const fetchPortfolios = async () => { + try { + // 使用简单用户ID和访问令牌获取投资组合 + const portfoliosData = await getUserPortfolios(user?.id, localStorage.getItem('auth_token')); + + // 为每个投资组合获取持仓数据并计算总价值 + const portfoliosWithReturns = await Promise.all(portfoliosData.map(async (portfolio) => { + // 获取该投资组合的持仓 + const portfolioPositions = await getPortfolioPositions(portfolio.id, localStorage.getItem('auth_token')); + + // 获取每个持仓的最新价格并计算总市值 + let holdingsValue = 0; + for (const position of portfolioPositions) { + try { + const latestPrice = await getLatestPrice(position.stock_id, localStorage.getItem('auth_token')); + if (latestPrice) { + holdingsValue += position.quantity * parseFloat(latestPrice.price); + } + } catch (error) { + console.warn(`无法获取股票 ${position.stock_id} 的最新价格:`, error); + // 如果无法获取最新价格,使用平均成本作为当前价格 + holdingsValue += position.quantity * position.avg_cost; + } + } + + // 计算总资产和收益率 + const totalAssets = portfolio.current_balance + holdingsValue; + const totalProfitLoss = totalAssets - portfolio.initial_balance; + const returnRate = portfolio.initial_balance ? + (totalProfitLoss / portfolio.initial_balance) * 100 : 0; + + return { + ...portfolio, + holdings_value: holdingsValue, + total_assets: totalAssets, + return_rate: returnRate + }; + })); + + setPortfolios(portfoliosWithReturns); + + // 设置默认选择第一个投资组合 + if (portfoliosWithReturns.length > 0 && !selectedPortfolio) { + setSelectedPortfolio(portfoliosWithReturns[0]); + } + } catch (error) { + console.error('Error fetching portfolios:', error); + } + }; + + const loadPortfolioData = async (priceOverrides = {}) => { + if (!selectedPortfolio) return; + + try { + const [positionsData, transactionsData] = await Promise.all([ + getPortfolioPositions(selectedPortfolio.id, localStorage.getItem('auth_token')), + getPortfolioTransactions(selectedPortfolio.id, localStorage.getItem('auth_token')) + ]); + + // 对于每个持仓股票,获取其最新价格 + const positionsWithPrices = await Promise.all( + positionsData.map(async (position) => { + try { + // 检查是否有价格覆盖(用于交易后立即显示,避免价格波动影响) + if (priceOverrides[position.stock_id]) { + return { + ...position, + latest_price: priceOverrides[position.stock_id] + }; + } + + const latestPrice = await getLatestPrice(position.stock_id, localStorage.getItem('auth_token')); + if (latestPrice) { + // 将最新价格添加到持仓数据中 + return { + ...position, + latest_price: parseFloat(latestPrice.price) + }; + } + } catch (error) { + console.warn(`无法获取股票 ${position.stock_id} 的最新价格:`, error); + } + // 如果无法获取最新价格,使用平均成本作为当前价格 + return { + ...position, + latest_price: position.avg_cost + }; + }) + ); + + setPositions(positionsWithPrices); + setTransactions(transactionsData); + } catch (error) { + console.error('Error loading portfolio data:', error); + } + }; + + const loadHistoricalData = async () => { + if (!selectedStock) return; + + try { + // 使用访问令牌获取历史价格数据 + const data = await getHistoricalPrices(selectedStock.id, 30, localStorage.getItem('auth_token')); // 最近30天数据 + + // 格式化数据以适应图表 + const formattedData = data.map((item: any) => ({ + timestamp: item.timestamp, + price: parseFloat(item.price), + volume: item.volume || 0 + })); + + setHistoricalData(formattedData); + } catch (error) { + console.error('Error loading historical data:', error); + } + }; + + const handleAddToWatchlist = async (stockId: number) => { + try { + // 使用简单用户ID和访问令牌添加到自选股 + const success = await addToWatchlist(user?.id, stockId, localStorage.getItem('auth_token')); + if (success) { + fetchWatchlist(); // 刷新自选股 + } + } catch (error) { + console.error('Error adding to watchlist:', error); + } + }; + + const handleRemoveFromWatchlist = async (watchlistId: number) => { + try { + // 使用访问令牌从自选股移除 + const success = await removeFromWatchlist(watchlistId, localStorage.getItem('auth_token')); + if (success) { + fetchWatchlist(); // 刷新自选股 + } + } catch (error) { + console.error('Error removing from watchlist:', error); + } + }; + + const handleCreatePortfolio = async () => { + if (!newPortfolioName.trim()) return; + + try { + // 使用简单用户ID和访问令牌创建投资组合 + const newPortfolio = await createPortfolio(user?.id, newPortfolioName, initialBalance, localStorage.getItem('auth_token')); + if (newPortfolio) { + setPortfolios([...portfolios, newPortfolio]); + setSelectedPortfolio(newPortfolio); + setNewPortfolioName(''); + setInitialBalance(100000); + } + } catch (error) { + console.error('Error creating portfolio:', error); + } + }; + + const handleDeletePortfolio = async (portfolioId: number) => { + if (!window.confirm('确定要删除这个投资组合吗?此操作不可撤销,将同时删除所有相关持仓和交易记录。')) { + return; + } + + try { + // 使用访问令牌删除投资组合 + const success = await deletePortfolio(portfolioId, localStorage.getItem('auth_token')); + + if (success) { + // 从状态中移除已删除的投资组合 + const updatedPortfolios = portfolios.filter(portfolio => portfolio.id !== portfolioId); + setPortfolios(updatedPortfolios); + + // 如果删除的是当前选中的投资组合,则选择第一个投资组合或设为null + if (selectedPortfolio?.id === portfolioId) { + setSelectedPortfolio(updatedPortfolios.length > 0 ? updatedPortfolios[0] : null); + } + + console.log('投资组合删除成功'); + } + } catch (error) { + console.error('删除投资组合时出错:', error); + alert('删除投资组合失败,请重试。'); + } + }; + + const toggleTradePanel = () => { + setShowTradePanel(!showTradePanel); + }; + + const handleExecuteTrade = async () => { + if (!selectedPortfolio || !selectedStock || tradeForm.quantity <= 0 || tradeForm.price <= 0) { + alert('请填写正确的交易信息:数量和价格必须大于0'); + return; + } + + // 如果是卖出操作,检查用户是否有足够的持仓 + if (tradeForm.type === 'sell') { + const currentHoldings = positions.find(pos => pos.stock_id === selectedStock.id); + if (!currentHoldings || currentHoldings.quantity < tradeForm.quantity) { + alert(`卖出失败:您没有足够的 ${selectedStock.symbol} 股票。当前持仓: ${currentHoldings ? currentHoldings.quantity : 0} 股`); + return; + } + } + + try { + // 使用访问令牌执行交易 + const tradeResult = await executeTrade( + selectedPortfolio.id, + selectedStock.id, + tradeForm.type, + tradeForm.quantity, + tradeForm.price, + localStorage.getItem('auth_token') + ); + + // 交易完成后,刷新整个页面以确保数据完全同步 + window.location.reload(); + + // 重置表单,但保留当前市场价格 + // 使用最新的历史数据中的价格作为当前市场价格 + const currentPrice = historicalData.length > 0 + ? parseFloat(historicalData[historicalData.length - 1].price) + : 0; + setTradeForm({ + quantity: 1, + price: currentPrice, + type: 'buy' + }); + } catch (error) { + console.error('Error executing trade:', error); + alert(`交易失败: ${error}`); + } + }; + + if (!user) { + return ( +
+
+
+

请先登录

+
+
+
+ ); + } + + return ( +
+ {/* 注意:欢迎信息现在在顶部的Navbar中显示,此处不再重复 */} + +
+ {/* 左侧栏 - 自选股和投资组合管理 */} +
+
+ {/* 自选股 */} +
+
+
我的自选股
+
+ + + +
+
+
+ {loading ? ( +
+
+ Loading... +
+
+ ) : watchlist.length === 0 ? ( +
+ + + +

暂无自选股

+
+ ) : ( +
    + {watchlist.map((item) => ( +
  • setSelectedStock(item.stocks)} + > +
    +
    {item.stocks.symbol}
    + {item.stocks.name} +
    + +
  • + ))} +
+ )} +
+
+
+ +
+ {/* 投资组合管理 */} +
+
+
我的投资组合
+
+ + + + +
+
+ + {/* 创建新投资组合 */} +
+
创建新投资组合
+
+ setNewPortfolioName(e.target.value)} + placeholder="投资组合名称" + className="form-control" + /> +
+
+ setInitialBalance(Number(e.target.value))} + placeholder="初始资金" + className="form-control" + /> +
+ +
+ + {/* 投资组合列表 */} +
+
现有投资组合
+
+ {portfolios.map((portfolio) => ( +
+
setSelectedPortfolio(portfolio)} + > +
{portfolio.name}
+ ${portfolio.current_balance != null ? portfolio.current_balance.toFixed(2) : '0.00'} +
+
+
+
${portfolio.current_balance != null ? portfolio.current_balance.toFixed(2) : '0.00'}
+ = 0 ? 'text-success' : 'text-danger'}> + {`${portfolio.return_rate != null ? portfolio.return_rate.toFixed(2) : '0.00'}%`} + +
+ +
+
+ ))} +
+
+
+
+
+ + {/* 右侧栏 - 图表和交易面板 */} +
+ {/* 股票图表 */} + {selectedStock && historicalData.length > 0 && ( +
+
+ +
+
+ )} + + {/* 交易面板 */} + {selectedPortfolio && selectedStock && showTradePanel && ( +
+
+
+ 交易 - {selectedStock.symbol} ({selectedStock.name}) +
+ +
+
+
+ {/* 交易表单 */} +
+
+ + + + 执行交易 +
+
+ +
+
+ setTradeForm({...tradeForm, type: 'buy'})} + /> + +
+
+ setTradeForm({...tradeForm, type: 'sell'})} + /> + +
+
+
+ +
+ + setTradeForm({...tradeForm, quantity: parseInt(e.target.value) || 0})} + min="1" + className="form-control" + /> +
+ +
+ + setTradeForm({...tradeForm, price: parseFloat(e.target.value) || 0})} + className="form-control" + /> +
+ +
+ +
+ ${(tradeForm.quantity * tradeForm.price).toFixed(2)} +
+
+ + +
+ + {/* 当前持仓 */} +
+
+ + + + 当前持仓 +
+ {positions.length > 0 ? ( +
+ {positions + .filter(pos => pos.stock_id === selectedStock.id) + .map((position) => { + const currentPrice = position.latest_price; + const currentValue = position.quantity * currentPrice; + const costValue = position.quantity * position.avg_cost; + const profitLoss = currentValue - costValue; + const profitLossPercentage = costValue !== 0 ? (profitLoss / costValue) * 100 : 0; + + return ( +
+
+
+
数量:
+
{position.quantity}
+
+
+
平均成本:
+
${position.avg_cost != null ? position.avg_cost.toFixed(2) : '0.00'}
+
+
+
当前价值:
+
${currentValue != null ? currentValue.toFixed(2) : '0.00'}
+
+
+
盈亏:
+
= 0 ? 'text-success' : 'text-danger' + }`}> + ${profitLoss != null ? profitLoss.toFixed(2) : '0.00'} (${profitLossPercentage != null ? profitLossPercentage.toFixed(2) : '0.00'}%) +
+
+
+
+ ); + })} +
+ ) : ( +
+ + + +

暂无持仓

+
+ )} +
+
+
+
+ )} + {!showTradePanel && selectedPortfolio && selectedStock && ( +
+
+
+ 交易 - {selectedStock.symbol} ({selectedStock.name}) +
+ +
+
+ )} + + {/* 持仓汇总 */} + {selectedPortfolio && ( +
+
+ + + +
持仓汇总 - {selectedPortfolio.name}
+
+
+
+
+
+
+

初始资金

+

${selectedPortfolio.initial_balance != null ? selectedPortfolio.initial_balance.toFixed(2) : '0.00'}

+
+
+
+
+
+
+

投资组合余额

+

${selectedPortfolio.current_balance != null ? selectedPortfolio.current_balance.toFixed(2) : '0.00'}

+
+
+
+
+
+
+

持仓总额

+

+ ${positions.reduce((sum, position) => { + return sum + (position.quantity * (position.latest_price != null ? position.latest_price : 0)); + }, 0).toFixed(2)} +

+
+
+
+
+
+
+

总盈亏

+ {(() => { + // 计算各项指标 + const initialBalance = selectedPortfolio.initial_balance; // 初始资金 + const currentBalance = selectedPortfolio.current_balance; // 投资组合余额 + const holdingsValue = positions.reduce((sum, position) => { // 持仓总额 + return sum + (position.quantity * position.latest_price); + }, 0); + + // 总盈亏 = 投资组合余额 + 持仓总额 - 初始资金 + const totalProfitLoss = currentBalance + holdingsValue - initialBalance; + + return ( +

= 0 + ? 'text-success' + : 'text-danger' + }`}> + ${totalProfitLoss != null ? totalProfitLoss.toFixed(2) : '0.00'} +
+ + ({totalProfitLoss != null && initialBalance != null ? (((totalProfitLoss / initialBalance) * 100).toFixed(2)) : '0.00'}%) + +

+ ); + })()} +
+
+
+
+ +
详细持仓
+ {positions.length > 0 ? ( +
+ + + + + + + + + + + + + + {positions.map((position) => { + const currentPrice = position.latest_price; + const currentValue = position.quantity * currentPrice; + const costValue = position.quantity * position.avg_cost; + const profitLoss = currentValue - costValue; + const profitLossPercentage = costValue !== 0 ? (profitLoss / costValue) * 100 : 0; + + return ( + + + + + + + + + + ); + })} + +
股票数量平均成本当前价格当前价值盈亏操作
+
{position.stocks.symbol}
+ {position.stocks.name} +
{position.quantity}${position.avg_cost != null ? position.avg_cost.toFixed(2) : '0.00'} + ${currentPrice != null ? currentPrice.toFixed(2) : '0.00'} + ${currentValue != null ? currentValue.toFixed(2) : '0.00'}= 0 ? 'text-success' : 'text-danger' + }`}> + ${profitLoss != null ? profitLoss.toFixed(2) : '0.00'}
+ (${profitLossPercentage != null ? profitLossPercentage.toFixed(2) : '0.00'}%) +
+
+ + +
+
+
+ ) : ( +
+ + + +

暂无持仓记录

+
+ )} +
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/components/Navbar.tsx b/apps/qwen-stock-visualization/components/Navbar.tsx new file mode 100644 index 0000000..3b971a5 --- /dev/null +++ b/apps/qwen-stock-visualization/components/Navbar.tsx @@ -0,0 +1,108 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { useSimpleAuth } from '@/components/SimpleAuthProvider'; + +const Navbar = () => { + const pathname = usePathname(); + const { user, signOut } = useSimpleAuth(); + + return ( + + ); +}; + +export default Navbar; \ No newline at end of file diff --git a/apps/qwen-stock-visualization/components/ProtectedRoute.tsx b/apps/qwen-stock-visualization/components/ProtectedRoute.tsx new file mode 100644 index 0000000..47d1824 --- /dev/null +++ b/apps/qwen-stock-visualization/components/ProtectedRoute.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { useSimpleAuth } from '@/components/SimpleAuthProvider'; +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +type ProtectedRouteProps = { + children: React.ReactNode; +}; + +export default function ProtectedRoute({ children }: ProtectedRouteProps) { + const { user, isLoading } = useSimpleAuth(); + const router = useRouter(); + + useEffect(() => { + if (!isLoading && !user) { + // 如果用户未登录且不再加载中,则重定向到登录页面 + router.push('/'); + } + }, [user, isLoading, router]); + + // 如果正在加载或用户已登录,则显示子组件 + if (isLoading || user) { + return <>{children}; + } + + // 否则返回空或加载指示器 + return null; +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/components/SimpleAuthForm.tsx b/apps/qwen-stock-visualization/components/SimpleAuthForm.tsx new file mode 100644 index 0000000..91f0acc --- /dev/null +++ b/apps/qwen-stock-visualization/components/SimpleAuthForm.tsx @@ -0,0 +1,156 @@ +'use client'; + +import { useState } from 'react'; +import { useSimpleAuth } from '@/components/SimpleAuthProvider'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; + +export default function SimpleAuthForm() { + const [isLogin, setIsLogin] = useState(true); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [nickname, setNickname] = useState(''); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + const { signIn, signUp } = useSimpleAuth(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + try { + let result; + if (isLogin) { + result = await signIn(email, password); + } else { + result = await signUp(email, password, nickname); + } + + // 检查登录或注册是否成功 + if (result && result.success !== false) { + // 使用 router.replace 替代 window.location.href + router.replace('/'); + } else { + alert(result?.error || '登录或注册失败'); + } + } catch (error: any) { + alert(error.message || error); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+
+
+ + + +
+

+ {isLogin ? '欢迎回来' : '创建账户'} +

+

+ {isLogin ? '请输入您的凭据以继续' : '开始您的个性化股票分析之旅'} +

+
+ +
+ {!isLogin && ( +
+ + setNickname(e.target.value)} + className="form-control form-control-lg" + placeholder="输入您的昵称" + /> +
+ )} + +
+ + setEmail(e.target.value)} + className="form-control form-control-lg" + placeholder="输入您的邮箱地址" + /> +
+ +
+ + setPassword(e.target.value)} + className="form-control form-control-lg" + placeholder="输入您的密码" + /> +
+ +
+ {isLogin && ( + + 忘记密码? + + )} +
+ +
+ +
+
+ +
+ +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/components/SimpleAuthProvider.tsx b/apps/qwen-stock-visualization/components/SimpleAuthProvider.tsx new file mode 100644 index 0000000..92a6cb1 --- /dev/null +++ b/apps/qwen-stock-visualization/components/SimpleAuthProvider.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { createContext, useContext, useEffect, useState } from 'react'; +import { registerUser, loginUser, validateSession, logoutUser, getUserById } from '@/lib/simpleAuth'; + +type SimpleAuthContextType = { + user: any | null; + signUp: (email: string, password: string, nickname?: string) => Promise; + signIn: (email: string, password: string) => Promise; + signOut: () => Promise; + isLoading: boolean; +}; + +const SimpleAuthContext = createContext(undefined); + +export function SimpleAuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + // Check for existing session on initial load + const checkExistingSession = async () => { + const token = localStorage.getItem('auth_token'); + if (token) { + const { valid, user } = await validateSession(token); + if (valid && user) { + setUser(user); + } else { + // Clear invalid token + localStorage.removeItem('auth_token'); + } + } + setIsLoading(false); + }; + + checkExistingSession(); + }, []); + + const signUp = async (email: string, password: string, nickname?: string) => { + const result = await registerUser(email, password, nickname); + + if (result.success) { + // Automatically log in after successful registration + return await signIn(email, password); + } + + return result; + }; + + const signIn = async (email: string, password: string) => { + const result = await loginUser(email, password); + + if (result.success) { + setUser(result.user); + localStorage.setItem('auth_token', result.token); + } + + return result; + }; + + const signOut = async () => { + const token = localStorage.getItem('auth_token'); + if (token) { + await logoutUser(token); + localStorage.removeItem('auth_token'); + } + setUser(null); + }; + + const value = { + user, + signUp, + signIn, + signOut, + isLoading, + }; + + return {children}; +} + +export function useSimpleAuth() { + const context = useContext(SimpleAuthContext); + if (context === undefined) { + throw new Error('useSimpleAuth must be used within a SimpleAuthProvider'); + } + return context; +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/components/StockChart.tsx b/apps/qwen-stock-visualization/components/StockChart.tsx new file mode 100644 index 0000000..a63de7b --- /dev/null +++ b/apps/qwen-stock-visualization/components/StockChart.tsx @@ -0,0 +1,108 @@ +'use client'; + +import React from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, ComposedChart, Area } from 'recharts'; + +type ChartData = { + timestamp: string; + price: number; + volume?: number; +}; + +type StockChartProps = { + data: ChartData[]; + title: string; + type?: 'line' | 'bar' | 'area'; +}; + +export default function StockChart({ data, title, type = 'line' }: StockChartProps) { + return ( +
+

{title}

+
+ + {type === 'line' ? ( + + + new Date(value).toLocaleDateString()} + /> + + [`$${value}`, '价格']} + labelFormatter={(value) => `日期: ${new Date(value).toLocaleString()}`} + /> + + + + ) : type === 'bar' ? ( + + + new Date(value).toLocaleDateString()} + /> + + [`${value}`, '成交量']} + labelFormatter={(value) => `日期: ${new Date(value).toLocaleString()}`} + /> + + + + ) : ( + + + new Date(value).toLocaleDateString()} + /> + + + + name === 'price' ? [`$${value}`, '价格'] : [`${value}`, '成交量'] + } + labelFormatter={(value) => `日期: ${new Date(value).toLocaleString()}`} + /> + + + + + )} + +
+
+ ); +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/lib/simpleAuth.ts b/apps/qwen-stock-visualization/lib/simpleAuth.ts new file mode 100644 index 0000000..cf85591 --- /dev/null +++ b/apps/qwen-stock-visualization/lib/simpleAuth.ts @@ -0,0 +1,190 @@ +// lib/simpleAuth.ts +import bcrypt from 'bcryptjs'; +import crypto from 'crypto'; + +// 动态导入Supabase客户端 +async function getSupabaseClient() { + if (typeof window !== 'undefined') { + // 客户端环境 + const { createClient } = await import('@/lib/supabaseClient'); + return createClient(); + } else { + // 服务端环境 + const { createClient } = await import('@supabase/supabase-js'); + + // 从环境变量获取Supabase配置 + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; + const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; + + if (!supabaseUrl || !supabaseAnonKey) { + throw new Error('Missing Supabase environment variables'); + } + + return createClient(supabaseUrl, supabaseAnonKey); + } +} + +// Hash password +export async function hashPassword(password: string): Promise { + const saltRounds = 10; + return await bcrypt.hash(password, saltRounds); +} + +// Compare password +export async function comparePassword(password: string, hash: string): Promise { + return await bcrypt.compare(password, hash); +} + +// Generate session token +export function generateToken(): string { + return crypto.randomBytes(32).toString('hex'); +} + +// Register new user +export async function registerUser(email: string, password: string, nickname?: string) { + try { + const supabase = await getSupabaseClient(); + + // Check if user already exists + const { data: existingUser } = await supabase + .from('users') + .select('id') + .eq('email', email) + .single(); + + if (existingUser) { + throw new Error('User with this email already exists'); + } + + // Hash password + const hashedPassword = await hashPassword(password); + + // Create new user + const { data: newUser, error } = await supabase + .from('users') + .insert([{ email, password_hash: hashedPassword, nickname }]) + .select() + .single(); + + if (error) throw error; + + return { success: true, user: newUser }; + } catch (error: any) { + console.error('Registration error:', error); + return { success: false, error: error.message || 'Registration failed' }; + } +} + +// Login user +export async function loginUser(email: string, password: string) { + try { + const supabase = await getSupabaseClient(); + + // Find user by email + const { data: user, error } = await supabase + .from('users') + .select('*') + .eq('email', email) + .single(); + + if (error || !user) { + throw new Error('Invalid email or password'); + } + + // Compare password + const isValid = await comparePassword(password, user.password_hash); + if (!isValid) { + throw new Error('Invalid email or password'); + } + + // Generate session token + const token = generateToken(); + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + 7); // Token expires in 7 days + + // Create session + const { error: sessionError } = await supabase + .from('user_sessions') + .insert([{ + user_id: user.id, + token, + expires_at: expiresAt.toISOString() + }]); + + if (sessionError) throw sessionError; + + return { success: true, user, token }; + } catch (error: any) { + console.error('Login error:', error); + return { success: false, error: error.message || 'Login failed' }; + } +} + +// Validate session token +export async function validateSession(token: string) { + try { + const supabase = await getSupabaseClient(); + + // Check if session exists and hasn't expired + const { data: session, error } = await supabase + .from('user_sessions') + .select(` + *, + users (*) + `) + .eq('token', token) + .gte('expires_at', new Date().toISOString()) + .single(); + + if (error || !session) { + return { valid: false, user: null }; + } + + return { valid: true, user: session.users }; + } catch (error) { + console.error('Session validation error:', error); + return { valid: false, user: null }; + } +} + +// Logout user +export async function logoutUser(token: string) { + try { + const supabase = await getSupabaseClient(); + + const { error } = await supabase + .from('user_sessions') + .delete() + .eq('token', token); + + if (error) { + console.error('Logout error:', error); + return { success: false, error: error.message }; + } + + return { success: true }; + } catch (error: any) { + console.error('Logout error:', error); + return { success: false, error: error.message }; + } +} + +// Get user by ID +export async function getUserById(userId: number) { + try { + const supabase = await getSupabaseClient(); + + const { data: user, error } = await supabase + .from('users') + .select('*') + .eq('id', userId) + .single(); + + if (error) throw error; + + return user; + } catch (error) { + console.error('Get user error:', error); + return null; + } +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/lib/stockDataLayer.ts b/apps/qwen-stock-visualization/lib/stockDataLayer.ts new file mode 100644 index 0000000..b29322b --- /dev/null +++ b/apps/qwen-stock-visualization/lib/stockDataLayer.ts @@ -0,0 +1,336 @@ +// lib/stockDataLayer.ts +import { createClient } from '@/lib/supabaseClient'; + +// 所有函数现在接受一个可选的访问令牌参数 +const getSupabaseInstance = (accessToken?: string) => createClient(accessToken); + +// 获取所有股票信息 +export const getAllStocks = async (accessToken?: string) => { + const supabase = getSupabaseInstance(accessToken); + const { data, error } = await supabase + .from('stocks') + .select('*') + .order('symbol'); + + if (error) { + console.error('Error fetching stocks:', error); + return []; + } + + return data; +}; + +// 获取特定股票的最新价格 +export const getLatestPrice = async (stockId: number, accessToken?: string) => { + const supabase = getSupabaseInstance(accessToken); + const { data, error } = await supabase + .from('stock_prices') + .select('*') + .eq('stock_id', stockId) + .order('timestamp', { ascending: false }) + .limit(1); + + if (error) { + console.error('Error fetching latest price:', error); + return null; + } + + return data?.[0] || null; +}; + +// 获取特定股票的历史价格数据 +export const getHistoricalPrices = async (stockId: number, days = 30, accessToken?: string) => { + const supabase = getSupabaseInstance(accessToken); + const { data, error } = await supabase + .from('stock_prices') + .select('*') + .eq('stock_id', stockId) + .gte('timestamp', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString()) + .order('timestamp', { ascending: true }); + + if (error) { + console.error('Error fetching historical prices:', error); + return []; + } + + return data; +}; + +// 获取用户的自选股 +export const getUserWatchlist = async (userId: number, accessToken?: string) => { + const supabase = getSupabaseInstance(accessToken); + const { data, error } = await supabase + .from('watchlists') + .select(` + *, + stocks (*) + `) + .eq('user_id', userId); + + if (error) { + console.error('Error fetching watchlist:', error); + return []; + } + + return data; +}; + +// 将股票添加到自选股 +export const addToWatchlist = async (userId: number, stockId: number, accessToken?: string) => { + const supabase = getSupabaseInstance(accessToken); + const { error } = await supabase + .from('watchlists') + .insert([{ user_id: userId, stock_id: stockId }]); + + if (error) { + console.error('Error adding to watchlist:', error); + return false; + } + + return true; +}; + +// 从自选股移除股票 +export const removeFromWatchlist = async (watchlistId: number, accessToken?: string) => { + const supabase = getSupabaseInstance(accessToken); + const { error } = await supabase + .from('watchlists') + .delete() + .eq('id', watchlistId); + + if (error) { + console.error('Error removing from watchlist:', error); + return false; + } + + return true; +}; + +// 获取用户的所有投资组合 +export const getUserPortfolios = async (userId: number, accessToken?: string) => { + const supabase = getSupabaseInstance(accessToken); + const { data, error } = await supabase + .from('portfolios') + .select('*') + .eq('user_id', userId); + + if (error) { + console.error('Error fetching portfolios:', error); + return []; + } + + return data; +}; + +// 创建新的投资组合 +export const createPortfolio = async (userId: number, name: string, initialBalance: number, accessToken?: string) => { + const supabase = getSupabaseInstance(accessToken); + const { data, error } = await supabase + .from('portfolios') + .insert([{ user_id: userId, name, initial_balance: initialBalance, current_balance: initialBalance }]) + .select(); + + if (error) { + console.error('Error creating portfolio:', error); + return null; + } + + return data?.[0]; +}; + +// 获取投资组合的持仓 +export const getPortfolioPositions = async (portfolioId: number, accessToken?: string) => { + const supabase = getSupabaseInstance(accessToken); + const { data, error } = await supabase + .from('portfolio_positions') + .select(` + *, + stocks (*) + `) + .eq('portfolio_id', portfolioId); + + if (error) { + console.error('Error fetching portfolio positions:', error); + return []; + } + + return data; +}; + +// 获取投资组合的交易记录 +export const getPortfolioTransactions = async (portfolioId: number, accessToken?: string) => { + const supabase = getSupabaseInstance(accessToken); + const { data, error } = await supabase + .from('portfolio_transactions') + .select(` + *, + stocks (symbol, name) + `) + .eq('portfolio_id', portfolioId) + .order('timestamp', { ascending: false }); + + if (error) { + console.error('Error fetching portfolio transactions:', error); + return []; + } + + return data; +}; + +// 删除投资组合 +export const deletePortfolio = async (portfolioId: number, accessToken?: string) => { + const supabase = getSupabaseInstance(accessToken); + + // 首先删除相关的持仓记录 + const { error: positionsError } = await supabase + .from('portfolio_positions') + .delete() + .eq('portfolio_id', portfolioId); + + if (positionsError) { + console.error('Error deleting portfolio positions:', positionsError); + throw positionsError; + } + + // 然后删除相关的交易记录 + const { error: transactionsError } = await supabase + .from('portfolio_transactions') + .delete() + .eq('portfolio_id', portfolioId); + + if (transactionsError) { + console.error('Error deleting portfolio transactions:', transactionsError); + throw transactionsError; + } + + // 最后删除投资组合本身 + const { error: portfolioError } = await supabase + .from('portfolios') + .delete() + .eq('id', portfolioId); + + if (portfolioError) { + console.error('Error deleting portfolio:', portfolioError); + throw portfolioError; + } + + return true; +}; + +// 执行交易(买入/卖出) +export const executeTrade = async ( + portfolioId: number, + stockId: number, + transactionType: 'buy' | 'sell', + quantity: number, + price: number, + accessToken?: string +) => { + const supabase = getSupabaseInstance(accessToken); + + // 计算总价值 + const totalValue = quantity * price; + + // 获取当前余额 + const { data: portfolioData, error: portfolioError } = await supabase + .from('portfolios') + .select('current_balance, initial_balance') + .eq('id', portfolioId) + .single(); + + if (portfolioError) throw portfolioError; + + // 检查余额是否足够(仅对买入操作) + if (transactionType === 'buy' && portfolioData.current_balance < totalValue) { + throw new Error('Insufficient balance'); + } + + // 更新投资组合余额 + const newBalance = transactionType === 'buy' + ? portfolioData.current_balance - totalValue + : portfolioData.current_balance + totalValue; + + const { error: updateBalanceError } = await supabase + .from('portfolios') + .update({ current_balance: newBalance }) + .eq('id', portfolioId); + + if (updateBalanceError) throw updateBalanceError; + + // 记录交易 + const { data: transactionData, error: transactionError } = await supabase + .from('portfolio_transactions') + .insert([{ + portfolio_id: portfolioId, + stock_id: stockId, + transaction_type: transactionType, + quantity, + price, + total_value: totalValue + }]) + .select() + .single(); + + if (transactionError) throw transactionError; + + // 更新持仓 + const { data: positionData } = await supabase + .from('portfolio_positions') + .select('*') + .eq('portfolio_id', portfolioId) + .eq('stock_id', stockId); + + if (positionData && positionData.length > 0) { + // 持仓已存在,更新数量和平均成本 + const currentPosition = positionData[0]; + let newQuantity, newAvgCost; + + if (transactionType === 'buy') { + newQuantity = currentPosition.quantity + quantity; + newAvgCost = ((currentPosition.quantity * currentPosition.avg_cost) + totalValue) / newQuantity; + } else { + newQuantity = currentPosition.quantity - quantity; + newAvgCost = currentPosition.avg_cost; // 卖出不影响平均成本 + + // 如果卖空了,数量变为负数(允许做空)或限制不能卖超过持有量 + if (newQuantity < 0) { + throw new Error('Cannot sell more than held quantity'); + } + } + + if (newQuantity === 0) { + // 如果数量为0,删除持仓记录 + const { error: deletePositionError } = await supabase + .from('portfolio_positions') + .delete() + .eq('id', currentPosition.id); + + if (deletePositionError) throw deletePositionError; + } else { + // 更新持仓 + const { error: updatePositionError } = await supabase + .from('portfolio_positions') + .update({ + quantity: newQuantity, + avg_cost: newAvgCost + }) + .eq('id', currentPosition.id); + + if (updatePositionError) throw updatePositionError; + } + } else if (transactionType === 'buy') { + // 新建持仓(只对买入操作) + const { error: newPositionError } = await supabase + .from('portfolio_positions') + .insert([{ + portfolio_id: portfolioId, + stock_id: stockId, + quantity, + avg_cost: price + }]); + + if (newPositionError) throw newPositionError; + } + + return transactionData; +}; \ No newline at end of file diff --git a/apps/qwen-stock-visualization/lib/supabaseClient.ts b/apps/qwen-stock-visualization/lib/supabaseClient.ts new file mode 100644 index 0000000..fea0a6c --- /dev/null +++ b/apps/qwen-stock-visualization/lib/supabaseClient.ts @@ -0,0 +1,18 @@ +// lib/supabaseClient.ts +import { createBrowserClient } from '@supabase/ssr'; + +export function createClient(supabaseAccessToken?: string) { + const options = supabaseAccessToken + ? { + global: { + headers: { Authorization: `Bearer ${supabaseAccessToken}` } + } + } + : {}; + + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + options + ); +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/mock_server/package-lock.json b/apps/qwen-stock-visualization/mock_server/package-lock.json new file mode 100644 index 0000000..9c39a04 --- /dev/null +++ b/apps/qwen-stock-visualization/mock_server/package-lock.json @@ -0,0 +1,736 @@ +{ + "name": "stock-price-mock-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "stock-price-mock-server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@supabase/supabase-js": "^2.91.0", + "dotenv": "^17.2.3" + }, + "devDependencies": { + "@types/node": "^25.0.9", + "tsx": "^4.21.0", + "typescript": "5.9.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.91.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.91.0.tgz", + "integrity": "sha512-9ywvsKLsxTwv7fvN5fXzP3UfRreqrX2waylTBDu0lkmeHXa8WtSQS9e0WV9FBduiazYqQbgfBQXBNPRPsRgWOQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.91.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.91.0.tgz", + "integrity": "sha512-WaakXOqLK1mLtBNFXp5o5T+LlI6KZuADSeXz+9ofPRG5OpVSvW148LVJB1DRZ16Phck1a0YqIUswOUgxCz6vMw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.91.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.91.0.tgz", + "integrity": "sha512-5S41zv2euNpGucvtM4Wy+xOmLznqt/XO+Lh823LOFEQ00ov7QJfvqb6VzIxufvzhooZpmGR0BxvMcJtWxCIFdQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.91.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.91.0.tgz", + "integrity": "sha512-u2YuJFG35umw8DO9beC27L/jYXm3KhF+73WQwbynMpV0tXsFIA0DOGRM0NgRyy03hJIdO6mxTTwe8efW3yx3Tg==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.91.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.91.0.tgz", + "integrity": "sha512-CI7fsVIBQHfNObqU9kmyQ1GWr+Ug44y4rSpvxT4LdQB9tlhg1NTBov6z7Dlmt8d6lGi/8a9lf/epCDxyWI792g==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.91.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.91.0.tgz", + "integrity": "sha512-Rjb0QqkKrmXMVwUOdEqysPBZ0ZDZakeptTkUa6k2d8r3strBdbWVDqjOdkCjAmvvZMtXecBeyTyMEXD1Zzjfvg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.91.0", + "@supabase/functions-js": "2.91.0", + "@supabase/postgrest-js": "2.91.0", + "@supabase/realtime-js": "2.91.0", + "@supabase/storage-js": "2.91.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@types/node": { + "version": "25.0.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz", + "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/apps/qwen-stock-visualization/mock_server/package.json b/apps/qwen-stock-visualization/mock_server/package.json new file mode 100644 index 0000000..94d0ce6 --- /dev/null +++ b/apps/qwen-stock-visualization/mock_server/package.json @@ -0,0 +1,22 @@ +{ + "name": "stock-price-mock-server", + "version": "1.0.0", + "description": "模拟股票价格更新服务器", + "main": "server.ts", + "scripts": { + "start": "tsx server.ts", + "dev": "tsx watch server.ts" + }, + "dependencies": { + "@supabase/supabase-js": "^2.91.0", + "dotenv": "^17.2.3" + }, + "devDependencies": { + "@types/node": "^25.0.9", + "tsx": "^4.21.0", + "typescript": "5.9.3" + }, + "keywords": ["stock", "mock", "api", "supabase"], + "author": "", + "license": "ISC" +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/mock_server/server.ts b/apps/qwen-stock-visualization/mock_server/server.ts new file mode 100644 index 0000000..36b98d7 --- /dev/null +++ b/apps/qwen-stock-visualization/mock_server/server.ts @@ -0,0 +1,164 @@ +import { createClient } from '@supabase/supabase-js'; +import dotenv from 'dotenv'; +import path from 'path'; + +// Load environment variables from project root +dotenv.config({ path: path.resolve(process.cwd(), '../.env.local') }); + +// Initialize Supabase client +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + +if (!supabaseUrl || !supabaseAnonKey) { + console.error('Missing Supabase environment variables'); + process.exit(1); +} + +const supabase = createClient(supabaseUrl, supabaseAnonKey); + +// 定义股票数据接口 +interface Stock { + id: number; + symbol: string; + name: string; + exchange: string; + currency: string; +} + +// 定义股票价格接口 +interface StockPrice { + id: number; + stock_id: number; + price: string; + change_amount: string; + change_percent: string; + volume: number; + timestamp: string; + created_at: string; +} + +// 获取所有股票 +async function getAllStocks(): Promise { + const { data, error } = await supabase + .from('stocks') + .select('*') + .order('id'); + + if (error) { + console.error('Error fetching stocks:', error); + return []; + } + + return data as Stock[]; +} + +// 获取指定股票的最新价格 +async function getLatestPrice(stockId: number): Promise { + const { data, error } = await supabase + .from('stock_prices') + .select('*') + .eq('stock_id', stockId) + .order('timestamp', { ascending: false }) + .limit(1); + + if (error) { + console.error(`Error fetching latest price for stock ${stockId}:`, error); + return null; + } + + return data && data.length > 0 ? data[0] as StockPrice : null; +} + +// 生成随机价格变动 +function generateRandomPriceChange(currentPrice: number): { newPrice: number; changeAmount: number; changePercent: number } { + // 价格变动范围在 -1 到 1 之间 + const change = (Math.random() * 2 - 1); // 生成 -1 到 1 之间的随机数 + const newPrice = Math.max(0.01, currentPrice + change); // 确保价格不低于 0.01 + const changeAmount = newPrice - currentPrice; + const changePercent = (changeAmount / currentPrice) * 100; + + return { + newPrice, + changeAmount, + changePercent + }; +} + +// 插入新的价格记录 +async function insertNewPrice(stockId: number, newPrice: number, changeAmount: number, changePercent: number) { + const newPriceRecord = { + stock_id: stockId, + price: newPrice.toFixed(2), + change_amount: changeAmount.toFixed(2), + change_percent: changePercent.toFixed(2), + volume: Math.floor(Math.random() * 1000000) + 100000, // 随机成交量 + timestamp: new Date().toISOString() + }; + + const { error } = await supabase + .from('stock_prices') + .insert([newPriceRecord]); + + if (error) { + console.error(`Error inserting new price for stock ${stockId}:`, error); + } else { + console.log(`Updated price for ${stockId}: $${newPrice.toFixed(2)} (${changePercent > 0 ? '+' : ''}${changePercent.toFixed(2)}%)`); + } +} + +// 主函数:更新所有股票价格 +async function updateAllStockPrices() { + console.log('Fetching all stocks...'); + const stocks = await getAllStocks(); + + if (stocks.length === 0) { + console.log('No stocks found.'); + return; + } + + console.log(`Updating prices for ${stocks.length} stocks...`); + + for (const stock of stocks) { + try { + // 获取最新价格 + const latestPrice = await getLatestPrice(stock.id); + + let currentPrice = 100; // 默认价格 + + if (latestPrice) { + currentPrice = parseFloat(latestPrice.price); + } else { + // 如果没有历史价格,使用一个随机初始价格 + currentPrice = 50 + Math.random() * 100; // 50-150 之间的随机价格 + } + + // 生成新的随机价格 + const { newPrice, changeAmount, changePercent } = generateRandomPriceChange(currentPrice); + + // 插入新的价格记录 + await insertNewPrice(stock.id, newPrice, changeAmount, changePercent); + } catch (error) { + console.error(`Error processing stock ${stock.id} (${stock.symbol}):`, error); + } + } +} + +// 启动模拟服务器 +function startMockServer() { + console.log('🚀 Starting mock stock price server...'); + console.log('📊 Prices will be updated every 5 seconds'); + console.log('💡 Press Ctrl+C to stop the server\n'); + + // 立即更新一次 + updateAllStockPrices().catch(console.error); + + // 每0.4秒更新一次价格 + setInterval(() => { + updateAllStockPrices().catch(console.error); + }, 400); // 0.4秒更新一次 +} + +// 启动服务器 +startMockServer(); + +export {}; \ No newline at end of file diff --git a/apps/qwen-stock-visualization/next-env.d.ts b/apps/qwen-stock-visualization/next-env.d.ts new file mode 100644 index 0000000..c4b7818 --- /dev/null +++ b/apps/qwen-stock-visualization/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/dev/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/qwen-stock-visualization/next.config.js b/apps/qwen-stock-visualization/next.config.js new file mode 100644 index 0000000..fb438d1 --- /dev/null +++ b/apps/qwen-stock-visualization/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {} + +export default nextConfig \ No newline at end of file diff --git a/apps/qwen-stock-visualization/package-lock.json b/apps/qwen-stock-visualization/package-lock.json new file mode 100644 index 0000000..cde7ba4 --- /dev/null +++ b/apps/qwen-stock-visualization/package-lock.json @@ -0,0 +1,2327 @@ +{ + "name": "supabase-demo-stock", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "supabase-demo-stock", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@supabase/ssr": "^0.8.0", + "@supabase/supabase-js": "^2.91.0", + "@types/node": "^25.0.9", + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", + "bcryptjs": "^3.0.3", + "bootstrap": "^5.3.8", + "dotenv": "^17.2.3", + "next": "^16.1.4", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "recharts": "^3.6.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "autoprefixer": "^10.4.23", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "tsx": "^4.21.0", + "typescript": "5.9.3" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.4.tgz", + "integrity": "sha512-gkrXnZyxPUy0Gg6SrPQPccbNVLSP3vmW8LU5dwEttEEC1RwDivk8w4O+sZIjFvPrSICXyhQDCG+y3VmjlJf+9A==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.4.tgz", + "integrity": "sha512-T8atLKuvk13XQUdVLCv1ZzMPgLPW0+DWWbHSQXs0/3TjPrKNxTmUIhOEaoEyl3Z82k8h/gEtqyuoZGv6+Ugawg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.4.tgz", + "integrity": "sha512-AKC/qVjUGUQDSPI6gESTx0xOnOPQ5gttogNS3o6bA83yiaSZJek0Am5yXy82F1KcZCx3DdOwdGPZpQCluonuxg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.4.tgz", + "integrity": "sha512-POQ65+pnYOkZNdngWfMEt7r53bzWiKkVNbjpmCt1Zb3V6lxJNXSsjwRuTQ8P/kguxDC8LRkqaL3vvsFrce4dMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.4.tgz", + "integrity": "sha512-3Wm0zGYVCs6qDFAiSSDL+Z+r46EdtCv/2l+UlIdMbAq9hPJBvGu/rZOeuvCaIUjbArkmXac8HnTyQPJFzFWA0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.4.tgz", + "integrity": "sha512-lWAYAezFinaJiD5Gv8HDidtsZdT3CDaCeqoPoJjeB57OqzvMajpIhlZFce5sCAH6VuX4mdkxCRqecCJFwfm2nQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.4.tgz", + "integrity": "sha512-fHaIpT7x4gA6VQbdEpYUXRGyge/YbRrkG6DXM60XiBqDM2g2NcrsQaIuj375egnGFkJow4RHacgBOEsHfGbiUw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.4.tgz", + "integrity": "sha512-MCrXxrTSE7jPN1NyXJr39E+aNFBrQZtO154LoCz7n99FuKqJDekgxipoodLNWdQP7/DZ5tKMc/efybx1l159hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.4.tgz", + "integrity": "sha512-JSVlm9MDhmTXw/sO2PE/MRj+G6XOSMZB+BcZ0a7d6KwVFZVpkHcb2okyoYFBaco6LeiL53BBklRlOrDDbOeE5w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@supabase/auth-js": { + "version": "2.91.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.91.0.tgz", + "integrity": "sha512-9ywvsKLsxTwv7fvN5fXzP3UfRreqrX2waylTBDu0lkmeHXa8WtSQS9e0WV9FBduiazYqQbgfBQXBNPRPsRgWOQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.91.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.91.0.tgz", + "integrity": "sha512-WaakXOqLK1mLtBNFXp5o5T+LlI6KZuADSeXz+9ofPRG5OpVSvW148LVJB1DRZ16Phck1a0YqIUswOUgxCz6vMw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.91.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.91.0.tgz", + "integrity": "sha512-5S41zv2euNpGucvtM4Wy+xOmLznqt/XO+Lh823LOFEQ00ov7QJfvqb6VzIxufvzhooZpmGR0BxvMcJtWxCIFdQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.91.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.91.0.tgz", + "integrity": "sha512-u2YuJFG35umw8DO9beC27L/jYXm3KhF+73WQwbynMpV0tXsFIA0DOGRM0NgRyy03hJIdO6mxTTwe8efW3yx3Tg==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.8.0.tgz", + "integrity": "sha512-/PKk8kNFSs8QvvJ2vOww1mF5/c5W8y42duYtXvkOSe+yZKRgTTZywYG2l41pjhNomqESZCpZtXuWmYjFRMV+dw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.76.1" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.91.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.91.0.tgz", + "integrity": "sha512-CI7fsVIBQHfNObqU9kmyQ1GWr+Ug44y4rSpvxT4LdQB9tlhg1NTBov6z7Dlmt8d6lGi/8a9lf/epCDxyWI792g==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.91.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.91.0.tgz", + "integrity": "sha512-Rjb0QqkKrmXMVwUOdEqysPBZ0ZDZakeptTkUa6k2d8r3strBdbWVDqjOdkCjAmvvZMtXecBeyTyMEXD1Zzjfvg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@supabase/auth-js": "2.91.0", + "@supabase/functions-js": "2.91.0", + "@supabase/postgrest-js": "2.91.0", + "@supabase/realtime-js": "2.91.0", + "@supabase/storage-js": "2.91.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz", + "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", + "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.16.tgz", + "integrity": "sha512-KeUZdBuxngy825i8xvzaK1Ncnkx0tBmb3k8DkEuqjKRkmtvNTjey2ZsNeh8Dw4lfKvbCOu9oeNx2TKm2vHqcRw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.4.tgz", + "integrity": "sha512-gKSecROqisnV7Buen5BfjmXAm7Xlpx9o2ueVQRo5DxQcjC8d330dOM1xiGWc2k3Dcnz0In3VybyRPOsudwgiqQ==", + "license": "MIT", + "dependencies": { + "@next/env": "16.1.4", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.1.4", + "@next/swc-darwin-x64": "16.1.4", + "@next/swc-linux-arm64-gnu": "16.1.4", + "@next/swc-linux-arm64-musl": "16.1.4", + "@next/swc-linux-x64-gnu": "16.1.4", + "@next/swc-linux-x64-musl": "16.1.4", + "@next/swc-win32-arm64-msvc": "16.1.4", + "@next/swc-win32-x64-msvc": "16.1.4", + "sharp": "^0.34.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-is": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", + "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/recharts": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz", + "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/apps/qwen-stock-visualization/package.json b/apps/qwen-stock-visualization/package.json new file mode 100644 index 0000000..12c6193 --- /dev/null +++ b/apps/qwen-stock-visualization/package.json @@ -0,0 +1,39 @@ +{ + "name": "supabase-demo-stock", + "version": "1.0.0", + "description": "", + "main": ".eslintrc.js", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "populate-mock-data": "tsx scripts/populateMockData.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module", + "dependencies": { + "@supabase/ssr": "^0.8.0", + "@supabase/supabase-js": "^2.91.0", + "@types/node": "^25.0.9", + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", + "bcryptjs": "^3.0.3", + "bootstrap": "^5.3.8", + "dotenv": "^17.2.3", + "next": "^16.1.4", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "recharts": "^3.6.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "autoprefixer": "^10.4.23", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "tsx": "^4.21.0", + "typescript": "5.9.3" + } +} diff --git a/apps/qwen-stock-visualization/scripts/insertUserData.js b/apps/qwen-stock-visualization/scripts/insertUserData.js new file mode 100644 index 0000000..39ccf87 --- /dev/null +++ b/apps/qwen-stock-visualization/scripts/insertUserData.js @@ -0,0 +1,224 @@ +import { createClient } from '@supabase/supabase-js'; +import dotenv from 'dotenv'; + +// 加载 .env.local 文件 +dotenv.config({ path: '.env.local' }); + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + +if (!supabaseUrl || !supabaseAnonKey) { + console.error('环境变量缺失'); + console.log('NEXT_PUBLIC_SUPABASE_URL:', process.env.NEXT_PUBLIC_SUPABASE_URL); + console.log('NEXT_PUBLIC_SUPABASE_ANON_KEY:', process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY); + process.exit(1); +} + +const supabase = createClient(supabaseUrl, supabaseAnonKey); + +async function insertUserData() { + try { + // 首先找到用户 + const { data: user, error: userError } = await supabase + .from('users') + .select('id') + .eq('email', '12345678@163.com') + .single(); + + if (userError) { + console.error('查询用户时出错:', userError); + return; + } + + if (!user) { + console.log('未找到指定邮箱的用户'); + return; + } + + console.log('找到用户ID:', user.id); + + // 检查是否已经存在投资组合 + const { data: existingPortfolio, error: portfolioCheckError } = await supabase + .from('portfolios') + .select('id') + .eq('user_id', user.id) + .limit(1); + + if (portfolioCheckError) { + console.error('检查投资组合时出错:', portfolioCheckError); + return; + } + + let portfolioId; + + if (existingPortfolio && existingPortfolio.length > 0) { + console.log('用户已有投资组合:', existingPortfolio[0].id); + portfolioId = existingPortfolio[0].id; + } else { + // 插入投资组合 + const { data: portfolio, error: portfolioError } = await supabase + .from('portfolios') + .insert({ + user_id: user.id, + name: '我的主要投资组合', + initial_balance: 100000, + current_balance: 98500 + }) + .select() + .single(); + + if (portfolioError) { + console.error('插入投资组合时出错:', portfolioError); + return; + } + + console.log('成功插入投资组合:', portfolio.id); + portfolioId = portfolio.id; + } + + // 检查持仓是否已存在 + const { data: existingPositions, error: positionsCheckError } = await supabase + .from('portfolio_positions') + .select('id') + .eq('portfolio_id', portfolioId) + .limit(1); + + if (positionsCheckError) { + console.error('检查持仓时出错:', positionsCheckError); + return; + } + + if (!existingPositions || existingPositions.length === 0) { + // 插入持仓 + const positionsData = [ + { + portfolio_id: portfolioId, + stock_id: 1, // AAPL + quantity: 10, + avg_cost: 140.50 + }, + { + portfolio_id: portfolioId, + stock_id: 2, // MSFT + quantity: 5, + avg_cost: 330.25 + }, + { + portfolio_id: portfolioId, + stock_id: 3, // GOOGL + quantity: 8, + avg_cost: 135.75 + } + ]; + + const { error: positionsError } = await supabase + .from('portfolio_positions') + .insert(positionsData); + + if (positionsError) { + console.error('插入持仓时出错:', positionsError); + } else { + console.log('成功插入持仓数据'); + } + } else { + console.log('用户已有持仓数据'); + } + + // 检查交易记录是否已存在 + const { data: existingTransactions, error: transactionsCheckError } = await supabase + .from('portfolio_transactions') + .select('id') + .eq('portfolio_id', portfolioId) + .limit(1); + + if (transactionsCheckError) { + console.error('检查交易记录时出错:', transactionsCheckError); + return; + } + + if (!existingTransactions || existingTransactions.length === 0) { + // 插入交易记录 + const transactionsData = [ + { + portfolio_id: portfolioId, + stock_id: 1, + transaction_type: 'buy', + quantity: 10, + price: 140.50, + total_value: 1405.00 + }, + { + portfolio_id: portfolioId, + stock_id: 2, + transaction_type: 'buy', + quantity: 5, + price: 330.25, + total_value: 1651.25 + }, + { + portfolio_id: portfolioId, + stock_id: 3, + transaction_type: 'buy', + quantity: 8, + price: 135.75, + total_value: 1086.00 + } + ]; + + const { error: transactionsError } = await supabase + .from('portfolio_transactions') + .insert(transactionsData); + + if (transactionsError) { + console.error('插入交易记录时出错:', transactionsError); + } else { + console.log('成功插入交易记录'); + } + } else { + console.log('用户已有交易记录'); + } + + // 检查自选股是否已存在 + const { data: existingWatchlist, error: watchlistCheckError } = await supabase + .from('watchlists') + .select('id') + .eq('user_id', user.id) + .limit(1); + + if (watchlistCheckError) { + console.error('检查自选股时出错:', watchlistCheckError); + return; + } + + if (!existingWatchlist || existingWatchlist.length === 0) { + // 添加一些自选股 + const watchlistData = [ + { user_id: user.id, stock_id: 1 }, + { user_id: user.id, stock_id: 4 }, // AMZN + { user_id: user.id, stock_id: 5 } // TSLA + ]; + + const { error: watchlistError } = await supabase + .from('watchlists') + .insert(watchlistData); + + if (watchlistError) { + console.error('插入自选股时出错:', watchlistError); + } else { + console.log('成功插入自选股'); + } + } else { + console.log('用户已有自选股'); + } + + console.log('用户数据操作完成'); + } catch (err) { + console.error('操作过程中出现错误:', err); + } +} + +// 执行函数 +insertUserData().then(() => { + console.log('脚本执行完成'); + process.exit(0); +}); \ No newline at end of file diff --git a/apps/qwen-stock-visualization/scripts/populateMockData.ts b/apps/qwen-stock-visualization/scripts/populateMockData.ts new file mode 100644 index 0000000..a99ba9c --- /dev/null +++ b/apps/qwen-stock-visualization/scripts/populateMockData.ts @@ -0,0 +1,79 @@ +// scripts/populateMockData.ts +import { createClient } from '@supabase/supabase-js'; +import dotenv from 'dotenv'; +import path from 'path'; + +// Load environment variables from .env.local +dotenv.config({ path: path.resolve(process.cwd(), '.env.local') }); + +// Initialize Supabase client +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + +if (!supabaseUrl || !supabaseAnonKey) { + console.error('Missing Supabase environment variables'); + process.exit(1); +} + +const supabase = createClient(supabaseUrl, supabaseAnonKey); + +async function populateMockData() { + console.log('开始填充模拟数据...'); + + // 获取所有股票 + const { data: stocks } = await supabase + .from('stocks') + .select('id, symbol'); + + if (!stocks) { + console.error('无法获取股票数据'); + return; + } + + console.log(`找到 ${stocks.length} 只股票`); + + // 为每只股票生成最近30天的模拟价格数据 + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + + for (const stock of stocks) { + console.log(`正在为 ${stock.symbol} 生成模拟价格数据...`); + + // 为每只股票生成随机价格数据 + let basePrice = 100 + Math.random() * 200; // 随机基础价格 + + for (let i = 30; i >= 0; i--) { + const date = new Date(); + date.setDate(date.getDate() - i); + + // 生成随机价格波动 + const changePercent = (Math.random() - 0.5) * 0.1; // -5% 到 +5% 的变化 + const changeAmount = basePrice * changePercent; + const newPrice = basePrice + changeAmount; + + // 更新基础价格用于下一天 + basePrice = newPrice; + + // 插入价格数据 + const { error } = await supabase + .from('stock_prices') + .insert({ + stock_id: stock.id, + price: parseFloat(newPrice.toFixed(2)), + change_amount: parseFloat(changeAmount.toFixed(2)), + change_percent: parseFloat((changePercent * 100).toFixed(2)), + volume: Math.floor(Math.random() * 1000000) + 100000, // 随机成交量 + timestamp: date.toISOString() + }); + + if (error) { + console.error(`插入 ${stock.symbol} 的价格数据时出错:`, error); + } + } + } + + console.log('模拟数据填充完成!'); +} + +// 运行函数 +populateMockData().catch(console.error); \ No newline at end of file diff --git a/apps/qwen-stock-visualization/supabase/config.toml b/apps/qwen-stock-visualization/supabase/config.toml new file mode 100644 index 0000000..02faf7e --- /dev/null +++ b/apps/qwen-stock-visualization/supabase/config.toml @@ -0,0 +1,39 @@ +# Setup and configuration for local Supabase development +# This file is used by the Supabase CLI to configure your local development setup +# For production deployments, configurations are managed in the Supabase dashboard + +[api] +# Port to start the API server on +port = 54321 + +# Schemas to expose in the API +schemas = ["public", "storage", "graphql_public"] + +# Extra search path for functions and relations +extra_search_path = ["public", "extensions"] + +# Maximum number of rows that can be returned in a single request +max_rows = 1000 + +[db] +# Port to start the Postgres database on +port = 54322 + +# Port to start the Studio on +studio_port = 54323 + +[auth] +# The base URL of your website +site_url = "http://localhost:3000" + +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication +additional_redirect_urls = ["https://localhost:3000"] + +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour) +jwt_expiry = 3600 + +# Enable email confirmations +enable_confirmations = false + +[analytics] +enabled = false \ No newline at end of file diff --git a/apps/qwen-stock-visualization/supabase/migrations/000001_init_simple_schema.sql b/apps/qwen-stock-visualization/supabase/migrations/000001_init_simple_schema.sql new file mode 100644 index 0000000..f7eb9a1 --- /dev/null +++ b/apps/qwen-stock-visualization/supabase/migrations/000001_init_simple_schema.sql @@ -0,0 +1,114 @@ +-- Simplified database schema with basic user authentication + +-- Create a simple users table for storing user information +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + nickname VARCHAR(255), + avatar_url TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create sessions table to store user sessions +CREATE TABLE IF NOT EXISTS user_sessions ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + token TEXT UNIQUE NOT NULL, + expires_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create Stocks Table for storing stock metadata +CREATE TABLE IF NOT EXISTS stocks ( + id SERIAL PRIMARY KEY, + symbol VARCHAR(10) UNIQUE NOT NULL, + name VARCHAR(255) NOT NULL, + exchange VARCHAR(50), + currency VARCHAR(10) DEFAULT 'USD', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create Stock Prices table for storing real-time price data +CREATE TABLE IF NOT EXISTS stock_prices ( + id SERIAL PRIMARY KEY, + stock_id INTEGER REFERENCES stocks(id) ON DELETE CASCADE, + price DECIMAL(10, 2) NOT NULL, + change_amount DECIMAL(10, 2), + change_percent DECIMAL(5, 2), + volume BIGINT, + timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create Watchlists table for user's favorite stocks +CREATE TABLE IF NOT EXISTS watchlists ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + stock_id INTEGER REFERENCES stocks(id) ON DELETE CASCADE, + added_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(user_id, stock_id) +); + +-- Create Portfolios table for user's investment portfolios +CREATE TABLE IF NOT EXISTS portfolios ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + initial_balance DECIMAL(15, 2) NOT NULL DEFAULT 100000.00, + current_balance DECIMAL(15, 2) NOT NULL DEFAULT 100000.00, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create Portfolio Positions table for tracking holdings +CREATE TABLE IF NOT EXISTS portfolio_positions ( + id SERIAL PRIMARY KEY, + portfolio_id INTEGER REFERENCES portfolios(id) ON DELETE CASCADE, + stock_id INTEGER REFERENCES stocks(id) ON DELETE CASCADE, + quantity INTEGER NOT NULL DEFAULT 0, + avg_cost DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(portfolio_id, stock_id) +); + +-- Create Portfolio Transactions table for recording trades +CREATE TABLE IF NOT EXISTS portfolio_transactions ( + id SERIAL PRIMARY KEY, + portfolio_id INTEGER REFERENCES portfolios(id) ON DELETE CASCADE, + stock_id INTEGER REFERENCES stocks(id) ON DELETE CASCADE, + transaction_type VARCHAR(10) CHECK (transaction_type IN ('buy', 'sell')) NOT NULL, + quantity INTEGER NOT NULL, + price DECIMAL(10, 2) NOT NULL, + total_value DECIMAL(15, 2) NOT NULL, + timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create indexes for performance +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_user_sessions_token ON user_sessions(token); +CREATE INDEX idx_user_sessions_expires ON user_sessions(expires_at); +CREATE INDEX idx_stocks_symbol ON stocks(symbol); +CREATE INDEX idx_stock_prices_stock_timestamp ON stock_prices(stock_id, timestamp DESC); +CREATE INDEX idx_watchlists_user_stock ON watchlists(user_id, stock_id); +CREATE INDEX idx_portfolios_user ON portfolios(user_id); +CREATE INDEX idx_portfolio_positions_portfolio_stock ON portfolio_positions(portfolio_id, stock_id); +CREATE INDEX idx_portfolio_transactions_portfolio_timestamp ON portfolio_transactions(portfolio_id, timestamp DESC); + +-- Insert sample stock data +INSERT INTO stocks (symbol, name, exchange) VALUES + ('AAPL', 'Apple Inc.', 'NASDAQ'), + ('MSFT', 'Microsoft Corporation', 'NASDAQ'), + ('GOOGL', 'Alphabet Inc.', 'NASDAQ'), + ('AMZN', 'Amazon.com Inc.', 'NASDAQ'), + ('TSLA', 'Tesla Inc.', 'NASDAQ'), + ('META', 'Meta Platforms Inc.', 'NASDAQ'), + ('NVDA', 'NVIDIA Corporation', 'NASDAQ'), + ('NFLX', 'Netflix Inc.', 'NASDAQ'), + ('DIS', 'The Walt Disney Company', 'NYSE'), + ('JPM', 'JPMorgan Chase & Co.', 'NYSE') +ON CONFLICT (symbol) DO NOTHING; \ No newline at end of file diff --git a/apps/qwen-stock-visualization/tailwind.config.js b/apps/qwen-stock-visualization/tailwind.config.js new file mode 100644 index 0000000..d619e6c --- /dev/null +++ b/apps/qwen-stock-visualization/tailwind.config.js @@ -0,0 +1,15 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./app/**/*.{js,ts,jsx,tsx,mdx}", + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + + // Or if using `src` directory: + "./src/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} \ No newline at end of file diff --git a/apps/qwen-stock-visualization/test-db-connection.ts b/apps/qwen-stock-visualization/test-db-connection.ts new file mode 100644 index 0000000..46617b7 --- /dev/null +++ b/apps/qwen-stock-visualization/test-db-connection.ts @@ -0,0 +1,60 @@ +// test-db-connection.ts +import { createClient } from '@supabase/supabase-js'; +import dotenv from 'dotenv'; +import path from 'path'; + +// Load environment variables from .env.local +dotenv.config({ path: path.resolve(process.cwd(), '.env.local') }); + +// Initialize Supabase client +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + +if (!supabaseUrl || !supabaseAnonKey) { + console.error('Missing Supabase environment variables'); + process.exit(1); +} + +const supabase = createClient(supabaseUrl, supabaseAnonKey); + +async function testConnection() { + console.log('Testing database connection...'); + console.log('Supabase URL:', supabaseUrl); + + try { + // 测试获取所有股票数据 + const { data: stocks, error: stocksError } = await supabase + .from('stocks') + .select('*') + .limit(5); + + if (stocksError) { + console.error('Error fetching stocks:', stocksError); + return; + } + + console.log('Successfully connected to database!'); + console.log('Sample stocks data:', stocks); + + // 测试获取价格数据 + if (stocks && stocks.length > 0) { + const stockId = stocks[0].id; + const { data: prices, error: pricesError } = await supabase + .from('stock_prices') + .select('*') + .eq('stock_id', stockId) + .limit(5); + + if (pricesError) { + console.log('No price data found or error:', pricesError); + } else { + console.log('Sample prices data:', prices); + } + } + + } catch (error) { + console.error('Connection failed:', error); + } +} + +testConnection(); \ No newline at end of file diff --git a/apps/qwen-stock-visualization/tsconfig.json b/apps/qwen-stock-visualization/tsconfig.json new file mode 100644 index 0000000..6aa117c --- /dev/null +++ b/apps/qwen-stock-visualization/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + }, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} From ff5511286bbd7a44f49dd0b5de72bb384de44dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=87=E6=BA=AF?= Date: Mon, 26 Jan 2026 20:41:45 -0800 Subject: [PATCH 2/3] remove useless scripts --- .../scripts/insertUserData.js | 224 ------------------ .../scripts/populateMockData.ts | 79 ------ 2 files changed, 303 deletions(-) delete mode 100644 apps/qwen-stock-visualization/scripts/insertUserData.js delete mode 100644 apps/qwen-stock-visualization/scripts/populateMockData.ts diff --git a/apps/qwen-stock-visualization/scripts/insertUserData.js b/apps/qwen-stock-visualization/scripts/insertUserData.js deleted file mode 100644 index 39ccf87..0000000 --- a/apps/qwen-stock-visualization/scripts/insertUserData.js +++ /dev/null @@ -1,224 +0,0 @@ -import { createClient } from '@supabase/supabase-js'; -import dotenv from 'dotenv'; - -// 加载 .env.local 文件 -dotenv.config({ path: '.env.local' }); - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; -const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - -if (!supabaseUrl || !supabaseAnonKey) { - console.error('环境变量缺失'); - console.log('NEXT_PUBLIC_SUPABASE_URL:', process.env.NEXT_PUBLIC_SUPABASE_URL); - console.log('NEXT_PUBLIC_SUPABASE_ANON_KEY:', process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY); - process.exit(1); -} - -const supabase = createClient(supabaseUrl, supabaseAnonKey); - -async function insertUserData() { - try { - // 首先找到用户 - const { data: user, error: userError } = await supabase - .from('users') - .select('id') - .eq('email', '12345678@163.com') - .single(); - - if (userError) { - console.error('查询用户时出错:', userError); - return; - } - - if (!user) { - console.log('未找到指定邮箱的用户'); - return; - } - - console.log('找到用户ID:', user.id); - - // 检查是否已经存在投资组合 - const { data: existingPortfolio, error: portfolioCheckError } = await supabase - .from('portfolios') - .select('id') - .eq('user_id', user.id) - .limit(1); - - if (portfolioCheckError) { - console.error('检查投资组合时出错:', portfolioCheckError); - return; - } - - let portfolioId; - - if (existingPortfolio && existingPortfolio.length > 0) { - console.log('用户已有投资组合:', existingPortfolio[0].id); - portfolioId = existingPortfolio[0].id; - } else { - // 插入投资组合 - const { data: portfolio, error: portfolioError } = await supabase - .from('portfolios') - .insert({ - user_id: user.id, - name: '我的主要投资组合', - initial_balance: 100000, - current_balance: 98500 - }) - .select() - .single(); - - if (portfolioError) { - console.error('插入投资组合时出错:', portfolioError); - return; - } - - console.log('成功插入投资组合:', portfolio.id); - portfolioId = portfolio.id; - } - - // 检查持仓是否已存在 - const { data: existingPositions, error: positionsCheckError } = await supabase - .from('portfolio_positions') - .select('id') - .eq('portfolio_id', portfolioId) - .limit(1); - - if (positionsCheckError) { - console.error('检查持仓时出错:', positionsCheckError); - return; - } - - if (!existingPositions || existingPositions.length === 0) { - // 插入持仓 - const positionsData = [ - { - portfolio_id: portfolioId, - stock_id: 1, // AAPL - quantity: 10, - avg_cost: 140.50 - }, - { - portfolio_id: portfolioId, - stock_id: 2, // MSFT - quantity: 5, - avg_cost: 330.25 - }, - { - portfolio_id: portfolioId, - stock_id: 3, // GOOGL - quantity: 8, - avg_cost: 135.75 - } - ]; - - const { error: positionsError } = await supabase - .from('portfolio_positions') - .insert(positionsData); - - if (positionsError) { - console.error('插入持仓时出错:', positionsError); - } else { - console.log('成功插入持仓数据'); - } - } else { - console.log('用户已有持仓数据'); - } - - // 检查交易记录是否已存在 - const { data: existingTransactions, error: transactionsCheckError } = await supabase - .from('portfolio_transactions') - .select('id') - .eq('portfolio_id', portfolioId) - .limit(1); - - if (transactionsCheckError) { - console.error('检查交易记录时出错:', transactionsCheckError); - return; - } - - if (!existingTransactions || existingTransactions.length === 0) { - // 插入交易记录 - const transactionsData = [ - { - portfolio_id: portfolioId, - stock_id: 1, - transaction_type: 'buy', - quantity: 10, - price: 140.50, - total_value: 1405.00 - }, - { - portfolio_id: portfolioId, - stock_id: 2, - transaction_type: 'buy', - quantity: 5, - price: 330.25, - total_value: 1651.25 - }, - { - portfolio_id: portfolioId, - stock_id: 3, - transaction_type: 'buy', - quantity: 8, - price: 135.75, - total_value: 1086.00 - } - ]; - - const { error: transactionsError } = await supabase - .from('portfolio_transactions') - .insert(transactionsData); - - if (transactionsError) { - console.error('插入交易记录时出错:', transactionsError); - } else { - console.log('成功插入交易记录'); - } - } else { - console.log('用户已有交易记录'); - } - - // 检查自选股是否已存在 - const { data: existingWatchlist, error: watchlistCheckError } = await supabase - .from('watchlists') - .select('id') - .eq('user_id', user.id) - .limit(1); - - if (watchlistCheckError) { - console.error('检查自选股时出错:', watchlistCheckError); - return; - } - - if (!existingWatchlist || existingWatchlist.length === 0) { - // 添加一些自选股 - const watchlistData = [ - { user_id: user.id, stock_id: 1 }, - { user_id: user.id, stock_id: 4 }, // AMZN - { user_id: user.id, stock_id: 5 } // TSLA - ]; - - const { error: watchlistError } = await supabase - .from('watchlists') - .insert(watchlistData); - - if (watchlistError) { - console.error('插入自选股时出错:', watchlistError); - } else { - console.log('成功插入自选股'); - } - } else { - console.log('用户已有自选股'); - } - - console.log('用户数据操作完成'); - } catch (err) { - console.error('操作过程中出现错误:', err); - } -} - -// 执行函数 -insertUserData().then(() => { - console.log('脚本执行完成'); - process.exit(0); -}); \ No newline at end of file diff --git a/apps/qwen-stock-visualization/scripts/populateMockData.ts b/apps/qwen-stock-visualization/scripts/populateMockData.ts deleted file mode 100644 index a99ba9c..0000000 --- a/apps/qwen-stock-visualization/scripts/populateMockData.ts +++ /dev/null @@ -1,79 +0,0 @@ -// scripts/populateMockData.ts -import { createClient } from '@supabase/supabase-js'; -import dotenv from 'dotenv'; -import path from 'path'; - -// Load environment variables from .env.local -dotenv.config({ path: path.resolve(process.cwd(), '.env.local') }); - -// Initialize Supabase client -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; -const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - -if (!supabaseUrl || !supabaseAnonKey) { - console.error('Missing Supabase environment variables'); - process.exit(1); -} - -const supabase = createClient(supabaseUrl, supabaseAnonKey); - -async function populateMockData() { - console.log('开始填充模拟数据...'); - - // 获取所有股票 - const { data: stocks } = await supabase - .from('stocks') - .select('id, symbol'); - - if (!stocks) { - console.error('无法获取股票数据'); - return; - } - - console.log(`找到 ${stocks.length} 只股票`); - - // 为每只股票生成最近30天的模拟价格数据 - const thirtyDaysAgo = new Date(); - thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); - - for (const stock of stocks) { - console.log(`正在为 ${stock.symbol} 生成模拟价格数据...`); - - // 为每只股票生成随机价格数据 - let basePrice = 100 + Math.random() * 200; // 随机基础价格 - - for (let i = 30; i >= 0; i--) { - const date = new Date(); - date.setDate(date.getDate() - i); - - // 生成随机价格波动 - const changePercent = (Math.random() - 0.5) * 0.1; // -5% 到 +5% 的变化 - const changeAmount = basePrice * changePercent; - const newPrice = basePrice + changeAmount; - - // 更新基础价格用于下一天 - basePrice = newPrice; - - // 插入价格数据 - const { error } = await supabase - .from('stock_prices') - .insert({ - stock_id: stock.id, - price: parseFloat(newPrice.toFixed(2)), - change_amount: parseFloat(changeAmount.toFixed(2)), - change_percent: parseFloat((changePercent * 100).toFixed(2)), - volume: Math.floor(Math.random() * 1000000) + 100000, // 随机成交量 - timestamp: date.toISOString() - }); - - if (error) { - console.error(`插入 ${stock.symbol} 的价格数据时出错:`, error); - } - } - } - - console.log('模拟数据填充完成!'); -} - -// 运行函数 -populateMockData().catch(console.error); \ No newline at end of file From 8972a66aa159d970c055230ddaa85ac6f2f78ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=87=E6=BA=AF?= Date: Mon, 26 Jan 2026 20:47:43 -0800 Subject: [PATCH 3/3] Update Readme and remove useless scripts --- apps/qwen-stock-visualization/README.md | 19 +++++- .../test-db-connection.ts | 60 ------------------- 2 files changed, 18 insertions(+), 61 deletions(-) delete mode 100644 apps/qwen-stock-visualization/test-db-connection.ts diff --git a/apps/qwen-stock-visualization/README.md b/apps/qwen-stock-visualization/README.md index b828527..3fce500 100644 --- a/apps/qwen-stock-visualization/README.md +++ b/apps/qwen-stock-visualization/README.md @@ -29,7 +29,24 @@ - `NEXT_PUBLIC_SUPABASE_URL` - 你的 Supabase 项目 URL - `NEXT_PUBLIC_SUPABASE_ANON_KEY` - 你的 Supabase 匿名密钥 -### 2. 安装依赖 +### 2. 数据库设置 + +**重要提示:** 你需要手动创建数据库表结构。本项目提供了数据库迁移文件,位于 `supabase/migrations/000001_init_simple_schema.sql`。 + +你可以通过以下方式之一来创建数据库表: + +**选项 A:使用 Supabase CLI** +```bash +# 安装 Supabase CLI 后执行 +supabase db push +``` + +**选项 B:手动执行 SQL** +1. 登录到你的 Supabase 项目控制台 +2. 转到 SQL 编辑器 +3. 复制并执行 `supabase/migrations/000001_init_simple_schema.sql` 文件中的所有 SQL 语句 + +### 3. 安装依赖 ```bash npm install diff --git a/apps/qwen-stock-visualization/test-db-connection.ts b/apps/qwen-stock-visualization/test-db-connection.ts deleted file mode 100644 index 46617b7..0000000 --- a/apps/qwen-stock-visualization/test-db-connection.ts +++ /dev/null @@ -1,60 +0,0 @@ -// test-db-connection.ts -import { createClient } from '@supabase/supabase-js'; -import dotenv from 'dotenv'; -import path from 'path'; - -// Load environment variables from .env.local -dotenv.config({ path: path.resolve(process.cwd(), '.env.local') }); - -// Initialize Supabase client -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; -const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - -if (!supabaseUrl || !supabaseAnonKey) { - console.error('Missing Supabase environment variables'); - process.exit(1); -} - -const supabase = createClient(supabaseUrl, supabaseAnonKey); - -async function testConnection() { - console.log('Testing database connection...'); - console.log('Supabase URL:', supabaseUrl); - - try { - // 测试获取所有股票数据 - const { data: stocks, error: stocksError } = await supabase - .from('stocks') - .select('*') - .limit(5); - - if (stocksError) { - console.error('Error fetching stocks:', stocksError); - return; - } - - console.log('Successfully connected to database!'); - console.log('Sample stocks data:', stocks); - - // 测试获取价格数据 - if (stocks && stocks.length > 0) { - const stockId = stocks[0].id; - const { data: prices, error: pricesError } = await supabase - .from('stock_prices') - .select('*') - .eq('stock_id', stockId) - .limit(5); - - if (pricesError) { - console.log('No price data found or error:', pricesError); - } else { - console.log('Sample prices data:', prices); - } - } - - } catch (error) { - console.error('Connection failed:', error); - } -} - -testConnection(); \ No newline at end of file