Skip to content

feat: add Reviews testimonial carousel and How We Work scroll-pinned section#233

Open
rabden wants to merge 2 commits into
mainfrom
feat/reviews-how-we-work
Open

feat: add Reviews testimonial carousel and How We Work scroll-pinned section#233
rabden wants to merge 2 commits into
mainfrom
feat/reviews-how-we-work

Conversation

@rabden

@rabden rabden commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • New Reviews section (src/components/reviews.tsx): staggered testimonial card deck. content/review.json.
  • New How We Work section (src/components/how-we-work.tsx): 100vh pinned GSAP ScrollTrigger timeline above the About section — title velocity pull-in → headline → word-by-word subtext → 5-step card carousel (local UI cutout images: bubble, laptop, floating UI, calendar, handshake) → info bento cards with custom animated SVG graphics → editorial values stack → closing CTA. Phases are split so each step reads cleanly.
  • Button: swapped to the MD3-expressive variant and refactored all usages to semantic variants/sizes.
  • Team fix: Hossain Jahed (rabden) type corrected marketerbuilder and role set to Developer.
  • Added @gsap/react dependency.

Test plan

  • pnpm format && pnpm typecheck && pnpm lint && pnpm test && pnpm build all pass (56 tests green).
  • Manual: scroll the homepage through How We Work phases and the Reviews deck.

🤖 Generated with opencode

Screencast.From.2026-07-14.23-39-12.mp4
Screencast.From.2026-07-14.21-39-44.mp4

…section

- New Reviews section: staggered testimonial card deck with the original
  OG shuffle logic (tempId key + wrap teleport), profile-pic support with
  initials fallback, removed gradient fades / bottom separator, bordered
  container to match testimonial card styling
- New How We Work section: 100vh pinned GSAP ScrollTrigger timeline
  (title float-in -> headline -> word-by-word subtext -> 5-step card
  carousel -> info bento cards with custom SVG animations -> values stack
  -> closing CTA), using local UI cutout images
- Swap Button to MD3-expressive variant, refactor usages to semantic
  variants/sizes across the site
- Fix Hossain Jahed team type (marketer -> builder) and role
- Add @gsap/react dependency
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
volvox-website Ready Ready Preview, Comment Jul 14, 2026 5:56pm

Request Review

@claude

claude Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @rabden's task in 2m 36s —— View job


PR Review Complete

  • Read and analyze main components (reviews.tsx, how-we-work.tsx)
  • Review button refactor (button.tsx)
  • Check content/schema changes
  • Review homepage integration
  • Analyze for bugs, performance, security, and accessibility issues
  • Fix any issues directly and leave inline comments for author attention
  • Provide comprehensive review summary

Summary

This PR adds two substantial new homepage sections (Reviews carousel and How We Work scroll-pinned section) along with a significant Button component refactor. Overall, the implementation is well-structured with good attention to animation physics and visual design.

What I Fixed

Semantic HTML (src/components/how-we-work.tsx): Changed <h1> to <h2> for the "How we work" title. The homepage should have a single <h1> (in the Hero section), and section headings should use <h2> for proper document outline and accessibility.

Issues Requiring Attention

I've left inline comments on specific lines. Summary:

File Issue Severity
how-we-work.tsx:408 Mobile responsiveness - w-1/2 without mobile breakpoint Medium
how-we-work.tsx:56 Long scroll depth (9500px) may cause UX friction Low
content/review.json:28 Placeholder profile URLs (pravatar.cc) shouldn't ship to production Medium

Positive Observations

  • Reviews Component: Clean implementation with proper accessibility (aria-live, sr-only labels, role="img" on star ratings)
  • Button Refactor: Well-structured MD3-expressive variants with proper TypeScript types and CVA patterns
  • Schema Validation: Proper Zod schemas for the new review content types
  • Animation Physics: Thoughtful spring constants and easing curves for the ripple effect

Minor Notes (No action needed)

  • The button's playTactilePopSound() function gracefully handles browsers that block Web Audio API before user interaction
  • The GSAP ScrollTrigger setup properly uses useGSAP hook with scope reference for cleanup
  • Array index keys are used in a few places but are acceptable since the lists are static

Commit pushed: 1f0f0bb - Fixed heading hierarchy in HowWeWork component

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a validated, content-driven homepage Reviews section and a GSAP-powered How We Work section. It also substantially updates the shared Button system and simplifies button styling across site interfaces.

Changes

Homepage reviews

Layer / File(s) Summary
Review content contract and source data
src/lib/schemas.ts, src/lib/types.ts, content/review.json, content/team.json
Adds validated review content, inferred types, review data, and two team metadata updates.
Review loading and homepage wiring
src/lib/content.ts, src/app/page.tsx, src/components/homepage-client.tsx, CHANGELOG.md
Loads reviews with error handling and passes them into a new homepage section with scroll tracking.
Animated review deck
src/components/reviews.tsx
Adds responsive animated review cards, trust badges, navigation, ratings, reduced-motion handling, and live accessibility text.

How We Work section

Layer / File(s) Summary
Pinned How We Work experience
src/components/how-we-work.tsx, package.json
Adds a GSAP/ScrollTrigger-pinned section containing process steps, information cards, values, and a closing CTA.

Button system and site-wide styling

Layer / File(s) Summary
Button behavior and variants
src/components/ui/button.tsx
Reworks button variants, ripple behavior, tactile feedback, wrapper layout, and adds SplitButton.
Button consumers and navigation controls
src/components/**, src/app/team/team-member-detail-client.tsx
Simplifies button styling and updates sizes, visibility, and variants across homepage, blog, products, team, navigation, cookie, and AI interfaces.

Suggested labels: feature, frontend, enhancement, dependencies, accessibility

Suggested reviewers: billchirico

🚥 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
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main additions: the Reviews carousel and the How We Work scroll-pinned section.
Description check ✅ Passed The description is directly related to the pull request and accurately summarizes the new sections, button updates, and dependency change.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/reviews-how-we-work

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.

@qltysh

qltysh Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

16 new issues

Tool Category Rule Count
qlty Duplication Found 20 lines of similar code in 2 locations (mass = 69) 11
qlty Structure Function with high complexity (count = 28): ReviewCard 2
qlty Structure Function with many returns (count = 8): Reviews 2
qlty Structure High total complexity (count = 79) 1

{/* Steps Wrapper (Phase 4, hidden initially) */}
<div className="hww-steps-wrapper opacity-0 absolute inset-0 flex items-center justify-center w-full max-w-7xl mx-auto px-4 md:px-8 z-30 pointer-events-none">
{/* Left Area: Cards */}
<div className="w-1/2 h-[60vh] relative flex items-center justify-center">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mobile Responsiveness Issue: The w-1/2 class doesn't have a mobile breakpoint, so on screens smaller than ~768px, the cards and text will be cramped into 50% width each, making the text difficult to read.

