Skip to content

Commit 569822c

Browse files
committed
responsiveness for ipad
1 parent fb17931 commit 569822c

9 files changed

Lines changed: 141 additions & 66 deletions

File tree

src/App.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect } from 'react';
1+
import { useEffect, useRef } from 'react';
22
import { HashRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom';
33
import { Navigation } from './components/Navigation';
44
import { Footer } from './components/Footer';
@@ -21,11 +21,40 @@ import './utils/navHeight';
2121
function AppContent() {
2222
const location = useLocation();
2323
const isHomePage = location.pathname === '/' || location.pathname === '/#/';
24+
const prevPathnameRef = useRef<string>(location.pathname);
25+
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
2426

25-
// Scroll to top when opening About, Stories, Projects, etc. so the top nav "moves up" into view
27+
// Check if current route is a detail page (project or story detail)
28+
const isDetailPage = /^\/(projects|impact|storiesofadventure)\/[^/]+$/.test(location.pathname);
29+
30+
// Scroll to top on route changes - simplified, no interference with user scrolling
2631
useEffect(() => {
27-
window.scrollTo({ top: 0, behavior: 'smooth' });
28-
}, [location.pathname]);
32+
const pathnameChanged = prevPathnameRef.current !== location.pathname;
33+
34+
if (pathnameChanged) {
35+
// Clear any pending scroll timeouts
36+
if (scrollTimeoutRef.current) {
37+
clearTimeout(scrollTimeoutRef.current);
38+
}
39+
40+
// Always scroll to top for detail pages, otherwise only if not already scrolled
41+
if (isDetailPage || window.scrollY === 0) {
42+
// Use a small delay to ensure DOM is ready
43+
scrollTimeoutRef.current = setTimeout(() => {
44+
window.scrollTo({ top: 0, behavior: 'instant' });
45+
scrollTimeoutRef.current = null;
46+
}, 0);
47+
}
48+
49+
prevPathnameRef.current = location.pathname;
50+
}
51+
52+
return () => {
53+
if (scrollTimeoutRef.current) {
54+
clearTimeout(scrollTimeoutRef.current);
55+
}
56+
};
57+
}, [location.pathname, isDetailPage]);
2958

3059
// Disable right-click context menu on all pages
3160
useEffect(() => {

src/components/Navigation.tsx

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,22 @@ export function Navigation({ inline = false }: NavigationProps) {
1919
const tabRefs = useRef<(HTMLAnchorElement | null)[]>([]);
2020

2121
useEffect(() => {
22+
let rafId: number | null = null;
2223
const handleScroll = () => {
23-
setIsScrolled(window.scrollY > 20);
24+
// Use requestAnimationFrame to throttle scroll updates
25+
if (rafId === null) {
26+
rafId = requestAnimationFrame(() => {
27+
setIsScrolled(window.scrollY > 20);
28+
rafId = null;
29+
});
30+
}
2431
};
2532

26-
window.addEventListener('scroll', handleScroll);
27-
return () => window.removeEventListener('scroll', handleScroll);
33+
window.addEventListener('scroll', handleScroll, { passive: true });
34+
return () => {
35+
window.removeEventListener('scroll', handleScroll);
36+
if (rafId !== null) cancelAnimationFrame(rafId);
37+
};
2838
}, []);
2939

3040
// Center the active tab with equal tabs on both sides when possible
@@ -185,34 +195,34 @@ export function Navigation({ inline = false }: NavigationProps) {
185195
});
186196
}
187197

188-
// Update immediately and after layout
198+
// Debounced update function to prevent excessive calls
199+
let updateTimeout: NodeJS.Timeout | null = null;
200+
const debouncedUpdateNavHeight = () => {
201+
if (updateTimeout) clearTimeout(updateTimeout);
202+
updateTimeout = setTimeout(() => {
203+
updateNavHeight();
204+
updateTimeout = null;
205+
}, 16); // ~60fps throttling
206+
};
207+
208+
// Update immediately
189209
updateNavHeight();
190-
requestAnimationFrame(() => {
191-
updateNavHeight();
192-
requestAnimationFrame(updateNavHeight);
193-
});
194210

195-
// Update after delays to catch all render states
196-
const timeouts = [
197-
setTimeout(updateNavHeight, 0),
198-
setTimeout(updateNavHeight, 10),
199-
setTimeout(updateNavHeight, 50),
200-
setTimeout(updateNavHeight, 100),
201-
setTimeout(updateNavHeight, 200),
202-
setTimeout(updateNavHeight, 500)
203-
];
211+
// Single delayed check after layout settles
212+
const initialTimeout = setTimeout(updateNavHeight, 100);
204213

205-
window.addEventListener('resize', updateNavHeight);
206-
window.addEventListener('orientationchange', updateNavHeight);
214+
window.addEventListener('resize', debouncedUpdateNavHeight, { passive: true });
215+
window.addEventListener('orientationchange', debouncedUpdateNavHeight);
207216

208217
return () => {
209218
resizeObserver.disconnect();
210219
mutationObserver.disconnect();
211-
window.removeEventListener('resize', updateNavHeight);
212-
window.removeEventListener('orientationchange', updateNavHeight);
213-
timeouts.forEach(clearTimeout);
220+
window.removeEventListener('resize', debouncedUpdateNavHeight);
221+
window.removeEventListener('orientationchange', debouncedUpdateNavHeight);
222+
if (updateTimeout) clearTimeout(updateTimeout);
223+
clearTimeout(initialTimeout);
214224
};
215-
}, [isScrolled, inline, isMobileMenuOpen]); // CRITICAL: Include isMobileMenuOpen
225+
}, [inline, isMobileMenuOpen]); // Removed isScrolled - it was causing layout shifts during scroll
216226

