feat: add Reviews testimonial carousel and How We Work scroll-pinned section#233
feat: add Reviews testimonial carousel and How We Work scroll-pinned section#233rabden wants to merge 2 commits into
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @rabden's task in 2m 36s —— View job PR Review Complete
SummaryThis 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 FixedSemantic HTML ( Issues Requiring AttentionI've left inline comments on specific lines. Summary:
Positive Observations
Minor Notes (No action needed)
Commit pushed: |
📝 WalkthroughWalkthroughThe 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. ChangesHomepage reviews
How We Work section
Button system and site-wide styling
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
16 new issues
|
| {/* 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"> |
There was a problem hiding this comment.
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:
| <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 |
There was a problem hiding this comment.
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:
- Reducing the scroll depth (e.g.,
+=6000or less) by combining some phases - Adding a "skip" button that scrolls past the section
- Detecting reduced motion preference and showing a static version instead
| "source": "product-hunt", | ||
| "product": "Volvox Bot", | ||
| "featured": true, | ||
| "profilePicUrl": "https://i.pravatar.cc/150?img=12" |
There was a problem hiding this comment.
Placeholder URLs: These profile picture URLs (i.pravatar.cc) are placeholder images. Before shipping to production, these should either be:
- Replaced with actual profile images (if available and permission granted)
- 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>
|
| 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. |
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
| const [orderedReviews, setOrderedReviews] = useState< | ||
| (HomepageReview & { tempId: string })[] | ||
| >([]); |
There was a problem hiding this 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.
| 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!
|
|
||
| useEffect(() => { | ||
| const handleScroll = () => { | ||
| const sections = ["products", "blog", "mentorship", "about"]; | ||
| const sections = ["products", "reviews", "blog", "mentorship", "about"]; |
There was a problem hiding this 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.
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.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 (3)
src/components/blog/blog-post-header.tsx (1)
57-66: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd 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 winPrefer anchor tags for external navigation instead of
window.open.Using
onClickwithwindow.openfor external links hinders accessibility, degrades SEO, and prevents users from natively middle-clicking to open in a new tab. Use theasChildprop onButtonto render a standard<a>element instead.
src/components/navigation.tsx#L251-L263: Replace theonClickhandler with an<a href={DISCORD_URL} target="_blank" rel="noopener noreferrer" onClick={handleDiscordClick}>inside the Discord button usingasChild.src/components/products.tsx#L229-L245: Replace theonClickhandler with an<a href={product.links.demo} target="_blank" rel="noopener noreferrer">inside the Launch button usingasChild.🤖 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 winPrefer
<Link>components for internal navigation instead ofrouter.push.Several navigation buttons use
onClick={() => router.push(...)}for internal routing. Using Next.js<Link>components (via theasChildprop) 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 addasChildto the button.src/components/blog/blog-post-navbar.tsx#L80-L88: Wrap the content with<Link href="/#blog">and addasChildto the button.src/components/products/product-post-navbar.tsx#L24-L32: Wrap the icon with<Link href="/products">and addasChildto the button.src/components/products/product-post-navbar.tsx#L81-L89: Wrap the content with<Link href="/products">and addasChildto the button.src/components/team/member-navbar.tsx#L24-L32: Wrap the icon with<Link href="/team">and addasChildto the button.src/components/team/member-navbar.tsx#L80-L88: Wrap the content with<Link href="/team">and addasChildto the button.src/components/team/team-navbar.tsx#L38-L46: Wrap the icon with<Link href="/">and addasChildto the button.src/components/products/products-navbar.tsx#L79-L87: Wrap the icon with<Link href="/">and addasChildto the button.src/components/products.tsx#L400-L414: Wrap the content with<Link href="/products">and addasChildto 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
⛔ Files ignored due to path filters (6)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlpublic/images/UI/calendar_cutout.pngis excluded by!**/*.pngpublic/images/UI/floating_ui_elements_cutout.pngis excluded by!**/*.pngpublic/images/UI/handshake_cutout.pngis excluded by!**/*.pngpublic/images/UI/laptop_cutout.pngis excluded by!**/*.pngpublic/images/UI/two_speech_bubble_cutout.pngis excluded by!**/*.png
📒 Files selected for processing (31)
CHANGELOG.mdcontent/review.jsoncontent/team.jsonpackage.jsonsrc/app/page.tsxsrc/app/team/team-member-detail-client.tsxsrc/components/ai/chat-header.tsxsrc/components/blog.tsxsrc/components/blog/back-to-posts-button.tsxsrc/components/blog/blog-navbar.tsxsrc/components/blog/blog-post-header.tsxsrc/components/blog/blog-post-navbar.tsxsrc/components/cookie-consent-banner.tsxsrc/components/hero/hero-section.tsxsrc/components/homepage-client.tsxsrc/components/how-we-work.tsxsrc/components/mentorship.tsxsrc/components/navigation.tsxsrc/components/products-list-client.tsxsrc/components/products.tsxsrc/components/products/product-hero.tsxsrc/components/products/product-post-navbar.tsxsrc/components/products/products-navbar.tsxsrc/components/reviews.tsxsrc/components/team/member-navbar.tsxsrc/components/team/team-navbar.tsxsrc/components/theme-toggle.tsxsrc/components/ui/button.tsxsrc/lib/content.tssrc/lib/schemas.tssrc/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
| "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" | ||
| } |
There was a problem hiding this comment.
🔒 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"]; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -A 5 -B 5 'how-we-work' src/components/navigation.tsxRepository: 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/componentsRepository: 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 -nRepository: 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' srcRepository: 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/componentsRepository: VolvoxCommunity/Volvox.Website
Length of output: 1209
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n 'currentSection' src/components src/app srcRepository: VolvoxCommunity/Volvox.Website
Length of output: 2608
Add how-we-work to the scroll spy src/components/homepage-client.tsx:112 — HowWeWork 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.
| 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); |
There was a problem hiding this comment.
📐 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); |
There was a problem hiding this comment.
📐 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.
| 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.
| 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 | ||
| }, |
There was a problem hiding this comment.
🎯 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.
| // --- 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) | ||
| } |
There was a problem hiding this comment.
🩺 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 || trueRepository: 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 -nRepository: 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
| 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, | ||
| }; |
There was a problem hiding this comment.
🎯 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 -nRepository: VolvoxCommunity/Volvox.Website
Length of output: 6049
🏁 Script executed:
sed -n '1,220p' src/components/ui/button.tsx | cat -nRepository: 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.
| className: cn( | ||
| (child as React.ReactElement<Record<string, unknown>>).props | ||
| .className as string | undefined, | ||
| "!h-auto self-stretch", | ||
| ), |
There was a problem hiding this comment.
🎯 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.tsxRepository: 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:
- 1: https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md
- 2: https://github.com/dxos/dxos/blob/e4919fd46f87a8107cac7af0def56ea6136edc1f/TAILWIND_V4_UTILITY_CHANGES.md
- 3: https://tailwindcss.com/docs/upgrade-guide
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.
| 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(), | ||
| }); |
There was a problem hiding this comment.
🗄️ 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.
| 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
| 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), | ||
| }); |
There was a problem hiding this comment.
🗄️ 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.
| 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.
Summary
src/components/reviews.tsx): staggered testimonial card deck.content/review.json.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.rabden) type correctedmarketer→builderand role set toDeveloper.@gsap/reactdependency.Test plan
pnpm format && pnpm typecheck && pnpm lint && pnpm test && pnpm buildall pass (56 tests green).🤖 Generated with opencode
Screencast.From.2026-07-14.23-39-12.mp4
Screencast.From.2026-07-14.21-39-44.mp4