Popup — src/popup.tsx
CultivaLead
Cultiva IA · Lead Capture12
Hoy
347
Total
Recientes
MP
María Pérez
Growth Lead · Factorial HR
2m
JR
Javier Ruiz
CEO · TechStartup BCN
18m
AL
Ana López
CMO · Glovo
1h
Content Script — LinkedIn overlay
Arquitectura de la Extensión
Popup UI
popup.tsx
messaging
Background
background.ts
storage
Content Script
contents/linkedin.tsx
webhook
Zapier/Make
API Webhook
src/popup.tsx
src/types.ts
// src/popup.tsx — Popup principal de CultivaLead Capture import { useEffect } from "react" import { useStorage } from "@plasmohq/storage/hook" import type { Lead } from "~types" function IndexPopup() { // Estado reactivo sincronizado entre popup, content scripts y background const [leads, setLeads] = useStorage<Lead[]>("leads", []) const [isEnabled, setEnabled] = useStorage<boolean>("isEnabled", true) const captureCurrentPage = async () => { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }) chrome.tabs.sendMessage(tab.id!, { type: "CAPTURE_NOW" }) } const todayLeads = leads.filter(l => new Date(l.savedAt).toDateString() === new Date().toDateString() ) return ( <div style={{ padding: 16, width: 320 }}> <Header enabled={isEnabled} onToggle={() => setEnabled(!isEnabled)} /> <StatsRow today={todayLeads.length} total={leads.length} /> <button onClick={captureCurrentPage}>🎯 Capturar Lead Actual</button> <RecentLeads leads={leads.slice(-5)} /> </div> ) } export default IndexPopup
src/contents/linkedin.tsx
src/utils/scraper.ts
// src/contents/linkedin.tsx — Overlay inteligente para LinkedIn import type { PlasmoCSConfig, PlasmoGetOverlayAnchor } from "plasmo" import { sendToBackground } from "@plasmohq/messaging" import { useState } from "react" export const config: PlasmoCSConfig = { matches: ["https://www.linkedin.com/in/*", "https://www.linkedin.com/company/*"] } // Ancla el overlay al header del perfil de LinkedIn export const getOverlayAnchor: PlasmoGetOverlayAnchor = async () => document.querySelector(".pv-top-card") function LinkedInCaptureButton() { const [status, setStatus] = useState<"idle" | "saving" | "saved">("idle") const captureLead = async () => { setStatus("saving") // Extrae datos del DOM de LinkedIn const lead = { name: document.querySelector("h1.text-heading-xlarge")?.textContent?.trim(), title: document.querySelector(".text-body-medium")?.textContent?.trim(), company: document.querySelector("[data-field='experience_company_logo']")?.textContent?.trim(), url: window.location.href, source: "linkedin", savedAt: Date.now() } // Envío tipado al background worker const result = await sendToBackground({ name: "save-lead", body: lead }) setStatus(result.success ? "saved" : "idle") setTimeout(() => setStatus("idle"), 2000) } return ( <button onClick={captureLead} disabled={status === "saving"} style={{ background: status === "saved" ? "#16A34A" : "#2563EB" }}> {status === "saved" ? "✅ Guardado" : status === "saving" ? "⏳..." : "🎯 Capturar"} </button> ) } export default LinkedInCaptureButton
src/background/messages/save-lead.ts
src/background.ts
// src/background/messages/save-lead.ts — Handler tipado MV3 import type { PlasmoMessaging } from "@plasmohq/messaging" import { Storage } from "@plasmohq/storage" const storage = new Storage() const WEBHOOK_URL = "https://hooks.zapier.com/hooks/catch/cultivaia/leads" const handler: PlasmoMessaging.MessageHandler = async (req, res) => { const leads = (await storage.get<Lead[]>("leads")) ?? [] const newLead = { ...req.body, id: crypto.randomUUID() } leads.push(newLead) await storage.set("leads", leads) // Badge dinámico con conteo de leads hoy const todayCount = leads.filter(l => new Date(l.savedAt).toDateString() === new Date().toDateString() ).length chrome.action.setBadgeText({ text: String(todayCount) }) chrome.action.setBadgeBackgroundColor({ color: "#2563EB" }) // Sync inmediato al webhook (fire & forget) fetch(WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newLead) }).catch(console.error) res.send({ success: true, id: newLead.id, total: leads.length }) } // Alarma periódica sync completo cada 15 min chrome.alarms.create("sync-leads", { periodInMinutes: 15 }) chrome.alarms.onAlarm.addListenerasync (alarm) => { if (alarm.name === "sync-leads") { const pending = (await storage.get<Lead[]>("pending_sync")) ?? [] if (pending.length > 0) { await batchSyncToWebhook(pending) await storage.set("pending_sync", []) } } }) export default handler
package.json
{ "name": "cultivalead-capture", "displayName": "CultivaLead Capture", "version": "1.0.0", "description": "Captura leads de LinkedIn y webs con un clic — by Cultiva IA", "scripts": { "dev": "plasmo dev", "build": "plasmo build", "package": "plasmo package", "build:ff": "plasmo build --target=firefox-mv3" }, "manifest": { "permissions": [ "storage", "tabs", "alarms", "contextMenus", "activeTab", "notifications" ], "host_permissions": [ "https://www.linkedin.com/*", "https://hooks.zapier.com/*" ] }, "dependencies": { "plasmo": "^0.90.3", "react": "^18.3.1", "react-dom": "^18.3.1", "@plasmohq/storage": "^1.13.0", "@plasmohq/messaging":"^0.6.2" } }
Crear proyecto
$
pnpm create plasmo cultivalead
Dev con hot reload
$
pnpm dev
Build produccion
$
pnpm build
Empaquetar .zip Chrome Web Store
$
pnpm package