整合資料庫、完成登入註冊忘記密碼功能

This commit is contained in:
2025-09-09 12:00:22 +08:00
parent af88c0f037
commit 32b19e9a0f
85 changed files with 11672 additions and 2350 deletions

View File

@@ -6,24 +6,134 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Progress } from "@/components/ui/progress"
import { BarChart3, Clock, Heart, ImageIcon, MessageSquare, FileText, TrendingUp } from "lucide-react"
import { BarChart3, Clock, Heart, ImageIcon, MessageSquare, FileText, TrendingUp, Trash2, RefreshCw } from "lucide-react"
import { useState, useEffect } from "react"
interface ActivityRecordsDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
// Recent apps data - empty for production
const recentApps: any[] = []
// Mock data for demonstration - will be replaced with real data
const mockRecentApps: any[] = []
// Category data - empty for production
const categoryData: any[] = []
const mockCategoryData: any[] = []
export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDialogProps) {
const { user } = useAuth()
const {
user,
getViewCount,
getAppLikes,
getUserLikeHistory,
getAppLikesInPeriod
} = useAuth()
const [recentApps, setRecentApps] = useState<any[]>([])
const [categoryData, setCategoryData] = useState<any[]>([])
const [isResetting, setIsResetting] = useState(false)
if (!user) return null
// Calculate user statistics
const calculateUserStats = () => {
if (!user) return {
totalUsage: 0,
totalDuration: 0,
favoriteApps: 0,
daysJoined: 0
}
// Calculate total usage count (views)
const totalUsage = Object.values(user.recentApps || []).length
// Calculate total duration (simplified - 5 minutes per app view)
const totalDuration = totalUsage * 5 // minutes
// Get favorite apps count
const favoriteApps = user.favoriteApps?.length || 0
// Calculate days joined
const joinDate = new Date(user.joinDate)
const now = new Date()
// Check if joinDate is valid
let daysJoined = 0
if (!isNaN(joinDate.getTime())) {
daysJoined = Math.floor((now.getTime() - joinDate.getTime()) / (1000 * 60 * 60 * 24))
}
return {
totalUsage,
totalDuration,
favoriteApps,
daysJoined: Math.max(0, daysJoined)
}
}
const stats = calculateUserStats()
// Load recent apps from user's recent apps
useEffect(() => {
if (user?.recentApps) {
// Convert recent app IDs to app objects (simplified)
const recentAppsData = user.recentApps.slice(0, 10).map((appId, index) => ({
id: appId,
name: `應用 ${appId}`,
author: "系統",
category: "AI應用",
usageCount: getViewCount(appId),
timeSpent: "5分鐘",
lastUsed: `${index + 1}天前`,
icon: MessageSquare,
color: "bg-blue-500"
}))
setRecentApps(recentAppsData)
} else {
setRecentApps([])
}
}, [user, getViewCount])
// Load category data (simplified)
useEffect(() => {
// This would normally be calculated from actual usage data
setCategoryData([])
}, [user])
// Reset user activity data
const resetActivityData = async () => {
setIsResetting(true)
try {
// Clear localStorage data
if (typeof window !== "undefined") {
localStorage.removeItem("appViews")
localStorage.removeItem("appLikes")
localStorage.removeItem("userLikes")
localStorage.removeItem("appLikesOld")
localStorage.removeItem("appRatings")
}
// Reset user's recent apps and favorites
if (user) {
const updatedUser = {
...user,
recentApps: [],
favoriteApps: [],
totalLikes: 0,
totalViews: 0
}
localStorage.setItem("user", JSON.stringify(updatedUser))
// Reload the page to refresh all data
window.location.reload()
}
} catch (error) {
console.error("Error resetting activity data:", error)
} finally {
setIsResetting(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl max-h-[85vh] overflow-hidden">
@@ -45,52 +155,86 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
<h3 className="text-lg font-semibold mb-2">使</h3>
<p className="text-sm text-muted-foreground mb-4"> AI </p>
<div className="grid gap-4">
{recentApps.map((app) => {
const IconComponent = app.icon
return (
<Card key={app.id} className="hover:shadow-md transition-shadow">
<CardContent className="flex items-center justify-between p-4">
<div className="flex items-center space-x-4">
<div className={`p-3 rounded-lg ${app.color}`}>
<IconComponent className="w-6 h-6 text-white" />
</div>
<div>
<h4 className="font-semibold">{app.name}</h4>
<p className="text-sm text-muted-foreground">by {app.author}</p>
<div className="flex items-center gap-4 mt-1">
<Badge variant="secondary" className="text-xs">
{app.category}
</Badge>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<BarChart3 className="w-3 h-3" />
使 {app.usageCount}
</span>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<Clock className="w-3 h-3" />
{app.timeSpent}
</span>
{recentApps.length > 0 ? (
<div className="grid gap-4">
{recentApps.map((app) => {
const IconComponent = app.icon
return (
<Card key={app.id} className="hover:shadow-md transition-shadow">
<CardContent className="flex items-center justify-between p-4">
<div className="flex items-center space-x-4">
<div className={`p-3 rounded-lg ${app.color}`}>
<IconComponent className="w-6 h-6 text-white" />
</div>
<div>
<h4 className="font-semibold">{app.name}</h4>
<p className="text-sm text-muted-foreground">by {app.author}</p>
<div className="flex items-center gap-4 mt-1">
<Badge variant="secondary" className="text-xs">
{app.category}
</Badge>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<BarChart3 className="w-3 h-3" />
使 {app.usageCount}
</span>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<Clock className="w-3 h-3" />
{app.timeSpent}
</span>
</div>
</div>
</div>
</div>
<div className="text-right">
<p className="text-xs text-muted-foreground mb-2">{app.lastUsed}</p>
<Button size="sm" variant="outline">
</Button>
</div>
</CardContent>
</Card>
)
})}
</div>
<div className="text-right">
<p className="text-xs text-muted-foreground mb-2">{app.lastUsed}</p>
<Button size="sm" variant="outline">
</Button>
</div>
</CardContent>
</Card>
)
})}
</div>
) : (
<Card>
<CardContent className="text-center py-12">
<MessageSquare className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-600 mb-2">使</h3>
<p className="text-gray-500 mb-4"> AI 使</p>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
>
</Button>
</CardContent>
</Card>
)}
</div>
</TabsContent>
<TabsContent value="statistics" className="space-y-6 max-h-[60vh] overflow-y-auto">
<div>
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-muted-foreground mb-4"></p>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-muted-foreground"></p>
</div>
<Button
variant="outline"
size="sm"
onClick={resetActivityData}
disabled={isResetting}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
{isResetting ? (
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
) : (
<Trash2 className="w-4 h-4 mr-2" />
)}
{isResetting ? "重置中..." : "清空數據"}
</Button>
</div>
{/* Statistics Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
@@ -100,8 +244,10 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">41</div>
<p className="text-xs text-muted-foreground"> 12%</p>
<div className="text-2xl font-bold">{isNaN(stats.totalUsage) ? 0 : stats.totalUsage}</div>
<p className="text-xs text-muted-foreground">
{(isNaN(stats.totalUsage) ? 0 : stats.totalUsage) > 0 ? "累計使用" : "尚未使用任何應用"}
</p>
</CardContent>
</Card>
@@ -111,8 +257,16 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">9.2</div>
<p className="text-xs text-muted-foreground"></p>
<div className="text-2xl font-bold">
{isNaN(stats.totalDuration) ? "0分鐘" : (
stats.totalDuration >= 60
? `${(stats.totalDuration / 60).toFixed(1)}小時`
: `${stats.totalDuration}分鐘`
)}
</div>
<p className="text-xs text-muted-foreground">
{(isNaN(stats.totalDuration) ? 0 : stats.totalDuration) > 0 ? "累計時長" : "尚未開始使用"}
</p>
</CardContent>
</Card>
@@ -122,8 +276,10 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
<Heart className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">6</div>
<p className="text-xs text-muted-foreground"></p>
<div className="text-2xl font-bold">{isNaN(stats.favoriteApps) ? 0 : stats.favoriteApps}</div>
<p className="text-xs text-muted-foreground">
{(isNaN(stats.favoriteApps) ? 0 : stats.favoriteApps) > 0 ? "個人收藏" : "尚未收藏任何應用"}
</p>
</CardContent>
</Card>
@@ -133,7 +289,10 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">0</div>
<div className="text-2xl font-bold">{isNaN(stats.daysJoined) ? 0 : stats.daysJoined}</div>
<p className="text-xs text-muted-foreground">
{(isNaN(stats.daysJoined) ? 0 : stats.daysJoined) > 0 ? "已加入平台" : "今天剛加入"}
</p>
</CardContent>
</Card>
</div>
@@ -145,18 +304,26 @@ export function ActivityRecordsDialog({ open, onOpenChange }: ActivityRecordsDia
<CardDescription>使</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{categoryData.map((category, index) => (
<div key={index} className="space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<div className={`w-3 h-3 rounded-full ${category.color}`} />
<span className="font-medium">{category.name}</span>
{categoryData.length > 0 ? (
categoryData.map((category, index) => (
<div key={index} className="space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<div className={`w-3 h-3 rounded-full ${category.color}`} />
<span className="font-medium">{category.name}</span>
</div>
<span className="text-muted-foreground">{category.usage}%</span>
</div>
<span className="text-muted-foreground">{category.usage}%</span>
<Progress value={category.usage} className="h-2" />
</div>
<Progress value={category.usage} className="h-2" />
))
) : (
<div className="text-center py-8">
<BarChart3 className="w-12 h-12 text-gray-400 mx-auto mb-3" />
<p className="text-gray-500 text-sm">使</p>
<p className="text-gray-400 text-xs mt-1">使</p>
</div>
))}
)}
</CardContent>
</Card>
</div>

View File

@@ -21,6 +21,8 @@ export function ForgotPasswordDialog({ open, onOpenChange, onBackToLogin }: Forg
const [isLoading, setIsLoading] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [error, setError] = useState("")
const [resetUrl, setResetUrl] = useState("")
const [expiresAt, setExpiresAt] = useState("")
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
@@ -38,59 +40,101 @@ export function ForgotPasswordDialog({ open, onOpenChange, onBackToLogin }: Forg
setIsLoading(true)
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 2000))
try {
const response = await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
})
setIsLoading(false)
setIsSuccess(true)
const data = await response.json()
if (data.success) {
setResetUrl(data.resetUrl)
setExpiresAt(data.expiresAt)
setIsSuccess(true)
} else {
setError(data.error || "生成重設連結失敗")
}
} catch (err) {
setError("生成重設連結時發生錯誤")
} finally {
setIsLoading(false)
}
}
const handleClose = () => {
setEmail("")
setError("")
setIsSuccess(false)
setResetUrl("")
setExpiresAt("")
onOpenChange(false)
}
const handleResend = async () => {
setIsLoading(true)
await new Promise((resolve) => setTimeout(resolve, 1000))
setIsLoading(false)
}
if (isSuccess) {
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<div className="flex flex-col items-center space-y-4">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<DialogTitle className="text-2xl font-bold text-center"></DialogTitle>
<DialogDescription className="text-center"></DialogDescription>
<DialogTitle className="text-2xl font-bold text-center"></DialogTitle>
<DialogDescription className="text-center"></DialogDescription>
</div>
</DialogHeader>
<div className="space-y-4">
<div className="bg-blue-50 p-4 rounded-lg">
<p className="text-sm text-blue-800 mb-2"></p>
<p className="text-sm text-blue-800 mb-2"></p>
<p className="text-sm text-blue-700 font-medium">{email}</p>
</div>
<div className="text-sm text-gray-600 space-y-2">
<p> </p>
<p> 24 </p>
<p> </p>
<div className="space-y-2">
<Label htmlFor="reset-link"></Label>
<div className="flex space-x-2">
<Input
id="reset-link"
value={resetUrl}
readOnly
className="font-mono text-sm"
/>
<Button
onClick={() => {
navigator.clipboard.writeText(resetUrl)
// 可以添加 toast 提示
}}
variant="outline"
size="sm"
>
</Button>
</div>
<p className="text-xs text-gray-500">
{new Date(expiresAt).toLocaleString('zh-TW')}
</p>
</div>
<div className="text-center">
<Button
onClick={() => window.open(resetUrl, '_blank')}
className="w-full"
>
</Button>
</div>
<div className="flex space-x-3">
<Button onClick={onBackToLogin} variant="outline" className="flex-1 bg-transparent">
<Button onClick={onBackToLogin} variant="outline" className="flex-1">
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
<Button onClick={handleResend} disabled={isLoading} className="flex-1">
{isLoading ? "發送中..." : "重新發送"}
<Button onClick={handleClose} className="flex-1">
</Button>
</div>
</div>
@@ -105,7 +149,7 @@ export function ForgotPasswordDialog({ open, onOpenChange, onBackToLogin }: Forg
<DialogHeader>
<DialogTitle className="text-2xl font-bold text-center"></DialogTitle>
<DialogDescription className="text-center">
</DialogDescription>
</DialogHeader>
@@ -138,11 +182,11 @@ export function ForgotPasswordDialog({ open, onOpenChange, onBackToLogin }: Forg
</Button>
<Button type="submit" disabled={isLoading} className="flex-1">
{isLoading ? "發送中..." : "發送重設連結"}
{isLoading ? "生成中..." : "生成重設連結"}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
)
}
}

View File

@@ -107,8 +107,6 @@ export function LoginDialog({ open, onOpenChange, onSwitchToRegister, onSwitchTo
</Alert>
)}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "登入中..." : "登入"}
</Button>

View File

@@ -10,7 +10,7 @@ import { Label } from "@/components/ui/label"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { CheckCircle, AlertTriangle, Loader2 } from "lucide-react"
import { CheckCircle, AlertTriangle, Loader2, Eye, EyeOff, Lock } from "lucide-react"
interface RegisterDialogProps {
open: boolean
@@ -28,6 +28,8 @@ export function RegisterDialog({ open, onOpenChange }: RegisterDialogProps) {
})
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState("")
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const [success, setSuccess] = useState(false)
const departments = ["HQBU", "ITBU", "MBU1", "MBU2", "SBU", "財務部", "人資部", "法務部"]
@@ -86,7 +88,7 @@ export function RegisterDialog({ open, onOpenChange }: RegisterDialogProps) {
<div className="text-center py-6">
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-green-700 mb-2"></h3>
<p className="text-gray-600">使</p>
<p className="text-gray-600"></p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
@@ -140,24 +142,46 @@ export function RegisterDialog({ open, onOpenChange }: RegisterDialogProps) {
<div className="space-y-2">
<Label htmlFor="password"></Label>
<Input
id="password"
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required
/>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
id="password"
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
className="pl-10 pr-10"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword"></Label>
<Input
id="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
required
/>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
value={formData.confirmPassword}
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
className="pl-10 pr-10"
required
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showConfirmPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div className="flex justify-end space-x-3 pt-4">