| // src/components/groups/InstantGroupCreateModal.tsx | |
| import { useState } from "react"; | |
| import api from "@/lib/api"; | |
| import { toast } from "react-toastify"; | |
| import { Plus, X, Users, Mail, Loader2, Check } from "lucide-react"; | |
| interface InstantGroupCreateModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| onCreate: (groupId: string) => void; | |
| } | |
| export function InstantGroupCreateModal({ isOpen, onClose, onCreate }: InstantGroupCreateModalProps) { | |
| const [name, setName] = useState(""); | |
| const [emails, setEmails] = useState<string[]>([]); | |
| const [emailInput, setEmailInput] = useState(""); | |
| const [creating, setCreating] = useState(false); | |
| const [created, setCreated] = useState(false); | |
| const addEmail = () => { | |
| const email = emailInput.trim(); | |
| if (email && !emails.includes(email) && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { | |
| setEmails(prev => [...prev, email]); | |
| setEmailInput(""); | |
| } | |
| }; | |
| const removeEmail = (index: number) => { | |
| setEmails(prev => prev.filter((_, i) => i !== index)); | |
| }; | |
| const handleCreate = async () => { | |
| if (!name.trim()) { | |
| toast.warning("Group name is required"); | |
| return; | |
| } | |
| setCreating(true); | |
| try { | |
| const res = await api.post("/groups/instant-create", { | |
| name: name.trim(), | |
| inviteEmails: emails, | |
| }); | |
| toast.success("Lab group created! Invitations sent."); | |
| setCreated(true); | |
| onCreate(res.data.groupId); | |
| // Auto-close after 1.5 seconds | |
| setTimeout(() => onClose(), 1500); | |
| } catch (err: any) { | |
| toast.error(err.response?.data?.error || "Failed to create group"); | |
| } finally { | |
| setCreating(false); | |
| } | |
| }; | |
| if (!isOpen) return null; | |
| if (created) { | |
| return ( | |
| <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={onClose}> | |
| <div className="bg-white dark:bg-gray-800 rounded-2xl max-w-sm w-full p-8 text-center shadow-xl" onClick={e => e.stopPropagation()}> | |
| <div className="w-16 h-16 mx-auto rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center mb-4"> | |
| <Check className="w-8 h-8 text-green-600" /> | |
| </div> | |
| <h3 className="text-lg font-bold mb-1">Group Created!</h3> | |
| <p className="text-sm text-gray-500">Invitations have been sent to the provided emails.</p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={onClose}> | |
| <div className="bg-white dark:bg-gray-800 rounded-2xl max-w-md w-full p-6 shadow-xl max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}> | |
| <div className="flex items-center justify-between mb-4"> | |
| <h2 className="text-xl font-bold">Create Lab Group</h2> | |
| <button onClick={onClose} className="text-gray-500 hover:text-gray-700"> | |
| <X className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="text-sm font-semibold mb-1 block">Group Name *</label> | |
| <input | |
| type="text" | |
| placeholder="e.g., Smith Lab – CRISPR Screening" | |
| value={name} | |
| onChange={e => setName(e.target.value)} | |
| className="w-full px-3 py-2.5 border border-gray-200 dark:border-gray-700 rounded-xl bg-transparent focus:ring-2 focus:ring-primary focus:outline-none text-sm" | |
| autoFocus | |
| /> | |
| </div> | |
| <div> | |
| <label className="text-sm font-semibold mb-1 block flex items-center gap-1"> | |
| <Mail className="w-4 h-4" /> Invite Members by Email | |
| </label> | |
| <div className="flex gap-2 mb-2"> | |
| <input | |
| type="email" | |
| placeholder="colleague@university.edu" | |
| value={emailInput} | |
| onChange={e => setEmailInput(e.target.value)} | |
| onKeyDown={e => { | |
| if (e.key === "Enter") { | |
| e.preventDefault(); | |
| addEmail(); | |
| } | |
| }} | |
| className="flex-1 px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-xl bg-transparent focus:ring-2 focus:ring-primary focus:outline-none text-sm" | |
| /> | |
| <button onClick={addEmail} className="btn btn-secondary btn-sm gap-1"> | |
| <Plus className="w-4 h-4" /> Add | |
| </button> | |
| </div> | |
| {emails.length > 0 && ( | |
| <div className="flex flex-wrap gap-1.5 mb-2"> | |
| {emails.map((email, i) => ( | |
| <span | |
| key={i} | |
| className="inline-flex items-center gap-1.5 text-xs bg-primary/10 text-primary px-2.5 py-1 rounded-full" | |
| > | |
| {email} | |
| <button onClick={() => removeEmail(i)} className="hover:text-red-500"> | |
| <X className="w-3 h-3" /> | |
| </button> | |
| </span> | |
| ))} | |
| </div> | |
| )} | |
| <p className="text-xs text-gray-400"> | |
| Each email will receive an invitation to join this group. | |
| </p> | |
| </div> | |
| <button | |
| onClick={handleCreate} | |
| disabled={!name.trim() || creating} | |
| className="btn btn-primary w-full gap-2" | |
| > | |
| {creating ? ( | |
| <Loader2 className="w-4 h-4 animate-spin" /> | |
| ) : ( | |
| <Users className="w-4 h-4" /> | |
| )} | |
| {creating ? "Creating..." : "Create Group & Invite"} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 5.76 kB
- Xet hash:
- dae920c9a51d565ebafa49cfcc2113269e0a7941dbfa93efe4ade1c40c6d54bc
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.