Spaces:
Sleeping
Sleeping
| import React, { useState } from "react"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| import { X, Mail, Send, Loader2 } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| export default function ShareModal({ isOpen, onClose, onShare, extractionId }) { | |
| const [email, setEmail] = useState(""); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [error, setError] = useState(""); | |
| const [success, setSuccess] = useState(false); | |
| const [successMessage, setSuccessMessage] = useState(""); | |
| const handleSubmit = async (e) => { | |
| e.preventDefault(); | |
| setError(""); | |
| setSuccess(false); | |
| // Parse and validate multiple emails (comma or semicolon separated) | |
| if (!email.trim()) { | |
| setError("Please enter at least one recipient email address"); | |
| return; | |
| } | |
| // Split by comma or semicolon, trim each email, and filter out empty strings | |
| const emailList = email | |
| .split(/[,;]/) | |
| .map((e) => e.trim()) | |
| .filter((e) => e.length > 0); | |
| if (emailList.length === 0) { | |
| setError("Please enter at least one recipient email address"); | |
| return; | |
| } | |
| // Validate each email | |
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
| const invalidEmails = emailList.filter((e) => !emailRegex.test(e)); | |
| if (invalidEmails.length > 0) { | |
| setError(`Invalid email address(es): ${invalidEmails.join(", ")}`); | |
| return; | |
| } | |
| setIsLoading(true); | |
| try { | |
| const result = await onShare(extractionId, emailList); | |
| setSuccessMessage(result?.message || `Successfully shared with ${emailList.length} recipient(s)`); | |
| setSuccess(true); | |
| setEmail(""); | |
| // Close modal after 2 seconds | |
| setTimeout(() => { | |
| setSuccess(false); | |
| setSuccessMessage(""); | |
| onClose(); | |
| }, 2000); | |
| } catch (err) { | |
| setError(err.message || "Failed to share extraction. Please try again."); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const handleClose = () => { | |
| if (!isLoading) { | |
| setEmail(""); | |
| setError(""); | |
| setSuccess(false); | |
| onClose(); | |
| } | |
| }; | |
| if (!isOpen) return null; | |
| return ( | |
| <AnimatePresence> | |
| <div className="fixed inset-0 z-50 flex items-center justify-center"> | |
| {/* Backdrop */} | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| className="absolute inset-0 bg-black/50 backdrop-blur-sm" | |
| onClick={handleClose} | |
| /> | |
| {/* Modal */} | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.95, y: 20 }} | |
| animate={{ opacity: 1, scale: 1, y: 0 }} | |
| exit={{ opacity: 0, scale: 0.95, y: 20 }} | |
| className="relative z-10 w-full max-w-md mx-4 bg-white rounded-2xl shadow-2xl overflow-hidden" | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| {/* Header */} | |
| <div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between"> | |
| <h2 className="text-xl font-semibold text-slate-900">Share Output</h2> | |
| <button | |
| onClick={handleClose} | |
| disabled={isLoading} | |
| className="p-2 rounded-lg hover:bg-slate-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" | |
| > | |
| <X className="h-5 w-5 text-slate-500" /> | |
| </button> | |
| </div> | |
| {/* Content */} | |
| <div className="px-6 py-6"> | |
| {success ? ( | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.9 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| className="text-center py-8" | |
| > | |
| <div className="w-16 h-16 mx-auto mb-4 rounded-full bg-emerald-100 flex items-center justify-center"> | |
| <Send className="h-8 w-8 text-emerald-600" /> | |
| </div> | |
| <h3 className="text-lg font-semibold text-slate-900 mb-2"> | |
| Share Sent Successfully! | |
| </h3> | |
| <p className="text-sm text-slate-600"> | |
| {successMessage || "The recipient(s) will receive an email with a link to view the extraction."} | |
| </p> | |
| </motion.div> | |
| ) : ( | |
| <form onSubmit={handleSubmit} className="space-y-4"> | |
| <div> | |
| <label | |
| htmlFor="recipient-email" | |
| className="block text-sm font-medium text-slate-700 mb-2" | |
| > | |
| Recipient Email(s) | |
| </label> | |
| <p className="text-xs text-slate-500 mb-2"> | |
| Separate multiple emails with commas or semicolons | |
| </p> | |
| <div className="relative"> | |
| <Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" /> | |
| <Input | |
| id="recipient-email" | |
| type="text" | |
| value={email} | |
| onChange={(e) => setEmail(e.target.value)} | |
| placeholder="Enter email addresses (comma or semicolon separated)" | |
| className="pl-10 h-12 rounded-xl border-slate-200 focus:border-indigo-500 focus:ring-indigo-500" | |
| disabled={isLoading} | |
| autoFocus | |
| /> | |
| </div> | |
| {error && ( | |
| <motion.p | |
| initial={{ opacity: 0, y: -10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="mt-2 text-sm text-red-600" | |
| > | |
| {error} | |
| </motion.p> | |
| )} | |
| </div> | |
| <div className="pt-4 flex gap-3"> | |
| <Button | |
| type="button" | |
| variant="outline" | |
| onClick={handleClose} | |
| disabled={isLoading} | |
| className="flex-1 h-11 rounded-xl" | |
| > | |
| Cancel | |
| </Button> | |
| <Button | |
| type="submit" | |
| disabled={isLoading || !email.trim()} | |
| className="flex-1 h-11 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700" | |
| > | |
| {isLoading ? ( | |
| <> | |
| <Loader2 className="h-4 w-4 mr-2 animate-spin" /> | |
| Sending... | |
| </> | |
| ) : ( | |
| <> | |
| <Send className="h-4 w-4 mr-2" /> | |
| Send | |
| </> | |
| )} | |
| </Button> | |
| </div> | |
| </form> | |
| )} | |
| </div> | |
| </motion.div> | |
| </div> | |
| </AnimatePresence> | |
| ); | |
| } | |