diff options
Diffstat (limited to 'frontend/src/pages')
| -rw-r--r-- | frontend/src/pages/NotFound.tsx | 15 | ||||
| -rw-r--r-- | frontend/src/pages/ReadArticles.tsx | 34 | ||||
| -rw-r--r-- | frontend/src/pages/Settings.tsx | 155 | ||||
| -rw-r--r-- | frontend/src/pages/UnreadArticles.tsx | 34 | ||||
| -rw-r--r-- | frontend/src/pages/index.ts | 4 |
5 files changed, 242 insertions, 0 deletions
diff --git a/frontend/src/pages/NotFound.tsx b/frontend/src/pages/NotFound.tsx new file mode 100644 index 0000000..23c1184 --- /dev/null +++ b/frontend/src/pages/NotFound.tsx @@ -0,0 +1,15 @@ +export function NotFound() { + return ( + <div className="flex min-h-96 flex-col items-center justify-center"> + <div className="text-center"> + <h1 className="text-6xl font-bold text-gray-900">404</h1> + <h2 className="mt-4 text-2xl font-semibold text-gray-700"> + Page Not Found + </h2> + <p className="mt-2 text-gray-500"> + The page you're looking for doesn't exist. + </p> + </div> + </div> + ); +} diff --git a/frontend/src/pages/ReadArticles.tsx b/frontend/src/pages/ReadArticles.tsx new file mode 100644 index 0000000..4863d63 --- /dev/null +++ b/frontend/src/pages/ReadArticles.tsx @@ -0,0 +1,34 @@ +import { useQuery } from "urql"; +import { ArticleList } from "../components"; +import { GetReadArticlesDocument } from "../graphql/generated/graphql"; + +export function ReadArticles() { + const [{ data, fetching, error }] = useQuery({ + query: GetReadArticlesDocument, + }); + + if (fetching) { + return <div className="p-4">Loading read articles...</div>; + } + + if (error) { + return <div className="p-4 text-red-600">Error: {error.message}</div>; + } + + return ( + <div> + <div className="border-b border-gray-200 bg-white px-4 py-3"> + <h1 className="text-xl font-semibold text-gray-900">Read Articles</h1> + {data?.readArticles && ( + <p className="text-sm text-gray-500"> + {data.readArticles.length} article + {data.readArticles.length !== 1 ? "s" : ""} + </p> + )} + </div> + {data?.readArticles && ( + <ArticleList articles={data.readArticles} showReadStatus={true} /> + )} + </div> + ); +} diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx new file mode 100644 index 0000000..78fc306 --- /dev/null +++ b/frontend/src/pages/Settings.tsx @@ -0,0 +1,155 @@ +import { useState } from "react"; +import { useMutation, useQuery } from "urql"; +import { AddFeedForm, FeedList } from "../components"; +import { + GetFeedsDocument, + MarkFeedReadDocument, + MarkFeedUnreadDocument, + RemoveFeedDocument, +} from "../graphql/generated/graphql"; + +export function Settings() { + const [{ data: feedsData }, refetchFeeds] = useQuery({ + query: GetFeedsDocument, + }); + const [, markFeedRead] = useMutation(MarkFeedReadDocument); + const [, markFeedUnread] = useMutation(MarkFeedUnreadDocument); + const [, removeFeed] = useMutation(RemoveFeedDocument); + + const [selectedFeeds, setSelectedFeeds] = useState<Set<string>>(new Set()); + + const handleFeedAdded = () => { + refetchFeeds(); + }; + + const handleFeedDeleted = () => { + refetchFeeds(); + setSelectedFeeds(new Set()); + }; + + const handleSelectFeed = (feedId: string, selected: boolean) => { + const newSelection = new Set(selectedFeeds); + if (selected) { + newSelection.add(feedId); + } else { + newSelection.delete(feedId); + } + setSelectedFeeds(newSelection); + }; + + const handleSelectAll = () => { + if (!feedsData?.feeds) return; + if (selectedFeeds.size === feedsData.feeds.length) { + setSelectedFeeds(new Set()); + } else { + setSelectedFeeds(new Set(feedsData.feeds.map((feed) => feed.id))); + } + }; + + const handleBulkMarkRead = async () => { + const promises = Array.from(selectedFeeds).map((feedId) => + markFeedRead({ id: feedId }), + ); + await Promise.all(promises); + refetchFeeds(); + }; + + const handleBulkMarkUnread = async () => { + const promises = Array.from(selectedFeeds).map((feedId) => + markFeedUnread({ id: feedId }), + ); + await Promise.all(promises); + refetchFeeds(); + }; + + const handleBulkDelete = async () => { + const confirmed = window.confirm( + `Are you sure you want to delete ${selectedFeeds.size} selected feeds?`, + ); + if (!confirmed) return; + + const promises = Array.from(selectedFeeds).map((feedId) => + removeFeed({ id: feedId }), + ); + await Promise.all(promises); + handleFeedDeleted(); + }; + + const hasFeeds = feedsData?.feeds && feedsData.feeds.length > 0; + const hasSelectedFeeds = selectedFeeds.size > 0; + + return ( + <div className="mx-auto max-w-4xl"> + <h1 className="mb-6 text-2xl font-bold text-gray-900">Feed Settings</h1> + + {/* Add New Feed Section */} + <div className="mb-8"> + <h2 className="mb-4 text-xl font-semibold text-gray-800"> + Add New Feed + </h2> + <AddFeedForm onFeedAdded={handleFeedAdded} /> + </div> + + {/* Manage Feeds Section */} + <div className="mb-8"> + <div className="flex items-center justify-between mb-4"> + <h2 className="text-xl font-semibold text-gray-800">Manage Feeds</h2> + {hasFeeds && ( + <div className="flex items-center gap-4"> + <label className="flex items-center gap-2 text-sm text-gray-600"> + <input + type="checkbox" + checked={selectedFeeds.size === feedsData.feeds.length} + onChange={handleSelectAll} + className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" + /> + Select All ({feedsData.feeds.length} feeds) + </label> + </div> + )} + </div> + + {/* Bulk Operations */} + {hasSelectedFeeds && ( + <div className="mb-4 rounded-lg bg-blue-50 border border-blue-200 p-4"> + <div className="flex items-center justify-between"> + <span className="text-sm font-medium text-blue-900"> + {selectedFeeds.size} feed{selectedFeeds.size > 1 ? "s" : ""}{" "} + selected + </span> + <div className="flex gap-2"> + <button + type="button" + onClick={handleBulkMarkRead} + className="rounded px-3 py-1 text-sm font-medium text-blue-700 hover:bg-blue-100" + > + Mark All Read + </button> + <button + type="button" + onClick={handleBulkMarkUnread} + className="rounded px-3 py-1 text-sm font-medium text-blue-700 hover:bg-blue-100" + > + Mark All Unread + </button> + <button + type="button" + onClick={handleBulkDelete} + className="rounded px-3 py-1 text-sm font-medium text-red-700 hover:bg-red-100" + > + Delete Selected + </button> + </div> + </div> + </div> + )} + + <FeedList + onFeedDeleted={handleFeedDeleted} + selectedFeeds={selectedFeeds} + onSelectFeed={handleSelectFeed} + /> + </div> + </div> + ); +} diff --git a/frontend/src/pages/UnreadArticles.tsx b/frontend/src/pages/UnreadArticles.tsx new file mode 100644 index 0000000..38bf077 --- /dev/null +++ b/frontend/src/pages/UnreadArticles.tsx @@ -0,0 +1,34 @@ +import { useQuery } from "urql"; +import { ArticleList } from "../components"; +import { GetUnreadArticlesDocument } from "../graphql/generated/graphql"; + +export function UnreadArticles() { + const [{ data, fetching, error }] = useQuery({ + query: GetUnreadArticlesDocument, + }); + + if (fetching) { + return <div className="p-4">Loading unread articles...</div>; + } + + if (error) { + return <div className="p-4 text-red-600">Error: {error.message}</div>; + } + + return ( + <div> + <div className="border-b border-gray-200 bg-white px-4 py-3"> + <h1 className="text-xl font-semibold text-gray-900">Unread Articles</h1> + {data?.unreadArticles && ( + <p className="text-sm text-gray-500"> + {data.unreadArticles.length} article + {data.unreadArticles.length !== 1 ? "s" : ""} + </p> + )} + </div> + {data?.unreadArticles && ( + <ArticleList articles={data.unreadArticles} showReadStatus={true} /> + )} + </div> + ); +} diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts new file mode 100644 index 0000000..a037a9d --- /dev/null +++ b/frontend/src/pages/index.ts @@ -0,0 +1,4 @@ +export { NotFound } from "./NotFound"; +export { ReadArticles } from "./ReadArticles"; +export { Settings } from "./Settings"; +export { UnreadArticles } from "./UnreadArticles"; |
