From 8f210ad003c28e8f11bdd10a12da6cbfed086c59 Mon Sep 17 00:00:00 2001 From: scanash00 Date: Wed, 28 Jan 2026 23:07:22 -0900 Subject: [PATCH] Back to top and view more buttons in feed and refined My-Feed pagination logic --- backend/internal/api/handler.go | 47 ++++++---- web/src/css/feed.css | 61 +++++++++++++ web/src/pages/Feed.jsx | 149 ++++++++++++++++++++++++-------- 3 files changed, 206 insertions(+), 51 deletions(-) diff --git a/backend/internal/api/handler.go b/backend/internal/api/handler.go index 535f10d..9cedb54 100644 --- a/backend/internal/api/handler.go +++ b/backend/internal/api/handler.go @@ -140,6 +140,7 @@ func (h *Handler) GetAnnotations(w http.ResponseWriter, r *http.Request) { func (h *Handler) GetFeed(w http.ResponseWriter, r *http.Request) { limit := parseIntParam(r, "limit", 50) + offset := parseIntParam(r, "offset", 0) tag := r.URL.Query().Get("tag") creator := r.URL.Query().Get("creator") feedType := r.URL.Query().Get("type") @@ -148,7 +149,7 @@ func (h *Handler) GetFeed(w http.ResponseWriter, r *http.Request) { if viewerDID != "" && (creator == viewerDID || (creator == "" && tag == "" && feedType == "my-feed")) { if creator == viewerDID { - h.serveUserFeedFromPDS(w, r, viewerDID, tag, limit) + h.serveUserFeedFromPDS(w, r, viewerDID, tag, limit, offset) return } } @@ -161,53 +162,55 @@ func (h *Handler) GetFeed(w http.ResponseWriter, r *http.Request) { motivation := r.URL.Query().Get("motivation") + fetchLimit := limit + offset + if tag != "" { if creator != "" { if motivation == "" || motivation == "commenting" { - annotations, _ = h.db.GetAnnotationsByTagAndAuthor(tag, creator, limit, 0) + annotations, _ = h.db.GetAnnotationsByTagAndAuthor(tag, creator, fetchLimit, 0) } if motivation == "" || motivation == "highlighting" { - highlights, _ = h.db.GetHighlightsByTagAndAuthor(tag, creator, limit, 0) + highlights, _ = h.db.GetHighlightsByTagAndAuthor(tag, creator, fetchLimit, 0) } if motivation == "" || motivation == "bookmarking" { - bookmarks, _ = h.db.GetBookmarksByTagAndAuthor(tag, creator, limit, 0) + bookmarks, _ = h.db.GetBookmarksByTagAndAuthor(tag, creator, fetchLimit, 0) } collectionItems = []db.CollectionItem{} } else { if motivation == "" || motivation == "commenting" { - annotations, _ = h.db.GetAnnotationsByTag(tag, limit, 0) + annotations, _ = h.db.GetAnnotationsByTag(tag, fetchLimit, 0) } if motivation == "" || motivation == "highlighting" { - highlights, _ = h.db.GetHighlightsByTag(tag, limit, 0) + highlights, _ = h.db.GetHighlightsByTag(tag, fetchLimit, 0) } if motivation == "" || motivation == "bookmarking" { - bookmarks, _ = h.db.GetBookmarksByTag(tag, limit, 0) + bookmarks, _ = h.db.GetBookmarksByTag(tag, fetchLimit, 0) } collectionItems = []db.CollectionItem{} } } else if creator != "" { if motivation == "" || motivation == "commenting" { - annotations, _ = h.db.GetAnnotationsByAuthor(creator, limit, 0) + annotations, _ = h.db.GetAnnotationsByAuthor(creator, fetchLimit, 0) } if motivation == "" || motivation == "highlighting" { - highlights, _ = h.db.GetHighlightsByAuthor(creator, limit, 0) + highlights, _ = h.db.GetHighlightsByAuthor(creator, fetchLimit, 0) } if motivation == "" || motivation == "bookmarking" { - bookmarks, _ = h.db.GetBookmarksByAuthor(creator, limit, 0) + bookmarks, _ = h.db.GetBookmarksByAuthor(creator, fetchLimit, 0) } collectionItems = []db.CollectionItem{} } else { if motivation == "" || motivation == "commenting" { - annotations, _ = h.db.GetRecentAnnotations(limit, 0) + annotations, _ = h.db.GetRecentAnnotations(fetchLimit, 0) } if motivation == "" || motivation == "highlighting" { - highlights, _ = h.db.GetRecentHighlights(limit, 0) + highlights, _ = h.db.GetRecentHighlights(fetchLimit, 0) } if motivation == "" || motivation == "bookmarking" { - bookmarks, _ = h.db.GetRecentBookmarks(limit, 0) + bookmarks, _ = h.db.GetRecentBookmarks(fetchLimit, 0) } if motivation == "" { - collectionItems, err = h.db.GetRecentCollectionItems(limit, 0) + collectionItems, err = h.db.GetRecentCollectionItems(fetchLimit, 0) if err != nil { log.Printf("Error fetching collection items: %v\n", err) } @@ -284,6 +287,12 @@ func (h *Handler) GetFeed(w http.ResponseWriter, r *http.Request) { sortFeed(feed) } + if offset < len(feed) { + feed = feed[offset:] + } else { + feed = []interface{}{} + } + if len(feed) > limit { feed = feed[:limit] } @@ -297,12 +306,12 @@ func (h *Handler) GetFeed(w http.ResponseWriter, r *http.Request) { }) } -func (h *Handler) serveUserFeedFromPDS(w http.ResponseWriter, r *http.Request, did, tag string, limit int) { +func (h *Handler) serveUserFeedFromPDS(w http.ResponseWriter, r *http.Request, did, tag string, limit, offset int) { var wg sync.WaitGroup var rawAnnos, rawHighs, rawBooks []interface{} var errAnnos, errHighs, errBooks error - fetchLimit := limit * 2 + fetchLimit := limit + offset if fetchLimit < 50 { fetchLimit = 50 } @@ -415,6 +424,12 @@ func (h *Handler) serveUserFeedFromPDS(w http.ResponseWriter, r *http.Request, d sortFeed(feed) + if offset < len(feed) { + feed = feed[offset:] + } else { + feed = []interface{}{} + } + if len(feed) > limit { feed = feed[:limit] } diff --git a/web/src/css/feed.css b/web/src/css/feed.css index b14c044..b026e72 100644 --- a/web/src/css/feed.css +++ b/web/src/css/feed.css @@ -17,6 +17,31 @@ position: relative; } +.feed-load-more { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 10px 24px; + background: var(--bg-tertiary); + border: none; + border-radius: var(--radius-md); + color: var(--text-secondary); + font-weight: 500; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.feed-load-more:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.feed-load-more:disabled { + opacity: 0.6; + cursor: not-allowed; +} + .feed > * { background: var(--bg-card); border: 1px solid var(--border); @@ -370,3 +395,39 @@ font-family: var(--font-mono); color: var(--text-secondary); } + +.back-to-top-btn { + position: fixed; + bottom: 24px; + right: 24px; + width: 44px; + height: 44px; + border-radius: var(--radius-full); + background: var(--bg-tertiary); + border: 1px solid var(--border); + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: var(--shadow-md); + transition: all 0.2s ease; + z-index: 100; + opacity: 0; + visibility: hidden; + transform: translateY(10px); +} + +.back-to-top-btn.visible { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.back-to-top-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); + border-color: var(--accent); + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} diff --git a/web/src/pages/Feed.jsx b/web/src/pages/Feed.jsx index 8ff5e3c..75088c4 100644 --- a/web/src/pages/Feed.jsx +++ b/web/src/pages/Feed.jsx @@ -8,7 +8,7 @@ import IOSInstallBanner from "../components/IOSInstallBanner"; import { getAnnotationFeed, deleteHighlight } from "../api/client"; import { AlertIcon, InboxIcon } from "../components/Icons"; import { useAuth } from "../context/AuthContext"; -import { X } from "lucide-react"; +import { X, ArrowUp } from "lucide-react"; import AddToCollectionModal from "../components/AddToCollectionModal"; @@ -27,6 +27,8 @@ export default function Feed() { const [annotations, setAnnotations] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [hasMore, setHasMore] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); useEffect(() => { localStorage.setItem("feedFilter", filter); @@ -43,45 +45,67 @@ export default function Feed() { const { user } = useAuth(); - useEffect(() => { - async function fetchFeed() { - try { + const fetchFeed = async (isLoadMore = false) => { + try { + if (isLoadMore) { + setLoadingMore(true); + } else { setLoading(true); - let creatorDid = ""; - - if (feedType === "my-feed") { - if (user?.did) { - creatorDid = user.did; - } else { - setAnnotations([]); - setLoading(false); - return; - } + } + + let creatorDid = ""; + + if (feedType === "my-feed") { + if (user?.did) { + creatorDid = user.did; + } else { + setAnnotations([]); + setLoading(false); + setLoadingMore(false); + return; } + } + + const motivationMap = { + commenting: "commenting", + highlighting: "highlighting", + bookmarking: "bookmarking", + }; + const motivation = motivationMap[filter] || ""; + const limit = 50; + const offset = isLoadMore ? annotations.length : 0; - const motivationMap = { - commenting: "commenting", - highlighting: "highlighting", - bookmarking: "bookmarking", - }; - const motivation = motivationMap[filter] || ""; - - const data = await getAnnotationFeed( - 50, - 0, - tagFilter || "", - creatorDid, - feedType, - motivation, - ); - setAnnotations(data.items || []); - } catch (err) { - setError(err.message); - } finally { - setLoading(false); + const data = await getAnnotationFeed( + limit, + offset, + tagFilter || "", + creatorDid, + feedType, + motivation, + ); + + const newItems = data.items || []; + if (newItems.length < limit) { + setHasMore(false); + } else { + setHasMore(true); + } + + if (isLoadMore) { + setAnnotations((prev) => [...prev, ...newItems]); + } else { + setAnnotations(newItems); } + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + setLoadingMore(false); } - fetchFeed(); + }; + + useEffect(() => { + fetchFeed(false); }, [tagFilter, feedType, filter, user]); const deduplicatedAnnotations = useMemo(() => { @@ -316,6 +340,25 @@ export default function Feed() { ); })} + + {hasMore && ( +
+ +
+ )} )} @@ -328,6 +371,42 @@ export default function Feed() { annotationUri={collectionModalState.uri} /> )} + + ); } + +function BackToTopButton() { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const toggleVisibility = () => { + if (window.scrollY > 300) { + setIsVisible(true); + } else { + setIsVisible(false); + } + }; + + window.addEventListener("scroll", toggleVisibility); + return () => window.removeEventListener("scroll", toggleVisibility); + }, []); + + const scrollToTop = () => { + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + }; + + return ( + + ); +} -- 2.51.2