vercel / vercel/examples

LeadFinder

Open
#1,492 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
5.2k
Forks
1.8k
Avg merge
3d 12h
Merged PRs (30d)
6

Description

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>

);
}

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up โ€” it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

The body provides a large React/TypeScript component but names no file, test, entry point, or acceptance criteria. Start by locating where this component belongs in the examples repository and clarify the intended LeadFinder behavior and definition of done before changing code.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
api, frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.