148 lines
5.6 KiB
TypeScript
148 lines
5.6 KiB
TypeScript
// components/pricing/ROICalculator.tsx
|
|
"use client";
|
|
|
|
import { useMemo, useState } from "react";
|
|
import { Plan } from "./types";
|
|
import { formatEUR } from "./money";
|
|
|
|
function targetUptime(planId: string) {
|
|
return planId === "mission" ? 0.9999 : planId === "growth" ? 0.9995 : 0.999;
|
|
}
|
|
|
|
export default function ROICalculator({ plans }: { plans: Plan[] }) {
|
|
const [planId, setPlanId] = useState<string>("growth");
|
|
const [monthlyLeads, setMonthlyLeads] = useState<number>(40);
|
|
const [avgLeadValue, setAvgLeadValue] = useState<number>(150);
|
|
const [currentUptime, setCurrentUptime] = useState<number>(0.995); // 99.5%
|
|
const [deliverability, setDeliverability] = useState<number>(0.85); // 85%
|
|
|
|
const plan = plans.find((p) => p.id === planId)!;
|
|
|
|
const result = useMemo(() => {
|
|
const target = targetUptime(planId);
|
|
const uptimeGain = Math.max(0, target - currentUptime); // fraction
|
|
// assume +10pp absolute deliverability improvement (conservative)
|
|
const deliverabilityGain = Math.max(0, 0.1 + (deliverability < 0.9 ? 0 : -0.02));
|
|
const recoveredLeads =
|
|
monthlyLeads * uptimeGain + monthlyLeads * (deliverabilityGain * (1 - deliverability));
|
|
const recoveredRevenue = recoveredLeads * avgLeadValue;
|
|
const planCost = plan.monthlyPrice;
|
|
const roi = planCost ? (recoveredRevenue - planCost) / planCost : 0;
|
|
|
|
return {
|
|
target,
|
|
uptimeGain,
|
|
deliverabilityGain,
|
|
recoveredLeads,
|
|
recoveredRevenue,
|
|
planCost,
|
|
roi,
|
|
};
|
|
}, [planId, plan, monthlyLeads, avgLeadValue, currentUptime, deliverability]);
|
|
|
|
return (
|
|
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
|
<h2 className="text-xl font-semibold">Estimate your ROI</h2>
|
|
<p className="mt-1 text-sm text-gray-600">
|
|
A simple, conservative model based on uptime & deliverability improvements.
|
|
</p>
|
|
|
|
<div className="mt-6 grid gap-6 md:grid-cols-5">
|
|
<div className="md:col-span-1">
|
|
<label className="block text-sm font-medium text-gray-700">Plan</label>
|
|
<select
|
|
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
|
|
value={planId}
|
|
onChange={(e) => setPlanId(e.target.value)}
|
|
>
|
|
{plans.map((p) => (
|
|
<option key={p.id} value={p.id}>
|
|
{p.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="md:col-span-1">
|
|
<label className="block text-sm font-medium text-gray-700">Monthly leads</label>
|
|
<input
|
|
type="number"
|
|
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
|
|
min={0}
|
|
value={monthlyLeads}
|
|
onChange={(e) => setMonthlyLeads(Number(e.target.value || 0))}
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-1">
|
|
<label className="block text-sm font-medium text-gray-700">Avg. lead value (€)</label>
|
|
<input
|
|
type="number"
|
|
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
|
|
min={0}
|
|
value={avgLeadValue}
|
|
onChange={(e) => setAvgLeadValue(Number(e.target.value || 0))}
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-1">
|
|
<label className="block text-sm font-medium text-gray-700">Current uptime</label>
|
|
<input
|
|
type="number"
|
|
step="0.001"
|
|
min={0.95}
|
|
max={0.9999}
|
|
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
|
|
value={currentUptime}
|
|
onChange={(e) => setCurrentUptime(Number(e.target.value || 0))}
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500">e.g., 0.995 = 99.5%</p>
|
|
</div>
|
|
|
|
<div className="md:col-span-1">
|
|
<label className="block text-sm font-medium text-gray-700">Inbox rate</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
min={0.5}
|
|
max={1}
|
|
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
|
|
value={deliverability}
|
|
onChange={(e) => setDeliverability(Number(e.target.value || 0))}
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500">e.g., 0.85 = 85%</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 grid gap-6 md:grid-cols-3">
|
|
<div className="rounded-xl border border-gray-200 p-4">
|
|
<p className="text-sm text-gray-600">Target uptime (plan)</p>
|
|
<p className="mt-1 text-2xl font-semibold">{(result.target * 100).toFixed(3)}%</p>
|
|
</div>
|
|
<div className="rounded-xl border border-gray-200 p-4">
|
|
<p className="text-sm text-gray-600">Recovered leads (est.)</p>
|
|
<p className="mt-1 text-2xl font-semibold">
|
|
{Math.max(0, Math.round(result.recoveredLeads))}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-xl border border-gray-200 p-4">
|
|
<p className="text-sm text-gray-600">Monthly impact</p>
|
|
<p className="mt-1 text-2xl font-semibold">{formatEUR(Math.max(0, Math.round(result.recoveredRevenue)))}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 rounded-xl border border-emerald-200 bg-emerald-50 p-4">
|
|
<p className="text-sm font-semibold text-emerald-900">Estimated ROI</p>
|
|
<p className="mt-1 text-lg font-semibold text-emerald-900">
|
|
{result.planCost
|
|
? `${Math.round(result.roi * 100)}%`
|
|
: "—"}
|
|
</p>
|
|
<p className="mt-1 text-sm text-emerald-900">
|
|
Impact is an estimate; we provide before/after proof during your first month.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|