qualifier / frontend /src /components /sequences /SequenceCard.jsx
Sethu0330's picture
Add FTL RFQ Qualifier agent + full SALES CRM app for HF Spaces testing
005df22 unverified
Raw History Blame Contribute Delete
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>
);
}