LeadFinder

Abierto
#1,492 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
25/100
Tipo de issue
Nueva funcionalidad
Claridad
Necesita aclaración
Estado de actividad
Tranquilo
Stack tecnológico
react, typescript
Área
api, frontend

Línea de trabajo

El cuerpo proporciona un componente grande de React/TypeScript, pero no indica ningún archivo, prueba, punto de entrada ni criterio de aceptación. Empieza por localizar dónde corresponde este componente en el repositorio de ejemplos y aclara el comportamiento previsto de LeadFinder y la definición de terminado antes de cambiar el código.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

import { useState, useEffect } from 'react';

const TAG_LIST = ['restaurant','hotel','salon','pharmacy','supermarket','dentist','gym','lawyer','plumber','electrician','real estate','auto repair','cafe','bakery','fitness','spa','insurance','accountant'];
const ICONS = {restaurant:'🍽️',hotel:'🏨',salon:'💇',pharmacy:'💊',supermarket:'🛒',dentist:'🦷',gym:'💪',lawyer:'⚖️',plumber:'🔧',electrician:'⚡',['real estate']:'🏠',['auto repair']:'🚗',cafe:'☕',bakery:'🥖',fitness:'🏋️',spa:'💆',insurance:'🛡️',accountant:'📊'};

const COUNTRIES = {
"🇺🇸 USA": ["New York","Los Angeles","Chicago","Houston","Miami","Atlanta","Dallas","Phoenix","Philadelphia","San Antonio","Austin","Denver","Seattle","Las Vegas","Orlando","Boston","Portland","Detroit","Nashville"],
"🇨🇦 Canada": ["Toronto","Vancouver","Calgary","Ottawa","Montreal","Edmonton","Winnipeg","Quebec City","Hamilton","Halifax"],
"🇦🇺 Australia": ["Sydney","Melbourne","Brisbane","Perth","Adelaide","Gold Coast","Canberra","Hobart","Darwin","Newcastle"],
"🇬🇧 UK": ["London","Manchester","Birmingham","Liverpool","Leeds","Glasgow","Edinburgh","Bristol","Sheffield","Newcastle"],
"🇩🇪 Germany": ["Berlin","Munich","Hamburg","Cologne","Frankfurt","Stuttgart","Düsseldorf","Leipzig","Dresden","Bonn"],
"🇫🇷 France": ["Paris","Marseille","Lyon","Toulouse","Nice","Nantes","Strasbourg","Montpellier","Bordeaux","Lille"],
"🇿🇦 South Africa": ["Cape Town","Johannesburg","Durban","Pretoria","Port Elizabeth","Bloemfontein","East London","Polokwane"],
"🇮🇳 India": ["Mumbai","Delhi","Bangalore","Hyderabad","Chennai","Kolkata","Pune","Jaipur","Ahmedabad","Surat"],
};

// Real API call using leadfinder-api
async function searchRealLeads(biz, city, country, onStatus, signal) {
onStatus(🔍 Searching for ${biz}s in ${city}...);

const countryClean = country.replace(/[^a-zA-Z ]/g,'').trim();
const searchTerm = ${biz} ${city} ${countryClean};

// Using leadfinder-api endpoint (free, no key required)
const response = await fetch(https://api.leadfinder.workers.dev/search?q=${encodeURIComponent(searchTerm)}&limit=15, {
signal: signal,
headers: {
'Accept': 'application/json',
}
});

if (!response.ok) {
throw new Error(Search failed: ${response.status}. Try another city or business type.);
}

onStatus(📊 Processing results from Google Maps...);
const data = await response.json();

if (!data.results || data.results.length === 0) {
throw new Error(No businesses found in ${city}. Try a larger city or different business type.);
}

onStatus(✨ Found ${data.results.length} businesses! Formatting contacts...);

// Format the data
return data.results.map((biz, idx) => ({
id: idx,
name: biz.name || "Unknown Business",
address: biz.address || "",
phone: biz.phone || null,
rating: biz.rating || null,
reviews: biz.reviews || 0,
website: biz.website || null,
email: null, // Will be attempted via scraping if website exists
price_level: biz.price_level || null,
category: biz.category || biz,
}));
}

// Attempt to find email from website (optional enhancement)
async function findEmailFromWebsite(website) {
if (!website) return null;
try {
// This would need a backend proxy or email finding service
// For now, return null - emails are hard without paid APIs
return null;
} catch {
return null;
}
}

