Skip to content

Commit 8a4f103

Browse files
Alex Bilozorclaude
andcommitted
feat: Add animated masonry layout to hero section
- 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]>
1 parent 300ee57 commit 8a4f103

File tree

2 files changed

+222
-32
lines changed

2 files changed

+222
-32
lines changed

app/src/app/globals.css

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,44 @@
185185
.border-brand-orange {
186186
border-color: var(--brand-orange);
187187
}
188+
189+
/* Masonry scroll animations */
190+
@keyframes scroll-up {
191+
0% {
192+
transform: translateY(0);
193+
}
194+
100% {
195+
transform: translateY(-50%);
196+
}
197+
}
198+
199+
.animate-scroll-slow {
200+
animation: scroll-up 80s linear infinite;
201+
}
202+
203+
.animate-scroll-medium {
204+
animation: scroll-up 70s linear infinite;
205+
}
206+
207+
.animate-scroll-fast {
208+
animation: scroll-up 60s linear infinite;
209+
}
210+
211+
.animate-scroll-slower {
212+
animation: scroll-up 90s linear infinite;
213+
}
214+
215+
.animate-scroll-faster {
216+
animation: scroll-up 50s linear infinite;
217+
}
218+
219+
@media (prefers-reduced-motion: reduce) {
220+
.animate-scroll-slow,
221+
.animate-scroll-medium,
222+
.animate-scroll-fast,
223+
.animate-scroll-slower,
224+
.animate-scroll-faster {
225+
animation: none;
226+
}
227+
}
188228
}

app/src/app/page.tsx

Lines changed: 182 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -251,40 +251,190 @@ export default async function HomePage() {
251251
}
252252

