Files
ai-showcase-platform/components/auth/profile-dialog.tsx
2025-09-21 22:11:20 +08:00

271 lines
9.0 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 { useAuth } from "@/contexts/auth-context"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Badge } from "@/components/ui/badge"
import { CheckCircle, AlertTriangle, Loader2, User, Camera } from "lucide-react"
interface ProfileDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function ProfileDialog({ open, onOpenChange }: ProfileDialogProps) {
const { user, updateProfile } = useAuth()
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState("")
const [success, setSuccess] = useState("")
const [profileData, setProfileData] = useState({
name: user?.name || "",
email: user?.email || "",
department: user?.department || "",
bio: user?.bio || "",
phone: user?.phone || "",
location: user?.location || "",
})
const [avatar, setAvatar] = useState(user?.avatar || "")
// 監聽用戶狀態變化,同步更新本地狀態
useEffect(() => {
if (user) {
setProfileData({
name: user.name || "",
email: user.email || "",
department: user.department || "",
bio: user.bio || "",
phone: user.phone || "",
location: user.location || "",
})
setAvatar(user.avatar || "")
}
}, [user])
const departments = ["HQBU", "ITBU", "MBU1", "MBU2", "SBU", "財務部", "人資部", "法務部"]
const handleSave = async () => {
setError("")
setSuccess("")
setIsLoading(true)
try {
await updateProfile({ ...profileData, avatar })
setSuccess("個人資料更新成功!")
setTimeout(() => setSuccess(""), 3000)
} catch (err) {
setError("更新失敗,請稍後再試")
} finally {
setIsLoading(false)
}
}
const handleAvatarFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
// 驗證文件類型
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
setError('只支援 JPEG、PNG、WebP 格式的圖片');
return;
}
// 驗證文件大小(限制 5MB
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
setError('圖片大小不能超過 5MB');
return;
}
setIsLoading(true);
setError('');
setSuccess('');
try {
const formData = new FormData();
formData.append('avatar', file);
formData.append('userId', user?.id || '');
const response = await fetch('/api/upload/avatar', {
method: 'POST',
body: formData,
});
const result = await response.json();
if (result.success) {
setSuccess('頭像上傳成功!');
// 更新全局用戶狀態
await updateProfile({ avatar: result.data.imageUrl });
setTimeout(() => setSuccess(''), 3000);
} else {
setError(result.error || '上傳失敗');
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : '上傳失敗';
setError(`頭像上傳失敗:${errorMsg}`);
} finally {
setIsLoading(false);
// 清空文件輸入
event.target.value = '';
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<User className="w-5 h-5" />
<span></span>
</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-6">
{error && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert className="border-green-200 bg-green-50">
<CheckCircle className="h-4 w-4 text-green-600" />
<AlertDescription className="text-green-800">{success}</AlertDescription>
</Alert>
)}
<div className="flex items-center space-x-6">
<div className="relative inline-block">
<Avatar className="w-24 h-24">
<AvatarImage src={user?.avatar || avatar} />
<AvatarFallback className="text-2xl bg-gradient-to-r from-blue-600 to-purple-600 text-white">
{user?.name?.charAt(0) || "U"}
</AvatarFallback>
</Avatar>
<Button
size="sm"
variant="outline"
className="absolute -bottom-1 -right-1 rounded-full w-8 h-8 p-0 bg-white/80 hover:bg-white/90 border-2 border-white/50 shadow-md backdrop-blur-sm"
onClick={() => document.getElementById('avatar-upload')?.click()}
>
<Camera className="w-4 h-4" />
</Button>
<input
id="avatar-upload"
type="file"
accept="image/jpeg,image/jpg,image/png,image/webp"
className="hidden"
onChange={handleAvatarFileSelect}
/>
</div>
<div>
<h3 className="text-xl font-semibold">{user?.name}</h3>
<p className="text-gray-600">{user?.email}</p>
<Badge variant="outline" className="mt-2">
{user?.department}
</Badge>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input
id="name"
value={profileData.name}
onChange={(e) => setProfileData({ ...profileData, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
value={profileData.email}
onChange={(e) => setProfileData({ ...profileData, email: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="department"></Label>
<Select
value={profileData.department}
onValueChange={(value) => setProfileData({ ...profileData, department: value })}
>
<SelectTrigger>
<SelectValue placeholder="選擇部門" />
</SelectTrigger>
<SelectContent>
{departments.map((dept) => (
<SelectItem key={dept} value={dept}>
{dept}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input
id="phone"
value={profileData.phone}
onChange={(e) => setProfileData({ ...profileData, phone: e.target.value })}
placeholder="輸入電話號碼"
/>
</div>
<div className="space-y-2">
<Label htmlFor="location"></Label>
<Input
id="location"
value={profileData.location}
onChange={(e) => setProfileData({ ...profileData, location: e.target.value })}
placeholder="輸入工作地點"
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="bio"></Label>
<Input
id="bio"
value={profileData.bio}
onChange={(e) => setProfileData({ ...profileData, bio: e.target.value })}
placeholder="簡單介紹一下自己..."
/>
</div>
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSave} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
"儲存變更"
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}