All files / src/components/whatsapp ConversationList.tsx

80% Statements 20/25
75% Branches 21/28
100% Functions 8/8
100% Lines 20/20

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155                              2x               30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x                         22x                 1x                   88x     1x                                   10x                         30x     5x                                                                                                      
import { Search, MessageCircle } from "lucide-react";
import { cn } from "../../lib/utils";
import type { WaConversation } from "../../lib/api/whatsapp";
 
type ConversationListProps = {
	conversations: WaConversation[];
	selectedId: number | null;
	onSelect: (id: number) => void;
	search: string;
	onSearchChange: (value: string) => void;
	contactTypeFilter: string;
	onContactTypeFilterChange: (value: string) => void;
	isLoading: boolean;
};
 
const contactTypeTabs = [
	{ value: "", label: "All" },
	{ value: "pro", label: "Pros" },
	{ value: "customer", label: "Customers" },
	{ value: "unknown", label: "Unknown" },
];
 
function formatRelativeTime(dateStr: string | null): string {
	Iif (!dateStr) return "";
	const date = new Date(dateStr);
	const now = new Date();
	const diffMs = now.getTime() - date.getTime();
	const diffMins = Math.floor(diffMs / 60000);
	Iif (diffMins < 1) return "now";
	Iif (diffMins < 60) return `${diffMins}m`;
	const diffHours = Math.floor(diffMins / 60);
	Iif (diffHours < 24) return `${diffHours}h`;
	const diffDays = Math.floor(diffHours / 24);
	Iif (diffDays < 7) return `${diffDays}d`;
	return date.toLocaleDateString("en-IN", { day: "numeric", month: "short" });
}
 
export function ConversationList({
	conversations,
	selectedId,
	onSelect,
	search,
	onSearchChange,
	contactTypeFilter,
	onContactTypeFilterChange,
	isLoading,
}: ConversationListProps) {
	return (
		<div className="flex flex-col h-full border-r border-border-default">
			{/* Search */}
			<div className="p-3 border-b border-border-default">
				<div className="relative">
					<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-foreground-subtle" />
					<input
						type="text"
						value={search}
						onChange={(e) => onSearchChange(e.target.value)}
						placeholder="Search conversations..."
						className="w-full rounded-md border border-border-default bg-background-default pl-9 pr-3 py-2 text-sm placeholder:text-foreground-subtle focus:outline-none focus:ring-2 focus:ring-whatsapp-green focus:border-transparent"
					/>
				</div>
			</div>
 
			{/* Contact Type Tabs */}
			<div className="flex border-b border-border-default">
				{contactTypeTabs.map((tab) => (
					<button
						key={tab.value}
						type="button"
						onClick={() => onContactTypeFilterChange(tab.value)}
						className={cn(
							"flex-1 px-2 py-2 text-xs font-medium transition-colors",
							contactTypeFilter === tab.value
								? "text-whatsapp-green border-b-2 border-whatsapp-green"
								: "text-foreground-muted hover:text-foreground-default",
						)}
					>
						{tab.label}
					</button>
				))}
			</div>
 
			{/* Conversation List */}
			<div className="flex-1 overflow-y-auto">
				{isLoading ? (
					<div className="p-4 space-y-3">
						{[1, 2, 3, 4, 5].map((i) => (
							<div
								key={i}
								className="h-16 bg-background-muted rounded-md animate-pulse"
							/>
						))}
					</div>
				) : conversations.length === 0 ? (
					<div className="flex flex-col items-center justify-center h-48 text-foreground-muted">
						<MessageCircle className="h-8 w-8 mb-2" />
						<p className="text-sm">No conversations found</p>
					</div>
				) : (
					conversations.map((conv) => (
						<button
							key={conv.id}
							type="button"
							onClick={() => onSelect(conv.id)}
							className={cn(
								"w-full text-left px-4 py-3 border-b border-border-default transition-colors",
								selectedId === conv.id
									? "bg-whatsapp-bg/30 dark:bg-whatsapp-green/10"
									: "hover:bg-background-muted",
							)}
						>
							<div className="flex items-start justify-between gap-2">
								<div className="flex items-center gap-3 min-w-0">
									<div
										className={cn(
											"h-10 w-10 rounded-full flex items-center justify-center flex-shrink-0 text-sm font-medium",
											conv.contactType === "pro"
												? "bg-info-light text-info"
												: conv.contactType === "customer"
													? "bg-warning-light text-warning"
													: "bg-background-muted text-foreground-muted",
										)}
									>
										{(conv.contactName || conv.phoneNumber)
											.charAt(0)
											.toUpperCase()}
									</div>
									<div className="min-w-0">
										<p className="text-sm font-medium text-foreground-default truncate">
											{conv.contactName || conv.phoneNumber}
										</p>
										<p className="text-xs text-foreground-muted truncate">
											{conv.phoneNumber}
										</p>
									</div>
								</div>
								<div className="flex flex-col items-end flex-shrink-0">
									<span className="text-xs text-foreground-subtle">
										{formatRelativeTime(conv.lastMessageAt)}
									</span>
									{conv.unreadCount > 0 && (
										<span className="mt-1 inline-flex items-center justify-center min-w-[20px] h-5 rounded-full bg-whatsapp-green px-1.5 text-xs font-medium text-foreground-inverse">
											{conv.unreadCount}
										</span>
									)}
								</div>
							</div>
						</button>
					))
				)}
			</div>
		</div>
	);
}