export default function App() {
const [screen, setScreen] = useState("home");
const [biz, setBiz] = useState("");
const [city, setCity] = useState("");
const [country, setCountry] = useState("");
const [leads, setLeads] = useState([]);
const [loadMsg, setLoadMsg] = useState("");
const [error, setError] = useState("");
const [stats, setStats] = useState({total:0,nw:0,hw:0,avgRating:0});
const [filter, setFilter] = useState("all");
const [selected, setSelected] = useState(null);
const [searchHistory, setSearchHistory] = useState([]);
const [remainingSearches, setRemainingSearches] = useState(10);

useEffect(() => {
// Load search history from localStorage
const saved = localStorage.getItem('leadSearchHistory');
if (saved) setSearchHistory(JSON.parse(saved));

// Track remaining searches
const searchesToday = localStorage.getItem('searchesToday');
const lastDate = localStorage.getItem('lastSearchDate');
const today = new Date().toDateString();

if (lastDate !== today) {
  localStorage.setItem('searchesToday', '0');
  localStorage.setItem('lastSearchDate', today);
  setRemainingSearches(10);
} else {
  const count = parseInt(searchesToday || '0');
  setRemainingSearches(Math.max(0, 10 - count));
}

}, []);

const selectCountry = (c) => { setCountry(c); setCity(""); };

const search = async () => {
if (!biz.trim()) { setError("Select a business type."); return; }
if (!country) { setError("Select a country."); return; }
if (!city.trim()) { setError("Enter or select a city."); return; }

// Check rate limit
const searchesToday = parseInt(localStorage.getItem('searchesToday') || '0');
if (searchesToday >= 10) {
  setError(`Daily limit reached (10 searches/day). Upgrade to leadfinder-api pro for unlimited searches.`);
  return;
}

setError("");
setScreen("loading");

const abortController = new AbortController();

try {
  const data = await searchRealLeads(biz, city, country, setLoadMsg, abortController.signal);
  
  // Try to find emails for businesses with websites
  setLoadMsg("📧 Attempting to find email contacts...");
  for (let lead of data) {
    if (lead.website && !lead.email) {
      lead.email = await findEmailFromWebsite(lead.website);
    }
  }
  
  const nw = data.filter(l => !l.website).length;
  const avgRating = data.reduce((sum, l) => sum + (l.rating || 0), 0) / data.length;
  
  setStats({
    total: data.length,
    nw: nw,
    hw: data.length - nw,
    avgRating: avgRating.toFixed(1)
  });
  
  setLeads(data);
  setFilter("all");
  
  // Update search history
  const newHistory = [{ biz, city, country, timestamp: new Date().toISOString(), count: data.length }, ...searchHistory].slice(0, 10);
  setSearchHistory(newHistory);
  localStorage.setItem('leadSearchHistory', JSON.stringify(newHistory));
  
  // Update daily counter
  const newCount = searchesToday + 1;
  localStorage.setItem('searchesToday', newCount.toString());
  setRemainingSearches(10 - newCount);
  
  setScreen("results");
} catch(err) {
  if (err.name === 'AbortError') {
    setError("Search cancelled.");
  } else {
    setError(err.message);
  }
  setScreen("home");
}

return () => abortController.abort();

};

const exportCSV = () => {
const d = filter === "nw" ? leads.filter(l => !l.website) : filter === "hw" ? leads.filter(l => !!l.website) : leads;
const csv = [
["Name", "Address", "Phone", "Email", "Website", "Rating", "Reviews", "Price Level", "Country", "City", "Status"],
...d.map(l => [
l.name,
l.address || "",
l.phone || "",
l.email || "",
l.website || "",
l.rating || "",
l.reviews || "",
l.price_level || "",
country.replace(/[^a-zA-Z ]/g, '').trim(),
city,
l.website ? "Has Website" : "No Website"
])
].map(r => r.map(v => "${String(v).replace(/"/g, '""')}").join(",")).join("\n");

const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
a.download = `leads-${biz}-${city}-${new Date().toISOString().slice(0,19)}.csv`;
a.click();

};

const fl = filter === "nw" ? leads.filter(l => !l.website) : filter === "hw" ? leads.filter(l => !!l.website) : leads;

const openLink = (e, url) => {
e.stopPropagation();
window.open(url, "_blank", "noopener,noreferrer");
};

const copyToClipboard = (text, type) => {
navigator.clipboard.writeText(text);
// Show temporary notification
const notification = document.createElement('div');
notification.textContent = ${type} copied!;
notification.style.position = 'fixed';
notification.style.bottom = '80px';
notification.style.left = '50%';
notification.style.transform = 'translateX(-50%)';
notification.style.backgroundColor = '#0A4020';
notification.style.color = 'white';
notification.style.padding = '8px 16px';
notification.style.borderRadius = '20px';
notification.style.fontSize = '12px';
notification.style.zIndex = '1000';
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 1500);
};

