Skip to content

Commit c0c9ba9

Browse files
committed
Fix URL Updates
1 parent 9e11583 commit c0c9ba9

2 files changed

Lines changed: 113 additions & 32 deletions

File tree

‎src/App.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import { SearchPage } from './pages/SearchPage';
77
import { ReaderPage } from './pages/ReaderPage';
88

99
function App() {
10+
const basename = import.meta.env.DEV ? '/' : '/ToK-Reader';
11+
1012
return (
1113
<ErrorBoundary>
1214
<DatabaseProvider>
13-
<BrowserRouter basename="/ToK-Reader">
15+
<BrowserRouter basename={basename}>
1416
<Routes>
1517
<Route path="/" element={<Layout />}>
1618
<Route index element={<HomePage />} />

‎src/pages/ReaderPage.tsx‎

Lines changed: 110 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useState, useEffect } from 'react';
2-
import { useParams } from 'react-router-dom';
1+
import { useState, useEffect, useRef } from 'react';
2+
import { useParams, useNavigate } from 'react-router-dom';
33
import { useDatabase } from '@/hooks/useDatabase';
44
import { getUtteranceContext, getUtteranceChain } from '@/lib/database/queries';
55
import { UtteranceCard } from '@/components/reader/UtteranceCard';
@@ -13,12 +13,18 @@ export function ReaderPage() {
1313
utteranceId: string;
1414
}>();
1515
const year = parseInt(yearParam || '1920');
16+
const navigate = useNavigate();
1617

1718
const { loading: dbLoading, error: dbError } = useDatabase(year);
1819
const [utterances, setUtterances] = useState<UtteranceWithPerson[]>([]);
1920
const [loading, setLoading] = useState(true);
2021
const [loadingMore, setLoadingMore] = useState({ top: false, bottom: false });
2122

23+
const topSentinelRef = useRef<HTMLDivElement>(null);
24+
const bottomSentinelRef = useRef<HTMLDivElement>(null);
25+
const containerRef = useRef<HTMLDivElement>(null);
26+
const utteranceRefs = useRef<Map<string, HTMLDivElement>>(new Map());
27+
2228
// Load initial context
2329
useEffect(() => {
2430
if (dbLoading !== 'success' || !utteranceId) return;
@@ -112,6 +118,77 @@ export function ReaderPage() {
112118
}
113119
};
114120

121+
// Intersection Observer for infinite scroll
122+
useEffect(() => {
123+
if (!topSentinelRef.current || !bottomSentinelRef.current) return;
124+
125+
const observer = new IntersectionObserver(
126+
(entries) => {
127+
entries.forEach((entry) => {
128+
if (entry.isIntersecting) {
129+
if (entry.target === topSentinelRef.current) {
130+
loadPrevious();
131+
} else if (entry.target === bottomSentinelRef.current) {
132+
loadNext();
133+
}
134+
}
135+
});
136+
},
137+
{ threshold: 0.1, rootMargin: '200px' }
138+
);
139+
140+
observer.observe(topSentinelRef.current);
141+
observer.observe(bottomSentinelRef.current);
142+
143+
return () => observer.disconnect();
144+
}, [utterances, loadingMore]);
145+
146+
// Update URL based on visible utterance
147+
useEffect(() => {
148+
if (utterances.length === 0) return;
149+
150+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
151+
152+
const handleScroll = () => {
153+
if (timeoutId) clearTimeout(timeoutId);
154+
155+
timeoutId = setTimeout(() => {
156+
// Find the utterance closest to the center of the viewport
157+
const viewportCenter = window.innerHeight / 2;
158+
let closestUtterance: UtteranceWithPerson | null = null;
159+
let closestDistance = Infinity;
160+
161+
utterances.forEach((utterance) => {
162+
const element = utteranceRefs.current.get(utterance.id);
163+
if (!element) return;
164+
165+
const rect = element.getBoundingClientRect();
166+
const elementCenter = rect.top + rect.height / 2;
167+
const distance = Math.abs(elementCenter - viewportCenter);
168+
169+
if (distance < closestDistance) {
170+
closestDistance = distance;
171+
closestUtterance = utterance;
172+
}
173+
});
174+
175+
if (closestUtterance !== null) {
176+
const newUtteranceId = (closestUtterance as UtteranceWithPerson).id;
177+
if (newUtteranceId !== utteranceId) {
178+
// Update URL without triggering navigation
179+
navigate(`/${year}/${newUtteranceId}`, { replace: true });
180+
}
181+
}
182+
}, config.scrollDebounceMs);
183+
};
184+
185+
window.addEventListener('scroll', handleScroll);
186+
return () => {
187+
window.removeEventListener('scroll', handleScroll);
188+
if (timeoutId) clearTimeout(timeoutId);
189+
};
190+
}, [utterances, year, utteranceId, navigate]);
191+
115192
if (dbLoading === 'loading') {
116193
return <LoadingSpinner message={`Loading database for year ${year}...`} />;
117194
}
@@ -137,7 +214,7 @@ export function ReaderPage() {
137214
}
138215

139216
return (
140-
<div className="max-w-4xl mx-auto">
217+
<div ref={containerRef} className="max-w-4xl mx-auto">
141218
<div className="mb-6">
142219
<h1 className="text-3xl font-bold text-gray-800 mb-2">
143220
Parliamentary Debate Reader
@@ -147,42 +224,44 @@ export function ReaderPage() {
147224
</p>
148225
</div>
149226

150-
{/* Load Previous Button */}
151-
{utterances[0]?.prev && (
152-
<div className="mb-4 text-center">
153-
<button
154-
onClick={loadPrevious}
155-
disabled={loadingMore.top}
156-
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
157-
>
158-
{loadingMore.top ? 'Loading...' : 'Load Previous'}
159-
</button>
160-
</div>
161-
)}
227+
{/* Top sentinel for infinite scroll */}
228+
<div ref={topSentinelRef} className="h-4">
229+
{loadingMore.top && (
230+
<div className="text-center py-2">
231+
<LoadingSpinner message="Loading previous..." />
232+
</div>
233+
)}
234+
</div>
162235

163236
{/* Utterances */}
164237
<div className="space-y-4">
165238
{utterances.map((utterance) => (
166-
<UtteranceCard
239+
<div
167240
key={utterance.id}
168-
utterance={utterance}
169-
highlighted={utterance.id === utteranceId}
170-
/>
241+
ref={(el) => {
242+
if (el) {
243+
utteranceRefs.current.set(utterance.id, el);
244+
} else {
245+
utteranceRefs.current.delete(utterance.id);
246+
}
247+
}}
248+
>
249+
<UtteranceCard
250+
utterance={utterance}
251+
highlighted={utterance.id === utteranceId}
252+
/>
253+
</div>
171254
))}
172255
</div>
173256

174-
{/* Load Next Button */}
175-
{utterances[utterances.length - 1]?.next && (
176-
<div className="mt-4 text-center">
177-
<button
178-
onClick={loadNext}
179-
disabled={loadingMore.bottom}
180-
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
181-
>
182-
{loadingMore.bottom ? 'Loading...' : 'Load Next'}
183-
</button>
184-
</div>
185-
)}
257+
{/* Bottom sentinel for infinite scroll */}
258+
<div ref={bottomSentinelRef} className="h-4">
259+
{loadingMore.bottom && (
260+
<div className="text-center py-2">
261+
<LoadingSpinner message="Loading next..." />
262+
</div>
263+
)}
264+
</div>
186265
</div>
187266
);
188267
}

0 commit comments

Comments
 (0)