aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/AddFeedForm.tsx20
-rw-r--r--frontend/src/components/ArticleItem.tsx33
-rw-r--r--frontend/src/components/ArticleList.tsx13
-rw-r--r--frontend/src/components/FeedItem.tsx34
-rw-r--r--frontend/src/components/FeedList.tsx50
-rw-r--r--frontend/src/components/FeedSidebar.tsx30
6 files changed, 102 insertions, 78 deletions
diff --git a/frontend/src/components/AddFeedForm.tsx b/frontend/src/components/AddFeedForm.tsx
index 9a56574..a60d86d 100644
--- a/frontend/src/components/AddFeedForm.tsx
+++ b/frontend/src/components/AddFeedForm.tsx
@@ -1,31 +1,31 @@
import { faPlus, faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react";
-import { useMutation } from "urql";
-import { AddFeedDocument } from "../graphql/generated/graphql";
+import { api } from "../services/api-client";
interface Props {
onFeedAdded?: () => void;
}
-const urqlContextFeed = { additionalTypenames: ["Feed"] };
-
export function AddFeedForm({ onFeedAdded }: Props) {
const [url, setUrl] = useState("");
const [error, setError] = useState<string | null>(null);
- const [{ fetching }, addFeed] = useMutation(AddFeedDocument);
+ const [fetching, setFetching] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!url.trim()) return;
setError(null);
+ setFetching(true);
try {
- const result = await addFeed({ url: url.trim() }, urqlContextFeed);
- if (result.error) {
- setError(result.error.message);
- } else if (result.data) {
+ const { data, error: fetchError } = await api.POST("/api/feeds", {
+ body: { url: url.trim() },
+ });
+ if (fetchError) {
+ setError(fetchError.message);
+ } else if (data) {
setUrl("");
onFeedAdded?.();
}
@@ -33,6 +33,8 @@ export function AddFeedForm({ onFeedAdded }: Props) {
setError(
error instanceof Error ? error.message : "Failed to subscribe to feed",
);
+ } finally {
+ setFetching(false);
}
};
diff --git a/frontend/src/components/ArticleItem.tsx b/frontend/src/components/ArticleItem.tsx
index dbdaf44..37664a9 100644
--- a/frontend/src/components/ArticleItem.tsx
+++ b/frontend/src/components/ArticleItem.tsx
@@ -1,30 +1,16 @@
import { faCheck, faCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useMutation } from "urql";
-import type {
- GetReadArticlesQuery,
- GetUnreadArticlesQuery,
-} from "../graphql/generated/graphql";
-import {
- MarkArticleReadDocument,
- MarkArticleUnreadDocument,
-} from "../graphql/generated/graphql";
+import type { components } from "../api/generated";
+import { api } from "../services/api-client";
-type Article =
- | GetUnreadArticlesQuery["unreadArticles"]["articles"][number]
- | GetReadArticlesQuery["readArticles"]["articles"][number];
+type Article = components["schemas"]["Article"];
interface Props {
article: Article;
onReadChange?: (articleId: string, isRead: boolean) => void;
}
-const urqlContextArticle = { additionalTypenames: ["Article"] };
-
export function ArticleItem({ article, onReadChange }: Props) {
- const [, markArticleRead] = useMutation(MarkArticleReadDocument);
- const [, markArticleUnread] = useMutation(MarkArticleUnreadDocument);
-
const handleToggleRead = async (
articleId: string,
isCurrentlyRead: boolean,
@@ -33,18 +19,23 @@ export function ArticleItem({ article, onReadChange }: Props) {
onReadChange?.(articleId, newReadState);
if (isCurrentlyRead) {
- await markArticleUnread({ id: articleId }, urqlContextArticle);
+ await api.POST("/api/articles/{articleId}/unread", {
+ params: { path: { articleId } },
+ });
} else {
- await markArticleRead({ id: articleId }, urqlContextArticle);
+ await api.POST("/api/articles/{articleId}/read", {
+ params: { path: { articleId } },
+ });
}
};
const handleArticleClick = async (article: Article) => {
- // Open article in new tab and mark as read if it's unread
window.open(article.url, "_blank", "noreferrer");
if (!article.isRead) {
onReadChange?.(article.id, true);
- await markArticleRead({ id: article.id }, urqlContextArticle);
+ await api.POST("/api/articles/{articleId}/read", {
+ params: { path: { articleId: article.id } },
+ });
}
};
diff --git a/frontend/src/components/ArticleList.tsx b/frontend/src/components/ArticleList.tsx
index ccf7826..2238a43 100644
--- a/frontend/src/components/ArticleList.tsx
+++ b/frontend/src/components/ArticleList.tsx
@@ -1,16 +1,11 @@
import { useState } from "react";
-import type {
- GetReadArticlesQuery,
- GetUnreadArticlesQuery,
-} from "../graphql/generated/graphql";
+import type { components } from "../api/generated";
import { ArticleItem } from "./ArticleItem";
-type ArticleType =
- | GetUnreadArticlesQuery["unreadArticles"]["articles"]
- | GetReadArticlesQuery["readArticles"]["articles"];
+type Article = components["schemas"]["Article"];
interface Props {
- articles: ArticleType;
+ articles: Article[];
isReadView?: boolean;
isSingleFeed?: boolean;
hasNextPage?: boolean;
@@ -82,7 +77,7 @@ export function ArticleList({
},
{} as Record<
string,
- { feed: { id: string; title: string }; articles: typeof articles }
+ { feed: { id: string; title: string }; articles: Article[] }
>,
);
diff --git a/frontend/src/components/FeedItem.tsx b/frontend/src/components/FeedItem.tsx
index 8333f75..1fb9001 100644
--- a/frontend/src/components/FeedItem.tsx
+++ b/frontend/src/components/FeedItem.tsx
@@ -1,33 +1,29 @@
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";
+import type { components } from "../api/generated";
+import { api } from "../services/api-client";
-type Feed = NonNullable<GetFeedsQuery["feeds"]>[0];
+type Feed = components["schemas"]["Feed"];
interface Props {
feed: Feed;
onFeedUnsubscribed?: () => void;
+ onFeedChanged?: () => void;
}
-const urqlContextFeed = { additionalTypenames: ["Feed"] };
-
-export function FeedItem({ feed, onFeedUnsubscribed }: Props) {
- const [, markFeedRead] = useMutation(MarkFeedReadDocument);
- const [, markFeedUnread] = useMutation(MarkFeedUnreadDocument);
- const [, unsubscribeFeed] = useMutation(UnsubscribeFeedDocument);
-
+export function FeedItem({ feed, onFeedUnsubscribed, onFeedChanged }: Props) {
const handleMarkAllRead = async (feedId: string) => {
- await markFeedRead({ id: feedId }, urqlContextFeed);
+ await api.POST("/api/feeds/{feedId}/read", {
+ params: { path: { feedId } },
+ });
+ onFeedChanged?.();
};
const handleMarkAllUnread = async (feedId: string) => {
- await markFeedUnread({ id: feedId }, urqlContextFeed);
+ await api.POST("/api/feeds/{feedId}/unread", {
+ params: { path: { feedId } },
+ });
+ onFeedChanged?.();
};
const handleUnsubscribeFeed = async (feedId: string) => {
@@ -35,7 +31,9 @@ export function FeedItem({ feed, onFeedUnsubscribed }: Props) {
"Are you sure you want to unsubscribe from this feed?",
);
if (confirmed) {
- await unsubscribeFeed({ id: feedId }, urqlContextFeed);
+ await api.DELETE("/api/feeds/{feedId}", {
+ params: { path: { feedId } },
+ });
onFeedUnsubscribed?.();
}
};
diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx
index 24bcfc7..a3ba124 100644
--- a/frontend/src/components/FeedList.tsx
+++ b/frontend/src/components/FeedList.tsx
@@ -1,18 +1,43 @@
-import { useQuery } from "urql";
-import { GetFeedsDocument } from "../graphql/generated/graphql";
+import { useCallback, useEffect, useState } from "react";
+import type { components } from "../api/generated";
+import { api } from "../services/api-client";
import { FeedItem } from "./FeedItem";
+type Feed = components["schemas"]["Feed"];
+
interface Props {
onFeedUnsubscribed?: () => void;
}
-const urqlContextFeed = { additionalTypenames: ["Feed"] };
-
export function FeedList({ onFeedUnsubscribed }: Props) {
- const [{ data, fetching, error }] = useQuery({
- query: GetFeedsDocument,
- context: urqlContextFeed,
- });
+ const [feeds, setFeeds] = useState<Feed[]>([]);
+ const [fetching, setFetching] = useState(true);
+ const [error, setError] = useState<string | null>(null);
+
+ const fetchFeeds = useCallback(async () => {
+ setFetching(true);
+ const { data } = await api.GET("/api/feeds");
+ if (data) {
+ setFeeds(data);
+ setError(null);
+ } else {
+ setError("Failed to load feeds");
+ }
+ setFetching(false);
+ }, []);
+
+ useEffect(() => {
+ fetchFeeds();
+ }, [fetchFeeds]);
+
+ const handleFeedUnsubscribed = () => {
+ fetchFeeds();
+ onFeedUnsubscribed?.();
+ };
+
+ const handleFeedChanged = () => {
+ fetchFeeds();
+ };
if (fetching) {
return (
@@ -24,11 +49,11 @@ export function FeedList({ onFeedUnsubscribed }: Props) {
if (error) {
return (
<div className="rounded-lg bg-red-50 p-4 text-sm text-red-600">
- Error: {error.message}
+ Error: {error}
</div>
);
}
- if (!data?.feeds || data.feeds.length === 0) {
+ if (feeds.length === 0) {
return (
<div className="py-8 text-center">
<p className="text-sm text-stone-400">No feeds added yet.</p>
@@ -38,11 +63,12 @@ export function FeedList({ onFeedUnsubscribed }: Props) {
return (
<div className="space-y-3">
- {data.feeds.map((feed) => (
+ {feeds.map((feed) => (
<FeedItem
key={feed.id}
feed={feed}
- onFeedUnsubscribed={onFeedUnsubscribed}
+ onFeedUnsubscribed={handleFeedUnsubscribed}
+ onFeedChanged={handleFeedChanged}
/>
))}
</div>
diff --git a/frontend/src/components/FeedSidebar.tsx b/frontend/src/components/FeedSidebar.tsx
index 73d9504..4f50566 100644
--- a/frontend/src/components/FeedSidebar.tsx
+++ b/frontend/src/components/FeedSidebar.tsx
@@ -1,23 +1,35 @@
-import { useQuery } from "urql";
+import { useCallback, useEffect, useState } from "react";
import { useLocation, useSearch } from "wouter";
-import { GetFeedsDocument } from "../graphql/generated/graphql";
+import type { components } from "../api/generated";
+import { api } from "../services/api-client";
+
+type Feed = components["schemas"]["Feed"];
interface Props {
basePath: string;
}
-const urqlContextFeed = { additionalTypenames: ["Feed", "Article"] };
-
export function FeedSidebar({ basePath }: Props) {
const search = useSearch();
const [, setLocation] = useLocation();
const params = new URLSearchParams(search);
const selectedFeedId = params.get("feed");
- const [{ data, fetching }] = useQuery({
- query: GetFeedsDocument,
- context: urqlContextFeed,
- });
+ const [feeds, setFeeds] = useState<Feed[]>([]);
+ const [fetching, setFetching] = useState(true);
+
+ const fetchFeeds = useCallback(async () => {
+ setFetching(true);
+ const { data } = await api.GET("/api/feeds");
+ if (data) {
+ setFeeds(data);
+ }
+ setFetching(false);
+ }, []);
+
+ useEffect(() => {
+ fetchFeeds();
+ }, [fetchFeeds]);
const handleSelect = (feedId: string | null) => {
if (feedId) {
@@ -49,7 +61,7 @@ export function FeedSidebar({ basePath }: Props) {
{fetching && (
<li className="px-3 py-1.5 text-xs text-stone-400">Loading...</li>
)}
- {data?.feeds.map((feed) => (
+ {feeds.map((feed) => (
<li key={feed.id}>
<button
type="button"