🌱

CultivaLead Capture

Extensión Chrome · Plasmo + React + TypeScript
Plasmo 0.90 Manifest V3 MV3 Ready
Popup — src/popup.tsx
🌱

CultivaLead

Cultiva IA · Lead Capture
12
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
linkedin.com/in/maria-perez-factorial
MP
María Pérez García
Head of Growth · Marketing
Factorial HR · Barcelona, España
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