| // src/components/project/ProjectMembersPanel.tsx | |
| import { useState } from "react"; | |
| import { useProjectSpace } from "@/hooks/useProjectSpace"; | |
| import type { Project } from "@/types/project"; | |
| import { Plus, Trash2, ChevronDown, User, Shield } from "lucide-react"; | |
| interface ProjectMembersPanelProps { | |
| projectId: string; | |
| project: Project; | |
| } | |
| const ROLE_OPTIONS = [ | |
| { value: "editor", label: "Editor" }, | |
| { value: "commenter", label: "Commenter" }, | |
| { value: "viewer", label: "Viewer" }, | |
| ]; | |
| export function ProjectMembersPanel({ projectId, project }: ProjectMembersPanelProps) { | |
| const { addMember, updateMemberRole, removeMember } = useProjectSpace(projectId); | |
| const [newUsername, setNewUsername] = useState(""); | |
| const [newRole, setNewRole] = useState<string>("editor"); | |
| const handleAdd = () => { | |
| if (!newUsername.trim()) return; | |
| addMember(newUsername.trim(), newRole as any); | |
| setNewUsername(""); | |
| }; | |
| return ( | |
| <div className="space-y-4"> | |
| <h3 className="font-bold text-lg">Team & Permissions</h3> | |
| <div className="flex gap-2 mb-4"> | |
| <input | |
| type="text" | |
| value={newUsername} | |
| onChange={e => setNewUsername(e.target.value)} | |
| placeholder="Username" | |
| className="input flex-1 text-sm" | |
| /> | |
| <select aria-label="Select option" value={newRole} onChange={e => setNewRole(e.target.value)} className="input text-sm w-28"> | |
| {ROLE_OPTIONS.map(r => <option key={r.value} value={r.value}>{r.label}</option>)} | |
| </select> | |
| <button onClick={handleAdd} className="btn btn-primary btn-sm gap-1"><Plus className="w-4 h-4"/> Add</button> | |
| </div> | |
| <div className="space-y-2"> | |
| {project.members.map(member => ( | |
| <div key={member.userId} className="flex items-center justify-between p-3 rounded-xl bg-white/5"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-8 h-8 rounded-full bg-linear-to-br from-primary to-secondary text-white flex items-center justify-center font-bold text-xs"> | |
| {member.fullName?.[0]?.toUpperCase()} | |
| </div> | |
| <div> | |
| <p className="font-semibold text-sm">{member.fullName || member.username}</p> | |
| <p className="text-xs text-gray-500">@{member.username}</p> | |
| </div> | |
| {member.role === "owner" && ( | |
| <span className="text-[10px] bg-amber-100 dark:bg-amber-900/30 text-amber-700 px-2 py-0.5 rounded-full">Owner</span> | |
| )} | |
| </div> | |
| {member.role !== "owner" && ( | |
| <div className="flex items-center gap-2"> | |
| <select | |
| value={member.role} | |
| onChange={e => updateMemberRole(member.userId, e.target.value as any)} | |
| className="text-xs bg-white/10 rounded-lg px-2 py-1 border-none" | |
| > | |
| {ROLE_OPTIONS.map(r => <option key={r.value} value={r.value}>{r.label}</option>)} | |
| </select> | |
| <button onClick={() => removeMember(member.userId)} className="text-gray-400 hover:text-red-500"><Trash2 className="w-4 h-4"/></button> | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 3.28 kB
- Xet hash:
- e26a055b3f24d43c461fcbe5b5107317df7dca2cfc782960734494c99c2a033e
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.