217227
return (
218228
<motion.nav

src/components/pages/ProjectDetail.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,16 @@ export function ProjectDetail() {
3030
useEffect(() => {
3131
if (!selectedProject) {
3232
navigate('/projects');
33+
return;
3334
}
35+
36+
// Scroll to top when project detail loads
37+
// Use double RAF to ensure it happens after App.tsx scroll handler
38+
requestAnimationFrame(() => {
39+
requestAnimationFrame(() => {
40+
window.scrollTo({ top: 0, behavior: 'instant' });
41+
});
42+
});
3443
}, [selectedProject, navigate]);
3544

3645
// Update meta tags for social sharing

src/components/pages/StoriesOfAdventure.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ export function StoriesOfAdventure() {
215215
return (
216216
<div className="min-h-screen" ref={contentRef} data-content-wrapper="true">
217217
{/* Header */}
218-
<section className="pt-4 sm:pt-16 lg:pt-24 pb-20 lg:pb-24 bg-gradient-to-b from-background to-blue-50/20">
218+
<section className="pb-20 lg:pb-24 bg-gradient-to-b from-background to-blue-50/20">
219219
<div className="max-w-7xl mx-auto px-6 lg:px-12">
220220
<motion.div
221221
initial={{ opacity: 0, y: 30 }}

src/components/pages/StoryDetail.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,12 @@ export function StoryDetail() {
8484
}
8585

8686
// Scroll to top when story loads
87-
window.scrollTo({ top: 0, behavior: 'instant' });
87+
// Use double RAF to ensure it happens after App.tsx scroll handler
88+
requestAnimationFrame(() => {
89+
requestAnimationFrame(() => {
90+
window.scrollTo({ top: 0, behavior: 'instant' });
91+
});
92+
});
8893
}, [selectedStory, navigate, storyId]);
8994

9095
// Update meta tags for social sharing

src/components/pages/StoryOfAdventureDetail.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,12 @@ export function StoryOfAdventureDetail() {
9797
}
9898

9999
// Scroll to top when story loads
100-
window.scrollTo({ top: 0, behavior: 'instant' });
100+
// Use double RAF to ensure it happens after App.tsx scroll handler
101+
requestAnimationFrame(() => {
102+
requestAnimationFrame(() => {
103+
window.scrollTo({ top: 0, behavior: 'instant' });
104+
});
105+
});
101106

102107
// Update meta tags immediately for better crawler support
103108
const baseUrl = window.location.origin;

src/index.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2297,7 +2297,9 @@
22972297

22982298
html {
22992299
font-size: var(--font-size);
2300-
scroll-behavior: smooth;
2300+
/* Smooth scrolling disabled to prevent double-scroll issue */
2301+
/* scroll-behavior: smooth; - Disabled to fix double-scroll */
2302+
-webkit-overflow-scrolling: touch;
23012303
}
23022304

23032305
@property --tw-translate-x {

src/styles/globals.css

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@
133133
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display', 'Inter', system-ui, sans-serif;
134134
-webkit-font-smoothing: antialiased;
135135
-moz-osx-font-smoothing: grayscale;
136+
/* Smooth scrolling disabled to prevent double-scroll issue */
137+
/* scroll-behavior: smooth; - Disabled to fix double-scroll */
138+
-webkit-overflow-scrolling: touch;
139+
/* Better scroll performance */
140+
will-change: scroll-position;
136141
}
137142
}
138143

@@ -198,7 +203,25 @@
198203

199204
html {
200205
font-size: var(--font-size);
201-
scroll-behavior: smooth;
206+
/* Smooth scrolling disabled to prevent double-scroll issue */
207+
/* scroll-behavior: smooth; - Disabled to fix double-scroll */
208+
/* Improve scroll performance and smoothness */
209+
-webkit-overflow-scrolling: touch;
210+
}
211+
212+
/* Additional scroll optimizations for scrollable containers */
213+
.overflow-x-auto,
214+
.overflow-y-auto,
215+
.overflow-auto {
216+
-webkit-overflow-scrolling: touch;
217+
/* scroll-behavior: smooth; - Disabled to prevent double-scroll */
218+
}
219+
220+
/* Smooth scrolling for horizontal navigation containers */
221+
nav[aria-label] ul,
222+
nav[aria-label] div {
223+
/* scroll-behavior: smooth; - Disabled to prevent double-scroll */
224+
-webkit-overflow-scrolling: touch;
202225
}
203226

204227
/* Mobile-specific scrollbar hiding */

src/utils/useMobilePadding.ts

Lines changed: 26 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export function useMobilePadding() {
88
const contentRef = useRef<HTMLDivElement>(null);
99

1010
useEffect(() => {
11+
let timeoutId: NodeJS.Timeout | null = null;
12+
1113
const applyPadding = () => {
1214
if (!contentRef.current) return;
1315

@@ -24,61 +26,51 @@ export function useMobilePadding() {
2426
return;
2527
}
2628

27-
// Remove conflicting Tailwind classes
28-
el.classList.remove('pt-48', 'sm:pt-24', 'lg:pt-32');
29+
// Remove conflicting Tailwind classes that add padding-top
30+
el.classList.remove('pt-48', 'sm:pt-24', 'lg:pt-32', 'pt-4', 'sm:pt-16', 'lg:pt-24', 'pt-8', 'pt-12', 'pt-16', 'pt-20');
2931

3032
// Apply padding using CSS variable
3133
el.style.setProperty('padding-top', navHeight, 'important');
3234

33-
// Verify no overlap
34-
requestAnimationFrame(() => {
35-
const navEl = document.querySelector('nav');
36-
if (navEl && el) {
37-
const navRect = navEl.getBoundingClientRect();
38-
const elRect = el.getBoundingClientRect();
39-
const isOverlapping = elRect.top < navRect.bottom;
40-
41-
// If still overlapping, add extra buffer
42-
if (isOverlapping) {
43-
const neededPadding = navRect.bottom - elRect.top + 20; // 20px buffer
44-
const currentPadding = parseFloat(navHeight) || 0;
45-
el.style.setProperty('padding-top', `${Math.max(currentPadding, neededPadding)}px`, 'important');
46-
}
47-
}
48-
});
35+
// Removed dynamic overlap checking during scroll to prevent layout shifts
36+
// Padding is set once based on nav height and doesn't change during scroll
37+
};
38+
39+
// Debounced version for observers and events
40+
const debouncedApplyPadding = () => {
41+
if (timeoutId) clearTimeout(timeoutId);
42+
timeoutId = setTimeout(() => {
43+
applyPadding();
44+
timeoutId = null;
45+
}, 50);
4946
};
5047

5148
// Apply immediately
5249
applyPadding();
5350

54-
// Watch for CSS variable changes
51+
// Watch for CSS variable changes with debouncing
5552
const observer = new MutationObserver(() => {
56-
applyPadding();
53+
debouncedApplyPadding();
5754
});
5855

5956
observer.observe(document.documentElement, {
6057
attributes: true,
6158
attributeFilter: ['style', 'data-nav-height']
6259
});
6360

64-
// Also listen for resize/orientation changes
65-
window.addEventListener('resize', applyPadding);
66-
window.addEventListener('orientationchange', applyPadding);
61+
// Also listen for resize/orientation changes (debounced)
62+
window.addEventListener('resize', debouncedApplyPadding, { passive: true });
63+
window.addEventListener('orientationchange', debouncedApplyPadding);
6764

68-
// Apply after delays to catch all render states
69-
const timeouts = [
70-
setTimeout(applyPadding, 0),
71-
setTimeout(applyPadding, 50),
72-
setTimeout(applyPadding, 100),
73-
setTimeout(applyPadding, 200),
74-
setTimeout(applyPadding, 500)
75-
];
65+
// Single delayed check after initial render
66+
const initialTimeout = setTimeout(applyPadding, 100);
7667

7768
return () => {
7869
observer.disconnect();
79-
window.removeEventListener('resize', applyPadding);
80-
window.removeEventListener('orientationchange', applyPadding);
81-
timeouts.forEach(clearTimeout);
70+
window.removeEventListener('resize', debouncedApplyPadding);
71+
window.removeEventListener('orientationchange', debouncedApplyPadding);
72+
if (timeoutId) clearTimeout(timeoutId);
73+
clearTimeout(initialTimeout);
8274
};
8375
}, []);
8476

0 commit comments

Comments
 (0)