commit 271230fca748709e11c6ca2e7d01f168930d1f44 Author: William Tso Date: Fri Mar 21 12:08:37 2025 +0800 init ana page with apis diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/analytics_requirements_frontend.md b/analytics_requirements_frontend.md new file mode 100644 index 0000000..5b4150f --- /dev/null +++ b/analytics_requirements_frontend.md @@ -0,0 +1,135 @@ +# 短链接分析系统 - Next.js 前端实现需求 + +## 项目概述 + +基于 Next.js 框架实现短链接分析系统的前端部分,提供丰富的数据可视化和分析功能。 + +## 技术栈要求 +技术栈要求是最新的且是稳定版本 + +## 特别说明 +- 增删改link都不是在这个项目做的 +- 这个项目时负责展示link的统计与分析的而不是管理link的 + +## 实际需求 + + + +## 短链接概览 + +- 卡片式布局展示每个短链接的关键表现指标 +- 显示每个短链接的基础信息(名称、原始URL、创建日期) +- 突出展示三个核心指标:总访问量、独立访问用户数、平均停留时间 +- 每个指标旁边显示环比变化百分比和趋势箭头 +- 颜色编码直观表示表现好坏(绿色增长,红色下降) +- 支持时间范围切换(7天/30天/90天) +- 可按表现指标排序以识别表现最佳/最差的短链接 +- 帮助团队快速评估每个短链接的效果 +- 点击卡片可展开查看该短链接的详细分析 + +## 访问转化漏斗 + +- 以漏斗图形式展示用户从点击短链接到最终目标完成的全过程 +- 显示6个转化阶段:访问、停留、交互、注册、订阅、购买 +- 每个阶段显示用户数量和占比 +- 相邻阶段间显示转化率百分比 +- 底部显示三个关键指标:平均转化率、最高转化阶段、最低转化阶段 +- 根据所选项目和时间范围自动更新数据 + +## 访问趋势 + +- 柱状图形式展示一段时间内访问数量的变化 +- 横轴显示日期,纵轴显示访问数量 +- 每个柱体代表当天的访问总数 +- 悬停时显示具体访问数量 +- 自动计算最大值设置合适的比例尺 +- 使用蓝色渐变效果提高视觉吸引力 +- 帮助团队了解用户访问的时间规律 + +## 短链接表现 + +- 表格形式展示所有短链接数据 +- 每行显示一个短链接的关键指标:名称、原URL、创建者、创建日期 +- 包含流量指标:访问量、独立访问用户、跳出率、平均停留时间 +- 显示转化率评分 +- 支持按创建者和标签筛选 +- 可排序功能便于查找表现最佳短链接 + +## 平台分布 + +- 横向条形图展示不同来源平台的访问分布 +- 每个平台显示对应品牌颜色和图标 +- 显示具体数量和所占百分比 +- 条形长度直观反映各平台占比 +- 帮助团队了解哪些平台引流效果更好 + +## 链接状态分布 + +- 环形图展示短链接状态的分布情况 +- 包括三种状态:活跃、已过期、已禁用 +- 每个状态使用不同颜色直观区分 +- 显示各状态的数量和百分比 +- 提供短链接管理流程的整体视图 + +## 设备分析详情 + +- 横向渐变条展示访问设备类型分析 +- 从移动设备到桌面设备的直观展示 +- 显示移动端、平板、桌面端访问的准确百分比 +- 黑色指针标记在渐变条上的当前设备偏好位置 +- 帮助评估用户设备使用习惯和优化方向 + +## 热门链接 + +- 列表形式展示最受欢迎的短链接 +- 按访问量或转化率排序 +- 显示链接名称和访问数据 +- 标记高转化链接以引起注意 +- 帮助识别最成功的短链接类型 + +## 热门引荐来源 + +- 词云形式展示访问来源中出现频率最高的网站 +- 根据引荐量调整来源网站大小和颜色 +- 使用不同颜色区分不同类别的来源 +- 视觉化展现用户来源分布 +- 帮助团队了解用户访问的主要渠道 + +## 用户访问时间分析 + +- 24小时热力图展示一天中用户访问的高峰时段 +- 横轴显示24小时时间段,纵轴显示7天的日期 +- 颜色深浅表示访问量的多少 +- 自动标注访问高峰和低谷时段 +- 悬停时显示具体时间点的访问数据 +- 帮助优化短链接发布时间 +- 可按平台筛选查看不同来源平台的用户活跃规律 + +## 链接表现分析 + +- 散点图形式展示所有短链接的表现分布 +- 横轴表示访问量,纵轴表示转化率 +- 点的大小代表链接的停留时间 +- 点的颜色代表不同类型或标签 +- 四象限划分帮助识别高价值短链接 +- 鼠标悬停显示详细指标和链接信息 +- 支持按时间段、链接类型和创建者筛选 +- 帮助团队发现最有效的短链接模式 + +## QR码分析 + +- 展示与短链接关联的QR码使用情况 +- 显示每个QR码的扫描量和转化率 +- 支持按位置、活动或使用场景筛选 +- 提供QR码与短链接效果的对比分析 +- 帮助评估线上线下引流效果 + +## 概览卡片 + +- 展示三个核心指标的卡片式布局 +- 每个卡片包含大数字显示当前值和环比增长 +- 总访问量卡片:显示所有短链接访问总量及周环比变化 +- 平均转化率卡片:显示转化目标完成率及周环比 +- 活跃短链接卡片:显示有访问的短链接占比及周环比提升 +- 每个卡片包含对应图标和趋势指示器 +- 直观展示短链接系统的整体健康状况 \ No newline at end of file diff --git a/app/api/analytics/device-analysis/route.ts b/app/api/analytics/device-analysis/route.ts new file mode 100644 index 0000000..c1643ae --- /dev/null +++ b/app/api/analytics/device-analysis/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDeviceAnalysis } from '@/lib/analytics'; + +export async function GET(request: NextRequest) { + try { + // 获取请求参数 + const { searchParams } = new URL(request.url); + const linkId = searchParams.get('linkId') || undefined; + const startDate = searchParams.get('startDate') || undefined; + const endDate = searchParams.get('endDate') || undefined; + + // 获取设备分析详情 + const analysisData = await getDeviceAnalysis( + startDate, + endDate, + linkId + ); + + // 返回数据 + return NextResponse.json(analysisData); + } catch (error) { + console.error('Error in device-analysis API:', error); + return NextResponse.json( + { error: 'Failed to fetch device analysis data' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/analytics/funnel/route.ts b/app/api/analytics/funnel/route.ts new file mode 100644 index 0000000..9654b2a --- /dev/null +++ b/app/api/analytics/funnel/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getConversionFunnel } from '@/lib/analytics'; + +export async function GET(request: NextRequest) { + try { + // 获取请求参数 + const { searchParams } = new URL(request.url); + const linkId = searchParams.get('linkId'); + const startDate = searchParams.get('startDate') || undefined; + const endDate = searchParams.get('endDate') || undefined; + + // 验证必要参数 + if (!linkId) { + return NextResponse.json( + { error: 'Missing required parameter: linkId' }, + { status: 400 } + ); + } + + // 获取转化漏斗数据 + const funnelData = await getConversionFunnel( + linkId, + startDate || undefined, + endDate || undefined + ); + + // 返回数据 + return NextResponse.json(funnelData); + } catch (error) { + console.error('Error in funnel API:', error); + return NextResponse.json( + { error: 'Failed to fetch funnel data' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/analytics/link-performance/route.ts b/app/api/analytics/link-performance/route.ts new file mode 100644 index 0000000..d3234b0 --- /dev/null +++ b/app/api/analytics/link-performance/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getLinkPerformance } from '@/lib/analytics'; + +export async function GET(request: NextRequest) { + try { + // 获取请求参数 + const { searchParams } = new URL(request.url); + const linkId = searchParams.get('linkId'); + const startDate = searchParams.get('startDate') || undefined; + const endDate = searchParams.get('endDate') || undefined; + + // 验证必要参数 + if (!linkId) { + return NextResponse.json( + { error: 'Missing required parameter: linkId' }, + { status: 400 } + ); + } + + // 获取链接表现数据 + const performanceData = await getLinkPerformance( + linkId, + startDate || undefined, + endDate || undefined + ); + + // 返回数据 + return NextResponse.json(performanceData); + } catch (error) { + console.error('Error in link-performance API:', error); + return NextResponse.json( + { error: 'Failed to fetch link performance data' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/analytics/link-status-distribution/route.ts b/app/api/analytics/link-status-distribution/route.ts new file mode 100644 index 0000000..ae4961e --- /dev/null +++ b/app/api/analytics/link-status-distribution/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getLinkStatusDistribution } from '@/lib/analytics'; + +export async function GET(request: NextRequest) { + try { + // 获取请求参数 + const { searchParams } = new URL(request.url); + const projectId = searchParams.get('projectId') || undefined; + const startDate = searchParams.get('startDate') || undefined; + const endDate = searchParams.get('endDate') || undefined; + + // 获取链接状态分布数据 + const distributionData = await getLinkStatusDistribution( + startDate, + endDate, + projectId + ); + + // 返回数据 + return NextResponse.json(distributionData); + } catch (error) { + console.error('Error in link-status-distribution API:', error); + return NextResponse.json( + { error: 'Failed to fetch link status distribution data' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/analytics/overview-cards/route.ts b/app/api/analytics/overview-cards/route.ts new file mode 100644 index 0000000..89ba2ca --- /dev/null +++ b/app/api/analytics/overview-cards/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getOverviewCards } from '@/lib/analytics'; + +export async function GET(request: NextRequest) { + try { + // 获取请求参数 + const { searchParams } = new URL(request.url); + const projectId = searchParams.get('projectId') || undefined; + const startDate = searchParams.get('startDate') || undefined; + const endDate = searchParams.get('endDate') || undefined; + + // 获取概览卡片数据 + const cardsData = await getOverviewCards( + startDate, + endDate, + projectId + ); + + // 返回数据 + return NextResponse.json(cardsData); + } catch (error) { + console.error('Error in overview-cards API:', error); + return NextResponse.json( + { error: 'Failed to fetch overview cards data' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/analytics/overview/route.ts b/app/api/analytics/overview/route.ts new file mode 100644 index 0000000..270f973 --- /dev/null +++ b/app/api/analytics/overview/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getLinkOverview } from '@/lib/analytics'; + +export async function GET(request: NextRequest) { + try { + // 获取请求参数 + const { searchParams } = new URL(request.url); + const linkId = searchParams.get('linkId'); + const startDate = searchParams.get('startDate') || undefined; + const endDate = searchParams.get('endDate') || undefined; + + // 验证必要参数 + if (!linkId) { + return NextResponse.json( + { error: 'Missing required parameter: linkId' }, + { status: 400 } + ); + } + + // 获取链接概览数据 + const overviewData = await getLinkOverview( + linkId, + startDate || undefined, + endDate || undefined + ); + + // 返回数据 + return NextResponse.json(overviewData); + } catch (error) { + console.error('Error in overview API:', error); + return NextResponse.json( + { error: 'Failed to fetch overview data' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/analytics/platform-distribution/route.ts b/app/api/analytics/platform-distribution/route.ts new file mode 100644 index 0000000..eee370c --- /dev/null +++ b/app/api/analytics/platform-distribution/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getPlatformDistribution } from '@/lib/analytics'; + +export async function GET(request: NextRequest) { + try { + // 获取请求参数 + const { searchParams } = new URL(request.url); + const linkId = searchParams.get('linkId') || undefined; + const startDate = searchParams.get('startDate') || undefined; + const endDate = searchParams.get('endDate') || undefined; + + // 获取平台分布数据 + const distributionData = await getPlatformDistribution( + startDate, + endDate, + linkId + ); + + // 返回数据 + return NextResponse.json(distributionData); + } catch (error) { + console.error('Error in platform-distribution API:', error); + return NextResponse.json( + { error: 'Failed to fetch platform distribution data' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/analytics/popular-links/route.ts b/app/api/analytics/popular-links/route.ts new file mode 100644 index 0000000..d4e630b --- /dev/null +++ b/app/api/analytics/popular-links/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getPopularLinks } from '@/lib/analytics'; + +export async function GET(request: NextRequest) { + try { + // 获取请求参数 + const { searchParams } = new URL(request.url); + const projectId = searchParams.get('projectId') || undefined; + const startDate = searchParams.get('startDate') || undefined; + const endDate = searchParams.get('endDate') || undefined; + const sortBy = searchParams.get('sortBy') as 'visits' | 'uniqueVisitors' | 'conversionRate' || 'visits'; + const limit = searchParams.get('limit') ? parseInt(searchParams.get('limit') as string, 10) : 10; + + // 获取热门链接数据 + const linksData = await getPopularLinks( + startDate, + endDate, + projectId, + sortBy, + limit + ); + + // 返回数据 + return NextResponse.json(linksData); + } catch (error) { + console.error('Error in popular-links API:', error); + return NextResponse.json( + { error: 'Failed to fetch popular links data' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/analytics/popular-referrers/route.ts b/app/api/analytics/popular-referrers/route.ts new file mode 100644 index 0000000..a337691 --- /dev/null +++ b/app/api/analytics/popular-referrers/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getPopularReferrers } from '@/lib/analytics'; + +export async function GET(request: NextRequest) { + try { + // 获取请求参数 + const { searchParams } = new URL(request.url); + const linkId = searchParams.get('linkId') || undefined; + const startDate = searchParams.get('startDate') || undefined; + const endDate = searchParams.get('endDate') || undefined; + const type = searchParams.get('type') as 'domain' | 'full' || 'domain'; + const limit = searchParams.get('limit') ? parseInt(searchParams.get('limit') as string, 10) : 10; + + // 获取热门引荐来源数据 + const referrersData = await getPopularReferrers( + startDate, + endDate, + linkId, + type, + limit + ); + + // 返回数据 + return NextResponse.json(referrersData); + } catch (error) { + console.error('Error in popular-referrers API:', error); + return NextResponse.json( + { error: 'Failed to fetch popular referrers data' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/analytics/qr-code-analysis/route.ts b/app/api/analytics/qr-code-analysis/route.ts new file mode 100644 index 0000000..fbab81e --- /dev/null +++ b/app/api/analytics/qr-code-analysis/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getQrCodeAnalysis } from '@/lib/analytics'; + +export async function GET(request: NextRequest) { + try { + // 获取请求参数 + const { searchParams } = new URL(request.url); + const linkId = searchParams.get('linkId') || undefined; + const qrCodeId = searchParams.get('qrCodeId') || undefined; + const startDate = searchParams.get('startDate') || undefined; + const endDate = searchParams.get('endDate') || undefined; + + // 获取QR码分析数据 + const analysisData = await getQrCodeAnalysis( + startDate, + endDate, + linkId, + qrCodeId + ); + + // 返回数据 + return NextResponse.json(analysisData); + } catch (error) { + console.error('Error in qr-code-analysis API:', error); + return NextResponse.json( + { error: 'Failed to fetch QR code analysis data' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/analytics/track/route.ts b/app/api/analytics/track/route.ts new file mode 100644 index 0000000..8264251 --- /dev/null +++ b/app/api/analytics/track/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { trackEvent, EventType, ConversionType } from '@/lib/analytics'; + +export async function POST(request: NextRequest) { + try { + // 解析请求体 + const body = await request.json(); + + // 验证必要字段 + if (!body.linkId) { + return NextResponse.json( + { error: 'Missing required field: linkId' }, + { status: 400 } + ); + } + + if (!body.eventType || !Object.values(EventType).includes(body.eventType)) { + return NextResponse.json( + { + error: 'Invalid or missing eventType', + validValues: Object.values(EventType) + }, + { status: 400 } + ); + } + + // 验证转化类型(如果提供) + if ( + body.conversionType && + !Object.values(ConversionType).includes(body.conversionType) + ) { + return NextResponse.json( + { + error: 'Invalid conversionType', + validValues: Object.values(ConversionType) + }, + { status: 400 } + ); + } + + // 添加客户端IP + const clientIp = request.headers.get('x-forwarded-for') || + request.headers.get('x-real-ip') || + '0.0.0.0'; + + // 添加用户代理 + const userAgent = request.headers.get('user-agent') || ''; + + // 合并数据 + const eventData = { + ...body, + ipAddress: body.ipAddress || clientIp, + userAgent: body.userAgent || userAgent, + }; + + // 追踪事件 + const result = await trackEvent(eventData); + + // 返回结果 + return NextResponse.json(result); + } catch (error) { + console.error('Error in track API:', error); + return NextResponse.json( + { error: 'Failed to track event' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/analytics/trends/route.ts b/app/api/analytics/trends/route.ts new file mode 100644 index 0000000..cc0fbfd --- /dev/null +++ b/app/api/analytics/trends/route.ts @@ -0,0 +1,50 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getVisitTrends, TimeGranularity } from '@/lib/analytics'; + +export async function GET(request: NextRequest) { + try { + // 获取请求参数 + const { searchParams } = new URL(request.url); + const linkId = searchParams.get('linkId'); + const startDate = searchParams.get('startDate') || undefined; + const endDate = searchParams.get('endDate') || undefined; + const granularity = searchParams.get('granularity') as TimeGranularity || TimeGranularity.DAY; + + // 验证必要参数 + if (!linkId) { + return NextResponse.json( + { error: 'Missing required parameter: linkId' }, + { status: 400 } + ); + } + + // 验证粒度参数 + const validGranularities = Object.values(TimeGranularity); + if (granularity && !validGranularities.includes(granularity)) { + return NextResponse.json( + { + error: 'Invalid granularity value', + validValues: validGranularities + }, + { status: 400 } + ); + } + + // 获取访问趋势数据 + const trendsData = await getVisitTrends( + linkId, + startDate || undefined, + endDate || undefined, + granularity + ); + + // 返回数据 + return NextResponse.json(trendsData); + } catch (error) { + console.error('Error in trends API:', error); + return NextResponse.json( + { error: 'Failed to fetch trends data' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/links/[linkId]/details/route.ts b/app/api/links/[linkId]/details/route.ts new file mode 100644 index 0000000..1132eac --- /dev/null +++ b/app/api/links/[linkId]/details/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getLinkDetailsById } from '@/app/api/links/service'; + +export async function GET( + request: NextRequest, + context: { params: { linkId: string } } +) { + try { + const params = await context.params; + const linkId = params.linkId; + const link = await getLinkDetailsById(linkId); + + if (!link) { + return NextResponse.json( + { error: 'Link not found' }, + { status: 404 } + ); + } + + return NextResponse.json(link); + } catch (error) { + console.error('Failed to fetch link details:', error); + return NextResponse.json( + { error: 'Failed to fetch link details', message: (error as Error).message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/links/[linkId]/route.ts b/app/api/links/[linkId]/route.ts new file mode 100644 index 0000000..67ffcd6 --- /dev/null +++ b/app/api/links/[linkId]/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getLinkById } from '../service'; + +export async function GET( + request: NextRequest, + { params }: { params: { linkId: string } } +) { + try { + const { linkId } = params; + const link = await getLinkById(linkId); + + if (!link) { + return NextResponse.json( + { error: 'Link not found' }, + { status: 404 } + ); + } + + return NextResponse.json(link); + } catch (error) { + console.error('Failed to fetch link details:', error); + return NextResponse.json( + { error: 'Failed to fetch link details', message: (error as Error).message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/links/repository.ts b/app/api/links/repository.ts new file mode 100644 index 0000000..3760af6 --- /dev/null +++ b/app/api/links/repository.ts @@ -0,0 +1,157 @@ +import { executeQuery, executeQuerySingle } from '@/lib/clickhouse'; +import { Link, LinkQueryParams } from '../types'; + +/** + * Find links with filtering options + */ +export async function findLinks({ + limit = 10, + offset = 0, + searchTerm = '', + tagFilter = '', + isActive = null, +}: LinkQueryParams) { + // Build WHERE conditions + const conditions = []; + + if (searchTerm) { + conditions.push(` + (lower(title) LIKE lower('%${searchTerm}%') OR + lower(original_url) LIKE lower('%${searchTerm}%')) + `); + } + + if (tagFilter) { + conditions.push(`hasAny(tags, ['${tagFilter}'])`); + } + + if (isActive !== null) { + conditions.push(`is_active = ${isActive ? 'true' : 'false'}`); + } + + const whereClause = conditions.length > 0 + ? `WHERE ${conditions.join(' AND ')}` + : ''; + + // Get total count + const countQuery = ` + SELECT count() as total + FROM links + ${whereClause} + `; + + const countData = await executeQuery<{ total: number }>(countQuery); + const total = countData.length > 0 ? countData[0].total : 0; + + // 使用左连接获取链接数据和统计信息 + const linksQuery = ` + SELECT + l.link_id, + l.original_url, + l.created_at, + l.created_by, + l.title, + l.description, + l.tags, + l.is_active, + l.expires_at, + l.team_id, + l.project_id, + count(le.event_id) as visits, + count(DISTINCT le.visitor_id) as unique_visits + FROM links l + LEFT JOIN link_events le ON l.link_id = le.link_id + ${whereClause} + GROUP BY + l.link_id, + l.original_url, + l.created_at, + l.created_by, + l.title, + l.description, + l.tags, + l.is_active, + l.expires_at, + l.team_id, + l.project_id + ORDER BY l.created_at DESC + LIMIT ${limit} + OFFSET ${offset} + `; + + const links = await executeQuery(linksQuery); + + return { + links, + total, + limit, + offset, + page: Math.floor(offset / limit) + 1, + totalPages: Math.ceil(total / limit) + }; +} + +/** + * Find a single link by ID + */ +export async function findLinkById(linkId: string): Promise { + const query = ` + SELECT + l.link_id, + l.original_url, + l.created_at, + l.created_by, + l.title, + l.description, + l.tags, + l.is_active, + l.expires_at, + l.team_id, + l.project_id, + count(le.event_id) as visits, + count(DISTINCT le.visitor_id) as unique_visits + FROM links l + LEFT JOIN link_events le ON l.link_id = le.link_id + WHERE l.link_id = '${linkId}' + GROUP BY + l.link_id, + l.original_url, + l.created_at, + l.created_by, + l.title, + l.description, + l.tags, + l.is_active, + l.expires_at, + l.team_id, + l.project_id + LIMIT 1 + `; + + return await executeQuerySingle(query); +} + +/** + * Find a single link by ID - only basic info without statistics + */ +export async function findLinkDetailsById(linkId: string): Promise | null> { + const query = ` + SELECT + link_id, + original_url, + created_at, + created_by, + title, + description, + tags, + is_active, + expires_at, + team_id, + project_id + FROM links + WHERE link_id = '${linkId}' + LIMIT 1 + `; + + return await executeQuerySingle>(query); +} \ No newline at end of file diff --git a/app/api/links/route.ts b/app/api/links/route.ts new file mode 100644 index 0000000..be7e473 --- /dev/null +++ b/app/api/links/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { LinkQueryParams } from '../types'; +import { getLinks } from './service'; + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + + // Parse request parameters + const params: LinkQueryParams = { + limit: searchParams.has('limit') ? Number(searchParams.get('limit')) : 10, + page: searchParams.has('page') ? Number(searchParams.get('page')) : 1, + searchTerm: searchParams.get('search') || '', + tagFilter: searchParams.get('tag') || '', + }; + + // Handle active status filter + const activeFilter = searchParams.get('active'); + if (activeFilter === 'true') params.isActive = true; + if (activeFilter === 'false') params.isActive = false; + + // Get link data + const result = await getLinks(params); + return NextResponse.json(result); + } catch (error) { + console.error('Failed to fetch links:', error); + return NextResponse.json( + { error: 'Failed to fetch links', message: (error as Error).message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/links/service.ts b/app/api/links/service.ts new file mode 100644 index 0000000..8e7db77 --- /dev/null +++ b/app/api/links/service.ts @@ -0,0 +1,42 @@ +import { Link, LinkQueryParams, PaginatedResponse } from '../types'; +import { findLinkById, findLinkDetailsById, findLinks } from './repository'; + +/** + * Get links with pagination information + */ +export async function getLinks(params: LinkQueryParams): Promise> { + // Convert page number to offset + const { page, limit = 10, ...otherParams } = params; + const offset = page ? (page - 1) * limit : params.offset || 0; + + const result = await findLinks({ + ...otherParams, + limit, + offset + }); + + return { + data: result.links, + pagination: { + total: result.total, + limit: result.limit, + offset: result.offset, + page: result.page, + totalPages: result.totalPages + } + }; +} + +/** + * Get a single link by ID with full details (including statistics) + */ +export async function getLinkById(linkId: string): Promise { + return await findLinkById(linkId); +} + +/** + * Get a single link by ID - only basic info without statistics + */ +export async function getLinkDetailsById(linkId: string): Promise | null> { + return await findLinkDetailsById(linkId); +} \ No newline at end of file diff --git a/app/api/stats/repository.ts b/app/api/stats/repository.ts new file mode 100644 index 0000000..d8c98ba --- /dev/null +++ b/app/api/stats/repository.ts @@ -0,0 +1,21 @@ +import { executeQuerySingle } from '@/lib/clickhouse'; +import { StatsOverview } from '../types'; + +/** + * Get overview statistics for links + */ +export async function findStatsOverview(): Promise { + const query = ` + WITH + toUInt64(count()) as total_links, + toUInt64(countIf(is_active = true)) as active_links + FROM links + SELECT + total_links as totalLinks, + active_links as activeLinks, + (SELECT count() FROM link_events) as totalVisits, + (SELECT count() FROM link_events) / NULLIF(total_links, 0) as conversionRate + `; + + return await executeQuerySingle(query); +} \ No newline at end of file diff --git a/app/api/stats/route.ts b/app/api/stats/route.ts new file mode 100644 index 0000000..ed8dd49 --- /dev/null +++ b/app/api/stats/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server'; +import { getStatsOverview } from './service'; + +export async function GET() { + try { + const stats = await getStatsOverview(); + return NextResponse.json(stats); + } catch (error) { + console.error('获取统计概览失败:', error); + return NextResponse.json( + { error: '获取统计概览失败', message: (error as Error).message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/stats/service.ts b/app/api/stats/service.ts new file mode 100644 index 0000000..2a2d560 --- /dev/null +++ b/app/api/stats/service.ts @@ -0,0 +1,21 @@ +import { StatsOverview } from '../types'; +import { findStatsOverview } from './repository'; + +/** + * Get link statistics overview + */ +export async function getStatsOverview(): Promise { + const stats = await findStatsOverview(); + + // Return default values if no data + if (!stats) { + return { + totalLinks: 0, + activeLinks: 0, + totalVisits: 0, + conversionRate: 0 + }; + } + + return stats; +} \ No newline at end of file diff --git a/app/api/tags/repository.ts b/app/api/tags/repository.ts new file mode 100644 index 0000000..0b7a10f --- /dev/null +++ b/app/api/tags/repository.ts @@ -0,0 +1,19 @@ +import { executeQuery } from '@/lib/clickhouse'; +import { Tag } from '../types'; + +/** + * Get all tags with usage counts + */ +export async function findAllTags(): Promise { + const query = ` + SELECT + tag, + count() as count + FROM links + ARRAY JOIN tags as tag + GROUP BY tag + ORDER BY count DESC + `; + + return await executeQuery(query); +} \ No newline at end of file diff --git a/app/api/tags/route.ts b/app/api/tags/route.ts new file mode 100644 index 0000000..7b3f7e2 --- /dev/null +++ b/app/api/tags/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server'; +import { getAllTags } from './service'; + +export async function GET() { + try { + const tags = await getAllTags(); + return NextResponse.json(tags); + } catch (error) { + console.error('Failed to fetch tags:', error); + return NextResponse.json( + { error: 'Failed to fetch tags', message: (error as Error).message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/tags/service.ts b/app/api/tags/service.ts new file mode 100644 index 0000000..29b869f --- /dev/null +++ b/app/api/tags/service.ts @@ -0,0 +1,9 @@ +import { Tag } from '../types'; +import { findAllTags } from './repository'; + +/** + * Get all available tags + */ +export async function getAllTags(): Promise { + return await findAllTags(); +} \ No newline at end of file diff --git a/app/api/types.ts b/app/api/types.ts new file mode 100644 index 0000000..e2f5ce3 --- /dev/null +++ b/app/api/types.ts @@ -0,0 +1,221 @@ +// 链接数据类型 +export interface Link { + link_id: string; + original_url: string; + created_at: string; + created_by: string; + title: string; + description: string; + tags: string[]; + is_active: boolean; + expires_at: string | null; + team_id: string; + project_id: string; + visits: number; + unique_visits: number; +} + +// 分页响应类型 +export interface PaginatedResponse { + data: T[]; + pagination: { + total: number; + limit: number; + offset: number; + page: number; + totalPages: number; + } +} + +// 链接查询参数 +export interface LinkQueryParams { + limit?: number; + offset?: number; + page?: number; + searchTerm?: string; + tagFilter?: string; + isActive?: boolean | null; +} + +// 标签类型 +export interface Tag { + tag: string; + count: number; +} + +// 统计概览类型 +export interface StatsOverview { + totalLinks: number; + activeLinks: number; + totalVisits: number; + conversionRate: number; +} + +// Analytics数据类型 +export interface LinkOverviewData { + totalVisits: number; + uniqueVisitors: number; + averageTimeSpent: number; + bounceCount: number; + conversionCount: number; + uniqueReferrers: number; + deviceTypes: { + mobile: number; + tablet: number; + desktop: number; + other: number; + }; + qrScanCount: number; + totalConversionValue: number; +} + +export interface FunnelStep { + name: string; + value: number; + percent: number; +} + +export interface ConversionFunnelData { + steps: FunnelStep[]; + totalConversions: number; + conversionRate: number; +} + +export interface TrendPoint { + timestamp: string; + visits: number; + uniqueVisitors: number; +} + +export interface VisitTrendsData { + trends: TrendPoint[]; + totals: { + visits: number; + uniqueVisitors: number; + }; +} + +export interface TrackEventRequest { + linkId: string; + eventType: string; + visitorId?: string; + sessionId?: string; + referrer?: string; + userAgent?: string; + ipAddress?: string; + timeSpent?: number; + conversionType?: string; + conversionValue?: number; + customData?: Record; + isQrScan?: boolean; + qrCodeId?: string; + utmSource?: string; + utmMedium?: string; + utmCampaign?: string; +} + +export interface TrackEventResponse { + success: boolean; + eventId: string; + timestamp: string; +} + +// 链接表现数据 +export interface LinkPerformanceData { + totalClicks: number; + uniqueVisitors: number; + averageTimeSpent: number; + bounceRate: number; + uniqueReferrers: number; + conversionRate: number; + activeDays: number; + lastClickTime: string | null; + deviceDistribution: { + mobile: number; + desktop: number; + }; +} + +// 平台分布数据 +export interface PlatformItem { + name: string; + count: number; + percent: number; +} + +export interface PlatformDistributionData { + totalVisits: number; + platforms: PlatformItem[]; + browsers: PlatformItem[]; +} + +// 设备分析数据 +export interface DeviceItem { + name: string; + count: number; + percent: number; +} + +export interface DeviceModelItem { + type: string; + brand: string; + model: string; + count: number; + percent: number; +} + +export interface DeviceAnalysisData { + totalVisits: number; + deviceTypes: DeviceItem[]; + deviceBrands: DeviceItem[]; + deviceModels: DeviceModelItem[]; +} + +// 热门引荐来源数据 +export interface ReferrerItem { + source: string; + visitCount: number; + uniqueVisitors: number; + conversionCount: number; + conversionRate: number; + averageTimeSpent: number; + percent: number; +} + +export interface PopularReferrersData { + referrers: ReferrerItem[]; + totalVisits: number; +} + +// QR码分析数据 +export interface LocationItem { + city: string; + country: string; + scanCount: number; + percent: number; +} + +export interface DeviceDistributionItem { + type: string; + count: number; + percent: number; +} + +export interface HourlyDistributionItem { + hour: number; + scanCount: number; + percent: number; +} + +export interface QrCodeAnalysisData { + overview: { + totalScans: number; + uniqueScanners: number; + conversionCount: number; + conversionRate: number; + averageTimeSpent: number; + }; + locations: LocationItem[]; + deviceDistribution: DeviceDistributionItem[]; + hourlyDistribution: HourlyDistributionItem[]; +} \ No newline at end of file diff --git a/app/components/charts/ChartPlaceholder.tsx b/app/components/charts/ChartPlaceholder.tsx new file mode 100644 index 0000000..06d1241 --- /dev/null +++ b/app/components/charts/ChartPlaceholder.tsx @@ -0,0 +1,46 @@ +interface ChartPlaceholderProps { + text: string; + height?: string; + colorScheme?: 'blue' | 'green' | 'red' | 'purple' | 'teal' | 'orange' | 'pink' | 'yellow'; +} + +export default function ChartPlaceholder({ + text, + height = "h-64", + colorScheme = 'blue' +}: ChartPlaceholderProps) { + const borderColor = { + blue: 'border-accent-blue', + green: 'border-accent-green', + red: 'border-accent-red', + purple: 'border-accent-purple', + teal: 'border-accent-teal', + orange: 'border-accent-orange', + pink: 'border-accent-pink', + yellow: 'border-accent-yellow', + }[colorScheme]; + + const textColor = { + blue: 'text-accent-blue', + green: 'text-accent-green', + red: 'text-accent-red', + purple: 'text-accent-purple', + teal: 'text-accent-teal', + orange: 'text-accent-orange', + pink: 'text-accent-pink', + yellow: 'text-accent-yellow', + }[colorScheme]; + + return ( +
+
+
+ + + +
+

