Download frontend/src/components/sequences/SequenceCard.jsx from Seth0330/qualifier: direct link, hf CLI and curl.
- Browser
- Download file 16.5 kB
-
https://huggingface.co/spaces/Seth0330/qualifier/resolve/main/frontend/src/components/sequences/SequenceCard.jsx
- Command line
-
hf download hf://spaces/Seth0330/qualifier/frontend/src/components/sequences/SequenceCard.jsx
-
curl -L -o SequenceCard.jsx https://huggingface.co/spaces/Seth0330/qualifier/resolve/main/frontend/src/components/sequences/SequenceCard.jsx
16.5 kB
| import React, { useMemo, useState } from 'react'; | |
| import { User, Building2, Mail, ChevronDown, ChevronUp, Copy, CheckCircle2 } from 'lucide-react'; | |
| import { Button } from "@/components/ui/button"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { motion } from 'framer-motion'; | |
| export default function SequenceCard({ contact, index }) { | |
| const [isExpanded, setIsExpanded] = useState(index < 3); | |
| const [copiedEmailNum, setCopiedEmailNum] = useState(null); | |
| const [expandedEmailNum, setExpandedEmailNum] = useState(null); | |
| const emails = contact.emails || []; | |
| const linkedin = contact.linkedin || []; | |
| const firstEmail = emails[0] || | |
| linkedin[0] || { | |
| firstName: contact.firstName, | |
| lastName: contact.lastName, | |
| company: contact.company, | |
| email: contact.email, | |
| product: contact.product, | |
| }; | |
| const mergedTimeline = useMemo(() => { | |
| const parts = [ | |
| ...(emails || []).map((m, i) => ({ | |
| kind: 'email', | |
| stepOrder: m.stepOrder, | |
| data: m, | |
| key: `e-${m.stepOrder ?? 'x'}-${m.emailNumber ?? i}`, | |
| })), | |
| ...(linkedin || []).map((m, i) => ({ | |
| kind: 'linkedin', | |
| stepOrder: m.stepOrder, | |
| data: m, | |
| key: `l-${m.stepOrder ?? 'x'}-${m.emailNumber ?? i}`, | |
| })), | |
| ]; | |
| const hasOrder = parts.some((p) => p.stepOrder != null); | |
| if (!hasOrder) return null; | |
| return [...parts].sort((a, b) => (a.stepOrder ?? 1e9) - (b.stepOrder ?? 1e9)); | |
| }, [emails, linkedin]); | |
| const handleCopy = (emailContent, emailNum) => { | |
| navigator.clipboard.writeText(emailContent); | |
| setCopiedEmailNum(emailNum); | |
| setTimeout(() => setCopiedEmailNum(null), 2000); | |
| }; | |
| return ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.3, delay: index * 0.05 }} | |
| className="rounded-xl border border-slate-200 bg-white overflow-hidden hover:shadow-md transition-shadow" | |
| > | |
| <div | |
| className="px-5 py-4 flex items-center justify-between cursor-pointer hover:bg-slate-50/50 transition-colors" | |
| onClick={() => setIsExpanded(!isExpanded)} | |
| > | |
| <div className="flex items-center gap-4"> | |
| <div className="h-10 w-10 rounded-full bg-gradient-to-br from-violet-500 to-purple-600 | |
| flex items-center justify-center text-white font-semibold text-sm"> | |
| {firstEmail.firstName?.[0]}{firstEmail.lastName?.[0]} | |
| </div> | |
| <div> | |
| <h4 className="font-semibold text-slate-800"> | |
| {firstEmail.firstName} {firstEmail.lastName} | |
| </h4> | |
| <div className="flex items-center gap-3 text-sm text-slate-500"> | |
| <span className="flex items-center gap-1"> | |
| <Building2 className="h-3.5 w-3.5" /> | |
| {firstEmail.company} | |
| </span> | |
| <span className="flex items-center gap-1"> | |
| <Mail className="h-3.5 w-3.5" /> | |
| {firstEmail.email} | |
| </span> | |
| {(emails.length > 0 || linkedin.length > 0) && ( | |
| <span className="text-violet-600 font-medium"> | |
| {emails.length > 0 | |
| ? `${emails.length} email${emails.length === 1 ? '' : 's'}` | |
| : null} | |
| {emails.length > 0 && linkedin.length > 0 ? ' · ' : null} | |
| {linkedin.length > 0 | |
| ? `${linkedin.length} LinkedIn` | |
| : null} | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| <Badge className="bg-violet-100 text-violet-700 hover:bg-violet-100"> | |
| {firstEmail.product} | |
| </Badge> | |
| {isExpanded ? ( | |
| <ChevronUp className="h-5 w-5 text-slate-400" /> | |
| ) : ( | |
| <ChevronDown className="h-5 w-5 text-slate-400" /> | |
| )} | |
| </div> | |
| </div> | |
| {isExpanded && ( | |
| <motion.div | |
| initial={{ opacity: 0, height: 0 }} | |
| animate={{ opacity: 1, height: 'auto' }} | |
| exit={{ opacity: 0, height: 0 }} | |
| className="border-t border-slate-100" | |
| > | |
| <div className="px-5 py-4 bg-slate-50/50 space-y-4"> | |
| {mergedTimeline ? ( | |
| mergedTimeline.map((item, idx) => { | |
| const copyId = | |
| item.kind === 'email' | |
| ? item.data.emailNumber || idx + 1 | |
| : `li-${item.data.emailNumber || idx + 1}`; | |
| const isLi = item.kind === 'linkedin'; | |
| return ( | |
| <div | |
| key={item.key} | |
| className={`rounded-lg border p-4 shadow-sm ${ | |
| isLi | |
| ? 'border-sky-200 bg-white' | |
| : 'border-slate-200 bg-white' | |
| }`} | |
| > | |
| <div className="mb-3 flex items-center justify-between"> | |
| <div className="flex flex-wrap items-center gap-2"> | |
| <Badge | |
| variant="outline" | |
| className={ | |
| isLi ? 'border-sky-200 text-sky-800' : 'text-slate-700' | |
| } | |
| > | |
| {isLi ? 'LinkedIn' : 'Gmail'} | |
| </Badge> | |
| <h5 | |
| className={`text-sm font-medium ${ | |
| isLi ? 'text-sky-800' : 'text-slate-600' | |
| }`} | |
| > | |
| {isLi | |
| ? `Message ${item.data.emailNumber || idx + 1}` | |
| : `Email ${item.data.emailNumber || idx + 1}`} | |
| </h5> | |
| </div> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleCopy(item.data.emailContent, copyId); | |
| }} | |
| className={`h-8 text-slate-500 ${ | |
| isLi ? 'hover:text-sky-600' : 'hover:text-violet-600' | |
| }`} | |
| > | |
| {copiedEmailNum === copyId ? ( | |
| <> | |
| <CheckCircle2 className="mr-1 h-4 w-4 text-green-500" /> | |
| Copied! | |
| </> | |
| ) : ( | |
| <> | |
| <Copy className="mr-1 h-4 w-4" /> | |
| Copy | |
| </> | |
| )} | |
| </Button> | |
| </div> | |
| {!isLi && ( | |
| <div className="mb-3 border-b border-slate-100 pb-3"> | |
| <span className="text-xs uppercase tracking-wide text-slate-400"> | |
| Subject | |
| </span> | |
| <p className="mt-1 text-sm font-medium text-slate-800"> | |
| {item.data.subject} | |
| </p> | |
| </div> | |
| )} | |
| <div className="whitespace-pre-wrap text-sm leading-relaxed text-slate-700"> | |
| {item.data.emailContent} | |
| </div> | |
| </div> | |
| ); | |
| }) | |
| ) : ( | |
| <> | |
| {emails.map((email, emailIdx) => ( | |
| <div key={`e-${emailIdx}`} className="rounded-lg border border-slate-200 bg-white p-4"> | |
| <div className="mb-3 flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <h5 className="text-sm font-medium text-slate-600"> | |
| Email {email.emailNumber || emailIdx + 1} | |
| </h5> | |
| {emails.length > 1 && ( | |
| <Badge variant="outline" className="text-xs"> | |
| {email.emailNumber || emailIdx + 1} of {emails.length} | |
| </Badge> | |
| )} | |
| </div> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleCopy( | |
| email.emailContent, | |
| email.emailNumber || emailIdx + 1 | |
| ); | |
| }} | |
| className="h-8 text-slate-500 hover:text-violet-600" | |
| > | |
| {copiedEmailNum === (email.emailNumber || emailIdx + 1) ? ( | |
| <> | |
| <CheckCircle2 className="mr-1 h-4 w-4 text-green-500" /> | |
| Copied! | |
| </> | |
| ) : ( | |
| <> | |
| <Copy className="mr-1 h-4 w-4" /> | |
| Copy | |
| </> | |
| )} | |
| </Button> | |
| </div> | |
| <div className="mb-3 border-b border-slate-100 pb-3"> | |
| <span className="text-xs uppercase tracking-wide text-slate-400"> | |
| Subject | |
| </span> | |
| <p className="mt-1 text-sm font-medium text-slate-800">{email.subject}</p> | |
| </div> | |
| <div className="whitespace-pre-wrap text-sm leading-relaxed text-slate-700"> | |
| {email.emailContent} | |
| </div> | |
| </div> | |
| ))} | |
| {linkedin.map((msg, liIdx) => ( | |
| <div | |
| key={`li-${liIdx}`} | |
| className="rounded-lg border border-sky-200 bg-white p-4 shadow-sm" | |
| > | |
| <div className="mb-3 flex items-center justify-between"> | |
| <h5 className="text-sm font-medium text-sky-800"> | |
| LinkedIn — Message {msg.emailNumber || liIdx + 1} | |
| </h5> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleCopy( | |
| msg.emailContent, | |
| `li-${msg.emailNumber || liIdx + 1}` | |
| ); | |
| }} | |
| className="h-8 text-slate-500 hover:text-sky-600" | |
| > | |
| {copiedEmailNum === `li-${msg.emailNumber || liIdx + 1}` ? ( | |
| <> | |
| <CheckCircle2 className="mr-1 h-4 w-4 text-green-500" /> | |
| Copied! | |
| </> | |
| ) : ( | |
| <> | |
| <Copy className="mr-1 h-4 w-4" /> | |
| Copy | |
| </> | |
| )} | |
| </Button> | |
| </div> | |
| <div className="whitespace-pre-wrap text-sm leading-relaxed text-slate-700"> | |
| {msg.emailContent} | |
| </div> | |
| </div> | |
| ))} | |
| </> | |
| )} | |
| </div> | |
| </motion.div> | |
| )} | |
| </motion.div> | |
| ); | |
| } | |