Skip to content

Conversation

@bilozorDev
Copy link

@bilozorDev bilozorDev commented Sep 18, 2025

Summary

This PR introduces an animated masonry layout for the hero section images, creating a dynamic and visually engaging landing page experience.

Changes

  • ✨ Implemented responsive masonry grid layout for hero images
    • 2 columns on mobile devices
    • 3 columns on tablet devices
    • 4 columns on desktop screens
  • 🎨 Added smooth scroll animations with varied speeds per column for visual depth
  • ♻️ Duplicated images to create seamless infinite scroll effect
  • ♿ Added accessibility support respecting prefers-reduced-motion setting
  • 📐 Preserved image aspect ratios using dynamic inline styles

Files Changed

  • src/app/page.tsx - Updated LandingHero component with masonry layout logic
  • src/app/globals.css - Added CSS animations and keyframes for scroll effects

Visual Impact

The hero section now features a Pinterest-style masonry grid with continuously scrolling columns at different speeds, creating an engaging visual effect while showcasing event images.

Testing

  • Tested on mobile (2 columns)
  • Tested on tablet (3 columns)
  • Tested on desktop (4 columns)
  • Verified animation stops with prefers-reduced-motion
  • Confirmed seamless loop behavior

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Replaced hero mosaic with a responsive masonry layout (desktop: 4 cols, tablet: 3, mobile: 2).
    • Continuous vertical scrolling per column with varied speeds and duplicated content for seamless looping.
    • Dedicated overlay layer for title and subtitle atop the masonry.
  • Performance
    • Optimized image loading priorities per breakpoint and preserved image aspect ratios.
  • Accessibility
    • Animations disabled when users prefer reduced motion.
  • Style
    • New utility animation classes for multiple scroll speeds.

- Implemented responsive masonry grid layout for hero images
  - 2 columns on mobile
  - 3 columns on tablet
  - 4 columns on desktop
- Added smooth scroll animations with varied speeds per column
- Duplicated images for seamless infinite scroll effect
- Added accessibility support with prefers-reduced-motion
- Preserved image aspect ratios using dynamic styles

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@vercel
Copy link

vercel bot commented Sep 18, 2025

Someone is attempting to deploy a commit to the personal Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Sep 18, 2025

Walkthrough

Adds a namespaced vertical scroll keyframe (@keyframes atw-scroll-up) and five CSS animation utilities, and refactors the homepage hero into a breakpoint-aware, multi-column masonry layout that duplicates images per column, assigns per-column scroll speeds, and layers the title/subtitle as an overlay.

Changes

Cohort / File(s) Summary
CSS animation utilities
app/src/app/globals.css
Adds @keyframes atw-scroll-up and five utility classes (.animate-scroll-slower, .animate-scroll-slow, .animate-scroll-medium, .animate-scroll-fast, .animate-scroll-faster) with durations 90s/80s/70s/60s/50s, performance hints, and a prefers-reduced-motion: reduce override setting animation: none.
Responsive masonry hero layout
app/src/app/page.tsx
Replaces previous hero mosaic with breakpoint-specific absolute sections (desktop/tablet/mobile), renders 4/3/2 column masonry, duplicates image stacks per column for seamless looping, assigns per-column animation speeds, preserves image aspect ratios, and adjusts NextImage loading priority.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Page as page.tsx
  participant Breakpoints as Breakpoint Sections
  participant Distributor as Image Distributor
  participant Column as Column Renderer
  participant NextImg as NextImage
  Note right of Breakpoints: Desktop (4 cols) / Tablet (3 cols) / Mobile (2 cols)

  User->>Page: Request "/"
  Page->>Breakpoints: Select active breakpoint (CSS/viewport)
  Breakpoints->>Distributor: Request images for N columns
  Distributor-->>Breakpoints: Arrays of images (duplicated for loop)
  loop per column
    Breakpoints->>Column: Render column with assigned speed class
    loop per image
      Column->>NextImg: Render image (aspectRatio, priority)
    end
  end
  Page->>Page: Render overlay (title/subtitle) above masonry

  alt prefers-reduced-motion
    Note over Column: CSS media query disables animations
  else animations allowed
    Note over Column: `.animate-scroll-*` uses `atw-scroll-up` (linear infinite)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

Columns rise and softly sweep,
Images march in ordered sleep.
Keyframes lift them, slow to fast,
Breakpoints choose their marching past.
Overlay watches, calm and neat.

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: Add animated masonry layout to hero section" is concise, specific, and accurately reflects the primary changes in src/app/page.tsx and src/app/globals.css (animated masonry hero with scroll animations); it clearly communicates the main intent to reviewers and is scannable in history.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (8)
app/src/app/globals.css (3)