{text}

+
+
+ ); +} \ No newline at end of file diff --git a/app/components/dashboard/LinkDetailsCard.tsx b/app/components/dashboard/LinkDetailsCard.tsx new file mode 100644 index 0000000..42818ac --- /dev/null +++ b/app/components/dashboard/LinkDetailsCard.tsx @@ -0,0 +1,432 @@ +"use client"; + +import { useState, useEffect } from 'react'; + +interface LinkDetails { + id: string; + name: string; + shortUrl: string; + originalUrl: string; + creator: string; + createdAt: string; + visits: number; + visitChange: number; + uniqueVisitors: number; + uniqueVisitorsChange: number; + avgTime: string; + avgTimeChange: number; + conversionRate: number; + conversionChange: number; + status: 'active' | 'inactive' | 'expired'; + tags: string[]; +} + +interface LinkDetailsCardProps { + linkId: string | null; + onClose: () => void; +} + +export default function LinkDetailsCard({ linkId, onClose }: LinkDetailsCardProps) { + const [linkDetails, setLinkDetails] = useState(null); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState<'overview' | 'referrers' | 'devices' | 'locations'>('overview'); + + useEffect(() => { + if (linkId) { + // Simulate API call to fetch link details + const fetchData = async () => { + setLoading(true); + // In a real app, this would be an API call like: + // const response = await fetch(`/api/links/${linkId}`); + // const data = await response.json(); + + // For demo, using mock data + setTimeout(() => { + setLinkDetails({ + id: linkId, + name: "Product Launch Campaign", + shortUrl: "short.io/prlaunch", + originalUrl: "https://example.com/products/new-product-launch-summer-2023", + creator: "Sarah Johnson", + createdAt: "2023-05-15", + visits: 3240, + visitChange: 12.5, + uniqueVisitors: 2180, + uniqueVisitorsChange: 8.3, + avgTime: "2m 45s", + avgTimeChange: -5.2, + conversionRate: 4.8, + conversionChange: 1.2, + status: 'active', + tags: ["marketing", "product", "summer"] + }); + setLoading(false); + }, 800); + }; + + fetchData(); + } + }, [linkId]); + + if (!linkId) return null; + + return ( +
+
+
+ {/* Header */} +
+
+ {loading ? ( +
+ ) : ( +
+
+ + + + +
+

+ {linkDetails?.name} +

+
+ )} +
+ + +
+ + {loading ? ( +
+
+

Loading link details...

+
+ ) : ( + <> + {/* Link Info */} +
+
+
+
+ Short URL +
+ + {linkDetails?.shortUrl} + + +
+
+
+ Original URL + +
+
+
+
+
+
+ Created By +

{linkDetails?.creator}

+
+
+ Created At +

{linkDetails?.createdAt}

+
+
+ Status +
+ {linkDetails && ( + + {linkDetails.status.charAt(0).toUpperCase() + linkDetails.status.slice(1)} + + )} +
+
+ {linkDetails?.tags && linkDetails.tags.length > 0 && ( +
+ Tags +
+ {linkDetails.tags.map((tag) => ( + + {tag} + + ))} +
+
+ )} +
+
+
+ + {/* Metrics Overview */} + {linkDetails && ( +
+

Performance Metrics

+
+ {/* Total Visits */} +
+
+
Total Visits
+ = 0 + ? 'bg-green-500/10 text-accent-green' + : 'bg-red-500/10 text-accent-red' + }`} + > + = 0 ? 'text-accent-green' : 'text-accent-red transform rotate-180'}`} + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + + + {Math.abs(linkDetails.visitChange)}% + +
+
+

{linkDetails.visits.toLocaleString()}

+
+
+ + {/* Unique Visitors */} +
+
+
Unique Visitors
+ = 0 + ? 'bg-green-500/10 text-accent-green' + : 'bg-red-500/10 text-accent-red' + }`} + > + = 0 ? 'text-accent-green' : 'text-accent-red transform rotate-180'}`} + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + + + {Math.abs(linkDetails.uniqueVisitorsChange)}% + +
+
+

{linkDetails.uniqueVisitors.toLocaleString()}

+
+
+ + {/* Average Time */} +
+
+
Average Time
+ = 0 + ? 'bg-green-500/10 text-accent-green' + : 'bg-red-500/10 text-accent-red' + }`} + > + = 0 ? 'text-accent-green' : 'text-accent-red transform rotate-180'}`} + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + + + {Math.abs(linkDetails.avgTimeChange)}% + +
+
+

{linkDetails.avgTime}

+
+
+ + {/* Conversion Rate */} +
+
+
Conversion Rate
+ = 0 + ? 'bg-green-500/10 text-accent-green' + : 'bg-red-500/10 text-accent-red' + }`} + > + = 0 ? 'text-accent-green' : 'text-accent-red transform rotate-180'}`} + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + + + {Math.abs(linkDetails.conversionChange)}% + +
+
+

{linkDetails.conversionRate}%

+
+
+
+
+ )} + + {/* Tabs Navigation */} +
+ +
+ + {/* Tab Content */} +
+ {activeTab === 'overview' && ( +
+ + + +

No chart data available

+

+ Charts and detailed analytics would appear here. +

+
+ )} + + {activeTab === 'referrers' && ( +
+ + + +

No referrer data available

+

+ Information about traffic sources would appear here. +

+
+ )} + + {activeTab === 'devices' && ( +
+ + + +

No device data available

+

+ Breakdown of devices used to access the link would appear here. +

+
+ )} + + {activeTab === 'locations' && ( +
+ + + + +

No location data available

+

+ Geographic distribution of visitors would appear here. +

+
+ )} +
+ + {/* Footer */} +
+ +
+ + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/dashboard/StatsCard.tsx b/app/components/dashboard/StatsCard.tsx new file mode 100644 index 0000000..f2945c5 --- /dev/null +++ b/app/components/dashboard/StatsCard.tsx @@ -0,0 +1,72 @@ +interface StatsCardProps { + title: string; + value: string | number; + change: number; + unit?: string; + colorScheme?: 'blue' | 'green' | 'red' | 'purple'; +} + +export default function StatsCard({ + title, + value, + change, + unit = '', + colorScheme = 'blue' +}: StatsCardProps) { + const isPositive = change >= 0; + const hasPercentUnit = unit === '%'; + + // Color mappings based on the colorScheme + const gradientBg = { + blue: 'bg-gradient-blue', + green: 'bg-gradient-green', + red: 'bg-gradient-red', + purple: 'bg-gradient-purple', + }[colorScheme]; + + const accentColor = { + blue: 'text-accent-blue', + green: 'text-accent-green', + red: 'text-accent-red', + purple: 'text-accent-purple', + }[colorScheme]; + + return ( +
+ {/* Colorful top bar */} +
+ +
+

{title}

+ +
+
+

+ {value} + {hasPercentUnit && %} +

+ +
+ + {isPositive ? '+' : ''}{change}% +
+
+ + {/* Visual indicator for percentages */} + {hasPercentUnit && ( +
+
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/layout/Navbar.tsx b/app/components/layout/Navbar.tsx new file mode 100644 index 0000000..573dc9b --- /dev/null +++ b/app/components/layout/Navbar.tsx @@ -0,0 +1,67 @@ +'use client'; + +import Link from 'next/link'; +import ThemeToggle from "../ui/ThemeToggle"; + +export default function Navbar() { + return ( +
+
+
+ + + + + + ShortURL + + +
+
+ + + +
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/ui/Card.tsx b/app/components/ui/Card.tsx new file mode 100644 index 0000000..804ccb4 --- /dev/null +++ b/app/components/ui/Card.tsx @@ -0,0 +1,57 @@ +import { ReactNode } from 'react'; + +interface CardProps { + title: string; + children: ReactNode; + className?: string; + colorScheme?: 'blue' | 'green' | 'red' | 'purple' | 'teal' | 'orange' | 'pink' | 'yellow' | 'none'; + glowEffect?: boolean; +} + +export default function Card({ + title, + children, + className = '', + colorScheme = 'none', + glowEffect = false +}: CardProps) { + // Only add color-specific classes if a colorScheme is specified + const headerColor = colorScheme !== 'none' ? { + blue: 'text-accent-blue', + green: 'text-accent-green', + red: 'text-accent-red', + purple: 'text-accent-purple', + teal: 'text-accent-teal', + orange: 'text-accent-orange', + pink: 'text-accent-pink', + yellow: 'text-accent-yellow', + }[colorScheme] : 'text-foreground'; + + const glowClass = glowEffect && colorScheme !== 'none' ? { + blue: 'shadow-[0_0_15px_rgba(59,130,246,0.15)]', + green: 'shadow-[0_0_15px_rgba(16,185,129,0.15)]', + red: 'shadow-[0_0_15px_rgba(244,63,94,0.15)]', + purple: 'shadow-[0_0_15px_rgba(139,92,246,0.15)]', + teal: 'shadow-[0_0_15px_rgba(20,184,166,0.15)]', + orange: 'shadow-[0_0_15px_rgba(249,115,22,0.15)]', + pink: 'shadow-[0_0_15px_rgba(236,72,153,0.15)]', + yellow: 'shadow-[0_0_15px_rgba(245,158,11,0.15)]', + }[colorScheme] : ''; + + // Define the indicator dot color + const indicatorColor = colorScheme !== 'none' ? `bg-accent-${colorScheme}` : 'bg-gray-500'; + + return ( +
+
+

{title}

+ {colorScheme !== 'none' && ( +
+ )} +
+
+ {children} +
+
+ ); +} \ No newline at end of file diff --git a/app/components/ui/CreateLinkModal.tsx b/app/components/ui/CreateLinkModal.tsx new file mode 100644 index 0000000..6b4da63 --- /dev/null +++ b/app/components/ui/CreateLinkModal.tsx @@ -0,0 +1,242 @@ +"use client"; + +import { useState } from 'react'; + +interface LinkData { + name: string; + originalUrl: string; + customSlug: string; + expiresAt: string; + tags: string[]; +} + +interface CreateLinkModalProps { + onClose: () => void; + onSubmit: (linkData: LinkData) => void; +} + +export default function CreateLinkModal({ onClose, onSubmit }: CreateLinkModalProps) { + const [formData, setFormData] = useState({ + name: '', + originalUrl: '', + customSlug: '', + expiresAt: '', + tags: [] as string[], + tagInput: '' + }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + const handleTagKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && formData.tagInput.trim()) { + e.preventDefault(); + addTag(); + } + }; + + const addTag = () => { + if (formData.tagInput.trim() && !formData.tags.includes(formData.tagInput.trim())) { + setFormData(prev => ({ + ...prev, + tags: [...prev.tags, prev.tagInput.trim()], + tagInput: '' + })); + } + }; + + const removeTag = (tagToRemove: string) => { + setFormData(prev => ({ + ...prev, + tags: prev.tags.filter(tag => tag !== tagToRemove) + })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { tagInput, ...submitData } = formData; + onSubmit(submitData as LinkData); + }; + + return ( +
+
+
+ {/* Header */} +
+
+
+ + + +
+

+ Create New Link +

+
+ + +
+ + {/* Form */} +
+ {/* Link Name */} +
+ + +
+ + {/* Original URL */} +
+ + +
+ + {/* Custom Slug */} +
+ +
+ + short.io/ + + +
+

+ Leave blank to generate a random slug +

+
+ + {/* Expiration Date */} +
+ + +

+ Leave blank for a non-expiring link +

+
+ + {/* Tags */} +
+ +
+ + +
+ + {formData.tags.length > 0 && ( +
+ {formData.tags.map(tag => ( + + {tag} + + + ))} +
+ )} +
+
+ + {/* Footer */} +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/ui/ThemeToggle.tsx b/app/components/ui/ThemeToggle.tsx new file mode 100644 index 0000000..1b6618c --- /dev/null +++ b/app/components/ui/ThemeToggle.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { useState, useEffect } from 'react'; + +export default function ThemeToggle() { + const [darkMode, setDarkMode] = useState(false); + + // Initialize theme on component mount + useEffect(() => { + const isDarkMode = localStorage.getItem('darkMode') === 'true'; + setDarkMode(isDarkMode); + + if (isDarkMode) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }, []); + + // Update theme when darkMode state changes + const toggleTheme = () => { + const newDarkMode = !darkMode; + setDarkMode(newDarkMode); + localStorage.setItem('darkMode', newDarkMode.toString()); + + if (newDarkMode) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }; + + return ( + + ); +} \ No newline at end of file diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..b739895 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,90 @@ +@import "tailwindcss"; + +:root { + /* Light Mode - Default */ + --background: #f8fafc; + --foreground: #0f172a; + + /* Card colors */ + --card-bg: #ffffff; + --card-border: #e2e8f0; + + /* Vibrant accent colors */ + --accent-blue: #3b82f6; + --accent-green: #10b981; + --accent-red: #f43f5e; + --accent-yellow: #f59e0b; + --accent-purple: #8b5cf6; + --accent-pink: #ec4899; + --accent-teal: #14b8a6; + --accent-orange: #f97316; + + /* UI colors */ + --text-secondary: #64748b; + --progress-bg: #e2e8f0; + + /* Gradient colors */ + --gradient-blue: linear-gradient(135deg, #3b82f6, #2563eb); + --gradient-purple: linear-gradient(135deg, #8b5cf6, #7c3aed); + --gradient-green: linear-gradient(135deg, #10b981, #059669); + --gradient-red: linear-gradient(135deg, #f43f5e, #e11d48); +} + +.dark { + /* Dark Mode */ + --background: #0f172a; + --foreground: #ffffff; + + /* Card colors */ + --card-bg: #1e293b; + --card-border: #334155; + + /* Vibrant accent colors */ + --accent-blue: #3b82f6; + --accent-green: #10b981; + --accent-red: #f43f5e; + --accent-yellow: #f59e0b; + --accent-purple: #8b5cf6; + --accent-pink: #ec4899; + --accent-teal: #14b8a6; + --accent-orange: #f97316; + + /* UI colors */ + --text-secondary: #94a3b8; + --progress-bg: #334155; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +body { + background: var(--background); + color: var(--foreground); + font-family: var(--font-sans), sans-serif; +} + +/* Colorful gradient borders */ +.gradient-border { + position: relative; + border-radius: 0.5rem; + overflow: hidden; +} + +.gradient-border::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 0.5rem; + padding: 2px; + background: linear-gradient(45deg, var(--accent-blue), var(--accent-purple), var(--accent-pink)); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..ba6fea6 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,38 @@ +import './globals.css'; +import type { Metadata } from 'next'; +import { Geist, Geist_Mono } from "next/font/google"; +import Navbar from "./components/layout/Navbar"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: 'ShortURL Analytics', + description: 'Analytics dashboard for short URL management', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + +
+ {children} +
+ + + ); +} diff --git a/app/layouts.tsx b/app/layouts.tsx new file mode 100644 index 0000000..83deb4c --- /dev/null +++ b/app/layouts.tsx @@ -0,0 +1,21 @@ +import './globals.css'; +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Link Management & Analytics', + description: 'Track and analyze shortened links', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} \ No newline at end of file diff --git a/app/links/[id]/page.tsx b/app/links/[id]/page.tsx new file mode 100644 index 0000000..28af355 --- /dev/null +++ b/app/links/[id]/page.tsx @@ -0,0 +1,1354 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { + LinkOverviewData, + ConversionFunnelData, + VisitTrendsData, + LinkPerformanceData, + PlatformDistributionData, + DeviceAnalysisData, + PopularReferrersData, + QrCodeAnalysisData, + DeviceItem, + ReferrerItem, +} from "@/app/api/types"; +import { TimeGranularity } from "@/lib/analytics"; + +interface LinkDetails { + id: string; + name: string; + shortUrl: string; + originalUrl: string; + creator: string; + createdAt: string; + visits: number; + visitChange: number; + uniqueVisitors: number; + uniqueVisitorsChange: number; + avgTime: string; + avgTimeChange: number; + conversionRate: number; + conversionChange: number; + status: "active" | "inactive" | "expired"; + tags: string[]; +} + +export default function LinkDetailsPage({ + params, +}: { + params: { id: string }; +}) { + const router = useRouter(); + const [linkId, setLinkId] = useState(""); + + const [linkDetails, setLinkDetails] = useState(null); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState< + | "overview" + | "referrers" + | "devices" + | "locations" + | "performance" + | "qrCodes" + >("overview"); + + // 添加state变量存储分析数据 + const [overviewData, setOverviewData] = useState( + null + ); + const [funnelData, setFunnelData] = useState( + null + ); + const [trendsData, setTrendsData] = useState(null); + // 添加新的state变量存储新API的数据 + const [performanceData, setPerformanceData] = + useState(null); + const [platformData, setPlatformData] = + useState(null); + const [deviceData, setDeviceData] = useState(null); + const [referrersData, setReferrersData] = + useState(null); + const [qrCodeData, setQrCodeData] = useState(null); + + const [timeGranularity, setTimeGranularity] = useState( + TimeGranularity.DAY + ); + const [dateRange, setDateRange] = useState({ + startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0], // 30天前 + endDate: new Date().toISOString().split("T")[0], // 今天 + }); + + // 获取并设置linkId + useEffect(() => { + const loadParams = async () => { + const resolvedParams = await params; + setLinkId(resolvedParams.id); + }; + + loadParams(); + }, [params]); + + // 获取链接详情 + useEffect(() => { + if (!linkId) return; // 等待linkId加载完成 + + const fetchLinkDetails = async () => { + setLoading(true); + try { + // 调用API获取链接基本信息 + const response = await fetch(`/api/links/${linkId}/details`); + if (!response.ok) { + throw new Error( + `Failed to fetch link details: ${response.statusText}` + ); + } + + const data = await response.json(); + + // 转换API数据为UI所需格式 + setLinkDetails({ + id: data.link_id, + name: data.title || "Untitled Link", + shortUrl: generateShortUrlDisplay(data.link_id, data.original_url), + originalUrl: data.original_url, + creator: data.created_by, + createdAt: new Date(data.created_at).toLocaleDateString(), + visits: 0, // 不包含统计数据 + visitChange: 0, + uniqueVisitors: 0, + uniqueVisitorsChange: 0, + avgTime: "0m 0s", + avgTimeChange: 0, + conversionRate: 0, + conversionChange: 0, + status: data.is_active ? "active" : "inactive", + tags: data.tags || [], + }); + + // 加载分析数据 + await fetchAnalyticsData(data.link_id); + } catch (error) { + console.error("Failed to fetch link details:", error); + setLoading(false); + } + }; + + fetchLinkDetails(); + }, [linkId]); + + // 获取链接分析数据 + const fetchAnalyticsData = async (id: string) => { + try { + // 构建查询参数 + const queryParams = new URLSearchParams({ + linkId: id, + startDate: dateRange.startDate, + endDate: dateRange.endDate, + }); + + // 并行获取所有数据 + const [ + overviewResponse, + funnelResponse, + trendsResponse, + performanceResponse, + platformResponse, + deviceResponse, + referrersResponse, + qrCodeResponse, + ] = await Promise.all([ + fetch(`/api/analytics/overview?${queryParams}`), + fetch(`/api/analytics/funnel?${queryParams}`), + fetch( + `/api/analytics/trends?${queryParams}&granularity=${timeGranularity}` + ), + fetch(`/api/analytics/link-performance?${queryParams}`), + fetch(`/api/analytics/platform-distribution?${queryParams}`), + fetch(`/api/analytics/device-analysis?${queryParams}`), + fetch(`/api/analytics/popular-referrers?${queryParams}`), + fetch(`/api/analytics/qr-code-analysis?${queryParams}`), + ]); + + // 检查所有响应 + if ( + !overviewResponse.ok || + !funnelResponse.ok || + !trendsResponse.ok || + !performanceResponse.ok || + !platformResponse.ok || + !deviceResponse.ok || + !referrersResponse.ok || + !qrCodeResponse.ok + ) { + throw new Error("Failed to fetch analytics data"); + } + + // 解析所有响应数据 + const [ + overviewResult, + funnelResult, + trendsResult, + performanceResult, + platformResult, + deviceResult, + referrersResult, + qrCodeResult, + ] = await Promise.all([ + overviewResponse.json(), + funnelResponse.json(), + trendsResponse.json(), + performanceResponse.json(), + platformResponse.json(), + deviceResponse.json(), + referrersResponse.json(), + qrCodeResponse.json(), + ]); + + // 设置状态 + setOverviewData(overviewResult); + setFunnelData(funnelResult); + setTrendsData(trendsResult); + setPerformanceData(performanceResult); + setPlatformData(platformResult); + setDeviceData(deviceResult); + setReferrersData(referrersResult); + setQrCodeData(qrCodeResult); + + // 更新链接详情中的统计数据 + setLinkDetails((prev) => { + if (!prev) return prev; + + return { + ...prev, + visits: overviewResult.totalVisits, + uniqueVisitors: overviewResult.uniqueVisitors, + avgTime: formatTime(overviewResult.averageTimeSpent), + conversionRate: funnelResult.conversionRate, + }; + }); + + setLoading(false); + } catch (error) { + console.error("Failed to fetch analytics data:", error); + setLoading(false); + } + }; + + // 格式化时间(秒转为分钟和秒) + const formatTime = (seconds: number) => { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.round(seconds % 60); + return `${minutes}m ${remainingSeconds}s`; + }; + + // 更新时间粒度并重新获取趋势数据 + const updateTimeGranularity = (granularity: TimeGranularity) => { + setTimeGranularity(granularity); + if (linkId) { + const queryParams = new URLSearchParams({ + linkId, + startDate: dateRange.startDate, + endDate: dateRange.endDate, + granularity, + }); + + fetch(`/api/analytics/trends?${queryParams}`) + .then((res) => res.json()) + .then((data) => setTrendsData(data)) + .catch((err) => console.error("Failed to update trends data:", err)); + } + }; + + // 更新日期范围并重新获取所有数据 + const updateDateRange = (startDate: string, endDate: string) => { + setDateRange({ startDate, endDate }); + if (linkId) { + fetchAnalyticsData(linkId); + } + }; + + // 从link_id和原始URL生成短URL显示 + const generateShortUrlDisplay = ( + linkId: string, + originalUrl: string + ): string => { + try { + // 尝试从原始URL提取域名 + const urlObj = new URL(originalUrl); + const domain = urlObj.hostname.replace("www.", ""); + + // 使用link_id的前8个字符作为短代码 + const shortCode = linkId.substring(0, 8); + + return `${domain}/${shortCode}`; + } catch { + // 如果URL解析失败,返回一个基于linkId的默认值 + return `short.link/${linkId.substring(0, 8)}`; + } + }; + + const goBack = () => { + router.back(); + }; + + return ( +
+
+ {/* 顶部导航栏 */} +
+
+ +
+ +
+ +
+
+ + {loading ? ( +
+
+

Loading link details...

+
+ ) : linkDetails ? ( + <> + {/* 链接基本信息卡片 */} +
+
+
+

+ {linkDetails.name} +

+ +
+
+ + Short URL + +
+ + {linkDetails.shortUrl} + + +
+
+ +
+ + Original URL + + +
+
+
+ +
+
+
+ + Created By + +

+ {linkDetails.creator} +

+
+ +
+ + Created At + +

+ {linkDetails.createdAt} +

+
+ +
+ + Status + +
+ + {linkDetails.status.charAt(0).toUpperCase() + + linkDetails.status.slice(1)} + +
+
+ + {linkDetails.tags && linkDetails.tags.length > 0 && ( +
+ + Tags + +
+ {linkDetails.tags.map((tag) => ( + + {tag} + + ))} +
+
+ )} +
+
+
+ + {/* 性能指标卡片 */} +
+

+ Performance Metrics +

+
+ {/* Total Visits */} +
+
+
+ Total Visits +
+ = 0 + ? "bg-green-500/10 text-accent-green" + : "bg-red-500/10 text-accent-red" + }`} + > + = 0 ? "text-accent-green" : "text-accent-red transform rotate-180"}`} + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + + + {Math.abs(linkDetails.visitChange)}% + +
+
+

+ {linkDetails.visits.toLocaleString()} +

+
+
+ + {/* Unique Visitors */} +
+
+
+ Unique Visitors +
+ = 0 + ? "bg-green-500/10 text-accent-green" + : "bg-red-500/10 text-accent-red" + }`} + > + = 0 ? "text-accent-green" : "text-accent-red transform rotate-180"}`} + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + + + {Math.abs(linkDetails.uniqueVisitorsChange)}% + +
+
+

+ {linkDetails.uniqueVisitors.toLocaleString()} +

+
+
+ + {/* Average Visit Time */} +
+
+
+ Avg. Time +
+ = 0 + ? "bg-green-500/10 text-accent-green" + : "bg-red-500/10 text-accent-red" + }`} + > + = 0 ? "text-accent-green" : "text-accent-red transform rotate-180"}`} + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + + + {Math.abs(linkDetails.avgTimeChange)}% + +
+
+

+ {linkDetails.avgTime} +

+
+
+ + {/* Conversion Rate */} +
+
+
+ Conversion +
+ = 0 + ? "bg-green-500/10 text-accent-green" + : "bg-red-500/10 text-accent-red" + }`} + > + = 0 ? "text-accent-green" : "text-accent-red transform rotate-180"}`} + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + + + {Math.abs(linkDetails.conversionChange)}% + +
+
+

