Skip to content

feat(web): landing page redesign with Terminal Chic aesthetic#239

Merged
BillChirico merged 25 commits intomainfrom
feat/landing-redesign
Mar 4, 2026
Merged

feat(web): landing page redesign with Terminal Chic aesthetic#239
BillChirico merged 25 commits intomainfrom
feat/landing-redesign

Conversation

@BillChirico
Copy link
Collaborator

Summary

Complete redesign of the volvox.dev landing page with a "Terminal Chic" aesthetic — clean, developer-focused, and tactical with professional wit.

Closes #238

Sections Implemented

Hero Section

  • Typewriter animation for "> volvox-bot" headline
  • Blinking green terminal cursor
  • Subheadline fade-in after typing
  • CTA buttons with slide-up animation
  • Mock chat preview

Feature Grid

  • Terminal window-style cards (AI Chat, Moderation, Starboard, Analytics)
  • Scroll-triggered reveal animations
  • Hover effects: border glow, lift
  • Window chrome with red/yellow/green dots

Pricing Section

  • 3 tiers: ~/dev/null (free), ./configure ($12/mo), make install ($49/mo)
  • Monthly/annual toggle with 20% savings
  • ★ POPULAR badge with pulse animation
  • Terminal-themed CTAs: git clone, npm install, curl | bash

Stats / Testimonials

  • Animated counters (servers, messages/day, GitHub stars)
  • 3 developer testimonials
  • Trust badge

Footer CTA

  • "Ready to upgrade your server?"
  • Final call-to-action
  • Links to docs, GitHub, support

Design

Colors: Terminal Chic palette (GitHub-style light/dark modes)
Typography: JetBrains Mono (headlines) + Inter (body)
Animations: Framer Motion for smooth scroll-triggered effects

Files Changed

  • web/src/app/page.tsx - Updated to compose all sections
  • web/src/components/landing/Hero.tsx - Existing, with typewriter
  • web/src/components/landing/FeatureGrid.tsx - New
  • web/src/components/landing/Pricing.tsx - New
  • web/src/components/landing/Stats.tsx - New
  • web/src/components/landing/Footer.tsx - New
  • web/src/components/landing/index.ts - Exports

Testing

  • Dark/light mode toggle works
  • Animations smooth at 60fps
  • Mobile responsive
  • Lighthouse score > 90 (needs verification)

Bill added 2 commits March 3, 2026 18:42
…nimation

- Set up CSS variables in globals.css for Terminal Chic theme (light/dark modes)
- Add JetBrains Mono font alongside Inter for terminal-style headlines
- Create Hero.tsx component with:
  - Typewriter effect for '> volvox-bot' headline
  - Blinking green terminal cursor
  - Subheadline fade-in after typing completes
  - CTA buttons with slide-up animation
  - Mock chat preview showing bot interaction
- Update page.tsx with new Hero component and scroll-triggered animations
- Install framer-motion for animations
- Update landing page tests to match new structure
…ooter

- FeatureGrid.tsx: Terminal window-style cards with scroll animations
- Pricing.tsx: 3-tier pricing (~/dev/null, ./configure, make install) with toggle
- Stats.tsx: Animated counters and developer testimonials
- Footer.tsx: Final CTA with social links
- Updated page.tsx to compose all sections
Copilot AI review requested due to automatic review settings March 3, 2026 23:59
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

Warning

Rate limit exceeded

@BillChirico has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Run ID: fdb394ba-ebbf-4d26-b459-2d4b51951796

📥 Commits

Reviewing files that changed from the base of the PR and between e83b855 and 5cb6ebc.

📒 Files selected for processing (8)
  • .github/workflows/railway-preview.yml
  • .gitignore
  • docs/railway.toml
  • web/next-env.d.ts
  • web/src/app/page.tsx
  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/Hero.tsx
  • web/src/components/landing/index.ts
📝 Walkthrough

Walkthrough

Reworks the landing into modular, animated components with "Terminal Chic" theming: adds JetBrains Mono + Inter fonts, extensive CSS tokens and animations, Framer Motion as a runtime dependency, new Hero/FeatureGrid/Pricing/Stats/Footer/InviteButton components, layout/metadata updates, tests mocking Framer Motion, and a Railway preview workflow.

Changes

Cohort / File(s) Summary
Theme & Global Styles
web/src/app/globals.css
Adds JetBrains Mono and Inter font variables, Terminal Chic color and semantic tokens for light/dark, new CSS variables, blink/typewriter keyframes, terminal-cursor styles, and other theme/token updates.
Layout & Metadata
web/src/app/layout.tsx
Configures JetBrains Mono and Inter font variables, applies combined fonts to root/body, sets lang="en" and suppressHydrationWarning, and updates site title/description.
Runtime Dependency
web/package.json
Adds runtime dependency framer-motion (^12.34.5).
Landing Page Composition
web/src/app/page.tsx
Replaces inline landing markup with composed components; updates header/navbar to include ThemeToggle, Sign In, and InviteButton; adjusts anchors/IDs and branding text.
Landing Components (new)
web/src/components/landing/Hero.tsx, web/src/components/landing/FeatureGrid.tsx, web/src/components/landing/Pricing.tsx, web/src/components/landing/Stats.tsx, web/src/components/landing/Footer.tsx, web/src/components/landing/InviteButton.tsx
Adds client-side components: Hero (typewriter, blinking cursor, chat preview, CTAs), FeatureGrid (terminal-style cards with staggered Framer Motion animations), Pricing (3-tier monthly/annual toggle + popular badge), Stats (animated counters/testimonials), Footer (CTA/links, conditional invite), and InviteButton (conditional Add-to-Server button).
Barrel Export
web/src/components/landing/index.ts
Adds re-exports for Hero, FeatureGrid, Pricing, Stats, Footer, and InviteButton.
Tests
web/tests/app/landing.test.tsx
Mocks Framer Motion primitives and useInView; updates tests for new branding/components, feature labels, conditional Add-to-Server button behavior, footer links, CTA text, and theme toggle presence.
CI / Preview Workflow
.github/workflows/railway-preview.yml
Adds GitHub Actions workflow to build the web preview, deploy per-PR Railway preview environments, post/update PR comments with preview URLs, and clean up previews on PR close.
Misc / Config
web/next-env.d.ts, docs/railway.toml, .gitignore
Tweaks next-env import path, removes Dockerfile path and sets restart retries in Railway config, and updates .gitignore to add openclaw-studio/.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(web): landing page redesign with Terminal Chic aesthetic' accurately and concisely describes the main change—a complete redesign of the landing page with a specific aesthetic theme.
Description check ✅ Passed The description comprehensively covers the landing page redesign, detailing implemented sections, design choices, files changed, and testing status—all directly related to the changeset.
Linked Issues check ✅ Passed The PR successfully implements all primary objectives from #238: Terminal Chic aesthetic with themed colors, JetBrains Mono + Inter typography, Hero with typewriter/cursor animation, feature cards with terminal styling and scroll animations, Pricing section with three tiers and toggle, Stats/Footer, Framer Motion animations, dark/light modes, and responsive design.
Out of Scope Changes check ✅ Passed All code changes are directly aligned with the landing page redesign objectives. Changes include landing components, typography/theming updates, dependency additions (framer-motion), tests, and CI workflow—all scoped to the redesign effort and issue #238.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/landing-redesign

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Mar 4, 2026

Greptile Summary

Complete redesign of the landing page with a polished "Terminal Chic" aesthetic — developer-focused with clean typography, smooth animations, and comprehensive dark/light mode support.

Key changes:

  • New hero section with typewriter animation and blinking cursor effect
  • Four terminal-styled feature cards (AI Chat, Moderation, Starboard, Analytics) with scroll-triggered reveals
  • Three-tier pricing section with monthly/annual toggle and terminal-themed CTAs
  • Stats section with animated counters and testimonials
  • New Railway preview deployment workflow for PR environments
  • Added framer-motion for animation library
  • Updated global styles with Terminal Chic color palette and semantic tokens

Implementation quality:

  • Proper cleanup of intervals and animation frames (previous memory leaks fixed)
  • Accessibility-friendly with reduced motion support
  • Tests updated and mocking framer-motion correctly
  • Free tier correctly links to GitHub repo for self-hosting

Issues found:

  • JetBrains Mono font not properly mapped to font-mono utility in Tailwind theme
  • Deploy workflow runs unnecessarily when PR closes, wasting CI resources

Confidence Score: 4/5

  • Safe to merge with minor configuration fixes needed for optimal functionality
  • High-quality landing page implementation with proper React patterns, accessibility support, and comprehensive test coverage. The two issues found (font mapping and workflow efficiency) are non-breaking and can be fixed post-merge. Code follows project conventions, uses ESM correctly, and includes proper animation cleanup.
  • Pay attention to web/src/app/globals.css for the font-mono mapping fix and .github/workflows/railway-preview.yml for the workflow condition

Important Files Changed

Filename Overview
.github/workflows/railway-preview.yml New Railway preview deployment workflow with minor efficiency issue - deploy job runs unnecessarily on PR close
web/src/app/globals.css Terminal Chic theme implementation with comprehensive color system and CSS variables - missing font-mono theme mapping for JetBrains Mono
web/src/app/page.tsx Landing page composition with responsive navbar and well-structured sections
web/src/components/landing/Hero.tsx Hero section with typewriter animation and chat preview - proper cleanup implemented for intervals and timeouts
web/src/components/landing/FeatureGrid.tsx Feature cards with terminal window styling, scroll animations, and accessibility-friendly reduced motion support
web/src/components/landing/Pricing.tsx Three-tier pricing section with annual/monthly toggle - free tier correctly links to GitHub repo
web/src/components/landing/Stats.tsx Stats counters with testimonials section - animation cleanup properly implemented, uses generic testimonials

