feat: add /case-studies page — customer stories with verified metrics - #54
feat: add /case-studies page — customer stories with verified metrics#54sidneyswift wants to merge 1 commit into
Conversation
Three case studies from enterprise customers with real numbers: - Rostrum Records: replaced $5K/mo content agency - Fat Beats: replaced $10K/mo creative director in 72 hours - Parlophone (WMG): automated 40+ hrs/mo of reporting Includes: - lib/copy/case-studies.ts — structured data (single source of truth) - app/case-studies/page.tsx — responsive page with metrics grid - Nav update (header + footer) - SEO metadata and OG tags Note: pre-existing build type error on /calculator route exists on main too. Quotes marked as pending customer approval in strategy/case-studies.md.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughChangesCase studies experience
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Visitor
participant NextRouter
participant CaseStudiesPage
participant caseStudiesCopy
Visitor->>NextRouter: Opens /case-studies
NextRouter->>CaseStudiesPage: Loads route
CaseStudiesPage->>caseStudiesCopy: Reads page content
caseStudiesCopy-->>CaseStudiesPage: Returns stats, studies, and CTA
CaseStudiesPage-->>Visitor: Renders case studies page
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
app/case-studies/page.tsx (1)
152-162: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep trust copy in
caseStudiesCopy.The heading and customer names are page copy embedded in JSX, creating a second source that markdown views cannot reuse. Add them to
caseStudiesCopyand render from that contract.As per coding guidelines, “Store page copy in
lib/copy/as a single source for both human pages and markdown views to keep them in sync.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/case-studies/page.tsx` around lines 152 - 162, Move the logos section heading and customer names from the JSX in the case studies page into the shared caseStudiesCopy contract under lib/copy. Update the logos rendering to read both the heading and names from caseStudiesCopy, preserving the existing text and display order so human and markdown views share one source.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/case-studies/page.tsx`:
- Around line 66-86: Update the case-study header containing the company name
and highlight badge so the two sections stack vertically below the sm breakpoint
and remain side-by-side at sm and above. Remove the badge’s shrink-only
constraint on narrow screens and allow the highlight label to wrap instead of
enforcing a single line, while preserving the existing styling at larger widths.
In `@lib/copy/case-studies.ts`:
- Line 49: Update the public-facing case-study strings at the referenced
locations to derive the product name from siteConfig rather than hardcoding
“Recoup.” Import siteConfig from the existing configuration module and
interpolate siteConfig.name while preserving the surrounding copy.
- Line 21: Extend the quote contract in lib/copy/case-studies.ts at lines 21-21
with an explicit approval status, mark the Rostrum, Fat Beats, and Parlophone
quotes pending at lines 73-76, 110-113, and 147-150, and update the quote
rendering logic in app/case-studies/page.tsx at lines 137-146 to display only
quotes with approved status.
---
Nitpick comments:
In `@app/case-studies/page.tsx`:
- Around line 152-162: Move the logos section heading and customer names from
the JSX in the case studies page into the shared caseStudiesCopy contract under
lib/copy. Update the logos rendering to read both the heading and names from
caseStudiesCopy, preserving the existing text and display order so human and
markdown views share one source.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 729f293d-f2e3-44c4-9a4b-d9be4b5ddfda
📒 Files selected for processing (4)
app/case-studies/page.tsxcomponents/layout/Footer.tsxlib/copy/case-studies.tslib/nav.ts
| <div className="flex items-start justify-between gap-4 mb-4"> | ||
| <div> | ||
| <span className="inline-block text-xs font-medium uppercase tracking-wider text-[var(--muted-foreground)] mb-2"> | ||
| {study.segment} | ||
| </span> | ||
| <h2 className="text-2xl sm:text-3xl font-bold tracking-tight text-[var(--foreground)]"> | ||
| {study.company} | ||
| </h2> | ||
| </div> | ||
| {/* Highlight badge */} | ||
| <div className="shrink-0 flex items-center gap-2 rounded-full border border-emerald-500/20 bg-emerald-500/5 px-4 py-2"> | ||
| <Icon className="w-4 h-4 text-emerald-400" /> | ||
| <div className="text-right"> | ||
| <div className="text-sm font-bold text-emerald-400 leading-tight"> | ||
| {study.highlight.value} | ||
| </div> | ||
| <div className="text-[10px] text-emerald-400/70 leading-tight whitespace-nowrap"> | ||
| {study.highlight.label} | ||
| </div> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Prevent highlight badges from overflowing on mobile.
Line 66 places the company name beside a shrink-0 badge, while Line 82 forces its long label onto one line. Several cards cannot fit both at narrow widths. Stack these elements below sm and allow the label to wrap.
Proposed fix
- <div className="flex items-start justify-between gap-4 mb-4">
+ <div className="flex flex-col items-start gap-4 mb-4 sm:flex-row sm:justify-between">
- <div className="shrink-0 flex items-center gap-2 ...">
+ <div className="self-start shrink-0 flex items-center gap-2 ... sm:self-auto">
- <div className="text-[10px] text-emerald-400/70 leading-tight whitespace-nowrap">
+ <div className="max-w-52 text-[10px] text-left text-emerald-400/70 leading-tight sm:max-w-none sm:text-right sm:whitespace-nowrap">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className="flex items-start justify-between gap-4 mb-4"> | |
| <div> | |
| <span className="inline-block text-xs font-medium uppercase tracking-wider text-[var(--muted-foreground)] mb-2"> | |
| {study.segment} | |
| </span> | |
| <h2 className="text-2xl sm:text-3xl font-bold tracking-tight text-[var(--foreground)]"> | |
| {study.company} | |
| </h2> | |
| </div> | |
| {/* Highlight badge */} | |
| <div className="shrink-0 flex items-center gap-2 rounded-full border border-emerald-500/20 bg-emerald-500/5 px-4 py-2"> | |
| <Icon className="w-4 h-4 text-emerald-400" /> | |
| <div className="text-right"> | |
| <div className="text-sm font-bold text-emerald-400 leading-tight"> | |
| {study.highlight.value} | |
| </div> | |
| <div className="text-[10px] text-emerald-400/70 leading-tight whitespace-nowrap"> | |
| {study.highlight.label} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex flex-col items-start gap-4 mb-4 sm:flex-row sm:justify-between"> | |
| <div> | |
| <span className="inline-block text-xs font-medium uppercase tracking-wider text-[var(--muted-foreground)] mb-2"> | |
| {study.segment} | |
| </span> | |
| <h2 className="text-2xl sm:text-3xl font-bold tracking-tight text-[var(--foreground)]"> | |
| {study.company} | |
| </h2> | |
| </div> | |
| {/* Highlight badge */} | |
| <div className="self-start shrink-0 flex items-center gap-2 rounded-full border border-emerald-500/20 bg-emerald-500/5 px-4 py-2 sm:self-auto"> | |
| <Icon className="w-4 h-4 text-emerald-400" /> | |
| <div className="text-right"> | |
| <div className="text-sm font-bold text-emerald-400 leading-tight"> | |
| {study.highlight.value} | |
| </div> | |
| <div className="max-w-52 text-[10px] text-left text-emerald-400/70 leading-tight sm:max-w-none sm:text-right sm:whitespace-nowrap"> | |
| {study.highlight.label} | |
| </div> | |
| </div> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/case-studies/page.tsx` around lines 66 - 86, Update the case-study header
containing the company name and highlight badge so the two sections stack
vertically below the sm breakpoint and remain side-by-side at sm and above.
Remove the badge’s shrink-only constraint on narrow screens and allow the
highlight label to wrap instead of enforcing a single line, while preserving the
existing styling at larger widths.
| solution: string; | ||
| metrics: CaseStudyMetric[]; | ||
| highlight: { value: string; label: string }; | ||
| quote?: { text: string; attribution: string }; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not publish customer quotes while approval is pending.
The PR states that these quotes await customer approval, but the contract cannot represent that state and the page renders all attributed testimonials immediately.
lib/copy/case-studies.ts#L21-L21: add an explicit quote approval status to the content contract.lib/copy/case-studies.ts#L73-L76: mark the Rostrum quote pending, or remove it until approved.lib/copy/case-studies.ts#L110-L113: mark the Fat Beats quote pending, or remove it until approved.lib/copy/case-studies.ts#L147-L150: mark the Parlophone quote pending, or remove it until approved.app/case-studies/page.tsx#L137-L146: render only quotes whose approval status is approved.
📍 Affects 2 files
lib/copy/case-studies.ts#L21-L21(this comment)lib/copy/case-studies.ts#L73-L76lib/copy/case-studies.ts#L110-L113lib/copy/case-studies.ts#L147-L150app/case-studies/page.tsx#L137-L146
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/copy/case-studies.ts` at line 21, Extend the quote contract in
lib/copy/case-studies.ts at lines 21-21 with an explicit approval status, mark
the Rostrum, Fat Beats, and Parlophone quotes pending at lines 73-76, 110-113,
and 147-150, and update the quote rendering logic in app/case-studies/page.tsx
at lines 137-146 to display only quotes with approved status.
| challenge: | ||
| "Rostrum's marketing team was paying $5,000 per month for an external content creation agency. Briefs went out, revisions went back and forth, and turnaround was measured in days. For a label releasing multiple projects per month, some artists got content and others didn't.", | ||
| solution: | ||
| "Recoup replaced the agency at the same price point. The team now generates 30 days of branded, artist-specific social content in a single session. Weekly release reports that required manual data pulls were automated. And Gatsby Grace — a new AI artist — was created entirely on the platform, with 22 finished videos produced in one session.", |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Derive the product name from siteConfig.
These public-facing strings hardcode “Recoup,” so a rebrand will drift across this copy module. Import siteConfig and interpolate siteConfig.name.
Proposed fix
+import { siteConfig } from "`@/lib/config`";
+
+const productName = siteConfig.name;
+
- "Recoup replaced the agency at the same price point. ...
+ `${productName} replaced the agency at the same price point. ...As per coding guidelines, “Never hardcode brand values; import from lib/config.ts.”
Also applies to: 86-86, 123-123, 155-157
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/copy/case-studies.ts` at line 49, Update the public-facing case-study
strings at the referenced locations to derive the product name from siteConfig
rather than hardcoding “Recoup.” Import siteConfig from the existing
configuration module and interpolate siteConfig.name while preserving the
surrounding copy.
Source: Coding guidelines
There was a problem hiding this comment.
4 issues found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/case-studies/page.tsx">
<violation number="1" location="app/case-studies/page.tsx:66">
P3: The card header uses `flex items-start justify-between` with a `shrink-0` badge whose label is `whitespace-nowrap`. On narrow mobile widths, long company names plus the long highlight label (e.g. "Net cost change — same price, 10× output") may not fit side-by-side and could overflow the card. Consider stacking these elements below `sm` and allowing the label to wrap.</violation>
<violation number="2" location="app/case-studies/page.tsx:120">
P2: Mobile metrics grid: index 2 (left column of second row) gets orphaned left border because `i > 0` adds border-l to every cell after index 0. Use `i % 2 !== 0` on mobile so only right-column items get border-l. On desktop (4-col), the md: breakpoint can override to all except first via a separate class.</violation>
</file>
<file name="lib/copy/case-studies.ts">
<violation number="1" location="lib/copy/case-studies.ts:21">
P1: The PR description notes these customer quotes are still 'Draft — Pending Customer Approval', but the `CaseStudy` type has no field to represent approval state, and `page.tsx` renders `study.quote` unconditionally whenever it's present. This means unapproved attributed testimonials (Rostrum, Fat Beats, Parlophone) will go live as soon as this page ships. Add an explicit approval status to the content contract and only render approved quotes, or omit the quotes until sign-off.</violation>
<violation number="2" location="lib/copy/case-studies.ts:49">
P3: This solution copy hardcodes the brand name "Recoup" directly instead of interpolating `siteConfig.name`, same for the other studies' solution text (lines ~86, 123, 155-157). Per the repo's convention of never hardcoding brand values, these strings should import and interpolate `siteConfig.name` so a rebrand doesn't require hunting through this copy module.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| solution: string; | ||
| metrics: CaseStudyMetric[]; | ||
| highlight: { value: string; label: string }; | ||
| quote?: { text: string; attribution: string }; |
There was a problem hiding this comment.
P1: The PR description notes these customer quotes are still 'Draft — Pending Customer Approval', but the CaseStudy type has no field to represent approval state, and page.tsx renders study.quote unconditionally whenever it's present. This means unapproved attributed testimonials (Rostrum, Fat Beats, Parlophone) will go live as soon as this page ships. Add an explicit approval status to the content contract and only render approved quotes, or omit the quotes until sign-off.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/copy/case-studies.ts, line 21:
<comment>The PR description notes these customer quotes are still 'Draft — Pending Customer Approval', but the `CaseStudy` type has no field to represent approval state, and `page.tsx` renders `study.quote` unconditionally whenever it's present. This means unapproved attributed testimonials (Rostrum, Fat Beats, Parlophone) will go live as soon as this page ships. Add an explicit approval status to the content contract and only render approved quotes, or omit the quotes until sign-off.</comment>
<file context>
@@ -0,0 +1,161 @@
+ solution: string;
+ metrics: CaseStudyMetric[];
+ highlight: { value: string; label: string };
+ quote?: { text: string; attribution: string };
+}
+
</file context>
| {study.metrics.map((metric, i) => ( | ||
| <div | ||
| key={metric.label} | ||
| className={`p-5 sm:p-6 ${i > 0 ? "border-l border-[var(--border)]" : ""} ${i >= 2 ? "border-t md:border-t-0 border-[var(--border)]" : ""}`} |
There was a problem hiding this comment.
P2: Mobile metrics grid: index 2 (left column of second row) gets orphaned left border because i > 0 adds border-l to every cell after index 0. Use i % 2 !== 0 on mobile so only right-column items get border-l. On desktop (4-col), the md: breakpoint can override to all except first via a separate class.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/case-studies/page.tsx, line 120:
<comment>Mobile metrics grid: index 2 (left column of second row) gets orphaned left border because `i > 0` adds border-l to every cell after index 0. Use `i % 2 !== 0` on mobile so only right-column items get border-l. On desktop (4-col), the md: breakpoint can override to all except first via a separate class.</comment>
<file context>
@@ -0,0 +1,184 @@
+ {study.metrics.map((metric, i) => (
+ <div
+ key={metric.label}
+ className={`p-5 sm:p-6 ${i > 0 ? "border-l border-[var(--border)]" : ""} ${i >= 2 ? "border-t md:border-t-0 border-[var(--border)]" : ""}`}
+ >
+ <div className="text-[10px] font-medium uppercase tracking-wider text-[var(--muted-foreground)] mb-2">
</file context>
| > | ||
| {/* Header */} | ||
| <div className="p-6 sm:p-10 pb-0 sm:pb-0"> | ||
| <div className="flex items-start justify-between gap-4 mb-4"> |
There was a problem hiding this comment.
P3: The card header uses flex items-start justify-between with a shrink-0 badge whose label is whitespace-nowrap. On narrow mobile widths, long company names plus the long highlight label (e.g. "Net cost change — same price, 10× output") may not fit side-by-side and could overflow the card. Consider stacking these elements below sm and allowing the label to wrap.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/case-studies/page.tsx, line 66:
<comment>The card header uses `flex items-start justify-between` with a `shrink-0` badge whose label is `whitespace-nowrap`. On narrow mobile widths, long company names plus the long highlight label (e.g. "Net cost change — same price, 10× output") may not fit side-by-side and could overflow the card. Consider stacking these elements below `sm` and allowing the label to wrap.</comment>
<file context>
@@ -0,0 +1,184 @@
+ >
+ {/* Header */}
+ <div className="p-6 sm:p-10 pb-0 sm:pb-0">
+ <div className="flex items-start justify-between gap-4 mb-4">
+ <div>
+ <span className="inline-block text-xs font-medium uppercase tracking-wider text-[var(--muted-foreground)] mb-2">
</file context>
| challenge: | ||
| "Rostrum's marketing team was paying $5,000 per month for an external content creation agency. Briefs went out, revisions went back and forth, and turnaround was measured in days. For a label releasing multiple projects per month, some artists got content and others didn't.", | ||
| solution: | ||
| "Recoup replaced the agency at the same price point. The team now generates 30 days of branded, artist-specific social content in a single session. Weekly release reports that required manual data pulls were automated. And Gatsby Grace — a new AI artist — was created entirely on the platform, with 22 finished videos produced in one session.", |
There was a problem hiding this comment.
P3: This solution copy hardcodes the brand name "Recoup" directly instead of interpolating siteConfig.name, same for the other studies' solution text (lines ~86, 123, 155-157). Per the repo's convention of never hardcoding brand values, these strings should import and interpolate siteConfig.name so a rebrand doesn't require hunting through this copy module.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/copy/case-studies.ts, line 49:
<comment>This solution copy hardcodes the brand name "Recoup" directly instead of interpolating `siteConfig.name`, same for the other studies' solution text (lines ~86, 123, 155-157). Per the repo's convention of never hardcoding brand values, these strings should import and interpolate `siteConfig.name` so a rebrand doesn't require hunting through this copy module.</comment>
<file context>
@@ -0,0 +1,161 @@
+ challenge:
+ "Rostrum's marketing team was paying $5,000 per month for an external content creation agency. Briefs went out, revisions went back and forth, and turnaround was measured in days. For a label releasing multiple projects per month, some artists got content and others didn't.",
+ solution:
+ "Recoup replaced the agency at the same price point. The team now generates 30 days of branded, artist-specific social content in a single session. Weekly release reports that required manual data pulls were automated. And Gatsby Grace — a new AI artist — was created entirely on the platform, with 22 finished videos produced in one session.",
+ metrics: [
+ {
</file context>
What
Customer Stories page at
/case-studieswith three verified enterprise case studies:Why
The marketing site has interactive prospect tools (valuation, compare, audit) and shows customer logos on the homepage — but zero customer stories. Case studies are the #1 conversion asset for enterprise B2B sales. Every competitor has them. We had the data (strategy/case-studies.md) but it wasn't on the site.
What's Included
lib/copy/case-studies.ts— structured data (single source of truth, matches compare/solutions pattern)app/case-studies/page.tsx— responsive page with aggregate stats strip, challenge/solution sections, before/after metrics grid, quotes, CTADesign
Follows existing patterns: CSS variables for dark/light mode, rounded cards with border hover,
buildPageMetadatafor SEO,var(--foreground)/var(--muted-foreground)theming, Instrument Serif headers, responsive grid.Note
/calculatorroute exists on main — not from this PR.Summary by cubic
Adds a new Customer Stories page at
/case-studieswith three verified enterprise case studies and clear before/after metrics. Updates site nav and SEO to surface this content./case-studiespage with Rostrum ($5K/mo agency replaced), Fat Beats ($10K/mo creative director replaced in 72 hrs), and Parlophone (40+ hrs/mo reporting automated).lib/copy/case-studies.ts(single source of truth).lib/nav.ts) and footer; page metadata viabuildPageMetadata.Written for commit 4bf2580. Summary will update on new commits.
Summary by CodeRabbit