Skip to content

Commit f26f106

Browse files
fix: stop the jump in layout when testimonials grid mounts
1 parent 70fd08a commit f26f106

File tree

2 files changed

+60
-139
lines changed

2 files changed

+60
-139
lines changed

components/Testimonials.css

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,58 @@
184184
color: var(--clr-primary);
185185
}
186186

187-
/* Masonry layout styles */
187+
/* Responsive Testimonials Layout - Mobile First Approach */
188+
/*
189+
* Default: Horizontal scroll for mobile
190+
* Breakpoints match the original JS logic:
191+
* - <590px: 1 column (horizontal scroll)
192+
* - 590px-899px: 2 columns (masonry grid)
193+
* - >=900px: 3 columns (masonry grid)
194+
*
195+
* Container padding accounts for p-7 (56px total) and gap-4 (16px between items)
196+
*/
197+
198+
/* Default horizontal layout for mobile */
199+
.testimonials-horizontal {
200+
display: flex;
201+
gap: 1rem;
202+
overflow-x: auto;
203+
padding-bottom: 0.5rem;
204+
align-items: flex-start;
205+
touch-action: pan-x;
206+
}
207+
208+
.testimonials-horizontal > * {
209+
flex-shrink: 0;
210+
width: 280px; /* Minimum card width, matches original JS calculation */
211+
}
212+
213+
/* CSS Grid masonry layout for tablets and desktop */
214+
.testimonials-masonry {
215+
display: grid;
216+
gap: 1rem;
217+
grid-template-columns: 1fr; /* Single column on mobile by default */
218+
}
219+
220+
.testimonials-masonry > * {
221+
width: 100%;
222+
}
223+
224+
/* Tablet: 2 columns at 590px+ (matches original breakpoint) */
225+
@media (min-width: 590px) {
226+
.testimonials-masonry {
227+
grid-template-columns: repeat(2, 1fr);
228+
}
229+
}
230+
231+
/* Desktop: 3 columns at 900px+ (matches original breakpoint) */
232+
@media (min-width: 900px) {
233+
.testimonials-masonry {
234+
grid-template-columns: repeat(3, 1fr);
235+
}
236+
}
237+
238+
/* Legacy masonry styles - keeping for backwards compatibility */
188239
.masonry-grid {
189240
column-gap: 1rem;
190241
orphans: 1;

components/Testimonials.tsx

Lines changed: 8 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,6 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React from 'react';
22
import './Testimonials.css';
33

4-
// Custom hook to track window width
5-
const useWindowWidth = () => {
6-
const [windowWidth, setWindowWidth] = useState<number>(() => {
7-
// Check if we're in a browser environment
8-
if (typeof window !== 'undefined') {
9-
return window.innerWidth;
10-
}
11-
return 1024; // Default fallback for SSR
12-
});
13-
14-
useEffect(() => {
15-
if (typeof window === 'undefined') return;
16-
17-
const handleResize = () => {
18-
setWindowWidth(window.innerWidth);
19-
};
20-
21-
window.addEventListener('resize', handleResize);
22-
return () => window.removeEventListener('resize', handleResize);
23-
}, []);
24-
25-
return windowWidth;
26-
};
27-
28-
// Custom hook to track if component has mounted
29-
const useHasMounted = () => {
30-
const [hasMounted, setHasMounted] = useState(false);
31-
32-
useEffect(() => {
33-
// Add a small delay to ensure horizontal layout is fully rendered
34-
const timer = setTimeout(() => {
35-
setHasMounted(true);
36-
}, 100);
37-
38-
return () => clearTimeout(timer);
39-
}, []);
40-
41-
return hasMounted;
42-
};
43-
44-
// Function to calculate card width based on screen size
45-
const calculateCardWidth = (screenWidth: number): number => {
46-
// Account for container padding (p-7 = 28px on each side = 56px total)
47-
// and gaps between cards (gap-4 = 16px between each card)
48-
const containerPadding = 56;
49-
50-
if (screenWidth < 590) {
51-
// Mobile: Single card takes most of the width
52-
return Math.max(280, screenWidth - containerPadding - 20); // Extra margin for safety
53-
} else if (screenWidth < 900) {
54-
// At 590px and above: Show 2 cards
55-
const availableWidth = screenWidth - containerPadding;
56-
const gapWidth = 16; // gap-4 between 2 cards = 1 gap
57-
return Math.max(280, (availableWidth - gapWidth) / 2);
58-
} else {
59-
// At 900px and above: Show 3 cards, but cap at 350px max
60-
const availableWidth = screenWidth - containerPadding;
61-
const gapWidth = 32; // gap-4 between 3 cards = 2 gaps
62-
const calculatedWidth = (availableWidth - gapWidth) / 3;
63-
return Math.min(350, calculatedWidth);
64-
}
65-
};
66-
674
// Types for testimonials data
685
export interface Testimonial {
696
id: string;
@@ -197,86 +134,19 @@ const TestimonialsContainer: React.FC<{
197134
testimonials: Testimonial[];
198135
layout?: TestimonialLayout;
199136
}> = ({ testimonials, layout = 'horizontal' }) => {
200-
const windowWidth = useWindowWidth();
201-
const hasMounted = useHasMounted();
202-
const cardWidth = calculateCardWidth(windowWidth);
203-
204-
// Determine the effective layout - start with horizontal on mobile until mounted
205-
const effectiveLayout = (() => {
206-
if (layout === 'masonry' && hasMounted) {
207-
// Only use masonry after component has mounted
208-
return 'masonry';
209-
}
210-
// Default to horizontal for better mobile experience
211-
return 'horizontal';
212-
})();
213-
214-
// Layout-specific classes and styles
215-
const getLayoutClasses = () => {
216-
switch (effectiveLayout) {
217-
case 'horizontal':
218-
return "flex gap-4 overflow-x-auto pb-2 items-start swiper-horizontal";
219-
case 'masonry':
220-
return "masonry-grid";
221-
default:
222-
return "flex gap-4 overflow-x-auto pb-2 items-start";
223-
}
224-
};
225-
226-
const getCardClasses = () => {
227-
switch (effectiveLayout) {
228-
case 'horizontal':
229-
return "flex-shrink-0";
230-
case 'masonry':
231-
return "masonry-item";
232-
default:
233-
return "flex-shrink-0";
234-
}
235-
};
236-
237-
const getContainerStyle = (): React.CSSProperties => {
238-
if (effectiveLayout === 'masonry') {
239-
return {
240-
columns: windowWidth < 590 ? 1 : windowWidth < 900 ? 2 : 3,
241-
columnGap: '1rem',
242-
columnFill: 'balance' as const
243-
};
244-
}
245-
return {};
246-
};
247-
248-
const getCardStyle = (testimonial: Testimonial): React.CSSProperties => {
249-
if (effectiveLayout === 'masonry') {
250-
return {
251-
breakInside: 'avoid' as const,
252-
marginBottom: '1rem',
253-
width: '100%'
254-
};
255-
}
256-
return { width: `${cardWidth}px` };
257-
};
137+
// Use CSS classes for responsive layout - no JavaScript calculations needed
138+
const containerClass = layout === 'masonry'
139+
? 'testimonials-masonry'
140+
: 'testimonials-horizontal';
258141

259142
return (
260143
<div className="p-7" data-id="d4c7d44a-95be-4b3c-978f-4ba863490a54">
261-
<div className="sj-container ">
144+
<div className="sj-container">
262145
<div className="relative w-full px-0">
263146
<div className="w-full">
264-
{/* Loading indicator for layout transition */}
265-
{layout === 'masonry' && !hasMounted && (
266-
<div className="text-center text-sm text-gray-500 mb-4">
267-
Loading masonry layout...
268-
</div>
269-
)}
270-
<div
271-
className={getLayoutClasses()}
272-
style={getContainerStyle()}
273-
>
147+
<div className={containerClass}>
274148
{testimonials.map((testimonial) => (
275-
<div
276-
key={testimonial.id}
277-
className={getCardClasses()}
278-
style={getCardStyle(testimonial)}
279-
>
149+
<div key={testimonial.id}>
280150
<TestimonialCard testimonial={testimonial} />
281151
</div>
282152
))}

0 commit comments

Comments
 (0)