aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/pages')
-rw-r--r--frontend/src/pages/NotFound.tsx15
-rw-r--r--frontend/src/pages/ReadArticles.tsx34
-rw-r--r--frontend/src/pages/Settings.tsx155
-rw-r--r--frontend/src/pages/UnreadArticles.tsx34
-rw-r--r--frontend/src/pages/index.ts4
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";