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

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

@@ -77,17 +77,27 @@ const mockNotifications: Notification[] = []
const mockSearchData: SearchResult[] = []
export function AdminLayout({ children, currentPage, onPageChange }: AdminLayoutProps) {
const { user, logout, isLoading } = useAuth()
// Move ALL hooks to the top, before any conditional logic
const { user, logout, isLoading, isInitialized } = useAuth()
const [sidebarOpen, setSidebarOpen] = useState(true)
const [isClient, setIsClient] = useState(false)
// Search state
const [searchQuery, setSearchQuery] = useState("")
const [searchResults, setSearchResults] = useState<SearchResult[]>([])
const [showSearchResults, setShowSearchResults] = useState(false)
// Notification state
const [notifications, setNotifications] = useState<Notification[]>(mockNotifications)
const [showNotifications, setShowNotifications] = useState(false)
// Logout confirmation state
const [showLogoutDialog, setShowLogoutDialog] = useState(false)
// Set client state after hydration
useEffect(() => {
setIsClient(true)
}, [])
// Handle search
useEffect(() => {
if (searchQuery.trim()) {
@@ -104,46 +114,6 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
}
}, [searchQuery])
// 認證檢查 - moved after all hooks
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<span className="text-gray-600">...</span>
</div>
</div>
)
}
if (!user) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4"></h1>
<p className="text-gray-600 mb-6"></p>
<Button onClick={() => window.location.href = '/'}>
</Button>
</div>
</div>
)
}
if (user.role !== 'admin') {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-4"></h1>
<p className="text-gray-600 mb-6"></p>
<Button onClick={() => window.location.href = '/'}>
</Button>
</div>
</div>
)
}
// Get unread notification count
const unreadCount = notifications.filter((n) => !n.read).length
@@ -207,13 +177,15 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
setShowLogoutDialog(false)
// Check if this is a popup/new tab opened from main site
if (typeof window !== 'undefined' && window.opener && !window.opener.closed) {
// If opened from another window, close this tab and focus parent
window.opener.focus()
window.close()
} else {
// If this is the main window or standalone, redirect to homepage
window.location.href = "/"
if (isClient) {
if (window.opener && !window.opener.closed) {
// If opened from another window, close this tab and focus parent
window.opener.focus()
window.close()
} else {
// If this is the main window or standalone, redirect to homepage
window.location.href = "/"
}
}
}
@@ -236,6 +208,19 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
}
}
// 如果還在載入中,顯示載入畫面
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
<p className="text-gray-600">...</p>
</div>
</div>
)
}
// 檢查用戶權限
if (!user || user.role !== "admin") {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
@@ -246,11 +231,20 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
<div>
<h2 className="text-2xl font-bold text-gray-900 mb-2"></h2>
<p className="text-gray-600 mb-4"></p>
{process.env.NODE_ENV === 'development' && (
<div className="text-xs text-gray-500 mb-4">
調試信息: 用戶={user ? '已登入' : '未登入'}, ={user?.role || '無'}
</div>
)}
<div className="space-x-3">
<Button onClick={() => (window.location.href = "/")} variant="outline">
<Button onClick={() => {
if (isClient) {
window.location.href = "/"
}
}} variant="outline">
</Button>
{typeof window !== 'undefined' && window.opener && !window.opener.closed && (
{isClient && window.opener && !window.opener.closed && (
<Button
onClick={() => {
window.opener.focus()

View File

@@ -15,7 +15,7 @@ export function AdminPanel() {
const renderPage = () => {
switch (currentPage) {
case "dashboard":
return <AdminDashboard onPageChange={setCurrentPage} />
return <AdminDashboard />
case "users":
return <UserManagement />
case "apps":
@@ -27,7 +27,7 @@ export function AdminPanel() {
case "settings":
return <SystemSettings />
default:
return <AdminDashboard onPageChange={setCurrentPage} />
return <AdminDashboard />
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -20,19 +20,9 @@ const recentActivities: any[] = []
const topApps: any[] = []
interface AdminDashboardProps {
onPageChange?: (page: string) => void
}
export function AdminDashboard({ onPageChange }: AdminDashboardProps) {
export function AdminDashboard() {
const { competitions } = useCompetition()
const handleManageUsers = () => {
if (onPageChange) {
onPageChange("users")
}
}
return (
<div className="space-y-6">
{/* Welcome Section */}
@@ -160,7 +150,7 @@ export function AdminDashboard({ onPageChange }: AdminDashboardProps) {
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Button className="h-20 flex flex-col space-y-2" onClick={handleManageUsers}>
<Button className="h-20 flex flex-col space-y-2">
<Users className="w-6 h-6" />
<span></span>
</Button>

View File

@@ -18,6 +18,9 @@ import {
Users,
Bell,
Save,
Eye,
EyeOff,
Lock,
TestTube,
CheckCircle,
HardDrive,
@@ -67,6 +70,7 @@ export function SystemSettings() {
const [activeTab, setActiveTab] = useState("general")
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle")
const [showSmtpPassword, setShowSmtpPassword] = useState(false)
const handleSave = async () => {
setSaveStatus("saving")
@@ -78,8 +82,8 @@ export function SystemSettings() {
}
const handleTestEmail = () => {
// 測試郵件功能 - 僅用於開發測試
console.log("測試郵件功能")
// 測試郵件功能
alert("測試郵件已發送!")
}
const updateSetting = (key: string, value: any) => {
@@ -286,12 +290,23 @@ export function SystemSettings() {
</div>
<div className="space-y-2">
<Label htmlFor="smtpPassword">SMTP </Label>
<Input
id="smtpPassword"
type="password"
value={settings.smtpPassword}
onChange={(e) => updateSetting("smtpPassword", e.target.value)}
/>
<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="smtpPassword"
type={showSmtpPassword ? "text" : "password"}
value={settings.smtpPassword}
onChange={(e) => updateSetting("smtpPassword", e.target.value)}
className="pl-10 pr-10"
/>
<button
type="button"
onClick={() => setShowSmtpPassword(!showSmtpPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showSmtpPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
</div>

View File

@@ -13,7 +13,6 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Label } from "@/components/ui/label"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination"
import {
Search,
MoreHorizontal,
@@ -39,11 +38,8 @@ import {
Users,
} from "lucide-react"
// User data - empty for production
const initialMockUsers: any[] = []
export function UserManagement() {
const [users, setUsers] = useState(initialMockUsers)
const [users, setUsers] = useState<any[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [selectedDepartment, setSelectedDepartment] = useState("all")
const [selectedRole, setSelectedRole] = useState("all")
@@ -53,94 +49,70 @@ export function UserManagement() {
const [showInviteUser, setShowInviteUser] = useState(false)
const [showEditUser, setShowEditUser] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [isClient, setIsClient] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [stats, setStats] = useState({
totalUsers: 0,
activeUsers: 0,
adminCount: 0,
developerCount: 0,
inactiveUsers: 0,
newThisMonth: 0
})
const [pagination, setPagination] = useState({
page: 1,
limit: 10,
total: 0,
totalPages: 0
})
// Set client state after hydration
useEffect(() => {
setIsClient(true)
}, [])
// 載入用戶數據
const loadUsers = async () => {
if (!isClient) return
setIsLoading(true)
try {
const params = new URLSearchParams({
page: pagination.page.toString(),
limit: pagination.limit.toString(),
search: searchTerm,
department: selectedDepartment,
role: selectedRole,
status: selectedStatus
})
const response = await fetch(`/api/admin/users?${params}`)
const data = await response.json()
if (data.success) {
setUsers(data.data.users)
setStats(data.data.stats)
setPagination(data.data.pagination)
} else {
console.error('載入用戶失敗:', data.error)
}
} catch (error) {
console.error('載入用戶錯誤:', error)
} finally {
setIsLoading(false)
}
}
// 當篩選條件改變時重新載入
useEffect(() => {
loadUsers()
}, [isClient, searchTerm, selectedDepartment, selectedRole, selectedStatus, pagination.page])
const [showInvitationLink, setShowInvitationLink] = useState(false)
const [userToDelete, setUserToDelete] = useState<any>(null)
const [generatedInvitation, setGeneratedInvitation] = useState<any>(null)
const [isLoading, setIsLoading] = useState(false)
const [success, setSuccess] = useState("")
const [error, setError] = useState("")
const [stats, setStats] = useState({
total: 0,
admin: 0,
developer: 0,
user: 0,
today: 0,
totalApps: 0,
totalReviews: 0
})
// Pagination state
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [totalUsers, setTotalUsers] = useState(0)
const [itemsPerPage] = useState(10) // Default to 10 items per page
// 載入用戶資料
useEffect(() => {
const fetchUsers = async () => {
try {
setIsLoading(true)
// 獲取用戶列表 with pagination
const usersResponse = await fetch(`/api/users?page=${currentPage}&limit=${itemsPerPage}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
if (usersResponse.ok) {
const usersData = await usersResponse.json()
setUsers(usersData.users || [])
setTotalPages(usersData.pagination?.totalPages || 1)
setTotalUsers(usersData.pagination?.total || 0)
} else {
const errorData = await usersResponse.json().catch(() => ({}))
console.error('獲取用戶列表失敗:', errorData.error || usersResponse.statusText)
setError(errorData.error || '獲取用戶列表失敗')
}
// 獲取統計資料
const statsResponse = await fetch('/api/users/stats', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
if (statsResponse.ok) {
const statsData = await statsResponse.json()
setStats(statsData)
} else {
const errorData = await statsResponse.json().catch(() => ({}))
console.error('獲取統計資料失敗:', errorData.error || statsResponse.statusText)
}
} catch (error) {
console.error('載入用戶資料失敗:', error)
setError('載入用戶資料失敗')
} finally {
setIsLoading(false)
}
}
fetchUsers()
}, [currentPage, itemsPerPage]) // Re-fetch when page changes
// 重新獲取統計數據的函數
const refreshStats = async () => {
try {
const statsResponse = await fetch('/api/users/stats', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
if (statsResponse.ok) {
const statsData = await statsResponse.json()
setStats(statsData)
}
} catch (error) {
console.error('重新獲取統計資料失敗:', error)
}
}
// 邀請用戶表單狀態 - 包含電子郵件和預設角色
const [inviteEmail, setInviteEmail] = useState("")
@@ -156,65 +128,11 @@ export function UserManagement() {
status: "",
})
const filteredUsers = users.filter((user) => {
const matchesSearch =
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase())
const matchesDepartment = selectedDepartment === "all" || user.department === selectedDepartment
const matchesRole =
selectedRole === "all" ||
user.role === selectedRole ||
(user.status === "invited" && (user as any).invitedRole === selectedRole)
const matchesStatus = selectedStatus === "all" || user.status === selectedStatus
// 篩選現在由 API 處理,不需要前端篩選
return matchesSearch && matchesDepartment && matchesRole && matchesStatus
})
const handleViewUser = async (user: any) => {
setIsLoading(true)
try {
const response = await fetch(`/api/users/${user.id}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
if (response.ok) {
const userData = await response.json()
// 獲取用戶活動記錄
const activityResponse = await fetch(`/api/users/${user.id}/activity`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
let activityData = []
if (activityResponse.ok) {
activityData = await activityResponse.json()
}
// 合併用戶資料和活動記錄
const userWithActivity = {
...userData,
activities: activityData
}
setSelectedUser(userWithActivity)
setShowUserDetail(true)
} else {
const errorData = await response.json()
setError(errorData.error || "獲取用戶詳情失敗")
setTimeout(() => setError(""), 3000)
}
} catch (error) {
console.error('Error fetching user details:', error)
setError("獲取用戶詳情失敗")
setTimeout(() => setError(""), 3000)
} finally {
setIsLoading(false)
}
const handleViewUser = (user: any) => {
setSelectedUser(user)
setShowUserDetail(true)
}
const handleEditUser = (user: any) => {
@@ -237,39 +155,18 @@ export function UserManagement() {
const handleToggleUserStatus = async (userId: string) => {
setIsLoading(true)
try {
const newStatus = users.find(user => user.id === userId)?.status === "active" ? "inactive" : "active"
const response = await fetch(`/api/users/${userId}/status`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ status: newStatus })
})
// 模擬 API 調用
await new Promise((resolve) => setTimeout(resolve, 1000))
if (response.ok) {
setUsers(
users.map((user) =>
user.id === userId ? { ...user, status: newStatus } : user,
),
)
setSuccess("用戶狀態更新成功!")
setTimeout(() => setSuccess(""), 3000)
refreshStats() // 更新統計數據
} else {
const errorData = await response.json()
setError(errorData.error || "更新用戶狀態失敗")
setTimeout(() => setError(""), 3000)
}
} catch (error) {
console.error('Error updating user status:', error)
setError("更新用戶狀態失敗")
setTimeout(() => setError(""), 3000)
} finally {
setIsLoading(false)
}
setUsers(
users.map((user) =>
user.id === userId ? { ...user, status: user.status === "active" ? "inactive" : "active" } : user,
),
)
setIsLoading(false)
setSuccess("用戶狀態更新成功!")
setTimeout(() => setSuccess(""), 3000)
}
const handleChangeUserRole = async (userId: string, newRole: string) => {
@@ -283,7 +180,6 @@ export function UserManagement() {
setIsLoading(false)
setSuccess(`用戶權限已更新為${getRoleText(newRole)}`)
setTimeout(() => setSuccess(""), 3000)
refreshStats() // 更新統計數據
}
const handleGenerateInvitation = async () => {
@@ -310,43 +206,62 @@ export function UserManagement() {
setIsLoading(true)
// 模擬生成邀請連結
await new Promise((resolve) => setTimeout(resolve, 1500))
try {
const response = await fetch('/api/admin/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: inviteEmail,
role: inviteRole
})
})
// 生成邀請 token實際應用中會由後端生成
const invitationToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
const invitationLink = `${window.location.origin}/register?token=${invitationToken}&email=${encodeURIComponent(inviteEmail)}&role=${inviteRole}`
const data = await response.json()
const newInvitation = {
id: Date.now().toString(),
name: "",
email: inviteEmail,
department: "",
role: "",
status: "invited",
joinDate: "",
lastLogin: "",
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
invitationSentAt: new Date().toLocaleString("zh-TW", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}),
invitationLink: invitationLink,
invitedRole: inviteRole, // 記錄邀請時的預設角色
if (data.success) {
const newInvitation = {
id: Date.now().toString(),
name: "",
email: inviteEmail,
department: "",
role: "",
status: "invited",
joinDate: "",
lastLogin: "",
totalApps: 0,
totalReviews: 0,
totalLikes: 0,
invitationSentAt: new Date().toLocaleString("zh-TW", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}),
invitationLink: data.data.invitationLink,
invitedRole: inviteRole, // 記錄邀請時的預設角色
}
setGeneratedInvitation(newInvitation)
setInviteEmail("")
setInviteRole("user")
setShowInviteUser(false)
setShowInvitationLink(true)
setSuccess("邀請連結已生成")
// 重新載入用戶列表
loadUsers()
} else {
setError(data.error || "生成邀請連結失敗")
}
} catch (error) {
console.error('邀請用戶錯誤:', error)
setError("生成邀請連結時發生錯誤")
} finally {
setIsLoading(false)
}
setUsers([...users, newInvitation])
setGeneratedInvitation(newInvitation)
setInviteEmail("")
setInviteRole("user")
setIsLoading(false)
setShowInviteUser(false)
setShowInvitationLink(true)
}
const handleCopyInvitationLink = async (link: string) => {
@@ -369,7 +284,9 @@ export function UserManagement() {
const newToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
const user = users.find((u) => u.id === userId)
const role = (user as any)?.invitedRole || "user"
const newInvitationLink = `${window.location.origin}/register?token=${newToken}&email=${encodeURIComponent(email)}&role=${role}`
const newInvitationLink = isClient
? `${window.location.origin}/register?token=${newToken}&email=${encodeURIComponent(email)}&role=${role}`
: `/register?token=${newToken}&email=${encodeURIComponent(email)}&role=${role}`
setUsers(
users.map((user) =>
@@ -402,42 +319,23 @@ export function UserManagement() {
return
}
// 檢查電子郵件是否被其他用戶使用
if (users.some((user) => user.email === editUser.email && user.id !== editUser.id)) {
setError("此電子郵件已被其他用戶使用")
return
}
setIsLoading(true)
try {
const response = await fetch(`/api/users/${editUser.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
name: editUser.name,
email: editUser.email,
department: editUser.department,
role: editUser.role
})
})
// 模擬 API 調用
await new Promise((resolve) => setTimeout(resolve, 1500))
if (response.ok) {
const result = await response.json()
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
setShowEditUser(false)
setSuccess("用戶資料更新成功!")
setTimeout(() => setSuccess(""), 3000)
refreshStats() // 更新統計數據
} else {
const errorData = await response.json()
setError(errorData.error || "更新用戶資料失敗")
setTimeout(() => setError(""), 3000)
}
} catch (error) {
console.error('Error updating user:', error)
setError("更新用戶資料失敗")
setTimeout(() => setError(""), 3000)
} finally {
setIsLoading(false)
}
setUsers(users.map((user) => (user.id === editUser.id ? { ...user, ...editUser } : user)))
setIsLoading(false)
setShowEditUser(false)
setSuccess("用戶資料更新成功!")
setTimeout(() => setSuccess(""), 3000)
}
const confirmDeleteUser = async () => {
@@ -445,33 +343,16 @@ export function UserManagement() {
setIsLoading(true)
try {
const response = await fetch(`/api/users/${userToDelete.id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
// 模擬 API 調用
await new Promise((resolve) => setTimeout(resolve, 1500))
if (response.ok) {
setUsers(users.filter((user) => user.id !== userToDelete.id))
setShowDeleteConfirm(false)
setUserToDelete(null)
setSuccess("用戶刪除成功!")
setTimeout(() => setSuccess(""), 3000)
refreshStats() // 更新統計數據
} else {
const errorData = await response.json()
setError(errorData.error || "刪除用戶失敗")
setTimeout(() => setError(""), 3000)
}
} catch (error) {
console.error('Error deleting user:', error)
setError("刪除用戶失敗")
setTimeout(() => setError(""), 3000)
} finally {
setIsLoading(false)
}
setUsers(users.filter((user) => user.id !== userToDelete.id))
setIsLoading(false)
setShowDeleteConfirm(false)
setUserToDelete(null)
setSuccess("用戶刪除成功!")
setTimeout(() => setSuccess(""), 3000)
}
const getRoleColor = (role: string) => {
@@ -572,13 +453,13 @@ export function UserManagement() {
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-8 gap-4">
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.total}</p>
<p className="text-2xl font-bold">{stats.totalUsers}</p>
</div>
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="w-4 h-4 text-blue-600" />
@@ -592,7 +473,7 @@ export function UserManagement() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.total}</p>
<p className="text-2xl font-bold">{stats.activeUsers}</p>
</div>
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<Activity className="w-4 h-4 text-green-600" />
@@ -606,7 +487,7 @@ export function UserManagement() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.admin}</p>
<p className="text-2xl font-bold">{stats.adminCount}</p>
</div>
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
<Shield className="w-4 h-4 text-purple-600" />
@@ -620,7 +501,7 @@ export function UserManagement() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.developer}</p>
<p className="text-2xl font-bold">{stats.developerCount}</p>
</div>
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<Code className="w-4 h-4 text-green-600" />
@@ -633,11 +514,11 @@ export function UserManagement() {
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.user}</p>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.inactiveUsers}</p>
</div>
<div className="w-8 h-8 bg-yellow-100 rounded-full flex items-center justify-center">
<User className="w-4 h-4 text-yellow-600" />
<Clock className="w-4 h-4 text-yellow-600" />
</div>
</div>
</CardContent>
@@ -647,8 +528,8 @@ export function UserManagement() {
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.today}</p>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.newThisMonth}</p>
</div>
<div className="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center">
<UserPlus className="w-4 h-4 text-orange-600" />
@@ -656,34 +537,6 @@ export function UserManagement() {
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.totalApps}</p>
</div>
<div className="w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
<Code className="w-4 h-4 text-indigo-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600"></p>
<p className="text-2xl font-bold">{stats.totalReviews}</p>
</div>
<div className="w-8 h-8 bg-pink-100 rounded-full flex items-center justify-center">
<Activity className="w-4 h-4 text-pink-600" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Filters */}
@@ -744,40 +597,25 @@ export function UserManagement() {
{/* Users Table */}
<Card>
<CardHeader>
<CardTitle> ({totalUsers} )</CardTitle>
<CardDescription>
- {currentPage} {totalPages}
{totalPages > 1 && ` (每頁 ${itemsPerPage} 筆)`}
</CardDescription>
<CardTitle> ({pagination.total})</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
<span className="ml-2 text-gray-600">...</span>
</div>
) : filteredUsers.length === 0 ? (
<div className="text-center py-8">
<Users className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600"></p>
<p className="text-sm text-gray-500 mt-1">調</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredUsers.map((user) => (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>
<div className="flex items-center space-x-3">
@@ -868,70 +706,9 @@ export function UserManagement() {
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center items-center py-4">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault()
if (currentPage > 1) setCurrentPage(currentPage - 1)
}}
className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
{/* Page numbers */}
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let pageNum
if (totalPages <= 5) {
pageNum = i + 1
} else if (currentPage <= 3) {
pageNum = i + 1
} else if (currentPage >= totalPages - 2) {
pageNum = totalPages - 4 + i
} else {
pageNum = currentPage - 2 + i
}
return (
<PaginationItem key={pageNum}>
<PaginationLink
href="#"
onClick={(e) => {
e.preventDefault()
setCurrentPage(pageNum)
}}
isActive={currentPage === pageNum}
>
{pageNum}
</PaginationLink>
</PaginationItem>
)
})}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault()
if (currentPage < totalPages) setCurrentPage(currentPage + 1)
}}
className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
{/* Invite User Dialog - 包含角色選擇 */}
<Dialog open={showInviteUser} onOpenChange={setShowInviteUser}>
<DialogContent className="max-w-md">
@@ -1106,7 +883,7 @@ export function UserManagement() {
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => window.open(generatedInvitation.invitationLink, "_blank")}>
<Button variant="outline" onClick={() => isClient && window.open(generatedInvitation.invitationLink, "_blank")}>
<ExternalLink className="w-4 h-4 mr-2" />
</Button>
@@ -1362,32 +1139,22 @@ export function UserManagement() {
<Clock className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500"></p>
</div>
) : selectedUser.activities && selectedUser.activities.length > 0 ? (
<div className="space-y-3">
{selectedUser.activities.map((activity: any, index: number) => (
<div key={index} className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
{activity.type === 'login' ? (
<Calendar className="w-4 h-4 text-blue-600" />
) : activity.type === 'view_app' ? (
<Eye className="w-4 h-4 text-green-600" />
) : activity.type === 'create_app' ? (
<Code className="w-4 h-4 text-purple-600" />
) : activity.type === 'review' ? (
<Activity className="w-4 h-4 text-orange-600" />
) : (
<Activity className="w-4 h-4 text-gray-600" />
)}
<div>
<p className="text-sm font-medium">{activity.description}</p>
<p className="text-xs text-gray-500">{activity.timestamp}</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<Activity className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500"></p>
<div className="space-y-3">
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<Calendar className="w-4 h-4 text-blue-600" />
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-gray-500">2024-01-20 16:45</p>
</div>
</div>
<div className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg">
<Eye className="w-4 h-4 text-green-600" />
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-gray-500">2024-01-20 15:30</p>
</div>
</div>
</div>
)}
</TabsContent>
@@ -1422,7 +1189,7 @@ export function UserManagement() {
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-orange-600">
{selectedUser.status === "invited" ? 0 : (selectedUser.loginDays || 0)}
{selectedUser.status === "invited" ? 0 : 15}
</p>
<p className="text-sm text-gray-600"></p>
</div>

View File

@@ -121,107 +121,33 @@ export function AppSubmissionDialog({ open, onOpenChange }: AppSubmissionDialogP
}
const handleSubmit = async () => {
if (!user) {
console.error('用戶未登入')
return
}
setIsSubmitting(true)
try {
// 準備應用程式資料
const appData = {
name: formData.name,
description: formData.description,
type: mapTypeToApiType(formData.type),
demoUrl: formData.appUrl || undefined,
githubUrl: formData.sourceCodeUrl || undefined,
docsUrl: formData.documentation || undefined,
techStack: formData.technicalDetails ? [formData.technicalDetails] : undefined,
tags: formData.features ? [formData.features] : undefined,
version: '1.0.0'
}
// 模擬提交過程
await new Promise((resolve) => setTimeout(resolve, 2000))
// 調用 API 創建應用程式
const token = localStorage.getItem('token')
const response = await fetch('/api/apps', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(appData)
setIsSubmitting(false)
setIsSubmitted(true)
// 3秒後關閉對話框
setTimeout(() => {
onOpenChange(false)
setIsSubmitted(false)
setStep(1)
setFormData({
name: "",
type: "文字處理",
description: "",
appUrl: "",
demoFile: null,
sourceCodeUrl: "",
documentation: "",
features: "",
technicalDetails: "",
requestFeatured: false,
agreeTerms: false,
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || '創建應用程式失敗')
}
const result = await response.json()
console.log('應用程式創建成功:', result)
setIsSubmitting(false)
setIsSubmitted(true)
// 3秒後關閉對話框
setTimeout(() => {
onOpenChange(false)
setIsSubmitted(false)
setStep(1)
setFormData({
name: "",
type: "文字處理",
description: "",
appUrl: "",
demoFile: null,
sourceCodeUrl: "",
documentation: "",
features: "",
technicalDetails: "",
requestFeatured: false,
agreeTerms: false,
})
}, 3000)
} catch (error) {
console.error('創建應用程式失敗:', error)
setIsSubmitting(false)
// 這裡可以添加錯誤提示
alert(`創建應用程式失敗: ${error instanceof Error ? error.message : '未知錯誤'}`)
}
}
// 將前端類型映射到 API 類型
const mapTypeToApiType = (frontendType: string): string => {
const typeMap: Record<string, string> = {
'文字處理': 'productivity',
'圖像生成': 'ai_model',
'圖像處理': 'ai_model',
'語音辨識': 'ai_model',
'推薦系統': 'ai_model',
'音樂生成': 'ai_model',
'程式開發': 'automation',
'影像處理': 'ai_model',
'對話系統': 'ai_model',
'數據分析': 'data_analysis',
'設計工具': 'productivity',
'語音技術': 'ai_model',
'教育工具': 'educational',
'健康醫療': 'healthcare',
'金融科技': 'finance',
'物聯網': 'iot_device',
'區塊鏈': 'blockchain',
'AR/VR': 'ar_vr',
'機器學習': 'machine_learning',
'電腦視覺': 'computer_vision',
'自然語言處理': 'nlp',
'機器人': 'robotics',
'網路安全': 'cybersecurity',
'雲端服務': 'cloud_service',
'其他': 'other'
}
return typeMap[frontendType] || 'other'
}, 3000)
}
const isStep1Valid = formData.name && formData.description && formData.appUrl
@@ -319,28 +245,9 @@ export function AppSubmissionDialog({ open, onOpenChange }: AppSubmissionDialogP
<SelectContent>
<SelectItem value="文字處理"></SelectItem>
<SelectItem value="圖像生成"></SelectItem>
<SelectItem value="圖像處理"></SelectItem>
<SelectItem value="語音辨識"></SelectItem>
<SelectItem value="推薦系統"></SelectItem>
<SelectItem value="音樂生成"></SelectItem>
<SelectItem value="程式開發"></SelectItem>
<SelectItem value="影像處理"></SelectItem>
<SelectItem value="對話系統"></SelectItem>
<SelectItem value="數據分析"></SelectItem>
<SelectItem value="設計工具"></SelectItem>
<SelectItem value="語音技術"></SelectItem>
<SelectItem value="教育工具"></SelectItem>
<SelectItem value="健康醫療"></SelectItem>
<SelectItem value="金融科技"></SelectItem>
<SelectItem value="物聯網"></SelectItem>
<SelectItem value="區塊鏈"></SelectItem>
<SelectItem value="AR/VR">AR/VR</SelectItem>
<SelectItem value="機器學習"></SelectItem>
<SelectItem value="電腦視覺"></SelectItem>
<SelectItem value="自然語言處理"></SelectItem>
<SelectItem value="機器人"></SelectItem>
<SelectItem value="網路安全"></SelectItem>
<SelectItem value="雲端服務"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>

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">

View File

@@ -24,7 +24,7 @@ interface Message {
quickQuestions?: string[]
}
const DEEPSEEK_API_KEY = process.env.NEXT_PUBLIC_DEEPSEEK_API_KEY || ""
const DEEPSEEK_API_KEY = process.env.NEXT_PUBLIC_DEEPSEEK_API_KEY || "sk-3640dcff23fe4a069a64f536ac538d75"
const DEEPSEEK_API_URL = process.env.NEXT_PUBLIC_DEEPSEEK_API_URL || "https://api.deepseek.com/v1/chat/completions"
const systemPrompt = `你是一個競賽管理系統的AI助手專門幫助用戶了解如何使用這個系統。