+ {linkDetails.conversionRate}% +

+
+
+
+
+
+ + {/* 图表和详细数据部分 */} +
+
+ +
+ +
+ {activeTab === "overview" && ( +
+ {/* 日期范围选择器 */} +
+
+ Analytics Overview +
+
+
+ + updateDateRange(e.target.value, dateRange.endDate) + } + className="px-3 py-2 text-sm border rounded-md bg-card-bg border-card-border" + /> +
+ + to + +
+ + updateDateRange( + dateRange.startDate, + e.target.value + ) + } + className="px-3 py-2 text-sm border rounded-md bg-card-bg border-card-border" + min={dateRange.startDate} + /> +
+
+
+ + {/* 设备类型分布 */} + {overviewData && ( +
+

+ Device Types +

+
+
+
+ Mobile +
+
+ {overviewData.deviceTypes.mobile} +
+
+ {overviewData.totalVisits + ? Math.round( + (overviewData.deviceTypes.mobile / + overviewData.totalVisits) * + 100 + ) + : 0} + % +
+
+
+
+ Desktop +
+
+ {overviewData.deviceTypes.desktop} +
+
+ {overviewData.totalVisits + ? Math.round( + (overviewData.deviceTypes.desktop / + overviewData.totalVisits) * + 100 + ) + : 0} + % +
+
+
+
+ Tablet +
+
+ {overviewData.deviceTypes.tablet} +
+
+ {overviewData.totalVisits + ? Math.round( + (overviewData.deviceTypes.tablet / + overviewData.totalVisits) * + 100 + ) + : 0} + % +
+
+
+
+ Other +
+
+ {overviewData.deviceTypes.other} +
+
+ {overviewData.totalVisits + ? Math.round( + (overviewData.deviceTypes.other / + overviewData.totalVisits) * + 100 + ) + : 0} + % +
+
+
+
+ )} + + {/* 转化漏斗 */} + {funnelData && ( +
+
+

+ Conversion Funnel +

+
+ Overall Conversion Rate:{" "} + + {(funnelData.conversionRate || 0).toFixed(2)}% + +
+
+ +
+ {funnelData.steps.map((step) => ( +
+
+ + {step.name} + + + {step.value} ({(step.percent || 0).toFixed(1)} + %) + +
+
+
+
+
+ ))} +
+
+ )} + + {/* 访问趋势 */} + {trendsData && ( +
+
+

+ Visit Trends +

+
+ {Object.values(TimeGranularity).map( + (granularity) => ( + + ) + )} +
+
+ +
+
+
+ Total Visits:{" "} + + {trendsData.totals.visits} + +
+
+ Unique Visitors:{" "} + + {trendsData.totals.uniqueVisitors} + +
+
+ + {/* 简单趋势表格 */} +
+ + + + + + + + + + {trendsData.trends.map((trend, i) => ( + + + + + + ))} + +
+ Time + + Visits + + Unique Visitors +
+ {trend.timestamp} + + {trend.visits} + + {trend.uniqueVisitors} +
+
+
+
+ )} +
+ )} + + {activeTab === "performance" && performanceData && ( +
+
+

+ Link Performance +

+
+
+
+ Total Clicks +
+
+ {performanceData.totalClicks} +
+
+
+
+ Unique Visitors +
+
+ {performanceData.uniqueVisitors} +
+
+
+
+ Bounce Rate +
+
+ {performanceData.bounceRate}% +
+
+
+
+ Conversion Rate +
+
+ {performanceData.conversionRate}% +
+
+
+
+ Avg. Time Spent +
+
+ {formatTime(performanceData.averageTimeSpent)} +
+
+
+
+ Active Days +
+
+ {performanceData.activeDays} +
+
+
+
+ Unique Referrers +
+
+ {performanceData.uniqueReferrers} +
+
+
+
+ Last Click +
+
+ {performanceData.lastClickTime + ? new Date( + performanceData.lastClickTime + ).toLocaleString() + : "Never"} +
+
+
+
+
+ )} + + {activeTab === "referrers" && referrersData && ( +
+
+

+ Popular Referrers +

+
+ + + + + + + + + + + + {referrersData.referrers.map( + (referrer: ReferrerItem, i: number) => ( + + + + + + + + ) + )} + +
+ Source + + Visits + + Unique Visitors + + Conversion Rate + + Avg. Time Spent +
+ {referrer.source} + + {referrer.visitCount} ({referrer.percent}%) + + {referrer.uniqueVisitors} + + {referrer.conversionRate}% + + {formatTime(referrer.averageTimeSpent)} +
+
+
+
+ )} + + {activeTab === "devices" && deviceData && ( +
+
+

+ Device Types +

+
+ {deviceData.deviceTypes.map( + (device: DeviceItem, i: number) => ( +
+
+ {device.name} +
+
+ {device.count} +
+
+ {device.percent}% +
+
+ ) + )} +
+
+ +
+

+ Device Brands +

+
+ + + + + + + + + + {deviceData.deviceBrands.map( + (brand: DeviceItem, i: number) => ( + + + + + + ) + )} + +
+ Brand + + Count + + Percentage +
+ {brand.name} + + {brand.count} + + {brand.percent}% +
+
+
+
+ )} + + {activeTab === "locations" && platformData && ( +
+
+

+ Platform Distribution +

+
+
+

+ Operating Systems +

+
+ + + + + + + + + + {platformData.platforms.map((platform, i) => ( + + + + + + ))} + +
+ OS + + Visits + + Percentage +
+ {platform.name} + + {platform.count} + + {platform.percent}% +
+
+
+ +
+

+ Browsers +

+
+ + + + + + + + + + {platformData.browsers.map((browser, i) => ( + + + + + + ))} + +
+ Browser + + Visits + + Percentage +
+ {browser.name} + + {browser.count} + + {browser.percent}% +
+
+
+
+
+
+ )} + + {activeTab === "qrCodes" && qrCodeData && ( +
+
+

+ QR Code Analysis +

+
+
+
+ Total Scans +
+
+ {qrCodeData.overview.totalScans} +
+
+
+
+ Unique Scanners +
+
+ {qrCodeData.overview.uniqueScanners} +
+
+
+
+ Conversion Rate +
+
+ {qrCodeData.overview.conversionRate}% +
+
+
+
+ Avg. Time Spent +
+
+ {formatTime(qrCodeData.overview.averageTimeSpent)} +
+
+
+
+ +
+

+ Scan Locations +

+
+ + + + + + + + + + {qrCodeData.locations.map((location, i) => ( + + + + + + ))} + +
+ Location + + Scans + + Percentage +
+ {location.city}, {location.country} + + {location.scanCount} + + {location.percent}% +
+
+
+ +
+

+ Scan Time Distribution +

+
+
+ + + + + + + + + + {qrCodeData.hourlyDistribution.map((hour) => ( + + + + + + ))} + +
+ Hour + + Scans + + Percentage +
+ {hour.hour}:00 - {hour.hour}:59 + + {hour.scanCount} + + {hour.percent.toFixed(1)}% +
+
+
+
+
+ )} +
+
+ + ) : ( +
+
+ + + +

+ Link Not Found +

+

+ The link you're looking for doesn't exist or has been removed. +

+ + Return to Links Page + +
+
+ )} +
+
+ ); +} diff --git a/app/links/page.tsx b/app/links/page.tsx new file mode 100644 index 0000000..cbc2b16 --- /dev/null +++ b/app/links/page.tsx @@ -0,0 +1,473 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import CreateLinkModal from '../components/ui/CreateLinkModal'; +import { Link, StatsOverview, Tag } from '../api/types'; + +// Define type for link data +interface LinkData { + name: string; + originalUrl: string; + customSlug: string; + expiresAt: string; + tags: string[]; +} + +// 映射API数据到UI所需格式 +interface UILink { + id: string; + name: string; + shortUrl: string; + originalUrl: string; + creator: string; + createdAt: string; + visits: number; + visitChange: number; + uniqueVisitors: number; + uniqueVisitorsChange: number; + avgTime: string; + avgTimeChange: number; + conversionRate: number; + conversionChange: number; + status: string; + tags: string[]; +} + +export default function LinksPage() { + const [links, setLinks] = useState([]); + const [allTags, setAllTags] = useState([]); + const [stats, setStats] = useState({ + totalLinks: 0, + activeLinks: 0, + totalVisits: 0, + conversionRate: 0 + }); + + const [searchQuery, setSearchQuery] = useState(''); + const [showCreateModal, setShowCreateModal] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // 映射API数据到UI所需格式的函数 + const mapApiLinkToUiLink = (apiLink: Link): UILink => { + // 生成短URL显示 - 因为数据库中没有short_url字段 + const shortUrlDisplay = generateShortUrlDisplay(apiLink.link_id, apiLink.original_url); + + return { + id: apiLink.link_id, + name: apiLink.title || 'Untitled Link', + shortUrl: shortUrlDisplay, + originalUrl: apiLink.original_url, + creator: apiLink.created_by, + createdAt: new Date(apiLink.created_at).toLocaleDateString(), + visits: apiLink.visits, + visitChange: 0, // API doesn't provide change data yet + uniqueVisitors: apiLink.unique_visits, + uniqueVisitorsChange: 0, + avgTime: '0m 0s', // API doesn't provide average time yet + avgTimeChange: 0, + conversionRate: 0, // API doesn't provide conversion rate yet + conversionChange: 0, + status: apiLink.is_active ? 'active' : 'inactive', + tags: apiLink.tags || [] + }; + }; + + // 从link_id和原始URL生成短URL显示 + const generateShortUrlDisplay = (linkId: string, originalUrl: string): string => { + try { + // 尝试从原始URL提取域名 + const urlObj = new URL(originalUrl); + const domain = urlObj.hostname.replace('www.', ''); + + // 使用link_id的前8个字符作为短代码 + const shortCode = linkId.substring(0, 8); + + return `${domain}/${shortCode}`; + } catch { + // 如果URL解析失败,返回一个基于linkId的默认值 + return `short.link/${linkId.substring(0, 8)}`; + } + }; + + // 获取链接数据 + useEffect(() => { + const fetchLinks = async () => { + try { + setIsLoading(true); + setError(null); + + // 获取链接列表 + const linksResponse = await fetch('/api/links'); + if (!linksResponse.ok) { + throw new Error(`Failed to fetch links: ${linksResponse.statusText}`); + } + const linksData = await linksResponse.json(); + + // 获取标签列表 + const tagsResponse = await fetch('/api/tags'); + if (!tagsResponse.ok) { + throw new Error(`Failed to fetch tags: ${tagsResponse.statusText}`); + } + const tagsData = await tagsResponse.json(); + + // 获取统计数据 + const statsResponse = await fetch('/api/stats'); + if (!statsResponse.ok) { + throw new Error(`Failed to fetch stats: ${statsResponse.statusText}`); + } + const statsData = await statsResponse.json(); + + // 处理并设置数据 + const uiLinks = linksData.data.map(mapApiLinkToUiLink); + setLinks(uiLinks); + setAllTags(tagsData); + setStats(statsData); + + } catch (err) { + console.error('Data loading failed:', err); + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setIsLoading(false); + } + }; + + fetchLinks(); + }, []); + + const filteredLinks = links.filter(link => + link.name.toLowerCase().includes(searchQuery.toLowerCase()) || + link.shortUrl.toLowerCase().includes(searchQuery.toLowerCase()) || + link.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())) + ); + + const handleOpenLinkDetails = (id: string) => { + window.location.href = `/links/${id}`; + }; + + const handleCreateLink = async (linkData: LinkData) => { + try { + setIsLoading(true); + // 在实际应用中,这里会发送 POST 请求到 API + console.log('创建链接:', linkData); + + // 刷新链接列表 + const response = await fetch('/api/links'); + if (!response.ok) { + throw new Error(`刷新链接列表失败: ${response.statusText}`); + } + + const newData = await response.json(); + const uiLinks = newData.data.map(mapApiLinkToUiLink); + setLinks(uiLinks); + + setShowCreateModal(false); + } catch (err) { + console.error('创建链接失败:', err); + setError(err instanceof Error ? err.message : '未知错误'); + } finally { + setIsLoading(false); + } + }; + + // 加载状态 + if (isLoading && links.length === 0) { + return ( +
+
+
+

Loading data...

+
+
+ ); + } + + // 错误状态 + if (error && links.length === 0) { + return ( +
+
+ + + +

Loading Failed

+

{error}

+ +
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+
+

Link Management

+

+ View and manage all your shortened links +

+
+ +
+
+
+ + + +
+ setSearchQuery(e.target.value)} + /> +
+ + +
+
+ + {/* Stats Summary */} +
+
+
+
+ + + + +
+
+

Total Links

+

{stats.totalLinks}

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

Active Links

+

{stats.activeLinks}

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

Total Visits

+

{stats.totalVisits.toLocaleString()}

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

Conversion Rate

+

{(stats.conversionRate * 100).toFixed(1)}%

