diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-21 09:26:44 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-21 09:26:44 +0900 |
| commit | 938863425bf8ad6c17e43b3da128f92cf6d6ab63 (patch) | |
| tree | 9210ea9d77c7c6fb030328cae5e8192093ebd61a | |
| parent | 4ebfe21a6c89ae0518c66bc6f06b0b15bf9b7724 (diff) | |
| download | feedaka-main.tar.gz feedaka-main.tar.zst feedaka-main.zip | |
Show "All feeds" button immediately and load individual feeds
asynchronously via internal Suspense boundary, avoiding the
spinner-to-content shift that occurred with the outer Suspense wrapper.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| -rw-r--r-- | frontend/src/components/FeedSidebar.tsx | 78 | ||||
| -rw-r--r-- | frontend/src/pages/ReadArticles.tsx | 10 | ||||
| -rw-r--r-- | frontend/src/pages/UnreadArticles.tsx | 10 |
3 files changed, 56 insertions, 42 deletions
diff --git a/frontend/src/components/FeedSidebar.tsx b/frontend/src/components/FeedSidebar.tsx index 209be23..3a367f5 100644 --- a/frontend/src/components/FeedSidebar.tsx +++ b/frontend/src/components/FeedSidebar.tsx @@ -1,6 +1,8 @@ import { useAtomValue } from "jotai"; +import { Suspense } from "react"; import { useLocation, useSearch } from "wouter"; import { feedsAtom } from "../atoms"; +import { ErrorBoundary } from "./ErrorBoundary"; interface Props { basePath: string; @@ -13,12 +15,6 @@ export function FeedSidebar({ basePath, isReadView = false }: Props) { const params = new URLSearchParams(search); const selectedFeedId = params.get("feed"); - const { data: allFeeds } = useAtomValue(feedsAtom); - - const feeds = isReadView - ? allFeeds - : allFeeds.filter((feed) => feed.unreadCount > 0); - const handleSelect = (feedId: string | null) => { if (feedId) { setLocation(`${basePath}?feed=${feedId}`); @@ -46,27 +42,57 @@ export function FeedSidebar({ basePath, isReadView = false }: Props) { All feeds </button> </li> - {feeds.map((feed) => ( - <li key={feed.id}> - <button - type="button" - onClick={() => handleSelect(feed.id)} - className={`flex w-full items-center justify-between rounded-md px-3 py-1.5 text-left text-sm transition-colors ${ - selectedFeedId === feed.id - ? "bg-stone-200 font-medium text-stone-900" - : "text-stone-600 hover:bg-stone-100" - }`} - > - <span className="min-w-0 truncate">{feed.title}</span> - {!isReadView && feed.unreadCount > 0 && ( - <span className="ml-2 shrink-0 rounded-full bg-sky-100 px-1.5 py-0.5 text-xs font-medium text-sky-700"> - {feed.unreadCount} - </span> - )} - </button> - </li> - ))} + <ErrorBoundary> + <Suspense> + <FeedListItems + isReadView={isReadView} + selectedFeedId={selectedFeedId} + onSelect={handleSelect} + /> + </Suspense> + </ErrorBoundary> </ul> </nav> ); } + +function FeedListItems({ + isReadView, + selectedFeedId, + onSelect, +}: { + isReadView: boolean; + selectedFeedId: string | null; + onSelect: (feedId: string | null) => void; +}) { + const { data: allFeeds } = useAtomValue(feedsAtom); + + const feeds = isReadView + ? allFeeds + : allFeeds.filter((feed) => feed.unreadCount > 0); + + return ( + <> + {feeds.map((feed) => ( + <li key={feed.id}> + <button + type="button" + onClick={() => onSelect(feed.id)} + className={`flex w-full items-center justify-between rounded-md px-3 py-1.5 text-left text-sm transition-colors ${ + selectedFeedId === feed.id + ? "bg-stone-200 font-medium text-stone-900" + : "text-stone-600 hover:bg-stone-100" + }`} + > + <span className="min-w-0 truncate">{feed.title}</span> + {!isReadView && feed.unreadCount > 0 && ( + <span className="ml-2 shrink-0 rounded-full bg-sky-100 px-1.5 py-0.5 text-xs font-medium text-sky-700"> + {feed.unreadCount} + </span> + )} + </button> + </li> + ))} + </> + ); +} diff --git a/frontend/src/pages/ReadArticles.tsx b/frontend/src/pages/ReadArticles.tsx index 8fcf9df..4d70558 100644 --- a/frontend/src/pages/ReadArticles.tsx +++ b/frontend/src/pages/ReadArticles.tsx @@ -1,5 +1,5 @@ import { useAtomValue, useSetAtom } from "jotai"; -import { Suspense, useEffect } from "react"; +import { useEffect } from "react"; import { useSearch } from "wouter"; import { articleFeedFilterAtom, @@ -7,9 +7,7 @@ import { articleViewAtom, } from "../atoms"; import { ArticleList } from "../components/ArticleList"; -import { ErrorBoundary } from "../components/ErrorBoundary"; import { FeedSidebar } from "../components/FeedSidebar"; -import { LoadingSpinner } from "../components/LoadingSpinner"; export function ReadArticles() { const search = useSearch(); @@ -27,11 +25,7 @@ export function ReadArticles() { return ( <div className="flex gap-8"> <div className="hidden w-56 shrink-0 md:block"> - <ErrorBoundary> - <Suspense fallback={<LoadingSpinner />}> - <FeedSidebar basePath="/read" isReadView /> - </Suspense> - </ErrorBoundary> + <FeedSidebar basePath="/read" isReadView /> </div> <div className="min-w-0 flex-1"> <ReadArticleList feedId={feedId} /> diff --git a/frontend/src/pages/UnreadArticles.tsx b/frontend/src/pages/UnreadArticles.tsx index 32d1190..e4754a3 100644 --- a/frontend/src/pages/UnreadArticles.tsx +++ b/frontend/src/pages/UnreadArticles.tsx @@ -1,5 +1,5 @@ import { useAtomValue, useSetAtom } from "jotai"; -import { Suspense, useEffect } from "react"; +import { useEffect } from "react"; import { useSearch } from "wouter"; import { articleFeedFilterAtom, @@ -7,9 +7,7 @@ import { articleViewAtom, } from "../atoms"; import { ArticleList } from "../components/ArticleList"; -import { ErrorBoundary } from "../components/ErrorBoundary"; import { FeedSidebar } from "../components/FeedSidebar"; -import { LoadingSpinner } from "../components/LoadingSpinner"; export function UnreadArticles() { const search = useSearch(); @@ -27,11 +25,7 @@ export function UnreadArticles() { return ( <div className="flex gap-8"> <div className="hidden w-56 shrink-0 md:block"> - <ErrorBoundary> - <Suspense fallback={<LoadingSpinner />}> - <FeedSidebar basePath="/unread" /> - </Suspense> - </ErrorBoundary> + <FeedSidebar basePath="/unread" /> </div> <div className="min-w-0 flex-1"> <UnreadArticleList feedId={feedId} /> |
