This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

Enhance CollectionsFeed component: Introduced content visibility state management for smoother loading transitions. Added a loading animation and refined error handling to improve user experience during data fetching. Updated styles for loading indicators and content display.

+260 -194
+22
src/components/CollectionsFeed/CollectionsFeed.css
··· 59 59 margin: 20px auto; 60 60 padding: 0 20px; 61 61 font-family: 'articulat-cf', sans-serif; 62 + position: relative; 63 + min-height: 80vh; 62 64 } 63 65 64 66 /* Alt Card Style (matching Alt Text Rating Tool) */ ··· 1263 1265 font-size: 12px; 1264 1266 overflow-x: auto; 1265 1267 white-space: pre-wrap; 1268 + } 1269 + 1270 + /* Matter Loading Animation Container */ 1271 + .matter-loading-container { 1272 + display: flex; 1273 + flex-direction: column; 1274 + align-items: center; 1275 + justify-content: center; 1276 + min-height: 80vh; 1277 + width: 100%; 1278 + } 1279 + 1280 + /* Add animation for content fade-in */ 1281 + @keyframes fadeIn { 1282 + from { opacity: 0; } 1283 + to { opacity: 1; } 1284 + } 1285 + 1286 + .fade-in { 1287 + animation: fadeIn 0.5s ease-in-out; 1266 1288 }
+238 -194
src/components/CollectionsFeed/CollectionsFeed.js
··· 37 37 const [displayCount, setDisplayCount] = useState(25); 38 38 const [debugInfo, setDebugInfo] = useState(null); 39 39 const [showDebug, setShowDebug] = useState(false); 40 + const [showContent, setShowContent] = useState(false); // Add state for content visibility 40 41 41 42 // Helper functions 42 43 const tidToTimestamp = (tid) => { ··· 365 366 366 367 // Reset state for new search 367 368 setLoading(true); 369 + setShowContent(false); // Hide content while loading 368 370 setError(''); 369 371 setDid(''); 370 372 setServiceEndpoint(''); ··· 521 523 setSearchPerformed(true); 522 524 setInitialLoad(false); 523 525 setLoading(false); 526 + // Add a slight delay before showing content for smooth transition 527 + setTimeout(() => setShowContent(true), 100); 524 528 } catch (err) { 525 529 console.error('Error loading user data:', err); 526 530 ··· 546 550 setError(userMessage); 547 551 setInitialLoad(false); 548 552 setLoading(false); 553 + setShowContent(true); // Show content even on error so user can see error message 549 554 } 550 555 }, [navigate, checkAuthStatus]); 551 556 ··· 556 561 const verifyAuth = async () => { 557 562 try { 558 563 setLoading(true); 564 + setInitialLoad(true); 565 + setShowContent(false); 566 + 567 + // Reset states if username changes 568 + if (username) { 569 + setError(''); 570 + setSearchTerm(username); 571 + } 572 + 559 573 const authResult = await checkAuthStatus(); 560 574 561 575 if (!authResult) { ··· 566 580 return; 567 581 } 568 582 569 - setLoading(false); 570 - 571 - // If we have a username in the URL and we're authenticated, load the data 583 + // If a username is provided in the URL, load that user's data 572 584 if (username && authResult) { 585 + console.log('Username provided in URL, loading data for:', username); 573 586 loadUserData(username); 587 + } else { 588 + // Only set loading to false if we're not loading a specific user 589 + setLoading(false); 590 + setInitialLoad(false); 591 + setShowContent(true); 574 592 } 575 593 } catch (err) { 576 594 console.error('Auth verification failed:', err); 577 595 setError('Authentication failed. Please try logging in again.'); 578 596 setLoading(false); 579 - navigate('/login'); 597 + setInitialLoad(false); 598 + setShowContent(true); 599 + 600 + // Add a delay before redirecting to show the error message 601 + setTimeout(() => { 602 + navigate('/login'); 603 + }, 2000); 580 604 } 581 605 }; 582 606 ··· 588 612 if (!authResult && isAuthenticated) { 589 613 // Session was lost during browsing 590 614 setError('Your session has expired. Please log in again.'); 615 + setInitialLoad(false); 591 616 // Show the error for 3 seconds before redirecting 592 617 setTimeout(() => { 593 618 const returnUrl = encodeURIComponent(window.location.pathname); ··· 596 621 } 597 622 }, 30000); // Check every 30 seconds 598 623 599 - return () => clearInterval(interval); 624 + // Safety timeout to ensure initialLoad is cleared after 10 seconds maximum 625 + const loadingTimeout = setTimeout(() => { 626 + setInitialLoad(false); 627 + }, 10000); 628 + 629 + return () => { 630 + clearInterval(interval); 631 + clearTimeout(loadingTimeout); 632 + }; 600 633 }, [isAuthenticated, checkAuthStatus, navigate, username, loadUserData]); 601 634 602 635 // Effect to watch for selected collections changes ··· 815 848 const handleSubmit = (e) => { 816 849 e.preventDefault(); 817 850 if (searchTerm.trim() !== '') { 818 - loadUserData(searchTerm.trim()); 851 + // Instead of directly loading the user data, navigate to their dedicated omnifeed page 852 + navigate(`/omnifeed/${searchTerm.trim()}`); 819 853 } 820 854 }; 821 855 ··· 848 882 <meta name="description" content={username ? `View ${username}'s AT Protocol collection records in chronological order` : 'View AT Protocol collection records in chronological order'} /> 849 883 </Helmet> 850 884 851 - <div className="search-container"> 852 - <h1>OmniFeed</h1> 853 - <p className="feed-description"> 854 - View all repository collections for a Bluesky user, including custom collections from AT Protocol apps. 855 - </p> 856 - 857 - {/* Authentication status banner */} 858 - {!isAuthenticated && ( 859 - <div className="auth-warning"> 860 - <p> 861 - <strong>Authentication Required:</strong> You need to be logged in to view the OmniFeed. 862 - Redirecting to login... 863 - </p> 864 - </div> 865 - )} 866 - 867 - <form onSubmit={handleSubmit} className="search-form"> 868 - <div className="search-box"> 869 - <input 870 - type="text" 871 - placeholder="Enter a Bluesky handle (e.g. cred.blue)" 872 - value={searchTerm} 873 - onChange={(e) => setSearchTerm(e.target.value)} 874 - disabled={loading} 875 - /> 876 - <button 877 - type="submit" 878 - disabled={loading || !searchTerm || searchTerm.trim() === ''} 879 - > 880 - {loading ? 'Loading...' : 'Search'} 881 - </button> 882 - </div> 883 - </form> 884 - 885 - {/* Error message with more styling and retry button */} 886 - {error && ( 887 - <div className="error-container"> 888 - <div className="error-message"> 889 - <p>{error}</p> 890 - {error.includes('authenticated') ? ( 891 - <button 892 - className="error-action-button" 893 - onClick={() => { 894 - const returnUrl = encodeURIComponent(window.location.pathname); 895 - navigate(`/login?returnUrl=${returnUrl}`); 896 - }} 897 - > 898 - Go to Login 899 - </button> 900 - ) : ( 901 - <button 902 - className="error-action-button" 903 - onClick={() => { 904 - setError(''); 905 - if (username) { 906 - loadUserData(username); 907 - } 908 - }} 909 - > 910 - Retry 911 - </button> 912 - )} 885 + {/* Display MatterLoadingAnimation for the main loading state when username is provided */} 886 + {username && loading && !error && ( 887 + <div className="matter-loading-container"> 888 + <MatterLoadingAnimation /> 889 + </div> 890 + )} 891 + 892 + {/* Only show the main content when not in full-screen loading mode */} 893 + {(!username || !loading || error) && ( 894 + <div className={`search-container ${showContent ? 'fade-in' : ''}`}> 895 + <h1>OmniFeed</h1> 896 + <p className="feed-description"> 897 + View all repository collections for a Bluesky user, including custom collections from AT Protocol apps. 898 + </p> 899 + 900 + {/* Authentication status banner */} 901 + {!isAuthenticated && ( 902 + <div className="auth-warning"> 903 + <p> 904 + <strong>Authentication Required:</strong> You need to be logged in to view the OmniFeed. 905 + Redirecting to login... 906 + </p> 913 907 </div> 914 - </div> 915 - )} 916 - 917 - {/* Initial loading state */} 918 - {initialLoad && !error && ( 919 - <div className="loading-indicator"> 920 - <div className="spinner"></div> 921 - <p>Connecting to AT Protocol services...</p> 922 - </div> 923 - )} 924 - 925 - {/* Main content once search is performed */} 926 - {searchPerformed && !initialLoad && ( 927 - <div className="user-info"> 928 - {displayName && ( 929 - <h2> 930 - Collections for {displayName} 931 - <span className="handle">@{handle}</span> 932 - </h2> 933 - )} 934 - 935 - {/* Collections count and timeframe */} 936 - {collections.length > 0 && ( 937 - <div className="collections-meta"> 938 - <p>{collections.length} collections found</p> 939 - <div className="time-toggle"> 940 - <label> 941 - <input 942 - type="checkbox" 943 - checked={useRkeyTimestamp} 944 - onChange={() => setUseRkeyTimestamp(!useRkeyTimestamp)} 945 - /> 946 - Use record IDs for timestamps 947 - </label> 948 - <span className="info-tooltip"> 949 - ? 950 - <span className="tooltip-text"> 951 - Toggle between using timestamps found within the content (more accurate) or derived from record IDs (complete coverage) 952 - </span> 953 - </span> 954 - </div> 955 - </div> 956 - )} 957 - 958 - {/* Collections filter area */} 959 - {collections.length > 0 && ( 960 - <div className="collections-filter"> 961 - <h3>Collections</h3> 962 - <div className="filter-actions"> 963 - <button onClick={selectAllCollections} className="select-all">Select All</button> 964 - <button onClick={deselectAllCollections} className="deselect-all">Deselect All</button> 965 - <button onClick={handleRefresh} className="refresh-button" disabled={loading || fetchingMore}> 966 - {loading ? 'Refreshing...' : 'Refresh'} 908 + )} 909 + 910 + <form onSubmit={handleSubmit} className="search-form"> 911 + <div className="search-box"> 912 + <input 913 + type="text" 914 + placeholder="Enter a Bluesky handle (e.g. cred.blue)" 915 + value={searchTerm} 916 + onChange={(e) => setSearchTerm(e.target.value)} 917 + disabled={loading} 918 + /> 919 + <button 920 + type="submit" 921 + disabled={loading || !searchTerm || searchTerm.trim() === ''} 922 + > 923 + {loading ? 'Loading...' : 'Search'} 924 + </button> 925 + </div> 926 + </form> 927 + 928 + {/* Error message with more styling and retry button */} 929 + {error && ( 930 + <div className="error-container"> 931 + <div className="error-message"> 932 + <p>{error}</p> 933 + {error.includes('authenticated') ? ( 934 + <button 935 + className="error-action-button" 936 + onClick={() => { 937 + const returnUrl = encodeURIComponent(window.location.pathname); 938 + navigate(`/login?returnUrl=${returnUrl}`); 939 + }} 940 + > 941 + Go to Login 967 942 </button> 968 - </div> 969 - 970 - <div className="collections-list"> 971 - {collections.map(collection => ( 972 - <div key={collection} className="collection-item"> 973 - <label> 974 - <input 975 - type="checkbox" 976 - checked={selectedCollections.includes(collection)} 977 - onChange={() => toggleCollection(collection)} 978 - /> 979 - {collection} 980 - </label> 981 - </div> 982 - ))} 983 - </div> 984 - </div> 985 - )} 986 - 987 - {/* Loading indicator for chart update */} 988 - {chartLoading && ( 989 - <div className="chart-loading"> 990 - <div className="spinner"></div> 991 - <p>Loading historical data for visualization...</p> 992 - </div> 993 - )} 994 - 995 - {/* Activity Chart */} 996 - {!chartLoading && filteredChartRecords.length > 0 && ( 997 - <div className="chart-container"> 998 - <h3>Activity Timeline</h3> 999 - <ActivityChart 1000 - records={filteredChartRecords} 1001 - useRkeyTimestamp={useRkeyTimestamp} 1002 - /> 1003 - </div> 1004 - )} 1005 - 1006 - {/* Feed heading */} 1007 - {selectedCollections.length > 0 && ( 1008 - <> 1009 - <h3 className="feed-heading">Record Feed</h3> 1010 - {filteredRecords.length === 0 && !loading && ( 1011 - <p className="no-records-message">No records found for the selected collections.</p> 943 + ) : ( 944 + <button 945 + className="error-action-button" 946 + onClick={() => { 947 + setError(''); 948 + if (username) { 949 + loadUserData(username); 950 + } 951 + }} 952 + > 953 + Retry 954 + </button> 1012 955 )} 1013 - </> 1014 - )} 1015 - 1016 - {/* Feed records */} 1017 - {selectedCollections.length === 0 ? ( 1018 - <p className="no-collections-selected">Select at least one collection to see records.</p> 1019 - ) : ( 1020 - <> 1021 - <FeedTimeline 1022 - records={filteredRecords} 1023 - useRkeyTimestamp={useRkeyTimestamp} 1024 - loading={loading} 1025 - /> 1026 - 1027 - {/* Load more button */} 1028 - {filteredRecords.length > 0 && (hasMoreRecordsLocally || hasMoreRecordsRemotely) && ( 1029 - <div className="load-more-container"> 1030 - <button 1031 - onClick={handleLoadMore} 1032 - disabled={fetchingMore} 1033 - className="load-more-button" 1034 - > 1035 - {fetchingMore ? 'Loading...' : 'Load More'} 956 + </div> 957 + </div> 958 + )} 959 + 960 + {/* Show simpler loading spinner when searching from the form, not the full MatterLoadingAnimation */} 961 + {!username && initialLoad && !error && ( 962 + <div className="loading-indicator"> 963 + <div className="spinner"></div> 964 + <p>Connecting to AT Protocol services...</p> 965 + </div> 966 + )} 967 + 968 + {/* Main content once search is performed */} 969 + {searchPerformed && !initialLoad && ( 970 + <div className="user-info"> 971 + {displayName && ( 972 + <h2> 973 + Collections for {displayName} 974 + <span className="handle">@{handle}</span> 975 + </h2> 976 + )} 977 + 978 + {/* Collections count and timeframe */} 979 + {collections.length > 0 && ( 980 + <div className="collections-meta"> 981 + <p>{collections.length} collections found</p> 982 + <div className="time-toggle"> 983 + <label> 984 + <input 985 + type="checkbox" 986 + checked={useRkeyTimestamp} 987 + onChange={() => setUseRkeyTimestamp(!useRkeyTimestamp)} 988 + /> 989 + Use record IDs for timestamps 990 + </label> 991 + <span className="info-tooltip"> 992 + ? 993 + <span className="tooltip-text"> 994 + Toggle between using timestamps found within the content (more accurate) or derived from record IDs (complete coverage) 995 + </span> 996 + </span> 997 + </div> 998 + </div> 999 + )} 1000 + 1001 + {/* Collections filter area */} 1002 + {collections.length > 0 && ( 1003 + <div className="collections-filter"> 1004 + <h3>Collections</h3> 1005 + <div className="filter-actions"> 1006 + <button onClick={selectAllCollections} className="select-all">Select All</button> 1007 + <button onClick={deselectAllCollections} className="deselect-all">Deselect All</button> 1008 + <button onClick={handleRefresh} className="refresh-button" disabled={loading || fetchingMore}> 1009 + {loading ? 'Refreshing...' : 'Refresh'} 1036 1010 </button> 1037 1011 </div> 1038 - )} 1039 - </> 1040 - )} 1041 - </div> 1042 - )} 1043 - </div> 1012 + 1013 + <div className="collections-list"> 1014 + {collections.map(collection => ( 1015 + <div key={collection} className="collection-item"> 1016 + <label> 1017 + <input 1018 + type="checkbox" 1019 + checked={selectedCollections.includes(collection)} 1020 + onChange={() => toggleCollection(collection)} 1021 + /> 1022 + {collection} 1023 + </label> 1024 + </div> 1025 + ))} 1026 + </div> 1027 + </div> 1028 + )} 1029 + 1030 + {/* Loading indicator for chart update */} 1031 + {chartLoading && ( 1032 + <div className="chart-loading"> 1033 + <div className="spinner"></div> 1034 + <p>Loading historical data for visualization...</p> 1035 + </div> 1036 + )} 1037 + 1038 + {/* Activity Chart */} 1039 + {!chartLoading && filteredChartRecords.length > 0 && ( 1040 + <div className="chart-container"> 1041 + <h3>Activity Timeline</h3> 1042 + <ActivityChart 1043 + records={filteredChartRecords} 1044 + useRkeyTimestamp={useRkeyTimestamp} 1045 + /> 1046 + </div> 1047 + )} 1048 + 1049 + {/* Feed heading */} 1050 + {selectedCollections.length > 0 && ( 1051 + <> 1052 + <h3 className="feed-heading">Record Feed</h3> 1053 + {filteredRecords.length === 0 && !loading && ( 1054 + <p className="no-records-message">No records found for the selected collections.</p> 1055 + )} 1056 + </> 1057 + )} 1058 + 1059 + {/* Feed records */} 1060 + {selectedCollections.length === 0 ? ( 1061 + <p className="no-collections-selected">Select at least one collection to see records.</p> 1062 + ) : ( 1063 + <> 1064 + <FeedTimeline 1065 + records={filteredRecords} 1066 + useRkeyTimestamp={useRkeyTimestamp} 1067 + loading={loading} 1068 + /> 1069 + 1070 + {/* Load more button */} 1071 + {filteredRecords.length > 0 && (hasMoreRecordsLocally || hasMoreRecordsRemotely) && ( 1072 + <div className="load-more-container"> 1073 + <button 1074 + onClick={handleLoadMore} 1075 + disabled={fetchingMore} 1076 + className="load-more-button" 1077 + > 1078 + {fetchingMore ? 'Loading...' : 'Load More'} 1079 + </button> 1080 + </div> 1081 + )} 1082 + </> 1083 + )} 1084 + </div> 1085 + )} 1086 + </div> 1087 + )} 1044 1088 1045 1089 {/* Debug button */} 1046 1090 <div className="debug-controls">