+
+
+
+
+ + {/* Links Table */} +
+
+ + + + + + + + + + + + + + {isLoading && links.length === 0 ? ( + + + + ) : filteredLinks.length === 0 ? ( + + + + ) : ( + filteredLinks.map((link) => ( + handleOpenLinkDetails(link.id)} + > + + + + + + + + + )) + )} + +
Link InfoVisitsUnique VisitorsAvg TimeConversionStatus + Actions +
+
+
+ Loading... +
+
+ No links found matching your search criteria +
+
{link.name}
+
{link.shortUrl}
+
+
{link.visits.toLocaleString()}
+
= 0 ? 'text-accent-green' : 'text-accent-red'}`}> + = 0 ? '' : 'transform rotate-180'}`} + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + + + {Math.abs(link.visitChange)}% +
+
+
{link.uniqueVisitors.toLocaleString()}
+
= 0 ? 'text-accent-green' : 'text-accent-red'}`}> + = 0 ? '' : 'transform rotate-180'}`} + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + + + {Math.abs(link.uniqueVisitorsChange)}% +
+
+
{link.avgTime}
+
= 0 ? 'text-accent-green' : 'text-accent-red'}`}> + = 0 ? '' : 'transform rotate-180'}`} + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + + + {Math.abs(link.avgTimeChange)}% +
+
+
{link.conversionRate}%
+
= 0 ? 'text-accent-green' : 'text-accent-red'}`}> + = 0 ? '' : 'transform rotate-180'}`} + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + + + {Math.abs(link.conversionChange)}% +
+
+ + {link.status === 'active' ? 'Active' : link.status === 'inactive' ? 'Inactive' : 'Expired'} + + + +
+
+
+ + {/* Tags Section */} + {allTags.length > 0 && ( +
+

Tags

+
+ {allTags.map(tagItem => ( + setSearchQuery(tagItem.tag)} + style={{ cursor: 'pointer' }} + > + {tagItem.tag} + + {tagItem.count} + + + ))} +
+
+ )} +
+ + {/* Create Link Modal */} + {showCreateModal && ( + setShowCreateModal(false)} + onSubmit={handleCreateLink} + /> + )} +
+ ); +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..3dfdce7 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,50 @@ +import Link from 'next/link'; + +export default function Home() { + return ( +
+ {/* Colorful background elements */} +
+
+
+
+
+ +
+
+
+ + + +
+

ShortURL Analytics

+
+ +

Your complete analytics suite for tracking and optimizing short URL performance

+ +
+ + Go to Dashboard + + + + + + + View Links + + + + + +
+
+
+ ) +} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..c85fb67 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/lib/analytics.ts b/lib/analytics.ts new file mode 100644 index 0000000..05ed933 --- /dev/null +++ b/lib/analytics.ts @@ -0,0 +1,1266 @@ +import { v4 as uuidv4 } from 'uuid'; +import { executeQuery, executeQuerySingle } from './clickhouse'; + +// 时间粒度枚举 +export enum TimeGranularity { + HOUR = 'hour', + DAY = 'day', + WEEK = 'week', + MONTH = 'month', +} + +// 事件类型枚举 +export enum EventType { + CLICK = 'click', + REDIRECT = 'redirect', + CONVERSION = 'conversion', + ERROR = 'error', +} + +// 转化类型枚举 +export enum ConversionType { + VISIT = 'visit', + STAY = 'stay', + INTERACT = 'interact', + SIGNUP = 'signup', + SUBSCRIPTION = 'subscription', + PURCHASE = 'purchase', +} + +// 构建日期过滤条件 +function buildDateFilter(startDate?: string, endDate?: string): string { + let dateFilter = ''; + + if (startDate && endDate) { + dateFilter = ` AND date >= '${startDate}' AND date <= '${endDate}'`; + } else if (startDate) { + dateFilter = ` AND date >= '${startDate}'`; + } else if (endDate) { + dateFilter = ` AND date <= '${endDate}'`; + } + + return dateFilter; +} + +/** + * 获取链接概览数据 + */ +export async function getLinkOverview( + linkId: string, + startDate?: string, + endDate?: string, +) { + try { + const dateFilter = buildDateFilter(startDate, endDate); + const query = ` + SELECT + count() as total_visits, + uniq(visitor_id) as unique_visitors, + avg(time_spent_sec) as average_time_spent, + countIf(time_spent_sec < 10) as bounce_count, + countIf(event_type = 'conversion') as conversion_count, + uniq(referrer) as unique_referrers, + countIf(device_type = 'mobile') as mobile_count, + countIf(device_type = 'tablet') as tablet_count, + countIf(device_type = 'desktop') as desktop_count, + countIf(device_type = 'other') as other_count, + countIf(is_qr_scan = true) as qr_scan_count, + sum(conversion_value) as total_conversion_value + FROM link_events + WHERE link_id = '${linkId}' + ${dateFilter} + `; + + const result = await executeQuerySingle<{ + total_visits: number; + unique_visitors: number; + average_time_spent: number; + bounce_count: number; + conversion_count: number; + unique_referrers: number; + mobile_count: number; + tablet_count: number; + desktop_count: number; + other_count: number; + qr_scan_count: number; + total_conversion_value: number; + }>(query); + + if (!result) { + return { + totalVisits: 0, + uniqueVisitors: 0, + averageTimeSpent: 0, + bounceCount: 0, + conversionCount: 0, + uniqueReferrers: 0, + deviceTypes: { + mobile: 0, + tablet: 0, + desktop: 0, + other: 0, + }, + qrScanCount: 0, + totalConversionValue: 0, + }; + } + + // 将设备类型计数转换为字典 + const deviceTypes = { + mobile: Number(result.mobile_count), + tablet: Number(result.tablet_count), + desktop: Number(result.desktop_count), + other: Number(result.other_count), + }; + + return { + totalVisits: Number(result.total_visits), + uniqueVisitors: Number(result.unique_visitors), + averageTimeSpent: Number(result.average_time_spent), + bounceCount: Number(result.bounce_count), + conversionCount: Number(result.conversion_count), + uniqueReferrers: Number(result.unique_referrers), + deviceTypes, + qrScanCount: Number(result.qr_scan_count), + totalConversionValue: Number(result.total_conversion_value), + }; + } catch (error) { + console.error('获取链接概览数据失败', error); + throw error; + } +} + +/** + * 获取转化漏斗数据 + */ +export async function getConversionFunnel( + linkId: string, + startDate?: string, + endDate?: string, +) { + try { + const dateFilter = buildDateFilter(startDate, endDate); + const query = ` + SELECT + countIf(conversion_type = 'visit') as visit_count, + countIf(conversion_type = 'stay') as stay_count, + countIf(conversion_type = 'interact') as interact_count, + countIf(conversion_type = 'signup') as signup_count, + countIf(conversion_type = 'subscription') as subscription_count, + countIf(conversion_type = 'purchase') as purchase_count + FROM link_events + WHERE link_id = '${linkId}' AND event_type = 'conversion' + ${dateFilter} + `; + + const result = await executeQuerySingle<{ + visit_count: number; + stay_count: number; + interact_count: number; + signup_count: number; + subscription_count: number; + purchase_count: number; + }>(query); + + if (!result) { + return { + steps: [ + { name: 'Visit', value: 0, percent: 0 }, + { name: 'Stay', value: 0, percent: 0 }, + { name: 'Interact', value: 0, percent: 0 }, + { name: 'Signup', value: 0, percent: 0 }, + { name: 'Subscription', value: 0, percent: 0 }, + { name: 'Purchase', value: 0, percent: 0 }, + ], + totalConversions: 0, + conversionRate: 0, + }; + } + + // 计算总转化数 + const totalConversions = + Number(result.visit_count) + + Number(result.stay_count) + + Number(result.interact_count) + + Number(result.signup_count) + + Number(result.subscription_count) + + Number(result.purchase_count); + + // 计算转化率 + const conversionRate = totalConversions > 0 ? + Number(result.purchase_count) / Number(result.visit_count) * 100 : 0; + + // 构建步骤数据 + const steps = [ + { + name: 'Visit', + value: Number(result.visit_count), + percent: 100, + }, + { + name: 'Stay', + value: Number(result.stay_count), + percent: result.visit_count > 0 + ? (Number(result.stay_count) / Number(result.visit_count)) * 100 + : 0, + }, + { + name: 'Interact', + value: Number(result.interact_count), + percent: result.visit_count > 0 + ? (Number(result.interact_count) / Number(result.visit_count)) * 100 + : 0, + }, + { + name: 'Signup', + value: Number(result.signup_count), + percent: result.visit_count > 0 + ? (Number(result.signup_count) / Number(result.visit_count)) * 100 + : 0, + }, + { + name: 'Subscription', + value: Number(result.subscription_count), + percent: result.visit_count > 0 + ? (Number(result.subscription_count) / Number(result.visit_count)) * 100 + : 0, + }, + { + name: 'Purchase', + value: Number(result.purchase_count), + percent: result.visit_count > 0 + ? (Number(result.purchase_count) / Number(result.visit_count)) * 100 + : 0, + }, + ]; + + return { + steps, + totalConversions, + conversionRate, + }; + } catch (error) { + console.error('获取转化漏斗数据失败', error); + throw error; + } +} + +/** + * 获取访问趋势数据 + */ +export async function getVisitTrends( + linkId: string, + startDate?: string, + endDate?: string, + granularity: TimeGranularity = TimeGranularity.DAY, +) { + try { + const dateFilter = buildDateFilter(startDate, endDate); + + const queryString = ` + SELECT + toStartOfInterval(event_time, INTERVAL 1 ${granularity}) as timestamp, + count() as visits, + uniq(visitor_id) as unique_visitors + FROM link_events + WHERE link_id = '${linkId}' + ${dateFilter} + GROUP BY timestamp + ORDER BY timestamp + `; + + const results = await executeQuery<{ + timestamp: string; + visits: number; + unique_visitors: number; + }>(queryString); + + // 计算总计 + const totals = { + visits: results.reduce((sum, item) => sum + Number(item.visits), 0), + uniqueVisitors: results.reduce((sum, item) => sum + Number(item.unique_visitors), 0), + }; + + // 格式化时间戳 + const trends = results.map(item => ({ + timestamp: formatTimestamp(item.timestamp, granularity), + visits: Number(item.visits), + uniqueVisitors: Number(item.unique_visitors), + })); + + return { + trends, + totals, + }; + } catch (error) { + console.error('获取访问趋势数据失败', error); + throw error; + } +} + +/** + * 追踪事件 + */ +export async function trackEvent(eventData: { + linkId: string; + eventType: EventType; + visitorId?: string; + sessionId?: string; + referrer?: string; + userAgent?: string; + ipAddress?: string; + timeSpent?: number; + conversionType?: ConversionType; + conversionValue?: number; + customData?: Record; + isQrScan?: boolean; + qrCodeId?: string; + utmSource?: string; + utmMedium?: string; + utmCampaign?: string; +}) { + try { + // 检查必要字段 + if (!eventData.linkId) { + throw new Error('Missing required field: linkId'); + } + + // 生成缺失的ID和时间戳 + const eventId = uuidv4(); + const timestamp = new Date().toISOString(); + const visitorId = eventData.visitorId || uuidv4(); + const sessionId = eventData.sessionId || uuidv4(); + + // 设置默认值 + const isQrScan = !!eventData.isQrScan; + const qrCodeId = eventData.qrCodeId || ''; + const conversionValue = eventData.conversionValue || 0; + const conversionType = eventData.conversionType || ConversionType.VISIT; + const timeSpentSec = eventData.timeSpent || 0; + + // 准备插入数据 + const insertQuery = ` + INSERT INTO link_events ( + event_id, event_time, link_id, visitor_id, session_id, + event_type, ip_address, referrer, utm_source, utm_medium, + utm_campaign, user_agent, time_spent_sec, is_qr_scan, + qr_code_id, conversion_type, conversion_value, custom_data + ) VALUES ( + '${eventId}', '${timestamp}', '${eventData.linkId}', '${visitorId}', '${sessionId}', + '${eventData.eventType}', '${eventData.ipAddress || ''}', '${eventData.referrer || ''}', + '${eventData.utmSource || ''}', '${eventData.utmMedium || ''}', + '${eventData.utmCampaign || ''}', '${eventData.userAgent || ''}', ${timeSpentSec}, ${isQrScan}, + '${qrCodeId}', '${conversionType}', ${conversionValue}, '${JSON.stringify(eventData.customData || {})}' + ) + `; + + await executeQuery(insertQuery); + + return { + success: true, + eventId, + timestamp, + }; + } catch (error) { + console.error('事件追踪失败', error); + throw error; + } +} + +/** + * 格式化时间戳 + */ +function formatTimestamp(timestamp: string, granularity: TimeGranularity): string { + const date = new Date(timestamp); + + switch (granularity) { + case TimeGranularity.HOUR: + return `${date.toISOString().substring(0, 13)}:00`; + case TimeGranularity.DAY: + return date.toISOString().substring(0, 10); + case TimeGranularity.WEEK: { + const firstDayOfYear = new Date(date.getFullYear(), 0, 1); + const pastDaysOfYear = (date.getTime() - firstDayOfYear.getTime()) / 86400000; + const weekNum = Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7); + return `${date.getFullYear()}-W${weekNum.toString().padStart(2, '0')}`; + } + case TimeGranularity.MONTH: + return date.toISOString().substring(0, 7); + default: + return date.toISOString().substring(0, 10); + } +} + +/** + * 获取链接表现数据 + */ +export async function getLinkPerformance( + linkId: string, + startDate?: string, + endDate?: string, +) { + try { + const dateFilter = buildDateFilter(startDate, endDate); + const query = ` + SELECT + count() as total_clicks, + uniq(visitor_id) as unique_visitors, + avg(time_spent_sec) as average_time_spent, + countIf(time_spent_sec < 10) as bounce_count, + uniq(referrer) as unique_referrers, + countIf(event_type = 'conversion' AND conversion_type = 'purchase') as conversion_count, + count(DISTINCT DATE(event_time)) as active_days, + max(event_time) as last_click_time, + countIf(device_type = 'mobile') as mobile_clicks, + countIf(device_type = 'desktop') as desktop_clicks + FROM link_events + WHERE link_id = '${linkId}' + ${dateFilter} + `; + + const result = await executeQuerySingle<{ + total_clicks: number; + unique_visitors: number; + average_time_spent: number; + bounce_count: number; + unique_referrers: number; + conversion_count: number; + active_days: number; + last_click_time: string; + mobile_clicks: number; + desktop_clicks: number; + }>(query); + + if (!result) { + return { + totalClicks: 0, + uniqueVisitors: 0, + averageTimeSpent: 0, + bounceRate: 0, + uniqueReferrers: 0, + conversionRate: 0, + activeDays: 0, + lastClickTime: null, + deviceDistribution: { + mobile: 0, + desktop: 0, + }, + }; + } + + // 计算跳出率 + const bounceRate = result.total_clicks > 0 ? + (result.bounce_count / result.total_clicks) * 100 : 0; + + // 计算转化率 + const conversionRate = result.unique_visitors > 0 ? + (result.conversion_count / result.unique_visitors) * 100 : 0; + + return { + totalClicks: Number(result.total_clicks), + uniqueVisitors: Number(result.unique_visitors), + averageTimeSpent: Number(result.average_time_spent), + bounceRate: Number(bounceRate.toFixed(2)), + uniqueReferrers: Number(result.unique_referrers), + conversionRate: Number(conversionRate.toFixed(2)), + activeDays: Number(result.active_days), + lastClickTime: result.last_click_time, + deviceDistribution: { + mobile: Number(result.mobile_clicks), + desktop: Number(result.desktop_clicks), + }, + }; + } catch (error) { + console.error('获取链接表现数据失败', error); + throw error; + } +} + +/** + * 获取平台分布数据 + */ +export async function getPlatformDistribution( + startDate?: string, + endDate?: string, + linkId?: string, +) { + try { + const dateFilter = buildDateFilter(startDate, endDate); + + // 构建链接过滤条件 + let linkFilter = ''; + if (linkId) { + linkFilter = ` AND link_id = '${linkId}'`; + } + + const query = ` + SELECT + os, + browser, + count() as visit_count + FROM link_events + WHERE 1=1 + ${dateFilter} + ${linkFilter} + GROUP BY os, browser + ORDER BY visit_count DESC + `; + + const results = await executeQuery<{ + os: string; + browser: string; + visit_count: number; + }>(query); + + // 平台统计 + const platforms: { [key: string]: number } = {}; + // 浏览器统计 + const browsers: { [key: string]: number } = {}; + + // 计算总访问量 + const totalVisits = results.reduce((sum, item) => sum + Number(item.visit_count), 0); + + // 处理平台和浏览器数据 + for (const item of results) { + const platform = item.os || 'unknown'; + const browser = item.browser || 'unknown'; + const count = Number(item.visit_count); + + // 累加平台数据 + platforms[platform] = (platforms[platform] || 0) + count; + + // 累加浏览器数据 + browsers[browser] = (browsers[browser] || 0) + count; + } + + // 计算百分比并格式化结果 + const platformData = Object.entries(platforms).map(([name, count]) => ({ + name, + count: Number(count), + percent: totalVisits > 0 ? Number(((count / totalVisits) * 100).toFixed(1)) : 0, + })); + + const browserData = Object.entries(browsers).map(([name, count]) => ({ + name, + count: Number(count), + percent: totalVisits > 0 ? Number(((count / totalVisits) * 100).toFixed(1)) : 0, + })); + + // 按访问量排序 + platformData.sort((a, b) => b.count - a.count); + browserData.sort((a, b) => b.count - a.count); + + return { + totalVisits: totalVisits, + platforms: platformData, + browsers: browserData, + }; + } catch (error) { + console.error('获取平台分布数据失败', error); + throw error; + } +} + +/** + * 获取链接状态分布数据 + */ +export async function getLinkStatusDistribution( + startDate?: string, + endDate?: string, + projectId?: string, +) { + try { + const dateFilter = buildDateFilter(startDate, endDate); + + // 构建项目过滤条件 + let projectFilter = ''; + if (projectId) { + projectFilter = ` AND project_id = '${projectId}'`; + } + + const query = ` + SELECT + is_active, + count() as link_count + FROM links + WHERE 1=1 + ${dateFilter} + ${projectFilter} + GROUP BY is_active + `; + + const results = await executeQuery<{ + is_active: boolean; + link_count: number; + }>(query); + + // 初始化数据 + let activeCount = 0; + let inactiveCount = 0; + + // 处理查询结果 + for (const item of results) { + if (item.is_active) { + activeCount = Number(item.link_count); + } else { + inactiveCount = Number(item.link_count); + } + } + + // 计算总数 + const totalLinks = activeCount + inactiveCount; + + // 计算百分比 + const activePercent = totalLinks > 0 ? (activeCount / totalLinks) * 100 : 0; + const inactivePercent = totalLinks > 0 ? (inactiveCount / totalLinks) * 100 : 0; + + // 构建状态分布数据 + const statusDistribution = [ + { + status: 'active', + count: activeCount, + percent: Number(activePercent.toFixed(1)), + }, + { + status: 'inactive', + count: inactiveCount, + percent: Number(inactivePercent.toFixed(1)), + }, + ]; + + return { + totalLinks, + statusDistribution, + }; + } catch (error) { + console.error('获取链接状态分布数据失败', error); + throw error; + } +} + +/** + * 获取设备分析详情 + */ +export async function getDeviceAnalysis( + startDate?: string, + endDate?: string, + linkId?: string, +) { + try { + const dateFilter = buildDateFilter(startDate, endDate); + + // 构建链接过滤条件 + let linkFilter = ''; + if (linkId) { + linkFilter = ` AND link_id = '${linkId}'`; + } + + const query = ` + SELECT + device_type, + count() as visit_count + FROM link_events + WHERE 1=1 + ${dateFilter} + ${linkFilter} + GROUP BY device_type + ORDER BY visit_count DESC + `; + + const results = await executeQuery<{ + device_type: string; + visit_count: number; + }>(query); + + // 设备类型统计 + const deviceTypes: { [key: string]: number } = {}; + + // 计算总访问量 + const totalVisits = results.reduce((sum, item) => sum + Number(item.visit_count), 0); + + // 处理设备数据 + for (const item of results) { + const type = item.device_type || 'unknown'; + const count = Number(item.visit_count); + + // 累加类型数据 + deviceTypes[type] = (deviceTypes[type] || 0) + count; + } + + // 计算百分比并格式化类型结果 + const typeData = Object.entries(deviceTypes).map(([name, count]) => ({ + name, + count: Number(count), + percent: totalVisits > 0 ? Number(((count / totalVisits) * 100).toFixed(1)) : 0, + })); + + // 排序类型数据 + typeData.sort((a, b) => b.count - a.count); + + return { + totalVisits, + deviceTypes: typeData, + deviceBrands: [], // 返回空数组,因为数据库中没有设备品牌信息 + deviceModels: [], // 返回空数组,因为数据库中没有设备型号信息 + }; + } catch (error) { + console.error('获取设备分析详情失败', error); + throw error; + } +} + +/** + * 获取热门链接数据 + */ +export async function getPopularLinks( + startDate?: string, + endDate?: string, + projectId?: string, + sortBy: 'visits' | 'uniqueVisitors' | 'conversionRate' = 'visits', + limit: number = 10, +) { + try { + const dateFilter = buildDateFilter(startDate, endDate); + + // 构建项目过滤条件 + let projectFilter = ''; + if (projectId) { + projectFilter = ` AND l.project_id = '${projectId}'`; + } + + // 根据排序字段构建ORDER BY子句 + let orderBy = ''; + switch (sortBy) { + case 'visits': + orderBy = 'ORDER BY total_visits DESC'; + break; + case 'uniqueVisitors': + orderBy = 'ORDER BY unique_visitors DESC'; + break; + case 'conversionRate': + orderBy = 'ORDER BY conversion_rate DESC'; + break; + default: + orderBy = 'ORDER BY total_visits DESC'; + } + + const query = ` + SELECT + l.link_id, + l.original_url, + l.title, + l.is_active, + count() as total_visits, + uniq(e.visitor_id) as unique_visitors, + countIf(e.event_type = 'conversion' AND e.conversion_type = 'purchase') as conversion_count, + countIf(e.time_spent_sec < 10) as bounce_count + FROM links l + JOIN link_events e ON l.link_id = e.link_id + WHERE 1=1 + ${dateFilter} + ${projectFilter} + GROUP BY l.link_id, l.original_url, l.title, l.is_active + ${orderBy} + LIMIT ${limit} + `; + + const results = await executeQuery<{ + link_id: string; + original_url: string; + title: string; + is_active: boolean; + total_visits: number; + unique_visitors: number; + conversion_count: number; + bounce_count: number; + }>(query); + + // 处理查询结果 + const links = results.map(link => { + const totalVisits = Number(link.total_visits); + const uniqueVisitors = Number(link.unique_visitors); + const conversionCount = Number(link.conversion_count); + const bounceCount = Number(link.bounce_count); + + // 计算转化率 + const conversionRate = uniqueVisitors > 0 + ? (conversionCount / uniqueVisitors) * 100 + : 0; + + // 计算跳出率 + const bounceRate = totalVisits > 0 + ? (bounceCount / totalVisits) * 100 + : 0; + + return { + id: link.link_id, + url: link.original_url, + title: link.title || '无标题', + isActive: link.is_active, + totalVisits, + uniqueVisitors, + conversionCount, + conversionRate: Number(conversionRate.toFixed(2)), + bounceCount, + bounceRate: Number(bounceRate.toFixed(2)), + }; + }); + + return { + links, + totalCount: links.length, + }; + } catch (error) { + console.error('获取热门链接数据失败', error); + throw error; + } +} + +/** + * 获取热门引荐来源数据 + */ +export async function getPopularReferrers( + startDate?: string, + endDate?: string, + linkId?: string, + type: 'domain' | 'full' = 'domain', + limit: number = 10, +) { + try { + const dateFilter = buildDateFilter(startDate, endDate); + + // 构建链接过滤条件 + let linkFilter = ''; + if (linkId) { + linkFilter = ` AND link_id = '${linkId}'`; + } + + // 决定是按域名还是完整URL分组 + const groupByField = type === 'domain' + ? 'domain(referrer)' + : 'referrer'; + + const query = ` + SELECT + ${groupByField} as source, + count() as visit_count, + uniq(visitor_id) as unique_visitors, + countIf(event_type = 'conversion' AND conversion_type = 'purchase') as conversion_count, + avg(time_spent_sec) as average_time_spent + FROM link_events + WHERE referrer != '' + ${dateFilter} + ${linkFilter} + GROUP BY ${groupByField} + ORDER BY visit_count DESC + LIMIT ${limit} + `; + + const results = await executeQuery<{ + source: string; + visit_count: number; + unique_visitors: number; + conversion_count: number; + average_time_spent: number; + }>(query); + + // 计算总访问量 + const totalVisits = results.reduce((sum, item) => sum + Number(item.visit_count), 0); + + // 处理查询结果 + const referrers = results.map(referrer => { + const visitCount = Number(referrer.visit_count); + const uniqueVisitors = Number(referrer.unique_visitors); + const conversionCount = Number(referrer.conversion_count); + + // 计算转化率 + const conversionRate = uniqueVisitors > 0 + ? (conversionCount / uniqueVisitors) * 100 + : 0; + + // 计算百分比 + const percent = totalVisits > 0 + ? (visitCount / totalVisits) * 100 + : 0; + + return { + source: referrer.source || '(direct)', + visitCount, + uniqueVisitors, + conversionCount, + conversionRate: Number(conversionRate.toFixed(2)), + averageTimeSpent: Number(referrer.average_time_spent), + percent: Number(percent.toFixed(1)), + }; + }); + + return { + referrers, + totalVisits, + }; + } catch (error) { + console.error('获取热门引荐来源数据失败', error); + throw error; + } +} + +/** + * 获取QR码分析数据 + */ +export async function getQrCodeAnalysis( + startDate?: string, + endDate?: string, + linkId?: string, + qrCodeId?: string, +) { + try { + const dateFilter = buildDateFilter(startDate, endDate); + + // 构建过滤条件 + let filters = ' AND is_qr_scan = true'; + if (linkId) { + filters += ` AND link_id = '${linkId}'`; + } + if (qrCodeId) { + filters += ` AND qr_code_id = '${qrCodeId}'`; + } + + // 查询QR码扫描基本指标 + const basicQuery = ` + SELECT + count() as total_scans, + uniq(visitor_id) as unique_scanners, + countIf(event_type = 'conversion' AND conversion_type = 'purchase') as conversion_count, + avg(time_spent_sec) as average_time_spent + FROM link_events + WHERE 1=1 + ${dateFilter} + ${filters} + `; + + const basicResult = await executeQuerySingle<{ + total_scans: number; + unique_scanners: number; + conversion_count: number; + average_time_spent: number; + }>(basicQuery); + + // 查询QR码扫描的位置分布 + const locationQuery = ` + SELECT + city, + country, + count() as scan_count + FROM link_events + WHERE 1=1 + ${dateFilter} + ${filters} + GROUP BY city, country + ORDER BY scan_count DESC + LIMIT 10 + `; + + const locationResults = await executeQuery<{ + city: string; + country: string; + scan_count: number; + }>(locationQuery); + + // 查询QR码扫描设备分布 + const deviceQuery = ` + SELECT + device_type, + count() as scan_count + FROM link_events + WHERE 1=1 + ${dateFilter} + ${filters} + GROUP BY device_type + ORDER BY scan_count DESC + `; + + const deviceResults = await executeQuery<{ + device_type: string; + scan_count: number; + }>(deviceQuery); + + // 查询QR码扫描时间分布 + const timeQuery = ` + SELECT + toHour(event_time) as hour, + count() as scan_count + FROM link_events + WHERE 1=1 + ${dateFilter} + ${filters} + GROUP BY hour + ORDER BY hour + `; + + const timeResults = await executeQuery<{ + hour: number; + scan_count: number; + }>(timeQuery); + + // 计算基本指标 + const totalScans = Number(basicResult?.total_scans || 0); + const uniqueScanners = Number(basicResult?.unique_scanners || 0); + const conversionCount = Number(basicResult?.conversion_count || 0); + + // 计算转化率 + const conversionRate = uniqueScanners > 0 + ? (conversionCount / uniqueScanners) * 100 + : 0; + + // 处理位置数据 + const locations = locationResults.map(loc => ({ + city: loc.city || 'Unknown', + country: loc.country || 'Unknown', + scanCount: Number(loc.scan_count), + percent: totalScans > 0 ? Number(((Number(loc.scan_count) / totalScans) * 100).toFixed(1)) : 0, + })); + + // 处理设备类型数据 + const deviceCounts: { [key: string]: number } = {}; + for (const device of deviceResults) { + const type = device.device_type || 'unknown'; + deviceCounts[type] = Number(device.scan_count); + } + + // 计算设备分布的百分比 + const totalDeviceCount = Object.values(deviceCounts).reduce((sum, count) => sum + count, 0); + const deviceDistribution = Object.entries(deviceCounts).map(([type, count]) => ({ + type, + count, + percent: totalDeviceCount > 0 ? Number(((count / totalDeviceCount) * 100).toFixed(1)) : 0, + })); + + // 排序设备分布 + deviceDistribution.sort((a, b) => b.count - a.count); + + // 处理时间分布数据 + const hourlyDistribution = Array.from({ length: 24 }, (_, i) => ({ + hour: i, + scanCount: 0, + percent: 0 + })); + + for (const time of timeResults) { + const hour = Number(time.hour); + const count = Number(time.scan_count); + + if (hour >= 0 && hour < 24) { + hourlyDistribution[hour].scanCount = count; + hourlyDistribution[hour].percent = totalScans > 0 ? (count / totalScans) * 100 : 0; + } + } + + return { + overview: { + totalScans, + uniqueScanners, + conversionCount, + conversionRate: Number(conversionRate.toFixed(2)), + averageTimeSpent: Number(basicResult?.average_time_spent || 0), + }, + locations, + deviceDistribution, + hourlyDistribution, + }; + } catch (error) { + console.error('获取QR码分析数据失败', error); + throw error; + } +} + +/** + * 获取概览卡片数据 + */ +export async function getOverviewCards( + startDate?: string, + endDate?: string, + projectId?: string, +) { + try { + const dateFilter = buildDateFilter(startDate, endDate); + + // 构建项目过滤条件 + let projectFilter = ''; + if (projectId) { + projectFilter = ` AND l.project_id = '${projectId}'`; + } + + // 获取当前周期的数据 + const currentQuery = ` + SELECT + count(DISTINCT e.link_id) as total_links, + count() as total_visits, + uniq(e.visitor_id) as unique_visitors, + countIf(e.event_type = 'conversion' AND e.conversion_type = 'purchase') as total_conversions, + sum(e.conversion_value) as total_revenue, + countIf(l.is_active = true) as active_links + FROM link_events e + JOIN links l ON e.link_id = l.link_id + WHERE 1=1 + ${dateFilter} + ${projectFilter} + `; + + const currentResult = await executeQuerySingle<{ + total_links: number; + total_visits: number; + unique_visitors: number; + total_conversions: number; + total_revenue: number; + active_links: number; + }>(currentQuery); + + // 计算前一时期的日期范围 + let previousStartDate = ''; + let previousEndDate = ''; + + if (startDate && endDate) { + const start = new Date(startDate); + const end = new Date(endDate); + const duration = end.getTime() - start.getTime(); + + const prevStart = new Date(start.getTime() - duration); + const prevEnd = new Date(end.getTime() - duration); + + previousStartDate = prevStart.toISOString().split('T')[0]; + previousEndDate = prevEnd.toISOString().split('T')[0]; + } + + // 获取前一时期的数据 + let previousResult = null; + + if (previousStartDate && previousEndDate) { + const previousDateFilter = buildDateFilter(previousStartDate, previousEndDate); + + const previousQuery = ` + SELECT + count(DISTINCT e.link_id) as total_links, + count() as total_visits, + uniq(e.visitor_id) as unique_visitors, + countIf(e.event_type = 'conversion' AND e.conversion_type = 'purchase') as total_conversions, + sum(e.conversion_value) as total_revenue, + countIf(l.is_active = true) as active_links + FROM link_events e + JOIN links l ON e.link_id = l.link_id + WHERE 1=1 + ${previousDateFilter} + ${projectFilter} + `; + + previousResult = await executeQuerySingle<{ + total_links: number; + total_visits: number; + unique_visitors: number; + total_conversions: number; + total_revenue: number; + active_links: number; + }>(previousQuery); + } + + // 计算同比变化 + function calculateChange(current: number, previous: number): number { + if (previous === 0) return 0; + return Number(((current - previous) / previous * 100).toFixed(1)); + } + + // 获取当前值,并设置默认值 + const currentTotalLinks = Number(currentResult?.total_links || 0); + const currentTotalVisits = Number(currentResult?.total_visits || 0); + const currentUniqueVisitors = Number(currentResult?.unique_visitors || 0); + const currentTotalConversions = Number(currentResult?.total_conversions || 0); + const currentTotalRevenue = Number(currentResult?.total_revenue || 0); + const currentActiveLinks = Number(currentResult?.active_links || 0); + + // 获取前一时期的值,并设置默认值 + const previousTotalLinks = Number(previousResult?.total_links || 0); + const previousTotalVisits = Number(previousResult?.total_visits || 0); + const previousUniqueVisitors = Number(previousResult?.unique_visitors || 0); + const previousTotalConversions = Number(previousResult?.total_conversions || 0); + const previousTotalRevenue = Number(previousResult?.total_revenue || 0); + const previousActiveLinks = Number(previousResult?.active_links || 0); + + // 计算转化率 + const currentConversionRate = currentUniqueVisitors > 0 + ? (currentTotalConversions / currentUniqueVisitors) * 100 + : 0; + + const previousConversionRate = previousUniqueVisitors > 0 + ? (previousTotalConversions / previousUniqueVisitors) * 100 + : 0; + + // 计算活跃链接百分比 + const currentActivePercentage = currentTotalLinks > 0 + ? (currentActiveLinks / currentTotalLinks) * 100 + : 0; + + const previousActivePercentage = previousTotalLinks > 0 + ? (previousActiveLinks / previousTotalLinks) * 100 + : 0; + + // 构建结果 + return { + cards: [ + { + title: '总访问量', + currentValue: currentTotalVisits, + previousValue: previousTotalVisits, + change: calculateChange(currentTotalVisits, previousTotalVisits), + format: 'number', + }, + { + title: '独立访客', + currentValue: currentUniqueVisitors, + previousValue: previousUniqueVisitors, + change: calculateChange(currentUniqueVisitors, previousUniqueVisitors), + format: 'number', + }, + { + title: '转化次数', + currentValue: currentTotalConversions, + previousValue: previousTotalConversions, + change: calculateChange(currentTotalConversions, previousTotalConversions), + format: 'number', + }, + { + title: '总收入', + currentValue: currentTotalRevenue, + previousValue: previousTotalRevenue, + change: calculateChange(currentTotalRevenue, previousTotalRevenue), + format: 'currency', + }, + { + title: '转化率', + currentValue: Number(currentConversionRate.toFixed(1)), + previousValue: Number(previousConversionRate.toFixed(1)), + change: calculateChange(currentConversionRate, previousConversionRate), + format: 'percent', + }, + { + title: '活跃链接率', + currentValue: Number(currentActivePercentage.toFixed(1)), + previousValue: Number(previousActivePercentage.toFixed(1)), + change: calculateChange(currentActivePercentage, previousActivePercentage), + format: 'percent', + }, + ], + timeRange: { + current: { + startDate: startDate || '', + endDate: endDate || '', + }, + previous: { + startDate: previousStartDate, + endDate: previousEndDate, + }, + }, + }; + } catch (error) { + console.error('获取概览卡片数据失败', error); + throw error; + } +} \ No newline at end of file diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts new file mode 100644 index 0000000..b034a90 --- /dev/null +++ b/lib/clickhouse.ts @@ -0,0 +1,47 @@ +import { createClient } from '@clickhouse/client'; + +// Create configuration object using the URL approach +const config = { + url: process.env.CLICKHOUSE_URL || 'http://localhost:8123', + username: process.env.CLICKHOUSE_USER || 'default', + password: process.env.CLICKHOUSE_PASSWORD || '', + database: process.env.CLICKHOUSE_DATABASE || 'limq' +}; + +// Log configuration (removing password for security) +console.log('ClickHouse config:', { + ...config, + password: config.password ? '****' : '' +}); + +// Create ClickHouse client with proper URL format +export const clickhouse = createClient(config); + +// Log connection status +console.log('ClickHouse client created with URL:', config.url); + +/** + * Execute ClickHouse query and return results + */ +export async function executeQuery(query: string): Promise { + try { + const result = await clickhouse.query({ + query, + format: 'JSONEachRow', + }); + + const data = await result.json(); + return data as T[]; + } catch (error) { + console.error('ClickHouse query error:', error); + throw error; + } +} + +/** + * Execute ClickHouse query and return a single result + */ +export async function executeQuerySingle(query: string): Promise { + const results = await executeQuery(query); + return results.length > 0 ? results[0] : null; +} \ No newline at end of file diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..1bfda9c --- /dev/null +++ b/next.config.ts @@ -0,0 +1,21 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ + // 设置需要转译的包 + transpilePackages: [], + + // 配置实验性选项 + experimental: { + // 禁用外部目录处理,避免monorepo问题 + externalDir: true, + }, + + // 禁用严格模式,避免开发时重复渲染 + reactStrictMode: false, + + // 设置输出为独立应用 + output: 'standalone', +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3d56314 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5716 @@ +{ + "name": "shorturl-analytics", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "shorturl-analytics", + "version": "0.1.0", + "dependencies": { + "@clickhouse/client": "^1.11.0", + "next": "15.2.3", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.2.3", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@clickhouse/client": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@clickhouse/client/-/client-1.11.0.tgz", + "integrity": "sha512-VYTQfR0y/BtrIDEjuSce1zv85OvHak5sUhZVyNYJzbAgWHy3jFf8Os7FdUSeqyKav0xGGy+2X+dRanTFjI5Oug==", + "license": "Apache-2.0", + "dependencies": { + "@clickhouse/client-common": "1.11.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@clickhouse/client-common": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@clickhouse/client-common/-/client-common-1.11.0.tgz", + "integrity": "sha512-O0xbwv7HiMXayokrf5dYIBpjBnYekcOXWz60T1cXLmiZ8vgrfNRCiOpybJkrMXKnw9D0mWCgPUu/rgMY7U1f4g==", + "license": "Apache-2.0" + }, + "node_modules/@emnapi/core": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", + "integrity": "sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz", + "integrity": "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", + "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz", + "integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.12.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "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.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "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.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "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.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "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.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "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.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "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.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "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.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "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.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "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.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "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.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "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.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "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.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "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.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "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.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "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.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "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.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "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.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "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.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "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/@napi-rs/wasm-runtime": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.7.tgz", + "integrity": "sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.3.1", + "@emnapi/runtime": "^1.3.1", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@next/env": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.3.tgz", + "integrity": "sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.2.3.tgz", + "integrity": "sha512-eNSOIMJtjs+dp4Ms1tB1PPPJUQHP3uZK+OQ7iFY9qXpGO6ojT6imCL+KcUOqE/GXGidWbBZJzYdgAdPHqeCEPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.3.tgz", + "integrity": "sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.3.tgz", + "integrity": "sha512-pVwKvJ4Zk7h+4hwhqOUuMx7Ib02u3gDX3HXPKIShBi9JlYllI0nU6TWLbPT94dt7FSi6mSBhfc2JrHViwqbOdw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.3.tgz", + "integrity": "sha512-50ibWdn2RuFFkOEUmo9NCcQbbV9ViQOrUfG48zHBCONciHjaUKtHcYFiCwBVuzD08fzvzkWuuZkd4AqbvKO7UQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.3.tgz", + "integrity": "sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.3.tgz", + "integrity": "sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.3.tgz", + "integrity": "sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.3.tgz", + "integrity": "sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.3.tgz", + "integrity": "sha512-gHYS9tc+G2W0ZC8rBL+H6RdtXIyk40uLiaos0yj5US85FNhbFEndMA2nW3z47nzOWiSvXTZ5kBClc3rD0zJg0w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz", + "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.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/@tailwindcss/node": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.14.tgz", + "integrity": "sha512-Ux9NbFkKWYE4rfUFz6M5JFLs/GEYP6ysxT8uSyPn6aTbh2K3xDE1zz++eVK4Vwx799fzMF8CID9sdHn4j/Ab8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "tailwindcss": "4.0.14" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.14.tgz", + "integrity": "sha512-M8VCNyO/NBi5vJ2cRcI9u8w7Si+i76a7o1vveoGtbbjpEYJZYiyc7f2VGps/DqawO56l3tImIbq2OT/533jcrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.0.14", + "@tailwindcss/oxide-darwin-arm64": "4.0.14", + "@tailwindcss/oxide-darwin-x64": "4.0.14", + "@tailwindcss/oxide-freebsd-x64": "4.0.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.0.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.0.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.0.14", + "@tailwindcss/oxide-linux-x64-musl": "4.0.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.0.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.0.14" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.14.tgz", + "integrity": "sha512-VBFKC2rFyfJ5J8lRwjy6ub3rgpY186kAcYgiUr8ArR8BAZzMruyeKJ6mlsD22Zp5ZLcPW/FXMasJiJBx0WsdQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.14.tgz", + "integrity": "sha512-U3XOwLrefGr2YQZ9DXasDSNWGPZBCh8F62+AExBEDMLDfvLLgI/HDzY8Oq8p/JtqkAY38sWPOaNnRwEGKU5Zmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.14.tgz", + "integrity": "sha512-V5AjFuc3ndWGnOi1d379UsODb0TzAS2DYIP/lwEbfvafUaD2aNZIcbwJtYu2DQqO2+s/XBvDVA+w4yUyaewRwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.14.tgz", + "integrity": "sha512-tXvtxbaZfcPfqBwW3f53lTcyH6EDT+1eT7yabwcfcxTs+8yTPqxsDUhrqe9MrnEzpNkd+R/QAjJapfd4tjWdLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.14.tgz", + "integrity": "sha512-cSeLNWWqIWeSTmBntQvyY2/2gcLX8rkPFfDDTQVF8qbRcRMVPLxBvFVJyfSAYRNch6ZyVH2GI6dtgALOBDpdNA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.14.tgz", + "integrity": "sha512-bwDWLBalXFMDItcSXzFk6y7QKvj6oFlaY9vM+agTlwFL1n1OhDHYLZkSjaYsh6KCeG0VB0r7H8PUJVOM1LRZyg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.14.tgz", + "integrity": "sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.14.tgz", + "integrity": "sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.14.tgz", + "integrity": "sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.14.tgz", + "integrity": "sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.14.tgz", + "integrity": "sha512-rNXXMDJfCJLw/ZaFTOLOHoGULxyXfh2iXTGiChFiYTSgKBKQHIGEpV0yn5N25WGzJJ+VBnRjHzlmDqRV+d//oQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.0.14.tgz", + "integrity": "sha512-+uIR6KtKhla1XeIanF27KtrfYy+PX+R679v5LxbkmEZlhQe3g8rk+wKj7Xgt++rWGRuFLGMXY80Ek8JNn+kN/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.0.14", + "@tailwindcss/oxide": "4.0.14", + "lightningcss": "1.29.2", + "postcss": "^8.4.41", + "tailwindcss": "4.0.14" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.17.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz", + "integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/react": { + "version": "19.0.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz", + "integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", + "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz", + "integrity": "sha512-4henw4zkePi5p252c8ncBLzLce52SEUz2Ebj8faDnuUXz2UuHEONYcJ+G0oaCF+bYCWVZtrGzq3FD7YXetmnSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.27.0", + "@typescript-eslint/type-utils": "8.27.0", + "@typescript-eslint/utils": "8.27.0", + "@typescript-eslint/visitor-keys": "8.27.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.27.0.tgz", + "integrity": "sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.27.0", + "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/typescript-estree": "8.27.0", + "@typescript-eslint/visitor-keys": "8.27.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.27.0.tgz", + "integrity": "sha512-8oI9GwPMQmBryaaxG1tOZdxXVeMDte6NyJA4i7/TWa4fBwgnAXYlIQP+uYOeqAaLJ2JRxlG9CAyL+C+YE9Xknw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/visitor-keys": "8.27.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.27.0.tgz", + "integrity": "sha512-wVArTVcz1oJOIEJxui/nRhV0TXzD/zMSOYi/ggCfNq78EIszddXcJb7r4RCp/oBrjt8n9A0BSxRMKxHftpDxDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.27.0", + "@typescript-eslint/utils": "8.27.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.27.0.tgz", + "integrity": "sha512-/6cp9yL72yUHAYq9g6DsAU+vVfvQmd1a8KyA81uvfDE21O2DwQ/qxlM4AR8TSdAu+kJLBDrEHKC5/W2/nxsY0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.27.0.tgz", + "integrity": "sha512-BnKq8cqPVoMw71O38a1tEb6iebEgGA80icSxW7g+kndx0o6ot6696HjG7NdgfuAVmVEtwXUr3L8R9ZuVjoQL6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/visitor-keys": "8.27.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.27.0.tgz", + "integrity": "sha512-njkodcwH1yvmo31YWgRHNb/x1Xhhq4/m81PhtvmRngD8iHPehxffz1SNCO+kwaePhATC+kOa/ggmvPoPza5i0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.27.0", + "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/typescript-estree": "8.27.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.27.0.tgz", + "integrity": "sha512-WsXQwMkILJvffP6z4U3FYJPlbf/j07HIxmDjZpbNvBJkMfvwXj5ACRkkHwBDvLBbDbtX5TdU64/rcvKJ/vuInQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.27.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/rspack-resolver-binding-darwin-arm64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-darwin-arm64/-/rspack-resolver-binding-darwin-arm64-1.2.2.tgz", + "integrity": "sha512-i7z0B+C0P8Q63O/5PXJAzeFtA1ttY3OR2VSJgGv18S+PFNwD98xHgAgPOT1H5HIV6jlQP8Avzbp09qxJUdpPNw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-darwin-x64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-darwin-x64/-/rspack-resolver-binding-darwin-x64-1.2.2.tgz", + "integrity": "sha512-YEdFzPjIbDUCfmehC6eS+AdJYtFWY35YYgWUnqqTM2oe/N58GhNy5yRllxYhxwJ9GcfHoNc6Ubze1yjkNv+9Qg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-freebsd-x64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-freebsd-x64/-/rspack-resolver-binding-freebsd-x64-1.2.2.tgz", + "integrity": "sha512-TU4ntNXDgPN2giQyyzSnGWf/dVCem5lvwxg0XYvsvz35h5H19WrhTmHgbrULMuypCB3aHe1enYUC9rPLDw45mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-arm-gnueabihf": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-arm-gnueabihf/-/rspack-resolver-binding-linux-arm-gnueabihf-1.2.2.tgz", + "integrity": "sha512-ik3w4/rU6RujBvNWiDnKdXi1smBhqxEDhccNi/j2rHaMjm0Fk49KkJ6XKsoUnD2kZ5xaMJf9JjailW/okfUPIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-arm64-gnu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-arm64-gnu/-/rspack-resolver-binding-linux-arm64-gnu-1.2.2.tgz", + "integrity": "sha512-fp4Azi8kHz6TX8SFmKfyScZrMLfp++uRm2srpqRjsRZIIBzH74NtSkdEUHImR4G7f7XJ+sVZjCc6KDDK04YEpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-arm64-musl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-arm64-musl/-/rspack-resolver-binding-linux-arm64-musl-1.2.2.tgz", + "integrity": "sha512-gMiG3DCFioJxdGBzhlL86KcFgt9HGz0iDhw0YVYPsShItpN5pqIkNrI+L/Q/0gfDiGrfcE0X3VANSYIPmqEAlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-x64-gnu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-x64-gnu/-/rspack-resolver-binding-linux-x64-gnu-1.2.2.tgz", + "integrity": "sha512-n/4n2CxaUF9tcaJxEaZm+lqvaw2gflfWQ1R9I7WQgYkKEKbRKbpG/R3hopYdUmLSRI4xaW1Cy0Bz40eS2Yi4Sw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-x64-musl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-x64-musl/-/rspack-resolver-binding-linux-x64-musl-1.2.2.tgz", + "integrity": "sha512-cHyhAr6rlYYbon1L2Ag449YCj3p6XMfcYTP0AQX+KkQo025d1y/VFtPWvjMhuEsE2lLvtHm7GdJozj6BOMtzVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-wasm32-wasi": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-wasm32-wasi/-/rspack-resolver-binding-wasm32-wasi-1.2.2.tgz", + "integrity": "sha512-eogDKuICghDLGc32FtP+WniG38IB1RcGOGz0G3z8406dUdjJvxfHGuGs/dSlM9YEp/v0lEqhJ4mBu6X2nL9pog==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/rspack-resolver-binding-win32-arm64-msvc": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-win32-arm64-msvc/-/rspack-resolver-binding-win32-arm64-msvc-1.2.2.tgz", + "integrity": "sha512-7sWRJumhpXSi2lccX8aQpfFXHsSVASdWndLv8AmD8nDRA/5PBi8IplQVZNx2mYRx6+Bp91Z00kuVqpXO9NfCTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-win32-x64-msvc": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-win32-x64-msvc/-/rspack-resolver-binding-win32-x64-msvc-1.2.2.tgz", + "integrity": "sha512-hewo/UMGP1a7O6FG/ThcPzSJdm/WwrYDNkdGgWl6M18H6K6MSitklomWpT9MUtT5KGj++QJb06va/14QBC4pvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001706", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001706.tgz", + "integrity": "sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==", + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz", + "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.2", + "@eslint/config-helpers": "^0.1.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.22.0", + "@eslint/plugin-kit": "^0.2.7", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.2.3.tgz", + "integrity": "sha512-VDQwbajhNMFmrhLWVyUXCqsGPN+zz5G8Ys/QwFubfsxTIrkqdx3N3x3QPW+pERz8bzGPP0IgEm8cNbZcd8PFRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.2.3", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.9.1.tgz", + "integrity": "sha512-euxa5rTGqHeqVxmOHT25hpk58PxkQ4mNoX6Yun4ooGaCHAxOCojJYNvjmyeOQxj/LyW+3fulH0+xtk+p2kPPTw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^1.3.0", + "rspack-resolver": "^1.1.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", + "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-15.2.3.tgz", + "integrity": "sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w==", + "license": "MIT", + "dependencies": { + "@next/env": "15.2.3", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.2.3", + "@next/swc-darwin-x64": "15.2.3", + "@next/swc-linux-arm64-gnu": "15.2.3", + "@next/swc-linux-arm64-musl": "15.2.3", + "@next/swc-linux-x64-gnu": "15.2.3", + "@next/swc-linux-x64-musl": "15.2.3", + "@next/swc-win32-arm64-msvc": "15.2.3", + "@next/swc-win32-x64-msvc": "15.2.3", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "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/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "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", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rspack-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rspack-resolver/-/rspack-resolver-1.2.2.tgz", + "integrity": "sha512-Fwc19jMBA3g+fxDJH2B4WxwZjE0VaaOL7OX/A4Wn5Zv7bOD/vyPZhzXfaO73Xc2GAlfi96g5fGUa378WbIGfFw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/JounQin" + }, + "optionalDependencies": { + "@unrs/rspack-resolver-binding-darwin-arm64": "1.2.2", + "@unrs/rspack-resolver-binding-darwin-x64": "1.2.2", + "@unrs/rspack-resolver-binding-freebsd-x64": "1.2.2", + "@unrs/rspack-resolver-binding-linux-arm-gnueabihf": "1.2.2", + "@unrs/rspack-resolver-binding-linux-arm64-gnu": "1.2.2", + "@unrs/rspack-resolver-binding-linux-arm64-musl": "1.2.2", + "@unrs/rspack-resolver-binding-linux-x64-gnu": "1.2.2", + "@unrs/rspack-resolver-binding-linux-x64-musl": "1.2.2", + "@unrs/rspack-resolver-binding-wasm32-wasi": "1.2.2", + "@unrs/rspack-resolver-binding-win32-arm64-msvc": "1.2.2", + "@unrs/rspack-resolver-binding-win32-x64-msvc": "1.2.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "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/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.14.tgz", + "integrity": "sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", + "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "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/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e5e0add --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "shorturl-analytics", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "NEXT_TELEMETRY_DISABLED=1 next dev", + "build": "NEXT_TELEMETRY_DISABLED=1 next build", + "start": "NEXT_TELEMETRY_DISABLED=1 next start", + "lint": "next lint" + }, + "dependencies": { + "@clickhouse/client": "^1.11.0", + "@types/uuid": "^10.0.0", + "next": "15.2.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.2.3", + "tailwindcss": "^4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/public/file.svg b/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/next.svg b/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/window.svg b/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/check-clickhouse.sh b/scripts/check-clickhouse.sh new file mode 100644 index 0000000..370729b --- /dev/null +++ b/scripts/check-clickhouse.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${YELLOW}开始检查ClickHouse表结构...${NC}" + +# 加载环境变量 +set -a +source .env +set +a + +# 获取ClickHouse配置 +CLICKHOUSE_HOST=${CLICKHOUSE_HOST:-"localhost"} +CLICKHOUSE_PORT=${CLICKHOUSE_PORT:-"8123"} +CLICKHOUSE_USER=${CLICKHOUSE_USER:-"default"} +CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-""} +CLICKHOUSE_DATABASE=${CLICKHOUSE_DATABASE:-"default"} + +echo -e "${GREEN}连接到ClickHouse: ${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}${NC}" + +# 检查link_events表结构 +echo -e "${GREEN}检查link_events表结构:${NC}" +curl -s "http://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}/?user=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}" \ + -d "DESCRIBE TABLE ${CLICKHOUSE_DATABASE}.link_events" + +# 查询一行数据样本 +echo -e "\n${GREEN}查询link_events表样本数据:${NC}" +curl -s "http://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}/?user=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}" \ + -d "SELECT * FROM ${CLICKHOUSE_DATABASE}.link_events LIMIT 1 FORMAT JSON" + +echo -e "\n${YELLOW}检查完成${NC}" \ No newline at end of file diff --git a/scripts/db/db-inspector/clickhouse-schema.js b/scripts/db/db-inspector/clickhouse-schema.js new file mode 100644 index 0000000..f855ce6 --- /dev/null +++ b/scripts/db/db-inspector/clickhouse-schema.js @@ -0,0 +1,212 @@ +// 检查ClickHouse数据库结构的脚本 +const { createClient } = require('@clickhouse/client'); +const dotenv = require('dotenv'); +const path = require('path'); +const fs = require('fs'); + +// 加载环境变量 +dotenv.config({ path: path.resolve(__dirname, '../../../.env') }); + +// 定义输出目录 +const DB_REPORTS_DIR = path.resolve(__dirname, '../db-reports'); + +// 获取ClickHouse配置 +const clickhouseHost = process.env.CLICKHOUSE_HOST || 'localhost'; +const clickhousePort = process.env.CLICKHOUSE_PORT || '8123'; +const clickhouseUser = process.env.CLICKHOUSE_USER || 'default'; +const clickhousePassword = process.env.CLICKHOUSE_PASSWORD || ''; +const clickhouseDatabase = process.env.CLICKHOUSE_DATABASE || 'default'; + +console.log('ClickHouse配置:'); +console.log(` - 主机: ${clickhouseHost}`); +console.log(` - 端口: ${clickhousePort}`); +console.log(` - 用户: ${clickhouseUser}`); +console.log(` - 数据库: ${clickhouseDatabase}`); + +// 创建ClickHouse客户端 - 使用0.2.10版本的API +const client = createClient({ + url: `http://${clickhouseHost}:${clickhousePort}`, + username: clickhouseUser, + password: clickhousePassword, + database: clickhouseDatabase +}); + +// 获取所有表 +async function getAllTables() { + console.log('\n获取所有表...'); + + try { + const query = ` + SELECT name + FROM system.tables + WHERE database = '${clickhouseDatabase}' + `; + + const resultSet = await client.query({ + query, + format: 'JSONEachRow' + }); + + const tables = await resultSet.json(); + + if (!tables || tables.length === 0) { + console.log(`数据库 ${clickhouseDatabase} 中没有找到任何表`); + return null; + } + + console.log(`数据库 ${clickhouseDatabase} 中找到以下表:`); + tables.forEach(table => { + console.log(` - ${table.name}`); + }); + + return tables.map(table => table.name); + } catch (error) { + console.error('获取所有表时出错:', error); + return null; + } +} + +// 获取表结构 +async function getTableSchema(tableName) { + console.log(`\n获取表 ${tableName} 的结构...`); + + try { + const query = ` + DESCRIBE TABLE ${clickhouseDatabase}.${tableName} + `; + + const resultSet = await client.query({ + query, + format: 'JSONEachRow' + }); + + const columns = await resultSet.json(); + + if (!columns || columns.length === 0) { + console.log(`表 ${tableName} 不存在或没有列`); + return null; + } + + console.log(`表 ${tableName} 的列:`); + columns.forEach(column => { + console.log(` - ${column.name} (${column.type}, ${column.default_type === '' ? '无默认值' : `默认值: ${column.default_expression}`})`); + }); + + return columns; + } catch (error) { + console.error(`获取表 ${tableName} 结构时出错:`, error); + return null; + } +} + +// 获取表数据示例 +async function getTableDataSample(tableName, limit = 5) { + console.log(`\n获取表 ${tableName} 的数据示例 (最多 ${limit} 行)...`); + + try { + const query = ` + SELECT * + FROM ${clickhouseDatabase}.${tableName} + LIMIT ${limit} + `; + + const resultSet = await client.query({ + query, + format: 'JSONEachRow' + }); + + const rows = await resultSet.json(); + + if (!rows || rows.length === 0) { + console.log(`表 ${tableName} 中没有数据`); + return null; + } + + console.log(`表 ${tableName} 的数据示例:`); + rows.forEach((row, index) => { + console.log(` 行 ${index + 1}:`); + Object.entries(row).forEach(([key, value]) => { + console.log(` ${key}: ${value}`); + }); + }); + + return rows; + } catch (error) { + console.error(`获取表 ${tableName} 数据示例时出错:`, error); + return null; + } +} + +// 主函数 +async function main() { + let outputBuffer = ''; + const originalConsoleLog = console.log; + + // 重定向console.log到buffer和控制台 + console.log = function() { + // 调用原始的console.log + originalConsoleLog.apply(console, arguments); + + // 写入到buffer + outputBuffer += Array.from(arguments).join(' ') + '\n'; + }; + + try { + // 获取所有表 + const tables = await getAllTables(); + + if (!tables) { + console.error('无法获取表列表'); + process.exit(1); + } + + console.log('\n所有ClickHouse表:'); + console.log(tables.join(', ')); + + // 获取每个表的结构,但不获取数据示例 + for (const tableName of tables) { + await getTableSchema(tableName); + // 移除数据示例检查 + // await getTableDataSample(tableName); + } + + console.log('\nClickHouse数据库结构检查完成'); + + // 保存输出到指定目录 + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + + // 确保目录存在 + if (!fs.existsSync(DB_REPORTS_DIR)) { + fs.mkdirSync(DB_REPORTS_DIR, { recursive: true }); + } + + const outputPath = path.join(DB_REPORTS_DIR, `clickhouse-schema-${timestamp}.log`); + fs.writeFileSync(outputPath, outputBuffer); + originalConsoleLog(`结果已保存到: ${outputPath}`); + + } catch (error) { + console.error('检查ClickHouse数据库结构时出错:', error); + } finally { + // 恢复原始的console.log + console.log = originalConsoleLog; + + // 关闭客户端连接 + await client.close(); + } +} + +// 导出函数 +module.exports = { + getAllTables, + getTableSchema, + getTableDataSample, + main +}; + +// 如果直接运行此脚本,则执行main函数 +if (require.main === module) { + main().catch(error => { + console.error('运行脚本时出错:', error); + process.exit(1); + }); +} \ No newline at end of file diff --git a/scripts/db/db-inspector/postgres-schema.js b/scripts/db/db-inspector/postgres-schema.js new file mode 100644 index 0000000..6c9e322 --- /dev/null +++ b/scripts/db/db-inspector/postgres-schema.js @@ -0,0 +1,329 @@ +// 检查数据库结构的脚本 +const { Client } = require('pg'); +const dotenv = require('dotenv'); +const path = require('path'); +const fs = require('fs'); + +// 加载环境变量 +dotenv.config({ path: path.resolve(__dirname, '../../../.env') }); + +// 获取数据库连接字符串 +const databaseUrl = process.env.DATABASE_URL; + +if (!databaseUrl) { + console.error('缺少数据库连接字符串。请确保.env文件中包含DATABASE_URL'); + process.exit(1); +} + +// 定义输出目录 +const DB_REPORTS_DIR = path.resolve(__dirname, '../db-reports'); + +// 连接数据库 +async function connect() { + console.log('使用PostgreSQL连接字符串连接数据库...'); + + // 创建PostgreSQL客户端 + const client = new Client({ + connectionString: databaseUrl, + }); + + try { + await client.connect(); + console.log('成功连接到数据库'); + return client; + } catch (error) { + console.error('连接数据库失败:', error); + throw error; + } +} + +// 断开数据库连接 +async function disconnect(client) { + try { + await client.end(); + console.log('已断开数据库连接'); + } catch (error) { + console.error('断开数据库连接失败:', error); + } +} + +// 获取所有表 +async function getAllTables(client) { + console.log('\n获取所有表...'); + + try { + const query = ` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'limq' + ORDER BY table_name; + `; + + const result = await client.query(query); + + if (!result.rows || result.rows.length === 0) { + console.log('没有找到任何表'); + return null; + } + + console.log('找到以下表:'); + result.rows.forEach(row => { + console.log(` - ${row.table_name}`); + }); + + return result.rows.map(row => row.table_name); + } catch (error) { + console.error('获取所有表时出错:', error); + return null; + } +} + +// 获取表结构 +async function getTableSchema(client, tableName) { + console.log(`\n获取表 ${tableName} 的结构...`); + + try { + // 获取基本列信息 + const columnsQuery = ` + SELECT + column_name, + data_type, + is_nullable, + column_default, + character_maximum_length, + numeric_precision, + numeric_scale + FROM + information_schema.columns + WHERE + table_schema = 'limq' AND + table_name = $1 + ORDER BY + ordinal_position; + `; + + const columnsResult = await client.query(columnsQuery, [tableName]); + + if (!columnsResult.rows || columnsResult.rows.length === 0) { + console.log(`表 ${tableName} 不存在或没有列`); + return null; + } + + // 获取主键信息 + const primaryKeyQuery = ` + SELECT + kcu.column_name + FROM + information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + WHERE + tc.constraint_type = 'PRIMARY KEY' AND + tc.table_schema = 'limq' AND + tc.table_name = $1 + ORDER BY + kcu.ordinal_position; + `; + + const primaryKeyResult = await client.query(primaryKeyQuery, [tableName]); + + // 获取外键信息 + const foreignKeysQuery = ` + SELECT + kcu.column_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name + FROM + information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage ccu + ON tc.constraint_name = ccu.constraint_name + AND tc.table_schema = ccu.table_schema + WHERE + tc.constraint_type = 'FOREIGN KEY' AND + tc.table_schema = 'limq' AND + tc.table_name = $1; + `; + + const foreignKeysResult = await client.query(foreignKeysQuery, [tableName]); + + // 获取索引信息 + const indexesQuery = ` + SELECT + indexname, + indexdef + FROM + pg_indexes + WHERE + schemaname = 'public' AND + tablename = $1; + `; + + const indexesResult = await client.query(indexesQuery, [tableName]); + + // 输出列信息 + console.log(`表 ${tableName} 的列:`); + columnsResult.rows.forEach(column => { + console.log(` - ${column.column_name} (${column.data_type}${ + column.character_maximum_length ? `(${column.character_maximum_length})` : + (column.numeric_precision ? `(${column.numeric_precision},${column.numeric_scale})` : '') + }, ${column.is_nullable === 'YES' ? '可为空' : '不可为空'}, 默认值: ${column.column_default || 'NULL'})`); + }); + + // 输出主键信息 + if (primaryKeyResult.rows.length > 0) { + console.log(` 主键: ${primaryKeyResult.rows.map(row => row.column_name).join(', ')}`); + } else { + console.log(' 主键: 无'); + } + + // 输出外键信息 + if (foreignKeysResult.rows.length > 0) { + console.log(' 外键:'); + foreignKeysResult.rows.forEach(fk => { + console.log(` - ${fk.column_name} -> ${fk.foreign_table_name}.${fk.foreign_column_name}`); + }); + } else { + console.log(' 外键: 无'); + } + + // 输出索引信息 + if (indexesResult.rows.length > 0) { + console.log(' 索引:'); + indexesResult.rows.forEach(idx => { + console.log(` - ${idx.indexname}: ${idx.indexdef}`); + }); + } else { + console.log(' 索引: 无'); + } + + return { + columns: columnsResult.rows, + primaryKey: primaryKeyResult.rows, + foreignKeys: foreignKeysResult.rows, + indexes: indexesResult.rows + }; + } catch (error) { + console.error(`获取表 ${tableName} 结构时出错:`, error); + return null; + } +} + +// 获取表数据示例 +async function getTableDataSample(client, tableName, limit = 5) { + console.log(`\n获取表 ${tableName} 的数据示例 (最多 ${limit} 行)...`); + + try { + const query = ` + SELECT * + FROM "${tableName}" + LIMIT $1; + `; + + const result = await client.query(query, [limit]); + + if (!result.rows || result.rows.length === 0) { + console.log(`表 ${tableName} 中没有数据`); + return null; + } + + console.log(`表 ${tableName} 的数据示例:`); + result.rows.forEach((row, index) => { + console.log(` 行 ${index + 1}:`); + Object.entries(row).forEach(([key, value]) => { + console.log(` ${key}: ${value}`); + }); + }); + + return result.rows; + } catch (error) { + console.error(`获取表 ${tableName} 数据示例时出错:`, error); + return null; + } +} + +// 主函数 +async function main() { + let client = null; + let outputBuffer = ''; + const originalConsoleLog = console.log; + + // 重定向console.log到buffer和控制台 + console.log = function() { + // 调用原始的console.log + originalConsoleLog.apply(console, arguments); + + // 写入到buffer + outputBuffer += Array.from(arguments).join(' ') + '\n'; + }; + + try { + // 连接数据库 + client = await connect(); + + // 获取所有表 + const tables = await getAllTables(client); + + if (!tables) { + console.error('无法获取表列表'); + process.exit(1); + } + + console.log('\n所有PostgreSQL表:'); + console.log(tables.join(', ')); + + // 获取所有表的结构,而不只是特定表 + for (const tableName of tables) { + await getTableSchema(client, tableName); + // 移除数据示例检查 + // await getTableDataSample(client, tableName); + } + + console.log('\n数据库结构检查完成'); + + // 保存输出到指定目录 + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + + // 确保目录存在 + if (!fs.existsSync(DB_REPORTS_DIR)) { + fs.mkdirSync(DB_REPORTS_DIR, { recursive: true }); + } + + const outputPath = path.join(DB_REPORTS_DIR, `postgres-schema-${timestamp}.log`); + fs.writeFileSync(outputPath, outputBuffer); + originalConsoleLog(`结果已保存到: ${outputPath}`); + + } catch (error) { + console.error('检查数据库结构时出错:', error); + process.exit(1); + } finally { + // 恢复原始的console.log + console.log = originalConsoleLog; + + // 关闭数据库连接 + if (client) { + await disconnect(client); + } + } +} + +// 导出函数 +module.exports = { + connect, + disconnect, + getAllTables, + getTableSchema, + getTableDataSample, + main +}; + +// 如果直接运行此脚本,则执行main函数 +if (require.main === module) { + main().catch(error => { + console.error('运行脚本时出错:', error); + process.exit(1); + }); +} \ No newline at end of file diff --git a/scripts/db/db-inspector/run-all.js b/scripts/db/db-inspector/run-all.js new file mode 100644 index 0000000..27905cd --- /dev/null +++ b/scripts/db/db-inspector/run-all.js @@ -0,0 +1,102 @@ +// 一键运行所有数据库检查脚本 +const { exec } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +// 定义脚本路径 +const postgresScriptPath = path.join(__dirname, 'postgres-schema.js'); +const clickhouseScriptPath = path.join(__dirname, 'clickhouse-schema.js'); + +// 定义输出目录 +const DB_REPORTS_DIR = path.resolve(__dirname, '../db-reports'); + +// 确保目录存在 +if (!fs.existsSync(DB_REPORTS_DIR)) { + fs.mkdirSync(DB_REPORTS_DIR, { recursive: true }); + console.log(`创建输出目录: ${DB_REPORTS_DIR}`); +} + +// 定义日期时间格式化函数,用于生成日志文件名 +function getTimestampString() { + return new Date().toISOString().replace(/[:.]/g, '-'); +} + +// 运行PostgreSQL脚本 +async function runPostgresScript() { + return new Promise((resolve, reject) => { + console.log('\n======================================='); + console.log('正在运行PostgreSQL数据库结构检查脚本...'); + console.log('=======================================\n'); + + const process = exec(`node --no-inspect ${postgresScriptPath}`, (error, stdout, stderr) => { + if (error) { + console.error(`PostgreSQL脚本运行出错: ${error.message}`); + reject(error); + return; + } + + if (stderr) { + console.error(`PostgreSQL脚本错误: ${stderr}`); + } + + console.log(stdout); + resolve(); + }); + }); +} + +// 运行ClickHouse脚本 +async function runClickHouseScript() { + return new Promise((resolve, reject) => { + console.log('\n======================================='); + console.log('正在运行ClickHouse数据库结构检查脚本...'); + console.log('=======================================\n'); + + const process = exec(`node --no-inspect ${clickhouseScriptPath}`, (error, stdout, stderr) => { + if (error) { + console.error(`ClickHouse脚本运行出错: ${error.message}`); + reject(error); + return; + } + + if (stderr) { + console.error(`ClickHouse脚本错误: ${stderr}`); + } + + console.log(stdout); + resolve(); + }); + }); +} + +// 主函数 +async function main() { + try { + console.log('开始运行所有数据库结构检查脚本...'); + console.log(`输出目录: ${DB_REPORTS_DIR}`); + console.log(`时间戳: ${getTimestampString()}`); + + // 运行PostgreSQL脚本 + await runPostgresScript(); + + // 运行ClickHouse脚本 + await runClickHouseScript(); + + console.log('\n======================================='); + console.log('所有数据库结构检查脚本已完成!'); + console.log('报告已保存到以下目录:'); + console.log(DB_REPORTS_DIR); + console.log('======================================='); + } catch (error) { + console.error('运行脚本时出错:', error); + process.exit(1); + } +} + +// 执行主函数 +if (require.main === module) { + main().catch(error => { + console.error('运行脚本时出错:', error); + process.exit(1); + }); +} \ No newline at end of file diff --git a/scripts/db/db-reports/clickhouse-schema-2025-03-20T13-57-59-013Z.log b/scripts/db/db-reports/clickhouse-schema-2025-03-20T13-57-59-013Z.log new file mode 100644 index 0000000..05c9b73 --- /dev/null +++ b/scripts/db/db-reports/clickhouse-schema-2025-03-20T13-57-59-013Z.log @@ -0,0 +1,225 @@ + +获取所有表... +数据库 limq 中找到以下表: + - .inner_id.5d9e5f95-ad7d-4750-ae56-bffea63e14fb + - .inner_id.711eb652-7c90-4f9a-80a0-8979011080cc + - .inner_id.abec445d-1704-4482-bc72-66c9eb67ecd1 + - .inner_id.c1eb844d-7f11-4cfc-8931-c433faaa16b0 + - .inner_id.f9640e70-5b7f-444c-80de-bc5b25848024 + - .inner_id.fe81eeba-acc5-4260-ac9a-973c2f9ce1ea + - link_daily_stats + - link_events + - link_hourly_patterns + - links + - platform_distribution + - project_daily_stats + - projects + - qr_scans + - qrcode_daily_stats + - qrcodes + - sessions + - team_daily_stats + - team_members + - teams + +所有ClickHouse表: +.inner_id.5d9e5f95-ad7d-4750-ae56-bffea63e14fb, .inner_id.711eb652-7c90-4f9a-80a0-8979011080cc, .inner_id.abec445d-1704-4482-bc72-66c9eb67ecd1, .inner_id.c1eb844d-7f11-4cfc-8931-c433faaa16b0, .inner_id.f9640e70-5b7f-444c-80de-bc5b25848024, .inner_id.fe81eeba-acc5-4260-ac9a-973c2f9ce1ea, link_daily_stats, link_events, link_hourly_patterns, links, platform_distribution, project_daily_stats, projects, qr_scans, qrcode_daily_stats, qrcodes, sessions, team_daily_stats, team_members, teams + +获取表 .inner_id.5d9e5f95-ad7d-4750-ae56-bffea63e14fb 的结构... + +获取表 .inner_id.711eb652-7c90-4f9a-80a0-8979011080cc 的结构... + +获取表 .inner_id.abec445d-1704-4482-bc72-66c9eb67ecd1 的结构... + +获取表 .inner_id.c1eb844d-7f11-4cfc-8931-c433faaa16b0 的结构... + +获取表 .inner_id.f9640e70-5b7f-444c-80de-bc5b25848024 的结构... + +获取表 .inner_id.fe81eeba-acc5-4260-ac9a-973c2f9ce1ea 的结构... + +获取表 link_daily_stats 的结构... +表 link_daily_stats 的列: + - date (Date, 无默认值) + - link_id (String, 无默认值) + - total_clicks (UInt64, 无默认值) + - unique_visitors (UInt64, 无默认值) + - unique_sessions (UInt64, 无默认值) + - total_time_spent (UInt64, 无默认值) + - avg_time_spent (Float64, 无默认值) + - bounce_count (UInt64, 无默认值) + - conversion_count (UInt64, 无默认值) + - unique_referrers (UInt64, 无默认值) + - mobile_count (UInt64, 无默认值) + - tablet_count (UInt64, 无默认值) + - desktop_count (UInt64, 无默认值) + - qr_scan_count (UInt64, 无默认值) + - total_conversion_value (Float64, 无默认值) + +获取表 link_events 的结构... +表 link_events 的列: + - event_id (UUID, 默认值: generateUUIDv4()) + - event_time (DateTime64(3), 默认值: now64()) + - date (Date, 默认值: toDate(event_time)) + - link_id (String, 无默认值) + - channel_id (String, 无默认值) + - visitor_id (String, 无默认值) + - session_id (String, 无默认值) + - event_type (Enum8('click' = 1, 'redirect' = 2, 'conversion' = 3, 'error' = 4), 无默认值) + - ip_address (String, 无默认值) + - country (String, 无默认值) + - city (String, 无默认值) + - referrer (String, 无默认值) + - utm_source (String, 无默认值) + - utm_medium (String, 无默认值) + - utm_campaign (String, 无默认值) + - user_agent (String, 无默认值) + - device_type (Enum8('mobile' = 1, 'tablet' = 2, 'desktop' = 3, 'other' = 4), 无默认值) + - browser (String, 无默认值) + - os (String, 无默认值) + - time_spent_sec (UInt32, 默认值: 0) + - is_bounce (Bool, 默认值: true) + - is_qr_scan (Bool, 默认值: false) + - qr_code_id (String, 默认值: '') + - conversion_type (Enum8('visit' = 1, 'stay' = 2, 'interact' = 3, 'signup' = 4, 'subscription' = 5, 'purchase' = 6), 默认值: 'visit') + - conversion_value (Float64, 默认值: 0) + - custom_data (String, 默认值: '{}') + +获取表 link_hourly_patterns 的结构... +表 link_hourly_patterns 的列: + - date (Date, 无默认值) + - hour (UInt8, 无默认值) + - link_id (String, 无默认值) + - visits (UInt64, 无默认值) + - unique_visitors (UInt64, 无默认值) + +获取表 links 的结构... +表 links 的列: + - link_id (String, 无默认值) + - original_url (String, 无默认值) + - created_at (DateTime64(3), 无默认值) + - created_by (String, 无默认值) + - title (String, 无默认值) + - description (String, 无默认值) + - tags (Array(String), 无默认值) + - is_active (Bool, 默认值: true) + - expires_at (Nullable(DateTime), 无默认值) + - team_id (String, 默认值: '') + - project_id (String, 默认值: '') + +获取表 platform_distribution 的结构... +表 platform_distribution 的列: + - date (Date, 无默认值) + - utm_source (String, 无默认值) + - device_type (Enum8('mobile' = 1, 'tablet' = 2, 'desktop' = 3, 'other' = 4), 无默认值) + - visits (UInt64, 无默认值) + - unique_visitors (UInt64, 无默认值) + +获取表 project_daily_stats 的结构... +表 project_daily_stats 的列: + - date (Date, 无默认值) + - project_id (String, 无默认值) + - total_clicks (UInt64, 无默认值) + - unique_visitors (UInt64, 无默认值) + - conversion_count (UInt64, 无默认值) + - links_used (UInt64, 无默认值) + - qr_scan_count (UInt64, 无默认值) + +获取表 projects 的结构... +表 projects 的列: + - project_id (String, 无默认值) + - team_id (String, 无默认值) + - name (String, 无默认值) + - created_at (DateTime, 无默认值) + - created_by (String, 无默认值) + - description (String, 默认值: '') + - is_archived (Bool, 默认值: false) + - links_count (UInt32, 默认值: 0) + - total_clicks (UInt64, 默认值: 0) + - last_updated (DateTime, 默认值: now()) + +获取表 qr_scans 的结构... +表 qr_scans 的列: + - scan_id (UUID, 默认值: generateUUIDv4()) + - qr_code_id (String, 无默认值) + - link_id (String, 无默认值) + - scan_time (DateTime64(3), 无默认值) + - visitor_id (String, 无默认值) + - location (String, 无默认值) + - device_type (Enum8('mobile' = 1, 'tablet' = 2, 'desktop' = 3, 'other' = 4), 无默认值) + - led_to_conversion (Bool, 默认值: false) + +获取表 qrcode_daily_stats 的结构... +表 qrcode_daily_stats 的列: + - date (Date, 无默认值) + - qr_code_id (String, 无默认值) + - total_scans (UInt64, 无默认值) + - unique_scanners (UInt64, 无默认值) + - conversions (UInt64, 无默认值) + - mobile_scans (UInt64, 无默认值) + - tablet_scans (UInt64, 无默认值) + - desktop_scans (UInt64, 无默认值) + - unique_locations (UInt64, 无默认值) + +获取表 qrcodes 的结构... +表 qrcodes 的列: + - qr_code_id (String, 无默认值) + - link_id (String, 无默认值) + - team_id (String, 无默认值) + - project_id (String, 默认值: '') + - name (String, 无默认值) + - description (String, 默认值: '') + - created_at (DateTime, 无默认值) + - created_by (String, 无默认值) + - updated_at (DateTime, 默认值: now()) + - qr_type (Enum8('standard' = 1, 'custom' = 2, 'dynamic' = 3), 默认值: 'standard') + - image_url (String, 默认值: '') + - design_config (String, 默认值: '{}') + - is_active (Bool, 默认值: true) + - total_scans (UInt64, 默认值: 0) + - unique_scanners (UInt32, 默认值: 0) + +获取表 sessions 的结构... +表 sessions 的列: + - session_id (String, 无默认值) + - visitor_id (String, 无默认值) + - link_id (String, 无默认值) + - started_at (DateTime64(3), 无默认值) + - last_activity (DateTime64(3), 无默认值) + - ended_at (Nullable(DateTime64(3)), 无默认值) + - duration_sec (UInt32, 默认值: 0) + - session_pages (UInt8, 默认值: 1) + - is_completed (Bool, 默认值: false) + +获取表 team_daily_stats 的结构... +表 team_daily_stats 的列: + - date (Date, 无默认值) + - team_id (String, 无默认值) + - total_clicks (UInt64, 无默认值) + - unique_visitors (UInt64, 无默认值) + - conversion_count (UInt64, 无默认值) + - links_used (UInt64, 无默认值) + - qr_scan_count (UInt64, 无默认值) + +获取表 team_members 的结构... +表 team_members 的列: + - team_id (String, 无默认值) + - user_id (String, 无默认值) + - role (Enum8('owner' = 1, 'admin' = 2, 'editor' = 3, 'viewer' = 4), 无默认值) + - joined_at (DateTime, 默认值: now()) + - invited_by (String, 无默认值) + - is_active (Bool, 默认值: true) + - last_active (DateTime, 默认值: now()) + +获取表 teams 的结构... +表 teams 的列: + - team_id (String, 无默认值) + - name (String, 无默认值) + - created_at (DateTime, 无默认值) + - created_by (String, 无默认值) + - description (String, 默认值: '') + - avatar_url (String, 默认值: '') + - is_active (Bool, 默认值: true) + - plan_type (Enum8('free' = 1, 'pro' = 2, 'enterprise' = 3), 无默认值) + - members_count (UInt32, 默认值: 1) + +ClickHouse数据库结构检查完成 diff --git a/scripts/db/db-reports/postgres-schema-2025-03-20T13-57-58-555Z.log b/scripts/db/db-reports/postgres-schema-2025-03-20T13-57-58-555Z.log new file mode 100644 index 0000000..977f400 --- /dev/null +++ b/scripts/db/db-reports/postgres-schema-2025-03-20T13-57-58-555Z.log @@ -0,0 +1,483 @@ +使用PostgreSQL连接字符串连接数据库... +成功连接到数据库 + +获取所有表... +找到以下表: + - ProjectTeams + - attribute_schemas + - channel + - channel_tag + - favorite + - form_field_metadata + - google_token + - materials + - permission_resources + - permissions + - platform_tokens + - project_resources + - projects + - qr_code + - queue_tasks + - resource_tags + - resources + - roles + - slide_presentations + - subscription + - sync_resource_typesense_queue + - tags + - team_invitation + - team_join_request + - team_membership + - team_projects + - teams + - type_order + - user_projects + - users + +所有PostgreSQL表: +ProjectTeams, attribute_schemas, channel, channel_tag, favorite, form_field_metadata, google_token, materials, permission_resources, permissions, platform_tokens, project_resources, projects, qr_code, queue_tasks, resource_tags, resources, roles, slide_presentations, subscription, sync_resource_typesense_queue, tags, team_invitation, team_join_request, team_membership, team_projects, teams, type_order, user_projects, users + +获取表 ProjectTeams 的结构... +表 ProjectTeams 的列: + - A (uuid, 不可为空, 默认值: NULL) + - B (uuid, 不可为空, 默认值: NULL) + 主键: 无 + 外键: + - A -> projects.id + 索引: 无 + +获取表 attribute_schemas 的结构... +表 attribute_schemas 的列: + - version (integer(32,0), 可为空, 默认值: NULL) + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - entity_type (text, 可为空, 默认值: NULL) + - schema (jsonb, 可为空, 默认值: NULL) + - updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - is_active (boolean, 可为空, 默认值: NULL) + 主键: id + 外键: 无 + 索引: 无 + +获取表 channel 的结构... +表 channel 的列: + - name (text, 不可为空, 默认值: NULL) + - path (text, 不可为空, 默认值: NULL) + - shortUrlId (uuid, 可为空, 默认值: NULL) + - qrCodeId (uuid, 可为空, 默认值: NULL) + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - isUserCreated (boolean, 可为空, 默认值: false) + 主键: id + 外键: + - qrCodeId -> qr_code.id + - shortUrlId -> resources.id + 索引: 无 + +获取表 channel_tag 的结构... +表 channel_tag 的列: + - channelId (uuid, 不可为空, 默认值: NULL) + - tagId (uuid, 不可为空, 默认值: NULL) + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + 主键: id + 外键: + - channelId -> channel.id + - tagId -> tags.id + 索引: 无 + +获取表 favorite 的结构... +表 favorite 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - user_id (uuid, 不可为空, 默认值: NULL) + - favoritable_id (uuid, 不可为空, 默认值: NULL) + - favoritable_type (text, 不可为空, 默认值: NULL) + - created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - deleted_at (timestamp with time zone, 可为空, 默认值: NULL) + 主键: id + 外键: 无 + 索引: 无 + +获取表 form_field_metadata 的结构... +表 form_field_metadata 的列: + - field_name (text, 可为空, 默认值: NULL) + - resource_type (text, 可为空, 默认值: NULL) + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - default_value (jsonb, 可为空, 默认值: NULL) + - options (jsonb, 可为空, 默认值: NULL) + - validation_rules (jsonb, 可为空, 默认值: NULL) + - is_required (boolean, 可为空, 默认值: NULL) + - order_index (integer(32,0), 可为空, 默认值: NULL) + - is_visible (boolean, 可为空, 默认值: NULL) + - created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - placeholder (text, 可为空, 默认值: NULL) + - label (text, 可为空, 默认值: NULL) + - field_type (text, 可为空, 默认值: NULL) + 主键: id + 外键: 无 + 索引: 无 + +获取表 google_token 的结构... +表 google_token 的列: + - id (text, 不可为空, 默认值: NULL) + - googleEmail (text, 不可为空, 默认值: NULL) + - created_at (timestamp without time zone, 不可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp without time zone, 不可为空, 默认值: CURRENT_TIMESTAMP) + - profile_data (jsonb, 可为空, 默认值: NULL) + - tokens (jsonb, 不可为空, 默认值: NULL) + - user_id (uuid, 不可为空, 默认值: NULL) + 主键: id + 外键: + - user_id -> users.id + 索引: 无 + +获取表 materials 的结构... +表 materials 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - name (text, 不可为空, 默认值: NULL) + - description (text, 可为空, 默认值: ''::text) + - attributes (jsonb, 可为空, 默认值: '{}'::jsonb) + - created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - deleted_at (timestamp with time zone, 可为空, 默认值: NULL) + - schema_version (integer(32,0), 可为空, 默认值: 1) + - type (text, 不可为空, 默认值: NULL) + - is_active (boolean, 可为空, 默认值: true) + - is_system (boolean, 可为空, 默认值: false) + - team_id (uuid, 可为空, 默认值: NULL) + - creator_id (uuid, 可为空, 默认值: NULL) + 主键: id + 外键: 无 + 索引: 无 + +获取表 permission_resources 的结构... +表 permission_resources 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - created_at (timestamp with time zone, 可为空, 默认值: now()) + - updated_at (timestamp with time zone, 可为空, 默认值: now()) + - resource_type (USER-DEFINED, 可为空, 默认值: NULL) + - resource_name (text, 不可为空, 默认值: NULL) + - attributes_type (text, 可为空, 默认值: NULL) + - description (text, 可为空, 默认值: NULL) + 主键: id + 外键: 无 + 索引: 无 + +获取表 permissions 的结构... +表 permissions 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - created_at (timestamp with time zone, 可为空, 默认值: now()) + - updated_at (timestamp with time zone, 可为空, 默认值: now()) + - role_id (uuid, 不可为空, 默认值: NULL) + - resource_id (uuid, 不可为空, 默认值: NULL) + - action (USER-DEFINED, 不可为空, 默认值: NULL) + - metadata (jsonb, 可为空, 默认值: NULL) + 主键: id + 外键: + - resource_id -> permission_resources.id + - role_id -> roles.id + 索引: 无 + +获取表 platform_tokens 的结构... +表 platform_tokens 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - user_id (uuid, 不可为空, 默认值: NULL) + - platform (text, 不可为空, 默认值: NULL) + - access_token (text, 不可为空, 默认值: NULL) + - refresh_token (text, 可为空, 默认值: NULL) + - expires_at (timestamp with time zone, 可为空, 默认值: NULL) + - token_type (text, 可为空, 默认值: NULL) + - scope (text, 可为空, 默认值: NULL) + - created_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP) + - metadata (jsonb, 可为空, 默认值: NULL) + 主键: id + 外键: + - user_id -> users.id + 索引: 无 + +获取表 project_resources 的结构... +表 project_resources 的列: + - project_id (uuid, 不可为空, 默认值: NULL) + - resource_id (uuid, 不可为空, 默认值: NULL) + - assigned_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + 主键: project_id, resource_id + 外键: + - project_id -> projects.id + - resource_id -> resources.id + 索引: 无 + +获取表 projects 的结构... +表 projects 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - name (text, 不可为空, 默认值: NULL) + - description (text, 可为空, 默认值: NULL) + - attributes (jsonb, 可为空, 默认值: NULL) + - created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - deleted_at (timestamp with time zone, 可为空, 默认值: NULL) + - schema_version (integer(32,0), 可为空, 默认值: NULL) + - creator_id (uuid, 可为空, 默认值: NULL) + 主键: id + 外键: 无 + 索引: 无 + +获取表 qr_code 的结构... +表 qr_code 的列: + - scan_count (integer(32,0), 不可为空, 默认值: 0) + - url (text, 不可为空, 默认值: NULL) + - options (jsonb, 可为空, 默认值: NULL) + - template_id (text, 可为空, 默认值: NULL) + - msg_template_id (text, 可为空, 默认值: NULL) + - template_name (text, 可为空, 默认值: NULL) + - attributes (jsonb, 可为空, 默认值: NULL) + - created_at (timestamp without time zone, 不可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp without time zone, 不可为空, 默认值: NULL) + - deleted_at (timestamp without time zone, 可为空, 默认值: NULL) + - resource_id (uuid, 可为空, 默认值: NULL) + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + 主键: id + 外键: + - resource_id -> resources.id + 索引: 无 + +获取表 queue_tasks 的结构... +表 queue_tasks 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - task_id (text, 不可为空, 默认值: NULL) + - type (text, 不可为空, 默认值: NULL) + - status (text, 不可为空, 默认值: NULL) + - data (jsonb, 可为空, 默认值: NULL) + - result (jsonb, 可为空, 默认值: NULL) + - error (text, 可为空, 默认值: NULL) + - created_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP) + - started_at (timestamp with time zone, 可为空, 默认值: NULL) + - finished_at (timestamp with time zone, 可为空, 默认值: NULL) + - creator_id (uuid, 可为空, 默认值: NULL) + 主键: id + 外键: + - creator_id -> users.id + 索引: 无 + +获取表 resource_tags 的结构... +表 resource_tags 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - tag_id (uuid, 不可为空, 默认值: NULL) + - created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - resource_id (uuid, 不可为空, 默认值: NULL) + 主键: id + 外键: + - resource_id -> resources.id + - tag_id -> tags.id + 索引: 无 + +获取表 resources 的结构... +表 resources 的列: + - created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - deleted_at (timestamp with time zone, 可为空, 默认值: NULL) + - schema_version (integer(32,0), 可为空, 默认值: NULL) + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - external_id (text, 可为空, 默认值: NULL) + - attributes (jsonb, 可为空, 默认值: NULL) + - type (text, 可为空, 默认值: NULL) + - creator_id (uuid, 可为空, 默认值: NULL) + 主键: id + 外键: + - creator_id -> users.id + 索引: 无 + +获取表 roles 的结构... +表 roles 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - created_at (timestamp with time zone, 可为空, 默认值: now()) + - updated_at (timestamp with time zone, 可为空, 默认值: now()) + - name (text, 不可为空, 默认值: NULL) + - description (text, 可为空, 默认值: NULL) + 主键: id + 外键: 无 + 索引: 无 + +获取表 slide_presentations 的结构... +表 slide_presentations 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - slide_id (text, 不可为空, 默认值: NULL) + - user_id (uuid, 不可为空, 默认值: NULL) + - content (jsonb, 不可为空, 默认值: NULL) + - created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - deleted_at (timestamp with time zone, 可为空, 默认值: NULL) + 主键: id + 外键: + - user_id -> users.id + 索引: 无 + +获取表 subscription 的结构... +表 subscription 的列: + - id (text, 不可为空, 默认值: NULL) + - team_id (uuid, 不可为空, 默认值: NULL) + - customer_id (text, 不可为空, 默认值: NULL) + - status (USER-DEFINED, 不可为空, 默认值: NULL) + - plan_id (text, 不可为空, 默认值: NULL) + - variant_id (text, 不可为空, 默认值: NULL) + - next_payment_date (timestamp without time zone, 可为空, 默认值: NULL) + 主键: id + 外键: 无 + 索引: 无 + +获取表 sync_resource_typesense_queue 的结构... +表 sync_resource_typesense_queue 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - resource_id (uuid, 不可为空, 默认值: NULL) + - entity_type (text, 不可为空, 默认值: NULL) + - action (text, 不可为空, 默认值: NULL) + - status (text, 不可为空, 默认值: NULL) + - error (text, 可为空, 默认值: NULL) + - created_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP) + - processed_at (timestamp with time zone, 可为空, 默认值: NULL) + 主键: id + 外键: + - resource_id -> resources.id + 索引: 无 + +获取表 tags 的结构... +表 tags 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - name (text, 可为空, 默认值: NULL) + - type (text, 可为空, 默认值: NULL) + - attributes (jsonb, 可为空, 默认值: NULL) + - created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - deleted_at (timestamp with time zone, 可为空, 默认值: NULL) + - parent_tag_id (uuid, 可为空, 默认值: NULL) + - team_id (uuid, 可为空, 默认值: NULL) + - is_shared (boolean, 可为空, 默认值: false) + - schema_version (integer(32,0), 可为空, 默认值: NULL) + - is_system (boolean, 可为空, 默认值: false) + 主键: id + 外键: + - parent_tag_id -> tags.id + 索引: 无 + +获取表 team_invitation 的结构... +表 team_invitation 的列: + - id (text, 不可为空, 默认值: NULL) + - team_id (uuid, 不可为空, 默认值: NULL) + - email (text, 可为空, 默认值: NULL) + - role (USER-DEFINED, 不可为空, 默认值: 'MEMBER'::limq.team_member_role) + - created_at (timestamp without time zone, 不可为空, 默认值: CURRENT_TIMESTAMP) + - expires_at (timestamp without time zone, 不可为空, 默认值: NULL) + - type (text, 可为空, 默认值: NULL) + - deleted_at (timestamp with time zone, 可为空, 默认值: NULL) + - status (USER-DEFINED, 可为空, 默认值: NULL) + 主键: id + 外键: + - team_id -> teams.id + 索引: 无 + +获取表 team_join_request 的结构... +表 team_join_request 的列: + - id (text, 不可为空, 默认值: NULL) + - team_id (uuid, 不可为空, 默认值: NULL) + - user_id (uuid, 不可为空, 默认值: NULL) + - invitation_id (text, 不可为空, 默认值: NULL) + - status (USER-DEFINED, 不可为空, 默认值: 'PENDING'::limq.request_status) + - message (text, 可为空, 默认值: NULL) + - created_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 不可为空, 默认值: NULL) + 主键: id + 外键: + - invitation_id -> team_invitation.id + - user_id -> users.id + 索引: 无 + +获取表 team_membership 的结构... +表 team_membership 的列: + - id (text, 不可为空, 默认值: NULL) + - team_id (uuid, 不可为空, 默认值: NULL) + - user_id (uuid, 不可为空, 默认值: NULL) + - is_creator (boolean, 不可为空, 默认值: false) + - role (text, 不可为空, 默认值: NULL) + 主键: id + 外键: + - team_id -> teams.id + - user_id -> users.id + 索引: 无 + +获取表 team_projects 的结构... +表 team_projects 的列: + - team_id (uuid, 不可为空, 默认值: NULL) + - project_id (uuid, 不可为空, 默认值: NULL) + - assigned_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + 主键: team_id, project_id + 外键: + - project_id -> projects.id + - team_id -> teams.id + 索引: 无 + +获取表 teams 的结构... +表 teams 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - name (text, 不可为空, 默认值: NULL) + - description (text, 可为空, 默认值: NULL) + - attributes (jsonb, 可为空, 默认值: NULL) + - created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - deleted_at (timestamp with time zone, 可为空, 默认值: NULL) + - schema_version (integer(32,0), 可为空, 默认值: NULL) + - avatar_url (text, 可为空, 默认值: NULL) + 主键: id + 外键: 无 + 索引: 无 + +获取表 type_order 的结构... +表 type_order 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - user_id (uuid, 不可为空, 默认值: NULL) + - category (text, 不可为空, 默认值: NULL) + - type (text, 不可为空, 默认值: NULL) + - order (integer(32,0), 不可为空, 默认值: NULL) + - created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + 主键: id + 外键: + - user_id -> users.id + 索引: 无 + +获取表 user_projects 的结构... +表 user_projects 的列: + - user_id (uuid, 不可为空, 默认值: NULL) + - project_id (uuid, 不可为空, 默认值: NULL) + - role (text, 可为空, 默认值: NULL) + - joined_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + 主键: user_id, project_id + 外键: + - project_id -> projects.id + - user_id -> users.id + 索引: 无 + +获取表 users 的结构... +表 users 的列: + - id (uuid, 不可为空, 默认值: gen_random_uuid()) + - email (text, 不可为空, 默认值: NULL) + - password_hash (text, 可为空, 默认值: NULL) + - first_name (text, 可为空, 默认值: NULL) + - last_name (text, 可为空, 默认值: NULL) + - phone_number (text, 可为空, 默认值: NULL) + - is_email_verified (boolean, 可为空, 默认值: NULL) + - is_phone_verified (boolean, 可为空, 默认值: NULL) + - last_login_at (timestamp with time zone, 可为空, 默认值: NULL) + - attributes (jsonb, 可为空, 默认值: NULL) + - external_id (text, 可为空, 默认值: NULL) + - created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP) + - deleted_at (timestamp with time zone, 可为空, 默认值: NULL) + - schema_version (integer(32,0), 可为空, 默认值: NULL) + - avatar_url (text, 可为空, 默认值: NULL) + 主键: id + 外键: 无 + 索引: 无 + +数据库结构检查完成 diff --git a/scripts/db/load-clickhouse-testdata.sh b/scripts/db/load-clickhouse-testdata.sh new file mode 100755 index 0000000..26f99c3 --- /dev/null +++ b/scripts/db/load-clickhouse-testdata.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# 脚本名称: load-clickhouse-testdata.sh +# 用途: 将测试数据加载到ClickHouse数据库中 + +# 设置脚本目录路径 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# 设置SQL文件路径 +SQL_FILE="$SCRIPT_DIR/sql/clickhouse/seed-clickhouse-analytics.sql" + +# 检查SQL文件是否存在 +if [ ! -f "$SQL_FILE" ]; then + echo "错误: SQL文件 '$SQL_FILE' 不存在" + exit 1 +fi + +# 执行CH查询脚本 +echo "开始加载测试数据到ClickHouse数据库..." +bash "$SCRIPT_DIR/sql/clickhouse/ch-query.sh" -f "$SQL_FILE" + +# 检查执行结果 +if [ $? -eq 0 ]; then + echo "测试数据已成功加载到ClickHouse数据库" +else + echo "错误: 加载测试数据失败" + exit 1 +fi + +exit 0 diff --git a/scripts/db/sql/clickhouse/ch-query.sh b/scripts/db/sql/clickhouse/ch-query.sh new file mode 100755 index 0000000..55ca3d8 --- /dev/null +++ b/scripts/db/sql/clickhouse/ch-query.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# 文件名: ch-query.sh +# 用途: 执行ClickHouse SQL查询的便捷脚本 + +# 连接参数 +CH_HOST="localhost" +CH_PORT="9000" +CH_USER="admin" +CH_PASSWORD="your_secure_password" + +# 基本查询函数 +function ch_query() { + clickhouse client --host $CH_HOST --port $CH_PORT --user $CH_USER --password $CH_PASSWORD -q "$1" +} + +# 显示帮助信息 +function show_help() { + echo "ClickHouse 查询工具" + echo "用法: $0 [选项] [SQL查询]" + echo "" + echo "选项:" + echo " -t 显示所有表" + echo " -d 显示所有数据库" + echo " -s <表名> 显示表结构" + echo " -p <表名> 显示表样本数据(前10行)" + echo " -c <表名> 计算表中的记录数" + echo " -h, --help 显示此帮助信息" + echo " -q \"SQL查询\" 执行自定义SQL查询" + echo " -f <文件名> 执行SQL文件" + echo "" + echo "示例:" + echo " $0 -d # 显示所有数据库" + echo " $0 -t # 显示所有表" + echo " $0 -s limq.link_events # 显示link_events表结构" + echo " $0 -q \"SELECT * FROM limq.link_events LIMIT 5\" # 执行自定义查询" +} + +# 没有参数时显示帮助 +if [ $# -eq 0 ]; then + show_help + exit 0 +fi + +# 处理命令行参数 +case "$1" in + -t) + ch_query "SHOW TABLES" + ;; + -d) + ch_query "SHOW DATABASES" + ;; + -s) + if [ -z "$2" ]; then + echo "错误: 需要提供表名" + exit 1 + fi + ch_query "DESCRIBE TABLE $2" + ;; + -p) + if [ -z "$2" ]; then + echo "错误: 需要提供表名" + exit 1 + fi + ch_query "SELECT * FROM $2 LIMIT 10" + ;; + -c) + if [ -z "$2" ]; then + echo "错误: 需要提供表名" + exit 1 + fi + ch_query "SELECT COUNT(*) FROM $2" + ;; + -q) + if [ -z "$2" ]; then + echo "错误: 需要提供SQL查询" + exit 1 + fi + ch_query "$2" + ;; + -f) + if [ -z "$2" ]; then + echo "错误: 需要提供SQL文件" + exit 1 + fi + if [ ! -f "$2" ]; then + echo "错误: 文件 '$2' 不存在" + exit 1 + fi + SQL=$(cat "$2") + ch_query "$SQL" + ;; + -h|--help) + show_help + ;; + *) + echo "未知选项: $1" + show_help + exit 1 + ;; +esac + +exit 0 \ No newline at end of file diff --git a/scripts/db/sql/clickhouse/clickhouse.md b/scripts/db/sql/clickhouse/clickhouse.md new file mode 100644 index 0000000..e73a71b --- /dev/null +++ b/scripts/db/sql/clickhouse/clickhouse.md @@ -0,0 +1,4 @@ +```bash +alias clickhouse-sql='clickhouse client --host localhost --port 9000 --user admin --password your_secure_password --database promote -q' +clickhouse-sql "SHOW TABLES" +``` \ No newline at end of file diff --git a/scripts/db/sql/clickhouse/create_limq.sql b/scripts/db/sql/clickhouse/create_limq.sql new file mode 100644 index 0000000..b58b700 --- /dev/null +++ b/scripts/db/sql/clickhouse/create_limq.sql @@ -0,0 +1,170 @@ +-- 创建数据库 +CREATE DATABASE IF NOT EXISTS limq; + +-- 切换到limq数据库 +USE limq; + +-- 创建短链接访问事件表 +CREATE TABLE IF NOT EXISTS limq.link_events ( + event_id UUID DEFAULT generateUUIDv4(), + event_time DateTime64(3) DEFAULT now64(), + date Date DEFAULT toDate(event_time), + link_id String, + channel_id String, + visitor_id String, + session_id String, + event_type Enum8( + 'click' = 1, + 'redirect' = 2, + 'conversion' = 3, + 'error' = 4 + ), + -- 访问者信息 + ip_address String, + country String, + city String, + -- 来源信息 + referrer String, + utm_source String, + utm_medium String, + utm_campaign String, + -- 设备信息 + user_agent String, + device_type Enum8( + 'mobile' = 1, + 'tablet' = 2, + 'desktop' = 3, + 'other' = 4 + ), + browser String, + os String, + -- 交互信息 + time_spent_sec UInt32 DEFAULT 0, + is_bounce Boolean DEFAULT true, + -- QR码相关 + is_qr_scan Boolean DEFAULT false, + qr_code_id String DEFAULT '', + -- 转化数据 + conversion_type String DEFAULT '', + conversion_value Float64 DEFAULT 0, + -- 其他属性 + custom_data String DEFAULT '{}' +) ENGINE = MergeTree() PARTITION BY toYYYYMM(date) +ORDER BY + (date, link_id, event_time) SETTINGS index_granularity = 8192; + +-- 短链接维度表 +CREATE TABLE IF NOT EXISTS limq.links ( + link_id String, + original_url String, + created_at DateTime, + created_by String, + title String, + description String, + tags Array(String), + is_active Boolean DEFAULT true, + expires_at Nullable(DateTime), + team_id String DEFAULT '', + project_id String DEFAULT '', + PRIMARY KEY (link_id) +) ENGINE = ReplacingMergeTree() +ORDER BY + link_id SETTINGS index_granularity = 8192; + +-- 会话跟踪表 +CREATE TABLE IF NOT EXISTS limq.sessions ( + session_id String, + visitor_id String, + link_id String, + started_at DateTime64(3), + last_activity DateTime64(3), + ended_at Nullable(DateTime64(3)), + duration_sec UInt32 DEFAULT 0, + session_pages UInt8 DEFAULT 1, + is_completed Boolean DEFAULT false, + PRIMARY KEY (session_id) +) ENGINE = ReplacingMergeTree(last_activity) +ORDER BY + (session_id, link_id, visitor_id) SETTINGS index_granularity = 8192; + +-- QR码统计表 +CREATE TABLE IF NOT EXISTS limq.qr_scans ( + scan_id UUID DEFAULT generateUUIDv4(), + qr_code_id String, + link_id String, + scan_time DateTime64(3), + visitor_id String, + location String, + device_type Enum8( + 'mobile' = 1, + 'tablet' = 2, + 'desktop' = 3, + 'other' = 4 + ), + led_to_conversion Boolean DEFAULT false, + PRIMARY KEY (scan_id) +) ENGINE = MergeTree() PARTITION BY toYYYYMM(scan_time) +ORDER BY + (qr_code_id, scan_time) SETTINGS index_granularity = 8192; + +-- 每日链接汇总视图 +CREATE MATERIALIZED VIEW limq.link_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date) +ORDER BY + (date, link_id) SETTINGS index_granularity = 8192 AS +SELECT + toDate(event_time) AS date, + link_id, + count() AS total_clicks, + uniqExact(visitor_id) AS unique_visitors, + uniqExact(session_id) AS unique_sessions, + sum(time_spent_sec) AS total_time_spent, + avg(time_spent_sec) AS avg_time_spent, + countIf(is_bounce) AS bounce_count, + countIf(event_type = 'conversion') AS conversion_count, + uniqExact(referrer) AS unique_referrers, + countIf(device_type = 'mobile') AS mobile_count, + countIf(device_type = 'tablet') AS tablet_count, + countIf(device_type = 'desktop') AS desktop_count, + countIf(is_qr_scan) AS qr_scan_count, + sum(conversion_value) AS total_conversion_value +FROM + limq.link_events +GROUP BY + date, + link_id; + +-- 每小时访问模式视图 +CREATE MATERIALIZED VIEW limq.link_hourly_patterns ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date) +ORDER BY + (date, hour, link_id) SETTINGS index_granularity = 8192 AS +SELECT + toDate(event_time) AS date, + toHour(event_time) AS hour, + link_id, + count() AS visits, + uniqExact(visitor_id) AS unique_visitors +FROM + limq.link_events +GROUP BY + date, + hour, + link_id; + +-- 平台分布视图 +CREATE MATERIALIZED VIEW limq.platform_distribution ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date) +ORDER BY + (date, utm_source, device_type) SETTINGS index_granularity = 8192 AS +SELECT + toDate(event_time) AS date, + utm_source, + device_type, + count() AS visits, + uniqExact(visitor_id) AS unique_visitors +FROM + limq.link_events +WHERE + utm_source != '' +GROUP BY + date, + utm_source, + device_type; \ No newline at end of file diff --git a/scripts/db/sql/clickhouse/create_team_project_qrcode.sql b/scripts/db/sql/clickhouse/create_team_project_qrcode.sql new file mode 100644 index 0000000..67259ae --- /dev/null +++ b/scripts/db/sql/clickhouse/create_team_project_qrcode.sql @@ -0,0 +1,146 @@ +-- 添加team、project和qrcode表到limq数据库 +USE limq; + +-- 团队表 +CREATE TABLE IF NOT EXISTS limq.teams ( + team_id String, + name String, + created_at DateTime, + created_by String, + description String DEFAULT '', + avatar_url String DEFAULT '', + is_active Boolean DEFAULT true, + plan_type Enum8( + 'free' = 1, + 'pro' = 2, + 'enterprise' = 3 + ), + members_count UInt32 DEFAULT 1, + PRIMARY KEY (team_id) +) ENGINE = ReplacingMergeTree() +ORDER BY + team_id SETTINGS index_granularity = 8192; + +-- 项目表 +CREATE TABLE IF NOT EXISTS limq.projects ( + project_id String, + team_id String, + name String, + created_at DateTime, + created_by String, + description String DEFAULT '', + is_archived Boolean DEFAULT false, + links_count UInt32 DEFAULT 0, + total_clicks UInt64 DEFAULT 0, + last_updated DateTime DEFAULT now(), + PRIMARY KEY (project_id) +) ENGINE = ReplacingMergeTree() +ORDER BY + (project_id, team_id) SETTINGS index_granularity = 8192; + +-- QR码表 (扩展现有的qr_scans表) +CREATE TABLE IF NOT EXISTS limq.qrcodes ( + qr_code_id String, + link_id String, + team_id String, + project_id String DEFAULT '', + name String, + description String DEFAULT '', + created_at DateTime, + created_by String, + updated_at DateTime DEFAULT now(), + qr_type Enum8( + 'standard' = 1, + 'custom' = 2, + 'dynamic' = 3 + ) DEFAULT 'standard', + image_url String DEFAULT '', + design_config String DEFAULT '{}', + is_active Boolean DEFAULT true, + total_scans UInt64 DEFAULT 0, + unique_scanners UInt32 DEFAULT 0, + PRIMARY KEY (qr_code_id) +) ENGINE = ReplacingMergeTree() +ORDER BY + (qr_code_id, link_id) SETTINGS index_granularity = 8192; + +-- 团队成员表 +CREATE TABLE IF NOT EXISTS limq.team_members ( + team_id String, + user_id String, + role Enum8( + 'owner' = 1, + 'admin' = 2, + 'editor' = 3, + 'viewer' = 4 + ), + joined_at DateTime DEFAULT now(), + invited_by String, + is_active Boolean DEFAULT true, + last_active DateTime DEFAULT now(), + PRIMARY KEY (team_id, user_id) +) ENGINE = ReplacingMergeTree() +ORDER BY + (team_id, user_id) SETTINGS index_granularity = 8192; + +-- 团队每日统计视图 +CREATE MATERIALIZED VIEW limq.team_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date) +ORDER BY + (date, team_id) SETTINGS index_granularity = 8192 AS +SELECT + toDate(event_time) AS date, + l.team_id AS team_id, + count() AS total_clicks, + uniqExact(e.visitor_id) AS unique_visitors, + countIf(e.event_type = 'conversion') AS conversion_count, + uniqExact(e.link_id) AS links_used, + countIf(e.is_qr_scan) AS qr_scan_count +FROM + limq.link_events e + JOIN limq.links l ON e.link_id = l.link_id +WHERE + l.team_id != '' +GROUP BY + date, + l.team_id; + +-- 项目每日统计视图 +CREATE MATERIALIZED VIEW limq.project_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date) +ORDER BY + (date, project_id) SETTINGS index_granularity = 8192 AS +SELECT + toDate(event_time) AS date, + l.project_id AS project_id, + count() AS total_clicks, + uniqExact(e.visitor_id) AS unique_visitors, + countIf(e.event_type = 'conversion') AS conversion_count, + uniqExact(e.link_id) AS links_used, + countIf(e.is_qr_scan) AS qr_scan_count +FROM + limq.link_events e + JOIN limq.links l ON e.link_id = l.link_id +WHERE + l.project_id != '' +GROUP BY + date, + l.project_id; + +-- QR码每日统计视图 +CREATE MATERIALIZED VIEW limq.qrcode_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date) +ORDER BY + (date, qr_code_id) SETTINGS index_granularity = 8192 AS +SELECT + toDate(scan_time) AS date, + qr_code_id, + count() AS total_scans, + uniqExact(visitor_id) AS unique_scanners, + countIf(led_to_conversion) AS conversions, + countIf(device_type = 'mobile') AS mobile_scans, + countIf(device_type = 'tablet') AS tablet_scans, + countIf(device_type = 'desktop') AS desktop_scans, + uniqExact(location) AS unique_locations +FROM + limq.qr_scans +GROUP BY + date, + qr_code_id; \ No newline at end of file diff --git a/scripts/db/sql/clickhouse/mock_link_data.sql b/scripts/db/sql/clickhouse/mock_link_data.sql new file mode 100644 index 0000000..2fa76cb --- /dev/null +++ b/scripts/db/sql/clickhouse/mock_link_data.sql @@ -0,0 +1,997 @@ +-- 移动端点击访问事件 +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-15 10:25:30', + '2025-03-15', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-123', + 's-456', + 'click', + '103.45.67.89', + 'China', + 'Shanghai', + 'https://www.google.com', + 'google', + 'organic', + 'none', + 'Mozilla/5.0 (iPhone)', + 'mobile', + 'Safari', + 'iOS', + 45, + false, + false, + 'visit', + 0 + ); + +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-15 11:32:21', + '2025-03-15', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-124', + 's-457', + 'click', + '43.78.123.45', + 'Japan', + 'Tokyo', + 'https://twitter.com', + 'twitter', + 'social', + 'spring_promo', + 'Mozilla/5.0 (Android 10)', + 'mobile', + 'Chrome', + 'Android', + 15, + true, + false, + 'visit', + 0 + ); + +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-15 14:15:45', + '2025-03-15', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-125', + 's-458', + 'click', + '72.34.67.81', + 'US', + 'New York', + 'https://www.facebook.com', + 'facebook', + 'social', + 'crypto_ad', + 'Mozilla/5.0 (iPhone)', + 'mobile', + 'Safari', + 'iOS', + 120, + false, + false, + 'interact', + 0 + ); + +-- 桌面设备点击事件 +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-15 08:45:12', + '2025-03-15', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-126', + 's-459', + 'click', + '89.67.43.21', + 'Germany', + 'Berlin', + 'https://www.reddit.com', + 'reddit', + 'referral', + 'none', + 'Mozilla/5.0 (Windows NT 10.0)', + 'desktop', + 'Chrome', + 'Windows', + 300, + false, + false, + 'visit', + 0 + ); + +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-15 16:20:33', + '2025-03-15', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-127', + 's-460', + 'click', + '178.65.43.12', + 'UK', + 'London', + 'https://www.linkedin.com', + 'linkedin', + 'social', + 'biz_campaign', + 'Mozilla/5.0 (Macintosh)', + 'desktop', + 'Safari', + 'MacOS', + 250, + false, + false, + 'stay', + 0 + ); + +-- 平板设备点击事件 +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-15 13:10:55', + '2025-03-15', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-128', + 's-461', + 'click', + '156.78.34.12', + 'Canada', + 'Toronto', + 'https://www.youtube.com', + 'youtube', + 'video', + 'tutorial', + 'Mozilla/5.0 (iPad)', + 'tablet', + 'Safari', + 'iOS', + 180, + false, + false, + 'visit', + 0 + ); + +-- QR扫描访问事件 +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-15 09:30:22', + '2025-03-15', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_qr', + 'v-129', + 's-462', + 'click', + '101.56.78.90', + 'China', + 'Beijing', + 'direct', + 'qr', + 'print', + 'offline_event', + 'Mozilla/5.0 (iPhone)', + 'mobile', + 'Safari', + 'iOS', + 75, + false, + true, + 'visit', + 0 + ); + +-- 转化事件 +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-15 10:27:45', + '2025-03-15', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-123', + 's-456', + 'conversion', + '103.45.67.89', + 'China', + 'Shanghai', + 'https://www.google.com', + 'google', + 'organic', + 'none', + 'Mozilla/5.0 (iPhone)', + 'mobile', + 'Safari', + 'iOS', + 120, + false, + false, + 'signup', + 50 + ); + +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-15 08:52:18', + '2025-03-15', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-126', + 's-459', + 'conversion', + '89.67.43.21', + 'Germany', + 'Berlin', + 'https://www.reddit.com', + 'reddit', + 'referral', + 'none', + 'Mozilla/5.0 (Windows NT 10.0)', + 'desktop', + 'Chrome', + 'Windows', + 450, + false, + false, + 'purchase', + 150.75 + ); + +-- 第二天的数据 (3/16) +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-16 11:15:30', + '2025-03-16', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-130', + 's-463', + 'click', + '178.91.45.67', + 'France', + 'Paris', + 'https://www.google.com', + 'google', + 'organic', + 'none', + 'Mozilla/5.0 (Android 11)', + 'mobile', + 'Chrome', + 'Android', + 60, + false, + false, + 'visit', + 0 + ); + +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-16 14:22:45', + '2025-03-16', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-131', + 's-464', + 'click', + '89.123.45.78', + 'Spain', + 'Madrid', + 'https://www.instagram.com', + 'instagram', + 'social', + 'influencer', + 'Mozilla/5.0 (iPhone)', + 'mobile', + 'Safari', + 'iOS', + 90, + false, + false, + 'interact', + 0 + ); + +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-16 16:40:12', + '2025-03-16', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-131', + 's-464', + 'conversion', + '89.123.45.78', + 'Spain', + 'Madrid', + 'https://www.instagram.com', + 'instagram', + 'social', + 'influencer', + 'Mozilla/5.0 (iPhone)', + 'mobile', + 'Safari', + 'iOS', + 200, + false, + false, + 'subscription', + 75.50 + ); + +-- 第三天数据 (3/17) +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-17 09:10:22', + '2025-03-17', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-132', + 's-465', + 'click', + '45.67.89.123', + 'US', + 'Los Angeles', + 'https://www.google.com', + 'google', + 'cpc', + 'spring_sale', + 'Mozilla/5.0 (Windows NT 10.0)', + 'desktop', + 'Edge', + 'Windows', + 150, + false, + false, + 'visit', + 0 + ); + +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-17 12:30:45', + '2025-03-17', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-133', + 's-466', + 'click', + '67.89.123.45', + 'Brazil', + 'Sao Paulo', + 'https://www.yahoo.com', + 'yahoo', + 'organic', + 'none', + 'Mozilla/5.0 (iPad)', + 'tablet', + 'Safari', + 'iOS', + 120, + false, + false, + 'stay', + 0 + ); + +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-17 15:45:33', + '2025-03-17', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-132', + 's-465', + 'conversion', + '45.67.89.123', + 'US', + 'Los Angeles', + 'https://www.google.com', + 'google', + 'cpc', + 'spring_sale', + 'Mozilla/5.0 (Windows NT 10.0)', + 'desktop', + 'Edge', + 'Windows', + 300, + false, + false, + 'purchase', + 225.50 + ); + +-- 添加一周前的数据 (对比期) +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-08 10:25:30', + '2025-03-08', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-140', + 's-470', + 'click', + '103.45.67.89', + 'China', + 'Shanghai', + 'https://www.google.com', + 'google', + 'organic', + 'none', + 'Mozilla/5.0 (iPhone)', + 'mobile', + 'Safari', + 'iOS', + 30, + false, + false, + 'visit', + 0 + ); + +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-08 11:32:21', + '2025-03-08', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-141', + 's-471', + 'click', + '89.67.43.21', + 'Germany', + 'Berlin', + 'https://www.reddit.com', + 'reddit', + 'referral', + 'none', + 'Mozilla/5.0 (Windows NT 10.0)', + 'desktop', + 'Chrome', + 'Windows', + 200, + false, + false, + 'visit', + 0 + ); + +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + conversion_type, + conversion_value + ) +VALUES + ( + generateUUIDv4(), + '2025-03-08 13:10:55', + '2025-03-08', + 'a71fcfe8-d293-4d6d-91c5-1528fa7f6294', + 'ch_main', + 'v-140', + 's-470', + 'conversion', + '103.45.67.89', + 'China', + 'Shanghai', + 'https://www.google.com', + 'google', + 'organic', + 'none', + 'Mozilla/5.0 (iPhone)', + 'mobile', + 'Safari', + 'iOS', + 100, + false, + false, + 'purchase', + 100.00 + ); \ No newline at end of file diff --git a/scripts/db/sql/clickhouse/recreate_limq.sql b/scripts/db/sql/clickhouse/recreate_limq.sql new file mode 100644 index 0000000..cbbfeeb --- /dev/null +++ b/scripts/db/sql/clickhouse/recreate_limq.sql @@ -0,0 +1,193 @@ +-- 删除现有的物化视图(需要先删除视图,因为它们依赖于表) +DROP TABLE IF EXISTS limq.platform_distribution; + +DROP TABLE IF EXISTS limq.link_hourly_patterns; + +DROP TABLE IF EXISTS limq.link_daily_stats; + +-- 删除现有的表 +DROP TABLE IF EXISTS limq.qr_scans; + +DROP TABLE IF EXISTS limq.sessions; + +DROP TABLE IF EXISTS limq.link_events; + +DROP TABLE IF EXISTS limq.links; + +-- 创建数据库(如果不存在) +CREATE DATABASE IF NOT EXISTS limq; + +-- 切换到limq数据库 +USE limq; + +-- 创建短链接访问事件表 +CREATE TABLE IF NOT EXISTS limq.link_events ( + event_id UUID DEFAULT generateUUIDv4(), + event_time DateTime64(3) DEFAULT now64(), + date Date DEFAULT toDate(event_time), + link_id String, + channel_id String, + visitor_id String, + session_id String, + event_type Enum8( + 'click' = 1, + 'redirect' = 2, + 'conversion' = 3, + 'error' = 4 + ), + -- 访问者信息 + ip_address String, + country String, + city String, + -- 来源信息 + referrer String, + utm_source String, + utm_medium String, + utm_campaign String, + -- 设备信息 + user_agent String, + device_type Enum8( + 'mobile' = 1, + 'tablet' = 2, + 'desktop' = 3, + 'other' = 4 + ), + browser String, + os String, + -- 交互信息 + time_spent_sec UInt32 DEFAULT 0, + is_bounce Boolean DEFAULT true, + -- QR码相关 + is_qr_scan Boolean DEFAULT false, + qr_code_id String DEFAULT '', + -- 转化数据 + conversion_type Enum8( + 'visit' = 1, + 'stay' = 2, + 'interact' = 3, + 'signup' = 4, + 'subscription' = 5, + 'purchase' = 6 + ) DEFAULT 'visit', + conversion_value Float64 DEFAULT 0, + -- 其他属性 + custom_data String DEFAULT '{}' +) ENGINE = MergeTree() PARTITION BY toYYYYMM(date) +ORDER BY + (date, link_id, event_time) SETTINGS index_granularity = 8192; + +-- 短链接维度表 +CREATE TABLE IF NOT EXISTS limq.links ( + link_id String, + original_url String, + created_at DateTime64(3), + created_by String, + title String, + description String, + tags Array(String), + is_active Boolean DEFAULT true, + expires_at Nullable(DateTime64(3)), + team_id String DEFAULT '', + project_id String DEFAULT '', + PRIMARY KEY (link_id) +) ENGINE = ReplacingMergeTree() +ORDER BY + link_id SETTINGS index_granularity = 8192; + +-- 会话跟踪表 +CREATE TABLE IF NOT EXISTS limq.sessions ( + session_id String, + visitor_id String, + link_id String, + started_at DateTime64(3), + last_activity DateTime64(3), + ended_at Nullable(DateTime64(3)), + duration_sec UInt32 DEFAULT 0, + session_pages UInt8 DEFAULT 1, + is_completed Boolean DEFAULT false, + PRIMARY KEY (session_id) +) ENGINE = ReplacingMergeTree(last_activity) +ORDER BY + (session_id, link_id, visitor_id) SETTINGS index_granularity = 8192; + +-- QR码统计表 +CREATE TABLE IF NOT EXISTS limq.qr_scans ( + scan_id UUID DEFAULT generateUUIDv4(), + qr_code_id String, + link_id String, + scan_time DateTime64(3), + visitor_id String, + location String, + device_type Enum8( + 'mobile' = 1, + 'tablet' = 2, + 'desktop' = 3, + 'other' = 4 + ), + led_to_conversion Boolean DEFAULT false, + PRIMARY KEY (scan_id) +) ENGINE = MergeTree() PARTITION BY toYYYYMM(scan_time) +ORDER BY + scan_id SETTINGS index_granularity = 8192; + +-- 每日链接汇总视图 +CREATE MATERIALIZED VIEW IF NOT EXISTS limq.link_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date) +ORDER BY + (date, link_id) SETTINGS index_granularity = 8192 AS +SELECT + toDate(event_time) AS date, + link_id, + count() AS total_clicks, + uniqExact(visitor_id) AS unique_visitors, + uniqExact(session_id) AS unique_sessions, + sum(time_spent_sec) AS total_time_spent, + avg(time_spent_sec) AS avg_time_spent, + countIf(is_bounce) AS bounce_count, + countIf(event_type = 'conversion') AS conversion_count, + uniqExact(referrer) AS unique_referrers, + countIf(device_type = 'mobile') AS mobile_count, + countIf(device_type = 'tablet') AS tablet_count, + countIf(device_type = 'desktop') AS desktop_count, + countIf(is_qr_scan) AS qr_scan_count, + sum(conversion_value) AS total_conversion_value +FROM + limq.link_events +GROUP BY + date, + link_id; + +-- 每小时访问模式视图 +CREATE MATERIALIZED VIEW IF NOT EXISTS limq.link_hourly_patterns ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date) +ORDER BY + (date, hour, link_id) SETTINGS index_granularity = 8192 AS +SELECT + toDate(event_time) AS date, + toHour(event_time) AS hour, + link_id, + count() AS visits, + uniqExact(visitor_id) AS unique_visitors +FROM + limq.link_events +GROUP BY + date, + hour, + link_id; + +-- 平台分布视图 +CREATE MATERIALIZED VIEW IF NOT EXISTS limq.platform_distribution ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date) +ORDER BY + (date, utm_source, device_type) SETTINGS index_granularity = 8192 AS +SELECT + toDate(event_time) AS date, + utm_source, + device_type, + count() AS visits, + uniqExact(visitor_id) AS unique_visitors +FROM + limq.link_events +WHERE + utm_source != '' +GROUP BY + date, + utm_source, + device_type; \ No newline at end of file diff --git a/scripts/db/sql/clickhouse/seed-clickhouse-analytics.sql b/scripts/db/sql/clickhouse/seed-clickhouse-analytics.sql new file mode 100644 index 0000000..d6bdcbc --- /dev/null +++ b/scripts/db/sql/clickhouse/seed-clickhouse-analytics.sql @@ -0,0 +1,828 @@ +-- 清空现有数据(可选) +TRUNCATE TABLE IF EXISTS limq.link_events; + +TRUNCATE TABLE IF EXISTS limq.link_daily_stats; + +TRUNCATE TABLE IF EXISTS limq.link_hourly_patterns; + +TRUNCATE TABLE IF EXISTS limq.links; + +-- 使用固定的UUID值插入链接 +INSERT INTO + limq.links ( + link_id, + original_url, + created_at, + created_by, + title, + description, + tags, + is_active + ) +VALUES + ( + '11111111-1111-1111-1111-111111111111', + 'https://example.com/page1', + now(), + 'user-1', + '产品页面', + '我们的主要产品页面', + [ '产品', + '营销' ], + true + ); + +INSERT INTO + limq.links ( + link_id, + original_url, + created_at, + created_by, + title, + description, + tags, + is_active + ) +VALUES + ( + '22222222-2222-2222-2222-222222222222', + 'https://example.com/promo', + now(), + 'user-1', + '促销活动', + '夏季特别促销活动', + [ '促销', + '活动' ], + true + ); + +INSERT INTO + limq.links ( + link_id, + original_url, + created_at, + created_by, + title, + description, + tags, + is_active + ) +VALUES + ( + '33333333-3333-3333-3333-333333333333', + 'https://example.com/blog', + now(), + 'user-2', + '公司博客', + '公司新闻和更新', + [ '博客', + '内容' ], + true + ); + +INSERT INTO + limq.links ( + link_id, + original_url, + created_at, + created_by, + title, + description, + tags, + is_active + ) +VALUES + ( + '44444444-4444-4444-4444-444444444444', + 'https://example.com/signup', + now(), + 'user-2', + '注册页面', + '新用户注册页面', + [ '转化', + '注册' ], + true + ); + +-- 为第一个链接创建500条记录 +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + qr_code_id, + conversion_type, + conversion_value, + custom_data + ) +SELECT + generateUUIDv4() AS event_id, + subtractDays(now(), rand() % 30) AS event_time, + toDate(event_time) AS date, + '11111111-1111-1111-1111-111111111111' AS link_id, + 'channel-1' AS channel_id, + concat('visitor-', toString(rand() % 100 + 1)) AS visitor_id, + concat('session-', toString(number % 50 + 1)) AS session_id, + multiIf( + rand() % 100 < 70, + 'click', + rand() % 100 < 90, + 'redirect', + rand() % 100 < 98, + 'conversion', + 'error' + ) AS event_type, + concat('192.168.1.', toString(rand() % 255)) AS ip_address, + multiIf( + rand() % 100 < 60, + 'China', + rand() % 100 < 85, + 'US', + rand() % 100 < 95, + 'Japan', + 'Other' + ) AS country, + multiIf( + rand() % 100 < 60, + 'Beijing', + rand() % 100 < 85, + 'New York', + rand() % 100 < 95, + 'Tokyo', + 'Other' + ) AS city, + multiIf( + rand() % 100 < 30, + 'https://google.com', + rand() % 100 < 50, + 'https://facebook.com', + rand() % 100 < 65, + 'https://twitter.com', + rand() % 100 < 75, + 'https://instagram.com', + rand() % 100 < 85, + 'https://linkedin.com', + rand() % 100 < 90, + 'https://bing.com', + rand() % 100 < 95, + 'https://baidu.com', + 'direct' + ) AS referrer, + multiIf( + rand() % 100 < 40, + 'google', + rand() % 100 < 70, + 'facebook', + rand() % 100 < 90, + 'email', + 'direct' + ) AS utm_source, + multiIf( + rand() % 100 < 40, + 'cpc', + rand() % 100 < 70, + 'social', + rand() % 100 < 90, + 'email', + 'direct' + ) AS utm_medium, + multiIf( + rand() % 100 < 40, + 'summer_sale', + rand() % 100 < 70, + 'product_launch', + rand() % 100 < 90, + 'newsletter', + 'brand' + ) AS utm_campaign, + 'Mozilla/5.0' AS user_agent, + multiIf( + rand() % 100 < 60, + 'mobile', + rand() % 100 < 85, + 'desktop', + rand() % 100 < 95, + 'tablet', + 'other' + ) AS device_type, + multiIf( + rand() % 100 < 50, + 'Chrome', + rand() % 100 < 80, + 'Safari', + rand() % 100 < 95, + 'Firefox', + 'Edge' + ) AS browser, + multiIf( + rand() % 100 < 50, + 'iOS', + rand() % 100 < 90, + 'Android', + 'Windows' + ) AS os, + rand() % 300 AS time_spent_sec, + rand() % 100 < 25 AS is_bounce, + rand() % 100 < 20 AS is_qr_scan, + concat('qr-', toString(rand() % 10 + 1)) AS qr_code_id, + multiIf( + rand() % 100 < 50, + 'visit', + rand() % 100 < 70, + 'stay', + rand() % 100 < 85, + 'interact', + rand() % 100 < 93, + 'signup', + rand() % 100 < 97, + 'subscription', + 'purchase' + ) AS conversion_type, + rand() % 100 * 1.5 AS conversion_value, + '{}' AS custom_data +FROM + numbers(500); + +-- 为第二个链接创建300条记录 +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + qr_code_id, + conversion_type, + conversion_value, + custom_data + ) +SELECT + generateUUIDv4() AS event_id, + subtractDays(now(), rand() % 30) AS event_time, + toDate(event_time) AS date, + '22222222-2222-2222-2222-222222222222' AS link_id, + 'channel-1' AS channel_id, + concat('visitor-', toString(rand() % 100 + 1)) AS visitor_id, + concat('session-', toString(number % 40 + 1)) AS session_id, + multiIf( + rand() % 100 < 70, + 'click', + rand() % 100 < 90, + 'redirect', + rand() % 100 < 98, + 'conversion', + 'error' + ) AS event_type, + concat('192.168.1.', toString(rand() % 255)) AS ip_address, + multiIf( + rand() % 100 < 60, + 'China', + rand() % 100 < 85, + 'US', + rand() % 100 < 95, + 'Japan', + 'Other' + ) AS country, + multiIf( + rand() % 100 < 60, + 'Beijing', + rand() % 100 < 85, + 'New York', + rand() % 100 < 95, + 'Tokyo', + 'Other' + ) AS city, + multiIf( + rand() % 100 < 30, + 'https://google.com', + rand() % 100 < 50, + 'https://facebook.com', + rand() % 100 < 65, + 'https://twitter.com', + rand() % 100 < 75, + 'https://instagram.com', + rand() % 100 < 85, + 'https://linkedin.com', + rand() % 100 < 90, + 'https://bing.com', + rand() % 100 < 95, + 'https://baidu.com', + 'direct' + ) AS referrer, + multiIf( + rand() % 100 < 40, + 'google', + rand() % 100 < 70, + 'facebook', + rand() % 100 < 90, + 'email', + 'direct' + ) AS utm_source, + multiIf( + rand() % 100 < 40, + 'cpc', + rand() % 100 < 70, + 'social', + rand() % 100 < 90, + 'email', + 'direct' + ) AS utm_medium, + multiIf( + rand() % 100 < 40, + 'summer_sale', + rand() % 100 < 70, + 'product_launch', + rand() % 100 < 90, + 'newsletter', + 'brand' + ) AS utm_campaign, + 'Mozilla/5.0' AS user_agent, + multiIf( + rand() % 100 < 60, + 'mobile', + rand() % 100 < 85, + 'desktop', + rand() % 100 < 95, + 'tablet', + 'other' + ) AS device_type, + multiIf( + rand() % 100 < 50, + 'Chrome', + rand() % 100 < 80, + 'Safari', + rand() % 100 < 95, + 'Firefox', + 'Edge' + ) AS browser, + multiIf( + rand() % 100 < 50, + 'iOS', + rand() % 100 < 90, + 'Android', + 'Windows' + ) AS os, + rand() % 300 AS time_spent_sec, + rand() % 100 < 25 AS is_bounce, + rand() % 100 < 15 AS is_qr_scan, + concat('qr-', toString(rand() % 10 + 1)) AS qr_code_id, + multiIf( + rand() % 100 < 50, + 'visit', + rand() % 100 < 70, + 'stay', + rand() % 100 < 85, + 'interact', + rand() % 100 < 93, + 'signup', + rand() % 100 < 97, + 'subscription', + 'purchase' + ) AS conversion_type, + rand() % 100 * 2.5 AS conversion_value, + '{}' AS custom_data +FROM + numbers(300); + +-- 为第三个链接创建200条记录 +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + qr_code_id, + conversion_type, + conversion_value, + custom_data + ) +SELECT + generateUUIDv4() AS event_id, + subtractDays(now(), rand() % 30) AS event_time, + toDate(event_time) AS date, + '33333333-3333-3333-3333-333333333333' AS link_id, + 'channel-2' AS channel_id, + concat('visitor-', toString(rand() % 100 + 1)) AS visitor_id, + concat('session-', toString(number % 30 + 1)) AS session_id, + multiIf( + rand() % 100 < 70, + 'click', + rand() % 100 < 90, + 'redirect', + rand() % 100 < 98, + 'conversion', + 'error' + ) AS event_type, + concat('192.168.1.', toString(rand() % 255)) AS ip_address, + multiIf( + rand() % 100 < 60, + 'China', + rand() % 100 < 85, + 'US', + rand() % 100 < 95, + 'Japan', + 'Other' + ) AS country, + multiIf( + rand() % 100 < 60, + 'Beijing', + rand() % 100 < 85, + 'New York', + rand() % 100 < 95, + 'Tokyo', + 'Other' + ) AS city, + multiIf( + rand() % 100 < 30, + 'https://google.com', + rand() % 100 < 50, + 'https://facebook.com', + rand() % 100 < 65, + 'https://twitter.com', + rand() % 100 < 75, + 'https://instagram.com', + rand() % 100 < 85, + 'https://linkedin.com', + rand() % 100 < 90, + 'https://bing.com', + rand() % 100 < 95, + 'https://baidu.com', + 'direct' + ) AS referrer, + multiIf( + rand() % 100 < 40, + 'google', + rand() % 100 < 70, + 'facebook', + rand() % 100 < 90, + 'email', + 'direct' + ) AS utm_source, + multiIf( + rand() % 100 < 40, + 'cpc', + rand() % 100 < 70, + 'social', + rand() % 100 < 90, + 'email', + 'direct' + ) AS utm_medium, + multiIf( + rand() % 100 < 40, + 'summer_sale', + rand() % 100 < 70, + 'product_launch', + rand() % 100 < 90, + 'newsletter', + 'brand' + ) AS utm_campaign, + 'Mozilla/5.0' AS user_agent, + multiIf( + rand() % 100 < 60, + 'mobile', + rand() % 100 < 85, + 'desktop', + rand() % 100 < 95, + 'tablet', + 'other' + ) AS device_type, + multiIf( + rand() % 100 < 50, + 'Chrome', + rand() % 100 < 80, + 'Safari', + rand() % 100 < 95, + 'Firefox', + 'Edge' + ) AS browser, + multiIf( + rand() % 100 < 50, + 'iOS', + rand() % 100 < 90, + 'Android', + 'Windows' + ) AS os, + rand() % 600 AS time_spent_sec, + rand() % 100 < 15 AS is_bounce, + rand() % 100 < 10 AS is_qr_scan, + concat('qr-', toString(rand() % 10 + 1)) AS qr_code_id, + multiIf( + rand() % 100 < 50, + 'visit', + rand() % 100 < 70, + 'stay', + rand() % 100 < 85, + 'interact', + rand() % 100 < 93, + 'signup', + rand() % 100 < 97, + 'subscription', + 'purchase' + ) AS conversion_type, + rand() % 100 * 1.2 AS conversion_value, + '{}' AS custom_data +FROM + numbers(200); + +-- 为第四个链接创建400条记录 +INSERT INTO + limq.link_events ( + event_id, + event_time, + date, + link_id, + channel_id, + visitor_id, + session_id, + event_type, + ip_address, + country, + city, + referrer, + utm_source, + utm_medium, + utm_campaign, + user_agent, + device_type, + browser, + os, + time_spent_sec, + is_bounce, + is_qr_scan, + qr_code_id, + conversion_type, + conversion_value, + custom_data + ) +SELECT + generateUUIDv4() AS event_id, + subtractDays(now(), rand() % 30) AS event_time, + toDate(event_time) AS date, + '44444444-4444-4444-4444-444444444444' AS link_id, + 'channel-2' AS channel_id, + concat('visitor-', toString(rand() % 100 + 1)) AS visitor_id, + concat('session-', toString(number % 60 + 1)) AS session_id, + multiIf( + rand() % 100 < 70, + 'click', + rand() % 100 < 90, + 'redirect', + rand() % 100 < 98, + 'conversion', + 'error' + ) AS event_type, + concat('192.168.1.', toString(rand() % 255)) AS ip_address, + multiIf( + rand() % 100 < 60, + 'China', + rand() % 100 < 85, + 'US', + rand() % 100 < 95, + 'Japan', + 'Other' + ) AS country, + multiIf( + rand() % 100 < 60, + 'Beijing', + rand() % 100 < 85, + 'New York', + rand() % 100 < 95, + 'Tokyo', + 'Other' + ) AS city, + multiIf( + rand() % 100 < 30, + 'https://google.com', + rand() % 100 < 50, + 'https://facebook.com', + rand() % 100 < 65, + 'https://twitter.com', + rand() % 100 < 75, + 'https://instagram.com', + rand() % 100 < 85, + 'https://linkedin.com', + rand() % 100 < 90, + 'https://bing.com', + rand() % 100 < 95, + 'https://baidu.com', + 'direct' + ) AS referrer, + multiIf( + rand() % 100 < 40, + 'google', + rand() % 100 < 70, + 'facebook', + rand() % 100 < 90, + 'email', + 'direct' + ) AS utm_source, + multiIf( + rand() % 100 < 40, + 'cpc', + rand() % 100 < 70, + 'social', + rand() % 100 < 90, + 'email', + 'direct' + ) AS utm_medium, + multiIf( + rand() % 100 < 40, + 'summer_sale', + rand() % 100 < 70, + 'product_launch', + rand() % 100 < 90, + 'newsletter', + 'brand' + ) AS utm_campaign, + 'Mozilla/5.0' AS user_agent, + multiIf( + rand() % 100 < 60, + 'mobile', + rand() % 100 < 85, + 'desktop', + rand() % 100 < 95, + 'tablet', + 'other' + ) AS device_type, + multiIf( + rand() % 100 < 50, + 'Chrome', + rand() % 100 < 80, + 'Safari', + rand() % 100 < 95, + 'Firefox', + 'Edge' + ) AS browser, + multiIf( + rand() % 100 < 50, + 'iOS', + rand() % 100 < 90, + 'Android', + 'Windows' + ) AS os, + rand() % 400 AS time_spent_sec, + rand() % 100 < 20 AS is_bounce, + rand() % 100 < 25 AS is_qr_scan, + concat('qr-', toString(rand() % 10 + 1)) AS qr_code_id, + multiIf( + rand() % 100 < 50, + 'visit', + rand() % 100 < 70, + 'stay', + rand() % 100 < 85, + 'interact', + rand() % 100 < 93, + 'signup', + rand() % 100 < 97, + 'subscription', + 'purchase' + ) AS conversion_type, + rand() % 100 * 3.5 AS conversion_value, + '{}' AS custom_data +FROM + numbers(400); + +-- 插入link_daily_stats表数据 +INSERT INTO + limq.link_daily_stats ( + date, + link_id, + total_clicks, + unique_visitors, + unique_sessions, + total_time_spent, + avg_time_spent, + bounce_count, + conversion_count, + unique_referrers, + mobile_count, + tablet_count, + desktop_count, + qr_scan_count, + total_conversion_value + ) +SELECT + subtractDays(today(), number) AS date, + multiIf( + number % 4 = 0, + '11111111-1111-1111-1111-111111111111', + number % 4 = 1, + '22222222-2222-2222-2222-222222222222', + number % 4 = 2, + '33333333-3333-3333-3333-333333333333', + '44444444-4444-4444-4444-444444444444' + ) AS link_id, + 50 + rand() % 100 AS total_clicks, + 30 + rand() % 50 AS unique_visitors, + 20 + rand() % 40 AS unique_sessions, + (500 + rand() % 1000) * 60 AS total_time_spent, + (rand() % 10) * 60 + rand() % 60 AS avg_time_spent, + 5 + rand() % 20 AS bounce_count, + rand() % 30 AS conversion_count, + 3 + rand() % 8 AS unique_referrers, + 20 + rand() % 40 AS mobile_count, + 5 + rand() % 15 AS tablet_count, + 15 + rand() % 30 AS desktop_count, + rand() % 10 AS qr_scan_count, + rand() % 1000 * 2.5 AS total_conversion_value +FROM + numbers(30) +WHERE + number < 30; + +-- 插入link_hourly_patterns表数据 +INSERT INTO + limq.link_hourly_patterns (date, hour, link_id, visits, unique_visitors) +SELECT + subtractDays(today(), number % 7) AS date, + number % 24 AS hour, + multiIf( + intDiv(number, 24) % 4 = 0, + '11111111-1111-1111-1111-111111111111', + intDiv(number, 24) % 4 = 1, + '22222222-2222-2222-2222-222222222222', + intDiv(number, 24) % 4 = 2, + '33333333-3333-3333-3333-333333333333', + '44444444-4444-4444-4444-444444444444' + ) AS link_id, + 5 + rand() % 20 AS visits, + 3 + rand() % 10 AS unique_visitors +FROM + numbers(672) -- 7天 x 24小时 x 4个链接 +WHERE + number < 672; + +-- 显示数据行数,验证插入成功 +SELECT + 'link_events 表行数:' AS metric, + count() AS value +FROM + limq.link_events +UNION +ALL +SELECT + 'link_daily_stats 表行数:', + count() +FROM + limq.link_daily_stats +UNION +ALL +SELECT + 'link_hourly_patterns 表行数:', + count() +FROM + limq.link_hourly_patterns; \ No newline at end of file diff --git a/scripts/db/sql/postgres/pg-query.js b/scripts/db/sql/postgres/pg-query.js new file mode 100755 index 0000000..439a683 --- /dev/null +++ b/scripts/db/sql/postgres/pg-query.js @@ -0,0 +1,331 @@ +#!/usr/bin/env node + +// # 显示所有表 +// ./pg-query.js -t + +// # 显示表结构 +// ./pg-query.js -d influencers + +// # 显示样本数据,限制5行 +// ./pg-query.js -s posts -l 5 + +// # 查看表记录数 +// ./pg-query.js -c posts + +// # 显示索引 +// ./pg-query.js -i posts + +// # 显示外键 +// ./pg-query.js -f posts + +// # 显示引用 +// ./pg-query.js -r influencers + +// # 执行自定义查询 +// ./pg-query.js -q "SELECT * FROM influencers WHERE platform = 'Instagram' LIMIT 5" + +// # 执行SQL文件 +// ./pg-query.js -e schema.sql + +const { Client } = require('pg'); +const path = require('path'); +const fs = require('fs'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); + +// 加载.env文件 - 使用正确的相对路径 +require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') }); + +// 显示连接信息(不含密码)以便调试 +function getConnectionString() { + // 使用.env中的DATABASE_URL + const databaseUrl = process.env.DATABASE_URL; + + if (!databaseUrl) { + console.error('错误: 未找到DATABASE_URL环境变量'); + process.exit(1); + } + + // 显示连接信息但隐藏密码 + const sanitizedUrl = databaseUrl.replace(/:[^:@]+@/, ':***@'); + console.log(`使用连接: ${sanitizedUrl}`); + + return databaseUrl; +} + +// 创建一个新的客户端 +async function runQuery(query, params = []) { + const client = new Client({ + connectionString: getConnectionString() + }); + + try { + await client.connect(); + console.log('数据库连接成功'); + const result = await client.query(query, params); + return result.rows; + } catch (err) { + console.error('查询执行错误:', err.message); + return null; + } finally { + await client.end(); + } +} + +// 显示所有表 +async function showTables() { + const query = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;"; + const tables = await runQuery(query); + + if (tables && tables.length > 0) { + console.log('数据库中的表:'); + console.table(tables); + } else { + console.log('没有找到表或连接失败'); + } +} + +// 显示表结构 +async function showTableStructure(tableName) { + const query = ` + SELECT + column_name AS "列名", + data_type AS "数据类型", + CASE WHEN is_nullable = 'YES' THEN '允许为空' ELSE '不允许为空' END AS "是否可空", + column_default AS "默认值", + character_maximum_length AS "最大长度" + FROM + information_schema.columns + WHERE + table_schema = 'public' AND + table_name = $1 + ORDER BY + ordinal_position; + `; + + const columns = await runQuery(query, [tableName]); + + if (columns && columns.length > 0) { + console.log(`表 ${tableName} 的结构:`); + console.table(columns); + } else { + console.log(`表 ${tableName} 不存在或连接失败`); + } +} + +// 显示样本数据 +async function showSampleData(tableName, limit = 10) { + const query = `SELECT * FROM "${tableName}" LIMIT ${limit};`; + const data = await runQuery(query); + + if (data && data.length > 0) { + console.log(`表 ${tableName} 的样本数据 (${limit} 行):`); + console.table(data); + } else { + console.log(`表 ${tableName} 为空或不存在`); + } +} + +// 显示记录计数 +async function showRecordCount(tableName) { + const query = `SELECT COUNT(*) AS "记录数" FROM "${tableName}";`; + const count = await runQuery(query); + + if (count) { + console.log(`表 ${tableName} 的记录数:`); + console.table(count); + } else { + console.log(`表 ${tableName} 不存在或连接失败`); + } +} + +// 显示索引信息 +async function showIndexes(tableName) { + const query = ` + SELECT + indexname AS "索引名称", + indexdef AS "索引定义" + FROM + pg_indexes + WHERE + tablename = $1 + ORDER BY + indexname; + `; + + const indexes = await runQuery(query, [tableName]); + + if (indexes && indexes.length > 0) { + console.log(`表 ${tableName} 的索引:`); + console.table(indexes); + } else { + console.log(`表 ${tableName} 没有索引或不存在`); + } +} + +// 显示外键 +async function showForeignKeys(tableName) { + const query = ` + SELECT + conname AS "外键名称", + pg_get_constraintdef(oid) AS "外键定义" + FROM + pg_constraint + WHERE + conrelid = $1::regclass AND contype = 'f'; + `; + + const foreignKeys = await runQuery(query, [tableName]); + + if (foreignKeys && foreignKeys.length > 0) { + console.log(`表 ${tableName} 的外键:`); + console.table(foreignKeys); + } else { + console.log(`表 ${tableName} 没有外键或不存在`); + } +} + +// 显示引用当前表的外键 +async function showReferencingKeys(tableName) { + const query = ` + SELECT + c.conname AS "外键名称", + t.relname AS "引用表", + pg_get_constraintdef(c.oid) AS "外键定义" + FROM + pg_constraint c + JOIN + pg_class t ON c.conrelid = t.oid + WHERE + c.confrelid = $1::regclass AND c.contype = 'f'; + `; + + const referencingKeys = await runQuery(query, [tableName]); + + if (referencingKeys && referencingKeys.length > 0) { + console.log(`引用表 ${tableName} 的外键关系:`); + console.table(referencingKeys); + } else { + console.log(`没有找到引用表 ${tableName} 的外键关系`); + } +} + +// 执行自定义查询 +async function executeQuery(query) { + const result = await runQuery(query); + + if (result) { + console.log('查询结果:'); + console.table(result); + } else { + console.log('查询执行失败或无结果'); + } +} + +// 执行SQL文件 +async function executeSqlFile(filename) { + try { + const sql = fs.readFileSync(filename, 'utf8'); + console.log(`执行SQL文件: ${filename}`); + await executeQuery(sql); + } catch (err) { + console.error(`执行SQL文件失败: ${err.message}`); + } +} + +// 主函数 +async function main() { + try { + const argv = yargs(hideBin(process.argv)) + .usage('PostgreSQL 查询工具\n\n用法: $0 [选项]') + .option('t', { + alias: 'tables', + describe: '显示所有表', + type: 'boolean' + }) + .option('d', { + alias: 'describe', + describe: '显示表结构', + type: 'string' + }) + .option('s', { + alias: 'sample', + describe: '显示表样本数据', + type: 'string' + }) + .option('l', { + alias: 'limit', + describe: '样本数据行数限制', + type: 'number', + default: 10 + }) + .option('c', { + alias: 'count', + describe: '计算表中的记录数', + type: 'string' + }) + .option('i', { + alias: 'indexes', + describe: '显示表索引', + type: 'string' + }) + .option('f', { + alias: 'foreign-keys', + describe: '显示表外键关系', + type: 'string' + }) + .option('r', { + alias: 'references', + describe: '显示引用此表的外键', + type: 'string' + }) + .option('q', { + alias: 'query', + describe: '执行自定义SQL查询', + type: 'string' + }) + .option('e', { + alias: 'execute-file', + describe: '执行SQL文件', + type: 'string' + }) + .example('$0 -t', '显示所有表') + .example('$0 -d influencers', '显示influencers表结构') + .example('$0 -s posts -l 5', '显示posts表前5行数据') + .epilog('更多信息请访问项目文档') + .help() + .alias('h', 'help') + .argv; + + if (argv.tables) { + await showTables(); + } else if (argv.describe) { + await showTableStructure(argv.describe); + } else if (argv.sample) { + await showSampleData(argv.sample, argv.limit); + } else if (argv.count) { + await showRecordCount(argv.count); + } else if (argv.indexes) { + await showIndexes(argv.indexes); + } else if (argv.foreignKeys) { + await showForeignKeys(argv.foreignKeys); + } else if (argv.references) { + await showReferencingKeys(argv.references); + } else if (argv.query) { + await executeQuery(argv.query); + } else if (argv.executeFile) { + await executeSqlFile(argv.executeFile); + } else { + yargs(hideBin(process.argv)).showHelp(); + } + } catch (err) { + console.error('程序执行错误:', err.message); + process.exit(1); + } +} + +// 执行主函数 +main().catch(err => { + console.error('程序执行错误:', err); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..0b5ff82 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +set -e +set -x + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${YELLOW}开始部署流程...${NC}" + +# 首先加载环境变量 +if [ "$NODE_ENV" = "production" ]; then + echo -e "${GREEN}加载生产环境配置...${NC}" + set -a + source .env.production + set +a +else + echo -e "${GREEN}加载开发环境配置...${NC}" + set -a + source .env.development + set +a +fi + +# 安装依赖 +echo -e "${GREEN}安装依赖...${NC}" +NODE_ENV= pnpm install --ignore-workspace + +# 生成 Prisma 客户端 +echo -e "${GREEN}生成 Prisma 客户端...${NC}" +npx prisma generate + +# 类型检查 +echo -e "${GREEN}运行类型检查...${NC}" +pnpm tsc --noEmit + +# 询问是否同步数据库架构 +echo -e "${YELLOW}是否需要同步数据库架构? (y/n)${NC}" +read -r sync_db +if [ "$sync_db" = "y" ] || [ "$sync_db" = "Y" ]; then + echo -e "${GREEN}开始同步数据库架构...${NC}" + if [ "$NODE_ENV" = "production" ]; then + npx prisma db push + else + npx prisma db push + fi +else + echo -e "${YELLOW}跳过数据库同步${NC}" +fi + +# 构建项目 +echo -e "${GREEN}构建项目...${NC}" +pnpm build + +# 检查并安装 PM2 +echo -e "${GREEN}检查 PM2...${NC}" +if ! command -v pm2 &> /dev/null; then + echo -e "${YELLOW}PM2 未安装,正在安装 5.4.3 版本...${NC}" + pnpm add pm2@5.4.3 -g +else + PM2_VERSION=$(pm2 -v) + if [ "$PM2_VERSION" != "5.4.3" ]; then + echo -e "${YELLOW}错误: PM2 版本必须是 5.4.3,当前版本是 ${PM2_VERSION}${NC}" + echo -e "${YELLOW}请运行以下命令更新 PM2:${NC}" + echo -e "${YELLOW}pm2 kill && pnpm remove pm2 -g && rm -rf ~/.pm2 && pnpm add pm2@5.4.3 -g${NC}" + exit 1 + else + echo -e "${GREEN}PM2 5.4.3 已安装${NC}" + fi +fi + +# 启动服务 +if [ "$NODE_ENV" = "production" ]; then + echo -e "${GREEN}以生产模式启动服务...${NC}" + pm2 start dist/src/main.js --name limq +else + echo -e "${GREEN}以开发模式启动服务...${NC}" + pm2 start dist/src/main.js --name limq-dev --watch +fi + +echo -e "${GREEN}部署完成!${NC}" \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..c43c707 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,42 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + darkMode: 'class', + theme: { + extend: { + colors: { + background: "var(--background)", + foreground: "var(--foreground)", + "card-bg": "var(--card-bg)", + "card-border": "var(--card-border)", + "accent-blue": "var(--accent-blue)", + "accent-green": "var(--accent-green)", + "accent-red": "var(--accent-red)", + "accent-yellow": "var(--accent-yellow)", + "accent-purple": "var(--accent-purple)", + "accent-pink": "var(--accent-pink)", + "accent-teal": "var(--accent-teal)", + "accent-orange": "var(--accent-orange)", + "text-secondary": "var(--text-secondary)", + "progress-bg": "var(--progress-bg)", + }, + borderColor: { + DEFAULT: "var(--card-border)", + }, + backgroundImage: { + "gradient-blue": "var(--gradient-blue)", + "gradient-purple": "var(--gradient-purple)", + "gradient-green": "var(--gradient-green)", + "gradient-red": "var(--gradient-red)", + }, + }, + }, + plugins: [], +}; + +export default config; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d8b9323 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}