190-197: Namespace keyframes to avoid collisions with imported animation lib.

tw-animate-css might define similarly named keyframes. Prefix to ensure isolation.

-  @keyframes scroll-up {
+  @keyframes atw-scroll-up {

And update usages below:

-  .animate-scroll-slow { animation: scroll-up 80s linear infinite; }
+  .animate-scroll-slow { animation: atw-scroll-up 80s linear infinite; }

-  .animate-scroll-medium { animation: scroll-up 70s linear infinite; }
+  .animate-scroll-medium { animation: atw-scroll-up 70s linear infinite; }

-  .animate-scroll-fast { animation: scroll-up 60s linear infinite; }
+  .animate-scroll-fast { animation: atw-scroll-up 60s linear infinite; }

-  .animate-scroll-slower { animation: scroll-up 90s linear infinite; }
+  .animate-scroll-slower { animation: atw-scroll-up 90s linear infinite; }

-  .animate-scroll-faster { animation: scroll-up 50s linear infinite; }
+  .animate-scroll-faster { animation: atw-scroll-up 50s linear infinite; }

199-217: Hint the compositor.

Add will-change: transform; backface-visibility: hidden; to animation classes to reduce paint jank.

 .animate-scroll-slow {
-  animation: atw-scroll-up 80s linear infinite;
+  animation: atw-scroll-up 80s linear infinite;
+  will-change: transform;
+  backface-visibility: hidden;
 }
 .animate-scroll-medium {
-  animation: atw-scroll-up 70s linear infinite;
+  animation: atw-scroll-up 70s linear infinite;
+  will-change: transform;
+  backface-visibility: hidden;
 }
 .animate-scroll-fast {
-  animation: atw-scroll-up 60s linear infinite;
+  animation: atw-scroll-up 60s linear infinite;
+  will-change: transform;
+  backface-visibility: hidden;
 }
 .animate-scroll-slower {
-  animation: atw-scroll-up 90s linear infinite;
+  animation: atw-scroll-up 90s linear infinite;
+  will-change: transform;
+  backface-visibility: hidden;
 }
 .animate-scroll-faster {
-  animation: atw-scroll-up 50s linear infinite;
+  animation: atw-scroll-up 50s linear infinite;
+  will-change: transform;
+  backface-visibility: hidden;
 }

219-227: Reduced‑motion OK. Consider also stopping on interaction.

Optional: pause animation on hover/focus for user control.

 @media (prefers-reduced-motion: reduce) {
   .animate-scroll-slow,
   .animate-scroll-medium,
   .animate-scroll-fast,
   .animate-scroll-slower,
   .animate-scroll-faster {
     animation: none;
   }
 }
+
+/* Optional user control */
+.animate-scroll-slow:hover,
+.animate-scroll-medium:hover,
+.animate-scroll-fast:hover,
+.animate-scroll-slower:hover,
+.animate-scroll-faster:hover { animation-play-state: paused; }
app/src/app/page.tsx (5)

283-328: Mark decorative image grids as hidden from assistive tech.

These mosaics are purely decorative; dozens of alt texts will create noise.

- <div className="absolute inset-0 hidden lg:grid grid-cols-4 gap-1">
+ <div className="absolute inset-0 hidden lg:grid grid-cols-4 gap-1" aria-hidden="true">

Apply same to tablet and mobile grids.

Also applies to: 331-376, 379-424


285-289: Desynchronize column starts to avoid visible sync.

Add a small negative animationDelay per column for a staggered start.

- className={`flex flex-col gap-1 ${animationSpeeds[columnIndex]}`}
+ className={`flex flex-col gap-1 ${animationSpeeds[columnIndex]}`}
+ style={{ animationDelay: `${-(columnIndex * 5)}s` }}

Also applies to: 333-336, 381-384


266-271: Compute columns once; avoid triple DOM for breakpoints (optional).

Rendering three absolute layers is heavy. Consider a single responsive grid (grid-cols-2 md:grid-cols-3 lg:grid-cols-4) and derive per‑breakpoint columns from a single 4‑col distribution (merge columns for smaller breakpoints).


254-265: Function is fine. Add memoization if moved client‑side later.

If this becomes a Client Component, wrap distributions in useMemo(images) to avoid recompute per render.


426-436: Overlay copy OK. Consider sr-only heading duplication for a11y.

Optional: add an offscreen <h1> before the mosaics to ensure a stable LCP and semantic order regardless of visual overlay.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 300ee57 and 8a4f103.

📒 Files selected for processing (2)
  • app/src/app/globals.css (1 hunks)
  • app/src/app/page.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
app/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/neon-auth.mdc)

app/**/*.{tsx,ts}: Use pre-built Stack UI components (e.g., , , ) when building auth UI
In Client Components, retrieve the current user via useUser(); use useUser({ or: "redirect" }) for protected pages
In Server Components, use stackServerApp.getUser() and getUser({ or: "redirect" }) for protection

Files:

  • app/src/app/page.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/tanstack-db.mdc)

