···11-import React, { useState, useEffect } from 'react';
11+import React, { useState, useEffect, useCallback } from 'react';
22import { useParams, useNavigate } from 'react-router-dom';
33import SearchBar from '../SearchBar/SearchBar';
44import FeedTimeline from './FeedTimeline';
···77import { resolveHandleToDid, getServiceEndpointForDid } from '../../accountData';
88import MatterLoadingAnimation from '../MatterLoadingAnimation';
99import { Helmet } from 'react-helmet';
1010+import { useAuth } from '../../contexts/AuthContext';
10111112const CollectionsFeed = () => {
1213 const { username } = useParams();
1314 const navigate = useNavigate();
1515+ const { isAuthenticated, checkAuthStatus } = useAuth();
14161517 // State variables
1618 const [handle, setHandle] = useState(username || '');
···3133 const [dropdownOpen, setDropdownOpen] = useState(false);
3234 const [useRkeyTimestamp, setUseRkeyTimestamp] = useState(false);
3335 const [compactView, setCompactView] = useState(false);
3434-3535- // Effect to load data if username is provided in URL
3636- useEffect(() => {
3737- if (username) {
3838- loadUserData(username);
3939- }
4040- }, [username]);
4141-4242- // Effect to watch for selected collections changes
4343- useEffect(() => {
4444- // Only trigger a data fetch when filters change if we haven't fetched enough data previously
4545- if (selectedCollections.length > 0 && did && serviceEndpoint && searchPerformed) {
4646- const hasUnfetchedCollections = selectedCollections.some(col =>
4747- !allRecordsForChart.some(record => record.collection === col)
4848- );
4949-5050- if (hasUnfetchedCollections) {
5151- // Only fetch collections we haven't fetched before
5252- const collectionsToFetch = selectedCollections.filter(col =>
5353- !allRecordsForChart.some(record => record.collection === col)
5454- );
5555-5656- if (collectionsToFetch.length > 0) {
5757- console.log(`Fetching data for new collections: ${collectionsToFetch.join(', ')}`);
5858- fetchCollectionRecords(did, serviceEndpoint, collectionsToFetch);
5959- }
6060- } else {
6161- console.log('All collections already fetched, just filtering existing data');
6262- }
6363- }
6464- }, [selectedCollections]);
6565-6666- // Function to load user data
6767- const loadUserData = async (userHandle) => {
6868- try {
6969- setLoading(true);
7070- setError('');
7171-7272- // Update URL with the username
7373- if (userHandle !== username) {
7474- navigate(`/omnifeed/${encodeURIComponent(userHandle)}`);
7575- }
7676-7777- // Resolve handle to DID
7878- const userDid = await resolveHandleToDid(userHandle);
7979- setDid(userDid);
8080-8181- // Get service endpoint
8282- const endpoint = await getServiceEndpointForDid(userDid);
8383- setServiceEndpoint(endpoint);
8484-8585- // Fetch profile information
8686- const publicApiEndpoint = "https://public.api.bsky.app";
8787- const profileResponse = await fetch(`${publicApiEndpoint}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(userDid)}`);
8888-8989- if (!profileResponse.ok) {
9090- throw new Error(`Error fetching profile: ${profileResponse.statusText}`);
9191- }
9292-9393- const profileData = await profileResponse.json();
9494- setHandle(profileData.handle);
9595- setDisplayName(profileData.displayName || profileData.handle);
9696-9797- // Fetch repo description to get collections
9898- const repoResponse = await fetch(`${endpoint}/xrpc/com.atproto.repo.describeRepo?repo=${encodeURIComponent(userDid)}`);
9999-100100- if (!repoResponse.ok) {
101101- throw new Error(`Error fetching repo description: ${repoResponse.statusText}`);
102102- }
103103-104104- const repoData = await repoResponse.json();
105105- if (repoData.collections && repoData.collections.length > 0) {
106106- const sortedCollections = [...repoData.collections].sort();
107107- setCollections(sortedCollections);
108108- // By default, select all collections
109109- setSelectedCollections(sortedCollections);
110110-111111- // Fetch records for each collection
112112- await fetchCollectionRecords(userDid, endpoint, sortedCollections);
113113- } else {
114114- setError('No collections found for this user.');
115115- }
116116-117117- setSearchPerformed(true);
118118- setInitialLoad(false);
119119- setLoading(false);
120120- } catch (err) {
121121- console.error('Error loading user data:', err);
122122- setError(err.message || 'An error occurred while loading data.');
123123- setInitialLoad(false);
124124- setLoading(false);
125125- }
126126- };
3636+ const [displayCount, setDisplayCount] = useState(25);
12737128128- // Helper function to decode TID (rkey) to timestamp
3838+ // Helper functions
12939 const tidToTimestamp = (tid) => {
13040 try {
13141 // TIDs use a custom base32 encoding
···15464 }
15565 };
15666157157- // Helper function to extract timestamp from record
15867 const extractTimestamp = (record) => {
15968 // First check if createdAt exists directly in the value
16069 if (record.value?.createdAt) {
···192101 return findTimestamp(record);
193102 };
194103195195- // Function to fetch records from collections
196196- const fetchCollectionRecords = async (userDid, endpoint, collectionsList, isLoadMore = false) => {
104104+ // Define fetchCollectionRecords with useCallback first
105105+ const fetchCollectionRecords = useCallback(async (userDid, endpoint, collectionsList, isLoadMore = false) => {
197106 try {
198107 setFetchingMore(isLoadMore);
199108200109 // Set chartLoading for initial load or when refreshing
201110 if (!isLoadMore || collectionsList.length > 0) {
202111 setChartLoading(true);
203203- console.log(`Setting chart loading to TRUE for ${isLoadMore ? 'refresh' : 'initial load'}`);
204112 }
205113206206- // Array to store all fetched records
114114+ // Arrays to store all fetched records
207115 let allRecords = isLoadMore ? [...records] : [];
208116 let allChartRecords = isLoadMore ? [...allRecordsForChart] : [];
209117 const newCursors = { ...collectionCursors };
···222130 let pageCount = 0;
223131 let collectionRecords = [];
224132 let reachedCutoff = false;
225225- let totalRecordsForCollection = 0;
226133227134 // For initial deep load, we need to paginate as many times as needed to get all historical data
228135 // For regular timeline browsing or load more, we just get one page
···230137 const maxPages = isInitialDeepLoad ? 1000 : 1;
231138232139 while (hasMoreRecords && pageCount < maxPages && !reachedCutoff) {
233233- // Fetch up to 100 records per page
234234- let url = `${endpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(userDid)}&collection=${encodeURIComponent(collection)}&limit=100`;
140140+ // Use our server-side API to fetch records
141141+ let url = `/api/collections/${encodeURIComponent(userDid)}/records?endpoint=${encodeURIComponent(endpoint)}&collection=${encodeURIComponent(collection)}&limit=100`;
235142236143 // Add cursor if we have one
237144 if (cursor) {
238145 url += `&cursor=${encodeURIComponent(cursor)}`;
239146 }
240147241241- console.log(`Fetching ${collection} page ${pageCount + 1}${cursor ? ' with cursor' : ''}`);
242242-243243- const response = await fetch(url);
148148+ const response = await fetch(url, {
149149+ credentials: 'include'
150150+ });
244151245152 if (!response.ok) {
153153+ if (response.status === 401) {
154154+ // Handle unauthorized
155155+ checkAuthStatus();
156156+ throw new Error('Authentication required. Please log in again.');
157157+ }
246158 console.error(`Error fetching records for ${collection}: ${response.statusText}`);
247159 break;
248160 }
···252164253165 // Process records from this page
254166 if (data.records && data.records.length > 0) {
255255- console.log(`Received ${data.records.length} records for ${collection} page ${pageCount}`);
256256- totalRecordsForCollection += data.records.length;
257257-258167 const processedRecords = data.records.map(record => {
259168 const contentTimestamp = extractTimestamp(record);
260169 const rkey = record.uri.split('/').pop();
···307216 // All records on this page are within our date range
308217 // OR we need to keep paginating through high-volume collections
309218 collectionRecords.push(...processedRecords);
310310-311311- // If we found some records close to the cutoff date but haven't reached it yet,
312312- // log this for debugging purposes
313313- if (oldestRecordTime < cutoffDate.getTime() + (7 * 24 * 60 * 60 * 1000)) { // within 7 days of cutoff
314314- console.log(` - Getting close to cutoff date, oldest record = ${new Date(oldestRecordTime).toISOString()}`);
315315- }
316219 }
317220 } else {
318221 // For regular browsing, include all records from the page
···341244 delete newCursors[collection];
342245 }
343246344344- console.log(`Finished fetching ${collection}: Retrieved ${totalRecordsForCollection} records in ${pageCount} pages`);
345345- console.log(` - After filtering: ${collectionRecords.length} records in 90-day window`);
346346- if (reachedCutoff) {
347347- console.log(` - Stopped because records older than 90 days were found`);
348348- } else if (!cursor) {
349349- console.log(` - Stopped because no more records were available`);
350350- } else if (pageCount >= maxPages) {
351351- console.log(` - Stopped because max page limit (${maxPages}) was reached`);
352352- }
353353-354247 // Add records to appropriate arrays
355248 allChartRecords = [...allChartRecords, ...collectionRecords];
356249357250 // For display timeline, we might want to be more selective
358251 if (isLoadMore || !isInitialDeepLoad) {
359252 allRecords = [...allRecords, ...collectionRecords];
360360- } else if (isInitialDeepLoad) {
361361- // For initial load, we'll filter later to just show the most recent
362362- // This keeps allRecords separate from chart data until we're done
363253 }
364254 }
365255···391281 displayRecords = filterAndSort(allRecords);
392282 }
393283394394- // Create a summary of records per collection for debugging
395395- const collectionSummary = {};
396396- sortedChartRecords.forEach(record => {
397397- if (!collectionSummary[record.collection]) {
398398- collectionSummary[record.collection] = 0;
399399- }
400400- collectionSummary[record.collection]++;
401401- });
402402-403403- console.log("Collection record counts in 90-day period:");
404404- Object.entries(collectionSummary)
405405- .sort((a, b) => b[1] - a[1]) // Sort by count descending
406406- .forEach(([collection, count]) => {
407407- console.log(` - ${collection}: ${count} records`);
408408- });
409409-410410- console.log(`Fetched ${sortedChartRecords.length} total records for chart, showing ${displayRecords.length} in timeline`);
411411-412284 // In the case of a refresh, sortedChartRecords only contains fresh data for selected collections
413285 // We need to merge this with any existing data for other collections
414286 const existingRecordsToKeep = isLoadMore ? [] : allRecordsForChart.filter(record =>
···420292421293 // For refresh, remove old data for the collections we just refreshed
422294 if (!isLoadMore) {
423423- console.log(`Removing old data for refreshed collections: ${collectionsList.join(', ')}`);
424424-425425- // Count for stats
426426- const beforeCount = existingRecordsToKeep.length + sortedChartRecords.length;
427427-428295 // Remove duplicates that might exist in both arrays
429296 // This can happen if we refreshed a collection we already had data for
430297 const uniqueNewRecords = sortedChartRecords.filter(newRecord => {
···436303437304 // Merge fresh data with existing data for other collections
438305 mergedRecords = [...existingRecordsToKeep, ...uniqueNewRecords];
439439- console.log(`Merged ${existingRecordsToKeep.length} existing records with ${uniqueNewRecords.length} new unique records`);
440440- console.log(`Total records: ${beforeCount} before deduplication, ${mergedRecords.length} after`);
441306 }
442307 else {
443308 // For load more, just add all the new records
444309 mergedRecords = [...existingRecordsToKeep, ...sortedChartRecords];
445310 }
446311447447- console.log(`Final chart dataset size: ${mergedRecords.length} records`);
448448-449312 // Update state
450313 setRecords(displayRecords);
451314 setAllRecordsForChart(mergedRecords);
···454317455318 // Always set chartLoading to false when done, regardless of initial state
456319 setChartLoading(false);
457457- console.log("Setting chart loading to FALSE");
458320459321 } catch (err) {
460322 console.error('Error fetching collection records:', err);
461323 setError('Failed to fetch records. Please try again.');
462324 setFetchingMore(false);
463325 setChartLoading(false); // Always reset on error
464464- console.log("Setting chart loading to FALSE (error case)");
326326+ throw err; // Re-throw to allow handling in calling functions
327327+ }
328328+ }, [records, allRecordsForChart, collectionCursors, useRkeyTimestamp, checkAuthStatus]);
329329+330330+ // Now define loadUserData after fetchCollectionRecords is defined
331331+ const loadUserData = useCallback(async (userHandle) => {
332332+ try {
333333+ setLoading(true);
334334+ setError('');
335335+336336+ // Update URL with the username
337337+ if (userHandle !== username) {
338338+ navigate(`/omnifeed/${encodeURIComponent(userHandle)}`);
339339+ }
340340+341341+ // Resolve handle to DID
342342+ const userDid = await resolveHandleToDid(userHandle);
343343+ setDid(userDid);
344344+345345+ // Get service endpoint
346346+ const endpoint = await getServiceEndpointForDid(userDid);
347347+ setServiceEndpoint(endpoint);
348348+349349+ // Fetch profile information
350350+ const publicApiEndpoint = "https://public.api.bsky.app";
351351+ const profileResponse = await fetch(`${publicApiEndpoint}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(userDid)}`);
352352+353353+ if (!profileResponse.ok) {
354354+ throw new Error(`Error fetching profile: ${profileResponse.statusText}`);
355355+ }
356356+357357+ const profileData = await profileResponse.json();
358358+ setHandle(profileData.handle);
359359+ setDisplayName(profileData.displayName || profileData.handle);
360360+361361+ // Use our server-side API to fetch collections
362362+ const collectionsResponse = await fetch(`/api/collections/${encodeURIComponent(userDid)}?endpoint=${encodeURIComponent(endpoint)}`, {
363363+ credentials: 'include'
364364+ });
365365+366366+ if (!collectionsResponse.ok) {
367367+ if (collectionsResponse.status === 401) {
368368+ // Handle unauthorized
369369+ checkAuthStatus();
370370+ throw new Error('Authentication required. Please log in again.');
371371+ }
372372+ throw new Error(`Error fetching collections: ${collectionsResponse.statusText}`);
373373+ }
374374+375375+ const collectionsData = await collectionsResponse.json();
376376+377377+ if (collectionsData.collections && collectionsData.collections.length > 0) {
378378+ const sortedCollections = [...collectionsData.collections].sort();
379379+ setCollections(sortedCollections);
380380+ // By default, select all collections
381381+ setSelectedCollections(sortedCollections);
382382+383383+ // Fetch records for each collection
384384+ await fetchCollectionRecords(userDid, endpoint, sortedCollections);
385385+ } else {
386386+ setError('No collections found for this user.');
387387+ }
388388+389389+ setSearchPerformed(true);
390390+ setInitialLoad(false);
391391+ setLoading(false);
392392+ } catch (err) {
393393+ console.error('Error loading user data:', err);
394394+ setError(err.message || 'An error occurred while loading data.');
395395+ setInitialLoad(false);
396396+ setLoading(false);
397397+ }
398398+ }, [username, navigate, checkAuthStatus, fetchCollectionRecords]);
399399+400400+ // Now place useEffects after all the callbacks are defined
401401+402402+ // Verify authentication first
403403+ useEffect(() => {
404404+ const verifyAuth = async () => {
405405+ try {
406406+ await checkAuthStatus();
407407+ if (!isAuthenticated) {
408408+ // Save the current path for redirect after login
409409+ const returnUrl = encodeURIComponent(window.location.pathname);
410410+ navigate(`/login?returnUrl=${returnUrl}`);
411411+ }
412412+ } catch (err) {
413413+ console.error('Auth verification failed:', err);
414414+ navigate('/login');
415415+ }
416416+ };
417417+418418+ verifyAuth();
419419+420420+ // Set up periodic auth checks
421421+ const interval = setInterval(checkAuthStatus, 30000); // Check every 30 seconds
422422+423423+ return () => clearInterval(interval);
424424+ }, [isAuthenticated, checkAuthStatus, navigate]);
425425+426426+ // Effect to load data if username is provided in URL
427427+ useEffect(() => {
428428+ // Only load data if authenticated and username is available
429429+ if (username && isAuthenticated) {
430430+ loadUserData(username);
431431+ }
432432+ }, [username, isAuthenticated, loadUserData]);
433433+434434+ // Effect to watch for selected collections changes
435435+ useEffect(() => {
436436+ // Only trigger a data fetch when filters change if we haven't fetched enough data previously
437437+ if (selectedCollections.length > 0 && did && serviceEndpoint && searchPerformed) {
438438+ const hasUnfetchedCollections = selectedCollections.some(col =>
439439+ !allRecordsForChart.some(record => record.collection === col)
440440+ );
441441+442442+ if (hasUnfetchedCollections) {
443443+ // Only fetch collections we haven't fetched before
444444+ const collectionsToFetch = selectedCollections.filter(col =>
445445+ !allRecordsForChart.some(record => record.collection === col)
446446+ );
447447+448448+ if (collectionsToFetch.length > 0) {
449449+ console.log(`Fetching data for new collections: ${collectionsToFetch.join(', ')}`);
450450+ fetchCollectionRecords(did, serviceEndpoint, collectionsToFetch);
451451+ }
452452+ } else {
453453+ console.log('All collections already fetched, just filtering existing data');
454454+ }
465455 }
466466- };
456456+ }, [selectedCollections, did, serviceEndpoint, searchPerformed, allRecordsForChart, fetchCollectionRecords]);
467457468458 // Toggle collection selection
469459 const toggleCollection = (collection) => {
···498488 // Handle refresh button click - only updates the feed, not the chart
499489 const handleRefresh = async () => {
500490 if (did && serviceEndpoint) {
501501- console.log("Refresh requested for selected collections:", selectedCollections);
502502-503491 // Set a loading state but not for the chart
504492 setLoading(true);
505493···514502 // Process each selected collection sequentially
515503 for (const collection of selectedCollections) {
516504 try {
517517- // Fetch just one page of the most recent records
518518- const url = `${serviceEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent(collection)}&limit=25`;
505505+ // Use server-side API endpoint for fetching records
506506+ const url = `/api/collections/${encodeURIComponent(did)}/records?endpoint=${encodeURIComponent(serviceEndpoint)}&collection=${encodeURIComponent(collection)}&limit=25`;
519507520520- const response = await fetch(url);
508508+ const response = await fetch(url, {
509509+ credentials: 'include'
510510+ });
521511522512 if (!response.ok) {
513513+ if (response.status === 401) {
514514+ // Handle unauthorized
515515+ checkAuthStatus();
516516+ throw new Error('Authentication required. Please log in again.');
517517+ }
523518 console.error(`Error refreshing ${collection}: ${response.statusText}`);
524519 continue; // Skip this collection but continue with others
525520 }
···527522 const data = await response.json();
528523529524 if (data.records && data.records.length > 0) {
530530- console.log(`Refreshed ${data.records.length} records for ${collection}`);
531531-532525 // Process the records with timestamps
533526 const processedRecords = data.records.map(record => {
534527 const contentTimestamp = extractTimestamp(record);
···569562570563 // Only update the feed display records, not the chart data
571564 setRecords(sortedRecords.slice(0, 25));
572572- console.log(`Feed refreshed with ${sortedRecords.length} records`);
573565 };
574566575567 await refreshOnlyFeed();
576576- console.log("Feed refresh completed successfully");
577568 } catch (err) {
578569 console.error("Error during feed refresh:", err);
579570 setError('Failed to refresh records. Please try again.');
···586577587578 // Handle load more button click
588579 const handleLoadMore = async () => {
580580+ // Verify authentication before proceeding
581581+ if (!isAuthenticated) {
582582+ checkAuthStatus();
583583+ return;
584584+ }
585585+589586 setFetchingMore(true);
590587591588 // First check if we already have more records locally that we can show
592589 if (hasMoreRecordsLocally) {
593593- console.log("Loading more records from local cache");
594590 // Simply increase the display count by 25 more records
595591 const nextBatchSize = 25;
596592 setDisplayCount(prevCount => prevCount + nextBatchSize);
597597- console.log(`Increasing display count to ${displayCount + nextBatchSize}`);
598593599594 setFetchingMore(false);
600595 }
601596 // If we've displayed all local records but have cursors to fetch more from the API
602597 else if (hasMoreRecordsRemotely) {
603603- console.log("Fetching more records from API");
604598 // Only load more from collections that have cursors and are selected
605599 const collectionsToLoad = selectedCollections.filter(collection => collectionCursors[collection]);
606600607601 if (collectionsToLoad.length > 0) {
608608- await fetchCollectionRecords(did, serviceEndpoint, collectionsToLoad, true);
609609- // After fetching more, we can increase the display count to show them
610610- setDisplayCount(prevCount => prevCount + 25);
602602+ try {
603603+ await fetchCollectionRecords(did, serviceEndpoint, collectionsToLoad, true);
604604+ // After fetching more, we can increase the display count to show them
605605+ setDisplayCount(prevCount => prevCount + 25);
606606+ } catch (error) {
607607+ if (error.message?.includes('Authentication required')) {
608608+ // Handle authentication errors
609609+ const returnUrl = encodeURIComponent(window.location.pathname);
610610+ navigate(`/login?returnUrl=${returnUrl}`);
611611+ } else {
612612+ setError('Failed to load more records. Please try again.');
613613+ }
614614+ }
611615 } else {
612616 setFetchingMore(false);
613617 }
614618 } else {
615615- console.log("No more records to load");
616619 setFetchingMore(false);
617620 }
618621 };
···622625 selectedCollections.includes(record.collection)
623626 );
624627625625- // State to track how many records to display
626626- const [displayCount, setDisplayCount] = useState(25);
627627-628628 // For timeline display, directly use the chart records but limit to the current displayCount
629629 // This ensures we always show the most recent records for the selected collections
630630 const filteredRecords = filteredChartRecords
···866866867867 // Refresh the feed with the new timestamp setting
868868 if (did && serviceEndpoint && selectedCollections.length > 0) {
869869- // We need to refetch to ensure we get all records
870870- // Store the current mode for fetchCollectionRecords
871871- const currentMode = useRkeyTimestamp;
872872-873869 // Temporarily reset records for the loading state
874870 const currentRecords = [...records];
875871 setRecords([]);
876872 setLoading(true);
877873878878- // Fetch new records with the current selection
879879- fetchCollectionRecords(did, serviceEndpoint, selectedCollections)
874874+ // Use our refreshed server-side approach
875875+ handleRefresh()
880876 .catch(err => {
881877 console.error("Error refreshing with new timestamp mode:", err);
878878+882879 // Restore the previous records and sort them
883880 const sorted = [...currentRecords].filter(record => {
884881 if (newTimestampMode) { // We're switching to rkey timestamps
+25-6
src/components/Login/Login.js
···11-import React, { useState } from 'react';
22-import { useNavigate } from 'react-router-dom';
11+import React, { useState, useEffect } from 'react';
22+import { useNavigate, useLocation } from 'react-router-dom';
33import { useAuth } from '../../contexts/AuthContext';
44import './Login.css';
55···77 const [handle, setHandle] = useState('');
88 const [isLoading, setIsLoading] = useState(false);
99 const [error, setError] = useState('');
1010+ const [returnUrl, setReturnUrl] = useState('');
1011 const { login, isAuthenticated } = useAuth();
1112 const navigate = useNavigate();
1313+ const location = useLocation();
1414+1515+ // Extract returnUrl from query params
1616+ useEffect(() => {
1717+ const searchParams = new URLSearchParams(location.search);
1818+ const returnPath = searchParams.get('returnUrl');
1919+ if (returnPath) {
2020+ setReturnUrl(returnPath);
2121+ }
2222+ }, [location]);
12231324 // Redirect if already authenticated
1414- React.useEffect(() => {
2525+ useEffect(() => {
1526 if (isAuthenticated) {
1616- navigate('/');
2727+ // Navigate to return URL if it exists, otherwise to home
2828+ navigate(returnUrl || '/');
1729 }
1818- }, [isAuthenticated, navigate]);
3030+ }, [isAuthenticated, navigate, returnUrl]);
19312032 const handleSubmit = async (e) => {
2133 e.preventDefault();
···2941 setError('');
30423143 try {
3232- await login(handle);
4444+ // Pass returnUrl to login function
4545+ await login(handle, returnUrl);
3346 // Note: This code won't run because login redirects to Bluesky OAuth page
3447 } catch (err) {
3548 setError('Authentication failed. Please try again.');
···4255 <div className="login-card">
4356 <h2>Login with Bluesky</h2>
4457 <p>Sign in with your Bluesky handle to access protected features.</p>
5858+5959+ {returnUrl && (
6060+ <div className="return-notice">
6161+ <p>You'll be redirected back to the page you were trying to access after logging in.</p>
6262+ </div>
6363+ )}
45644665 {error && <div className="login-error">{error}</div>}
4766
+49-17
src/components/Login/LoginCallback.js
···11import React, { useEffect, useState } from 'react';
22-import { Navigate } from 'react-router-dom';
22+import { Navigate, useLocation } from 'react-router-dom';
33import { useAuth } from '../../contexts/AuthContext';
44import Loading from '../Loading/Loading';
5566// This component handles the callback redirect from the Bluesky OAuth process
77const LoginCallback = () => {
88- const { loading } = useAuth();
88+ const { loading, checkAuthStatus } = useAuth();
99 const [error, setError] = useState(null);
1010+ const [returnUrl, setReturnUrl] = useState('/');
1111+ const location = useLocation();
10121113 useEffect(() => {
1212- // The actual callback handling is done in the AuthContext.js
1313- // through the client.init() method that automatically processes
1414- // the URL params when the page loads
1515-1616- // We just check if there are any errors in the URL
1717- const urlParams = new URLSearchParams(window.location.search);
1818- const errorParam = urlParams.get('error');
1919- const errorDescription = urlParams.get('error_description');
2020-2121- if (errorParam) {
2222- setError(errorDescription || errorParam);
2323- }
2424- }, []);
1414+ const handleCallback = async () => {
1515+ try {
1616+ // Get return URL from session storage or state parameter
1717+ const sessionReturnUrl = sessionStorage.getItem('returnUrl');
1818+ const params = new URLSearchParams(location.search);
1919+ const stateParam = params.get('state');
2020+2121+ // If state contains encoded returnUrl, extract it
2222+ let decodedState = null;
2323+ if (stateParam) {
2424+ try {
2525+ decodedState = JSON.parse(atob(stateParam));
2626+ if (decodedState && decodedState.returnUrl) {
2727+ setReturnUrl(decodedState.returnUrl);
2828+ }
2929+ } catch (e) {
3030+ console.error('Failed to decode state parameter:', e);
3131+ }
3232+ }
3333+3434+ // Prioritize returnUrl from session storage if available
3535+ if (sessionReturnUrl) {
3636+ setReturnUrl(sessionReturnUrl);
3737+ sessionStorage.removeItem('returnUrl');
3838+ }
3939+4040+ // Check for error in URL parameters
4141+ const errorParam = params.get('error');
4242+ if (errorParam) {
4343+ setError(errorParam);
4444+ return;
4545+ }
4646+4747+ // Check server-side authentication status
4848+ await checkAuthStatus();
4949+ } catch (err) {
5050+ console.error('Error handling login callback:', err);
5151+ setError('Failed to complete login process');
5252+ }
5353+ };
5454+5555+ handleCallback();
5656+ }, [location, checkAuthStatus]);
25572658 if (loading) {
2759 return <Loading message="Processing login..." />;
···3971 );
4072 }
41734242- // Redirect to the home page if no errors
4343- return <Navigate to="/" replace />;
7474+ // Redirect to the return URL (or home by default)
7575+ return <Navigate to={returnUrl} replace />;
4476};
45774678export default LoginCallback;
+10-2
src/components/ProtectedRoute.js
···11-import React from 'react';
11+import React, { useEffect } from 'react';
22import { Navigate } from 'react-router-dom';
33import { useAuth } from '../contexts/AuthContext';
44import { isAccountAllowed } from '../config/allowlist';
···6677// Component to protect routes that require authentication
88const ProtectedRoute = ({ children }) => {
99- const { isAuthenticated, loading, session } = useAuth();
99+ const { isAuthenticated, loading, session, checkAuthStatus } = useAuth();
1010+1111+ useEffect(() => {
1212+ // Check auth status on mount and periodically
1313+ checkAuthStatus();
1414+ const interval = setInterval(checkAuthStatus, 30000); // Check every 30 seconds
1515+1616+ return () => clearInterval(interval);
1717+ }, [checkAuthStatus]);
10181119 // Show loading state while authentication is being checked
1220 if (loading) {
+13-4
src/contexts/AuthContext.js
···6969 }, []);
70707171 // Initiate the login process
7272- const login = async (handle) => {
7272+ const login = async (handle, returnUrl) => {
7373 if (!client) return;
74747575 try {
7676- // The signIn method will redirect the user to the OAuth server
7777- await client.signIn(handle);
7878- // This code won't execute as the page will be redirected
7676+ // Save returnUrl to session storage if provided
7777+ if (returnUrl) {
7878+ sessionStorage.setItem('returnUrl', returnUrl);
7979+ }
8080+8181+ // Create state parameter with returnUrl
8282+ const state = returnUrl ?
8383+ btoa(JSON.stringify({ returnUrl })) :
8484+ undefined;
8585+8686+ // Pass state parameter to signIn method
8787+ await client.signIn(handle, { state });
7988 } catch (err) {
8089 console.error('Login failed:', err);
8190 setError(err.message);