From 6d53ed1bdea3fc140ab1fb6660a282be4ceec7f4 Mon Sep 17 00:00:00 2001 From: Recoupable Date: Fri, 10 Jul 2026 01:06:01 -0400 Subject: [PATCH] feat: interactive ROI calculator at /tools/roi + resources hub - New /tools/roi page with 4-slider calculator (roster, hours, cost, content) - Animated result cards: time saved, cost saved, content boost, ROI multiple - Comparison bar showing weekly hours with vs without Recoup - Copy file lib/copy/roi.ts (single source of truth) - Custom slider CSS in globals.css - Resources page now links to ROI calc, blog, playbook, demos (was 'Coming soon') - TypeScript clean (no new errors) --- app/globals.css | 45 ++++ app/resources/page.tsx | 83 +++++++- app/tools/roi/page.tsx | 57 ++++++ components/roi/ROICalculator.tsx | 341 +++++++++++++++++++++++++++++++ lib/copy/roi.ts | 68 ++++++ 5 files changed, 585 insertions(+), 9 deletions(-) create mode 100644 app/tools/roi/page.tsx create mode 100644 components/roi/ROICalculator.tsx create mode 100644 lib/copy/roi.ts diff --git a/app/globals.css b/app/globals.css index 8d3ae6e..a88e47c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -84,6 +84,51 @@ background: radial-gradient(ellipse 80% 50% at 50% 0%, #1c1c1c 0%, #111 100%); } + /* ROI Calculator slider */ + .roi-slider { + -webkit-appearance: none; + appearance: none; + height: 6px; + border-radius: 9999px; + background: linear-gradient( + to right, + var(--foreground) 0%, + var(--foreground) var(--slider-pct, 50%), + var(--border) var(--slider-pct, 50%), + var(--border) 100% + ); + outline: none; + cursor: pointer; + } + + .roi-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--foreground); + border: 2px solid var(--background); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + cursor: grab; + transition: transform 0.15s; + } + + .roi-slider::-webkit-slider-thumb:active { + cursor: grabbing; + transform: scale(1.15); + } + + .roi-slider::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--foreground); + border: 2px solid var(--background); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + cursor: grab; + } + .dark-section-cta { background: radial-gradient(ellipse 80% 60% at 50% 100%, #0f0f0f 0%, #060606 100%); } diff --git a/app/resources/page.tsx b/app/resources/page.tsx index 84d9e24..5651137 100644 --- a/app/resources/page.tsx +++ b/app/resources/page.tsx @@ -1,23 +1,88 @@ import type { Metadata } from "next"; +import Link from "next/link"; import { siteConfig } from "@/lib/config"; import { buildPageMetadata } from "@/lib/seo"; export const metadata: Metadata = buildPageMetadata({ - title: `Resources — Blog & Demos | ${siteConfig.name}`, + title: `Resources — Tools, Blog & More | ${siteConfig.name}`, description: - "Blog posts and demos. Everything you need to run your music business with AI agents — strategy, content, fans, and operations.", + "Tools, blog posts, playbooks, and demos. Everything you need to run your music business with AI agents.", path: "/resources", }); +const resources = [ + { + title: "ROI Calculator", + description: + "See how much time and money AI agents could save your team. Input your numbers, get your answer.", + href: "/tools/roi", + badge: "New", + }, + { + title: "Blog", + description: + "Insights on AI in music, agent architecture, and running a modern label.", + href: "/blog", + }, + { + title: "The Playbook", + description: + "A tactical guide to running your music business with AI agents.", + href: "/playbook", + }, + { + title: "Demos", + description: + "See Recoup in action. Walkthroughs of agents running real music operations.", + href: "/learn/demos", + }, +]; + export default function ResourcesPage() { return ( -
-

- Resources -

-

- Coming soon. -

+
+
+

+ Resources +

+

+ Tools, guides, and insights to help you run your music business with + AI. +

+
+ +
+ {resources.map((r) => ( + +
+
+

{r.title}

+ {r.badge && ( + + {r.badge} + + )} +
+

+ {r.description} +

+
+ + → + + + ))} +
); } diff --git a/app/tools/roi/page.tsx b/app/tools/roi/page.tsx new file mode 100644 index 0000000..1577f87 --- /dev/null +++ b/app/tools/roi/page.tsx @@ -0,0 +1,57 @@ +import type { Metadata } from "next"; +import { buildPageMetadata } from "@/lib/seo"; +import { roiCopy } from "@/lib/copy/roi"; +import { ROICalculator } from "@/components/roi/ROICalculator"; + +export const metadata: Metadata = buildPageMetadata({ + title: "ROI Calculator — How Much Could You Save? | Recoup", + description: + "Calculate how much time and money AI agents could save your music business. Input your roster size, team costs, and content volume — get your ROI in seconds.", + path: "/tools/roi", +}); + +export default function ROIPage() { + return ( +
+ {/* Hero */} +
+
+ {roiCopy.badge} +
+

+ {roiCopy.headline} +

+

+ {roiCopy.subheadline} +

+
+ + {/* Calculator */} + + + {/* Methodology */} +
+

+ How we calculate +

+
    + {roiCopy.methodology.map((item) => ( +
  • + · + {item} +
  • + ))} +
+
+
+ ); +} diff --git a/components/roi/ROICalculator.tsx b/components/roi/ROICalculator.tsx new file mode 100644 index 0000000..a29d6a6 --- /dev/null +++ b/components/roi/ROICalculator.tsx @@ -0,0 +1,341 @@ +"use client"; + +import { useState, useCallback, useRef, useEffect } from "react"; +import Link from "next/link"; +import { roiCopy } from "@/lib/copy/roi"; + +/* ── Animated number ─────────────────────────────────────────────── */ +function AnimatedNumber({ + value, + prefix = "", + suffix = "", + duration = 600, +}: { + value: number; + prefix?: string; + suffix?: string; + duration?: number; +}) { + const ref = useRef(null); + const prevValue = useRef(value); + + useEffect(() => { + const el = ref.current; + if (!el) return; + + const from = prevValue.current; + const to = value; + prevValue.current = value; + + if (from === to) { + el.textContent = `${prefix}${format(to)}${suffix}`; + return; + } + + const start = performance.now(); + let raf: number; + + function tick(now: number) { + const t = Math.min((now - start) / duration, 1); + const eased = 1 - Math.pow(1 - t, 3); // ease-out cubic + const current = Math.round(from + (to - from) * eased); + if (el) el.textContent = `${prefix}${format(current)}${suffix}`; + if (t < 1) raf = requestAnimationFrame(tick); + } + + raf = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf); + }, [value, prefix, suffix, duration]); + + return ( + + {prefix} + {format(value)} + {suffix} + + ); +} + +function format(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (n >= 10_000) return `${(n / 1_000).toFixed(0)}K`; + if (n >= 1_000) return n.toLocaleString("en-US"); + return n.toString(); +} + +/* ── Slider input ────────────────────────────────────────────────── */ +function SliderInput({ + label, + value, + min, + max, + prefix = "", + onChange, +}: { + label: string; + value: number; + min: number; + max: number; + prefix?: string; + onChange: (v: number) => void; +}) { + const pct = ((value - min) / (max - min)) * 100; + + return ( +
+
+ + + {prefix} + {value.toLocaleString("en-US")} + +
+ onChange(Number(e.target.value))} + className="roi-slider w-full" + style={ + { + "--slider-pct": `${pct}%`, + } as React.CSSProperties + } + /> +
+ ); +} + +/* ── Result card ─────────────────────────────────────────────────── */ +function ResultCard({ + label, + value, + prefix, + suffix, + highlight, +}: { + label: string; + value: number; + prefix?: string; + suffix?: string; + highlight?: boolean; +}) { + return ( +
+
+ +
+
+ {label} +
+
+ ); +} + +/* ── Comparison bar ──────────────────────────────────────────────── */ +function ComparisonBar({ + withoutHours, + withHours, +}: { + withoutHours: number; + withHours: number; +}) { + const maxHours = withoutHours; + const withoutPct = 100; + const withPct = maxHours > 0 ? (withHours / maxHours) * 100 : 0; + + return ( +
+
+
+ + {roiCopy.comparison.without} + + + {withoutHours.toLocaleString()} {roiCopy.comparison.unit} + +
+
+
+
+
+
+
+ + {roiCopy.comparison.with} + + + {withHours.toLocaleString()} {roiCopy.comparison.unit} + +
+
+
+
+
+
+ ); +} + +/* ── Main calculator ─────────────────────────────────────────────── */ +export function ROICalculator() { + const c = roiCopy.inputs; + + const [roster, setRoster] = useState(c.roster.default); + const [hoursPerArtist, setHoursPerArtist] = useState(c.hoursPerArtist.default); + const [hourlyCost, setHourlyCost] = useState(c.hourlyCost.default); + const [postsPerWeek, setPostsPerWeek] = useState(c.postsPerWeek.default); + + const calc = useCallback(() => { + const weeklyHours = roster * hoursPerArtist; + const annualManualCost = weeklyHours * hourlyCost * 52; + const automationRate = 0.8; + + const annualTimeSaved = Math.round(weeklyHours * automationRate * 52); + const annualCostSaved = Math.round(annualManualCost * automationRate); + + const contentWithout = postsPerWeek * roster * 52; + const contentWith = Math.round(contentWithout * 2.5); + const contentBoost = contentWithout > 0 + ? Math.round(((contentWith - contentWithout) / contentWithout) * 100) + : 0; + + let recoupAnnual: number; + if (roster <= 1) recoupAnnual = 19 * 12; + else if (roster <= 10) recoupAnnual = 99 * 12; + else recoupAnnual = roster * 15 * 12; + + const netSavings = annualCostSaved - recoupAnnual; + const roiMultiple = + recoupAnnual > 0 ? Math.max(0, Math.round(netSavings / recoupAnnual)) : 0; + + const weeklyWithRecoup = Math.round(weeklyHours * (1 - automationRate)); + + return { + annualTimeSaved, + annualCostSaved, + contentBoost, + roiMultiple, + weeklyHours, + weeklyWithRecoup, + }; + }, [roster, hoursPerArtist, hourlyCost, postsPerWeek]); + + const r = calc(); + + return ( +
+ {/* ── Inputs ──────────────────────────────────────────────── */} +
+ + + + +
+ + {/* ── Results ─────────────────────────────────────────────── */} +
+ + + + +
+ + {/* ── Comparison ──────────────────────────────────────────── */} + + + {/* ── CTAs ────────────────────────────────────────────────── */} +
+ + {roiCopy.cta.primary} + + + {roiCopy.cta.secondary} + +
+ + {/* ── Disclaimer ──────────────────────────────────────────── */} +

+ {roiCopy.disclaimer} +

+
+ ); +} diff --git a/lib/copy/roi.ts b/lib/copy/roi.ts new file mode 100644 index 0000000..549dea6 --- /dev/null +++ b/lib/copy/roi.ts @@ -0,0 +1,68 @@ +/** + * ROI Calculator page copy — single source of truth. + */ +import { siteConfig } from "@/lib/config"; + +export const roiCopy = { + headline: "How much could you save?", + subheadline: + "See what happens when AI agents run your music marketing. Input your numbers, get your answer in seconds.", + badge: "Free — No signup required", + + inputs: { + roster: { + label: "Artists in your roster", + min: 1, + max: 500, + default: 10, + }, + hoursPerArtist: { + label: "Hours / week on marketing per artist", + min: 1, + max: 20, + default: 5, + }, + hourlyCost: { + label: "Team cost per hour", + min: 20, + max: 200, + default: 75, + prefix: "$", + }, + postsPerWeek: { + label: "Posts per artist per week", + min: 1, + max: 20, + default: 3, + }, + }, + + results: { + timeSaved: { label: "Hours saved / year", suffix: "hrs" }, + costSaved: { label: "Annual cost saved", prefix: "$" }, + contentBoost: { label: "Content output increase", suffix: "%" }, + roiMultiple: { label: "ROI multiple", suffix: "×" }, + }, + + comparison: { + without: "Without Recoup", + with: "With Recoup", + unit: "hrs / week", + }, + + cta: { + primary: "Start free", + primaryHref: siteConfig.appUrl, + secondary: "Book a demo", + secondaryHref: "/advisory/book", + }, + + disclaimer: + "Based on 80% automation rate across research, content creation, and scheduling. Actual results vary by workflow.", + + methodology: [ + "80% automation rate (conservative — most customers see higher)", + "2.5× content amplification with AI-assisted generation", + "Pricing based on current Recoup plans", + ], +} as const;