**/*.{ts,tsx}: In mutationFn implementations, ensure server writes have synced back before returning (e.g., await collection.refetch() or equivalent) so optimistic state can be safely discarded
When creating a collection, prefer supplying a Standard Schema (e.g., Zod/Effect) for validation and typing
If a schema is provided to createCollection, do not also pass a generic type parameter (e.g., avoid createCollection() when schema is present)
When using queryCollectionOptions, provide a stable getKey function to identify items
When using electricCollectionOptions, provide shapeOptions (including url and params.table) and getKey
When using trailBaseCollectionOptions, provide recordApi and getKey; use parse/serialize for field transformations as needed
When using rxdbCollectionOptions, provide rxCollection (and optionally startSync) to wire RxDB with TanStack DB
When using localStorageCollectionOptions, provide storageKey and getKey (optionally a storage backend like sessionStorage)
When using localOnlyCollectionOptions, provide getKey; optionally use initialData and mutation handlers
Use collection operations (insert, update, delete) with draft updaters instead of mutating items directly

Files:

  • app/src/app/page.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/tanstack-db.mdc)

In React components, use the useLiveQuery hook to bind live query results to component state

Files:

  • app/src/app/page.tsx
app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)

Do not use Bun-specific APIs in production code; ensure compatibility with Node.js runtime

Files:

  • app/src/app/page.tsx
🧬 Code graph analysis (1)
app/src/app/page.tsx (1)
app/src/lib/events.ts (1)
  • Image (1-7)
🔇 Additional comments (1)
app/src/app/page.tsx (1)

253-281: Approve — image width/height fields verified

Schema requires width/height and the main image producers populate them (getPastEventImages / signImage flows / image-gen utils; common sizes seen: 1200x630, 400x400). Only intentional exception: app/src/app/api/v1/profile/route.ts sets width/height = 0 for signing.

Comment on lines +302 to +303
priority={index < 5}
alt={image.alt || `Event image ${index + 1}`}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Excessive Next/Image priority preloads (up to ~20 on desktop).

This will tank LCP and bandwidth. Only one (or none) should be priority.

- priority={index < 5}
+ priority={columnIndex === 0 && index === 0}

Consider making all background images non‑priority if text is LCP.

Also applies to: 350-351, 398-399

🤖 Prompt for AI Agents
In app/src/app/page.tsx around lines 302-303 (and similarly at 350-351,
398-399), multiple Next/Image components are being marked priority causing
excessive preloads; change the logic so only a single image (e.g., the first
hero or index === 0) has priority (or remove priority entirely for background
images when text is expected to be LCP). Update the conditional from index < 5
to something like index === 0 (or false for background images), and apply the
same change at the other listed locations so only one image is priority and
background images are non-priority.

Alex Bilozor and others added 2 commits September 17, 2025 19:16
- Created safeAspectRatio() helper function that validates width/height
- Returns fallback ratio "1/1" when dimensions are null/undefined
- Logs warning once when missing dimensions are detected
- Updated all 6 aspect ratio occurrences in masonry layout
- Prevents zero-height tiles and layout breakage

This addresses PR feedback about potential invalid aspect ratio values.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
CSS improvements:
- Namespaced keyframes to atw-scroll-up to avoid conflicts
- Added will-change and backface-visibility for GPU optimization
- Improved animation performance to reduce paint jank

Accessibility improvements:
- Added aria-hidden="true" to decorative image grids
- Prevents screen readers from announcing decorative images

Visual improvements:
- Added staggered animation delay per column (0s, -5s, -10s, -15s)
- Creates more natural, desynchronized scrolling effect

Addresses all CSS-related PR review feedback.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
app/src/app/page.tsx (1)

324-324: Remove Next/Image priority to avoid triple preloads across hidden breakpoints.

All three breakpoint sections render server-side; each priority emits a <link rel="preload">, causing excessive network/LCP regressions. Make these background images non-priority.

Apply this diff:

-                    priority={index < 5}
-                    alt={image.alt || `Event image ${index + 1}`}
+                    alt={image.alt || `Event image ${index + 1}`}
-                    priority={index < 3}
-                    alt={image.alt || `Event image ${index + 1}`}
+                    alt={image.alt || `Event image ${index + 1}`}
-                    priority={index < 2}
-                    alt={image.alt || `Event image ${index + 1}`}
+                    alt={image.alt || `Event image ${index + 1}`}
#!/bin/bash
# Verify no priority images remain in the hero
rg -nP '\bpriority\s*=' app/src/app/page.tsx

Also applies to: 373-373, 422-422

🧹 Nitpick comments (3)
app/src/app/page.tsx (3)

325-325: Mark masonry images as decorative (a11y).

These are purely visual; use empty alt and hide from AT.

Apply this diff:

-                    alt={image.alt || `Event image ${index + 1}`}
+                    alt=""
+                    role="presentation"
+                    aria-hidden="true"

Also applies to: 343-343, 374-374, 392-392, 423-423, 441-441


266-269: Warn once only in dev to avoid noisy server logs.

Gate the warn behind NODE_ENV to prevent production log spam; keep the once-only behavior.

-  if (!missingDimensionsWarned) {
+  if (process.env.NODE_ENV !== "production" && !missingDimensionsWarned) {
     console.warn("Missing or invalid image dimensions detected, using fallback aspect ratio");
     missingDimensionsWarned = true;
   }

308-308: Defensive index for animation speed.

Future-proof against column count changes by modding the index.

-              className={`flex flex-col gap-1 ${animationSpeeds[columnIndex]}`}
+              className={`flex flex-col gap-1 ${animationSpeeds[columnIndex % animationSpeeds.length]}`}

Also applies to: 357-357, 406-406

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8a4f103 and 957996f.

📒 Files selected for processing (2)
  • app/src/app/globals.css (1 hunks)
  • app/src/app/page.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/app/globals.css
🧰 Additional context used
📓 Path-based instructions (4)
app/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/neon-auth.mdc)

app/**/*.{tsx,ts}: Use pre-built Stack UI components (e.g., , , ) when building auth UI
In Client Components, retrieve the current user via useUser(); use useUser({ or: "redirect" }) for protected pages
In Server Components, use stackServerApp.getUser() and getUser({ or: "redirect" }) for protection