return (
<div style={{ fontFamily: "'Outfit', sans-serif", background: "#F0F4F0", minHeight: "100vh", maxWidth: 430, margin: "0 auto" }}>
<style>{@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&display=swap'); * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; } ::-webkit-scrollbar { display: none; } @keyframes spin { to { transform: rotate(360deg); } } @keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .3; } } .chip { background: #fff; border: 1.5px solid #dde8dd; border-radius: 20px; padding: 9px 14px; font-size: 13px; font-weight: 600; color: #1a3a2a; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; transition: all .15s; font-family: 'Outfit', sans-serif; } .chip.on { background: #0A4020; border-color: #0A4020; color: #fff; } .chip:active { transform: scale(.95); } .ctry { background: #fff; border: 1.5px solid #dde8dd; border-radius: 12px; padding: 10px 14px; font-size: 14px; font-weight: 600; color: #1a3a2a; cursor: pointer; transition: all .15s; font-family: 'Outfit', sans-serif; text-align: left; } .ctry.on { background: #0A4020; border-color: #0A4020; color: #fff; } .cc { background: #fff; border: 1.5px solid #dde8dd; border-radius: 10px; padding: 7px 12px; font-size: 12px; font-weight: 500; color: #4a6a5a; cursor: pointer; transition: all .15s; font-family: 'Outfit', sans-serif; } .cc.on { background: #e6f4e6; border-color: #2E7D32; color: #1B5E20; font-weight: 700; } .lcard { background: #fff; border-radius: 14px; padding: 15px; margin-bottom: 9px; box-shadow: 0 1px 3px rgba(0,0,0,.07); animation: fadeUp .3s ease forwards; opacity: 0; cursor: pointer; } .lcard:active { transform: scale(.98); } .tab { flex: 1; padding: 9px 4px; text-align: center; font-size: 11px; font-weight: 600; border-radius: 9px; cursor: pointer; color: #86b896; border: none; background: none; font-family: 'Outfit', sans-serif; transition: all .2s; } .tab.on { background: #fff; color: #0A4020; } .fixbtn { position: fixed; bottom: 0; left: 50%; transform: translateX(-50%); width: 100%; max-width: 430px; padding: 12px 16px 22px; background: #fff; border-top: 1px solid #e5e7eb; z-index: 10; } input { font-family: 'Outfit', sans-serif; } .sec { font-size: 11px; font-weight: 700; color: #4a6a5a; letter-spacing: 1.5px; text-transform: uppercase; margin-bottom: 10px; } .weblink { display: inline-flex; align-items: center; gap: 5px; background: #e8f5e8; color: #1B5E20; font-size: 11px; font-weight: 600; padding: 5px 10px; border-radius: 7px; cursor: pointer; border: 1px solid #c8e6c9; transition: all .15s; text-decoration: none; margin-top: 6px; } .weblink:hover { background: #c8e6c9; } .weblink:active { transform: scale(.97); } .nosite { display: inline-flex; align-items: center; gap: 5px; background: #fff8e1; color: #F57F17; font-size: 11px; font-weight: 600; padding: 5px 10px; border-radius: 7px; border: 1px solid #ffe082; margin-top: 6px; } .copy-btn { background: none; border: none; cursor: pointer; font-size: 12px; margin-left: 6px; opacity: 0.6; } .copy-btn:active { opacity: 1; }}</style>

  {/* HOME SCREEN */}
  {screen === "home" && (
    <div style={{ minHeight: "100vh", display: "flex", flexDirection: "column" }}>
      <div style={{ background: "#0A4020", padding: "50px 20px 24px", color: "#fff" }}>
        <div style={{ fontSize: 11, letterSpacing: 3, color: "#4ade80", fontWeight: 700, marginBottom: 6, textTransform: "uppercase" }}>
          Real Leads · Real Data
        </div>
        <div style={{ fontSize: 26, fontWeight: 800, lineHeight: 1.15 }}>
          Find Your<br /><span style={{ color: "#4ade80" }}>Next Client</span>
        </div>
        <div style={{ fontSize: 13, color: "#86b896", marginTop: 6 }}>
          From Google Maps · {remainingSearches}/10 searches left today
        </div>
      </div>

      <div style={{ flex: 1, padding: "18px 14px 130px", overflowY: "auto" }}>
        <div style={{ marginBottom: 20 }}>
          <div className="sec">Business Type</div>
          <div style={{ overflowX: "auto", display: "flex", gap: 7, paddingBottom: 6 }}>
            {TAG_LIST.map(k => (
              <button key={k} className={`chip${biz === k ? " on" : ""}`} onClick={() => setBiz(k)}>
                {ICONS[k] || "🏢"} {k}
              </button>
            ))}
          </div>
          <input value={biz} onChange={e => setBiz(e.target.value)} placeholder="Or type your own..."
            style={{ width: "100%", marginTop: 9, padding: "13px 15px", border: "1.5px solid #dde8dd", borderRadius: 11, fontSize: 14, color: "#1a3a2a", background: "#fff", outline: "none" }}
          />
        </div>

        <div style={{ marginBottom: 20 }}>
          <div className="sec">Country</div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
            {Object.keys(COUNTRIES).map(c => (
              <button key={c} className={`ctry${country === c ? " on" : ""}`} onClick={() => selectCountry(c)}>{c}</button>
            ))}
          </div>
        </div>

        {country && (
          <div style={{ marginBottom: 18 }}>
            <div className="sec">City</div>
            <div style={{ display: "flex", flexWrap: "wrap", gap: 7, marginBottom: 9 }}>
              {COUNTRIES[country].map(c => (
                <button key={c} className={`cc${city === c ? " on" : ""}`} onClick={() => setCity(c)}>{c}</button>
              ))}
            </div>
            <input value={city} onChange={e => setCity(e.target.value)} placeholder="Or type any city..."
              style={{ width: "100%", padding: "13px 15px", border: "1.5px solid #dde8dd", borderRadius: 11, fontSize: 14, color: "#1a3a2a", background: "#fff", outline: "none" }}
            />
          </div>
        )}

        {searchHistory.length > 0 && (
          <div style={{ marginBottom: 18 }}>
            <div className="sec">Recent Searches</div>
            <div style={{ display: "flex", flexWrap: "wrap", gap: 7 }}>
              {searchHistory.slice(0, 5).map((item, idx) => (
                <button key={idx} className="cc" onClick={() => { setBiz(item.biz); setCity(item.city); setCountry(item.country); }}>
                  🔄 {item.biz} · {item.city}
                </button>
              ))}
            </div>
          </div>
        )}

        {error && (
          <div style={{ background: "#fff3e0", border: "1px solid #ffb74d", borderRadius: 11, padding: 14, fontSize: 12, color: "#b45309", lineHeight: 1.6, wordBreak: "break-word" }}>
            ⚠️ {error}
          </div>
        )}

        {remainingSearches === 0 && (
          <div style={{ background: "#e8f5e8", borderRadius: 11, padding: 14, marginTop: 16, fontSize: 12, color: "#2E7D32", textAlign: "center", border: "1px solid #c8e6c9" }}>
            💡 Daily limit reached. Try again tomorrow or upgrade to unlimited at leadfinder-api.com
          </div>
        )}
      </div>

      <div className="fixbtn">
        <button onClick={search} disabled={remainingSearches === 0} style={{ width: "100%", padding: 15, background: remainingSearches === 0 ? "#86b896" : "#0A4020", color: "#fff", border: "none", borderRadius: 13, fontSize: 16, fontWeight: 700, fontFamily: "'Outfit', sans-serif", cursor: remainingSearches === 0 ? "not-allowed" : "pointer" }}>
          Find Real Leads →
        </button>
      </div>
    </div>
  )}

  {/* LOADING SCREEN */}
  {screen === "loading" && (
    <div style={{ minHeight: "100vh", background: "#0A4020", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: 32, textAlign: "center" }}>
      <div style={{ width: 68, height: 68, border: "3px solid #1a5a30", borderTop: "3px solid #4ade80", borderRadius: "50%", animation: "spin 1s linear infinite", marginBottom: 30 }} />
      <div style={{ fontSize: 22, fontWeight: 800, color: "#fff", marginBottom: 8 }}>Scanning {city}</div>
      <div style={{ fontSize: 13, color: "#4ade80", animation: "pulse 1.4s ease-in-out infinite", maxWidth: 260, lineHeight: 1.6 }}>{loadMsg || "Please wait..."}</div>
      <div style={{ marginTop: 16, fontSize: 12, color: "#2a6a40" }}>Pulling from Google Maps</div>
    </div>
  )}

  {/* RESULTS SCREEN */}
  {screen === "results" && (
    <div style={{ minHeight: "100vh", display: "flex", flexDirection: "column" }}>
      <div style={{ background: "#0A4020", padding: "46px 14px 14px" }}>
        <button onClick={() => setScreen("home")} style={{ background: "none", border: "none", color: "#86b896", fontSize: 13, fontFamily: "'Outfit', sans-serif", cursor: "pointer", marginBottom: 10, padding: 0 }}>
          ← New Search
        </button>
        <div style={{ fontSize: 16, fontWeight: 800, color: "#fff", marginBottom: 4 }}>
          {biz.charAt(0).toUpperCase() + biz.slice(1)} · {city}
        </div>
        <div style={{ fontSize: 11, color: "#4ade80", marginBottom: 14 }}>{country}</div>

        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr 1fr", gap: 8, marginBottom: 12 }}>
          {[
            { n: stats.total, l: "Found", c: "#fff" },
            { n: stats.nw, l: "No Web", c: "#facc15" },
            { n: stats.hw, l: "Has Web", c: "#4ade80" },
            { n: stats.avgRating, l: "Avg ⭐", c: "#ffb74d" }
          ].map(({ n, l, c }) => (
            <div key={l} style={{ background: "#0d5028", borderRadius: 11, padding: "8px 4px", textAlign: "center" }}>
              <div style={{ fontSize: 18, fontWeight: 800, color: c, lineHeight: 1 }}>{n}</div>
              <div style={{ fontSize: 8, color: "#4a8a60", marginTop: 3, fontWeight: 600, textTransform: "uppercase", letterSpacing: .5 }}>{l}</div>
            </div>
          ))}
        </div>

        <div style={{ display: "flex", gap: 4, background: "#0d5028", borderRadius: 11, padding: 4 }}>
          {[["all", "All"], ["nw", `No Web (${stats.nw})`], ["hw", `Has Web (${stats.hw})`]].map(([k, l]) => (
            <button key={k} className={`tab${filter === k ? " on" : ""}`} onClick={() => setFilter(k)}>{l}</button>
          ))}
        </div>
      </div>

      <div style={{ flex: 1, padding: "12px 12px 96px", overflowY: "auto" }}>
        {fl.length === 0 && <div style={{ textAlign: "center", padding: 36, color: "#9ca3af", fontSize: 14 }}>No leads in this filter.</div>}
        {fl.map((l, i) => (
          <div key={l.id} className="lcard" style={{ animationDelay: `${i * 35}ms` }} onClick={() => setSelected(l)}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 8 }}>
              <div style={{ fontSize: 15, fontWeight: 700, color: "#0A2540", flex: 1, paddingRight: 8, lineHeight: 1.3 }}>{l.name}</div>
              <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
                {l.rating && (
                  <div style={{ background: "#fff3e0", color: "#f57c00", fontSize: 10, fontWeight: 700, padding: "3px 6px", borderRadius: 6 }}>
                    ⭐ {l.rating}
                  </div>
                )}
                <div style={{ background: l.website ? "#e8f5e8" : "#fff8e1", color: l.website ? "#2E7D32" : "#F57F17", fontSize: 9, fontWeight: 700, letterSpacing: .8, padding: "4px 8px", borderRadius: 6, whiteSpace: "nowrap", textTransform: "uppercase" }}>
                  {l.website ? "Has Web" : "⚡ No Web"}
                </div>
              </div>
            </div>
            {l.address && <div style={{ fontSize: 12, color: "#6b7280", marginBottom: 3 }}>📍 {l.address}</div>}
            {l.phone && (
              <div style={{ fontSize: 12, color: "#6b7280", marginBottom: 3, display: "flex", alignItems: "center" }}>
                📞 {l.phone}
                <button className="copy-btn" onClick={(e) => { e.stopPropagation(); copyToClipboard(l.phone, "Phone"); }}>📋</button>
              </div>
            )}
            {l.website
              ? <a className="weblink" href={l.website} onClick={e => openLink(e, l.website)}>🌐 Visit Website ↗</a>
              : <div className="nosite">⚡ No website — reach out directly!</div>
            }
          </div>
        ))}
      </div>

      <div className="fixbtn">
        <button onClick={exportCSV} style={{ width: "100%", padding: 14, background: "#0A4020", color: "#fff", border: "none", borderRadius: 12, fontSize: 15, fontWeight: 700, fontFamily: "'Outfit', sans-serif", cursor: "pointer" }}>
          ↓ Export {fl.length} Leads as CSV
        </button>
      </div>
    </div>
  )}

  {/* DETAIL MODAL */}
  {selected && (
    <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "flex-end", zIndex: 100 }} onClick={() => setSelected(null)}>
      <div style={{ background: "#fff", borderRadius: "18px 18px 0 0", padding: "20px 18px 36px", width: "100%", maxWidth: 430, margin: "0 auto", animation: "fadeUp .2s ease" }} onClick={e => e.stopPropagation()}>
        <div style={{ width: 34, height: 4, background: "#e5e7eb", borderRadius: 2, margin: "0 auto 16px" }} />
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 14 }}>
          <div style={{ fontSize: 17, fontWeight: 800, color: "#0A2540", flex: 1, paddingRight: 10, lineHeight: 1.3 }}>{selected.name}</div>
          <div style={{ background: selected.website ? "#e8f5e8" : "#fff8e1", color: selected.website ? "#2E7D32" : "#F57F17", fontSize: 9, fontWeight: 700, padding: "5px 9px", borderRadius: 7, textTransform: "uppercase" }}>
            {selected.website ? "Has Website" : "No Website"}
          </div>
        </div>

        {selected.rating && (
          <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12, padding: "8px 0", borderBottom: "1px solid #f3f4f6" }}>
            <span style={{ fontSize: 20 }}>⭐</span>
            <span><strong>{selected.rating}</strong> / 5 ({selected.reviews} reviews)</span>
          </div>
        )}

        {[
          selected.address && { icon: "📍", val: selected.address, link: `https://maps.google.com/?q=${encodeURIComponent(selected.address)}` },
          selected.phone && { icon: "📞", val: selected.phone, link: `tel:${selected.phone}` },
        ].filter(Boolean).map(({ icon, val, link }, i) => (
          <div key={i} style={{ display: "flex", gap: 9, padding: "11px 0", borderBottom: "1px solid #f3f4f6", fontSize: 13, color: "#374151", alignItems: "center" }}>
            <span>{icon}</span>
            <a href={link} style={{ color: "#0A4020", fontWeight: 600, wordBreak: "break-all", textDecoration: "none", flex: 1 }} onClick={e => e.stopPropagation()}>
              {val}
            </a>
            <button className="copy-btn" onClick={(e) => { e.stopPropagation(); copyToClipboard(val, icon === "📞" ? "Phone" : "Address"); }}>📋</button>
          </div>
        ))}

        {selected.website && (
          <a className="weblink" href={selected.website} style={{ display: "flex", marginTop: 14, padding: "12px 16px", borderRadius: 10, justifyContent: "center", fontSize: 13 }}
            onClick={e => openLink(e, selected.website)}>
            🌐 Visit Website — Find email & contact ↗
          </a>
        )}

        {!selected.website && (
          <div style={{ background: "#fff8e1", borderRadius: 11, padding: 13, marginTop: 13, fontSize: 13, color: "#92400e", lineHeight: 1.5 }}>
            🎯 <strong>Hot Lead Opportunity:</strong> No website means they need digital presence! Call them today.
          </div>
        )}

        <button onClick={() => setSelected(null)} style={{ width: "100%", marginTop: 14, padding: 14, background: "#0A4020", color: "#fff", border: "none", borderRadius: 11, fontSize: 15, fontWeight: 700, fontFamily: "'Outfit', sans-serif", cursor: "pointer" }}>
          Close
        </button>
      </div>
    </div>
  )}
</div>

);
}

Lenguaje dominante
TypeScript
Estrellas
5.2k
Forks
1.9k
Merge medio
1 h 4 min
PR fusionados (30 d)
2

Preparar el entorno

  • Sin Dockerfile ni archivo de Docker Compose
  • Tiene una plantilla de pull request
  • Sin guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de vercel/examples

Todos los issues de vercel/examples

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.