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

414 lines
20 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 { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Sparkles, ArrowLeft, Search, Plus, Filter, X, BarChart3, Eye, Users } from "lucide-react"
import WishCard from "@/components/wish-card"
import HeaderMusicControl from "@/components/header-music-control"
import { categories, categorizeWishMultiple, getCategoryStats, type Wish } from "@/lib/categorization"
export default function WishesPage() {
const [wishes, setWishes] = useState<Wish[]>([])
const [publicWishes, setPublicWishes] = useState<Wish[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
const [filteredWishes, setFilteredWishes] = useState<Wish[]>([])
const [categoryStats, setCategoryStats] = useState<{ [key: string]: number }>({})
const [showFilters, setShowFilters] = useState(false)
const [totalWishes, setTotalWishes] = useState(0)
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)
setWishes(savedWishes)
setPublicWishes(publicOnly.reverse())
setTotalWishes(savedWishes.length)
setPrivateCount(privateOnly.length)
setCategoryStats(getCategoryStats(publicOnly)) // 只統計公開的困擾
}, [])
useEffect(() => {
let filtered = publicWishes
// 按搜尋詞篩選
if (searchTerm) {
filtered = filtered.filter(
(wish) =>
wish.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
wish.currentPain.toLowerCase().includes(searchTerm.toLowerCase()) ||
wish.expectedSolution.toLowerCase().includes(searchTerm.toLowerCase()),
)
}
// 按分類篩選(支持多標籤)
if (selectedCategories.length > 0) {
filtered = filtered.filter((wish) => {
const wishCategories = categorizeWishMultiple(wish)
return selectedCategories.some((selectedCategory) =>
wishCategories.some((wishCategory) => wishCategory.name === selectedCategory),
)
})
}
setFilteredWishes(filtered)
}, [publicWishes, searchTerm, selectedCategories])
const toggleCategory = (categoryName: string) => {
setSelectedCategories((prev) =>
prev.includes(categoryName) ? prev.filter((cat) => cat !== categoryName) : [...prev, categoryName],
)
}
const clearAllFilters = () => {
setSelectedCategories([])
setSearchTerm("")
}
const hasActiveFilters = selectedCategories.length > 0 || searchTerm.length > 0
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-cyan-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="/analytics">
<Button
variant="ghost"
size="sm"
className="text-blue-200 hover:text-white hover:bg-blue-800/50 px-4"
>
<BarChart3 className="w-4 h-4 mr-2" />
</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="/analytics">
<Button
variant="ghost"
size="sm"
className="text-blue-200 hover:text-white hover:bg-blue-800/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="/analytics">
<Button
variant="ghost"
size="sm"
className="text-blue-200 hover:text-white hover:bg-blue-800/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-4xl">
<div className="text-center mb-6 md:mb-8">
<h2 className="text-2xl md:text-3xl font-bold text-white mb-3 md:mb-4"></h2>
<p className="text-blue-200 mb-4 md:mb-6 text-sm md:text-base px-4">
</p>
{/* Search Bar and Filter Button - 並排布局 */}
<div className="flex flex-col sm:flex-row gap-3 max-w-lg mx-auto px-2 md:px-0 mb-4">
{/* Search Input */}
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-blue-300 w-4 h-4" />
<Input
placeholder="搜尋相似的工作困擾..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 bg-slate-700/50 border-blue-600/50 text-white placeholder:text-blue-300 focus:border-cyan-400 text-sm md:text-base"
/>
</div>
{/* Filter Button */}
<Button
variant="outline"
onClick={() => setShowFilters(!showFilters)}
className={`
border-blue-400/50 bg-slate-800/50 text-blue-200 hover:bg-slate-700/50 hover:text-white
flex-shrink-0 px-4 py-2 h-10
${showFilters ? "bg-slate-700/70 border-cyan-400/70 text-cyan-200" : ""}
`}
>
<Filter className="w-4 h-4 mr-2" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
{selectedCategories.length > 0 && (
<Badge className="ml-2 bg-cyan-500 text-white text-xs px-1.5 py-0.5 min-w-[20px] h-5">
{selectedCategories.length}
</Badge>
)}
</Button>
</div>
</div>
{/* Category Filters */}
{showFilters && (
<div className="mb-6 md:mb-8 p-4 md:p-6 bg-slate-800/30 backdrop-blur-sm rounded-xl border border-slate-600/50 animate-in slide-in-from-top-2 duration-200">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<Filter className="w-5 h-5" />
<Badge className="bg-blue-600/20 text-blue-200 text-xs px-2 py-1"></Badge>
</h3>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearAllFilters}
className="text-blue-300 hover:text-white hover:bg-blue-800/50"
>
<X className="w-4 h-4 mr-1" />
</Button>
)}
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{categories.map((category) => {
const count = categoryStats[category.name] || 0
const isSelected = selectedCategories.includes(category.name)
return (
<button
key={category.name}
onClick={() => toggleCategory(category.name)}
disabled={count === 0}
className={`
relative p-3 rounded-lg border transition-all duration-200 text-left
${
isSelected
? `bg-gradient-to-r ${category.bgColor} ${category.borderColor} ${category.textColor} border-2 shadow-lg transform scale-[1.02]`
: count > 0
? "bg-slate-700/30 border-slate-600/50 text-slate-300 hover:bg-slate-600/40 hover:border-slate-500/70 hover:scale-[1.01]"
: "bg-slate-800/20 border-slate-700/30 text-slate-500 cursor-not-allowed opacity-50"
}
`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-lg">{category.icon}</span>
<div>
<div className="font-medium text-sm">{category.name}</div>
<div className="text-xs opacity-75">{count} </div>
</div>
</div>
{isSelected && <div className="w-2 h-2 rounded-full bg-current opacity-80 animate-pulse"></div>}
</div>
</button>
)
})}
</div>
{/* Active Filters Display */}
{selectedCategories.length > 0 && (
<div className="mt-4 pt-4 border-t border-slate-600/30">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-blue-200"></span>
{selectedCategories.map((categoryName) => {
const category = categories.find((cat) => cat.name === categoryName)
return (
<Badge
key={categoryName}
className={`bg-gradient-to-r ${category?.bgColor} ${category?.borderColor} ${category?.textColor} border cursor-pointer hover:opacity-80 transition-opacity`}
onClick={() => toggleCategory(categoryName)}
>
<span className="mr-1">{category?.icon}</span>
{categoryName}
<X className="w-3 h-3 ml-1" />
</Badge>
)
})}
</div>
</div>
)}
</div>
)}
{/* Stats - 手機優化,增加隱私說明 */}
<div className="text-center mb-6 md:mb-8">
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 mb-4">
<div className="inline-flex items-center gap-2 bg-slate-800/50 backdrop-blur-sm rounded-full px-3 md:px-4 py-2 text-blue-200 border border-blue-700/50 text-xs md:text-sm">
<Eye className="w-3 h-3 md:w-4 md:h-4 text-cyan-400" />
<span className="hidden sm:inline">
{publicWishes.length}
{hasActiveFilters && `,找到 ${filteredWishes.length} 個相關經歷`}
</span>
<span className="sm:hidden">
{publicWishes.length}
{hasActiveFilters && ` (${filteredWishes.length})`}
</span>
</div>
{privateCount > 0 && (
<div className="inline-flex items-center gap-2 bg-slate-700/50 backdrop-blur-sm rounded-full px-3 md:px-4 py-2 text-slate-300 border border-slate-600/50 text-xs md:text-sm">
<Users className="w-3 h-3 md:w-4 md:h-4 text-slate-400" />
<span className="hidden sm:inline"> {privateCount} </span>
<span className="sm:hidden">{privateCount} </span>
</div>
)}
</div>
{privateCount > 0 && (
<p className="text-xs md:text-sm text-slate-400 px-4">
</p>
)}
</div>
{/* Wishes Grid - 手機優化 */}
{filteredWishes.length > 0 ? (
<div className="grid gap-4 md:gap-6 lg:grid-cols-1">
{filteredWishes.map((wish) => (
<WishCard key={wish.id} wish={wish} />
))}
</div>
) : publicWishes.length === 0 ? (
<div className="text-center py-8 sm:py-12 md:py-16 px-4">
<div className="mb-4 sm:mb-6">
<div className="relative mx-auto w-16 h-20 sm:w-20 sm:h-26 md:w-24 md:h-32">
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-14 h-20 md:w-16 md:h-24 bg-gradient-to-b from-cyan-100/20 to-blue-200/30 rounded-t-xl md:rounded-t-2xl rounded-b-md md:rounded-b-lg shadow-xl shadow-cyan-500/20 backdrop-blur-sm border border-cyan-300/30 opacity-50">
<div className="absolute -top-1.5 md:-top-2 left-1/2 transform -translate-x-1/2 w-3 h-2.5 md:w-4 md:h-3 bg-slate-700 rounded-t-sm md:rounded-t-md"></div>
<div className="absolute top-0.5 md:top-1 left-0.5 md:left-1 w-1.5 h-14 md:w-2 md:h-16 bg-white/20 rounded-full"></div>
</div>
</div>
</div>
<h3 className="text-base sm:text-lg md:text-xl font-semibold text-blue-100 mb-2">
{totalWishes > 0 ? "還沒有人公開分享經歷" : "還沒有人分享經歷"}
</h3>
<p className="text-blue-300 mb-4 sm:mb-6 text-sm sm:text-base leading-relaxed px-2">
{totalWishes > 0
? `目前有 ${totalWishes} 個案例,但都選擇保持私密。成為第一個公開分享的人吧!`
: "成為第一個分享工作困擾的人,幫助更多人找到解決方案"}
</p>
<Link href="/submit">
<Button 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 text-sm md:text-base">
<Plus className="w-4 h-4 mr-2" />
{totalWishes > 0 ? "公開分享第一個案例" : "分享第一個案例"}
</Button>
</Link>
</div>
) : (
<div className="text-center py-8 sm:py-12 md:py-16 px-4">
<Search className="w-10 h-10 sm:w-12 sm:h-12 md:w-16 md:h-16 text-blue-400 mx-auto mb-3 sm:mb-4 opacity-50" />
<h3 className="text-base sm:text-lg md:text-xl font-semibold text-blue-100 mb-2"></h3>
<p className="text-blue-300 mb-4 md:mb-6 text-sm md:text-base">
{hasActiveFilters ? "試試調整篩選條件,或分享你的獨特經歷" : "試試其他關鍵字,或分享你的困擾"}
</p>
<div className="flex flex-col sm:flex-row gap-3 md:gap-4 justify-center">
{hasActiveFilters && (
<Button
variant="outline"
onClick={clearAllFilters}
className="border-blue-400 bg-slate-800/50 text-blue-100 hover:bg-slate-700/50 text-sm md:text-base"
>
</Button>
)}
<Link href="/submit">
<Button className="w-full sm:w-auto 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 text-sm md:text-base">
<Plus className="w-4 h-4 mr-2" />
</Button>
</Link>
</div>
</div>
)}
</div>
</main>
</div>
)
}