feat(web): landing page redesign with Terminal Chic aesthetic#239
feat(web): landing page redesign with Terminal Chic aesthetic#239BillChirico merged 25 commits intomainfrom
Conversation
…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
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughReworks 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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
| 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
There was a problem hiding this comment.
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 blinkis defined twice (once inside the@themeblock 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.
There was a problem hiding this comment.
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 | 🔵 TrivialDeduplicate
InviteButtoninto 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
web/package.jsonweb/src/app/globals.cssweb/src/app/layout.tsxweb/src/app/page.tsxweb/src/components/landing/FeatureGrid.tsxweb/src/components/landing/Footer.tsxweb/src/components/landing/Hero.tsxweb/src/components/landing/Pricing.tsxweb/src/components/landing/Stats.tsxweb/src/components/landing/index.tsweb/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 useconsole.*
Files:
web/src/components/landing/index.tsweb/src/components/landing/Footer.tsxweb/src/components/landing/FeatureGrid.tsxweb/tests/app/landing.test.tsxweb/src/app/layout.tsxweb/src/app/page.tsxweb/src/components/landing/Stats.tsxweb/src/components/landing/Hero.tsxweb/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.tsweb/src/components/landing/Footer.tsxweb/src/components/landing/FeatureGrid.tsxweb/tests/app/landing.test.tsxweb/src/app/layout.tsxweb/src/app/page.tsxweb/src/components/landing/Stats.tsxweb/src/components/landing/Hero.tsxweb/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.jsonweb/tests/app/landing.test.tsxweb/src/app/layout.tsxweb/src/app/page.tsxweb/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
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (2)
web/src/components/landing/Pricing.tsx (1)
83-95:⚠️ Potential issue | 🟡 MinorAdd
type="button"to the billing toggle.The pipeline failure indicates the button is missing an explicit
typeattribute. Without it, buttons default totype="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 | 🟡 MinorAvoid the
#fallback for the CTA link.Same concern as in
Pricing.tsx:getBotInviteUrl() || '#'renders an inert anchor whenCLIENT_IDis 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
📒 Files selected for processing (6)
web/src/app/globals.cssweb/src/components/landing/Footer.tsxweb/src/components/landing/Hero.tsxweb/src/components/landing/Pricing.tsxweb/src/components/landing/Stats.tsxweb/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 useconsole.*
Files:
web/src/components/landing/Hero.tsxweb/src/components/landing/Stats.tsxweb/src/components/landing/Pricing.tsxweb/src/components/landing/Footer.tsxweb/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.tsxweb/src/components/landing/Stats.tsxweb/src/components/landing/Pricing.tsxweb/src/components/landing/Footer.tsxweb/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 blinkdefinitions.The
blinkanimation is defined twice: once inside the@themeblock (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
@themeblock 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-monofollows 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 theuseTypewriterhook 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
InviteButtoncomponent correctly returnsnullwhengetBotInviteUrl()is falsy, avoiding broken CTAs. This pattern should be applied inPricing.tsxandFooter.tsxas well.
161-229:prefers-reduced-motionsupport is still missing.The Hero component runs animations unconditionally without respecting the
prefers-reduced-motionpreference. This affects the typewriter effect, blinking cursor, Framer Motion transitions, and the ChatPreview typing indicator. Consider adding ausePrefersReducedMotionhook 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 whenisCompletebecomes 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
useInViewto returntrue, 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 Serverbutton visibility is well-structured.web/src/components/landing/Stats.tsx (2)
7-36: LGTM on theAnimatedCounterRAF cleanup.The
rafRefproperly 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:
- Remove named attributions and use anonymous testimonials
- Obtain documented written consent from referenced individuals and organizations
- 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)
- Add explicit type for testimonials array with id field - Update pnpm-lock.yaml for framer-motion dependency
There was a problem hiding this comment.
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-typewriterreferences atypewriterkeyframe, but no@keyframes typewriteris 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
InviteButtonis re-implemented here and again insidecomponents/landing/Hero.tsxwith 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 sharedInviteButtoncomponent (e.g., undersrc/components/landing/InviteButton.tsxorsrc/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.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
web/src/components/landing/Stats.tsx (1)
38-57:⚠️ Potential issue | 🟠 MajorUse 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 | 🟠 MajorDon’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 | 🟠 MajorAvoid
'#'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
📒 Files selected for processing (4)
web/src/components/landing/FeatureGrid.tsxweb/src/components/landing/Footer.tsxweb/src/components/landing/Pricing.tsxweb/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 useconsole.*
Files:
web/src/components/landing/Footer.tsxweb/src/components/landing/FeatureGrid.tsxweb/src/components/landing/Pricing.tsxweb/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.tsxweb/src/components/landing/FeatureGrid.tsxweb/src/components/landing/Pricing.tsxweb/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.tsxweb/src/components/landing/Pricing.tsxweb/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).
Replace require('react') with async import inside vi.mock factory.
The vi.hoisted approach doesn't work with ESM module initialization order.
|
✅ 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. |
- 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
There was a problem hiding this comment.
Actionable comments posted: 11
♻️ Duplicate comments (6)
web/tests/app/landing.test.tsx (1)
38-44: 🧹 Nitpick | 🔵 TrivialTighten 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 | 🟠 MajorNavbar 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 | 🔵 TrivialKeep only one
@keyframes blinkdefinition.
blinkis 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 | 🟠 MajorRespect 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 | 🟡 MinorCursor 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 | 🟠 MajorHero 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
📒 Files selected for processing (11)
.github/workflows/railway-preview.ymlweb/src/app/globals.cssweb/src/app/page.tsxweb/src/components/landing/FeatureGrid.tsxweb/src/components/landing/Footer.tsxweb/src/components/landing/Hero.tsxweb/src/components/landing/InviteButton.tsxweb/src/components/landing/Pricing.tsxweb/src/components/landing/Stats.tsxweb/src/components/landing/index.tsweb/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 useconsole.*
Files:
web/src/components/landing/Stats.tsxweb/src/components/landing/Footer.tsxweb/src/components/landing/index.tsweb/src/components/landing/Hero.tsxweb/src/components/landing/Pricing.tsxweb/src/components/landing/FeatureGrid.tsxweb/tests/app/landing.test.tsxweb/src/components/landing/InviteButton.tsxweb/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.tsxweb/src/components/landing/Footer.tsxweb/src/components/landing/index.tsweb/src/components/landing/Hero.tsxweb/src/components/landing/Pricing.tsxweb/src/components/landing/FeatureGrid.tsxweb/tests/app/landing.test.tsxweb/src/components/landing/InviteButton.tsxweb/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.tsxweb/src/components/landing/Footer.tsxweb/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.tsxweb/src/components/landing/Hero.tsxweb/tests/app/landing.test.tsxweb/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
closedevent is added to the workflow triggers.
8-14: Good security practices: concurrency control and minimal permissions.The concurrency group with
cancel-in-progress: trueprevents 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
asChildpattern 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
asChildprop 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
rafRefpattern 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
forwardRefcorrectly handles ref forwarding and provides stable return values foruseInViewanduseReducedMotion.
59-69: LGTM — Environment-dependent UI tests are well-structured.Tests properly manipulate and restore
NEXT_PUBLIC_DISCORD_CLIENT_IDto verify the Add to Server button visibility logic.
There was a problem hiding this comment.
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
Resolves GHAS security alert about unpinned third-party action.
There was a problem hiding this comment.
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
blinkkeyframes 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 increateComponentbefore 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
🧹 Preview Environment Cleaned UpThe Railway preview environment for this PR has been removed. Environment: |
| deploy-preview: | ||
| name: Deploy to Railway Preview | ||
| runs-on: ubuntu-latest | ||
| if: github.event.pull_request.head.repo.full_name == github.repository |
There was a problem hiding this 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:
| 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"; |
There was a problem hiding this 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:
| --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.
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
Feature Grid
Pricing Section
~/dev/null(free),./configure($12/mo),make install($49/mo)git clone,npm install,curl | bashStats / Testimonials
Footer CTA
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 sectionsweb/src/components/landing/Hero.tsx- Existing, with typewriterweb/src/components/landing/FeatureGrid.tsx- Newweb/src/components/landing/Pricing.tsx- Newweb/src/components/landing/Stats.tsx- Newweb/src/components/landing/Footer.tsx- Newweb/src/components/landing/index.ts- ExportsTesting