feat: add billing plan foundations and refresh LocaleScope pricing
Introduce a shared pricing and entitlement model so plan metadata can drive public pricing and future subscription enforcement. Rebrand the product to LocaleScope and align the UI copy around market intelligence and business research workflows.
This commit is contained in:
+102
-102
@@ -22,6 +22,7 @@ import { MapView } from './components/MapView';
|
||||
import { ResearchWorkspace } from './components/ResearchWorkspace';
|
||||
import { ResultsWorkspace } from './components/ResultsWorkspace';
|
||||
import { Alert, Badge, Button, Card, FieldLabel, Input, Surface } from './components/ui';
|
||||
import { getPlanCardBullets, getPlanDisplayMeta, getPublicPricingPlans } from '../shared/billing/plans';
|
||||
import type { SessionUser } from '../shared/types';
|
||||
import { getLocalSessionUser, signInWithLocalAuth, signOutWithLocalAuth, signUpWithLocalAuth } from './lib/auth';
|
||||
import { hasApiConfig } from './lib/api';
|
||||
@@ -301,6 +302,26 @@ function navigatePublicPage(page: 'landing' | 'auth', setPublicPage: (page: 'lan
|
||||
setPublicPage(page);
|
||||
}
|
||||
|
||||
function formatPlanPrice(priceCents: number | null, currencyCode: string) {
|
||||
if (priceCents === null) {
|
||||
return 'Custom';
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: currencyCode,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(priceCents / 100);
|
||||
}
|
||||
|
||||
function formatPlanPeriod(billingInterval: 'monthly' | 'annual' | 'custom', contactSalesRequired: boolean) {
|
||||
if (contactSalesRequired || billingInterval === 'custom') {
|
||||
return 'pricing';
|
||||
}
|
||||
|
||||
return billingInterval === 'annual' ? '/year' : '/month';
|
||||
}
|
||||
|
||||
function LandingPage(props: {
|
||||
onGoToAuth: (mode: 'sign_in' | 'sign_up') => void;
|
||||
}) {
|
||||
@@ -310,72 +331,44 @@ function LandingPage(props: {
|
||||
{
|
||||
icon: Search,
|
||||
title: 'Research Runs',
|
||||
description: 'Search by city, radius, business type, and keywords without juggling spreadsheets or manual lookups.',
|
||||
description: 'Research local markets 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.',
|
||||
description: 'Drop one pin and expand intelligently into nearby postal areas to uncover surrounding territory opportunities.',
|
||||
},
|
||||
{
|
||||
icon: Map,
|
||||
title: 'Clean Map View',
|
||||
description: 'Review returned businesses on a focused map built for operational decision-making, not map clutter.',
|
||||
title: 'Territory Mapping',
|
||||
description: 'Review returned businesses on a focused map built for territory analysis and operational decision-making.',
|
||||
},
|
||||
{
|
||||
icon: Briefcase,
|
||||
title: 'Lead Workspace',
|
||||
description: 'Keep past runs, saved businesses, and mapped results in one place for repeatable prospecting.',
|
||||
title: 'Operational Workspace',
|
||||
description: 'Keep past runs, saved businesses, and mapped results in one place for repeatable prospecting workflows.',
|
||||
},
|
||||
] as const;
|
||||
|
||||
const audienceCards = [
|
||||
{
|
||||
icon: User,
|
||||
title: 'Personal Use',
|
||||
description: 'For solo operators, freelancers, and independent prospectors who need a focused local research workflow.',
|
||||
title: 'Starter Teams',
|
||||
description: 'For solo operators, freelancers, and small local teams that need a structured market research workflow.',
|
||||
},
|
||||
{
|
||||
icon: Building2,
|
||||
title: 'Small Business',
|
||||
description: 'For agencies and local teams running prospecting every week across multiple service areas.',
|
||||
title: 'Growth Teams',
|
||||
description: 'For agencies and outbound teams running recurring territory research across multiple service areas.',
|
||||
},
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: 'Enterprise',
|
||||
description: 'For larger organizations that need custom research volume, rollout support, and tailored operating limits.',
|
||||
description: 'For larger organizations that need custom market intelligence capacity, rollout support, and tailored operating controls.',
|
||||
},
|
||||
] 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;
|
||||
const pricingPlans = getPublicPricingPlans();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(16,185,129,0.08),_transparent_28%),linear-gradient(180deg,#fafaf9_0%,#f5f5f4_100%)] text-stone-900">
|
||||
@@ -387,8 +380,8 @@ function LandingPage(props: {
|
||||
<Briefcase className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-bold tracking-tight">Leads4less</p>
|
||||
<p className="text-sm text-stone-500">Local market research for modern teams</p>
|
||||
<p className="text-lg font-bold tracking-tight">LocaleScope</p>
|
||||
<p className="text-sm text-stone-500">Local market intelligence for modern outbound teams</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -421,15 +414,15 @@ function LandingPage(props: {
|
||||
<div className="space-y-8">
|
||||
<Badge variant="primary" className="px-4 py-2 text-sm">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Built for local lead generation workflows
|
||||
Geographic prospecting intelligence platform
|
||||
</Badge>
|
||||
|
||||
<div className="space-y-5">
|
||||
<h1 className="max-w-4xl text-5xl font-bold tracking-tight text-stone-950 sm:text-6xl">
|
||||
Research local markets, map opportunities, and build better lead lists faster.
|
||||
Discover underserved markets, map territories, and turn local research into repeatable intelligence.
|
||||
</h1>
|
||||
<p className="max-w-3xl text-lg leading-8 text-stone-600 sm:text-xl">
|
||||
Run targeted searches, expand coverage from a single pin, and review every result in one focused workspace designed for real prospecting operations.
|
||||
Run targeted local research, expand coverage from a single pin, and review every result in one focused workspace built for modern prospecting operations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -451,9 +444,9 @@ function LandingPage(props: {
|
||||
<section id="product" className="rounded-[2rem] border border-stone-200 bg-white px-6 py-10 shadow-sm sm:px-8 lg:px-10">
|
||||
<div className="max-w-3xl">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-emerald-600">Product</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight text-stone-900 sm:text-4xl">One workspace for local lead generation</h2>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight text-stone-900 sm:text-4xl">One workspace for local market intelligence</h2>
|
||||
<p className="mt-4 text-base leading-8 text-stone-600">
|
||||
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.
|
||||
LocaleScope keeps market research, deep area expansion, map review, and saved business history in a single operating flow so teams can move faster without losing context.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -462,13 +455,13 @@ function LandingPage(props: {
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-emerald-300">Operational Workflow</p>
|
||||
<p className="mt-4 text-2xl font-bold tracking-tight">Search a market, expand intelligently, and review results visually.</p>
|
||||
<p className="mt-4 max-w-2xl text-sm leading-7 text-stone-300">
|
||||
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.
|
||||
Instead of stitching together Google tabs, spreadsheets, and hand-written notes, run the full territory research loop from one product surface built for repeatable analysis. Launch deeper market coverage from a single map interaction while keeping research, deep research, dashboard, and map review connected in one workspace.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-3xl border border-stone-200 bg-stone-50 p-6">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-stone-500">Best Fit</p>
|
||||
<p className="mt-4 text-2xl font-bold tracking-tight text-stone-900">Local teams who need speed and structure</p>
|
||||
<p className="mt-4 text-sm leading-7 text-stone-600">Built for recurring lead generation, territory research, and targeted market expansion.</p>
|
||||
<p className="mt-4 text-sm leading-7 text-stone-600">Built for recurring territory research, geographic prospecting, and targeted market expansion.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -498,7 +491,7 @@ function LandingPage(props: {
|
||||
<section className="rounded-[2rem] border border-stone-200 bg-white px-6 py-10 shadow-sm sm:px-8 lg:px-10">
|
||||
<div className="max-w-3xl">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-emerald-600">Who It's For</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight text-stone-900 sm:text-4xl">Designed for personal use, small business teams, and enterprise rollouts</h2>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight text-stone-900 sm:text-4xl">Designed for growing prospecting teams and enterprise rollouts</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-4 lg:grid-cols-3">
|
||||
@@ -517,61 +510,68 @@ function LandingPage(props: {
|
||||
<section id="pricing" className="py-16">
|
||||
<div className="max-w-3xl">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-emerald-600">Pricing</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight text-stone-900 sm:text-4xl">Choose the plan that matches your research volume</h2>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight text-stone-900 sm:text-4xl">Choose the plan that matches your market intelligence workflow</h2>
|
||||
<p className="mt-4 text-base leading-8 text-stone-600">
|
||||
Start small, run local research consistently, and upgrade when your market coverage or team needs expand.
|
||||
Start with focused territory research, then upgrade as your operational scale, collaboration needs, and research capacity expand.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-5 xl:grid-cols-3">
|
||||
{plans.map((plan) => (
|
||||
<div
|
||||
key={plan.name}
|
||||
className={`rounded-[2rem] border p-7 shadow-sm ${
|
||||
plan.featured ? 'border-emerald-300 bg-emerald-50/60 shadow-emerald-100' : 'border-stone-200 bg-white'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-xl font-bold tracking-tight text-stone-900">{plan.name}</p>
|
||||
<p className="mt-2 text-sm text-stone-600">{plan.audience}</p>
|
||||
</div>
|
||||
{plan.featured && (
|
||||
<span className="rounded-full bg-emerald-600 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white">
|
||||
Most Popular
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{pricingPlans.map((plan) => {
|
||||
const display = getPlanDisplayMeta(plan.code);
|
||||
const isFeatured = display.badgeLabel === 'Best Value';
|
||||
|
||||
<div className="mt-8 flex items-end gap-1">
|
||||
<span className="text-4xl font-bold tracking-tight text-stone-950">{plan.price}</span>
|
||||
<span className="pb-1 text-sm text-stone-500">{plan.period}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onGoToAuth(plan.name === 'Enterprise' ? 'sign_in' : 'sign_up')}
|
||||
className={`mt-8 inline-flex w-full items-center justify-center rounded-2xl px-4 py-3 text-sm font-semibold transition ${
|
||||
plan.featured
|
||||
? 'bg-emerald-600 text-white hover:bg-emerald-700'
|
||||
: 'border border-stone-200 bg-white text-stone-800 hover:bg-stone-50'
|
||||
return (
|
||||
<div
|
||||
key={plan.code}
|
||||
className={`rounded-[2rem] border p-7 shadow-sm ${
|
||||
isFeatured ? 'border-emerald-300 bg-emerald-50/60 shadow-emerald-100' : 'border-stone-200 bg-white'
|
||||
}`}
|
||||
>
|
||||
{plan.cta}
|
||||
</button>
|
||||
|
||||
<div className="mt-8 space-y-3">
|
||||
{plan.items.map((item) => (
|
||||
<div key={item} className="flex items-start gap-3 text-sm text-stone-700">
|
||||
<div className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-emerald-700">
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<span>{item}</span>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-xl font-bold tracking-tight text-stone-900">{plan.name}</p>
|
||||
<p className="mt-2 text-sm text-stone-600">{display.audience}</p>
|
||||
</div>
|
||||
))}
|
||||
{display.badgeLabel ? (
|
||||
<span className="rounded-full bg-emerald-600 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white">
|
||||
{display.badgeLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-end gap-1">
|
||||
<span className="text-4xl font-bold tracking-tight text-stone-950">{formatPlanPrice(plan.priceCents, plan.currencyCode)}</span>
|
||||
<span className="pb-1 text-sm text-stone-500">{formatPlanPeriod(plan.billingInterval, plan.contactSalesRequired)}</span>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-sm leading-7 text-stone-600">{display.summary}</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onGoToAuth(display.ctaMode)}
|
||||
className={`mt-8 inline-flex w-full items-center justify-center rounded-2xl px-4 py-3 text-sm font-semibold transition ${
|
||||
isFeatured
|
||||
? 'bg-emerald-600 text-white hover:bg-emerald-700'
|
||||
: 'border border-stone-200 bg-white text-stone-800 hover:bg-stone-50'
|
||||
}`}
|
||||
>
|
||||
{display.ctaLabel}
|
||||
</button>
|
||||
|
||||
<div className="mt-8 space-y-3">
|
||||
{getPlanCardBullets(plan.code).map((item) => (
|
||||
<div key={item} className="flex items-start gap-3 text-sm text-stone-700">
|
||||
<div className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 text-emerald-700">
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -581,7 +581,7 @@ function LandingPage(props: {
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-emerald-300">Start Now</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight sm:text-4xl">Turn local market research into a repeatable system.</h2>
|
||||
<p className="mt-4 text-base leading-8 text-stone-300">
|
||||
Create an account, run your first research job, and build a cleaner lead workflow from day one.
|
||||
Create an account, run your first research job, and start building a repeatable local intelligence workflow from day one.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
@@ -648,8 +648,8 @@ function AuthPage(props: {
|
||||
<Briefcase className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-bold tracking-tight">Leads4less</p>
|
||||
<p className="text-sm text-stone-500">Local market research for modern teams</p>
|
||||
<p className="text-lg font-bold tracking-tight">LocaleScope</p>
|
||||
<p className="text-sm text-stone-500">Local market intelligence for modern outbound teams</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -665,12 +665,12 @@ function AuthPage(props: {
|
||||
<div className="space-y-8">
|
||||
<Badge variant="primary" className="px-4 py-2 text-sm">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Secure access to your lead workspace
|
||||
Secure access to your intelligence workspace
|
||||
</Badge>
|
||||
|
||||
<div className="space-y-5">
|
||||
<h1 className="max-w-3xl text-5xl font-bold tracking-tight text-stone-950 sm:text-6xl">
|
||||
{authMode === 'sign_up' ? 'Create your workspace and start researching local markets.' : 'Sign in and continue your lead research workflow.'}
|
||||
{authMode === 'sign_up' ? 'Create your workspace and start researching local markets.' : 'Sign in and continue your market intelligence workflow.'}
|
||||
</h1>
|
||||
<p className="max-w-2xl text-lg leading-8 text-stone-600 sm:text-xl">
|
||||
Access research runs, deep research coverage, clean map review, and saved business history from one focused operating surface.
|
||||
@@ -681,7 +681,7 @@ function AuthPage(props: {
|
||||
{[
|
||||
['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.'],
|
||||
['Persistent history', 'Keep research runs and saved businesses available whenever you come back.'],
|
||||
].map(([title, description]) => (
|
||||
<Surface key={title} className="p-5">
|
||||
<p className="text-base font-bold tracking-tight text-stone-900">{title}</p>
|
||||
@@ -699,7 +699,7 @@ function AuthPage(props: {
|
||||
{authMode === 'sign_up' ? 'Create account' : 'Sign in'}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-stone-600">
|
||||
{authMode === 'sign_up' ? 'Set up your account to start using Leads4less.' : 'Use your account to continue where you left off.'}
|
||||
{authMode === 'sign_up' ? 'Set up your account to start using LocaleScope.' : 'Use your account to continue where you left off.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-stone-100 p-3 text-stone-900">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Building2, CreditCard, Loader2, Shield, Users } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getPlanByCode } from '../../shared/billing/plans';
|
||||
import type { AccountPageData, AppUser } from '../../shared/types';
|
||||
import { getAccountPageData, updateAccountProfile } from '../lib/account';
|
||||
import { Alert, Badge, Button, Card, FieldLabel, Input, LoadingState, PageContainer, PageShell, SectionHeader, StatCard } from './ui';
|
||||
@@ -99,12 +100,14 @@ export function AccountPage({ user, onUserUpdated }: AccountPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const activePlan = account.billing.planCode ? getPlanByCode(account.billing.planCode) : null;
|
||||
|
||||
return (
|
||||
<PageShell>
|
||||
<PageContainer>
|
||||
<SectionHeader
|
||||
title="Account"
|
||||
description="Manage your profile, workspace, and upcoming billing settings."
|
||||
description="Manage your profile, workspace, and subscription settings."
|
||||
/>
|
||||
|
||||
{error ? <Alert variant="error">{error}</Alert> : null}
|
||||
@@ -185,7 +188,7 @@ export function AccountPage({ user, onUserUpdated }: AccountPageProps) {
|
||||
<Badge>{account.workspace.memberCount === 1 ? '1 member' : `${account.workspace.memberCount} members`}</Badge>
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-stone-600">
|
||||
This workspace is the foundation for future team access, billing, and shared company management.
|
||||
This workspace is the foundation for future team access, subscriptions, and shared territory research workflows.
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
@@ -196,7 +199,7 @@ export function AccountPage({ user, onUserUpdated }: AccountPageProps) {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-stone-500">Plan & Billing</p>
|
||||
<h3 className="text-lg font-semibold text-stone-950">Billing coming soon</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-950">{activePlan ? activePlan.name : 'Subscription foundation in progress'}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-stone-600">{account.billing.message}</p>
|
||||
|
||||
@@ -246,7 +246,7 @@ export function BasicResultsView({ user, selectedJobIds, onToggleJobSelection, o
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between border-t border-stone-100 pt-4 text-sm font-medium text-stone-500">
|
||||
<span>{job.totalResults === 1 ? '1 lead found' : `${job.totalResults} leads found`}</span>
|
||||
<span>{job.totalResults === 1 ? '1 business found' : `${job.totalResults} businesses found`}</span>
|
||||
<span className={isSelected ? 'text-emerald-700' : 'text-stone-400'}>{isSelected ? 'Included in selection' : 'Available to select'}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -117,7 +117,7 @@ export function Dashboard({ user }: DashboardProps) {
|
||||
: 0;
|
||||
|
||||
return [
|
||||
{ name: 'Total Leads', value: total, icon: Briefcase },
|
||||
{ name: 'Saved Businesses', value: total, icon: Briefcase },
|
||||
{ name: 'With Website', value: withWebsite, icon: Globe },
|
||||
{ name: 'With Phone', value: withPhone, icon: Phone },
|
||||
{ name: 'Avg Rating', value: avgRating.toFixed(1), icon: Star },
|
||||
@@ -149,7 +149,7 @@ export function Dashboard({ user }: DashboardProps) {
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `leads_export_${new Date().toISOString().split('T')[0]}.csv`);
|
||||
link.setAttribute('download', `localescope_export_${new Date().toISOString().split('T')[0]}.csv`);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
@@ -180,8 +180,8 @@ export function Dashboard({ user }: DashboardProps) {
|
||||
<PageShell>
|
||||
<PageContainer>
|
||||
<SectionHeader
|
||||
title="Lead Dashboard"
|
||||
description="Browse saved search results from your local workspace and export targeted lead lists."
|
||||
title="Research Dashboard"
|
||||
description="Browse saved search results from your workspace and export targeted business datasets."
|
||||
actions={
|
||||
<Button onClick={handleExport} size="lg" className="w-full sm:w-auto">
|
||||
<Download className="h-5 w-5" />
|
||||
@@ -238,7 +238,7 @@ export function Dashboard({ user }: DashboardProps) {
|
||||
</Card>
|
||||
|
||||
{filteredBusinesses.length === 0 ? (
|
||||
<EmptyState title="No leads found matching your filters." description="Try adjusting your search term or filters to see more businesses." />
|
||||
<EmptyState title="No businesses found matching your filters." description="Try adjusting your search term or filters to see more results." />
|
||||
) : (
|
||||
<Card className="overflow-hidden p-0">
|
||||
<div className="md:hidden">
|
||||
@@ -353,7 +353,7 @@ export function Dashboard({ user }: DashboardProps) {
|
||||
<p className="text-sm text-stone-500">
|
||||
Showing <span className="font-medium">{(currentPage - 1) * itemsPerPage + 1}</span> to{' '}
|
||||
<span className="font-medium">{Math.min(currentPage * itemsPerPage, filteredBusinesses.length)}</span> of{' '}
|
||||
<span className="font-medium">{filteredBusinesses.length}</span> leads
|
||||
<span className="font-medium">{filteredBusinesses.length}</span> businesses
|
||||
</p>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
|
||||
@@ -90,7 +90,7 @@ export function DeepResearchResultsView({ onShowBatchOnMap }: DeepResearchResult
|
||||
<p className="mt-1 text-base font-bold text-stone-900">{batch.childJobCount}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-stone-400">Leads</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-stone-400">Businesses</p>
|
||||
<p className="mt-1 text-base font-bold text-stone-900">{batch.totalResults}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ export function Layout({ user, activeTab, setActiveTab, onLogout, children }: La
|
||||
<Briefcase className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-stone-950">Leads4less</p>
|
||||
<p className="truncate text-sm font-semibold text-stone-950">LocaleScope</p>
|
||||
<p className="truncate text-xs text-stone-500">{activeNavigationItem.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,8 +54,8 @@ export function Layout({ user, activeTab, setActiveTab, onLogout, children }: La
|
||||
<Briefcase className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-semibold tracking-tight text-stone-950">Leads4less</p>
|
||||
<p className="text-sm text-stone-500">Professional lead research workspace</p>
|
||||
<p className="text-lg font-semibold tracking-tight text-stone-950">LocaleScope</p>
|
||||
<p className="text-sm text-stone-500">Operational local market intelligence workspace</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ export function MapView({ user, jobIds }: MapViewProps) {
|
||||
const nextBusinesses = selectedJobCount > 0 ? await listBusinessesForJobs(user.id, jobIds ?? []) : await listBusinesses(user.id);
|
||||
setBusinesses(nextBusinesses);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load map leads.');
|
||||
setError(err instanceof Error ? err.message : 'Failed to load map results.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -105,10 +105,10 @@ export function MapView({ user, jobIds }: MapViewProps) {
|
||||
<div className="w-full max-w-lg">
|
||||
<EmptyState
|
||||
icon={MapPin}
|
||||
title="No leads to show on the map"
|
||||
title="No businesses to show on the map"
|
||||
description={selectedJobCount > 0
|
||||
? 'The selected research jobs do not have saved map results yet. Try completed jobs or run the research again.'
|
||||
: 'No saved leads are available yet. Run a research job to populate the map.'}
|
||||
: 'No saved businesses are available yet. Run a research job to populate the map.'}
|
||||
/>
|
||||
{error ? <Alert variant="error" className="mt-4">{error}</Alert> : null}
|
||||
</div>
|
||||
@@ -210,11 +210,11 @@ export function MapView({ user, jobIds }: MapViewProps) {
|
||||
<h4 className="mb-2 text-sm font-semibold text-stone-950">Map Summary</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-stone-500">Total Leads on Map</span>
|
||||
<span className="text-stone-500">Businesses on Map</span>
|
||||
<span className="font-bold text-emerald-600">{businesses.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-stone-500">Selected Lead</span>
|
||||
<span className="text-stone-500">Selected Business</span>
|
||||
<span className="max-w-[140px] truncate font-bold text-stone-900 lg:max-w-[120px]">{selected ? selected.name : 'None'}</span>
|
||||
</div>
|
||||
{selectedJobCount > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user