Last reviewed commit: 5cb6ebc

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Redesigns the volvox.dev landing page into a “Terminal Chic” aesthetic by splitting the page into animated, themed sections and updating global styling/fonts to match the new design system.

Changes:

  • Replaces the legacy landing page with a section-based composition (Hero, Features, Pricing, Stats/Testimonials, Footer CTA).
  • Introduces new animated landing components using Framer Motion and updates global theme styles (colors/animations) and fonts (Inter + JetBrains Mono).
  • Updates/extends landing-page tests and adds Framer Motion as a dependency.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
web/src/app/page.tsx Composes the new landing sections and updates the navbar styling/links.
web/src/components/landing/Hero.tsx New hero with typewriter headline, CTAs, and mock chat preview.
web/src/components/landing/FeatureGrid.tsx New terminal-style feature cards with scroll/hover animations.
web/src/components/landing/Pricing.tsx New pricing tiers with monthly/annual toggle and animated cards.
web/src/components/landing/Stats.tsx New stats counters + testimonials + trust badge.
web/src/components/landing/Footer.tsx New footer CTA + links + copyright.
web/src/components/landing/index.ts Barrel exports for landing components.
web/src/app/layout.tsx Loads Inter + JetBrains Mono and updates metadata/body font class usage.
web/src/app/globals.css Updates global theme tokens and adds terminal-themed animations/styles.
web/tests/app/landing.test.tsx Updates landing page tests and mocks Framer Motion.
web/package.json Adds framer-motion dependency.
pnpm-lock.yaml Lockfile updates for framer-motion and transitive deps.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

web/src/app/globals.css:150

  • @keyframes blink is defined twice (once inside the @theme block and again at the bottom of the file). Keeping a single definition will avoid confusion about which animation is active and reduce the chance of future edits diverging.
/* Terminal cursor blink animation */
.terminal-cursor {
  animation: blink 1s step-end infinite;
}

