新增資料庫架構

This commit is contained in:
2025-07-19 02:12:37 +08:00
parent e3832acfa8
commit 924f03c3d7
45 changed files with 12858 additions and 324 deletions

View File

@@ -24,6 +24,7 @@ import {
import RadarChart from "@/components/radar-chart"
import HeaderMusicControl from "@/components/header-music-control"
import { categories, categorizeWishMultiple, type Wish } from "@/lib/categorization"
import { WishService } from "@/lib/supabase-service"
interface CategoryData {
name: string
@@ -63,6 +64,7 @@ export default function AnalyticsPage() {
const [wishes, setWishes] = useState<Wish[]>([])
const [analytics, setAnalytics] = useState<AnalyticsData | null>(null)
const [showCategoryGuide, setShowCategoryGuide] = useState(false)
const [showPrivacyDetails, setShowPrivacyDetails] = useState(false) // 新增:隱私說明收放狀態
// 分析許願內容(包含所有數據,包括私密的)
const analyzeWishes = (wishList: (Wish & { isPublic?: boolean })[]): AnalyticsData => {
@@ -181,9 +183,39 @@ export default function AnalyticsPage() {
}
useEffect(() => {
const savedWishes = JSON.parse(localStorage.getItem("wishes") || "[]")
setWishes(savedWishes)
setAnalytics(analyzeWishes(savedWishes))
const fetchWishes = async () => {
try {
// 獲取所有困擾案例(包含私密的,用於完整分析)
const allWishesData = await WishService.getAllWishes()
// 轉換數據格式以匹配 categorization.ts 的 Wish 接口
const convertWish = (wish: any) => ({
id: wish.id,
title: wish.title,
currentPain: wish.current_pain,
expectedSolution: wish.expected_solution,
expectedEffect: wish.expected_effect || "",
createdAt: wish.created_at,
isPublic: wish.is_public,
email: wish.email,
images: wish.images,
like_count: wish.like_count || 0, // 包含點讚數
})
const allWishes = allWishesData.map(convertWish)
setWishes(allWishes)
setAnalytics(analyzeWishes(allWishes))
} catch (error) {
console.error("獲取分析數據失敗:", error)
// 如果 Supabase 連接失敗,回退到 localStorage
const savedWishes = JSON.parse(localStorage.getItem("wishes") || "[]")
setWishes(savedWishes)
setAnalytics(analyzeWishes(savedWishes))
}
}
fetchWishes()
}, [])
if (!analytics) {
@@ -320,71 +352,92 @@ export default function AnalyticsPage() {
</header>
{/* Main Content */}
<main className="py-8 md:py-12 px-4">
<main className="py-6 md:py-12 px-3 md: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 className="text-center mb-6 md:mb-12">
<div className="flex flex-col sm:flex-row items-center justify-center gap-2 sm:gap-3 mb-3 md:mb-4">
<div className="w-10 h-10 md:w-12 md:h-12 bg-gradient-to-br from-purple-600 to-indigo-700 rounded-full flex items-center justify-center shadow-lg shadow-purple-500/25">
<BarChart3 className="w-5 h-5 md:w-6 md:h-6 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">
<h2 className="text-2xl md:text-4xl font-bold text-white text-center sm:text-left"></h2>
<Badge className="bg-gradient-to-r from-pink-700/60 to-purple-700/60 text-white border border-pink-400/50 px-2 md:px-3 py-1 text-xs md:text-sm">
</Badge>
</div>
<p className="text-blue-200 text-lg"></p>
<p className="text-blue-300 text-sm mt-2"></p>
<p className="text-blue-200 text-base md:text-lg px-2"></p>
<p className="text-blue-300 text-xs md:text-sm mt-1 px-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" />
{/* 隱私說明卡片 - 手機版可收放 */}
<Card className="bg-gradient-to-r from-blue-900/80 to-indigo-800/80 backdrop-blur-sm border border-blue-500/50 mb-6 md:mb-12">
<CardHeader className="pb-3 md:pb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 md:gap-3 min-w-0 flex-1">
<div className="w-6 h-6 md:w-8 md:h-8 bg-gradient-to-br from-indigo-600 to-purple-700 rounded-full flex items-center justify-center flex-shrink-0">
<Shield className="w-3 h-3 md:w-4 md:h-4 text-white" />
</div>
<div className="min-w-0 flex-1">
<CardTitle className="text-base md:text-xl lg:text-2xl text-white truncate"></CardTitle>
<CardDescription className="text-white/90 text-xs md:text-sm lg:text-base">
</CardDescription>
</div>
</div>
</CardTitle>
<CardDescription className="text-indigo-200 text-sm sm:text-base">
</CardDescription>
{/* 手機版收放按鈕 */}
<Button
variant="ghost"
size="sm"
onClick={() => setShowPrivacyDetails(!showPrivacyDetails)}
className="text-indigo-200 hover:text-white hover:bg-indigo-700/50 px-2 md:px-3 flex-shrink-0 md:hidden"
>
{showPrivacyDetails ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</Button>
</div>
</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 className={`${showPrivacyDetails ? "block" : "hidden"} md:block`}>
<CardContent className="pt-0">
<div className="grid sm:grid-cols-2 gap-3 md:gap-4 text-sm">
<div className="space-y-2">
<h4 className="font-semibold text-indigo-200 flex items-center gap-2">
<Eye className="w-3 h-3 md:w-4 md:h-4" />
({analytics.publicWishes} )
</h4>
<p className="text-indigo-100 text-xs md:text-sm leading-relaxed">
</p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-indigo-200 flex items-center gap-2">
<EyeOff className="w-3 h-3 md:w-4 md:h-4" />
({analytics.privateWishes} )
</h4>
<p className="text-indigo-100 text-xs md:text-sm leading-relaxed">
</p>
</div>
</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 className="mt-3 md:mt-4 p-2 md: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>
</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>
</CardContent>
</div>
</Card>
{/* 統計概覽 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6 mb-8 md:mb-12">
{/* 統計概覽 - 手機優化 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-6 mb-6 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" />
<CardContent className="p-3 md:p-6 text-center">
<div className="w-8 h-8 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-4 h-4 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-xl 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}
@@ -393,21 +446,21 @@ export default function AnalyticsPage() {
</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" />
<CardContent className="p-3 md:p-6 text-center">
<div className="w-8 h-8 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-4 h-4 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-xl 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" />
<CardContent className="p-3 md:p-6 text-center">
<div className="w-8 h-8 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-4 h-4 md:w-6 md:h-6 text-white" />
</div>
<div className="text-2xl md:text-3xl font-bold text-white mb-1">
<div className="text-xl 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>
@@ -415,16 +468,16 @@ export default function AnalyticsPage() {
</Card>
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50">
<CardContent className="p-4 md:p-6 text-center">
<CardContent className="p-3 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"
className="w-8 h-8 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" />
<GrowthIcon className="w-4 h-4 md:w-6 md:h-6 text-white" />
</div>
<div className="text-2xl md:text-3xl font-bold text-white mb-1">
<div className="text-xl md:text-3xl font-bold text-white mb-1">
{analytics.recentTrends.growth > 0 ? "+" : ""}
{analytics.recentTrends.growth}%
</div>
@@ -437,17 +490,17 @@ export default function AnalyticsPage() {
</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">
{/* 分類指南 - 手機優化 */}
<Card className="bg-gradient-to-r from-blue-900/80 to-indigo-800/80 backdrop-blur-sm border border-blue-500/50 mb-6 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="flex items-center gap-2 md:gap-3 min-w-0 flex-1">
<div className="w-6 h-6 md:w-8 md:h-8 bg-gradient-to-br from-indigo-600 to-purple-700 rounded-full flex items-center justify-center flex-shrink-0">
<BookOpen className="w-3 h-3 md:w-4 md: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">
<CardTitle className="text-base md:text-xl lg:text-2xl text-white"></CardTitle>
<CardDescription className="text-white/90 text-xs md:text-sm lg:text-base">
</CardDescription>
</div>
@@ -475,32 +528,35 @@ export default function AnalyticsPage() {
{showCategoryGuide && (
<CardContent>
<div className="grid md:grid-cols-2 gap-4">
<div className="grid md:grid-cols-2 gap-3 md: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"
className="p-3 md:p-4 rounded-lg bg-slate-800/50 border border-slate-600/30 hover:bg-slate-700/60 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-start gap-2 md:gap-3 mb-2">
<div className="text-lg md:text-2xl">{category.icon}</div>
<div className="flex-1 min-w-0">
<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>
<h4 className="font-semibold text-white text-sm md:text-base">{category.name}</h4>
<div
className="w-2 h-2 md:w-3 md:h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: category.color }}
></div>
</div>
<p className="text-sm text-slate-300 leading-relaxed">{category.description}</p>
<p className="text-xs md: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="mt-2 md:mt-3 pt-2 md: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"
className="text-xs px-1.5 md:px-2 py-0.5 bg-slate-600/50 text-slate-300 border-slate-500/50"
>
{keyword}
</Badge>
@@ -508,7 +564,7 @@ export default function AnalyticsPage() {
{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"
className="text-xs px-1.5 md:px-2 py-0.5 bg-slate-600/30 text-slate-400 border-slate-500/30"
>
+{category.keywords.length - 6}
</Badge>
@@ -523,21 +579,23 @@ export default function AnalyticsPage() {
</Card>
{/* 手機版:垂直佈局,桌面版:並排佈局 */}
<div className="space-y-8 lg:space-y-0 lg:grid lg:grid-cols-2 lg:gap-8 md:gap-12">
<div className="space-y-6 md: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" />
<CardTitle className="text-lg md:text-xl lg:text-2xl text-white flex items-center gap-2 md:gap-3">
<div className="w-6 h-6 md:w-8 md:h-8 bg-gradient-to-br from-purple-600 to-indigo-700 rounded-full flex items-center justify-center">
<BarChart3 className="w-3 h-3 md:w-4 md:h-4 text-white" />
</div>
</CardTitle>
<CardDescription className="text-blue-200"></CardDescription>
<CardDescription className="text-white/90 text-xs md:text-sm">
</CardDescription>
</CardHeader>
<CardContent>
{/* 手機版使用更大的高度,桌面版保持原有高度 */}
<div className="h-80 sm:h-96 lg:h-80 xl:h-96">
<div className="h-64 sm:h-80 md:h-64 lg:h-80 xl:h-96">
<RadarChart data={analytics.categories} />
</div>
</CardContent>
@@ -546,16 +604,16 @@ export default function AnalyticsPage() {
{/* 分類詳細統計 */}
<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" />
<CardTitle className="text-lg md:text-xl lg:text-2xl text-white flex items-center gap-2 md:gap-3">
<div className="w-6 h-6 md:w-8 md:h-8 bg-gradient-to-br from-cyan-600 to-blue-700 rounded-full flex items-center justify-center">
<Target className="w-3 h-3 md:w-4 md: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 className="bg-gradient-to-r from-pink-700/60 to-purple-700/60 text-white border border-pink-400/50 text-xs px-2 py-1">
</Badge>
</CardTitle>
<CardDescription className="text-blue-200">
<CardDescription className="text-white/90 text-xs md:text-sm">
{analytics.categories.filter((cat) => cat.count > 0).length > 0 && (
<span className="block text-xs text-slate-400 mt-1">
@@ -565,39 +623,42 @@ export default function AnalyticsPage() {
)}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<CardContent className="space-y-3 md: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">
<div className="max-h-64 md:max-h-80 overflow-y-auto pr-2 space-y-3 md: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"
className="flex items-center justify-between p-3 md: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">
<div className="flex items-center gap-2 md:gap-3 min-w-0 flex-1">
<div className="text-base md: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>
<div className="min-w-0 flex-1">
<div className="font-semibold text-white flex items-center gap-2 mb-1">
<span className="truncate">{category.name}</span>
<div
className="w-2 h-2 md:w-3 md:h-3 rounded-full flex-shrink-0"
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">
<span className="text-xs bg-gradient-to-r from-cyan-500/20 to-blue-500/20 text-cyan-200 px-1.5 md:px-2 py-0.5 rounded-full border border-cyan-500/30 flex-shrink-0">
TOP {index + 1}
</span>
)}
</div>
<div className="text-sm text-slate-300">{category.count} </div>
<div className="text-xs md: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 className="text-xs text-slate-400 mt-1 line-clamp-2">{category.description}</div>
)}
</div>
</div>
<Badge variant="secondary" className="bg-slate-600/50 text-slate-200">
<Badge variant="secondary" className="bg-slate-600/50 text-slate-200 flex-shrink-0 ml-2">
{category.percentage}%
</Badge>
</div>
@@ -622,33 +683,41 @@ export default function AnalyticsPage() {
</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">
<Card className="bg-gradient-to-r from-blue-900/80 to-indigo-800/80 backdrop-blur-sm border border-blue-500/50 mt-6 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" />
<CardTitle className="text-lg md:text-xl lg:text-2xl text-white flex items-center gap-2 md:gap-3">
<div className="w-6 h-6 md:w-8 md:h-8 bg-gradient-to-br from-purple-400 to-pink-500 rounded-full flex items-center justify-center">
<Sparkles className="w-3 h-3 md:w-4 md:h-4 text-white" />
</div>
</CardTitle>
<CardDescription className="text-purple-200"></CardDescription>
<CardDescription className="text-white/90 text-xs md:text-sm">
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid sm:grid-cols-2 gap-4 text-sm">
<div className="grid sm:grid-cols-2 gap-3 md:gap-4 text-xs md:text-sm">
<div className="space-y-2">
<h4 className="font-semibold text-purple-200">🔍 </h4>
<p className="text-purple-100"></p>
<p className="text-purple-100 leading-relaxed">
</p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-purple-200">📊 </h4>
<p className="text-purple-100"></p>
<p className="text-purple-100 leading-relaxed">
</p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-purple-200">🎯 </h4>
<p className="text-purple-100"></p>
<p className="text-purple-100 leading-relaxed">
</p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-purple-200">🔒 </h4>
<p className="text-purple-100"></p>
<p className="text-purple-100 leading-relaxed"></p>
</div>
</div>
</CardContent>
@@ -656,15 +725,15 @@ export default function AnalyticsPage() {
{/* 熱門關鍵字 */}
{analytics.topKeywords.length > 0 && (
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50 mt-8 md:mt-12">
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50 mt-6 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" />
<CardTitle className="text-lg md:text-xl lg:text-2xl text-white flex items-center gap-2 md:gap-3">
<div className="w-6 h-6 md:w-8 md:h-8 bg-gradient-to-br from-green-400 to-emerald-500 rounded-full flex items-center justify-center">
<TrendingUp className="w-3 h-3 md:w-4 md:h-4 text-white" />
</div>
</CardTitle>
<CardDescription className="text-blue-200">
<CardDescription className="text-white/90 text-xs md:text-sm">
</CardDescription>
</CardHeader>
@@ -674,7 +743,7 @@ export default function AnalyticsPage() {
<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"
className="bg-gradient-to-r from-cyan-500/20 to-blue-500/20 text-cyan-200 border border-cyan-500/30 px-2 md:px-3 py-1 md:py-1.5 text-xs md:text-sm"
>
{keyword.word} ({keyword.count})
</Badge>

View File

@@ -1,40 +1,72 @@
"use client"
import { useEffect, useState } from "react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Sparkles, MessageCircle, Users, BarChart3 } from "lucide-react"
import HeaderMusicControl from "@/components/header-music-control"
interface Star {
id: number;
style: {
left: string;
top: string;
animationDelay: string;
animationDuration: string;
};
}
export default function HomePage() {
const [stars, setStars] = useState<Star[]>([]);
const [bigStars, setBigStars] = useState<Star[]>([]);
useEffect(() => {
// 生成小星星
setStars(
Array.from({ length: 30 }, (_, i) => ({
id: i,
style: {
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 3}s`,
animationDuration: `${2 + Math.random() * 2}s`,
},
}))
);
// 生成大星星
setBigStars(
Array.from({ length: 15 }, (_, i) => ({
id: i,
style: {
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 4}s`,
animationDuration: `${3 + Math.random() * 2}s`,
},
}))
);
}, []);
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-blue-900 to-indigo-900 relative overflow-hidden flex flex-col">
{/* 星空背景 */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{/* 星星 */}
{[...Array(30)].map((_, i) => (
{stars.map((star) => (
<div
key={i}
key={star.id}
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`,
}}
style={star.style}
/>
))}
{/* 較大的星星 */}
{[...Array(15)].map((_, i) => (
{bigStars.map((star) => (
<div
key={`big-${i}`}
key={`big-${star.id}`}
className="absolute w-1 h-1 md:w-2 md:h-2 bg-blue-200 rounded-full animate-pulse opacity-60"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 4}s`,
animationDuration: `${3 + Math.random() * 2}s`,
}}
style={star.style}
/>
))}

263
app/settings/page.tsx Normal file
View File

@@ -0,0 +1,263 @@
"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, Database, Settings, TestTube, Trash2 } from "lucide-react"
import HeaderMusicControl from "@/components/header-music-control"
import MigrationDialog from "@/components/migration-dialog"
import { testSupabaseConnection, MigrationService } from "@/lib/supabase-service"
export default function SettingsPage() {
const [showMigration, setShowMigration] = useState(false)
const [isConnected, setIsConnected] = useState(false)
const [localDataCount, setLocalDataCount] = useState(0)
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
checkLocalData()
checkConnection()
}, [])
const checkLocalData = () => {
try {
const wishes = JSON.parse(localStorage.getItem("wishes") || "[]")
setLocalDataCount(wishes.length)
} catch (error) {
setLocalDataCount(0)
}
}
const checkConnection = async () => {
setIsLoading(true)
try {
const connected = await testSupabaseConnection()
setIsConnected(connected)
} catch (error) {
setIsConnected(false)
} finally {
setIsLoading(false)
}
}
const clearAllData = () => {
if (confirm("確定要清除所有本地數據嗎?此操作無法復原。")) {
MigrationService.clearLocalStorageData()
// 也清除其他設定
localStorage.removeItem("backgroundMusicState")
localStorage.removeItem("user_session")
setLocalDataCount(0)
alert("本地數據已清除")
}
}
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(25)].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 left-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">
<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>
<Link href="/">
<Button variant="ghost" size="sm" className="text-blue-200 hover:text-white hover:bg-blue-800/50">
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
</Link>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="py-8 md:py-12 px-4">
<div className="container mx-auto max-w-4xl">
<div className="text-center mb-8 md:mb-12">
<h2 className="text-2xl md:text-3xl font-bold text-white mb-4 flex items-center justify-center gap-3">
<Settings className="w-8 h-8 text-cyan-400" />
</h2>
<p className="text-blue-200 text-sm md:text-base"></p>
</div>
<div className="space-y-6 md:space-y-8">
{/* Supabase 連接狀態 */}
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-3">
<Database className="w-6 h-6 text-blue-400" />
Supabase
</CardTitle>
<CardDescription className="text-blue-200"></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-4 bg-slate-700/50 rounded-lg">
<div className="flex items-center gap-3">
<div className={`w-4 h-4 rounded-full ${isConnected ? "bg-green-400" : "bg-red-400"}`}></div>
<span className="text-white"></span>
</div>
<div className="flex items-center gap-2">
<Badge className={isConnected ? "bg-green-500/20 text-green-200" : "bg-red-500/20 text-red-200"}>
{isLoading ? "檢查中..." : isConnected ? "已連接" : "未連接"}
</Badge>
<Button
variant="ghost"
size="sm"
onClick={checkConnection}
disabled={isLoading}
className="text-blue-200 hover:text-white"
>
<TestTube className="w-4 h-4" />
</Button>
</div>
</div>
{!isConnected && (
<div className="p-4 bg-red-900/20 border border-red-600/30 rounded-lg">
<p className="text-red-200 text-sm"> Supabase</p>
<ul className="text-red-200 text-xs mt-2 ml-4 space-y-1">
<li> NEXT_PUBLIC_SUPABASE_URL </li>
<li> NEXT_PUBLIC_SUPABASE_ANON_KEY </li>
<li> Supabase </li>
<li> </li>
</ul>
</div>
)}
</CardContent>
</Card>
{/* 數據遷移 */}
{localDataCount > 0 && (
<Card className="bg-slate-800/50 backdrop-blur-sm border border-blue-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-3">
<Database className="w-6 h-6 text-blue-400" />
<Badge className="bg-orange-500/20 text-orange-200 border border-orange-400/30"></Badge>
</CardTitle>
<CardDescription className="text-blue-200">
{localDataCount}
</CardDescription>
</CardHeader>
<CardContent>
<Button
onClick={() => setShowMigration(true)}
className="w-full bg-gradient-to-r from-blue-500 to-cyan-600 hover:from-blue-600 hover:to-cyan-700 text-white"
>
</Button>
</CardContent>
</Card>
)}
{/* 數據管理 */}
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-3">
<Trash2 className="w-6 h-6 text-red-400" />
</CardTitle>
<CardDescription className="text-blue-200"></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-4 bg-slate-700/50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-white"></span>
<Badge variant="secondary">{localDataCount} </Badge>
</div>
<p className="text-slate-300 text-sm"></p>
</div>
<Button onClick={clearAllData} disabled={localDataCount === 0} variant="destructive" className="w-full">
<Trash2 className="w-4 h-4 mr-2" />
</Button>
</CardContent>
</Card>
{/* 系統資訊 */}
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50">
<CardHeader>
<CardTitle className="text-white"></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-slate-400"></span>
<div className="text-white">v1.0.0</div>
</div>
<div>
<span className="text-slate-400"></span>
<div className="text-white">{isConnected ? "Supabase" : "LocalStorage"}</div>
</div>
<div>
<span className="text-slate-400"></span>
<div className="text-white text-xs truncate">
{typeof window !== "undefined"
? localStorage.getItem("user_session")?.slice(-8) || "未設定"
: "載入中..."}
</div>
</div>
<div>
<span className="text-slate-400"></span>
<div className="text-white">
{typeof window !== "undefined" ? navigator.userAgent.split(" ").pop() : "未知"}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</main>
{/* 遷移對話框 */}
{showMigration && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="w-full max-w-2xl">
<MigrationDialog
onComplete={() => {
setShowMigration(false)
checkLocalData()
}}
onSkip={() => setShowMigration(false)}
/>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,219 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Cloud, Trash2, RefreshCw, CheckCircle, AlertTriangle, HardDrive } from "lucide-react"
import { StorageHealthService } from "@/lib/supabase-service-updated"
export default function StorageManagement() {
const [storageHealth, setStorageHealth] = useState<{
healthy: boolean
stats?: { totalFiles: number; totalSize: number }
error?: string
} | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [cleanupResult, setCleanupResult] = useState<{ cleaned: number; error?: string } | null>(null)
useEffect(() => {
checkStorageHealth()
}, [])
const checkStorageHealth = async () => {
setIsLoading(true)
try {
const health = await StorageHealthService.checkStorageHealth()
setStorageHealth(health)
} catch (error) {
setStorageHealth({ healthy: false, error: `檢查失敗: ${error}` })
} finally {
setIsLoading(false)
}
}
const cleanupOrphanedImages = async () => {
if (!confirm("確定要清理孤立的圖片嗎?這將刪除沒有被任何困擾案例引用的圖片。")) {
return
}
setIsLoading(true)
try {
const result = await StorageHealthService.cleanupOrphanedImages()
setCleanupResult(result)
// 重新檢查存儲狀態
await checkStorageHealth()
} catch (error) {
setCleanupResult({ cleaned: 0, error: `清理失敗: ${error}` })
} finally {
setIsLoading(false)
}
}
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return "0 Bytes"
const k = 1024
const sizes = ["Bytes", "KB", "MB", "GB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]
}
return (
<div className="space-y-6">
{/* 存儲狀態 */}
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-3">
<Cloud className="w-6 h-6 text-blue-400" />
Supabase Storage
</CardTitle>
<CardDescription className="text-blue-200">使</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-4 bg-slate-700/50 rounded-lg">
<div className="flex items-center gap-3">
<div
className={`w-4 h-4 rounded-full ${
storageHealth?.healthy ? "bg-green-400" : "bg-red-400"
} ${isLoading ? "animate-pulse" : ""}`}
></div>
<span className="text-white"></span>
</div>
<div className="flex items-center gap-2">
<Badge
className={storageHealth?.healthy ? "bg-green-500/20 text-green-200" : "bg-red-500/20 text-red-200"}
>
{isLoading ? "檢查中..." : storageHealth?.healthy ? "正常運行" : "服務異常"}
</Badge>
<Button
variant="ghost"
size="sm"
onClick={checkStorageHealth}
disabled={isLoading}
className="text-blue-200 hover:text-white"
>
<RefreshCw className={`w-4 h-4 ${isLoading ? "animate-spin" : ""}`} />
</Button>
</div>
</div>
{/* 存儲統計 */}
{storageHealth?.stats && (
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-slate-700/50 rounded-lg text-center">
<div className="text-2xl font-bold text-white mb-1">{storageHealth.stats.totalFiles}</div>
<div className="text-sm text-slate-300"></div>
</div>
<div className="p-4 bg-slate-700/50 rounded-lg text-center">
<div className="text-2xl font-bold text-white mb-1">
{formatFileSize(storageHealth.stats.totalSize)}
</div>
<div className="text-sm text-slate-300">使</div>
</div>
</div>
)}
{/* 存儲使用進度條 */}
{storageHealth?.stats && (
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-slate-300">使</span>
<span className="text-slate-400">{formatFileSize(storageHealth.stats.totalSize)} / 1GB ()</span>
</div>
<Progress
value={Math.min((storageHealth.stats.totalSize / (1024 * 1024 * 1024)) * 100, 100)}
className="w-full"
/>
</div>
)}
{/* 錯誤訊息 */}
{storageHealth?.error && (
<Alert className="border-red-500/50 bg-red-900/20">
<AlertTriangle className="h-4 w-4 text-red-400" />
<AlertDescription className="text-red-100">
<div className="space-y-1">
<p></p>
<p className="text-sm">{storageHealth.error}</p>
</div>
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
{/* 存儲管理 */}
<Card className="bg-slate-800/50 backdrop-blur-sm border border-slate-600/50">
<CardHeader>
<CardTitle className="text-white flex items-center gap-3">
<HardDrive className="w-6 h-6 text-purple-400" />
</CardTitle>
<CardDescription className="text-blue-200"></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-4 bg-slate-700/50 rounded-lg">
<h4 className="text-white font-semibold mb-2 flex items-center gap-2">
<Trash2 className="w-4 h-4 text-orange-400" />
</h4>
<p className="text-slate-300 text-sm mb-3"></p>
<Button
onClick={cleanupOrphanedImages}
disabled={isLoading || !storageHealth?.healthy}
className="bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 text-white"
>
<Trash2 className="w-4 h-4 mr-2" />
{isLoading ? "清理中..." : "開始清理"}
</Button>
</div>
{/* 清理結果 */}
{cleanupResult && (
<Alert
className={`${
cleanupResult.error ? "border-red-500/50 bg-red-900/20" : "border-green-500/50 bg-green-900/20"
}`}
>
{cleanupResult.error ? (
<AlertTriangle className="h-4 w-4 text-red-400" />
) : (
<CheckCircle className="h-4 w-4 text-green-400" />
)}
<AlertDescription className={cleanupResult.error ? "text-red-100" : "text-green-100"}>
{cleanupResult.error ? (
<div>
<p></p>
<p className="text-sm mt-1">{cleanupResult.error}</p>
</div>
) : (
<div>
<p></p>
<p className="text-sm mt-1">
{cleanupResult.cleaned > 0
? `成功清理了 ${cleanupResult.cleaned} 個孤立的圖片檔案`
: "沒有發現需要清理的孤立圖片"}
</p>
</div>
)}
</AlertDescription>
</Alert>
)}
{/* 存儲最佳實踐 */}
<div className="p-4 bg-blue-900/20 border border-blue-600/30 rounded-lg">
<h4 className="text-blue-200 font-semibold mb-2">💡 </h4>
<ul className="text-blue-100 text-sm space-y-1">
<li> </li>
<li> 使</li>
<li> </li>
<li> 使WebP </li>
</ul>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -11,10 +11,15 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Checkbox } from "@/components/ui/checkbox"
import { Sparkles, ArrowLeft, Send, BarChart3, Eye, EyeOff, Shield, Info } from "lucide-react"
import { Sparkles, ArrowLeft, Send, BarChart3, Eye, EyeOff, Shield, Info, Mail, ImageIcon } from "lucide-react"
import { useToast } from "@/hooks/use-toast"
import { soundManager } from "@/lib/sound-effects"
import HeaderMusicControl from "@/components/header-music-control"
import { moderateWishForm, type ModerationResult } from "@/lib/content-moderation"
import ContentModerationFeedback from "@/components/content-moderation-feedback"
import ImageUpload from "@/components/image-upload"
import type { ImageFile } from "@/lib/image-utils"
import { WishService } from "@/lib/supabase-service"
export default function SubmitPage() {
const [formData, setFormData] = useState({
@@ -22,11 +27,15 @@ export default function SubmitPage() {
currentPain: "",
expectedSolution: "",
expectedEffect: "",
isPublic: true, // 預設為公開
isPublic: true,
email: "",
})
const [images, setImages] = useState<ImageFile[]>([])
const [isSubmitting, setIsSubmitting] = useState(false)
const { toast } = useToast()
const router = useRouter()
const [moderationResult, setModerationResult] = useState<ModerationResult | null>(null)
const [showModerationFeedback, setShowModerationFeedback] = useState(false)
// 初始化音效系統
useEffect(() => {
@@ -43,31 +52,64 @@ export default function SubmitPage() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// 先進行內容審核
const moderation = moderateWishForm(formData)
setModerationResult(moderation)
if (!moderation.isAppropriate) {
setShowModerationFeedback(true)
await soundManager.play("click") // 播放提示音效
toast({
title: "內容需要修改",
description: "請根據建議修改內容後再次提交",
variant: "destructive",
})
return
}
setIsSubmitting(true)
setShowModerationFeedback(false)
// 播放提交音效
await soundManager.play("submit")
await new Promise((resolve) => setTimeout(resolve, 1500))
try {
// 創建困擾案例到 Supabase 數據庫
await WishService.createWish({
title: formData.title,
currentPain: formData.currentPain,
expectedSolution: formData.expectedSolution,
expectedEffect: formData.expectedEffect,
isPublic: formData.isPublic,
email: formData.email,
images: images, // 直接傳遞 ImageFile 數組
})
const wishes = JSON.parse(localStorage.getItem("wishes") || "[]")
const newWish = {
id: Date.now(),
...formData,
createdAt: new Date().toISOString(),
// 播放成功音效
await soundManager.play("success")
toast({
title: "你的困擾已成功提交",
description: formData.isPublic
? "正在為你準備專業的回饋,其他人也能看到你的分享..."
: "正在為你準備專業的回饋,你的分享將保持私密...",
})
} catch (error) {
console.error("提交困擾失敗:", error)
// 播放錯誤音效
await soundManager.play("click")
toast({
title: "提交失敗",
description: "請稍後再試或檢查網路連接",
variant: "destructive",
})
setIsSubmitting(false)
return
}
wishes.push(newWish)
localStorage.setItem("wishes", JSON.stringify(wishes))
// 播放成功音效
await soundManager.play("success")
toast({
title: "你的困擾已成功提交",
description: formData.isPublic
? "正在為你準備專業的回饋,其他人也能看到你的分享..."
: "正在為你準備專業的回饋,你的分享將保持私密...",
})
setFormData({
title: "",
@@ -75,8 +117,11 @@ export default function SubmitPage() {
expectedSolution: "",
expectedEffect: "",
isPublic: true,
email: "",
})
setImages([])
setIsSubmitting(false)
setModerationResult(null)
// 跳轉到感謝頁面
setTimeout(() => {
@@ -354,6 +399,70 @@ export default function SubmitPage() {
/>
</div>
{/* 圖片上傳區域 */}
<div className="space-y-2">
<Label className="text-blue-100 font-semibold text-sm md:text-base flex items-center gap-2">
<ImageIcon className="w-4 h-4" />
()
</Label>
<div className="text-xs md:text-sm text-slate-400 mb-3">
</div>
<ImageUpload images={images} onImagesChange={setImages} disabled={isSubmitting} />
</div>
{/* Email 聯絡資訊 - 可選 */}
<div className="space-y-2">
<Label
htmlFor="email"
className="text-blue-100 font-semibold text-sm md:text-base flex items-center gap-2"
>
<Mail className="w-4 h-4" />
()
</Label>
<Input
id="email"
type="email"
placeholder="your.email@example.com"
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
className="bg-slate-700/50 border-blue-600/50 text-white placeholder:text-blue-300 focus:border-cyan-400 text-sm md:text-base"
/>
<div className="text-xs md:text-sm text-slate-400 leading-relaxed">
<div className="flex items-start gap-2 mb-2">
<Shield className="w-3 h-3 text-blue-400 mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium text-blue-300 mb-1"></p>
<ul className="space-y-1 text-slate-400">
<li> </li>
<li> Email </li>
<li> </li>
<li> Email </li>
</ul>
</div>
</div>
</div>
</div>
{/* 內容審核回饋 */}
{showModerationFeedback && moderationResult && (
<ContentModerationFeedback
result={moderationResult}
onRetry={() => {
const newModeration = moderateWishForm(formData)
setModerationResult(newModeration)
if (newModeration.isAppropriate) {
setShowModerationFeedback(false)
toast({
title: "內容檢查通過",
description: "現在可以提交你的困擾了!",
})
}
}}
className="animate-in slide-in-from-top-2 duration-300"
/>
)}
{/* 隱私設定區塊 */}
<div className="space-y-4 p-4 md:p-5 bg-gradient-to-r from-slate-700/30 to-slate-800/30 rounded-lg border border-slate-600/50">
<div className="flex items-center gap-3">
@@ -391,12 +500,12 @@ export default function SubmitPage() {
<div className="text-xs md:text-sm text-slate-300 leading-relaxed">
{formData.isPublic ? (
<span>
<br />
</span>
) : (
<span>
🔒
🔒
<br />
</span>
)}
@@ -413,6 +522,7 @@ export default function SubmitPage() {
<ul className="space-y-1 text-slate-400">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
@@ -437,6 +547,7 @@ export default function SubmitPage() {
<>
<Send className="w-4 h-4 mr-2" />
{formData.isPublic ? "公開提交困擾" : "私密提交困擾"}
{images.length > 0 && <span className="ml-1 text-xs opacity-75">({images.length} )</span>}
</>
)}
</Button>

View File

@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Sparkles, Heart, Users, ArrowRight, Home, MessageCircle, BarChart3, Eye, EyeOff } from "lucide-react"
import HeaderMusicControl from "@/components/header-music-control"
import { WishService } from "@/lib/supabase-service"
export default function ThankYouPage() {
const [wishes, setWishes] = useState<any[]>([])
@@ -13,15 +14,48 @@ export default function ThankYouPage() {
const [lastWishIsPublic, setLastWishIsPublic] = useState(true)
useEffect(() => {
const savedWishes = JSON.parse(localStorage.getItem("wishes") || "[]")
setWishes(savedWishes)
const fetchWishes = async () => {
try {
// 獲取所有困擾案例
const allWishesData = await WishService.getAllWishes()
// 轉換數據格式
const convertWish = (wish: any) => ({
id: wish.id,
title: wish.title,
currentPain: wish.current_pain,
expectedSolution: wish.expected_solution,
expectedEffect: wish.expected_effect || "",
createdAt: wish.created_at,
isPublic: wish.is_public,
email: wish.email,
images: wish.images,
like_count: wish.like_count || 0, // 包含點讚數
})
const allWishes = allWishesData.map(convertWish)
setWishes(allWishes)
// 檢查最後一個提交的願望是否為公開
if (savedWishes.length > 0) {
const lastWish = savedWishes[savedWishes.length - 1]
setLastWishIsPublic(lastWish.isPublic !== false)
// 檢查最後一個提交的願望是否為公開
if (allWishes.length > 0) {
const lastWish = allWishes[allWishes.length - 1]
setLastWishIsPublic(lastWish.isPublic !== false)
}
} catch (error) {
console.error("獲取統計數據失敗:", error)
// 如果 Supabase 連接失敗,回退到 localStorage
const savedWishes = JSON.parse(localStorage.getItem("wishes") || "[]")
setWishes(savedWishes)
if (savedWishes.length > 0) {
const lastWish = savedWishes[savedWishes.length - 1]
setLastWishIsPublic(lastWish.isPublic !== false)
}
}
}
fetchWishes()
// 延遲顯示內容,創造進入效果
setTimeout(() => setShowContent(true), 300)
}, [])
@@ -250,7 +284,7 @@ export default function ThankYouPage() {
{/* 統計卡片 */}
<div className="grid sm:grid-cols-2 md:grid-cols-3 gap-4 sm:gap-6 md:gap-8 mb-8 sm:mb-12 md:mb-16">
<Card className="bg-gradient-to-br from-pink-800/30 to-purple-800/30 backdrop-blur-sm border border-pink-600/50 shadow-2xl shadow-pink-500/20 transform hover:scale-105 transition-all duration-300">
<Card className="bg-gradient-to-br from-pink-900/60 to-purple-900/60 backdrop-blur-sm border border-pink-700/40 shadow-2xl shadow-pink-500/20 transform hover:scale-105 transition-all duration-300">
<CardContent className="p-4 sm:p-6 md:p-8 text-center">
<div className="w-12 h-12 md:w-16 md:h-16 bg-gradient-to-br from-pink-400 to-purple-500 rounded-full flex items-center justify-center mx-auto mb-4 shadow-lg shadow-pink-500/30">
<Users className="w-6 h-6 md:w-8 md:h-8 text-white" />
@@ -261,7 +295,7 @@ export default function ThankYouPage() {
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-cyan-800/30 to-blue-800/30 backdrop-blur-sm border border-cyan-600/50 shadow-2xl shadow-cyan-500/20 transform hover:scale-105 transition-all duration-300">
<Card className="bg-gradient-to-br from-cyan-900/60 to-blue-900/60 backdrop-blur-sm border border-cyan-700/40 shadow-2xl shadow-cyan-500/20 transform hover:scale-105 transition-all duration-300">
<CardContent className="p-4 sm:p-6 md:p-8 text-center">
<div className="w-12 h-12 md:w-16 md:h-16 bg-gradient-to-br from-cyan-400 to-blue-500 rounded-full flex items-center justify-center mx-auto mb-4 shadow-lg shadow-cyan-500/30">
<Sparkles className="w-6 h-6 md:w-8 md:h-8 text-white" />
@@ -272,7 +306,7 @@ export default function ThankYouPage() {
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-purple-800/30 to-indigo-800/30 backdrop-blur-sm border border-purple-600/50 shadow-2xl shadow-purple-500/20 transform hover:scale-105 transition-all duration-300">
<Card className="bg-gradient-to-br from-purple-900/60 to-indigo-900/60 backdrop-blur-sm border border-purple-700/40 shadow-2xl shadow-purple-500/20 transform hover:scale-105 transition-all duration-300">
<CardContent className="p-4 sm:p-6 md:p-8 text-center">
<div className="w-12 h-12 md:w-16 md:h-16 bg-gradient-to-br from-purple-400 to-indigo-500 rounded-full flex items-center justify-center mx-auto mb-4 shadow-lg shadow-purple-500/30">
<Heart className="w-6 h-6 md:w-8 md:h-8 text-white" fill="currentColor" />

View File

@@ -9,6 +9,7 @@ import { Sparkles, ArrowLeft, Search, Plus, Filter, X, BarChart3, Eye, Users } f
import WishCard from "@/components/wish-card"
import HeaderMusicControl from "@/components/header-music-control"
import { categories, categorizeWishMultiple, getCategoryStats, type Wish } from "@/lib/categorization"
import { WishService } from "@/lib/supabase-service"
export default function WishesPage() {
const [wishes, setWishes] = useState<Wish[]>([])
@@ -22,15 +23,55 @@ export default function WishesPage() {
const [privateCount, setPrivateCount] = useState(0)
useEffect(() => {
const savedWishes = JSON.parse(localStorage.getItem("wishes") || "[]")
const publicOnly = savedWishes.filter((wish: Wish & { isPublic?: boolean }) => wish.isPublic !== false)
const privateOnly = savedWishes.filter((wish: Wish & { isPublic?: boolean }) => wish.isPublic === false)
const fetchWishes = async () => {
try {
// 獲取所有困擾(用於統計)
const allWishesData = await WishService.getAllWishes()
// 獲取公開困擾(用於顯示)
const publicWishesData = await WishService.getPublicWishes()
// 轉換數據格式以匹配 categorization.ts 的 Wish 接口
const convertWish = (wish: any) => ({
id: wish.id,
title: wish.title,
currentPain: wish.current_pain,
expectedSolution: wish.expected_solution,
expectedEffect: wish.expected_effect || "",
createdAt: wish.created_at,
isPublic: wish.is_public,
email: wish.email,
images: wish.images,
like_count: wish.like_count || 0, // 包含點讚數
})
const allWishes = allWishesData.map(convertWish)
const publicWishes = publicWishesData.map(convertWish)
// 計算私密困擾數量
const privateCount = allWishes.length - publicWishes.length
setWishes(savedWishes)
setPublicWishes(publicOnly.reverse())
setTotalWishes(savedWishes.length)
setPrivateCount(privateOnly.length)
setCategoryStats(getCategoryStats(publicOnly)) // 只統計公開的困擾
setWishes(allWishes)
setPublicWishes(publicWishes)
setTotalWishes(allWishes.length)
setPrivateCount(privateCount)
setCategoryStats(getCategoryStats(publicWishes))
} catch (error) {
console.error("獲取困擾數據失敗:", error)
// 如果 Supabase 連接失敗,回退到 localStorage
const savedWishes = JSON.parse(localStorage.getItem("wishes") || "[]")
const publicOnly = savedWishes.filter((wish: Wish & { isPublic?: boolean }) => wish.isPublic !== false)
const privateOnly = savedWishes.filter((wish: Wish & { isPublic?: boolean }) => wish.isPublic === false)
setWishes(savedWishes)
setPublicWishes(publicOnly.reverse())
setTotalWishes(savedWishes.length)
setPrivateCount(privateOnly.length)
setCategoryStats(getCategoryStats(publicOnly))
}
}
fetchWishes()
}, [])
useEffect(() => {