Consider stacking vertically on mobile:

Suggested change
<div className="w-1/2 h-[60vh] relative flex items-center justify-center">
<div className="w-full md:w-1/2 h-[60vh] relative flex items-center justify-center">

You'd also need to update line 424 similarly, and potentially hide this entire section on mobile or provide an alternative mobile-friendly layout (vertical scrolling steps instead of pinned animation).

scrollTrigger: {
trigger: pinRef.current,
start: "top top",
end: "+=9500", // Expanded scroll depth to accommodate split phases perfectly

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

UX Concern: The scroll depth of 9500px is quite long for a pinned section. Users on trackpads or without smooth scrolling may find this tedious to scroll through.

Consider either:

  1. Reducing the scroll depth (e.g., +=6000 or less) by combining some phases
  2. Adding a "skip" button that scrolls past the section
  3. Detecting reduced motion preference and showing a static version instead

Comment thread content/review.json
"source": "product-hunt",
"product": "Volvox Bot",
"featured": true,
"profilePicUrl": "https://i.pravatar.cc/150?img=12"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Placeholder URLs: These profile picture URLs (i.pravatar.cc) are placeholder images. Before shipping to production, these should either be:

  1. Replaced with actual profile images (if available and permission granted)
  2. Removed entirely to use the fallback initials display

Using placeholder services in production may have reliability/GDPR concerns.

The homepage should have a single h1 (in the Hero section). Section headings
should use h2 to maintain proper semantic HTML structure.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds new homepage storytelling sections and updates shared UI pieces. The main changes are:

  • Content-driven Reviews carousel with profile images and trust badges.
  • GSAP-pinned How We Work section before About.
  • MD3-style Button primitive and updated button usages.
  • Review schema/content loading and team content cleanup.

Confidence Score: 4/5

The Reviews initial render path needs a fix before merging.

  • Valid review content renders as null until hydration fills carousel state.
  • The new pinned section can leave the active nav state stale while users scroll through it.
  • No security issue was identified in the changed code.

src/components/reviews.tsx, src/components/homepage-client.tsx

Important Files Changed

Filename Overview
src/components/reviews.tsx Adds the Reviews carousel; derived carousel state is populated after the first render, so initial HTML omits the section.
src/components/how-we-work.tsx Adds the pinned GSAP How We Work timeline with local image assets and a closing CTA.
src/components/homepage-client.tsx Wires Reviews and How We Work into the homepage; scroll tracking now includes Reviews but not the new pinned section.
src/components/ui/button.tsx Refactors the shared Button primitive with new variants, sizes, ripple behavior, and split-button support.
src/lib/content.ts Adds review content loading through the existing validated JSON content pipeline.
src/lib/schemas.ts Adds Zod schemas and exported types for homepage reviews and trust badges.

Fix All in Codex

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/components/reviews.tsx:318-320
**Reviews Missing Until Hydration**

`orderedReviews` starts empty and is filled only after `useEffect` runs, so the first render hits the `orderedReviews.length === 0` guard and returns `null` even when valid review content was passed in. The Reviews section is missing from the initial HTML and then pops in after hydration, which can cause layout shift and leaves the testimonial content unavailable on the first page render.

```suggestion
  const [orderedReviews, setOrderedReviews] = useState<
    (HomepageReview & { tempId: string })[]
  >(() => buildInitialReviews(content?.reviews ?? []));
```

### Issue 2 of 2
src/components/homepage-client.tsx:109-112
**Pinned Section Leaves Stale Nav**

The new `#how-we-work` section sits between Mentorship and About and pins the viewport for a long scroll range, but the scroll spy still only checks products, reviews, blog, mentorship, and about. While users scroll through How We Work, no tracked section matches, so the nav keeps the previous section active for the whole pinned animation.

Reviews (1): Last reviewed commit: "fix: change h1 to h2 in HowWeWork for pr..." | Re-trigger Greptile

Comment on lines +318 to +320
const [orderedReviews, setOrderedReviews] = useState<
(HomepageReview & { tempId: string })[]
>([]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Reviews Missing Until Hydration

orderedReviews starts empty and is filled only after useEffect runs, so the first render hits the orderedReviews.length === 0 guard and returns null even when valid review content was passed in. The Reviews section is missing from the initial HTML and then pops in after hydration, which can cause layout shift and leaves the testimonial content unavailable on the first page render.

Suggested change
const [orderedReviews, setOrderedReviews] = useState<
(HomepageReview & { tempId: string })[]
>([]);
const [orderedReviews, setOrderedReviews] = useState<
(HomepageReview & { tempId: string })[]
>(() => buildInitialReviews(content?.reviews ?? []));
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/reviews.tsx
Line: 318-320

Comment:
**Reviews Missing Until Hydration**

`orderedReviews` starts empty and is filled only after `useEffect` runs, so the first render hits the `orderedReviews.length === 0` guard and returns `null` even when valid review content was passed in. The Reviews section is missing from the initial HTML and then pops in after hydration, which can cause layout shift and leaves the testimonial content unavailable on the first page render.

```suggestion
  const [orderedReviews, setOrderedReviews] = useState<
    (HomepageReview & { tempId: string })[]
  >(() => buildInitialReviews(content?.reviews ?? []));
```

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex

Comment on lines 109 to +112

useEffect(() => {
const handleScroll = () => {
const sections = ["products", "blog", "mentorship", "about"];
const sections = ["products", "reviews", "blog", "mentorship", "about"];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Pinned Section Leaves Stale Nav

The new #how-we-work section sits between Mentorship and About and pins the viewport for a long scroll range, but the scroll spy still only checks products, reviews, blog, mentorship, and about. While users scroll through How We Work, no tracked section matches, so the nav keeps the previous section active for the whole pinned animation.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/homepage-client.tsx
Line: 109-112

Comment:
**Pinned Section Leaves Stale Nav**

The new `#how-we-work` section sits between Mentorship and About and pins the viewport for a long scroll range, but the scroll spy still only checks products, reviews, blog, mentorship, and about. While users scroll through How We Work, no tracked section matches, so the nav keeps the previous section active for the whole pinned animation.

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

Fix in Codex

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (3)
src/components/blog/blog-post-header.tsx (1)

57-66: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the required theme-toggle test ID.

Proposed fix
               <Button
                 variant="ghost"
                 size="icon"
+                data-testid="theme-toggle"
                 onClick={toggleTheme}

As per coding guidelines, “Add data-testid attributes to key elements: … theme-toggle …”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/blog/blog-post-header.tsx` around lines 57 - 66, Add the
required data-testid="theme-toggle" attribute to the theme-toggle Button in the
blog post header, identified by its toggleTheme onClick handler, while
preserving the existing variant, accessibility label, and behavior.

Source: Coding guidelines

src/components/navigation.tsx (1)

251-263: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer anchor tags for external navigation instead of window.open.

Using onClick with window.open for external links hinders accessibility, degrades SEO, and prevents users from natively middle-clicking to open in a new tab. Use the asChild prop on Button to render a standard <a> element instead.

  • src/components/navigation.tsx#L251-L263: Replace the onClick handler with an <a href={DISCORD_URL} target="_blank" rel="noopener noreferrer" onClick={handleDiscordClick}> inside the Discord button using asChild.
  • src/components/products.tsx#L229-L245: Replace the onClick handler with an <a href={product.links.demo} target="_blank" rel="noopener noreferrer"> inside the Launch button using asChild.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/navigation.tsx` around lines 251 - 263, Replace the
window.open-based Discord navigation in the Button at
src/components/navigation.tsx:251-263 with an asChild anchor using
href={DISCORD_URL}, target="_blank", rel="noopener noreferrer", and
onClick={handleDiscordClick}; update the Launch Button at
src/components/products.tsx:229-245 similarly with an anchor using
product.links.demo, target="_blank", and rel="noopener noreferrer".
src/components/blog/blog-post-navbar.tsx (1)

24-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer <Link> components for internal navigation instead of router.push.

Several navigation buttons use onClick={() => router.push(...)} for internal routing. Using Next.js <Link> components (via the asChild prop) improves performance through route prefetching, enhances SEO, and allows users to middle-click to open links in a new tab.

  • src/components/blog/blog-post-navbar.tsx#L24-L32: Wrap the icon with <Link href="/#blog"> and add asChild to the button.
  • src/components/blog/blog-post-navbar.tsx#L80-L88: Wrap the content with <Link href="/#blog"> and add asChild to the button.
  • src/components/products/product-post-navbar.tsx#L24-L32: Wrap the icon with <Link href="/products"> and add asChild to the button.
  • src/components/products/product-post-navbar.tsx#L81-L89: Wrap the content with <Link href="/products"> and add asChild to the button.
  • src/components/team/member-navbar.tsx#L24-L32: Wrap the icon with <Link href="/team"> and add asChild to the button.
  • src/components/team/member-navbar.tsx#L80-L88: Wrap the content with <Link href="/team"> and add asChild to the button.
  • src/components/team/team-navbar.tsx#L38-L46: Wrap the icon with <Link href="/"> and add asChild to the button.
  • src/components/products/products-navbar.tsx#L79-L87: Wrap the icon with <Link href="/"> and add asChild to the button.
  • src/components/products.tsx#L400-L414: Wrap the content with <Link href="/products"> and add asChild to the button.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/blog/blog-post-navbar.tsx` around lines 24 - 32, Replace the
internal router.push navigation with Next.js Link components using Button’s
asChild prop: in src/components/blog/blog-post-navbar.tsx lines 24-32 and 80-88
use /#blog; in src/components/products/product-post-navbar.tsx lines 24-32 and
81-89 use /products; in src/components/team/member-navbar.tsx lines 24-32 and
80-88 use /team; in src/components/team/team-navbar.tsx lines 38-46 and
src/components/products/products-navbar.tsx lines 79-87 use /; and in
src/components/products.tsx lines 400-414 use /products. Wrap each existing icon
or content child with the matching Link and remove the router-based click
handlers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@content/review.json`:
- Around line 20-69: Update the testimonial entries shown in
content/review.json, including Muhammad Yasir, Great game 202, KathyMcHugh,
TheBapes, and `@Chimemaram1`, to replace the i.pravatar.cc placeholder
profilePicUrl values with consented real reviewer avatars; if those are
unavailable, remove each profilePicUrl field entirely.

In `@src/components/homepage-client.tsx`:
- Line 112: Add "how-we-work" to the sections list used by the scroll spy in the
homepage client, and add the matching entry to NAV_ITEMS so the section is
tracked and its navigation item is highlighted while visible.

In `@src/components/how-we-work.tsx`:
- Line 370: Update the pinned layout around pinRef and the related step/card
content so screens below the md breakpoint are not clipped by the h-screen
overflow-hidden container. Disable pinning and allow natural vertical scrolling
on small screens, while preserving the existing pinned presentation for md and
larger viewports.
- Around line 382-406: Update the phase containers and the invisible mail link
identified in the diff, including the headline, subtext, steps wrapper, and
related ranges, to use visibility and/or inert state in addition to opacity and
pointer-events. Ensure inactive phases are removed from the accessibility tree
and cannot receive sequential focus, while the currently revealed phase becomes
accessible when activated.
- Around line 47-360: Update the useGSAP animation setup to detect
prefers-reduced-motion and provide a static rendering branch that skips the
pinned ScrollTrigger and scrubbed timeline, while keeping all phase
wrappers/content visible in their final non-animated state. Disable or prevent
the SVG animations in this branch, and ensure the normal GSAP timeline remains
unchanged for users without the preference.
- Around line 1-7: Reorder imports in the how-we-work component so the
React/framework import comes first, followed by GSAP third-party imports and
then the local Button import. Add an explicit public return type to the exported
component function, likely HowWeWork, without changing its existing behavior.
- Line 7: Update the GSAP plugin registration near the existing ScrollTrigger
registration to include useGSAP alongside ScrollTrigger, preserving the current
registration flow and ensuring the React integration is explicitly registered.

In `@src/components/reviews.tsx`:
- Around line 318-331: Stabilize the fallback used by the reviews value in the
component containing the orderedReviews synchronization: replace the per-render
[] created by content?.reviews ?? [] with a shared empty-array constant or
memoized fallback. Preserve the existing reviews data when content is present
and keep the useEffect dependency on the stabilized reviews value.

In `@src/components/ui/button.tsx`:
- Around line 634-638: Update the className composition in the button component
to replace the legacy Tailwind important utility `!h-auto` with the v4 trailing
syntax `h-auto!`, while preserving the existing `self-stretch` class and class
merging behavior.
- Around line 17-50: Update playTactilePopSound to close each newly created
AudioContext from osc.onended after the oscillator finishes, while retaining one
context per click only when audio is available. Replace the blanket silent catch
with handling that ignores only expected browser restrictions and forwards
unexpected exceptions to reportError().
- Around line 538-556: Update the component props assembly around componentProps
to destructure the four pointer callbacks from props, compose each with its
corresponding events handler, and spread only the remaining props before
assigning the composed handlers. Invoke the external callback and internal
handler while short-circuiting appropriately when event.defaultPrevented is set,
preserving the ripple hover/press state behavior.
- Around line 1-6: Add the "use client" directive at the top of the button
component before its imports so the component can use its React hooks and
browser-only APIs, including useState, useRef, and window.AudioContext.

In `@src/lib/schemas.ts`:
- Around line 236-241: Add a schema-level uniqueness validation to
reviewsContentSchema for reviews[].id, ensuring duplicate IDs produce a
validation error during parsing while preserving the existing review item
validation and other defaults.
- Around line 211-221: Update profilePicUrl in homepageReviewSchema to validate
URL values with Zod’s top-level z.url() schema while preserving its optional
behavior; leave the other review fields unchanged.

---

Outside diff comments:
In `@src/components/blog/blog-post-header.tsx`:
- Around line 57-66: Add the required data-testid="theme-toggle" attribute to
the theme-toggle Button in the blog post header, identified by its toggleTheme
onClick handler, while preserving the existing variant, accessibility label, and
behavior.

In `@src/components/blog/blog-post-navbar.tsx`:
- Around line 24-32: Replace the internal router.push navigation with Next.js
Link components using Button’s asChild prop: in
src/components/blog/blog-post-navbar.tsx lines 24-32 and 80-88 use /#blog; in
src/components/products/product-post-navbar.tsx lines 24-32 and 81-89 use
/products; in src/components/team/member-navbar.tsx lines 24-32 and 80-88 use
/team; in src/components/team/team-navbar.tsx lines 38-46 and
src/components/products/products-navbar.tsx lines 79-87 use /; and in
src/components/products.tsx lines 400-414 use /products. Wrap each existing icon
or content child with the matching Link and remove the router-based click
handlers.

In `@src/components/navigation.tsx`:
- Around line 251-263: Replace the window.open-based Discord navigation in the
Button at src/components/navigation.tsx:251-263 with an asChild anchor using
href={DISCORD_URL}, target="_blank", rel="noopener noreferrer", and
onClick={handleDiscordClick}; update the Launch Button at
src/components/products.tsx:229-245 similarly with an anchor using
product.links.demo, target="_blank", and rel="noopener noreferrer".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 84c72e09-87ba-4f56-8f2d-d092ea31ca4a

📥 Commits

Reviewing files that changed from the base of the PR and between 5c37e09 and c32fc03.

⛔ Files ignored due to path filters (6)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/images/UI/calendar_cutout.png is excluded by !**/*.png
  • public/images/UI/floating_ui_elements_cutout.png is excluded by !**/*.png
  • public/images/UI/handshake_cutout.png is excluded by !**/*.png
  • public/images/UI/laptop_cutout.png is excluded by !**/*.png
  • public/images/UI/two_speech_bubble_cutout.png is excluded by !**/*.png
📒 Files selected for processing (31)
  • CHANGELOG.md
  • content/review.json
  • content/team.json
  • package.json
  • src/app/page.tsx
  • src/app/team/team-member-detail-client.tsx
  • src/components/ai/chat-header.tsx
  • src/components/blog.tsx
  • src/components/blog/back-to-posts-button.tsx
  • src/components/blog/blog-navbar.tsx
  • src/components/blog/blog-post-header.tsx
  • src/components/blog/blog-post-navbar.tsx
  • src/components/cookie-consent-banner.tsx
  • src/components/hero/hero-section.tsx
  • src/components/homepage-client.tsx
  • src/components/how-we-work.tsx
  • src/components/mentorship.tsx
  • src/components/navigation.tsx
  • src/components/products-list-client.tsx
  • src/components/products.tsx
  • src/components/products/product-hero.tsx
  • src/components/products/product-post-navbar.tsx
  • src/components/products/products-navbar.tsx
  • src/components/reviews.tsx
  • src/components/team/member-navbar.tsx
  • src/components/team/team-navbar.tsx
  • src/components/theme-toggle.tsx
  • src/components/ui/button.tsx
  • src/lib/content.ts
  • src/lib/schemas.ts
  • src/lib/types.ts
💤 Files with no reviewable changes (5)
  • src/components/blog/back-to-posts-button.tsx
  • src/components/mentorship.tsx
  • src/components/products/product-hero.tsx
  • src/components/theme-toggle.tsx
  • src/components/ai/chat-header.tsx

Comment thread content/review.json
Comment on lines +20 to +69
"id": "muhammad-yasir",
"quote": "I've been part of the Volvox community for a while, and I also had the chance to be one of the beta testers for Volvox Bot. Watching it evolve from the early builds to what it is now has been genuinely exciting. What stood out to me during testing wasn't just the moderation features themselves, but how much the team actually listened to feedback. If you manage a Discord server, you know that good moderation tools need to be reliable first — and that's something Volvox Bot has consistently focused on.",
"name": "Muhammad Yasir",
"role": "Beta tester, Volvox Bot",
"rating": 5,
"source": "product-hunt",
"product": "Volvox Bot",
"featured": true,
"profilePicUrl": "https://i.pravatar.cc/150?img=12"
},
{
"id": "great-game-202",
"quote": "Decision Jar is an absolute game-changer AI decision maker app that instantly eliminates decision fatigue for couples. It's the 'why didn't this exist sooner' tool we've been missing.",
"name": "Great game 202",
"role": "Verified App Store Review",
"rating": 5,
"source": "app-store",
"product": "Decision Jar",
"profilePicUrl": "https://i.pravatar.cc/150?img=33"
},
{
"id": "kathy-mchugh",
"quote": "Finally an app that reduces my headaches instead of adds to them. This app helps me and my partner decide dinner every night — the AI suggestions brought up ideas we would've never thought of.",
"name": "KathyMcHugh",
"role": "Verified App Store Review",
"rating": 5,
"source": "app-store",
"product": "Decision Jar",
"profilePicUrl": "https://i.pravatar.cc/150?img=47"
},
{
"id": "thebapes",
"quote": "This app helped me and my girlfriend finally decide what we're going to do for dinner. And it even suggested ideas we didn't even think of.",
"name": "TheBapes",
"role": "Verified App Store Review",
"rating": 5,
"source": "app-store",
"product": "Decision Jar",
"profilePicUrl": "https://i.pravatar.cc/150?img=60"
},
{
"id": "chimemaram1",
"quote": "Volvox the best bot ever",
"name": "@Chimemaram1",
"role": "on X",
"rating": 5,
"source": "x",
"product": "Volvox Bot",
"profilePicUrl": "https://i.pravatar.cc/150?img=68"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Real reviewer names are paired with placeholder avatars.

profilePicUrl points to i.pravatar.cc, so these testimonials ship with random placeholder faces instead of the named reviewers’ actual photos. Replace them with consented real avatars or remove the image field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@content/review.json` around lines 20 - 69, Update the testimonial entries
shown in content/review.json, including Muhammad Yasir, Great game 202,
KathyMcHugh, TheBapes, and `@Chimemaram1`, to replace the i.pravatar.cc
placeholder profilePicUrl values with consented real reviewer avatars; if those
are unavailable, remove each profilePicUrl field entirely.

useEffect(() => {
const handleScroll = () => {
const sections = ["products", "blog", "mentorship", "about"];
const sections = ["products", "reviews", "blog", "mentorship", "about"];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -A 5 -B 5 'how-we-work' src/components/navigation.tsx

Repository: VolvoxCommunity/Volvox.Website

Length of output: 168


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- homepage-client outline ---'
ast-grep outline src/components/homepage-client.tsx --view expanded || true

echo '--- navigation outline ---'
ast-grep outline src/components/navigation.tsx --view expanded || true

echo '--- search for HowWeWork / how-we-work ---'
rg -n -C 3 'HowWeWork|how-we-work|sections = \[' src/components

Repository: VolvoxCommunity/Volvox.Website

Length of output: 3072


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- homepage-client excerpt ---'
sed -n '90,140p' src/components/homepage-client.tsx | cat -n

echo '--- navigation excerpt ---'
sed -n '1,220p' src/components/navigation.tsx | cat -n

Repository: VolvoxCommunity/Volvox.Website

Length of output: 11705


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- files mentioning how-we-work ---'
rg -n -i 'how-we-work|how we work|HowWeWork' src

Repository: VolvoxCommunity/Volvox.Website

Length of output: 1019


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- NAV_ITEMS definition ---'
rg -n -A 30 -B 5 'NAV_ITEMS' src/lib/constants.ts src/lib/constants/* 2>/dev/null || true

echo '--- any how-we-work entry in constants ---'
rg -n -i 'how-we-work' src/lib src/components

Repository: VolvoxCommunity/Volvox.Website

Length of output: 1209


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'currentSection' src/components src/app src

Repository: VolvoxCommunity/Volvox.Website

Length of output: 2608


Add how-we-work to the scroll spy src/components/homepage-client.tsx:112HowWeWork renders with id="how-we-work", but this list skips it, so scrolling through that section keeps the previous nav item highlighted. If it should be part of the section nav, add it here (and to NAV_ITEMS).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/homepage-client.tsx` at line 112, Add "how-we-work" to the
sections list used by the scroll spy in the homepage client, and add the
matching entry to NAV_ITEMS so the section is tracked and its navigation item is
highlighted while visible.

Comment on lines +1 to +7
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useRef } from "react";
import { Button } from "@/components/ui/button";

gsap.registerPlugin(ScrollTrigger);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add the public return type and restore framework-first imports.

Proposed fix
+import { type JSX, useRef } from "react";
 import { useGSAP } from "`@gsap/react`";
 import gsap from "gsap";
 import { ScrollTrigger } from "gsap/ScrollTrigger";
-import { useRef } from "react";
+
 import { Button } from "`@/components/ui/button`";

-export function HowWeWork() {
+export function HowWeWork(): JSX.Element {

As per coding guidelines, “Organize imports in order: framework → third-party → local” and “Use strict TypeScript mode with explicit types for public APIs.”

Also applies to: 43-43

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/how-we-work.tsx` around lines 1 - 7, Reorder imports in the
how-we-work component so the React/framework import comes first, followed by
GSAP third-party imports and then the local Button import. Add an explicit
public return type to the exported component function, likely HowWeWork, without
changing its existing behavior.

Source: Coding guidelines

import { useRef } from "react";
import { Button } from "@/components/ui/button";

gsap.registerPlugin(ScrollTrigger);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Register useGSAP alongside ScrollTrigger.

Only ScrollTrigger is registered. The official React integration recommends registering useGSAP to avoid bundler tree-shaking and version-conflict problems. (gsap.com)

Proposed fix
-gsap.registerPlugin(ScrollTrigger);
+gsap.registerPlugin(useGSAP, ScrollTrigger);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
gsap.registerPlugin(ScrollTrigger);
gsap.registerPlugin(useGSAP, ScrollTrigger);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/how-we-work.tsx` at line 7, Update the GSAP plugin
registration near the existing ScrollTrigger registration to include useGSAP
alongside ScrollTrigger, preserving the current registration flow and ensuring
the React integration is explicitly registered.

Comment on lines +47 to +360
useGSAP(
() => {
if (!pinRef.current) return;

// Pinned timeline with substantial depth for choreographing all phases
const tl = gsap.timeline({
scrollTrigger: {
trigger: pinRef.current,
start: "top top",
end: "+=9500", // Expanded scroll depth to accommodate split phases perfectly
scrub: 1,
pin: true,
},
});

// --- PHASE 1: "How we work" Title (Velocity 3D entrance & exit) ---
tl.fromTo(
".hww-title-word",
{
y: 120,
opacity: 0,
rotationX: -60,
transformOrigin: "50% 50% -50px",
},
{
y: 0,
opacity: 1,
rotationX: 0,
duration: 1,
stagger: 0.1,
ease: "power3.out",
},
);

tl.to({}, { duration: 0.4 }); // beat pause

// Title exit: clean fade + scale out to make space for headline (no top-left move)
tl.to(".hww-title-container", {
opacity: 0,
scale: 0.8,
y: -30,
duration: 0.8,
ease: "power2.in",
});

// --- PHASE 2: Headline Entrance & Exit ---
tl.fromTo(
".hww-headline",
{ opacity: 0, y: 50, scale: 0.95 },
{ opacity: 1, y: 0, scale: 1, duration: 1, ease: "power2.out" },
"-=0.4",
);

tl.to({}, { duration: 0.6 });

tl.to(".hww-headline", {
opacity: 0,
y: -50,
scale: 1.05,
duration: 0.8,
ease: "power2.in",
});

// --- PHASE 3: Subtext Word-by-Word Pull-up (Opacity Fix Applied!) ---
// Force parent container opacity to 1 so the word-by-word reveal actually renders!
tl.to(".hww-subtext", { opacity: 1, duration: 0.1 });
tl.fromTo(
".hww-subtext-word-inner",
{ opacity: 0, y: "100%" },
{
opacity: 1,
y: "0%",
duration: 0.6,
stagger: 0.04,
ease: "power2.out",
},
"<",
);

tl.to({}, { duration: 0.8 });

tl.to(".hww-subtext", {
opacity: 0,
y: -40,
duration: 0.8,
ease: "power2.in",
});

// --- PHASE 4: Steps Carousel ---
const cardNodes = gsap.utils.toArray(".hww-card");
const textNodes = gsap.utils.toArray(".hww-text");

// Set initial states for steps
gsap.set(cardNodes, {
opacity: 0,
scale: 0.3,
y: 200,
x: 200,
zIndex: 1,
});
gsap.set(textNodes, { opacity: 0, y: 50 });

// Bring steps container into focus
tl.to(
".hww-steps-wrapper",
{ opacity: 1, pointerEvents: "auto", duration: 0.5 },
"-=0.3",
);

// Card 0 entry & Text 0 entry
tl.to(
cardNodes[0] as HTMLElement,
{
opacity: 1,
scale: 1,
y: 0,
x: 0,
zIndex: 10,
duration: 1,
ease: "power2.out",
},
"<",
);
tl.to(
textNodes[0] as HTMLElement,
{ opacity: 1, y: 0, duration: 1, ease: "power2.out" },
"<",
);

// Card 1 Standby (bottom right)
tl.to(
cardNodes[1] as HTMLElement,
{
opacity: 1,
scale: 0.4,
y: 250,
x: 150,
zIndex: 5,
duration: 1,
ease: "power2.out",
},
"<0.2",
);

// Sequential steps transition loop
for (let i = 0; i < steps.length; i++) {
// Active image parallax zoom
const img = (cardNodes[i] as HTMLElement).querySelector("img");
tl.to(img, { scale: 1.15, duration: 1.5, ease: "none" });

if (i < steps.length - 1) {
const label = `stepTrans${i}`;

// Old card sweeps out to top-left and fades
tl.to(
cardNodes[i] as HTMLElement,
{
opacity: 0,
scale: 0.3,
x: -300,
y: -300,
duration: 1,
ease: "power2.inOut",
},
label,
);
// Old text fades out/up
tl.to(
textNodes[i] as HTMLElement,
{ opacity: 0, y: -50, duration: 0.8, ease: "power2.in" },
label,
);

// Standby card moves to center (scale up)
tl.to(
cardNodes[i + 1] as HTMLElement,
{
scale: 1,
x: 0,
y: 0,
zIndex: 10,
duration: 1,
ease: "power2.inOut",
},
label,
);
// New text fades in
tl.to(
textNodes[i + 1] as HTMLElement,
{ opacity: 1, y: 0, duration: 0.8, ease: "power2.out" },
`${label}+=0.3`,
);

// Next standby enters bottom-right
if (i + 2 < steps.length) {
tl.to(
cardNodes[i + 2] as HTMLElement,
{
opacity: 1,
scale: 0.4,
y: 250,
x: 150,
zIndex: 5,
duration: 1,
ease: "power2.out",
},
`${label}+=0.2`,
);
}
}
}

tl.to({}, { duration: 0.4 }); // wait a bit on the final step

// Fade out steps stage
tl.to(".hww-steps-wrapper", {
opacity: 0,
y: -40,
pointerEvents: "none",
duration: 0.8,
ease: "power2.in",
});

// --- PHASE 5: Info Cards (3D staggered fly-in + SVG animations) ---
const infoCards = gsap.utils.toArray(".hww-info-card");
gsap.set(infoCards, {
opacity: 0,
y: 80,
scale: 0.9,
rotationX: -15,
transformOrigin: "50% 50%",
});

tl.to(
".hww-info-wrapper",
{ opacity: 1, pointerEvents: "auto", duration: 0.5 },
"-=0.2",
);
tl.to(
infoCards,
{
opacity: 1,
y: 0,
scale: 1,
rotationX: 0,
stagger: 0.2,
duration: 1.2,
ease: "back.out(1.2)",
},
"<0.2",
);

tl.to({}, { duration: 1.5 }); // pause on info cards to let SVG animations play out

// Fade out info wrapper
tl.to(".hww-info-wrapper", {
opacity: 0,
scale: 1.05,
y: -30,
pointerEvents: "none",
duration: 0.8,
ease: "power2.in",
});

// --- PHASE 6: Values Stack (Editorial rows - Dedicated phase) ---
tl.to(
".hww-values-wrapper",
{ opacity: 1, pointerEvents: "auto", duration: 0.5 },
"-=0.2",
);

// Staggered value items entrance (Editorial Stack)
tl.fromTo(
".hww-value-item",
{ opacity: 0, y: 30 },
{
opacity: 1,
y: 0,
stagger: 0.18,
duration: 0.9,
ease: "power2.out",
},
"<0.1",
);

tl.to({}, { duration: 1.4 }); // wait and read values

// Fade out values wrapper
tl.to(".hww-values-wrapper", {
opacity: 0,
scale: 0.95,
y: -30,
pointerEvents: "none",
duration: 0.8,
ease: "power2.in",
});

// --- PHASE 7: Closing CTA (Unified Crescendo - Dedicated phase) ---
tl.to(
".hww-cta-wrapper",
{ opacity: 1, pointerEvents: "auto", duration: 0.5 },
"-=0.2",
);

// CTA content float and scale up
tl.fromTo(
".hww-cta-content",
{ opacity: 0, y: 50, scale: 0.95 },
{ opacity: 1, y: 0, scale: 1, duration: 1.2, ease: "power2.out" },
"<0.1",
);

tl.to({}, { duration: 1.5 }); // lingering pause on final CTA before unpinning
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Honor prefers-reduced-motion across GSAP and SVG animations.

The component always creates a 9,500px pinned scrub while its SVG animations repeat indefinitely. Provide a reduced-motion branch that skips pinning/scrubbing, renders the phases statically, and disables the SVG animations.

Also applies to: 522-1009

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/how-we-work.tsx` around lines 47 - 360, Update the useGSAP
animation setup to detect prefers-reduced-motion and provide a static rendering
branch that skips the pinned ScrollTrigger and scrubbed timeline, while keeping
all phase wrappers/content visible in their final non-animated state. Disable or
prevent the SVG animations in this branch, and ensure the normal GSAP timeline
remains unchanged for users without the preference.

Comment on lines +17 to +50
// --- 2. DYNAMIC AUDIO GENERATOR (For XL/2XL Buttons) ---
const playTactilePopSound = () => {
try {
// Safely initialize Web Audio API
const AudioContext: typeof window.AudioContext | undefined =
window.AudioContext ||
(window as { webkitAudioContext?: typeof window.AudioContext })
.webkitAudioContext;
if (!AudioContext) return;

const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gainNode = ctx.createGain();

osc.connect(gainNode);
gainNode.connect(ctx.destination);

// Create a pleasant, snappy "pop" sound using a sine wave
osc.type = "sine";
const now = ctx.currentTime;

// Pitch drops rapidly to simulate a physical click
osc.frequency.setValueAtTime(600, now);
osc.frequency.exponentialRampToValueAtTime(100, now + 0.05);

// Volume fades out cleanly over 50ms
gainNode.gain.setValueAtTime(0.15, now);
gainNode.gain.exponentialRampToValueAtTime(0.001, now + 0.05);

osc.start(now);
osc.stop(now + 0.05);
} catch {
// Silently fail if audio context is restricted by the browser (e.g. before user interaction)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File map ==\n'
git ls-files 'src/components/ui/button.tsx' 'src/lib/logger.ts'

printf '\n== Outline: src/components/ui/button.tsx ==\n'
ast-grep outline src/components/ui/button.tsx --view expanded || true

printf '\n== Relevant lines: src/components/ui/button.tsx ==\n'
nl -ba src/components/ui/button.tsx | sed -n '1,220p'

printf '\n== Relevant lines: src/lib/logger.ts ==\n'
nl -ba src/lib/logger.ts | sed -n '1,220p'

printf '\n== Search for reportError usage ==\n'
rg -n "reportError\\(" src || true

printf '\n== Search for AudioContext close/end handling ==\n'
rg -n "close\\(|onended|webkitAudioContext|AudioContext" src/components src/lib || true

Repository: VolvoxCommunity/Volvox.Website

Length of output: 1280


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== src/components/ui/button.tsx (lines 1-120) ==\n'
sed -n '1,120p' src/components/ui/button.tsx | cat -n

printf '\n== src/components/ui/button.tsx (search for AudioContext cleanup and error reporting) ==\n'
rg -n "reportError\\(|close\\(|onended|AudioContext|webkitAudioContext|catch" src/components/ui/button.tsx

printf '\n== src/lib/logger.ts ==\n'
sed -n '1,220p' src/lib/logger.ts | cat -n

Repository: VolvoxCommunity/Volvox.Website

Length of output: 6435


Close the AudioContext and surface unexpected failures.
Create one context per click only if needed, close it in osc.onended, and send non-expected exceptions through reportError() instead of swallowing them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/button.tsx` around lines 17 - 50, Update
playTactilePopSound to close each newly created AudioContext from osc.onended
after the oscillator finishes, while retaining one context per click only when
audio is available. Replace the blanket silent catch with handling that ignores
only expected browser restrictions and forwards unexpected exceptions to
reportError().

Source: Coding guidelines

Comment on lines 538 to 556
const componentProps = {
className: cn(buttonVariants({ variant, size, shape, className })),
style: style,
"data-pressed": noMorph ? undefined : pressed,
...events,
onPointerDown: events.onPointerDown,
onPointerUp: events.onPointerUp,
onPointerEnter: events.onPointerEnter,
onPointerLeave: events.onPointerLeave,
onClick: (e: React.MouseEvent<HTMLButtonElement>) => {
events.onClick();

if (size && ["xl", "2xl", "icon-xl", "icon-2xl"].includes(size)) {
playTactilePopSound();
}

onClick?.(e);
},
...props,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -e
ast-grep outline src/components/ui/button.tsx --view expanded
sed -n '480,620p' src/components/ui/button.tsx | cat -n

Repository: VolvoxCommunity/Volvox.Website

Length of output: 6049


🏁 Script executed:

sed -n '1,220p' src/components/ui/button.tsx | cat -n

Repository: VolvoxCommunity/Volvox.Website

Length of output: 8543


Compose the pointer handlers before spreading ...props. ...props currently overrides the internal onPointerDown/onPointerUp/onPointerEnter/onPointerLeave handlers, which can leave ripple hover/press state stuck. Destructure those callbacks and invoke both handlers, short-circuiting when event.defaultPrevented is set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/button.tsx` around lines 538 - 556, Update the component
props assembly around componentProps to destructure the four pointer callbacks
from props, compose each with its corresponding events handler, and spread only
the remaining props before assigning the composed handlers. Invoke the external
callback and internal handler while short-circuiting appropriately when
event.defaultPrevented is set, preserving the ripple hover/press state behavior.

Comment on lines +634 to +638
className: cn(
(child as React.ReactElement<Record<string, unknown>>).props
.className as string | undefined,
"!h-auto self-stretch",
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
jq '.dependencies.tailwindcss // .devDependencies.tailwindcss' package.json
rg -n '!?h-auto!?' src/components/ui/button.tsx

Repository: VolvoxCommunity/Volvox.Website

Length of output: 219


🌐 Web query:

Tailwind CSS 4.3 trailing important modifier h-auto! !h-auto documentation

💡 Result:

In Tailwind CSS v4, the syntax for the important modifier has changed from a leading bang (!) to a trailing bang (!) [1][2]. To apply the!important declaration to a utility like h-auto, you must now place the exclamation mark at the end of the class name [2]. Correct usage: h-auto! This replaces the legacy v3 syntax, which used!h-auto [3][2]. This change applies to all utility classes, including those using arbitrary values [2]. For example, a custom arbitrary height with the important modifier would be written as h-[100px]! [2].

Citations:


Use Tailwind v4 trailing important syntax. !h-auto is legacy; use h-auto! instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/button.tsx` around lines 634 - 638, Update the className
composition in the button component to replace the legacy Tailwind important
utility `!h-auto` with the v4 trailing syntax `h-auto!`, while preserving the
existing `self-stretch` class and class merging behavior.

Comment thread src/lib/schemas.ts
Comment on lines +211 to +221
export const homepageReviewSchema = z.object({
id: z.string().min(1),
quote: z.string().min(1),
name: z.string().min(1),
role: z.string().min(1),
rating: z.number().int().min(1).max(5),
source: reviewSourceSchema,
product: z.string().optional(),
featured: z.boolean().optional(),
profilePicUrl: z.string().optional(),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

profilePicUrl isn't validated as a URL.

It's rendered directly as <img src={review.profilePicUrl}> in reviews.tsx (Line 199), so a malformed value would break the image without any validation-time signal. Per Zod 4, top-level z.url() is the current recommended form.

🛡️ Proposed fix
-  profilePicUrl: z.string().optional(),
+  profilePicUrl: z.url().optional(),

As per coding guidelines, "Validate all content (MDX, JSON) using Zod schemas defined in lib/schemas.ts" — this closes a gap in that validation for the reviews content file.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const homepageReviewSchema = z.object({
id: z.string().min(1),
quote: z.string().min(1),
name: z.string().min(1),
role: z.string().min(1),
rating: z.number().int().min(1).max(5),
source: reviewSourceSchema,
product: z.string().optional(),
featured: z.boolean().optional(),
profilePicUrl: z.string().optional(),
});
export const homepageReviewSchema = z.object({
id: z.string().min(1),
quote: z.string().min(1),
name: z.string().min(1),
role: z.string().min(1),
rating: z.number().int().min(1).max(5),
source: reviewSourceSchema,
product: z.string().optional(),
featured: z.boolean().optional(),
profilePicUrl: z.url().optional(),
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/schemas.ts` around lines 211 - 221, Update profilePicUrl in
homepageReviewSchema to validate URL values with Zod’s top-level z.url() schema
while preserving its optional behavior; leave the other review fields unchanged.

Source: Coding guidelines

Comment thread src/lib/schemas.ts
Comment on lines +236 to +241
export const reviewsContentSchema = z.object({
eyebrow: z.string().min(1),
headline: z.string().min(1),
trustBadges: z.array(reviewTrustBadgeSchema).default([]),
reviews: z.array(homepageReviewSchema).min(1),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

No uniqueness guard on reviews[].id.

Downstream, reviews.tsx uses review.id as the stable React key (tempId) for the shuffle deck. A future duplicate id in content/review.json would silently produce duplicate/colliding React keys with no schema-level error to catch it at build/parse time.

♻️ Proposed fix
 export const reviewsContentSchema = z.object({
   eyebrow: z.string().min(1),
   headline: z.string().min(1),
   trustBadges: z.array(reviewTrustBadgeSchema).default([]),
   reviews: z.array(homepageReviewSchema).min(1),
-});
+}).refine(
+  (data) => new Set(data.reviews.map((r) => r.id)).size === data.reviews.length,
+  { error: "review ids must be unique" },
+);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const reviewsContentSchema = z.object({
eyebrow: z.string().min(1),
headline: z.string().min(1),
trustBadges: z.array(reviewTrustBadgeSchema).default([]),
reviews: z.array(homepageReviewSchema).min(1),
});
export const reviewsContentSchema = z.object({
eyebrow: z.string().min(1),
headline: z.string().min(1),
trustBadges: z.array(reviewTrustBadgeSchema).default([]),
reviews: z.array(homepageReviewSchema).min(1),
}).refine(
(data) => new Set(data.reviews.map((r) => r.id)).size === data.reviews.length,
{ error: "review ids must be unique" },
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/schemas.ts` around lines 236 - 241, Add a schema-level uniqueness
validation to reviewsContentSchema for reviews[].id, ensuring duplicate IDs
produce a validation error during parsing while preserving the existing review
item validation and other defaults.

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.

1 participant