@keyframes blink {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/app/page.tsx (1)

11-22: 🧹 Nitpick | 🔵 Trivial

Deduplicate InviteButton into a shared landing component.

This function duplicates invite-button behavior already present in web/src/components/landing/Hero.tsx (same URL resolution and render path). Extracting one shared component avoids copy drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/page.tsx` around lines 11 - 22, There's duplicate invite-button
logic in InviteButton (page.tsx) and the Hero component; extract a single
reusable InviteButton component that uses getBotInviteUrl(), accepts props
size?: 'sm' | 'lg' and className?: string, preserves rendering (Button with
variant="discord" and asChild, anchor with target/rel, and conditional Bot icon
when size === 'lg'), and export it for reuse; then update the original
InviteButton in page.tsx and the one in Hero to import and use this shared
InviteButton to avoid copy drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/app/globals.css`:
- Around line 69-76: There are two duplicate `@keyframes` blink definitions;
remove the redundant one and consolidate into a single canonical `@keyframes`
blink block so the animation is defined only once; locate both occurrences of
"@keyframes blink" in globals.css, keep the preferred animation keyframe
(preserving the 0%,50%,100% opacity steps) and remove the other duplicate, then
run a quick CSS build/test to confirm no regressions.
- Around line 80-125: Add the missing landing semantic tokens referenced by the
landing components (e.g., --bg-primary, --bg-secondary, --text-primary,
--border-default, --accent-*, etc.) by mapping them to the existing palette
tokens defined in :root and .dark (for example map --bg-primary to --background
or --card, --text-primary to --foreground or --card-foreground, --border-default
to --border, and --accent-* to --accent / --accent-foreground); update both the
light (:root) and dark (.dark) blocks so the variables used in
web/src/components/landing/Pricing.tsx, FeatureGrid.tsx, and Stats.tsx resolve
correctly across themes.

In `@web/src/app/layout.tsx`:
- Around line 6-14: The Next.js font variables collide with hardcoded CSS
variables and the Inter font is not exposed as --font-sans; rename the injected
Next.js variables to unique names (e.g. change variable in Inter() and
JetBrains_Mono() calls from '--font-inter'/'--font-mono' to '--next-font-sans'
and '--next-font-mono'), update globals.css `@theme` block to map CSS variables to
Tailwind theme names (add "--font-sans": "var(--next-font-sans)" and set
"--font-mono": "var(--next-font-mono)"), and update the body/className usage
(where font-sans is applied) to rely on the mapped --font-sans so the Inter font
is used correctly.

In `@web/src/app/page.tsx`:
- Around line 36-68: The nav currently renders all links/actions in one row
which will overflow on small screens; update the nav in page.tsx so the link
group uses responsive utilities (e.g., hide on small screens and show at md+
like replacing the current nav container with two parts: a collapsed mobile menu
and a desktop menu), move ThemeToggle and InviteButton into the same responsive
groups, and add a hamburger toggle (a local stateful MobileMenu or use a
Disclosure/Popover) that shows the stacked links (Features, Pricing, Docs,
GitHub, ThemeToggle, Sign In Button, InviteButton) on mobile; ensure the
Button/Link usage (Button variant="outline" size="sm" asChild with Link
href="/login") and components ThemeToggle and InviteButton keep their props and
styling when rendered inside the mobile menu so layout and accessibility are
preserved.

In `@web/src/components/landing/Footer.tsx`:
- Around line 26-31: The primary footer CTA in Footer.tsx is a styled Button
with no click handler or link; update the Footer component so the "Add to
Discord — Free" Button triggers navigation: either render it as an anchor/link
(e.g., wrap with Next.js Link or an <a> with the Discord OAuth/install URL) or
add an onClick that calls router.push to the intended add-to-discord URL; ensure
the Button (symbol: Button in Footer.tsx) includes the href/onClick and
preserves existing styles and accessibility attributes (role, rel, target if
external).

In `@web/src/components/landing/Hero.tsx`:
- Around line 175-176: The cursor is unmounted as soon as isComplete turns true
so it never performs the required 3 post-typing blinks; keep the cursor mounted
after typing finishes and hide it only after three additional blink cycles by
adding a short-lived state or callback: change the render condition around
<BlinkingCursor /> (currently {!isComplete && <BlinkingCursor />}) to render
while (!isComplete || cursorVisible) and implement cursorVisible in the Hero
component (or pass a prop to BlinkingCursor) so that when isComplete becomes
true you either start a timer based on the cursor blink interval to set
cursorVisible=false after 3 blinks, or have BlinkingCursor accept a prop like
onPostBlinkComplete / postBlinkCount to perform the 3-blink sequence and call
back to Hero to unmount; reference the isComplete flag, the BlinkingCursor
component, and the new cursorVisible/onPostBlinkComplete mechanism to locate and
implement the change.
- Around line 153-218: The Hero component currently ignores
prefers-reduced-motion; add a small hook (e.g., usePrefersReducedMotion) and
wire it into Hero to disable/shorten animations: pass the flag into
useTypewriter to immediately complete or use minimal delay, render
BlinkingCursor conditionally (or render a static cursor) when reduced motion is
requested, and set Framer Motion props on the motion.div/motion.p blocks to use
non-animated states (animate === initial) or zero-duration transitions when
reduced motion is true. Also update globals.css to wrap the existing blink
keyframe rules (and the ChatPreview pulse animation) in `@media`
(prefers-reduced-motion: no-preference) and add a `@media`
(prefers-reduced-motion: reduce) rule that stops those animations. References:
Hero, useTypewriter, BlinkingCursor, ChatPreview, motion.div/motion.p, and the
blink/pulse rules in globals.css.
- Around line 19-35: The effect in useTypewriter currently returns the interval
cleanup from inside the setTimeout callback (so React never sees it) and can
leak intervals; move the interval identifier (e.g., let interval) to the outer
scope of the effect so you can assign it from inside setTimeout, and return a
single cleanup function from the effect that clears both the timeout and the
interval (clearTimeout(timeout) and clearInterval(interval)); also ensure the
code that completes typing still calls clearInterval(interval) and avoids
scheduling further state updates after unmount.

In `@web/src/components/landing/Pricing.tsx`:
- Around line 145-154: The tier CTA Buttons are only visual (Button component)
and need click handling: add an onClick or wrap with a Link to navigate or
trigger the intended action. Update the Button(s) where tier.cta is rendered
(the Button in Pricing.tsx) to either call a navigation function (e.g.,
useRouter().push(targetPath) in an onClick) or wrap the Button in a <Link> that
points to the appropriate route/URL (or call a provided handler prop like
onSelectTier(tier.id)). Ensure the chosen target (route, handler, or URL) is
derived from the tier object (e.g., tier.href or tier.id) so each tier button
performs the expected navigation/action.
- Around line 82-91: The billing toggle button lacks accessible semantics;
update the element that currently uses setIsAnnual and isAnnual (the <button>
wrapping the motion.div) to expose switch semantics by adding role="switch",
aria-checked={isAnnual}, and a clear accessible name (aria-label or
aria-labelledby) such as "Toggle annual billing"; keep the onClick that calls
setIsAnnual(!isAnnual) and preserve keyboard activation (button already handles
Enter/Space) and focus styling so assistive tech will announce the control and
its state correctly.

In `@web/src/components/landing/Stats.tsx`:
- Around line 12-25: The useEffect in AnimatedCounter starts
requestAnimationFrame loops via the animate function but never cancels them;
modify the effect to store the RAF id returned by requestAnimationFrame and call
cancelAnimationFrame(id) in the effect cleanup so pending frames are cancelled
when dependencies (isInView, target, duration) change or the component unmounts;
ensure the RAF id variable is accessible to the cleanup, and also guard setCount
to avoid updates after unmount by cancelling before any further setCount calls
from animate.
- Around line 30-45: The testimonials array in Stats.tsx currently contains
named attributions and brand affiliations (the testimonials constant and the
trust badge rendering) without authorization; to fix, either remove the named
fields and replace with anonymous/testimonial-only objects (e.g., keep only
quote and anonymize author/role) or obtain and attach documented written consent
for the specific names/brands and only then keep them, and also remove or
disable the "Trusted by" trust-badge rendering unless written partnership
approval exists; update the testimonials constant and any JSX that renders
author/role or the trust badge accordingly (look for the testimonials variable
and the trust badge component/markup) so the UI no longer uses unverified
names/brands.

In `@web/tests/app/landing.test.tsx`:
- Around line 44-51: The feature-card assertions in the LandingPage test are
stale; update the tests in landing.test.tsx that reference LandingPage (the
blocks asserting 'AI Chat', 'Moderation', 'Welcome Messages', 'Spam Detection',
'Runtime Config', 'Web Dashboard' and the similar group later) so they match the
redesigned landing content — either replace those exact string expectations with
the current feature titles used by the new UI or make the checks resilient
(e.g., assert presence of feature-card elements via a stable
selector/data-testid or use getByRole/getByLabelText for the visible headings)
so the tests no longer rely on old copy.
- Around line 36-42: The test 'renders the hero heading with volvox-bot' is too
broad (uses screen.getAllByText) and may match navbar or other text; update the
assertion to target the hero heading specifically by querying the heading role
and its accessible name (e.g., use screen.getByRole('heading', { name:
/volvox-bot/i }) or a case-insensitive regex) after rendering LandingPage so the
test verifies the hero heading text rather than any occurrence of "volvox-bot".

---

Outside diff comments:
In `@web/src/app/page.tsx`:
- Around line 11-22: There's duplicate invite-button logic in InviteButton
(page.tsx) and the Hero component; extract a single reusable InviteButton
component that uses getBotInviteUrl(), accepts props size?: 'sm' | 'lg' and
className?: string, preserves rendering (Button with variant="discord" and
asChild, anchor with target/rel, and conditional Bot icon when size === 'lg'),
and export it for reuse; then update the original InviteButton in page.tsx and
the one in Hero to import and use this shared InviteButton to avoid copy drift.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7f086f and 5eb1b5c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • web/package.json
  • web/src/app/globals.css
  • web/src/app/layout.tsx
  • web/src/app/page.tsx
  • web/src/components/landing/FeatureGrid.tsx
  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/Hero.tsx
  • web/src/components/landing/Pricing.tsx
  • web/src/components/landing/Stats.tsx
  • web/src/components/landing/index.ts
  • web/tests/app/landing.test.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Agent
  • GitHub Check: Greptile Review
  • GitHub Check: Docker Build Validation
🧰 Additional context used
📓 Path-based instructions (2)
{src,web}/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Winston logger from src/logger.js, NEVER use console.*

Files:

  • web/src/components/landing/index.ts
  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/FeatureGrid.tsx
  • web/tests/app/landing.test.tsx
  • web/src/app/layout.tsx
  • web/src/app/page.tsx
  • web/src/components/landing/Stats.tsx
  • web/src/components/landing/Hero.tsx
  • web/src/components/landing/Pricing.tsx
web/**/*.{ts,tsx,jsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

Use Next.js 16 App Router for web dashboard, with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design

Files:

  • web/src/components/landing/index.ts
  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/FeatureGrid.tsx
  • web/tests/app/landing.test.tsx
  • web/src/app/layout.tsx
  • web/src/app/page.tsx
  • web/src/components/landing/Stats.tsx
  • web/src/components/landing/Hero.tsx
  • web/src/components/landing/Pricing.tsx
🧠 Learnings (1)
📚 Learning: 2026-03-02T21:23:59.512Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T21:23:59.512Z
Learning: Applies to web/**/*.{ts,tsx,jsx,js} : Use Next.js 16 App Router for web dashboard, with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design

Applied to files:

  • web/package.json
  • web/tests/app/landing.test.tsx
  • web/src/app/layout.tsx
  • web/src/app/page.tsx
  • web/src/components/landing/Hero.tsx
🪛 Stylelint (17.3.0)
web/src/app/globals.css

[error] 5-5: Expected "SFMono-Regular" to be "sfmono-regular" (value-keyword-case)

(value-keyword-case)


[error] 5-5: Expected "Menlo" to be "menlo" (value-keyword-case)

(value-keyword-case)


[error] 5-5: Expected "Monaco" to be "monaco" (value-keyword-case)

(value-keyword-case)


[error] 5-5: Expected "Consolas" to be "consolas" (value-keyword-case)

(value-keyword-case)


[error] 4-4: Unexpected unknown at-rule "@theme" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)


[error] 134-134: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

- Hero.tsx: Store interval ID in ref and clear in effect cleanup
- Stats.tsx: Store raf ID in ref and cancel in effect cleanup
Add --bg-primary, --bg-secondary, --text-primary, --accent-primary,
--border-default and related tokens to :root and .dark themes
- Footer: Add href to primary CTA button using getBotInviteUrl()
- Pricing: Wire tier buttons to invite URL
- Feature cards: AI Chat, Moderation, Starboard, Analytics
- Footer link: Support Server (not Discord)
- CTA text: Ready to upgrade your server?
- Handle multiple GitHub elements with getAllByText
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (2)
web/src/components/landing/Pricing.tsx (1)

83-95: ⚠️ Potential issue | 🟡 Minor

Add type="button" to the billing toggle.

The pipeline failure indicates the button is missing an explicit type attribute. Without it, buttons default to type="submit" inside forms, which can cause unexpected behavior.

🔧 Proposed fix
             <button
+              type="button"
               onClick={() => setIsAnnual(!isAnnual)}
               role="switch"
               aria-checked={isAnnual}
               aria-label="Toggle annual billing"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/landing/Pricing.tsx` around lines 83 - 95, The billing
toggle button in Pricing.tsx is missing an explicit type and defaults to submit;
update the button element (the one with onClick={() => setIsAnnual(!isAnnual)}
and aria-checked={isAnnual}) to include type="button" so it won't submit parent
forms unexpectedly; locate the button that uses setIsAnnual/isAnnual and add the
type attribute.
web/src/components/landing/Footer.tsx (1)

27-35: ⚠️ Potential issue | 🟡 Minor

Avoid the # fallback for the CTA link.

Same concern as in Pricing.tsx: getBotInviteUrl() || '#' renders an inert anchor when CLIENT_ID is unset. Consider conditionally rendering or disabling the button.

🔧 Proposed fix
-          <Button
-            size="lg"
-            className="font-mono text-lg px-8 py-6 bg-[var(--accent-success)] hover:bg-[var(--accent-success)]/90 text-white"
-            asChild
-          >
-            <a href={getBotInviteUrl() || '#'} target="_blank" rel="noopener noreferrer">
-              Add to Discord — Free
-            </a>
-          </Button>
+          {getBotInviteUrl() ? (
+            <Button
+              size="lg"
+              className="font-mono text-lg px-8 py-6 bg-[var(--accent-success)] hover:bg-[var(--accent-success)]/90 text-white"
+              asChild
+            >
+              <a href={getBotInviteUrl()!} target="_blank" rel="noopener noreferrer">
+                Add to Discord — Free
+              </a>
+            </Button>
+          ) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/landing/Footer.tsx` around lines 27 - 35, The CTA
currently falls back to an inert '#' when getBotInviteUrl() is unset, which
produces a clickable-but-no-op link; update Footer (and the Button usage) to
avoid rendering an inert anchor by checking the result of getBotInviteUrl() and
either (A) conditionally render the anchor child only if url is truthy and
otherwise render a disabled Button (add Button's disabled prop and accessible
label/tooltip) or (B) render a non-anchor element (e.g., a span) inside Button
when no url to prevent navigation. Locate getBotInviteUrl() usage in Footer.tsx
and implement one of these flows, ensuring target/rel are only set when an
actual href exists and the disabled state is communicated to assistive tech.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/landing/Footer.tsx`:
- Around line 3-6: Imports in Footer.tsx are failing Biome's organizeImports
because the named imports from lucide-react are not alphabetized; update the
lucide-react import to sort the icons alphabetically (e.g., BookOpen, Github,
Heart, MessageCircle) and then run the organizer so the overall import blocks
remain in Biome's expected order, targeting the line with "import { Github,
BookOpen, MessageCircle, Heart } from 'lucide-react';".

In `@web/src/components/landing/Pricing.tsx`:
- Around line 156-161: The CTA currently renders an anchor with
getBotInviteUrl() || '#' which produces a non-functional link when
NEXT_PUBLIC_DISCORD_CLIENT_ID is unset; change the rendering inside the Button
wrapper to conditionally render the anchor only when getBotInviteUrl() returns a
truthy url (similar to InviteButton in Hero.tsx) and otherwise render nothing
(or omit the Button/anchor for that tier) so users don't see a non-working CTA;
locate the button block that uses Button and tier.cta alongside
getBotInviteUrl() and add the conditional check around the anchor render (or
return null for that CTA) to hide the invite when no URL is available.
- Around line 3-7: Sort the imports in Pricing.tsx to satisfy Biome's
organizeImports rule: reorder the import statements so external packages come
first (e.g., 'framer-motion', 'react', 'lucide-react'), then internal project
imports (e.g., '@/components/ui/button', '@/lib/discord'), and ensure named
imports stay grouped (motion, useInView), (useRef, useState), (Check), (Button),
(getBotInviteUrl); update the import block accordingly so linter passes.

In `@web/src/components/landing/Stats.tsx`:
- Around line 3-5: Imports in Stats.tsx are not sorted per the linter; reorder
the import statements so external/library imports come first (e.g.,
'framer-motion' then 'react' then 'lucide-react') and alphabetize named imports
inside each import (e.g., motion, useInView; useEffect, useRef, useState;
MessageSquare, Star, Users) to satisfy Biome's organizeImports rule and
eliminate the pipeline error.
- Around line 107-126: The map over testimonials uses the array index as the
React key (key={i}) which is brittle; instead add a stable unique identifier to
each testimonial object (e.g., id) in the testimonials data source and change
the mapping to use that id as the key (use key={t.id}); if you cannot modify the
data source, derive a stable key from immutable fields such as a hash or
concatenation of t.author and t.quote and replace key={i} in the map inside the
Testimonials rendering block (the motion.div created for each t) with that
stable identifier.

---

Duplicate comments:
In `@web/src/components/landing/Footer.tsx`:
- Around line 27-35: The CTA currently falls back to an inert '#' when
getBotInviteUrl() is unset, which produces a clickable-but-no-op link; update
Footer (and the Button usage) to avoid rendering an inert anchor by checking the
result of getBotInviteUrl() and either (A) conditionally render the anchor child
only if url is truthy and otherwise render a disabled Button (add Button's
disabled prop and accessible label/tooltip) or (B) render a non-anchor element
(e.g., a span) inside Button when no url to prevent navigation. Locate
getBotInviteUrl() usage in Footer.tsx and implement one of these flows, ensuring
target/rel are only set when an actual href exists and the disabled state is
communicated to assistive tech.

In `@web/src/components/landing/Pricing.tsx`:
- Around line 83-95: The billing toggle button in Pricing.tsx is missing an
explicit type and defaults to submit; update the button element (the one with
onClick={() => setIsAnnual(!isAnnual)} and aria-checked={isAnnual}) to include
type="button" so it won't submit parent forms unexpectedly; locate the button
that uses setIsAnnual/isAnnual and add the type attribute.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5eb1b5c and 5a493ef.

📒 Files selected for processing (6)
  • web/src/app/globals.css
  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/Hero.tsx
  • web/src/components/landing/Pricing.tsx
  • web/src/components/landing/Stats.tsx
  • web/tests/app/landing.test.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
{src,web}/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Winston logger from src/logger.js, NEVER use console.*

Files:

  • web/src/components/landing/Hero.tsx
  • web/src/components/landing/Stats.tsx
  • web/src/components/landing/Pricing.tsx
  • web/src/components/landing/Footer.tsx
  • web/tests/app/landing.test.tsx
web/**/*.{ts,tsx,jsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

Use Next.js 16 App Router for web dashboard, with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design

Files:

  • web/src/components/landing/Hero.tsx
  • web/src/components/landing/Stats.tsx
  • web/src/components/landing/Pricing.tsx
  • web/src/components/landing/Footer.tsx
  • web/tests/app/landing.test.tsx
🧠 Learnings (2)
📚 Learning: 2026-03-02T21:23:59.512Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T21:23:59.512Z
Learning: Applies to web/**/*.{ts,tsx,jsx,js} : Use Next.js 16 App Router for web dashboard, with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design

Applied to files:

  • web/src/components/landing/Hero.tsx
📚 Learning: 2026-03-02T21:23:59.512Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T21:23:59.512Z
Learning: Applies to tests/**/*.{js,ts} : Maintain 80% test coverage threshold — Never lower the coverage requirement

Applied to files:

  • web/tests/app/landing.test.tsx
🪛 GitHub Actions: CI
web/src/components/landing/Stats.tsx

[error] 109-109: lint/suspicious/noArrayIndexKey: Avoid using the index of an array as key property in an element.


[error] 3-3: assist/source/organizeImports: The imports and exports are not sorted. Organize Imports (Biome) suggested.

web/src/components/landing/Pricing.tsx

[error] 83-83: lint/a11y/useButtonType: Provide an explicit type prop for the button element.


[error] 3-3: assist/source/organizeImports: The imports and exports are not sorted. Organize Imports (Biome) suggested.

web/src/components/landing/Footer.tsx

[error] 3-3: assist/source/organizeImports: The imports and exports are not sorted. Organize Imports (Biome) suggested.

🪛 Stylelint (17.3.0)
web/src/app/globals.css

[error] 5-5: Expected "SFMono-Regular" to be "sfmono-regular" (value-keyword-case)

(value-keyword-case)


[error] 5-5: Expected "Menlo" to be "menlo" (value-keyword-case)

(value-keyword-case)


[error] 5-5: Expected "Monaco" to be "monaco" (value-keyword-case)

(value-keyword-case)


[error] 5-5: Expected "Consolas" to be "consolas" (value-keyword-case)

(value-keyword-case)


[error] 4-4: Unexpected unknown at-rule "@theme" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)


[error] 160-160: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🔇 Additional comments (12)
web/src/app/globals.css (2)

69-76: Consolidate duplicate @keyframes blink definitions.

The blink animation is defined twice: once inside the @theme block (lines 69-76) and again in the global scope (lines 169-176). This creates potential drift and unnecessary duplication. Keep only one definition.

🔧 Proposed fix

Remove the duplicate at lines 169-176 since the @theme block definition is already sufficient:

 /* Terminal cursor blink animation */
 .terminal-cursor {
   animation: blink 1s step-end infinite;
 }
-
-@keyframes blink {
-  0%, 100% {
-    opacity: 1;
-  }
-  50% {
-    opacity: 0;
-  }
-}

Also applies to: 169-176


3-52: LGTM on the Terminal Chic theme tokens and font configuration.

The CSS variable structure is well-organized with clear separation between terminal-specific colors and semantic tokens. The font stack for --font-mono follows proper fallback conventions.

web/src/components/landing/Footer.tsx (1)

57-77: LGTM on the external links.

Links include proper target="_blank" and hover transitions. The structure is clean and accessible.

web/src/components/landing/Hero.tsx (4)

10-46: LGTM on the useTypewriter hook cleanup.

The interval is now properly stored in a ref and cleaned up in the effect's return function, preventing memory leaks when the component unmounts or dependencies change.


147-159: Good pattern for conditional invite button rendering.

The InviteButton component correctly returns null when getBotInviteUrl() is falsy, avoiding broken CTAs. This pattern should be applied in Pricing.tsx and Footer.tsx as well.


161-229: prefers-reduced-motion support is still missing.

The Hero component runs animations unconditionally without respecting the prefers-reduced-motion preference. This affects the typewriter effect, blinking cursor, Framer Motion transitions, and the ChatPreview typing indicator. Consider adding a usePrefersReducedMotion hook and conditionally disabling/simplifying animations.


183-184: Cursor still doesn't satisfy the 3-blink-cycle requirement.

Per the linked issue requirements, the cursor should blink 3 times after typing completes before being removed. Currently, {!isComplete && <BlinkingCursor />} removes the cursor immediately when isComplete becomes true.

web/tests/app/landing.test.tsx (3)

5-22: LGTM on the Framer Motion mock.

The mock correctly replaces motion components with simple DOM elements and stubs useInView to return true, allowing tests to run without animation timing issues.


36-42: Tighten the hero assertion to avoid false positives.

The current check using getAllByText(/volvox-bot/) could match text outside the hero section (e.g., navbar brand). Consider asserting on a heading role for more precise validation.

🔧 Proposed fix
   it('renders the hero heading with volvox-bot', () => {
     render(<LandingPage />);
-    // The typewriter effect renders "volvox-bot" after the ">" prompt
-    // Check that the brand name appears somewhere in the document
-    const volvoxElements = screen.getAllByText(/volvox-bot/);
-    expect(volvoxElements.length).toBeGreaterThan(0);
+    // Verify the hero heading contains the brand name
+    // Note: The typewriter effect is mocked, so full text is rendered
+    expect(screen.getByText(/> volvox-bot/)).toBeInTheDocument();
   });

44-79: LGTM on the updated assertions.

The tests now correctly check for the new feature labels (AI Chat, Moderation, Starboard, Analytics), footer links (GitHub, Support Server), and CTA text ("Ready to upgrade your server"). The environment variable handling for Add to Server button visibility is well-structured.

web/src/components/landing/Stats.tsx (2)

7-36: LGTM on the AnimatedCounter RAF cleanup.

The rafRef properly stores the animation frame ID and cancels it in the cleanup function, preventing memory leaks and stale state updates when the component unmounts or dependencies change.


38-54: Remove or verify testimonials and trust claims.

The testimonials attribute quotes to named individuals with specific company affiliations (Vercel, Linear, OpenSaaS), and the trust badge claims endorsement from major brands (Vercel, Linear, GitHub). Without documented authorization, this creates legal exposure for false advertising and brand misuse.

Options:

  1. Remove named attributions and use anonymous testimonials
  2. Obtain documented written consent from referenced individuals and organizations
  3. Remove the "Trusted by" claim unless partnerships exist

Also applies to: 137-139

- Sort imports in FeatureGrid, Footer, Pricing, Stats
- Add unique IDs to testimonials array
- Fix button type for billing toggle (type=button)
Copilot AI review requested due to automatic review settings March 4, 2026 00:28
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 4, 2026
- Add explicit type for testimonials array with id field
- Update pnpm-lock.yaml for framer-motion dependency
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (2)

web/src/app/globals.css:52

  • --animate-typewriter references a typewriter keyframe, but no @keyframes typewriter is defined in this file (and the variable doesn't appear to be used elsewhere). Either define the missing keyframes or remove the unused token to avoid dead/incorrect theme configuration.
  /* Animations */
  --animate-accordion-down: accordion-down 0.2s ease-out;
  --animate-accordion-up: accordion-up 0.2s ease-out;
  --animate-blink: blink 1s step-end infinite;
  --animate-typewriter: typewriter 0.1s steps(1) forwards;
  

web/src/app/page.tsx:21

  • InviteButton is re-implemented here and again inside components/landing/Hero.tsx with the same logic/markup. Duplicating this logic makes future changes (copy updates, invite URL behavior, tracking, etc.) easy to miss in one place. Consider extracting a shared InviteButton component (e.g., under src/components/landing/InviteButton.tsx or src/components/InviteButton.tsx) and reusing it in both places.
/** Render an "Add to Server" button — disabled/hidden when CLIENT_ID is unset. */
function InviteButton({ size = 'sm', className }: { size?: 'sm' | 'lg'; className?: string }) {
  const url = getBotInviteUrl();
  if (!url) return null;
  return (
    <Button variant="discord" size={size} className={className} asChild>
      <a href={url} target="_blank" rel="noopener noreferrer">
        {size === 'lg' && <Bot className="mr-2 h-5 w-5" />}
        Add to Server
      </a>
    </Button>
  );

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
web/src/components/landing/Stats.tsx (1)

38-57: ⚠️ Potential issue | 🟠 Major

Use only verified testimonial/brand claims in public copy.

Line [42]-Line [55] attributes quotes to named people and employer brands, and Line [139] makes explicit third-party trust claims. Please ship only claims with documented permission/verification, or anonymize/remove these references.

🛠️ Safer fallback copy example
 const testimonials: { id: string; quote: string; author: string; role: string }[] = [
   {
     id: 'testimonial-1',
     quote: "Finally, a Discord bot that doesn't suck. The AI actually understands context.",
-    author: 'Sarah Chen',
-    role: 'DevOps Engineer @ Vercel',
+    author: 'Verified user',
+    role: 'DevOps Engineer',
   },
@@
           <p className="text-[var(--text-muted)] text-sm">
-            Trusted by teams at Vercel, Linear, GitHub, and thousands of open-source communities
+            Trusted by thousands of open-source communities
           </p>

Also applies to: 139-140

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/landing/Stats.tsx` around lines 38 - 57, The testimonials
array in Stats.tsx contains named individuals and employer brands without
documented permission; update the testimonials data (the testimonials constant)
to remove or anonymize personal names and company brands by replacing author
with generic labels like "Verified user" or "Anonymous" and replacing role
fields with non-branded descriptors like "Software engineer" or "Community
manager" (or omit role entirely), and ensure any third-party trust text
elsewhere (the explicit trust claim referenced near lines 139-140) is removed or
rewritten to a non-branded, permission-safe statement; keep the same array shape
(id, quote, author, role) if needed so components like the Testimonials renderer
still work.
web/src/components/landing/Pricing.tsx (1)

149-161: ⚠️ Potential issue | 🟠 Major

Don’t render a dead '#' CTA when invite URL is unavailable.

Line [158] uses getBotInviteUrl() || '#', which produces a clickable no-op when the OAuth client ID is unset. Render a disabled/non-clickable fallback (or hide CTA) instead.

🔧 Suggested fix
 export function Pricing() {
   const [isAnnual, setIsAnnual] = useState(false);
   const containerRef = useRef(null);
   const isInView = useInView(containerRef, { once: true, margin: '-100px' });
+  const inviteUrl = getBotInviteUrl();
@@
-              <Button
-                variant={tier.popular ? 'default' : 'outline'}
-                className={`w-full mb-6 font-mono ${
-                  tier.popular
-                    ? 'bg-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/90'
-                    : ''
-                }`}
-                asChild
-              >
-                <a href={getBotInviteUrl() || '#'} target="_blank" rel="noopener noreferrer">
-                  {tier.cta}
-                </a>
-              </Button>
+              {inviteUrl ? (
+                <Button
+                  variant={tier.popular ? 'default' : 'outline'}
+                  className={`w-full mb-6 font-mono ${
+                    tier.popular
+                      ? 'bg-[var(--accent-primary)] hover:bg-[var(--accent-primary)]/90'
+                      : ''
+                  }`}
+                  asChild
+                >
+                  <a href={inviteUrl} target="_blank" rel="noopener noreferrer">
+                    {tier.cta}
+                  </a>
+                </Button>
+              ) : (
+                <Button
+                  variant={tier.popular ? 'default' : 'outline'}
+                  className="w-full mb-6 font-mono"
+                  disabled
+                >
+                  {tier.cta}
+                </Button>
+              )}

As per coding guidelines web/**/*.{ts,tsx,jsx,js}: Use Next.js 16 App Router for web dashboard, with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/landing/Pricing.tsx` around lines 149 - 161, The CTA
currently uses getBotInviteUrl() || '#' which renders a clickable dead link;
change Pricing.tsx to detect when getBotInviteUrl() returns falsy and in that
case render a non-clickable fallback instead of an <a href="#">: either hide the
CTA for that tier or render the Button (component used in this block) without
the asChild anchor and with disabled/aria-disabled set and appropriate
styling/tooltip, otherwise render the existing anchor with
href={getBotInviteUrl()} target/_rel as now; update the conditional around
tier.cta rendering to use getBotInviteUrl() truthiness rather than using '#'.
web/src/components/landing/Footer.tsx (1)

27-35: ⚠️ Potential issue | 🟠 Major

Avoid '#' fallback for the primary footer CTA.

Line [32] creates a non-functional click target when the invite URL is unavailable. Condition the CTA on a valid URL (or render a disabled state) so users never hit a dead path.

🔧 Suggested fix
 export function Footer() {
+  const inviteUrl = getBotInviteUrl();
+
   return (
@@
-          <Button
-            size="lg"
-            className="font-mono text-lg px-8 py-6 bg-[var(--accent-success)] hover:bg-[var(--accent-success)]/90 text-white"
-            asChild
-          >
-            <a href={getBotInviteUrl() || '#'} target="_blank" rel="noopener noreferrer">
-              Add to Discord — Free
-            </a>
-          </Button>
+          {inviteUrl && (
+            <Button
+              size="lg"
+              className="font-mono text-lg px-8 py-6 bg-[var(--accent-success)] hover:bg-[var(--accent-success)]/90 text-white"
+              asChild
+            >
+              <a href={inviteUrl} target="_blank" rel="noopener noreferrer">
+                Add to Discord — Free
+              </a>
+            </Button>
+          )}

As per coding guidelines web/**/*.{ts,tsx,jsx,js}: Use Next.js 16 App Router for web dashboard, with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/landing/Footer.tsx` around lines 27 - 35, The primary
footer CTA uses getBotInviteUrl() with a '#' fallback creating a dead click
target; update Footer.tsx to require a valid invite URL before rendering the
anchor link: call getBotInviteUrl() once, store it in a variable, and if it's
truthy render the current Button as an anchor (with target and rel) using that
URL, otherwise render the Button in a disabled/aria-disabled state (or render
alternate copy) so it is non-clickable and accessible; reference the Button
component and getBotInviteUrl() in Footer.tsx and ensure accessibility
attributes (disabled/aria-disabled, title) reflect the disabled state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/landing/FeatureGrid.tsx`:
- Around line 3-5: The component currently applies Framer Motion entrance and
hover animations unconditionally; update FeatureGrid to respect
prefers-reduced-motion by importing and using useReducedMotion() and gating all
motion props and interactive animations (the entrance variants used where
useInView is combined and the hover/whileHover props around the
BarChart3/MessageSquare/Shield/Star items) so that when useReducedMotion()
returns true you skip or replace animations with non-animated static props;
apply the same pattern to the header animation block (the motion usage around
the header at lines referenced) so both entrance/hover and header animations are
disabled when reduced motion is requested.

---

Duplicate comments:
In `@web/src/components/landing/Footer.tsx`:
- Around line 27-35: The primary footer CTA uses getBotInviteUrl() with a '#'
fallback creating a dead click target; update Footer.tsx to require a valid
invite URL before rendering the anchor link: call getBotInviteUrl() once, store
it in a variable, and if it's truthy render the current Button as an anchor
(with target and rel) using that URL, otherwise render the Button in a
disabled/aria-disabled state (or render alternate copy) so it is non-clickable
and accessible; reference the Button component and getBotInviteUrl() in
Footer.tsx and ensure accessibility attributes (disabled/aria-disabled, title)
reflect the disabled state.

In `@web/src/components/landing/Pricing.tsx`:
- Around line 149-161: The CTA currently uses getBotInviteUrl() || '#' which
renders a clickable dead link; change Pricing.tsx to detect when
getBotInviteUrl() returns falsy and in that case render a non-clickable fallback
instead of an <a href="#">: either hide the CTA for that tier or render the
Button (component used in this block) without the asChild anchor and with
disabled/aria-disabled set and appropriate styling/tooltip, otherwise render the
existing anchor with href={getBotInviteUrl()} target/_rel as now; update the
conditional around tier.cta rendering to use getBotInviteUrl() truthiness rather
than using '#'.

In `@web/src/components/landing/Stats.tsx`:
- Around line 38-57: The testimonials array in Stats.tsx contains named
individuals and employer brands without documented permission; update the
testimonials data (the testimonials constant) to remove or anonymize personal
names and company brands by replacing author with generic labels like "Verified
user" or "Anonymous" and replacing role fields with non-branded descriptors like
"Software engineer" or "Community manager" (or omit role entirely), and ensure
any third-party trust text elsewhere (the explicit trust claim referenced near
lines 139-140) is removed or rewritten to a non-branded, permission-safe
statement; keep the same array shape (id, quote, author, role) if needed so
components like the Testimonials renderer still work.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a493ef and e7e07c0.

📒 Files selected for processing (4)
  • web/src/components/landing/FeatureGrid.tsx
  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/Pricing.tsx
  • web/src/components/landing/Stats.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
{src,web}/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Winston logger from src/logger.js, NEVER use console.*

Files:

  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/FeatureGrid.tsx
  • web/src/components/landing/Pricing.tsx
  • web/src/components/landing/Stats.tsx
web/**/*.{ts,tsx,jsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

Use Next.js 16 App Router for web dashboard, with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design

Files:

  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/FeatureGrid.tsx
  • web/src/components/landing/Pricing.tsx
  • web/src/components/landing/Stats.tsx
🧠 Learnings (2)
📚 Learning: 2026-03-02T21:23:59.512Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T21:23:59.512Z
Learning: Applies to web/**/*.{ts,tsx,jsx,js} : Use Next.js 16 App Router for web dashboard, with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design

Applied to files:

  • web/src/components/landing/Footer.tsx
📚 Learning: 2026-03-02T21:23:59.512Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T21:23:59.512Z
Learning: Applies to src/**/*.{js,ts,jsx,tsx} : Use 2-space indent, enforced by Biome

Applied to files:

  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/Pricing.tsx
  • web/src/components/landing/Stats.tsx

When getBotInviteUrl() returns null, show disabled button instead of
opening useless blank tab with '#' href.
- Free tier (~/dev/null) now links to GitHub repo for self-hosting
- Paid tiers use bot invite URL when available
- When bot invite URL is null, show disabled button instead of
  opening blank tab with '#' href
Update the framer-motion mock to use React.forwardRef so motion
components can receive refs. This eliminates React warnings about
function components not being able to receive refs when landing
components pass refs to motion.div (e.g., FeatureGrid cards).
Copilot AI review requested due to automatic review settings March 4, 2026 00:45
Replace require('react') with async import inside vi.mock factory.
The vi.hoisted approach doesn't work with ESM module initialization order.
@BillChirico
Copy link
Collaborator Author

Thread resolved: Fixed the ESM compatibility issue in the Framer Motion mock by using async import inside the vi.mock factory instead of require(). Tests now pass. See commit 66120bd.

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 4, 2026
- Deploys each PR to a unique preview environment (pr-{number})
- Posts/updates comment on PR with preview URL
- Automatically cleans up preview environment when PR is closed
- Builds web dashboard before deployment
- Uses Railway CLI for deployments
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

♻️ Duplicate comments (6)
web/tests/app/landing.test.tsx (1)

38-44: 🧹 Nitpick | 🔵 Trivial

Tighten the hero assertion to avoid false positives.

The current check can pass from non-hero text (e.g., navbar brand "volvox-bot"). Assert on a heading role to validate the actual hero heading behavior.

🔧 Proposed fix
   it('renders the hero heading with volvox-bot', () => {
     render(<LandingPage />);
-    // The typewriter effect renders "volvox-bot" after the ">" prompt
-    // Check that the brand name appears somewhere in the document
-    const volvoxElements = screen.getAllByText(/volvox-bot/);
-    expect(volvoxElements.length).toBeGreaterThan(0);
+    expect(screen.getByRole('heading', { name: /volvox-bot/i })).toBeInTheDocument();
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/tests/app/landing.test.tsx` around lines 38 - 44, The test is too loose:
replace the generic text check that uses screen.getAllByText(/volvox-bot/) and
the length assertion with a role-based heading assertion to ensure the hero
heading is targeted; after render(<LandingPage />) use
screen.getByRole('heading', { name: /volvox-bot/i }) (or getAllByRole and assert
one matches) and assert it exists (e.g., toBeInTheDocument or truthy) so the
test specifically verifies the hero heading rendered by LandingPage.
web/src/app/page.tsx (1)

20-52: ⚠️ Potential issue | 🟠 Major

Navbar needs a mobile breakpoint strategy.

All nav links and actions are rendered in a single row, which will overflow on narrow screens and break the header UX. As per coding guidelines, web/**/*.{ts,tsx,jsx,js} should use mobile-responsive design.

Consider hiding the link group on small screens and showing a hamburger menu, or using responsive utilities to collapse secondary items.

📱 Suggested responsive fix
           <nav className="flex items-center gap-4">
+            <div className="hidden md:flex items-center gap-4">
             <a
               href="#features"
               className="text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
             >
               Features
             </a>
             <!-- ... other links ... -->
+            </div>
             <ThemeToggle />
             <Button variant="outline" size="sm" asChild>
               <Link href="/login">Sign In</Link>
             </Button>
             <InviteButton size="sm" />
           </nav>

For a complete solution, add a mobile menu toggle component that shows the stacked links on small screens.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/page.tsx` around lines 20 - 52, The navbar currently renders all
links and actions in one row and will overflow on narrow screens; update the
component so the main link group is hidden on small screens and replaced with a
mobile menu toggle: wrap the current link group (the <nav> links including
Features, Pricing, Docs, GitHub, ThemeToggle, Button/Link Sign In, InviteButton)
in a container with responsive Tailwind classes (e.g., hidden on small, md:flex)
and add a new MobileMenu / Hamburger button component (rendered md:hidden) that
toggles a stacked mobile menu; implement the toggle state in the page component,
render the same link items inside the mobile menu (stacked and full-width), and
ensure accessible attributes (aria-expanded, aria-controls) on the toggle and
proper focus/escape handling for the mobile menu.
web/src/app/globals.css (2)

69-76: 🧹 Nitpick | 🔵 Trivial

Keep only one @keyframes blink definition.

blink is defined twice (Line 69 and Line 169). Keeping both invites drift and makes animation behavior harder to reason about.

Suggested cleanup
 .terminal-cursor {
   animation: blink 1s step-end infinite;
 }
-
-@keyframes blink {
-  0%, 100% {
-    opacity: 1;
-  }
-  50% {
-    opacity: 0;
-  }
-}

Also applies to: 169-176

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/globals.css` around lines 69 - 76, There are two duplicate
`@keyframes` blink definitions; remove one so only a single `@keyframes` blink
remains to avoid drift and inconsistent animation behavior—locate the two blocks
named "@keyframes blink" and delete the redundant one (keep the intended
timing/opacity version you want to preserve), then verify any CSS rules
referencing "animation: blink" still behave as expected.

165-167: ⚠️ Potential issue | 🟠 Major

Respect reduced-motion for the infinite cursor animation.

Line 166 always blinks infinitely. Add a reduced-motion fallback so motion-sensitive users can opt out.

Suggested a11y-safe animation guard
-/* Terminal cursor blink animation */
-.terminal-cursor {
-  animation: blink 1s step-end infinite;
-}
+/* Terminal cursor blink animation */
+@media (prefers-reduced-motion: no-preference) {
+  .terminal-cursor {
+    animation: blink 1s step-end infinite;
+  }
+}
+
+@media (prefers-reduced-motion: reduce) {
+  .terminal-cursor {
+    animation: none;
+    opacity: 1;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/globals.css` around lines 165 - 167, The .terminal-cursor
infinite blink animation doesn't respect users' reduced-motion preference; wrap
or override the animation with a prefers-reduced-motion media query so
motion-sensitive users don't see the blinking cursor (e.g., inside a `@media`
(prefers-reduced-motion: reduce) block set .terminal-cursor animation to none or
a non-animated fallback). Update the rule that targets .terminal-cursor and the
associated blink animation so the media query disables the animation while
preserving the normal visual fallback.
web/src/components/landing/Hero.tsx (2)

167-167: ⚠️ Potential issue | 🟡 Minor

Cursor unmounts too early for the 3-blink completion requirement.

At Line 167, the cursor disappears immediately when typing completes, so post-typing blink cycles never occur.

Suggested behavior fix
 export function Hero() {
@@
   const { displayText, isComplete } = useTypewriter('volvox-bot', 80, 300);
+  const [showCursor, setShowCursor] = useState(true);
+
+  useEffect(() => {
+    if (!isComplete) {
+      setShowCursor(true);
+      return;
+    }
+    const hideAfterThreeBlinks = setTimeout(() => setShowCursor(false), 3000);
+    return () => clearTimeout(hideAfterThreeBlinks);
+  }, [isComplete]);
@@
-            {!isComplete && <BlinkingCursor />}
+            {showCursor && <BlinkingCursor />}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/landing/Hero.tsx` at line 167, The BlinkingCursor
currently unmounts immediately because Hero.tsx only renders it when
!isComplete; change rendering to keep the cursor mounted until three
post-completion blinks finish by introducing a local state (e.g., showCursor or
remainingBlinks) and render <BlinkingCursor /> while (!isComplete ||
showCursor). In the effect that watches isComplete, when isComplete becomes true
start a blink counter/timer (useEffect with setInterval or a timeout chain) to
perform three additional blink cycles, decrement remainingBlinks each cycle and
clear the timer when it reaches zero, then set showCursor false; update the
render condition and any props to BlinkingCursor as needed so it continues
blinking during those extra cycles.

11-43: ⚠️ Potential issue | 🟠 Major

Hero animations still ignore prefers-reduced-motion.

Typewriter timers, Framer Motion transitions, and the infinite typing-indicator loop all run unconditionally. This is an accessibility regression for motion-sensitive users.

Suggested direction (single reduced-motion flag)
-import { motion, useInView } from 'framer-motion';
+import { motion, useInView } from 'framer-motion';
 import { ArrowRight, Bot, MessageSquare, Sparkles, Terminal } from 'lucide-react';
 import Link from 'next/link';
 import { useEffect, useRef, useState } from 'react';

+function usePrefersReducedMotion() {
+  const [reduced, setReduced] = useState(false);
+  useEffect(() => {
+    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
+    setReduced(mq.matches);
+    const onChange = (e: MediaQueryListEvent) => setReduced(e.matches);
+    mq.addEventListener('change', onChange);
+    return () => mq.removeEventListener('change', onChange);
+  }, []);
+  return reduced;
+}
+
-function useTypewriter(text: string, speed = 100, delay = 500) {
+function useTypewriter(text: string, speed = 100, delay = 500, reducedMotion = false) {
   const [displayText, setDisplayText] = useState('');
   const [isComplete, setIsComplete] = useState(false);
@@
   useEffect(() => {
+    if (reducedMotion) {
+      setDisplayText(text);
+      setIsComplete(true);
+      return;
+    }
@@
-  }, [text, speed, delay]);
+  }, [text, speed, delay, reducedMotion]);
@@
 export function Hero() {
+  const reducedMotion = usePrefersReducedMotion();
   const ref = useRef(null);
   const isInView = useInView(ref, { once: true });
-  const { displayText, isComplete } = useTypewriter('volvox-bot', 80, 300);
+  const { displayText, isComplete } = useTypewriter('volvox-bot', 80, 300, reducedMotion);

Then gate transitions/repeats with reducedMotion ? 0 : ... and avoid infinite repeats when reduced motion is enabled.

Also applies to: 67-70, 112-136, 160-187

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/landing/Hero.tsx` around lines 11 - 43, The typewriter
hook useTypewriter currently always runs timers and intervals, ignoring users'
prefers-reduced-motion; modify it to accept or read a reducedMotion flag (e.g.,
from Framer Motion's useReducedMotion or window.matchMedia) and if reducedMotion
is true immediately set displayText to text and setIsComplete(true) without
creating any setTimeout/setInterval; otherwise run the existing delayed interval
logic as-is, and ensure all timers are still cleared on cleanup. Also apply the
same reducedMotion gating to the typing-indicator loop and any Framer Motion
transition props (replace repeats/infinite and transition durations with 0 or
no-repeat when reducedMotion is true) so animations do not run for
motion-sensitive users.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/railway-preview.yml:
- Around line 80-83: The bot comment detection is too broad—update the
comments.find used to set botComment so it also verifies the bot's login (e.g.,
check comment.user.login === 'github-actions[bot]' or the specific app name) in
addition to comment.user.type === 'Bot' and body.includes('🚀 Railway Preview
Deployment'); modify the predicate that currently references comment.user.type
and comment.body to include comment.user.login for precise matching (or use an
environment/secret value for the expected bot login if configurable).
- Around line 3-6: The workflow's pull_request trigger omits the 'closed' action
so the cleanup-preview job (checks if: github.event.action == 'closed') never
runs; update the trigger types for the pull_request event to include 'closed'
(i.e., add 'closed' to the array of types) so the cleanup-preview job can
execute when PRs are closed.
- Around line 50-64: The railway domain lookup runs immediately after an
asynchronous detached deployment (railway up --detach), causing a race where
DEPLOY_URL can be empty; update the railway-deploy step to poll/retry the domain
lookup (railway domain --service=volvox-bot --environment=$PREVIEW_ENV) with a
reasonable backoff and timeout, sleeping between attempts and breaking once a
non-empty DEPLOY_URL is returned, and only then echo "deploy_url=$DEPLOY_URL" >>
$GITHUB_OUTPUT; ensure the logic uses PREVIEW_ENV and fails the job if the
timeout is reached to avoid publishing an empty URL.
- Around line 26-27: The workflow step named "Setup pnpm" currently uses the
mutable tag pnpm/[email protected]; replace that tag with the corresponding
full commit SHA for the action (e.g., pnpm/action-setup@<full-commit-sha>) to
pin the action to an immutable reference, and do the same for any other
third-party actions in this file to improve supply-chain security.
- Around line 38-45: The preview workflow's "Build web dashboard" step is
missing the public Discord client ID env var used by the new InviteButton; add
NEXT_PUBLIC_DISCORD_CLIENT_ID to the step's env block and map it to the
corresponding secret (e.g., set NEXT_PUBLIC_DISCORD_CLIENT_ID: ${{
secrets.NEXT_PUBLIC_DISCORD_CLIENT_ID }}) so the landing page's InviteButton
receives the value at build time.

In `@web/src/app/globals.css`:
- Around line 159-161: Remove the stray blank line before the font declaration
to satisfy stylelint's declaration-empty-line-before rule: ensure the
declarations in the same rule are consecutive by placing `font-feature-settings:
"rlig" 1, "calt" 1;` directly after `@apply bg-background text-foreground
antialiased;` (no empty line), update the rule containing `@apply` and
`font-feature-settings`, then re-run stylelint/formatter to confirm the warning
is gone.

In `@web/src/app/page.tsx`:
- Around line 3-6: Imports in page.tsx are not sorted per the linter; reorder
the import statements so they follow the project's organizeImports rule (e.g.,
group external packages first, then absolute/internal imports alphabetically).
Specifically, sort the imports for Link, ThemeToggle, Button, and the landing
components (Hero, FeatureGrid, Pricing, Stats, Footer, InviteButton) into the
correct order and grouping so Biome's organizeImports passes.

In `@web/src/components/landing/Footer.tsx`:
- Around line 9-10: In Footer.tsx, remove the trailing whitespace or other
formatting mismatch on the line where botInviteUrl is assigned (the statement
using getBotInviteUrl()), then re-run the project's formatter/linters so the
file matches the repository style; specifically fix the line "const botInviteUrl
= getBotInviteUrl();" to have no trailing spaces and ensure surrounding
whitespace/indentation conforms to the formatter.

In `@web/src/components/landing/Hero.tsx`:
- Around line 49-51: The BlinkingCursor component's JSX is not formatted to the
project's formatting rules causing CI failures; run the project's formatter
(e.g., prettier/tsconfig formatting script) on the BlinkingCursor function to
fix whitespace/indentation and ensure the JSX return is properly formatted and
closed (refer to the BlinkingCursor function name and its JSX span element),
then stage the formatted change so the CI no longer reports formatting drift.

In `@web/src/components/landing/index.ts`:
- Around line 1-6: The export list in web/src/components/landing/index.ts is not
sorted; reorder the named exports (Hero, FeatureGrid, Pricing, Stats, Footer,
InviteButton) into the expected alphabetical order (e.g., FeatureGrid, Footer,
Hero, InviteButton, Pricing, Stats) so the formatter (Biome) passes; update the
export lines in the file to match that sorted sequence.

In `@web/src/components/landing/Pricing.tsx`:
- Around line 56-61: The Pricing component runs Framer Motion animations
unconditionally; update it to respect user reduced-motion preferences by
importing and calling useReducedMotion() inside Pricing (e.g., const
shouldReduceMotion = useReducedMotion()), then gate or disable animations on the
Motion elements (or pass reduced variants/animate props) when shouldReduceMotion
is true—use the existing containerRef/isInView logic but skip or simplify motion
(no heavy transforms/transitions) whenever shouldReduceMotion is set so
animation behavior matches FeatureGrid/Hero.

---

Duplicate comments:
In `@web/src/app/globals.css`:
- Around line 69-76: There are two duplicate `@keyframes` blink definitions;
remove one so only a single `@keyframes` blink remains to avoid drift and
inconsistent animation behavior—locate the two blocks named "@keyframes blink"
and delete the redundant one (keep the intended timing/opacity version you want
to preserve), then verify any CSS rules referencing "animation: blink" still
behave as expected.
- Around line 165-167: The .terminal-cursor infinite blink animation doesn't
respect users' reduced-motion preference; wrap or override the animation with a
prefers-reduced-motion media query so motion-sensitive users don't see the
blinking cursor (e.g., inside a `@media` (prefers-reduced-motion: reduce) block
set .terminal-cursor animation to none or a non-animated fallback). Update the
rule that targets .terminal-cursor and the associated blink animation so the
media query disables the animation while preserving the normal visual fallback.

In `@web/src/app/page.tsx`:
- Around line 20-52: The navbar currently renders all links and actions in one
row and will overflow on narrow screens; update the component so the main link
group is hidden on small screens and replaced with a mobile menu toggle: wrap
the current link group (the <nav> links including Features, Pricing, Docs,
GitHub, ThemeToggle, Button/Link Sign In, InviteButton) in a container with
responsive Tailwind classes (e.g., hidden on small, md:flex) and add a new
MobileMenu / Hamburger button component (rendered md:hidden) that toggles a
stacked mobile menu; implement the toggle state in the page component, render
the same link items inside the mobile menu (stacked and full-width), and ensure
accessible attributes (aria-expanded, aria-controls) on the toggle and proper
focus/escape handling for the mobile menu.

In `@web/src/components/landing/Hero.tsx`:
- Line 167: The BlinkingCursor currently unmounts immediately because Hero.tsx
only renders it when !isComplete; change rendering to keep the cursor mounted
until three post-completion blinks finish by introducing a local state (e.g.,
showCursor or remainingBlinks) and render <BlinkingCursor /> while (!isComplete
|| showCursor). In the effect that watches isComplete, when isComplete becomes
true start a blink counter/timer (useEffect with setInterval or a timeout chain)
to perform three additional blink cycles, decrement remainingBlinks each cycle
and clear the timer when it reaches zero, then set showCursor false; update the
render condition and any props to BlinkingCursor as needed so it continues
blinking during those extra cycles.
- Around line 11-43: The typewriter hook useTypewriter currently always runs
timers and intervals, ignoring users' prefers-reduced-motion; modify it to
accept or read a reducedMotion flag (e.g., from Framer Motion's useReducedMotion
or window.matchMedia) and if reducedMotion is true immediately set displayText
to text and setIsComplete(true) without creating any setTimeout/setInterval;
otherwise run the existing delayed interval logic as-is, and ensure all timers
are still cleared on cleanup. Also apply the same reducedMotion gating to the
typing-indicator loop and any Framer Motion transition props (replace
repeats/infinite and transition durations with 0 or no-repeat when reducedMotion
is true) so animations do not run for motion-sensitive users.

In `@web/tests/app/landing.test.tsx`:
- Around line 38-44: The test is too loose: replace the generic text check that
uses screen.getAllByText(/volvox-bot/) and the length assertion with a
role-based heading assertion to ensure the hero heading is targeted; after
render(<LandingPage />) use screen.getByRole('heading', { name: /volvox-bot/i })
(or getAllByRole and assert one matches) and assert it exists (e.g.,
toBeInTheDocument or truthy) so the test specifically verifies the hero heading
rendered by LandingPage.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7e07c0 and e83b855.

📒 Files selected for processing (11)
  • .github/workflows/railway-preview.yml
  • web/src/app/globals.css
  • web/src/app/page.tsx
  • web/src/components/landing/FeatureGrid.tsx
  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/Hero.tsx
  • web/src/components/landing/InviteButton.tsx
  • web/src/components/landing/Pricing.tsx
  • web/src/components/landing/Stats.tsx
  • web/src/components/landing/index.ts
  • web/tests/app/landing.test.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
{src,web}/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Winston logger from src/logger.js, NEVER use console.*

Files:

  • web/src/components/landing/Stats.tsx
  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/index.ts
  • web/src/components/landing/Hero.tsx
  • web/src/components/landing/Pricing.tsx
  • web/src/components/landing/FeatureGrid.tsx
  • web/tests/app/landing.test.tsx
  • web/src/components/landing/InviteButton.tsx
  • web/src/app/page.tsx
web/**/*.{ts,tsx,jsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

Use Next.js 16 App Router for web dashboard, with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design

Files:

  • web/src/components/landing/Stats.tsx
  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/index.ts
  • web/src/components/landing/Hero.tsx
  • web/src/components/landing/Pricing.tsx
  • web/src/components/landing/FeatureGrid.tsx
  • web/tests/app/landing.test.tsx
  • web/src/components/landing/InviteButton.tsx
  • web/src/app/page.tsx
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T21:23:59.512Z
Learning: Applies to web/**/*.{ts,tsx,jsx,js} : Use Next.js 16 App Router for web dashboard, with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design
📚 Learning: 2026-03-02T21:23:59.512Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T21:23:59.512Z
Learning: Applies to src/**/*.{js,ts,jsx,tsx} : Use 2-space indent, enforced by Biome

Applied to files:

  • web/src/components/landing/Stats.tsx
  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/Pricing.tsx
📚 Learning: 2026-03-02T21:23:59.512Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T21:23:59.512Z
Learning: Applies to web/**/*.{ts,tsx,jsx,js} : Use Next.js 16 App Router for web dashboard, with Discord OAuth2 authentication, dark/light theme support, and mobile-responsive design

Applied to files:

  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/Hero.tsx
  • web/tests/app/landing.test.tsx
  • web/src/app/page.tsx
📚 Learning: 2026-03-02T21:23:59.512Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T21:23:59.512Z
Learning: Applies to tests/**/*.{js,ts} : Maintain 80% test coverage threshold — Never lower the coverage requirement

Applied to files:

  • web/tests/app/landing.test.tsx
📚 Learning: 2026-03-02T21:23:59.512Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T21:23:59.512Z
Learning: Applies to src/**/*.{js,ts,jsx,tsx} : Use ESM only — Use `import/export`, no CommonJS

Applied to files:

  • web/tests/app/landing.test.tsx
🪛 GitHub Actions: CI
web/src/components/landing/Footer.tsx

[warning] 10-10: Formatter would have printed different content in Footer.tsx.

web/src/components/landing/index.ts

[warning] 1-1: Imports/exports are not sorted in index.ts. Formatter would reorder exports.

web/src/components/landing/Hero.tsx

[warning] 49-51: Formatter would have printed a different content in BlinkingCursor component.

web/src/app/page.tsx

[error] 3-3: The imports and exports are not sorted. Safe fix suggested by Biome: organize imports.


[warning] 4-4: Formatter would have printed a different import block from '@/components/landing'.

🪛 GitHub Check: CodeQL
.github/workflows/railway-preview.yml

[warning] 27-27: Unpinned tag for a non-immutable Action in workflow
Unpinned 3rd party Action 'Railway Preview Deploy' step Uses Step uses 'pnpm/action-setup' with ref 'v4.2.0', not a pinned commit hash

🪛 Stylelint (17.3.0)
web/src/app/globals.css

[error] 4-4: Unexpected unknown at-rule "@theme" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)


[error] 160-160: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🔇 Additional comments (11)
.github/workflows/railway-preview.yml (2)

116-119: LGTM on cleanup logic, but unreachable due to trigger issue.

The cleanup job logic is correct—it properly deletes the environment and notifies via comment. However, as noted above, this job will never execute until the closed event is added to the workflow triggers.


8-14: Good security practices: concurrency control and minimal permissions.

The concurrency group with cancel-in-progress: true prevents resource waste on rapid pushes, and the permissions are appropriately scoped to only what's needed.

web/src/components/landing/InviteButton.tsx (1)

1-24: LGTM!

Clean implementation with proper null-URL handling. The asChild pattern correctly delegates button styling to the anchor element, and the external link has appropriate security attributes.

web/src/components/landing/Pricing.tsx (1)

154-176: LGTM — CTA handling is well-implemented.

The conditional rendering properly handles all three cases: tier-specific href, bot invite URL, and disabled fallback. The asChild prop is correctly set only when a valid link target exists.

web/src/components/landing/Stats.tsx (3)

7-36: LGTM — AnimatedCounter has proper RAF cleanup.

The rafRef pattern correctly cancels pending animation frames on cleanup, preventing memory leaks and stale state updates.


38-57: Verify testimonial authorization before shipping.

These testimonials attribute quotes to named individuals (Sarah Chen, Marcus Johnson, Alex Rivera). While the company names have been changed to generic ones (TechFlow, Streamline, OpenSaaS), using real-sounding personal names without documented consent still poses legal risk.

Consider either:

  • Using fully anonymous testimonials (e.g., "DevOps Engineer", "Community Manager")
  • Obtaining documented written consent from real users
  • Adding a disclaimer that names are pseudonyms

109-128: LGTM — Uses stable keys for testimonials.

The key={t.id} pattern correctly uses stable unique identifiers instead of array indices.

web/src/components/landing/FeatureGrid.tsx (1)

1-113: LGTM!

Well-structured component with proper reduced motion support. The useReducedMotion() hook correctly gates all animations, and the terminal card styling creates a cohesive "Terminal Chic" aesthetic.

web/src/components/landing/Footer.tsx (1)

29-47: LGTM — CTA properly handles missing invite URL.

The conditional rendering correctly shows a disabled "Coming Soon" button when getBotInviteUrl() returns null, and an active link otherwise.

web/tests/app/landing.test.tsx (2)

5-24: LGTM — Framer Motion mock is well-structured.

The ESM-compatible mock using async import and forwardRef correctly handles ref forwarding and provides stable return values for useInView and useReducedMotion.


59-69: LGTM — Environment-dependent UI tests are well-structured.

Tests properly manipulate and restore NEXT_PUBLIC_DISCORD_CLIENT_ID to verify the Add to Server button visibility logic.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…view builds

- Add 'closed' to pull_request trigger types so cleanup-preview job runs
- Export NEXT_PUBLIC_DISCORD_CLIENT_ID for invite buttons in preview builds
- Sort imports alphabetically in page.tsx
- Sort exports alphabetically in index.ts
- Fix formatting in Footer.tsx and Hero.tsx
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 4, 2026
Resolves GHAS security alert about unpinned third-party action.
Copilot AI review requested due to automatic review settings March 4, 2026 01:49
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 4, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (2)

web/src/app/globals.css:168

  • blink keyframes are defined twice (once earlier inside @theme, and again here). Keeping a single definition avoids confusion about which one is authoritative and makes future edits safer.
/* Terminal cursor blink animation */
.terminal-cursor {
  animation: blink 1s step-end infinite;
}

web/tests/app/landing.test.tsx:11

  • The Framer Motion mock forwards all props (e.g. initial, animate, transition) directly onto DOM nodes. React will warn on unknown DOM attributes, which can clutter test output and hide real failures. Consider stripping common motion props in createComponent before spreading props onto the element.
vi.mock('framer-motion', async () => {
  const React = await import('react');
  const createComponent = (tag: string) =>
    React.forwardRef((props: any, ref: any) =>
      React.createElement(tag, { ...props, ref }, props.children)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Add openclaw-studio to .gitignore
- Remove dockerfilePath from railway.toml
- Update import path in next-env.d.ts for routes
- Adjust container styling in page.tsx and Hero.tsx for better responsiveness
@BillChirico BillChirico merged commit 098d120 into main Mar 4, 2026
12 of 24 checks passed
@BillChirico BillChirico deleted the feat/landing-redesign branch March 4, 2026 02:25
@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

🧹 Preview Environment Cleaned Up

The Railway preview environment for this PR has been removed.

Environment: pr-239

deploy-preview:
name: Deploy to Railway Preview
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deploy job runs unnecessarily when PR closes

when action is closed, both deploy-preview and cleanup-preview jobs run simultaneously, wasting CI resources. add condition to skip:

Suggested change
if: github.event.pull_request.head.repo.full_name == github.repository
if: github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/railway-preview.yml
Line: 20

Comment:
deploy job runs unnecessarily when PR closes

when action is `closed`, both deploy-preview and cleanup-preview jobs run simultaneously, wasting CI resources. add condition to skip:

```suggestion
    if: github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository
```

How can I resolve this? If you propose a fix, please make it concise.


/* JetBrains Mono for headlines, Inter for body (loaded in layout.tsx) */
@theme {
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

font-mono utility won't use JetBrains Mono

the layout loads JetBrains Mono as --font-mono variable, but @theme doesn't map it to the font-mono utility. currently using Tailwind's default monospace stack instead.

add after line 5:

Suggested change
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: var(--font-mono), ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/app/globals.css
Line: 5

Comment:
`font-mono` utility won't use JetBrains Mono

the layout loads JetBrains Mono as `--font-mono` variable, but @theme doesn't map it to the `font-mono` utility. currently using Tailwind's default monospace stack instead.

add after line 5:

```suggestion
  --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
  --font-mono: var(--font-mono), ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
```

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(landing): complete redesign with animations and terminal-chic aesthetic

2 participants