Files
wish-pool/app/analytics/page.tsx
2025-07-18 13:07:28 +08:00

691 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect } from "react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import {
Sparkles,
ArrowLeft,
BarChart3,
TrendingUp,
Users,
Target,
BookOpen,
ChevronDown,
ChevronUp,
TrendingDown,
Minus,
Shield,
Eye,
EyeOff,
} from "lucide-react"
import RadarChart from "@/components/radar-chart"
import HeaderMusicControl from "@/components/header-music-control"
import { categories, categorizeWishMultiple, type Wish } from "@/lib/categorization"
interface CategoryData {
name: string
count: number
percentage: number
color: string
keywords: string[]
description?: string
difficulty?: {
level: number
stars: string
label: string
estimatedTime: string
techStack: string[]
solutionType: string
complexity: string
}
}
interface AnalyticsData {
totalWishes: number
publicWishes: number
privateWishes: number
categories: CategoryData[]
recentTrends: {
thisWeek: number
lastWeek: number
growth: number
growthLabel: string
growthIcon: "up" | "down" | "flat"
growthColor: string
}
topKeywords: { word: string; count: number }[]
}
export default function AnalyticsPage() {
const [wishes, setWishes] = useState<Wish[]>([])
const [analytics, setAnalytics] = useState<AnalyticsData | null>(null)
const [showCategoryGuide, setShowCategoryGuide] = useState(false)
// 分析許願內容(包含所有數據,包括私密的)
const analyzeWishes = (wishList: (Wish & { isPublic?: boolean })[]): AnalyticsData => {
const totalWishes = wishList.length
const publicWishes = wishList.filter((wish) => wish.isPublic !== false).length
const privateWishes = wishList.filter((wish) => wish.isPublic === false).length
const categoryStats: { [key: string]: number } = {}
const keywordCount: { [key: string]: number } = {}
// 初始化分類統計
categories.forEach((cat) => {
categoryStats[cat.name] = 0
})
categoryStats["其他問題"] = 0
// 分析每個許願(多標籤統計)- 包含所有數據
wishList.forEach((wish) => {
const wishCategories = categorizeWishMultiple(wish)
wishCategories.forEach((category) => {
categoryStats[category.name]++
// 統計關鍵字
if (category.keywords) {
const fullText =
`${wish.title} ${wish.currentPain} ${wish.expectedSolution} ${wish.expectedEffect}`.toLowerCase()
category.keywords.forEach((keyword: string) => {
if (fullText.includes(keyword.toLowerCase())) {
keywordCount[keyword] = (keywordCount[keyword] || 0) + 1
}
})
}
})
})
// 計算百分比和準備數據
const categoriesData: CategoryData[] = categories.map((cat) => ({
name: cat.name,
count: categoryStats[cat.name] || 0,
percentage: totalWishes > 0 ? Math.round(((categoryStats[cat.name] || 0) / totalWishes) * 100) : 0,
color: cat.color,
keywords: cat.keywords,
description: cat.description,
}))
// 改進的趨勢計算
const now = new Date()
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
const twoWeeksAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000)
const thisWeek = wishList.filter((wish) => new Date(wish.createdAt) >= oneWeekAgo).length
const lastWeek = wishList.filter((wish) => {
const date = new Date(wish.createdAt)
return date >= twoWeeksAgo && date < oneWeekAgo
}).length
// 改進的成長趨勢計算
let growth = 0
let growthLabel = "持平"
let growthIcon: "up" | "down" | "flat" = "flat"
let growthColor = "#6B7280"
if (lastWeek === 0 && thisWeek > 0) {
// 上週沒有,本週有 → 全新開始
growth = 100
growthLabel = "開始增長"
growthIcon = "up"
growthColor = "#10B981"
} else if (lastWeek === 0 && thisWeek === 0) {
// 兩週都沒有
growth = 0
growthLabel = "尚無數據"
growthIcon = "flat"
growthColor = "#6B7280"
} else if (lastWeek > 0) {
// 正常計算成長率
growth = Math.round(((thisWeek - lastWeek) / lastWeek) * 100)
if (growth > 0) {
growthLabel = "持續增長"
growthIcon = "up"
growthColor = "#10B981"
} else if (growth < 0) {
growthLabel = "有所下降"
growthIcon = "down"
growthColor = "#EF4444"
} else {
growthLabel = "保持穩定"
growthIcon = "flat"
growthColor = "#6B7280"
}
}
// 取得熱門關鍵字
const topKeywords = Object.entries(keywordCount)
.sort(([, a], [, b]) => b - a)
.slice(0, 15)
.map(([word, count]) => ({ word, count }))
return {
totalWishes,
publicWishes,
privateWishes,
categories: categoriesData,
recentTrends: {
thisWeek,
lastWeek,
growth,
growthLabel,
growthIcon,
growthColor,
},
topKeywords,
}
}
useEffect(() => {
const savedWishes = JSON.parse(localStorage.getItem("wishes") || "[]")
setWishes(savedWishes)
setAnalytics(analyzeWishes(savedWishes))
}, [])
if (!analytics) {
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-blue-900 to-indigo-900 flex items-center justify-center">
<div className="text-white text-xl">...</div>
</div>
)
}
// 根據成長趨勢選擇圖標
const GrowthIcon =
analytics.recentTrends.growthIcon === "up"
? TrendingUp
: analytics.recentTrends.growthIcon === "down"
? TrendingDown
: Minus
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-blue-900 to-indigo-900 relative overflow-hidden">
{/* 星空背景 */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{[...Array(30)].map((_, i) => (
<div
key={i}
className="absolute w-0.5 h-0.5 md:w-1 md:h-1 bg-white rounded-full animate-pulse"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 3}s`,
animationDuration: `${2 + Math.random() * 2}s`,
}}
/>
))}
<div className="absolute top-1/4 right-1/3 w-64 h-64 md:w-96 md:h-96 bg-gradient-radial from-purple-400/20 via-blue-500/10 to-transparent rounded-full blur-3xl"></div>
</div>
{/* Header - 修復跑版問題 */}
<header className="border-b border-blue-800/50 bg-slate-900/80 backdrop-blur-sm sticky top-0 z-50">
<div className="container mx-auto px-3 sm:px-4 py-3 md:py-4">
<div className="flex items-center justify-between gap-2">
{/* Logo 區域 - 防止文字換行 */}
<Link href="/" className="flex items-center gap-2 md:gap-3 min-w-0 flex-shrink-0">
<div className="w-7 h-7 sm:w-8 sm:h-8 md:w-10 md:h-10 bg-gradient-to-br from-cyan-400 to-blue-500 rounded-full flex items-center justify-center shadow-lg shadow-cyan-500/25">
<Sparkles className="w-3.5 h-3.5 sm:w-4 sm:h-4 md:w-6 md:h-6 text-white" />
</div>
<h1 className="text-base sm:text-lg md:text-2xl font-bold text-white whitespace-nowrap"></h1>
</Link>
{/* 導航區域 */}
<nav className="flex items-center gap-1 sm:gap-2 md:gap-4 flex-shrink-0">
{/* 音樂控制 */}
<div className="hidden sm:block">
<HeaderMusicControl />
</div>
<div className="sm:hidden">
<HeaderMusicControl mobileSimplified />
</div>
{/* 桌面版完整導航 */}
<div className="hidden md:flex items-center gap-4">
<Link href="/wishes">
<Button
variant="outline"
size="sm"
className="border-blue-400 bg-slate-800/50 text-blue-100 hover:bg-slate-700/50 px-4"
>
</Button>
</Link>
<Link href="/submit">
<Button
size="sm"
className="bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-600 hover:to-blue-700 text-white shadow-lg shadow-cyan-500/25 px-4"
>
</Button>
</Link>
</div>
{/* 平板版導航 */}
<div className="hidden sm:flex md:hidden items-center gap-1">
<Link href="/">
<Button
variant="ghost"
size="sm"
className="text-blue-200 hover:text-white hover:bg-blue-800/50 px-2"
>
<ArrowLeft className="w-4 h-4" />
</Button>
</Link>
<Link href="/wishes">
<Button
variant="outline"
size="sm"
className="border-blue-400 bg-slate-800/50 text-blue-100 hover:bg-slate-700/50 px-2 text-xs"
>
</Button>
</Link>
<Link href="/submit">
<Button
size="sm"
className="bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-600 hover:to-blue-700 text-white shadow-lg shadow-cyan-500/25 px-3"
>
</Button>
</Link>
</div>
{/* 手機版導航 - 移除首頁按鈕,避免與 logo 功能重疊 */}
<div className="flex sm:hidden items-center gap-1">
<Link href="/wishes">
<Button
variant="outline"
size="sm"
className="border-blue-400 bg-slate-800/50 text-blue-100 hover:bg-slate-700/50 px-2 text-xs"
>
</Button>
</Link>
<Link href="/submit">
<Button
size="sm"
className="bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-600 hover:to-blue-700 text-white shadow-lg shadow-cyan-500/25 px-3 text-xs"
>
</Button>
</Link>
</div>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="py-8 md:py-12 px-4">
<div className="container mx-auto max-w-7xl">
{/* 頁面標題 */}
<div className="text-center mb-8 md:mb-12">
<div className="flex items-center justify-center gap-3 mb-4">
<div className="w-12 h-12 bg-gradient-to-br from-purple-400 to-indigo-500 rounded-full flex items-center justify-center shadow-lg shadow-purple-500/25">
<BarChart3 className="w-6 h-6 md:w-8 md:h-8 text-white" />
</div>
<h2 className="text-3xl md:text-4xl font-bold text-white"></h2>
<Badge className="bg-gradient-to-r from-pink-500/20 to-purple-500/20 text-pink-200 border border-pink-400/30 px-3 py-1">
</Badge>
</div>
<p className="text-blue-200 text-lg"></p>
<p className="text-blue-300 text-sm mt-2"></p>
</div>
{/* 隱私說明卡片 */}
<Card className="bg-gradient-to-r from-indigo-800/30 to-purple-800/30 backdrop-blur-sm border border-indigo-600/50 mb-8 md:mb-12">
<CardHeader>
<CardTitle className="text-lg sm:text-xl md:text-2xl text-white flex items-center gap-3">
<div className="w-8 h-8 bg-gradient-to-br from-indigo-400 to-purple-500 rounded-full flex items-center justify-center">
<Shield className="w-4 h-4 text-white" />
</div>
</CardTitle>
<CardDescription className="text-indigo-200 text-sm sm:text-base">
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid sm:grid-cols-2 gap-4 text-sm">
<div className="space-y-2">
<h4 className="font-semibold text-indigo-200 flex items-center gap-2">
<Eye className="w-4 h-4" />
({analytics.publicWishes} )
</h4>
<p className="text-indigo-100"></p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-indigo-200 flex items-center gap-2">
<EyeOff className="w-4 h-4" />
({analytics.privateWishes} )
</h4>
<p className="text-indigo-100"></p>
</div>
</div>
<div className="mt-4 p-3 bg-slate-800/50 rounded-lg border border-slate-600/30">
<p className="text-xs md:text-sm text-slate-300 leading-relaxed">
<strong className="text-blue-200"></strong>
</p>
</div>
</CardContent>
</Card>
{/* 統計概覽 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6 mb-8 md:mb-12">
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50">
<CardContent className="p-4 md:p-6 text-center">
<div className="w-10 h-10 md:w-12 md:h-12 bg-gradient-to-br from-cyan-400 to-blue-500 rounded-full flex items-center justify-center mx-auto mb-2 md:mb-3">
<Users className="w-5 h-5 md:w-6 md:h-6 text-white" />
</div>
<div className="text-2xl md:text-3xl font-bold text-white mb-1">{analytics.totalWishes}</div>
<div className="text-xs md:text-sm text-blue-200"></div>
<div className="text-xs text-slate-400 mt-1">
{analytics.publicWishes} + {analytics.privateWishes}
</div>
</CardContent>
</Card>
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50">
<CardContent className="p-4 md:p-6 text-center">
<div className="w-10 h-10 md:w-12 md:h-12 bg-gradient-to-br from-green-400 to-emerald-500 rounded-full flex items-center justify-center mx-auto mb-2 md:mb-3">
<TrendingUp className="w-5 h-5 md:w-6 md:h-6 text-white" />
</div>
<div className="text-2xl md:text-3xl font-bold text-white mb-1">{analytics.recentTrends.thisWeek}</div>
<div className="text-xs md:text-sm text-blue-200"></div>
</CardContent>
</Card>
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50">
<CardContent className="p-4 md:p-6 text-center">
<div className="w-10 h-10 md:w-12 md:h-12 bg-gradient-to-br from-purple-400 to-indigo-500 rounded-full flex items-center justify-center mx-auto mb-2 md:mb-3">
<Target className="w-5 h-5 md:w-6 md:h-6 text-white" />
</div>
<div className="text-2xl md:text-3xl font-bold text-white mb-1">
{analytics.categories.filter((c) => c.count > 0).length}
</div>
<div className="text-xs md:text-sm text-blue-200"></div>
</CardContent>
</Card>
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50">
<CardContent className="p-4 md:p-6 text-center">
<div
className="w-10 h-10 md:w-12 md:h-12 rounded-full flex items-center justify-center mx-auto mb-2 md:mb-3"
style={{
background: `linear-gradient(135deg, ${analytics.recentTrends.growthColor}80, ${analytics.recentTrends.growthColor}60)`,
}}
>
<GrowthIcon className="w-5 h-5 md:w-6 md:h-6 text-white" />
</div>
<div className="text-2xl md:text-3xl font-bold text-white mb-1">
{analytics.recentTrends.growth > 0 ? "+" : ""}
{analytics.recentTrends.growth}%
</div>
<div className="text-xs md:text-sm" style={{ color: analytics.recentTrends.growthColor }}>
{analytics.recentTrends.growthLabel}
</div>
{/* 詳細說明 */}
<div className="text-xs text-slate-400 mt-1">: {analytics.recentTrends.lastWeek} </div>
</CardContent>
</Card>
</div>
{/* 分類指南 */}
<Card className="bg-gradient-to-r from-indigo-800/30 to-purple-800/30 backdrop-blur-sm border border-indigo-600/50 mb-8 md:mb-12">
<CardHeader>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-gradient-to-br from-indigo-400 to-purple-500 rounded-full flex items-center justify-center flex-shrink-0">
<BookOpen className="w-4 h-4 text-white" />
</div>
<div className="min-w-0 flex-1">
<CardTitle className="text-lg sm:text-xl md:text-2xl text-white"></CardTitle>
<CardDescription className="text-indigo-200 text-sm sm:text-base">
</CardDescription>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setShowCategoryGuide(!showCategoryGuide)}
className="text-indigo-200 hover:text-white hover:bg-indigo-800/50 self-start sm:self-auto flex-shrink-0"
>
{showCategoryGuide ? (
<>
<ChevronUp className="w-4 h-4 mr-1" />
</>
) : (
<>
<ChevronDown className="w-4 h-4 mr-1" />
</>
)}
</Button>
</div>
</CardHeader>
{showCategoryGuide && (
<CardContent>
<div className="grid md:grid-cols-2 gap-4">
{categories.map((category, index) => (
<div
key={category.name}
className="p-4 rounded-lg bg-slate-700/30 border border-slate-600/30 hover:bg-slate-600/40 transition-all duration-200"
>
<div className="flex items-start gap-3 mb-2">
<div className="text-2xl">{category.icon}</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-semibold text-white">{category.name}</h4>
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: category.color }}></div>
</div>
<p className="text-sm text-slate-300 leading-relaxed">{category.description}</p>
</div>
</div>
{/* 關鍵字示例 */}
<div className="mt-3 pt-3 border-t border-slate-600/30">
<div className="text-xs text-slate-400 mb-2"></div>
<div className="flex flex-wrap gap-1">
{category.keywords.slice(0, 6).map((keyword, idx) => (
<Badge
key={idx}
variant="secondary"
className="text-xs px-2 py-0.5 bg-slate-600/50 text-slate-300 border-slate-500/50"
>
{keyword}
</Badge>
))}
{category.keywords.length > 6 && (
<Badge
variant="secondary"
className="text-xs px-2 py-0.5 bg-slate-600/30 text-slate-400 border-slate-500/30"
>
+{category.keywords.length - 6}
</Badge>
)}
</div>
</div>
</div>
))}
</div>
</CardContent>
)}
</Card>
{/* 手機版:垂直佈局,桌面版:並排佈局 */}
<div className="space-y-8 lg:space-y-0 lg:grid lg:grid-cols-2 lg:gap-8 md:gap-12">
{/* 雷達圖 - 手機版給予更多高度 */}
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50">
<CardHeader>
<CardTitle className="text-xl md:text-2xl text-white flex items-center gap-3">
<div className="w-8 h-8 bg-gradient-to-br from-purple-400 to-indigo-500 rounded-full flex items-center justify-center">
<BarChart3 className="w-4 h-4 text-white" />
</div>
</CardTitle>
<CardDescription className="text-blue-200"></CardDescription>
</CardHeader>
<CardContent>
{/* 手機版使用更大的高度,桌面版保持原有高度 */}
<div className="h-80 sm:h-96 lg:h-80 xl:h-96">
<RadarChart data={analytics.categories} />
</div>
</CardContent>
</Card>
{/* 分類詳細統計 */}
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50">
<CardHeader>
<CardTitle className="text-xl md:text-2xl text-white flex items-center gap-3">
<div className="w-8 h-8 bg-gradient-to-br from-cyan-400 to-blue-500 rounded-full flex items-center justify-center">
<Target className="w-4 h-4 text-white" />
</div>
<Badge className="bg-gradient-to-r from-pink-500/20 to-purple-500/20 text-pink-200 border border-pink-400/30 text-xs px-2 py-1">
</Badge>
</CardTitle>
<CardDescription className="text-blue-200">
{analytics.categories.filter((cat) => cat.count > 0).length > 0 && (
<span className="block text-xs text-slate-400 mt-1">
{analytics.categories.filter((cat) => cat.count > 0).length}
{analytics.categories.filter((cat) => cat.count > 0).length > 4 && ",可滾動查看全部"}
</span>
)}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* 設定固定高度並添加滾動 */}
<div className="max-h-80 overflow-y-auto pr-2 space-y-4 scrollbar-thin scrollbar-thumb-slate-600 scrollbar-track-slate-800">
{analytics.categories
.filter((cat) => cat.count > 0)
.sort((a, b) => b.count - a.count)
.map((category, index) => (
<div
key={category.name}
className="flex items-center justify-between p-4 rounded-lg bg-slate-700/30 border border-slate-600/30 hover:bg-slate-600/40 transition-all duration-200"
>
<div className="flex items-center gap-3">
<div className="text-xl">
{categories.find((cat) => cat.name === category.name)?.icon || "❓"}
</div>
<div>
<div className="font-semibold text-white flex items-center gap-2">
{category.name}
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: category.color }}></div>
{/* 添加排名標示 */}
{index < 3 && (
<span className="text-xs bg-gradient-to-r from-cyan-500/20 to-blue-500/20 text-cyan-200 px-2 py-0.5 rounded-full border border-cyan-500/30">
TOP {index + 1}
</span>
)}
</div>
<div className="text-sm text-slate-300">{category.count} </div>
{category.description && (
<div className="text-xs text-slate-400 mt-1 max-w-xs">{category.description}</div>
)}
</div>
</div>
<Badge variant="secondary" className="bg-slate-600/50 text-slate-200">
{category.percentage}%
</Badge>
</div>
))}
</div>
{/* 滾動提示 */}
{analytics.categories.filter((cat) => cat.count > 0).length > 4 && (
<div className="text-center pt-2 border-t border-slate-600/30">
<div className="text-xs text-slate-400 flex items-center justify-center gap-2">
<div className="w-1 h-1 bg-slate-400 rounded-full animate-bounce"></div>
<span></span>
<div
className="w-1 h-1 bg-slate-400 rounded-full animate-bounce"
style={{ animationDelay: "0.5s" }}
></div>
</div>
</div>
)}
</CardContent>
</Card>
</div>
{/* 多維度分析說明 */}
<Card className="bg-gradient-to-r from-purple-800/30 to-indigo-800/30 backdrop-blur-sm border border-purple-600/50 mt-8 md:mt-12">
<CardHeader>
<CardTitle className="text-xl md:text-2xl text-white flex items-center gap-3">
<div className="w-8 h-8 bg-gradient-to-br from-purple-400 to-pink-500 rounded-full flex items-center justify-center">
<Sparkles className="w-4 h-4 text-white" />
</div>
</CardTitle>
<CardDescription className="text-purple-200"></CardDescription>
</CardHeader>
<CardContent>
<div className="grid sm:grid-cols-2 gap-4 text-sm">
<div className="space-y-2">
<h4 className="font-semibold text-purple-200">🔍 </h4>
<p className="text-purple-100"></p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-purple-200">📊 </h4>
<p className="text-purple-100"></p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-purple-200">🎯 </h4>
<p className="text-purple-100"></p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-purple-200">🔒 </h4>
<p className="text-purple-100"></p>
</div>
</div>
</CardContent>
</Card>
{/* 熱門關鍵字 */}
{analytics.topKeywords.length > 0 && (
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50 mt-8 md:mt-12">
<CardHeader>
<CardTitle className="text-xl md:text-2xl text-white flex items-center gap-3">
<div className="w-8 h-8 bg-gradient-to-br from-green-400 to-emerald-500 rounded-full flex items-center justify-center">
<TrendingUp className="w-4 h-4 text-white" />
</div>
</CardTitle>
<CardDescription className="text-blue-200">
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2 sm:gap-3">
{analytics.topKeywords.map((keyword, index) => (
<Badge
key={keyword.word}
variant="secondary"
className="bg-gradient-to-r from-cyan-500/20 to-blue-500/20 text-cyan-200 border border-cyan-500/30 px-3 py-1.5 text-sm"
>
{keyword.word} ({keyword.count})
</Badge>
))}
</div>
</CardContent>
</Card>
)}
</div>
</main>
</div>
)
}