Files
chinese-family-tree-2/components/tree/collaborator-dialog.tsx
T
freedakgmail bade25ff26 0.0.1.2
2025-11-23 09:13:25 +08:00

264 lines
9.1 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 { useSession } from "next-auth/react"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Users, Mail, Trash2, Plus, Shield, Eye, Edit } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { useFamily } from "@/context/family-context"
import { useDialog } from "@/components/ui/alert-dialog-custom"
interface Collaborator {
id: string
userId: string
role: "EDITOR" | "VIEWER"
invitedAt: string
user: {
id: string
name: string | null
email: string
avatar: string | null
}
}
export function CollaboratorDialog() {
const { data: session } = useSession()
const { currentTree } = useFamily()
const { showConfirm, showAlert } = useDialog()
const [open, setOpen] = useState(false)
const [collaborators, setCollaborators] = useState<Collaborator[]>([])
const [isInviting, setIsInviting] = useState(false)
const [email, setEmail] = useState("")
const [role, setRole] = useState<"EDITOR" | "VIEWER">("VIEWER")
const [password, setPassword] = useState("")
const [error, setError] = useState("")
const [success, setSuccess] = useState("")
// 加载协作者
useEffect(() => {
if (open && currentTree?.id) {
loadCollaborators()
}
}, [open, currentTree?.id])
const loadCollaborators = async () => {
if (!currentTree?.id) return
try {
const res = await fetch(`/api/trees/${currentTree.id}/collaborators`)
if (res.ok) {
const data = await res.json()
setCollaborators(data.collaborators)
}
} catch (err) {
console.error("加载协作者失败:", err)
}
}
const handleInvite = async (e: React.FormEvent) => {
e.preventDefault()
if (!currentTree?.id) return
setIsInviting(true)
setError("")
setSuccess("")
try {
const res = await fetch(`/api/trees/${currentTree.id}/invite`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
role,
password: password || undefined,
}),
})
const data = await res.json()
if (!res.ok) {
throw new Error(data.error || "邀请失败")
}
setSuccess(data.message)
setEmail("")
setPassword("")
loadCollaborators()
} catch (err: any) {
setError(err.message)
} finally {
setIsInviting(false)
}
}
const handleRemove = async (userId: string) => {
if (!currentTree?.id) return
const confirmed = await showConfirm("确定要移除此协作者吗?", "移除协作者")
if (!confirmed) return
try {
const res = await fetch(`/api/trees/${currentTree.id}/collaborators?userId=${userId}`, {
method: 'DELETE',
})
if (!res.ok) throw new Error('移除失败')
loadCollaborators()
} catch (err) {
console.error(err)
await showAlert("移除失败", "错误")
}
}
if (!currentTree) return null
if (currentTree.ownerId !== session?.user?.id) return null
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" className="gap-2">
<Users className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle> - {currentTree.name}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* 邀请表单 */}
<form onSubmit={handleInvite} className="space-y-4 border-b pb-6">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
placeholder="输入对方邮箱"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="role"></Label>
<Select value={role} onValueChange={(v: any) => setRole(v)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="EDITOR">
<div className="flex items-center">
<Edit className="mr-2 h-4 w-4" />
()
</div>
</SelectItem>
<SelectItem value="VIEWER">
<div className="flex items-center">
<Eye className="mr-2 h-4 w-4" />
()
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2 sm:col-span-2">
<Label htmlFor="password"> ()</Label>
<div className="flex gap-2">
<Input
id="password"
type="text"
placeholder="为新用户设置密码(可选)"
value={password}
onChange={(e) => setPassword(e.target.value)}
minLength={6}
className="flex-1"
/>
<Button type="submit" disabled={isInviting}>
{isInviting ? "处理中..." : "发送邀请"}
</Button>
</div>
<p className="text-xs text-muted-foreground"></p>
</div>
</div>
{error && (
<Alert variant="destructive">
<AlertTitle></AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert className="bg-green-50 text-green-700 border-green-200">
<AlertTitle></AlertTitle>
<AlertDescription>{success}</AlertDescription>
</Alert>
)}
</form>
{/* 协作者列表 */}
<div className="space-y-4">
<h3 className="font-medium text-sm text-muted-foreground"></h3>
{collaborators.length === 0 ? (
<div className="text-center py-8 text-muted-foreground border border-dashed rounded-lg">
</div>
) : (
<div className="space-y-2 max-h-[300px] overflow-y-auto pr-2">
{collaborators.map((collab) => (
<div
key={collab.id}
className="flex items-center justify-between p-3 rounded-lg border bg-muted/30"
>
<div className="flex items-center gap-3">
<Avatar className="h-9 w-9 border border-border">
<AvatarFallback>
{collab.user.name?.charAt(0).toUpperCase() || collab.user.email.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium text-sm">{collab.user.name || "用户"}</div>
<div className="text-xs text-muted-foreground">{collab.user.email}</div>
</div>
</div>
<div className="flex items-center gap-3">
<span className={`text-[10px] px-2 py-0.5 rounded-full border ${
collab.role === 'EDITOR'
? 'bg-blue-50 text-blue-700 border-blue-200'
: 'bg-gray-50 text-gray-700 border-gray-200'
}`}>
{collab.role === 'EDITOR' ? '编辑者' : '查看者'}
</span>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-destructive"
onClick={() => handleRemove(collab.userId)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
)
}