import { type ReactElement, type SVGProps, useEffect, useState } from 'react'; import { APIProvider } from '@vis.gl/react-google-maps'; import { AlertCircle, ArrowRight, Briefcase, Building2, Check, LogIn, Map, MapPinned, Search, ShieldAlert, Sparkles, User, UserPlus, } from 'lucide-react'; import { Layout, type AppTab } from './components/Layout'; import { Dashboard } from './components/Dashboard'; import { MapView } from './components/MapView'; import { ResearchWorkspace } from './components/ResearchWorkspace'; import { ResultsWorkspace } from './components/ResultsWorkspace'; import type { SessionUser } from '../shared/types'; import { getLocalSessionUser, signInWithLocalAuth, signOutWithLocalAuth, signUpWithLocalAuth } from './lib/auth'; import { hasApiConfig } from './lib/api'; const GOOGLE_MAPS_API_KEY = import.meta.env.VITE_GOOGLE_MAPS_PLATFORM_KEY ?? ''; const hasValidMapsKey = Boolean(GOOGLE_MAPS_API_KEY) && GOOGLE_MAPS_API_KEY !== 'YOUR_API_KEY'; export default function App() { const [publicPage, setPublicPage] = useState<'landing' | 'auth'>(() => getPublicPageFromLocation()); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState('setup'); const [selectedJobIds, setSelectedJobIds] = useState([]); const [authError, setAuthError] = useState(null); const [authNotice, setAuthNotice] = useState(null); const [isAuthenticating, setIsAuthenticating] = useState(false); const [authMode, setAuthMode] = useState<'sign_in' | 'sign_up'>('sign_in'); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [displayName, setDisplayName] = useState(''); useEffect(() => { const handlePopState = () => { setPublicPage(getPublicPageFromLocation()); }; window.addEventListener('popstate', handlePopState); return () => { window.removeEventListener('popstate', handlePopState); }; }, []); useEffect(() => { let isMounted = true; const loadSession = async () => { try { const sessionUser = await getLocalSessionUser(); if (!isMounted) { return; } setUser(sessionUser); } catch (error) { if (!isMounted) { return; } setAuthError(error instanceof Error ? error.message : 'Failed to load session.'); } finally { if (isMounted) { setLoading(false); } } }; void loadSession(); return () => { isMounted = false; }; }, []); const handleToggleJobSelection = (jobId: string) => { setSelectedJobIds((currentJobIds) => currentJobIds.includes(jobId) ? currentJobIds.filter((currentJobId) => currentJobId !== jobId) : [...currentJobIds, jobId], ); }; const handleSelectCreatedJob = (jobId: string) => { setSelectedJobIds((currentJobIds) => (currentJobIds.includes(jobId) ? currentJobIds : [...currentJobIds, jobId])); }; const handleShowSelectedOnMap = () => { if (selectedJobIds.length === 0) { return; } setActiveTab('map'); }; const handleClearSelectedJobs = () => { setSelectedJobIds([]); }; const handleShowJobIdsOnMap = (jobIds: string[]) => { setSelectedJobIds(jobIds); setActiveTab('map'); }; const handleLogin = async () => { setAuthError(null); setAuthNotice(null); setIsAuthenticating(true); try { if (authMode === 'sign_up') { const nextUser = await signUpWithLocalAuth({ email, password, displayName: displayName.trim() || undefined, }); setUser(nextUser); setAuthNotice('Account created and signed in.'); return; } const nextUser = await signInWithLocalAuth({ email, password, }); setUser(nextUser); navigatePublicPage('landing', setPublicPage); } catch (error) { setAuthError(error instanceof Error ? error.message : 'Authentication failed.'); } finally { setIsAuthenticating(false); } }; const handleLogout = async () => { const sessionId = user?.sessionId; setAuthError(null); setAuthNotice(null); setSelectedJobIds([]); setUser(null); setActiveTab('setup'); try { await signOutWithLocalAuth(sessionId); } catch (error) { setAuthError(error instanceof Error ? error.message : 'Failed to sign out.'); } }; if (loading) { return (
); } if (!hasApiConfig) { return ( } title="Local API Config Required" description="Add your local API base URL before running the app." steps={[ 'Start the local Fastify API server.', 'Add VITE_API_BASE_URL to your local environment.', 'Ensure the API can reach your local PostgreSQL database.', 'Restart the Vite dev server after updating env vars.', ]} footer="The app now uses a local API, PostgreSQL, and local session auth." /> ); } if (!user) { const handleSetAuthMode = (mode: 'sign_in' | 'sign_up') => { setAuthMode(mode); setAuthError(null); setAuthNotice(null); }; if (publicPage === 'auth') { return ( navigatePublicPage('landing', setPublicPage)} onSetAuthMode={handleSetAuthMode} onSubmit={() => void handleLogin()} /> ); } return ( { handleSetAuthMode(mode); navigatePublicPage('auth', setPublicPage); }} /> ); } if (!hasValidMapsKey) { return ( } title="Google Maps API Key Required" description="Add a browser key for map rendering and a server key for the local search API." steps={[ 'Create a Google Maps Platform API key for the browser app.', 'Set VITE_GOOGLE_MAPS_PLATFORM_KEY locally for the frontend.', 'Set GOOGLE_MAPS_SERVER_KEY for the local API server.', 'Enable Geocoding API and Places API in Google Cloud.', ]} footer="You can create a local account without the maps key, but the workspace needs it before loading." /> ); } return ( void handleLogout()} > {activeTab === 'setup' && ( )} {activeTab === 'results' && ( )} {activeTab === 'dashboard' && } {activeTab === 'map' && } ); } function getPublicPageFromLocation(): 'landing' | 'auth' { if (typeof window === 'undefined') { return 'landing'; } return window.location.pathname === '/auth' ? 'auth' : 'landing'; } function navigatePublicPage(page: 'landing' | 'auth', setPublicPage: (page: 'landing' | 'auth') => void) { const nextPath = page === 'auth' ? '/auth' : '/'; if (typeof window !== 'undefined' && window.location.pathname !== nextPath) { window.history.pushState({}, '', nextPath); } setPublicPage(page); } function LandingPage(props: { onGoToAuth: (mode: 'sign_in' | 'sign_up') => void; }) { const { onGoToAuth } = props; const featureCards = [ { icon: Search, title: 'Research Runs', description: 'Search by city, radius, business type, and keywords without juggling spreadsheets or manual lookups.', }, { icon: MapPinned, title: 'Deep Research', description: 'Drop one pin and expand intelligently into nearby postal areas to widen market coverage.', }, { icon: Map, title: 'Clean Map View', description: 'Review returned businesses on a focused map built for operational decision-making, not map clutter.', }, { icon: Briefcase, title: 'Lead Workspace', description: 'Keep past runs, saved businesses, and mapped results in one place for repeatable prospecting.', }, ] as const; const audienceCards = [ { icon: User, title: 'Personal Use', description: 'For solo operators, freelancers, and independent prospectors who need a focused local research workflow.', }, { icon: Building2, title: 'Small Business', description: 'For agencies and local teams running prospecting every week across multiple service areas.', }, { icon: Sparkles, title: 'Enterprise', description: 'For larger organizations that need custom research volume, rollout support, and tailored operating limits.', }, ] as const; const plans = [ { name: 'Personal', audience: 'For solo operators', price: '$19', period: '/month', cta: 'Start free', featured: false, items: ['1 user workspace', '40 research runs / month', 'Map view and dashboard history', 'Email support'], }, { name: 'Small Business', audience: 'For growing local teams', price: '$79', period: '/month', cta: 'Choose Small Business', featured: true, items: ['Everything in Personal', '250 research runs / month', 'Deep research workflows', 'Extended lead history', 'Priority support'], }, { name: 'Enterprise', audience: 'For custom rollouts', price: 'Contact', period: 'sales', cta: 'Talk to sales', featured: false, items: ['Custom research volume', 'Custom onboarding plan', 'Tailored support model', 'Deployment and process guidance'], }, ] as const; return (

Leads4less

Local market research for modern teams

Built for local lead generation workflows

Research local markets, map opportunities, and build better lead lists faster.

Run targeted searches, expand coverage from a single pin, and review every result in one focused workspace designed for real prospecting operations.

See pricing

Product

One workspace for local lead generation

Leads4less keeps market research, deep area expansion, map review, and saved business history in a single operating flow so your team can move faster without losing context.

Operational Workflow

Search a market, expand intelligently, and review results visually.

Instead of stitching together Google tabs, spreadsheets, and hand-written notes, run the full prospecting loop from one product surface built for repeatable research. Launch deeper market coverage from a single map interaction while keeping research, deep research, dashboard, and map review connected in one workspace.

Best Fit

Local teams who need speed and structure

Built for recurring lead generation, territory research, and targeted market expansion.

Workflow

A clear research process from first search to mapped review

{[ ['01', 'Search', 'Start with location, radius, business type, and optional keywords.'], ['02', 'Expand', 'Use deep research to fan out from a pin into nearby postal areas.'], ['03', 'Review', 'Inspect returned businesses on a clean map and in the dashboard.'], ['04', 'Act', 'Keep high-value results organized for follow-up and repeated market work.'], ].map(([step, title, description]) => (

{step}

{title}

{description}

))}

Who It's For

Designed for personal use, small business teams, and enterprise rollouts

{audienceCards.map((audience) => (

{audience.title}

{audience.description}

))}

Pricing

Choose the plan that matches your research volume

Start small, run local research consistently, and upgrade when your market coverage or team needs expand.

{plans.map((plan) => (

{plan.name}

{plan.audience}

{plan.featured && ( Most Popular )}
{plan.price} {plan.period}
{plan.items.map((item) => (
{item}
))}
))}

Start Now

Turn local market research into a repeatable system.

Create an account, run your first research job, and build a cleaner lead workflow from day one.

Compare plans
); } function AuthPage(props: { authMode: 'sign_in' | 'sign_up'; authError: string | null; authNotice: string | null; displayName: string; email: string; isAuthenticating: boolean; password: string; onDisplayNameChange: (value: string) => void; onEmailChange: (value: string) => void; onPasswordChange: (value: string) => void; onGoHome: () => void; onSetAuthMode: (mode: 'sign_in' | 'sign_up') => void; onSubmit: () => void; }) { const { authMode, authError, authNotice, displayName, email, isAuthenticating, password, onDisplayNameChange, onEmailChange, onPasswordChange, onGoHome, onSetAuthMode, onSubmit, } = props; return (
Secure access to your lead workspace

{authMode === 'sign_up' ? 'Create your workspace and start researching local markets.' : 'Sign in and continue your lead research workflow.'}

Access research runs, deep research coverage, clean map review, and saved business history from one focused operating surface.

{[ ['Targeted search', 'Run location-based business research with clear inputs and repeatable jobs.'], ['Map review', 'Inspect returned businesses on a cleaner map built for operational use.'], ['Persistent history', 'Keep lead runs and saved businesses available whenever you come back.'], ].map(([title, description]) => (

{title}

{description}

))}

Account Access

{authMode === 'sign_up' ? 'Create account' : 'Sign in'}

{authMode === 'sign_up' ? 'Set up your account to start using Leads4less.' : 'Use your account to continue where you left off.'}

{authMode === 'sign_up' ? : }
{authError && (

Authentication Error

{authError}

)} {authNotice &&
{authNotice}
}
{ event.preventDefault(); onSubmit(); }} > {authMode === 'sign_up' && (
onDisplayNameChange(event.target.value)} placeholder="Your name" className="w-full rounded-2xl border border-stone-200 bg-stone-50 px-4 py-3 text-sm outline-none transition-all focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500" />
)}
onEmailChange(event.target.value)} placeholder="you@example.com" className="w-full rounded-2xl border border-stone-200 bg-stone-50 px-4 py-3 text-sm outline-none transition-all focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500" />
onPasswordChange(event.target.value)} placeholder="At least 6 characters" className="w-full rounded-2xl border border-stone-200 bg-stone-50 px-4 py-3 text-sm outline-none transition-all focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500" />
); } function ConfigScreen(props: { icon: ReactElement; title: string; description: string; steps: string[]; footer: string; }) { const { icon, title, description, steps, footer } = props; return (
{icon}

{title}

{description}

Follow these steps:

    {steps.map((step) => (
  1. {step}
  2. ))}

{footer}

); } function MapIcon(props: SVGProps) { return ( ); }