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..3fce500
--- /dev/null
+++ b/apps/qwen-stock-visualization/README.md
@@ -0,0 +1,123 @@
+# 股票数据实时可视化系统
+
+这是一个直观、交互性强且个性化的股票数据可视化与模拟投资平台。
+
+## 功能特性
+
+- 用户身份认证(注册、登录)
+- 个性化股票仪表盘
+- 实时股票价格可视化
+- 自选股管理
+- 模拟交易功能
+- 投资组合管理
+- 收益追踪与分析
+
+## 技术栈
+
+- Next.js 14+
+- React
+- Supabase (PostgreSQL数据库、身份验证)
+- Tailwind CSS
+- Recharts (数据可视化)
+
+## 快速开始
+
+### 1. 环境配置
+
+首先,您需要在 Supabase 上创建一个项目并获取以下凭据:
+
+- `NEXT_PUBLIC_SUPABASE_URL` - 你的 Supabase 项目 URL
+- `NEXT_PUBLIC_SUPABASE_ANON_KEY` - 你的 Supabase 匿名密钥
+
+### 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
+```
+
+### 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 一键部署:
+
+[](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}
+
+
+
+
+
+
+ );
+}
\ 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 ? (
+
+ ) : 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 (
+
+ );
+ }
+
+ 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 (
+
+ {loading ? (
+ <>
+
+ 处理中...
+ >
+ ) : isAdded ? (
+ '已添加 ✓'
+ ) : (
+ '添加到自选股'
+ )}
+
+ );
+}
\ 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 ? (
+
+ ) : watchlist.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {/* 投资组合管理 */}
+
+
+
+ {/* 创建新投资组合 */}
+
+
创建新投资组合
+
+ 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'}%`}
+
+
+
{
+ e.stopPropagation();
+ handleDeletePortfolio(portfolio.id);
+ }}
+ className="btn btn-sm btn-outline-danger"
+ >
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+ {/* 右侧栏 - 图表和交易面板 */}
+
+ {/* 股票图表 */}
+ {selectedStock && historicalData.length > 0 && (
+
+ )}
+
+ {/* 交易面板 */}
+ {selectedPortfolio && selectedStock && showTradePanel && (
+
+
+
+ 交易 - {selectedStock.symbol} ({selectedStock.name})
+
+
+
+
+
+
+
+
+
+ {/* 交易表单 */}
+
+
+
+
+
+ 执行交易
+
+
+
+
+ 数量
+ setTradeForm({...tradeForm, quantity: parseInt(e.target.value) || 0})}
+ min="1"
+ className="form-control"
+ />
+
+
+
+ 价格 (USD)
+ setTradeForm({...tradeForm, price: parseFloat(e.target.value) || 0})}
+ className="form-control"
+ />
+
+
+
+
总金额
+
+ ${(tradeForm.quantity * tradeForm.price).toFixed(2)}
+
+
+
+
+ {tradeForm.type === 'buy' ? '买入' : '卖出'} {selectedStock.symbol}
+
+
+
+ {/* 当前持仓 */}
+
+
+
+
+
+ 当前持仓
+
+ {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'}%)
+
+
+
+ {
+ setSelectedStock(position.stocks);
+ setTradeForm({
+ quantity: 1,
+ price: currentPrice || position.avg_cost,
+ type: 'buy'
+ });
+ }}
+ >
+ 买入
+
+ {
+ setSelectedStock(position.stocks);
+ setTradeForm({
+ quantity: 1, // 默认为1股,用户可以修改
+ price: currentPrice || position.avg_cost,
+ type: 'sell'
+ });
+ }}
+ >
+ 卖出
+
+
+
+
+ );
+ })}
+
+
+
+ ) : (
+
+ )}
+
+
+ )}
+
+
+
+ );
+}
\ 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="输入您的密码"
+ />
+
+
+
+
+
+
+ {loading ? (
+ <>
+
+ 处理中...
+ >
+ ) : (
+ isLogin ? '登录' : '注册'
+ )}
+
+
+
+
+
+ setIsLogin(!isLogin)}
+ className="btn btn-link text-decoration-none text-primary fw-medium"
+ >
+ {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/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/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"
+ ]
+}