diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-07-13 16:01:51 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-07-13 16:01:51 +0900 |
| commit | 1b929f36ebecda48d4327b9598fce26151f04087 (patch) | |
| tree | ffe8ed4d3c8049b5fec70b0ca52226c51df9183e /frontend/src/components | |
| parent | 43e7807034db67652bada00eee17a425d332b3f3 (diff) | |
| download | feedaka-1b929f36ebecda48d4327b9598fce26151f04087.tar.gz feedaka-1b929f36ebecda48d4327b9598fce26151f04087.tar.zst feedaka-1b929f36ebecda48d4327b9598fce26151f04087.zip | |
refactor(frontend): extract FeedItem from FeedListv0.3.0
Diffstat (limited to 'frontend/src/components')
| -rw-r--r-- | frontend/src/components/FeedItem.tsx | 86 | ||||
| -rw-r--r-- | frontend/src/components/FeedList.tsx | 100 |
2 files changed, 101 insertions, 85 deletions
diff --git a/frontend/src/components/FeedItem.tsx b/frontend/src/components/FeedItem.tsx new file mode 100644 index 0000000..020825a --- /dev/null +++ b/frontend/src/components/FeedItem.tsx @@ -0,0 +1,86 @@ +import { faCheck, faCircle, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useMutation } from "urql"; +import type { GetFeedsQuery } from "../graphql/generated/graphql"; +import { + MarkFeedReadDocument, + MarkFeedUnreadDocument, + UnsubscribeFeedDocument, +} from "../graphql/generated/graphql"; + +type Feed = NonNullable<GetFeedsQuery["feeds"]>[0]; + +interface Props { + feed: Feed; + onFeedUnsubscribed?: () => void; +} + +export function FeedItem({ feed, onFeedUnsubscribed }: Props) { + const [, markFeedRead] = useMutation(MarkFeedReadDocument); + const [, markFeedUnread] = useMutation(MarkFeedUnreadDocument); + const [, unsubscribeFeed] = useMutation(UnsubscribeFeedDocument); + + const handleMarkAllRead = async (feedId: string) => { + await markFeedRead({ id: feedId }); + }; + + const handleMarkAllUnread = async (feedId: string) => { + await markFeedUnread({ id: feedId }); + }; + + const handleUnsubscribeFeed = async (feedId: string) => { + const confirmed = window.confirm( + "Are you sure you want to unsubscribe from this feed?", + ); + if (confirmed) { + await unsubscribeFeed({ id: feedId }); + onFeedUnsubscribed?.(); + } + }; + + return ( + <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm"> + <div className="flex items-start justify-between"> + <div className="flex-1"> + <h3 className="text-lg font-semibold text-gray-900">{feed.title}</h3> + <p className="mt-1 text-sm text-gray-500"> + <a href={feed.url} target="_blank" rel="noreferrer"> + {feed.url} + </a> + </p> + <div className="mt-2 flex items-center gap-4 text-sm"> + <span className="text-gray-400"> + Last fetched: {new Date(feed.fetchedAt).toLocaleString()} + </span> + </div> + </div> + <div className="flex items-center gap-2"> + <button + type="button" + onClick={() => handleMarkAllRead(feed.id)} + className="rounded p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900" + title="Mark all as read" + > + <FontAwesomeIcon icon={faCheck} /> + </button> + <button + type="button" + onClick={() => handleMarkAllUnread(feed.id)} + className="rounded p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900" + title="Mark all as unread" + > + <FontAwesomeIcon icon={faCircle} /> + </button> + <button + type="button" + onClick={() => handleUnsubscribeFeed(feed.id)} + className="rounded p-2 text-red-600 hover:bg-red-50 hover:text-red-700" + title="Unsubscribe from feed" + > + <FontAwesomeIcon icon={faTrash} /> + </button> + </div> + </div> + </div> + ); +} diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx index db12b13..d1d7bf3 100644 --- a/frontend/src/components/FeedList.tsx +++ b/frontend/src/components/FeedList.tsx @@ -1,12 +1,6 @@ -import { faCheck, faCircle, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useMutation, useQuery } from "urql"; -import { - GetFeedsDocument, - MarkFeedReadDocument, - MarkFeedUnreadDocument, - UnsubscribeFeedDocument, -} from "../graphql/generated/graphql"; +import { useQuery } from "urql"; +import { GetFeedsDocument } from "../graphql/generated/graphql"; +import { FeedItem } from "./FeedItem"; interface Props { onFeedUnsubscribed?: () => void; @@ -17,89 +11,25 @@ export function FeedList({ onFeedUnsubscribed }: Props) { query: GetFeedsDocument, }); - const [, markFeedRead] = useMutation(MarkFeedReadDocument); - const [, markFeedUnread] = useMutation(MarkFeedUnreadDocument); - const [, unsubscribeFeed] = useMutation(UnsubscribeFeedDocument); - - const handleMarkAllRead = async (feedId: string) => { - await markFeedRead({ id: feedId }); - }; - - const handleMarkAllUnread = async (feedId: string) => { - await markFeedUnread({ id: feedId }); - }; - - const handleUnsubscribeFeed = async (feedId: string) => { - const confirmed = window.confirm( - "Are you sure you want to unsubscribe from this feed?", - ); - if (confirmed) { - await unsubscribeFeed({ id: feedId }); - onFeedUnsubscribed?.(); - } - }; - - if (fetching) return <div className="p-4">Loading feeds...</div>; - if (error) + if (fetching) { + return <div className="p-4">Loading feeds...</div>; + } + if (error) { return <div className="p-4 text-red-600">Error: {error.message}</div>; + } if (!data?.feeds || data.feeds.length === 0) { return <div className="p-4 text-gray-500">No feeds added yet.</div>; } return ( <div className="space-y-4 p-4"> - {data.feeds.map((feed) => { - return ( - <div - key={feed.id} - className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm" - > - <div className="flex items-start justify-between"> - <div className="flex-1"> - <h3 className="text-lg font-semibold text-gray-900"> - {feed.title} - </h3> - <p className="mt-1 text-sm text-gray-500"> - <a href={feed.url} target="_blank" rel="noreferrer"> - {feed.url} - </a> - </p> - <div className="mt-2 flex items-center gap-4 text-sm"> - <span className="text-gray-400"> - Last fetched: {new Date(feed.fetchedAt).toLocaleString()} - </span> - </div> - </div> - <div className="flex items-center gap-2"> - <button - type="button" - onClick={() => handleMarkAllRead(feed.id)} - className="rounded p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900" - title="Mark all as read" - > - <FontAwesomeIcon icon={faCheck} /> - </button> - <button - type="button" - onClick={() => handleMarkAllUnread(feed.id)} - className="rounded p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900" - title="Mark all as unread" - > - <FontAwesomeIcon icon={faCircle} /> - </button> - <button - type="button" - onClick={() => handleUnsubscribeFeed(feed.id)} - className="rounded p-2 text-red-600 hover:bg-red-50 hover:text-red-700" - title="Unsubscribe from feed" - > - <FontAwesomeIcon icon={faTrash} /> - </button> - </div> - </div> - </div> - ); - })} + {data.feeds.map((feed) => ( + <FeedItem + key={feed.id} + feed={feed} + onFeedUnsubscribed={onFeedUnsubscribed} + /> + ))} </div> ); } |
