| | 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); |
| |
|
| | |
| | if (!email.trim()) { |
| | setError("Please enter at least one recipient email address"); |
| | return; |
| | } |
| |
|
| | |
| | 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; |
| | } |
| |
|
| | |
| | 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(""); |
| | |
| | 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> |
| | ); |
| | } |
| |
|