253253
function LandingHero({ images }: { images: Image[] }) {
254+
// Distribute images across columns for masonry effect
255+
const distributeImagesAcrossColumns = (images: Image[], columnCount: number) => {
256+
const columns: Image[][] = Array.from({ length: columnCount }, () => []);
257+
258+
images.forEach((image, index) => {
259+
const columnIndex = index % columnCount;
260+
columns[columnIndex].push(image);
261+
});
262+
263+
return columns;
264+
};
265+
266+
// Create different column counts for different screen sizes
267+
// We'll use CSS to show/hide columns based on screen size
268+
const desktopColumns = distributeImagesAcrossColumns(images, 4);
269+
const tabletColumns = distributeImagesAcrossColumns(images, 3);
270+
const mobileColumns = distributeImagesAcrossColumns(images, 2);
271+
272+
// Animation speed classes for each column
273+
const animationSpeeds = [
274+
'animate-scroll-slow',
275+
'animate-scroll-medium',
276+
'animate-scroll-fast',
277+
'animate-scroll-slower'
278+
];
279+
254280
return (
255-
<section className="w-full h-[80vh] overflow-hidden grid [&>*]:col-[1] [&>*]:row-[1]">
256-
<div className="w-full h-full grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-1">
257-
{images.map((image, index) => (
258-
<div
259-
key={image.url + index}
260-
className="relative w-full aspect-square overflow-hidden"
261-
>
262-
<NextImage
263-
src={image.url}
264-
placeholder={image.placeholder ? "blur" : undefined}
265-
blurDataURL={image.placeholder || undefined}
266-
fill
267-
className="object-cover"
268-
priority
269-
alt={image.alt || `Event image ${index + 1}`}
270-
sizes="(max-width: 768px) 50vw, (max-width: 1024px) 33vw, (max-width: 1280px) 25vw, 20vw"
271-
/>
272-
</div>
273-
))}
274-
</div>
281+
<section className="relative w-full h-[80vh] overflow-hidden">
282+
{/* Desktop Layout - 4 columns */}
283+
<div className="absolute inset-0 hidden lg:grid grid-cols-4 gap-1">
284+
{desktopColumns.map((columnImages, columnIndex) => (
285+
<div
286+
key={`desktop-column-${columnIndex}`}
287+
className={`flex flex-col gap-1 ${animationSpeeds[columnIndex]}`}
288+
>
289+
{/* First set of images */}
290+
{columnImages.map((image, index) => (
291+
<div
292+
key={`${image.url}-${index}-1`}
293+
className="relative w-full"
294+
style={{ aspectRatio: `${image.width}/${image.height}` }}
295+
>
296+
<NextImage
297+
src={image.url}
298+
placeholder={image.placeholder ? "blur" : undefined}
299+
blurDataURL={image.placeholder || undefined}
300+
fill
301+
className="object-cover"
302+
priority={index < 5}
303+
alt={image.alt || `Event image ${index + 1}`}
304+
sizes="25vw"
305+
/>
306+
</div>
307+
))}
308+
{/* Duplicate set for seamless loop */}
309+
{columnImages.map((image, index) => (
310+
<div
311+
key={`${image.url}-${index}-2`}
312+
className="relative w-full"
313+
style={{ aspectRatio: `${image.width}/${image.height}` }}
314+
>
315+
<NextImage
316+
src={image.url}
317+
placeholder={image.placeholder ? "blur" : undefined}
318+
blurDataURL={image.placeholder || undefined}
319+
fill
320+
className="object-cover"
321+
alt={image.alt || `Event image ${index + 1}`}
322+
sizes="25vw"
323+
/>
324+
</div>
325+
))}
326+
</div>
327+
))}
328+
</div>
275329

276-
{/* Content */}
277-
<div className="z-20 bg-gradient-to-b from-black/70 to-black/30 flex flex-col items-center pt-[30vh] text-center text-white px-4">
278-
<h1 className="mb-4 text-4xl sm:text-5xl md:text-6xl font-bold tracking-tight">
279-
All Things Web 🚀
280-
</h1>
281-
<p className="max-w-2xl text-lg sm:text-xl">
282-
Discover exciting web development events in the Bay Area and San
283-
Francisco. Join us for hackathons, hangouts, and meetups to connect
284-
with fellow developers and web enthusiasts.
285-
</p>
286-
</div>
287-
</section>
330+
{/* Tablet Layout - 3 columns */}
331+
<div className="absolute inset-0 hidden md:grid lg:hidden grid-cols-3 gap-1">
332+
{tabletColumns.map((columnImages, columnIndex) => (
333+
<div
334+
key={`tablet-column-${columnIndex}`}
335+
className={`flex flex-col gap-1 ${animationSpeeds[columnIndex]}`}
336+
>
337+
{/* First set of images */}
338+
{columnImages.map((image, index) => (
339+
<div
340+
key={`${image.url}-${index}-1`}
341+
className="relative w-full"
342+
style={{ aspectRatio: `${image.width}/${image.height}` }}
343+
>
344+
<NextImage
345+
src={image.url}
346+
placeholder={image.placeholder ? "blur" : undefined}
347+
blurDataURL={image.placeholder || undefined}
348+
fill
349+
className="object-cover"
350+
priority={index < 3}
351+
alt={image.alt || `Event image ${index + 1}`}
352+
sizes="33vw"
353+
/>
354+
</div>
355+
))}
356+
{/* Duplicate set for seamless loop */}
357+
{columnImages.map((image, index) => (
358+
<div
359+
key={`${image.url}-${index}-2`}
360+
className="relative w-full"
361+
style={{ aspectRatio: `${image.width}/${image.height}` }}
362+
>
363+
<NextImage
364+
src={image.url}
365+
placeholder={image.placeholder ? "blur" : undefined}
366+
blurDataURL={image.placeholder || undefined}
367+
fill
368+
className="object-cover"
369+
alt={image.alt || `Event image ${index + 1}`}
370+
sizes="33vw"
371+
/>
372+
</div>
373+
))}
374+
</div>
375+
))}
376+
</div>
377+
378+
{/* Mobile Layout - 2 columns */}
379+
<div className="absolute inset-0 grid md:hidden grid-cols-2 gap-1">
380+
{mobileColumns.map((columnImages, columnIndex) => (
381+
<div
382+
key={`mobile-column-${columnIndex}`}
383+
className={`flex flex-col gap-1 ${animationSpeeds[columnIndex]}`}
384+
>
385+
{/* First set of images */}
386+
{columnImages.map((image, index) => (
387+
<div
388+
key={`${image.url}-${index}-1`}
389+
className="relative w-full"
390+
style={{ aspectRatio: `${image.width}/${image.height}` }}
391+
>
392+
<NextImage
393+
src={image.url}
394+
placeholder={image.placeholder ? "blur" : undefined}
395+
blurDataURL={image.placeholder || undefined}
396+
fill
397+
className="object-cover"
398+
priority={index < 2}
399+
alt={image.alt || `Event image ${index + 1}`}
400+
sizes="50vw"
401+
/>
402+
</div>
403+
))}
404+
{/* Duplicate set for seamless loop */}
405+
{columnImages.map((image, index) => (
406+
<div
407+
key={`${image.url}-${index}-2`}
408+
className="relative w-full"
409+
style={{ aspectRatio: `${image.width}/${image.height}` }}
410+
>
411+
<NextImage
412+
src={image.url}
413+
placeholder={image.placeholder ? "blur" : undefined}
414+
blurDataURL={image.placeholder || undefined}
415+
fill
416+
className="object-cover"
417+
alt={image.alt || `Event image ${index + 1}`}
418+
sizes="50vw"
419+
/>
420+
</div>
421+
))}
422+
</div>
423+
))}
424+
</div>
425+
426+
{/* Content Overlay */}
427+
<div className="absolute inset-0 z-20 bg-gradient-to-b from-black/70 to-black/30 flex flex-col items-center justify-center text-center text-white px-4">
428+
<h1 className="mb-4 text-4xl sm:text-5xl md:text-6xl font-bold tracking-tight">
429+
All Things Web 🚀
430+
</h1>
431+
<p className="max-w-2xl text-lg sm:text-xl">
432+
Discover exciting web development events in the Bay Area and San
433+
Francisco. Join us for hackathons, hangouts, and meetups to connect
434+
with fellow developers and web enthusiasts.
435+
</p>
436+
</div>
437+
</section>
288438
);
289439
}
290440

0 commit comments

Comments
 (0)