Initial commit

This commit is contained in:
2025-07-18 13:07:28 +08:00
commit e3832acfa8
91 changed files with 10929 additions and 0 deletions

413
app/wishes/page.tsx Normal file
View File

@@ -0,0 +1,413 @@
"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>
)
}