1const n=`import { useState, forwardRef } from "react"; 2import { Link } from "react-router-dom"; 3import { useTranslation } from "react-i18next"; 4import { Slider } from "@/components/ui/slider"; 5import { Button } from "@/components/ui/button"; 6import { Card, CardContent } from "@/components/ui/card"; 7import { Input } from "@/components/ui/input"; 8import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; 9import { Info, Mail, Loader2, CheckCircle2 } from "lucide-react"; 10import { toast } from "sonner"; 11import { supabase } from "@/integrations/supabase/client"; 12import { trackROIReport } from "@/lib/gtm"; 13import { useLanguage } from "@/hooks/useLanguage"; 14import bottlesIllustration from "@/assets/bottles-illustration.png"; 15import { 16 ZA_SETUP_FEE, 17 ZA_COMMITMENT_MONTHS, 18 calcZaMonthly, 19 recommendedHubs, 20 fmtZar, 21} from "@/data/za-pricing"; 22 23/* ââ ZA Pricing Logic ââââââââââââââââââââââââââââââââââââââââ */ 24 25const SETUP_FEE = ZA_SETUP_FEE; 26const TOTS_PER_BOTTLE = 30; 27 28/** Calculate Pourify monthly cost - uses single source of truth in src/data/za-pricing.ts */ 29const calcPourifyMonthly = (totalBrands: number): number => 30 calcZaMonthly(totalBrands, recommendedHubs(totalBrands)); 31 32/** 33 * Calculate Competitor monthly cost. Anchored to the same baseline as 34 * Pourify (R950 base + R300/extra hub) so the comparison is apples-to-apples, 35 * with a flat per-pourer mark-up on top to reflect typical competitor pricing. 36 */ 37const calcCompetitorMonthly = (totalBrands: number): number => { 38 const baseCost = 950; 39 const additional = Math.max(0, totalBrands - 5) * 195; 40 const extraHubs = Math.max(0, recommendedHubs(totalBrands) - 1); 41 return baseCost + additional + extraHubs * 300; 42}; 43 44const fmt = fmtZar; 45 46/* ââ Component âââââââââââââââââââââââââââââââââââââââââââââââ */ 47 48const ZAROICalculator = () => { 49 const { t } = useTranslation(); 50 const { localePath, lang } = useLanguage(); 51 52 const [brands, setBrands] = useState(20); 53 const [bottlesPerBrand, setBottlesPerBrand] = useState(5); 54 const [pricePerTot, setPricePerTot] = useState(40); 55 const [lossPercent, setLossPercent] = useState(12); 56 57 // Calculations 58 const weeklyGross = brands * bottlesPerBrand * TOTS_PER_BOTTLE * pricePerTot; 59 const annualLoss = weeklyGross * 52 * (lossPercent / 100); 60 61 const pourifyEfficiency = 0.05; 62 const competitorEfficiency = 0.10; 63 64 const pourifyGrossSavings = annualLoss - (weeklyGross * 52 * pourifyEfficiency); 65 const competitorGrossSavings = annualLoss - (weeklyGross * 52 * competitorEfficiency); 66 67 const pourifyMonthly = calcPourifyMonthly(brands); 68 const competitorMonthly = calcCompetitorMonthly(brands); 69 70 const pourifyAnnualCost = pourifyMonthly * 12; 71 const competitorAnnualCost = competitorMonthly * 12; 72 73 const pourifyNetSavings = Math.max(0, pourifyGrossSavings - pourifyAnnualCost); 74 const competitorNetSavings = Math.max(0, competitorGrossSavings - competitorAnnualCost); 75 const pourifyDifference = pourifyNetSavings - competitorNetSavings; 76 77 return ( 78 <div className="container-pourify"> 79 <div className="grid lg:grid-cols-[1fr_auto] gap-10 items-start"> 80 <div className="max-w-3xl"> 81 <div className="mb-8"> 82 <p className="text-sm font-medium text-accent mb-2">{t("roi.badge")}</p> 83 <h2 className="text-3xl md:text-5xl font-bold mb-3 text-foreground"> 84 Calculate your savings 85 </h2> 86 <p className="text-muted-foreground text-lg"> 87 See how much Pourify can save your venue - and how we compare to the competition. 88 </p> 89 </div> 90 91 {/* Sliders */} 92 <div className="space-y-5 mb-6"> 93 <SliderRow label="Number of brands" value={brands} min={5} max={100} display={\`\${brands}\`} onChange={setBrands} /> 94 <SliderRow label="Bottles poured per brand per week" value={bottlesPerBrand} min={1} max={50} display={\`\${bottlesPerBrand}\`} onChange={setBottlesPerBrand} /> 95 <SliderRow label="Price per tot" value={pricePerTot} min={25} max={150} display={fmt(pricePerTot)} onChange={setPricePerTot} />
95 96 <SliderRow 97 label="Estimated current loss" 98 value={lossPercent} 99 min={5} 100 max={50} 101 display={\`\${lossPercent}%\`} 102 onChange={setLossPercent} 103 hint="Industry average: 9â15%" 104 presets={[ 105 { label: "Bars", value: 12 }, 106 { label: "Restaurants", value: 10 }, 107 { label: "Hotels", value: 9 }, 108 { label: "Nightclubs", value: 15 }, 109 ]} 110 /> 111 </div> 112 113 {/* Formula breakdown */} 114 <div className="flex flex-wrap items-center justify-center gap-2 mb-6"> 115 <FormulaChip value={\`\${brands}\`} label="Brands" /> 116 <span className="text-muted-foreground font-medium">Ã</span> 117 <FormulaChip value={\`\${bottlesPerBrand}\`} label="Bottles/wk" /> 118 <span className="text-muted-foreground font-medium">Ã</span> 119 <FormulaChip value="30" label="Tots/bottle" /> 120 <span className="text-muted-foreground font-medium">Ã</span> 121 <FormulaChip value={fmt(pricePerTot)} label="Per tot" /> 122 <span className="text-muted-foreground font-medium">Ã</span> 123 <FormulaChip value={\`\${lossPercent}%\`} label="Loss" /> 124 <span className="text-muted-foreground font-medium">Ã</span> 125 <FormulaChip value="52" label="Weeks" /> 126 <span className="text-muted-foreground font-medium">=</span> 127 <FormulaChip value={fmt(annualLoss)} label="Annual loss" highlight /> 128 </div> 129 130 {/* Comparison Table */} 131 <div className="rounded-xl overflow-hidden border border-border mb-6"> 132 <table className="w-full text-sm"> 133 <thead> 134 <tr className="bg-foreground text-background"> 135 <th className="text-left p-3 font-medium">Metric</th> 136 <th className="text-right p-3 font-medium"> 137 <span className="inline-flex items-center gap-1.5"> 138 <span className="w-2 h-2 rounded-full bg-accent" /> Pourify 139 </span> 140 </th> 141 <th className="text-right p-3 font-medium">Competitor</th> 142 <th className="text-right p-3 font-medium">Difference</th> 143 </tr> 144 </thead> 145 <tbody className="divide-y divide-border"> 146 <tr className="bg-background/60"> 147 <td className="p-3 text-muted-foreground">Residual loss rate</td> 148 <td className="p-3 text-right font-semibold text-foreground">5%</td> 149 <td className="p-3 text-right text-foreground">10%</td> 150 <td className="p-3 text-right text-accent font-semibold">5%</td> 151 </tr> 152 <tr className="bg-background/40"> 153 <td className="p-3 text-muted-foreground">Gross annual savings</td> 154 <td className="p-3 text-right font-semibold text-foreground">{fmt(pourifyGrossSavings)}</td> 155 <td className="p-3 text-right text-foreground">{fmt(competitorGrossSavings)}</td> 156 <td className="p-3 text-right text-accent font-semibold">{fmt(Math.abs(pourifyGrossSavings - competitorGrossSavings))}</td> 157 </tr> 158 <tr className="bg-background/60"> 159 <td className="p-3 text-muted-foreground">Monthly Plan</td> 160 <td className="p-3 text-right font-semibold text-foreground">{fmt(pourifyMonthly)}/mo</td> 161 <td className="p-3 text-right text-foreground">{fmt(competitorMonthly)}/mo</td> 162 <td className="p-3 text-right text-accent font-semibold">{fmt(Math.abs(pourifyMonthly - competitorMonthly))}/mo</td> 163 </tr> 164 <tr className="bg-background/40"> 165 <td className="p-3 text-muted-foreground">Annual cost</td> 166 <td className="p-3 text-right font-semibold text-foreground">{fmt(pourifyAnnualCost)}/yr</td> 167 <td className="p-3 text-right text-foreground">{fmt(competitorAnnualCost)}/yr</td> 168 <td className="p-3 text-right text-accent font-semibold">{fmt(Math.abs(pourifyAnnualCost - competitorAnnualCost))}/yr</td> 169 </tr> 170 <tr className="bg-background/60"> 171 <td className="p-3 text-muted-foreground">Setup fee</td> 172 <td className="p-3 text-right font-semibold text-foreground">{fmt(SETUP_FEE)}</td> 173 <td className="p-3 text-right text-foreground">â</td> 174 <td className="p-3 text-right text-muted-foreground">â</td> 175 </tr> 176 <tr className="bg-accent/10"> 177 <td className="p-3 font-bold text-foreground">Net annual savings</td> 178 <td className="p-3 text-right font-bold text-accent text-lg">{fmt(pourifyNetSavings)}</td> 179 <td className="p-3 text-right font-bold text-foreground">{fmt(competitorNetSavings)}</td> 180 <td className="p-3 text-right font-bold text-accent text-lg">{fmt(Math.abs(pourifyDifference))}</td> 181 </tr> 182 </tbody> 183 </table> 184 </div> 185 186 {/* Highlight cards */} 187 <div className="grid sm:grid-cols-3 gap-3 mb-6"> 188 <div className="rounded-xl bg-primary text-primary-foreground p-5 text-center"> 189 <p className="text-xs opacity-70 mb-1">Your annual loss</p> 190 <p className="text-3xl font-bold text-accent">{fmt(annualLoss)}</p> 191 </div> 192 <div className="rounded-xl bg-primary text-primary-foreground p-5 text-center"> 193 <p className="text-xs opacity-70 mb-1">Net savings with Pourify</p> 194 <p className="text-3xl font-bold text-accent">{fmt(pourifyNetSavings)}</p> 195 <p className="text-xs opacity-70 mt-1">per year</p> 196 </div> 197 <div className="rounded-xl bg-primary text-primary-foreground p-5 text-center"> 198 <p className="text-xs opacity-70 mb-1">Pourify advantage</p> 199 <p className="text-3xl font-bold text-accent">{fmt(pourifyDifference)}</p> 200 <p className="text-xs opacity-70 mt-1">vs. competitor</p> 201 </div> 202 </div> 203 204 {/* Pricing tiers */} 205 <div className="mb-6"> 206 <h3 className="text-lg font-bold mb-2 text-foreground">Monthly Plan (ex VAT)</h3> 207 <p className="text-sm text-muted-foreground mb-4"> 208 Your tier for {brands} pourers: <span className="font-bold text-foreground">{fmt(pourifyMonthly)}/month</span> 209 {" "}+ once-off setup fee of <span className="font-bold text-foreground">{fmt(SETUP_FEE)}</span>.{" "} 210 {ZA_COMMITMENT_MONTHS}-month minimum term. 211 </p> 212 </div> 213 214 {/* CTA + Email capture */} 215 <div className="text-center space-y-3"> 216 <div className="flex flex-col sm:flex-row gap-3 justify-center"> 217 <Button asChild size="lg" variant="accent" className="px-8"> 218 <Link to={localePath("/pricing")}>View pricing</Link> 219 </Button> 220 <Button asChild size="lg" variant="outline" className="px-8"> 221 <Link to={localePath("/demo")}>Book a Demo</Link> 222 </Button> 223 </div> 224 </div> 225 226 {/* ROI Report email capture - also serves as Request Quote */} 227 <ZAReportCapture 228 brands={brands} 229 bottlesPerBrand={bottlesPerBrand} 230 pricePerTot={pricePerTot} 231 lossPercent={lossPercent} 232 annualLoss={annualLoss} 233 pourifyMonthly={pourifyMonthly} 234 pourifyNetSavings={pourifyNetSavings} 235 competitorMonthly={competitorMonthly} 236 competitorNetSavings={competitorNetSavings} 237 pourifyDifference={pourifyDifference} 238 language={lang} 239 /> 240 </div> 241 242 {/* Illustration */} 243 <div className="hidden lg:block w-[340px] xl:w-[400px] shrink-0 sticky top-28"> 244 <img src={bottlesIllustration} alt="Bar bottles illustration" loading="lazy" className="w-full h-auto rounded-2xl" /> 245 </div> 246 </div> 247 </div> 248 ); 249}; 250 251/* ââ Shared sub-components âââââââââââââââââââââââââââââââââââ */ 252 253const SliderRow = ({ 254 label, value, min, max, display, onChange, hint, presets, 255}: { 256 label: string; value: number; min: number; max: number; display: string; 257 onChange: (v: number) => void; hint?: string; 258 presets?: { label: string; value: number }[]; 259}) => ( 260 <div> 261 <div className="flex justify-between mb-1">
262 <span className="text-sm font-medium text-foreground">{label}</span> 263 <span className="text-2xl font-bold text-foreground">{display}</span> 264 </div> 265 {hint && <p className="text-xs text-muted-foreground mb-1">{hint}</p>} 266 {presets && presets.length > 0 && ( 267 <div className="flex flex-wrap gap-1.5 mb-2"> 268 {presets.map((p) => ( 269 <button 270 key={p.label} 271 onClick={() => onChange(p.value)} 272 className={\`px-2.5 py-1 rounded-full text-xs font-medium border transition-all \${ 273 value === p.value 274 ? "border-primary bg-primary/10 text-primary" 275 : "border-border hover:border-primary/50 text-muted-foreground hover:text-foreground" 276 }\`} 277 > 278 {p.label} ({p.value}%) 279 </button> 280 ))} 281 </div> 282 )} 283 <Slider value={[value]} min={min} max={max} step={1} onValueChange={([v]) => onChange(v)} /> 284 </div> 285); 286 287const FormulaChip = ({ value, label, highlight }: { value: string; label: string; highlight?: boolean }) => ( 288 <div className="flex flex-col items-center"> 289 <span className={\`px-3 py-1.5 rounded-lg text-sm font-semibold \${highlight ? "bg-foreground text-background" : "bg-background/60 text-foreground"}\`}> 290 {value} 291 </span> 292 <span className="text-[10px] text-muted-foreground mt-1 text-center max-w-[80px]">{label}</span> 293 </div> 294); 295 296/* ââ ZA Email Capture + Quote Request ââââââââââââââââââââââââ */ 297 298interface ZAReportCaptureProps { 299 brands: number; 300 bottlesPerBrand: number; 301 pricePerTot: number; 302 lossPercent: number; 303 annualLoss: number; 304 pourifyMonthly: number; 305 pourifyNetSavings: number; 306 competitorMonthly: number; 307 competitorNetSavings: number; 308 pourifyDifference: number; 309 language: string; 310} 311 312const ZAReportCapture = (props: ZAReportCaptureProps) => { 313 const { t } = useTranslation(); 314 const [email, setEmail] = useState(""); 315 const [contactName, setContactName] = useState(""); 316 const [phone, setPhone] = useState(""); 317 const [venueName, setVenueName] = useState(""); 318 const [company, setCompany] = useState(""); 319 const [pos, setPos] = useState(""); 320 const [pourers, setPourers] = useState(""); 321 const [loading, setLoading] = useState(false); 322 const [sent, setSent] = useState(false); 323 const [honeypot, setHoneypot] = useState(""); 324 325 const handleSubmit = async (e: React.FormEvent) => { 326 e.preventDefault(); 327 if (honeypot || !email || !contactName) return; 328 329 setLoading(true); 330 try { 331 // Save lead 332 await supabase.from("roi_leads" as any).insert({ 333 email, 334 contact_name: contactName, 335 phone: phone || null, 336 venue_name: venueName || null, 337 brands: props.brands, 338 net_saving: props.pourifyNetSavings, 339 region: "ZA", 340 loss_percent: props.lossPercent, 341 bottles_per_brand: props.bottlesPerBrand, 342 price_per_bottle: props.pricePerTot, 343 annual_lost_revenue: props.annualLoss, 344 currency_symbol: "R", 345 locale: "en-ZA", 346 language: props.language, 347 monthly_software_cost: props.pourifyMonthly, 348 pourify_annual_cost: props.pourifyMonthly * 12, 349 one_time_investment: ZA_SETUP_FEE, 350 annual_spillage_cost: props.annualLoss, 351 } as any); 352 353 // Send report 354 const { buildLeadContext } = await import("@/lib/sessionContext"); 355 await supabase.functions.invoke("send-roi-report", { 356 body: { 357 email, 358 contactName, 359 phone: phone || undefined, 360 venueName: venueName || undefined, 361 company: company || undefined, 362 posSystem: pos || undefined, 363 spiritBrands: \`\${props.brands}\`, 364 brands: props.brands, 365 numberOfBars: 1, 366 bottlesPerBrand: props.bottlesPerBrand, 367 pricePerBottle: props.pricePerTot, 368 lossPercent: props.lossPercent, 369 netSaving: props.pourifyNetSavings, 370 annualSpillageCost: props.annualLoss, 371 annualLostRevenue: props.annualLoss, 372 pourifyAnnualCost: props.pourifyMonthly * 12, 373 oneTimeInvestment: ZA_SETUP_FEE, 374 monthlySoftwareCost: props.pourifyMonthly, 375 region: "ZA", 376 currencySymbol: "R", 377 locale: "en-ZA", 378 language: props.language, 379 competitorMonthly: props.competitorMonthly, 380 competitorNetSavings: props.competitorNetSavings, 381 pourifyDifference: props.pourifyDifference, 382 leadContext: buildLeadContext({ email }), 383 }, 384 }); 385 386 trackROIReport({ 387 email, 388 region: "ZA", 389 brands: props.brands, 390 net_saving: props.pourifyNetSavings, 391 }); 392 393 setSent(true); 394 toast.success("ROI report sent! Check your inbox."); 395 } catch { 396 toast.error("Something went wrong. Please try again."); 397 } finally {
398 setLoading(false); 399 } 400 }; 401 402 if (sent) { 403 return ( 404 <div className="mt-6 rounded-xl bg-background/60 p-5 flex items-center gap-3"> 405 <CheckCircle2 className="w-5 h-5 text-accent shrink-0" /> 406 <p className="text-sm text-foreground">Your personalised ROI report has been sent. Check your inbox!</p> 407 </div> 408 ); 409 } 410 411 return ( 412 <div className="mt-6 rounded-xl bg-background/60 p-5"> 413 <p className="text-sm font-semibold text-foreground mb-1">ð Get your ROI report + request a quote</p> 414 <p className="text-xs text-muted-foreground mb-3">Enter your details to receive a detailed savings breakdown and a personalised quote.</p> 415 <form onSubmit={handleSubmit} className="space-y-3"> 416 <div className="absolute opacity-0 pointer-events-none" aria-hidden="true" tabIndex={-1}> 417 <input name="website" type="text" autoComplete="off" value={honeypot} onChange={(e) => setHoneypot(e.target.value)} tabIndex={-1} /> 418 </div> 419 <div className="grid sm:grid-cols-2 gap-3"> 420 <Input 421 type="text" 422 placeholder="Full name *" 423 value={contactName} 424 onChange={(e) => setContactName(e.target.value)} 425 required 426 /> 427 <Input 428 type="email" 429 placeholder="Email address *" 430 value={email} 431 onChange={(e) => setEmail(e.target.value)} 432 required 433 /> 434 </div> 435 <Input 436 type="text" 437 placeholder="Company / Venue name" 438 value={venueName} 439 onChange={(e) => setVenueName(e.target.value)} 440 /> 441 <div className="grid sm:grid-cols-2 gap-3"> 442 <Input 443 type="tel" 444 inputMode="tel" 445 placeholder="Cellphone number (e.g. +27 82 123 4567)" 446 value={phone} 447 onChange={(e) => setPhone(e.target.value)} 448 required 449 /> 450 <select 451 value={pos} 452 onChange={(e) => setPos(e.target.value)} 453 className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm r
453ing-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" 454 > 455 <option value="">POS system</option> 456 <option value="Pilot POS">Pilot POS</option> 457 <option value="SallyPOS">SallyPOS</option> 458 <option value="Lightspeed">Lightspeed</option> 459 <option value="Micros">Micros</option> 460 <option value="Other">Other</option> 461 </select> 462 </div> 463 <Button type="submit" size="sm" disabled={loading} className="shrink-0"> 464 {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Mail className="w-4 h-4 mr-1" />} 465 Send Report & Quote 466 </Button> 467 </form> 468 </div> 469 ); 470}; 471 472export default ZAROICalculator; 473`;export{n as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.