Files:

  • app/src/app/page.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/tanstack-db.mdc)

**/*.{ts,tsx}: In mutationFn implementations, ensure server writes have synced back before returning (e.g., await collection.refetch() or equivalent) so optimistic state can be safely discarded
When creating a collection, prefer supplying a Standard Schema (e.g., Zod/Effect) for validation and typing
If a schema is provided to createCollection, do not also pass a generic type parameter (e.g., avoid createCollection() when schema is present)
When using queryCollectionOptions, provide a stable getKey function to identify items
When using electricCollectionOptions, provide shapeOptions (including url and params.table) and getKey
When using trailBaseCollectionOptions, provide recordApi and getKey; use parse/serialize for field transformations as needed
When using rxdbCollectionOptions, provide rxCollection (and optionally startSync) to wire RxDB with TanStack DB
When using localStorageCollectionOptions, provide storageKey and getKey (optionally a storage backend like sessionStorage)
When using localOnlyCollectionOptions, provide getKey; optionally use initialData and mutation handlers
Use collection operations (insert, update, delete) with draft updaters instead of mutating items directly

Files:

  • app/src/app/page.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/tanstack-db.mdc)

In React components, use the useLiveQuery hook to bind live query results to component state

Files:

  • app/src/app/page.tsx
app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)

Do not use Bun-specific APIs in production code; ensure compatibility with Node.js runtime

Files:

  • app/src/app/page.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-18T00:04:06.390Z
Learnt from: CR
PR: allthingsweb-dev/allthingsweb#0
File: .cursor/rules/s3-image-signing.mdc:0-0
Timestamp: 2025-09-18T00:04:06.390Z
Learning: Applies to {app/src/app/api/**/route.ts,app/src/lib/**/*.{ts,tsx}} : Handle missing images explicitly (check for existence before signing and return null when absent)

Applied to files:

  • app/src/app/page.tsx
🧬 Code graph analysis (1)
app/src/app/page.tsx (1)
app/src/lib/events.ts (1)
  • Image (1-7)
🔇 Additional comments (3)
app/src/app/page.tsx (3)

256-272: safeAspectRatio helper LGTM.

Solid guard against null/zero dims; good fallback.


275-286: Image distribution helper is clean and deterministic.

Even spread via modulo; good for seamless looping.


450-461: Overlay content structure and contrast look good.

Readable hierarchy; gradient ensures text legibility over media.

@bilozorDev
Copy link
Author

Crying Baby

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