aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-14 11:52:56 +0900
committernsfisis <nsfisis@gmail.com>2026-02-14 11:53:08 +0900
commit2889b562e64993482bd13fd806af8ed0865bab8b (patch)
tree39400ac4d994fb33d2c544e7d4b9d98f8ecbd86a /frontend/src
parente216c3bc97994b4172d15d52b46d5f6b75f35ea4 (diff)
downloadfeedaka-2889b562e64993482bd13fd806af8ed0865bab8b.tar.gz
feedaka-2889b562e64993482bd13fd806af8ed0865bab8b.tar.zst
feedaka-2889b562e64993482bd13fd806af8ed0865bab8b.zip
refactor: migrate API from GraphQL to REST (TypeSpec/OpenAPI)
Replace the entire GraphQL stack (gqlgen, urql, graphql-codegen) with a TypeSpec → OpenAPI 3.x pipeline using oapi-codegen for Go server stubs and openapi-fetch + openapi-typescript for the frontend client. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/api/generated.d.ts666
-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
-rw-r--r--frontend/src/contexts/AuthContext.tsx75
-rw-r--r--frontend/src/graphql/generated/fragment-masking.ts87
-rw-r--r--frontend/src/graphql/generated/gql.ts52
-rw-r--r--frontend/src/graphql/generated/graphql.ts304
-rw-r--r--frontend/src/graphql/generated/index.ts2
-rw-r--r--frontend/src/graphql/mutations.graphql65
-rw-r--r--frontend/src/graphql/queries.graphql94
l---------frontend/src/graphql/schema.graphql1
-rw-r--r--frontend/src/hooks/usePaginatedArticles.ts68
-rw-r--r--frontend/src/main.tsx10
-rw-r--r--frontend/src/pages/Settings.tsx21
-rw-r--r--frontend/src/services/api-client.ts6
-rw-r--r--frontend/src/services/graphql-client.ts10
20 files changed, 837 insertions, 804 deletions
diff --git a/frontend/src/api/generated.d.ts b/frontend/src/api/generated.d.ts
new file mode 100644
index 0000000..ae8fa60
--- /dev/null
+++ b/frontend/src/api/generated.d.ts
@@ -0,0 +1,666 @@
+/**
+ * This file was auto-generated by openapi-typescript.
+ * Do not make direct changes to the file.
+ */
+
+export interface paths {
+ "/api/articles/read": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["Articles_listReadArticles"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/articles/unread": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["Articles_listUnreadArticles"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/articles/{articleId}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["Articles_getArticle"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/articles/{articleId}/read": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["Articles_markArticleRead"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/articles/{articleId}/unread": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["Articles_markArticleUnread"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/auth/login": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["Auth_login"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/auth/logout": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["Auth_logout"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/auth/me": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["Auth_getCurrentUser"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/feeds": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["Feeds_listFeeds"];
+ put?: never;
+ post: operations["Feeds_addFeed"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/feeds/{feedId}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["Feeds_getFeed"];
+ put?: never;
+ post?: never;
+ delete: operations["Feeds_unsubscribeFeed"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/feeds/{feedId}/read": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["Feeds_markFeedRead"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/feeds/{feedId}/unread": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["Feeds_markFeedUnread"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+}
+export type webhooks = Record<string, never>;
+export interface components {
+ schemas: {
+ AddFeedRequest: {
+ url: string;
+ };
+ Article: {
+ id: string;
+ feedId: string;
+ guid: string;
+ title: string;
+ url: string;
+ isRead: boolean;
+ feed: components["schemas"]["ArticleFeed"];
+ };
+ ArticleConnection: {
+ articles: components["schemas"]["Article"][];
+ pageInfo: components["schemas"]["PageInfo"];
+ };
+ ArticleFeed: {
+ id: string;
+ url: string;
+ title: string;
+ isSubscribed: boolean;
+ };
+ ErrorResponse: {
+ message: string;
+ };
+ Feed: {
+ id: string;
+ url: string;
+ title: string;
+ fetchedAt: string;
+ isSubscribed: boolean;
+ /** Format: int32 */
+ unreadCount: number;
+ };
+ LoginRequest: {
+ username: string;
+ password: string;
+ };
+ LoginResponse: {
+ user: components["schemas"]["User"];
+ };
+ PageInfo: {
+ hasNextPage: boolean;
+ endCursor?: string;
+ };
+ User: {
+ id: string;
+ username: string;
+ };
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+export type $defs = Record<string, never>;
+export interface operations {
+ Articles_listReadArticles: {
+ parameters: {
+ query?: {
+ feedId?: string;
+ after?: string;
+ first?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The request has succeeded. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ArticleConnection"];
+ };
+ };
+ };
+ };
+ Articles_listUnreadArticles: {
+ parameters: {
+ query?: {
+ feedId?: string;
+ after?: string;
+ first?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The request has succeeded. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ArticleConnection"];
+ };
+ };
+ };
+ };
+ Articles_getArticle: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ articleId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The request has succeeded. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Article"];
+ };
+ };
+ /** @description The server cannot find the requested resource. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ Articles_markArticleRead: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ articleId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The request has succeeded. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Article"];
+ };
+ };
+ /** @description The server cannot find the requested resource. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ Articles_markArticleUnread: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ articleId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The request has succeeded. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Article"];
+ };
+ };
+ /** @description The server cannot find the requested resource. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ Auth_login: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["LoginRequest"];
+ };
+ };
+ responses: {
+ /** @description The request has succeeded. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["LoginResponse"];
+ };
+ };
+ /** @description Access is unauthorized. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ Auth_logout: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description There is no content to send for this request, but the headers may be useful. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Access is unauthorized. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ Auth_getCurrentUser: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The request has succeeded. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["User"];
+ };
+ };
+ /** @description Access is unauthorized. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ Feeds_listFeeds: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The request has succeeded. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Feed"][];
+ };
+ };
+ };
+ };
+ Feeds_addFeed: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AddFeedRequest"];
+ };
+ };
+ responses: {
+ /** @description The request has succeeded and a new resource has been created as a result. */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Feed"];
+ };
+ };
+ /** @description The server could not understand the request due to invalid syntax. */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ Feeds_getFeed: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ feedId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The request has succeeded. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Feed"];
+ };
+ };
+ /** @description The server cannot find the requested resource. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ Feeds_unsubscribeFeed: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ feedId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description There is no content to send for this request, but the headers may be useful. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description The server cannot find the requested resource. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ Feeds_markFeedRead: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ feedId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The request has succeeded. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Feed"];
+ };
+ };
+ /** @description The server cannot find the requested resource. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+ Feeds_markFeedUnread: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ feedId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The request has succeeded. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Feed"];
+ };
+ };
+ /** @description The server cannot find the requested resource. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ErrorResponse"];
+ };
+ };
+ };
+ };
+}
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"
diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx
index 7e38786..9b157cb 100644
--- a/frontend/src/contexts/AuthContext.tsx
+++ b/frontend/src/contexts/AuthContext.tsx
@@ -1,10 +1,12 @@
-import { createContext, type ReactNode, useContext } from "react";
-import { useMutation, useQuery } from "urql";
import {
- GetCurrentUserDocument,
- LoginDocument,
- LogoutDocument,
-} from "../graphql/generated/graphql";
+ createContext,
+ type ReactNode,
+ useCallback,
+ useContext,
+ useEffect,
+ useState,
+} from "react";
+import { api } from "../services/api-client";
type LoginResult = { success: true } | { success: false; error: string };
@@ -17,56 +19,43 @@ interface AuthContextType {
const AuthContext = createContext<AuthContextType | undefined>(undefined);
-const urqlContextUser = { additionalTypenames: ["User"] };
-
export function AuthProvider({ children }: { children: ReactNode }) {
- const [, executeLogin] = useMutation(LoginDocument);
- const [, executeLogout] = useMutation(LogoutDocument);
- const [currentUserResult, reexecuteGetCurrentUser] = useQuery({
- query: GetCurrentUserDocument,
- context: urqlContextUser,
- });
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const checkAuth = useCallback(async () => {
+ const { data } = await api.GET("/api/auth/me");
+ setIsLoggedIn(!!data);
+ setIsLoading(false);
+ }, []);
- const isLoggedIn = !!currentUserResult.data?.currentUser;
- const isLoading = currentUserResult.fetching || currentUserResult.stale;
+ useEffect(() => {
+ checkAuth();
+ }, [checkAuth]);
const login = async (
username: string,
password: string,
): Promise<LoginResult> => {
- try {
- const result = await executeLogin(
- { username, password },
- urqlContextUser,
- );
-
- if (result.error) {
- const errorMessage =
- result.error.graphQLErrors[0]?.message || result.error.message;
- return { success: false, error: errorMessage };
- }
+ const { data, error } = await api.POST("/api/auth/login", {
+ body: { username, password },
+ });
- if (result.data?.login?.user) {
- reexecuteGetCurrentUser({ requestPolicy: "network-only" });
- return { success: true };
- }
+ if (error) {
+ return { success: false, error: error.message };
+ }
- const errorMessage = "Invalid username or password";
- return { success: false, error: errorMessage };
- } catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "An unknown error occurred";
- console.error("Login failed:", error);
- return { success: false, error: errorMessage };
+ if (data?.user) {
+ setIsLoggedIn(true);
+ return { success: true };
}
+
+ return { success: false, error: "Invalid username or password" };
};
const logout = async () => {
- try {
- await executeLogout({}, urqlContextUser);
- } catch (error) {
- console.error("Logout failed:", error);
- }
+ await api.POST("/api/auth/logout");
+ setIsLoggedIn(false);
};
return (
diff --git a/frontend/src/graphql/generated/fragment-masking.ts b/frontend/src/graphql/generated/fragment-masking.ts
deleted file mode 100644
index 743a364..0000000
--- a/frontend/src/graphql/generated/fragment-masking.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/* eslint-disable */
-import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
-import type { FragmentDefinitionNode } from 'graphql';
-import type { Incremental } from './graphql';
-
-
-export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<
- infer TType,
- any
->
- ? [TType] extends [{ ' $fragmentName'?: infer TKey }]
- ? TKey extends string
- ? { ' $fragmentRefs'?: { [key in TKey]: TType } }
- : never
- : never
- : never;
-
-// return non-nullable if `fragmentType` is non-nullable
-export function useFragment<TType>(
- _documentNode: DocumentTypeDecoration<TType, any>,
- fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
-): TType;
-// return nullable if `fragmentType` is undefined
-export function useFragment<TType>(
- _documentNode: DocumentTypeDecoration<TType, any>,
- fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | undefined
-): TType | undefined;
-// return nullable if `fragmentType` is nullable
-export function useFragment<TType>(
- _documentNode: DocumentTypeDecoration<TType, any>,
- fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null
-): TType | null;
-// return nullable if `fragmentType` is nullable or undefined
-export function useFragment<TType>(
- _documentNode: DocumentTypeDecoration<TType, any>,
- fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
-): TType | null | undefined;
-// return array of non-nullable if `fragmentType` is array of non-nullable
-export function useFragment<TType>(
- _documentNode: DocumentTypeDecoration<TType, any>,
- fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>>
-): Array<TType>;
-// return array of nullable if `fragmentType` is array of nullable
-export function useFragment<TType>(
- _documentNode: DocumentTypeDecoration<TType, any>,
- fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
-): Array<TType> | null | undefined;
-// return readonly array of non-nullable if `fragmentType` is array of non-nullable
-export function useFragment<TType>(
- _documentNode: DocumentTypeDecoration<TType, any>,
- fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
-): ReadonlyArray<TType>;
-// return readonly array of nullable if `fragmentType` is array of nullable
-export function useFragment<TType>(
- _documentNode: DocumentTypeDecoration<TType, any>,
- fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
-): ReadonlyArray<TType> | null | undefined;
-export function useFragment<TType>(
- _documentNode: DocumentTypeDecoration<TType, any>,
- fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | Array<FragmentType<DocumentTypeDecoration<TType, any>>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
-): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {
- return fragmentType as any;
-}
-
-
-export function makeFragmentData<
- F extends DocumentTypeDecoration<any, any>,
- FT extends ResultOf<F>
->(data: FT, _fragment: F): FragmentType<F> {
- return data as FragmentType<F>;
-}
-export function isFragmentReady<TQuery, TFrag>(
- queryNode: DocumentTypeDecoration<TQuery, any>,
- fragmentNode: TypedDocumentNode<TFrag>,
- data: FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> | null | undefined
-): data is FragmentType<typeof fragmentNode> {
- const deferredFields = (queryNode as { __meta__?: { deferredFields: Record<string, (keyof TFrag)[]> } }).__meta__
- ?.deferredFields;
-
- if (!deferredFields) return true;
-
- const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined;
- const fragName = fragDef?.name?.value;
-
- const fields = (fragName && deferredFields[fragName]) || [];
- return fields.length > 0 && fields.every(field => data && field in data);
-}
diff --git a/frontend/src/graphql/generated/gql.ts b/frontend/src/graphql/generated/gql.ts
deleted file mode 100644
index 40e292f..0000000
--- a/frontend/src/graphql/generated/gql.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-/* eslint-disable */
-import * as types from './graphql';
-import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
-
-/**
- * Map of all GraphQL operations in the project.
- *
- * This map has several performance disadvantages:
- * 1. It is not tree-shakeable, so it will include all operations in the project.
- * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
- * 3. It does not support dead code elimination, so it will add unused operations.
- *
- * Therefore it is highly recommended to use the babel or swc plugin for production.
- * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
- */
-type Documents = {
- "mutation AddFeed($url: String!) {\n addFeed(url: $url) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation UnsubscribeFeed($id: ID!) {\n unsubscribeFeed(id: $id)\n}\n\nmutation MarkArticleRead($id: ID!) {\n markArticleRead(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkArticleUnread($id: ID!) {\n markArticleUnread(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkFeedRead($id: ID!) {\n markFeedRead(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation MarkFeedUnread($id: ID!) {\n markFeedUnread(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation Login($username: String!, $password: String!) {\n login(username: $username, password: $password) {\n user {\n id\n username\n }\n }\n}\n\nmutation Logout {\n logout\n}": typeof types.AddFeedDocument,
- "query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n isSubscribed\n unreadCount\n }\n}\n\nquery GetUnreadArticles($feedId: ID, $after: ID, $first: Int) {\n unreadArticles(feedId: $feedId, after: $after, first: $first) {\n articles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n\nquery GetReadArticles($feedId: ID, $after: ID, $first: Int) {\n readArticles(feedId: $feedId, after: $after, first: $first) {\n articles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\n isSubscribed\n articles {\n id\n guid\n title\n url\n isRead\n }\n }\n}\n\nquery GetArticle($id: ID!) {\n article(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}\n\nquery GetCurrentUser {\n currentUser {\n id\n username\n }\n}": typeof types.GetFeedsDocument,
-};
-const documents: Documents = {
- "mutation AddFeed($url: String!) {\n addFeed(url: $url) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation UnsubscribeFeed($id: ID!) {\n unsubscribeFeed(id: $id)\n}\n\nmutation MarkArticleRead($id: ID!) {\n markArticleRead(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkArticleUnread($id: ID!) {\n markArticleUnread(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkFeedRead($id: ID!) {\n markFeedRead(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation MarkFeedUnread($id: ID!) {\n markFeedUnread(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation Login($username: String!, $password: String!) {\n login(username: $username, password: $password) {\n user {\n id\n username\n }\n }\n}\n\nmutation Logout {\n logout\n}": types.AddFeedDocument,
- "query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n isSubscribed\n unreadCount\n }\n}\n\nquery GetUnreadArticles($feedId: ID, $after: ID, $first: Int) {\n unreadArticles(feedId: $feedId, after: $after, first: $first) {\n articles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n\nquery GetReadArticles($feedId: ID, $after: ID, $first: Int) {\n readArticles(feedId: $feedId, after: $after, first: $first) {\n articles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\n isSubscribed\n articles {\n id\n guid\n title\n url\n isRead\n }\n }\n}\n\nquery GetArticle($id: ID!) {\n article(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}\n\nquery GetCurrentUser {\n currentUser {\n id\n username\n }\n}": types.GetFeedsDocument,
-};
-
-/**
- * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- *
- *
- * @example
- * ```ts
- * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
- * ```
- *
- * The query argument is unknown!
- * Please regenerate the types.
- */
-export function graphql(source: string): unknown;
-
-/**
- * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function graphql(source: "mutation AddFeed($url: String!) {\n addFeed(url: $url) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation UnsubscribeFeed($id: ID!) {\n unsubscribeFeed(id: $id)\n}\n\nmutation MarkArticleRead($id: ID!) {\n markArticleRead(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkArticleUnread($id: ID!) {\n markArticleUnread(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkFeedRead($id: ID!) {\n markFeedRead(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation MarkFeedUnread($id: ID!) {\n markFeedUnread(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation Login($username: String!, $password: String!) {\n login(username: $username, password: $password) {\n user {\n id\n username\n }\n }\n}\n\nmutation Logout {\n logout\n}"): (typeof documents)["mutation AddFeed($url: String!) {\n addFeed(url: $url) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation UnsubscribeFeed($id: ID!) {\n unsubscribeFeed(id: $id)\n}\n\nmutation MarkArticleRead($id: ID!) {\n markArticleRead(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkArticleUnread($id: ID!) {\n markArticleUnread(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n }\n}\n\nmutation MarkFeedRead($id: ID!) {\n markFeedRead(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation MarkFeedUnread($id: ID!) {\n markFeedUnread(id: $id) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation Login($username: String!, $password: String!) {\n login(username: $username, password: $password) {\n user {\n id\n username\n }\n }\n}\n\nmutation Logout {\n logout\n}"];
-/**
- * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function graphql(source: "query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n isSubscribed\n unreadCount\n }\n}\n\nquery GetUnreadArticles($feedId: ID, $after: ID, $first: Int) {\n unreadArticles(feedId: $feedId, after: $after, first: $first) {\n articles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n\nquery GetReadArticles($feedId: ID, $after: ID, $first: Int) {\n readArticles(feedId: $feedId, after: $after, first: $first) {\n articles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\n isSubscribed\n articles {\n id\n guid\n title\n url\n isRead\n }\n }\n}\n\nquery GetArticle($id: ID!) {\n article(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}\n\nquery GetCurrentUser {\n currentUser {\n id\n username\n }\n}"): (typeof documents)["query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n isSubscribed\n unreadCount\n }\n}\n\nquery GetUnreadArticles($feedId: ID, $after: ID, $first: Int) {\n unreadArticles(feedId: $feedId, after: $after, first: $first) {\n articles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n\nquery GetReadArticles($feedId: ID, $after: ID, $first: Int) {\n readArticles(feedId: $feedId, after: $after, first: $first) {\n articles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\n isSubscribed\n articles {\n id\n guid\n title\n url\n isRead\n }\n }\n}\n\nquery GetArticle($id: ID!) {\n article(id: $id) {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n isSubscribed\n }\n }\n}\n\nquery GetCurrentUser {\n currentUser {\n id\n username\n }\n}"];
-
-export function graphql(source: string) {
- return (documents as any)[source] ?? {};
-}
-
-export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never; \ No newline at end of file
diff --git a/frontend/src/graphql/generated/graphql.ts b/frontend/src/graphql/generated/graphql.ts
deleted file mode 100644
index 9d7c168..0000000
--- a/frontend/src/graphql/generated/graphql.ts
+++ /dev/null
@@ -1,304 +0,0 @@
-/* eslint-disable */
-import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
-export type Maybe<T> = T | null;
-export type InputMaybe<T> = Maybe<T>;
-export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
-export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
-export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
-export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
-export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
-/** All built-in and custom scalars, mapped to their actual values */
-export type Scalars = {
- ID: { input: string; output: string; }
- String: { input: string; output: string; }
- Boolean: { input: boolean; output: boolean; }
- Int: { input: number; output: number; }
- Float: { input: number; output: number; }
- DateTime: { input: string; output: string; }
-};
-
-/** Represents an individual article/post from a feed */
-export type Article = {
- /** The feed this article belongs to */
- feed: Feed;
- /** ID of the feed this article belongs to */
- feedId: Scalars['ID']['output'];
- /** GUID from the RSS/Atom feed (unique identifier from feed) */
- guid: Scalars['String']['output'];
- /** Unique identifier for the article */
- id: Scalars['ID']['output'];
- /** Whether the article has been marked as read */
- isRead: Scalars['Boolean']['output'];
- /** Title of the article */
- title: Scalars['String']['output'];
- /** URL/link to the original article */
- url: Scalars['String']['output'];
-};
-
-/** A paginated list of articles */
-export type ArticleConnection = {
- /** The list of articles */
- articles: Array<Article>;
- /** Pagination information */
- pageInfo: PageInfo;
-};
-
-/** Authentication payload returned from login mutation */
-export type AuthPayload = {
- /** The authenticated user */
- user: User;
-};
-
-/** Represents a feed subscription in the system */
-export type Feed = {
- /** Articles belonging to this feed */
- articles: Array<Article>;
- /** Timestamp when the feed was last fetched */
- fetchedAt: Scalars['DateTime']['output'];
- /** Unique identifier for the feed */
- id: Scalars['ID']['output'];
- /** Whether the user is currently subscribed to this feed */
- isSubscribed: Scalars['Boolean']['output'];
- /** Title of the feed (extracted from feed metadata) */
- title: Scalars['String']['output'];
- /** Number of unread articles in this feed */
- unreadCount: Scalars['Int']['output'];
- /** URL of the RSS/Atom feed */
- url: Scalars['String']['output'];
-};
-
-/** Root mutation type for modifying data */
-export type Mutation = {
- /** Add a new feed subscription */
- addFeed: Feed;
- /** Login with username and password. Creates a session cookie. */
- login: AuthPayload;
- /** Logout the current user and destroy the session */
- logout: Scalars['Boolean']['output'];
- /** Mark an article as read */
- markArticleRead: Article;
- /** Mark an article as unread */
- markArticleUnread: Article;
- /** Mark all articles in a feed as read */
- markFeedRead: Feed;
- /** Mark all articles in a feed as unread */
- markFeedUnread: Feed;
- /** Unsubscribe from a feed (preserves feed and article data) */
- unsubscribeFeed: Scalars['Boolean']['output'];
-};
-
-
-/** Root mutation type for modifying data */
-export type MutationAddFeedArgs = {
- url: Scalars['String']['input'];
-};
-
-
-/** Root mutation type for modifying data */
-export type MutationLoginArgs = {
- password: Scalars['String']['input'];
- username: Scalars['String']['input'];
-};
-
-
-/** Root mutation type for modifying data */
-export type MutationMarkArticleReadArgs = {
- id: Scalars['ID']['input'];
-};
-
-
-/** Root mutation type for modifying data */
-export type MutationMarkArticleUnreadArgs = {
- id: Scalars['ID']['input'];
-};
-
-
-/** Root mutation type for modifying data */
-export type MutationMarkFeedReadArgs = {
- id: Scalars['ID']['input'];
-};
-
-
-/** Root mutation type for modifying data */
-export type MutationMarkFeedUnreadArgs = {
- id: Scalars['ID']['input'];
-};
-
-
-/** Root mutation type for modifying data */
-export type MutationUnsubscribeFeedArgs = {
- id: Scalars['ID']['input'];
-};
-
-/** Pagination information for cursor-based pagination */
-export type PageInfo = {
- /** Cursor of the last item in this page */
- endCursor?: Maybe<Scalars['ID']['output']>;
- /** Whether there are more items after the last item in this page */
- hasNextPage: Scalars['Boolean']['output'];
-};
-
-/** Root query type for reading data */
-export type Query = {
- /** Get a specific article by ID */
- article?: Maybe<Article>;
- /** Get the currently authenticated user */
- currentUser?: Maybe<User>;
- /** Get a specific feed by ID */
- feed?: Maybe<Feed>;
- /** Get all feeds with their metadata */
- feeds: Array<Feed>;
- /** Get read articles with optional feed filter and cursor-based pagination */
- readArticles: ArticleConnection;
- /** Get unread articles with optional feed filter and cursor-based pagination */
- unreadArticles: ArticleConnection;
-};
-
-
-/** Root query type for reading data */
-export type QueryArticleArgs = {
- id: Scalars['ID']['input'];
-};
-
-
-/** Root query type for reading data */
-export type QueryFeedArgs = {
- id: Scalars['ID']['input'];
-};
-
-
-/** Root query type for reading data */
-export type QueryReadArticlesArgs = {
- after?: InputMaybe<Scalars['ID']['input']>;
- feedId?: InputMaybe<Scalars['ID']['input']>;
- first?: InputMaybe<Scalars['Int']['input']>;
-};
-
-
-/** Root query type for reading data */
-export type QueryUnreadArticlesArgs = {
- after?: InputMaybe<Scalars['ID']['input']>;
- feedId?: InputMaybe<Scalars['ID']['input']>;
- first?: InputMaybe<Scalars['Int']['input']>;
-};
-
-/** Represents a user in the system */
-export type User = {
- /** Unique identifier for the user */
- id: Scalars['ID']['output'];
- /** Username of the user */
- username: Scalars['String']['output'];
-};
-
-export type AddFeedMutationVariables = Exact<{
- url: Scalars['String']['input'];
-}>;
-
-
-export type AddFeedMutation = { addFeed: { id: string, url: string, title: string, fetchedAt: string } };
-
-export type UnsubscribeFeedMutationVariables = Exact<{
- id: Scalars['ID']['input'];
-}>;
-
-
-export type UnsubscribeFeedMutation = { unsubscribeFeed: boolean };
-
-export type MarkArticleReadMutationVariables = Exact<{
- id: Scalars['ID']['input'];
-}>;
-
-
-export type MarkArticleReadMutation = { markArticleRead: { id: string, feedId: string, guid: string, title: string, url: string, isRead: boolean } };
-
-export type MarkArticleUnreadMutationVariables = Exact<{
- id: Scalars['ID']['input'];
-}>;
-
-
-export type MarkArticleUnreadMutation = { markArticleUnread: { id: string, feedId: string, guid: string, title: string, url: string, isRead: boolean } };
-
-export type MarkFeedReadMutationVariables = Exact<{
- id: Scalars['ID']['input'];
-}>;
-
-
-export type MarkFeedReadMutation = { markFeedRead: { id: string, url: string, title: string, fetchedAt: string } };
-
-export type MarkFeedUnreadMutationVariables = Exact<{
- id: Scalars['ID']['input'];
-}>;
-
-
-export type MarkFeedUnreadMutation = { markFeedUnread: { id: string, url: string, title: string, fetchedAt: string } };
-
-export type LoginMutationVariables = Exact<{
- username: Scalars['String']['input'];
- password: Scalars['String']['input'];
-}>;
-
-
-export type LoginMutation = { login: { user: { id: string, username: string } } };
-
-export type LogoutMutationVariables = Exact<{ [key: string]: never; }>;
-
-
-export type LogoutMutation = { logout: boolean };
-
-export type GetFeedsQueryVariables = Exact<{ [key: string]: never; }>;
-
-
-export type GetFeedsQuery = { feeds: Array<{ id: string, url: string, title: string, fetchedAt: string, isSubscribed: boolean, unreadCount: number }> };
-
-export type GetUnreadArticlesQueryVariables = Exact<{
- feedId?: InputMaybe<Scalars['ID']['input']>;
- after?: InputMaybe<Scalars['ID']['input']>;
- first?: InputMaybe<Scalars['Int']['input']>;
-}>;
-
-
-export type GetUnreadArticlesQuery = { unreadArticles: { articles: Array<{ id: string, feedId: string, guid: string, title: string, url: string, isRead: boolean, feed: { id: string, title: string, isSubscribed: boolean } }>, pageInfo: { hasNextPage: boolean, endCursor?: string | null } } };
-
-export type GetReadArticlesQueryVariables = Exact<{
- feedId?: InputMaybe<Scalars['ID']['input']>;
- after?: InputMaybe<Scalars['ID']['input']>;
- first?: InputMaybe<Scalars['Int']['input']>;
-}>;
-
-
-export type GetReadArticlesQuery = { readArticles: { articles: Array<{ id: string, feedId: string, guid: string, title: string, url: string, isRead: boolean, feed: { id: string, title: string, isSubscribed: boolean } }>, pageInfo: { hasNextPage: boolean, endCursor?: string | null } } };
-
-export type GetFeedQueryVariables = Exact<{
- id: Scalars['ID']['input'];
-}>;
-
-
-export type GetFeedQuery = { feed?: { id: string, url: string, title: string, fetchedAt: string, isSubscribed: boolean, articles: Array<{ id: string, guid: string, title: string, url: string, isRead: boolean }> } | null };
-
-export type GetArticleQueryVariables = Exact<{
- id: Scalars['ID']['input'];
-}>;
-
-
-export type GetArticleQuery = { article?: { id: string, feedId: string, guid: string, title: string, url: string, isRead: boolean, feed: { id: string, title: string, isSubscribed: boolean } } | null };
-
-export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
-
-
-export type GetCurrentUserQuery = { currentUser?: { id: string, username: string } | null };
-
-
-export const AddFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"url"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addFeed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"url"},"value":{"kind":"Variable","name":{"kind":"Name","value":"url"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"fetchedAt"}}]}}]}}]} as unknown as DocumentNode<AddFeedMutation, AddFeedMutationVariables>;
-export const UnsubscribeFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UnsubscribeFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unsubscribeFeed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode<UnsubscribeFeedMutation, UnsubscribeFeedMutationVariables>;
-export const MarkArticleReadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MarkArticleRead"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"markArticleRead"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"feedId"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}}]}}]}}]} as unknown as DocumentNode<MarkArticleReadMutation, MarkArticleReadMutationVariables>;
-export const MarkArticleUnreadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MarkArticleUnread"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"markArticleUnread"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"feedId"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}}]}}]}}]} as unknown as DocumentNode<MarkArticleUnreadMutation, MarkArticleUnreadMutationVariables>;
-export const MarkFeedReadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MarkFeedRead"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"markFeedRead"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"fetchedAt"}}]}}]}}]} as unknown as DocumentNode<MarkFeedReadMutation, MarkFeedReadMutationVariables>;
-export const MarkFeedUnreadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MarkFeedUnread"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"markFeedUnread"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"fetchedAt"}}]}}]}}]} as unknown as DocumentNode<MarkFeedUnreadMutation, MarkFeedUnreadMutationVariables>;
-export const LoginDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Login"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"username"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"login"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"username"},"value":{"kind":"Variable","name":{"kind":"Name","value":"username"}}},{"kind":"Argument","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]} as unknown as DocumentNode<LoginMutation, LoginMutationVariables>;
-export const LogoutDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Logout"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logout"}}]}}]} as unknown as DocumentNode<LogoutMutation, LogoutMutationVariables>;
-export const GetFeedsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetFeeds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feeds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"fetchedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isSubscribed"}},{"kind":"Field","name":{"kind":"Name","value":"unreadCount"}}]}}]}}]} as unknown as DocumentNode<GetFeedsQuery, GetFeedsQueryVariables>;
-export const GetUnreadArticlesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUnreadArticles"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"feedId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unreadArticles"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"feedId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"feedId"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"articles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"feedId"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isSubscribed"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<GetUnreadArticlesQuery, GetUnreadArticlesQueryVariables>;
-export const GetReadArticlesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetReadArticles"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"feedId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readArticles"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"feedId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"feedId"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"articles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"feedId"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isSubscribed"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<GetReadArticlesQuery, GetReadArticlesQueryVariables>;
-export const GetFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"fetchedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isSubscribed"}},{"kind":"Field","name":{"kind":"Name","value":"articles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}}]}}]}}]}}]} as unknown as DocumentNode<GetFeedQuery, GetFeedQueryVariables>;
-export const GetArticleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetArticle"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"article"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"feedId"}},{"kind":"Field","name":{"kind":"Name","value":"guid"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}},{"kind":"Field","name":{"kind":"Name","value":"feed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isSubscribed"}}]}}]}}]}}]} as unknown as DocumentNode<GetArticleQuery, GetArticleQueryVariables>;
-export const GetCurrentUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCurrentUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currentUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]} as unknown as DocumentNode<GetCurrentUserQuery, GetCurrentUserQueryVariables>; \ No newline at end of file
diff --git a/frontend/src/graphql/generated/index.ts b/frontend/src/graphql/generated/index.ts
deleted file mode 100644
index f515991..0000000
--- a/frontend/src/graphql/generated/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./fragment-masking";
-export * from "./gql"; \ No newline at end of file
diff --git a/frontend/src/graphql/mutations.graphql b/frontend/src/graphql/mutations.graphql
deleted file mode 100644
index f919e66..0000000
--- a/frontend/src/graphql/mutations.graphql
+++ /dev/null
@@ -1,65 +0,0 @@
-mutation AddFeed($url: String!) {
- addFeed(url: $url) {
- id
- url
- title
- fetchedAt
- }
-}
-
-mutation UnsubscribeFeed($id: ID!) {
- unsubscribeFeed(id: $id)
-}
-
-mutation MarkArticleRead($id: ID!) {
- markArticleRead(id: $id) {
- id
- feedId
- guid
- title
- url
- isRead
- }
-}
-
-mutation MarkArticleUnread($id: ID!) {
- markArticleUnread(id: $id) {
- id
- feedId
- guid
- title
- url
- isRead
- }
-}
-
-mutation MarkFeedRead($id: ID!) {
- markFeedRead(id: $id) {
- id
- url
- title
- fetchedAt
- }
-}
-
-mutation MarkFeedUnread($id: ID!) {
- markFeedUnread(id: $id) {
- id
- url
- title
- fetchedAt
- }
-}
-
-mutation Login($username: String!, $password: String!) {
- login(username: $username, password: $password) {
- user {
- id
- username
- }
- }
-}
-
-mutation Logout {
- logout
-}
diff --git a/frontend/src/graphql/queries.graphql b/frontend/src/graphql/queries.graphql
deleted file mode 100644
index 0b0e25c..0000000
--- a/frontend/src/graphql/queries.graphql
+++ /dev/null
@@ -1,94 +0,0 @@
-query GetFeeds {
- feeds {
- id
- url
- title
- fetchedAt
- isSubscribed
- unreadCount
- }
-}
-
-query GetUnreadArticles($feedId: ID, $after: ID, $first: Int) {
- unreadArticles(feedId: $feedId, after: $after, first: $first) {
- articles {
- id
- feedId
- guid
- title
- url
- isRead
- feed {
- id
- title
- isSubscribed
- }
- }
- pageInfo {
- hasNextPage
- endCursor
- }
- }
-}
-
-query GetReadArticles($feedId: ID, $after: ID, $first: Int) {
- readArticles(feedId: $feedId, after: $after, first: $first) {
- articles {
- id
- feedId
- guid
- title
- url
- isRead
- feed {
- id
- title
- isSubscribed
- }
- }
- pageInfo {
- hasNextPage
- endCursor
- }
- }
-}
-
-query GetFeed($id: ID!) {
- feed(id: $id) {
- id
- url
- title
- fetchedAt
- isSubscribed
- articles {
- id
- guid
- title
- url
- isRead
- }
- }
-}
-
-query GetArticle($id: ID!) {
- article(id: $id) {
- id
- feedId
- guid
- title
- url
- isRead
- feed {
- id
- title
- isSubscribed
- }
- }
-}
-
-query GetCurrentUser {
- currentUser {
- id
- username
- }
-}
diff --git a/frontend/src/graphql/schema.graphql b/frontend/src/graphql/schema.graphql
deleted file mode 120000
index 5771f01..0000000
--- a/frontend/src/graphql/schema.graphql
+++ /dev/null
@@ -1 +0,0 @@
-../../../graphql/schema.graphql \ No newline at end of file
diff --git a/frontend/src/hooks/usePaginatedArticles.ts b/frontend/src/hooks/usePaginatedArticles.ts
index 56098d7..5ddf888 100644
--- a/frontend/src/hooks/usePaginatedArticles.ts
+++ b/frontend/src/hooks/usePaginatedArticles.ts
@@ -1,17 +1,8 @@
import { useCallback, useEffect, useState } from "react";
-import { useClient } from "urql";
-import type {
- GetReadArticlesQuery,
- GetUnreadArticlesQuery,
-} from "../graphql/generated/graphql";
-import {
- GetReadArticlesDocument,
- GetUnreadArticlesDocument,
-} from "../graphql/generated/graphql";
+import type { components } from "../api/generated";
+import { api } from "../services/api-client";
-type ArticleType =
- | GetUnreadArticlesQuery["unreadArticles"]["articles"][number]
- | GetReadArticlesQuery["readArticles"]["articles"][number];
+export type ArticleType = components["schemas"]["Article"];
interface UsePaginatedArticlesOptions {
isReadView: boolean;
@@ -31,7 +22,6 @@ export function usePaginatedArticles({
isReadView,
feedId,
}: UsePaginatedArticlesOptions): UsePaginatedArticlesResult {
- const client = useClient();
const [articles, setArticles] = useState<ArticleType[]>([]);
const [hasNextPage, setHasNextPage] = useState(false);
const [endCursor, setEndCursor] = useState<string | null>(null);
@@ -41,49 +31,33 @@ export function usePaginatedArticles({
const fetchArticles = useCallback(
async (after: string | null, append: boolean) => {
- const variables: Record<string, unknown> = {};
- if (feedId) variables.feedId = feedId;
- if (after) variables.after = after;
+ const query: { feedId?: string; after?: string } = {};
+ if (feedId) query.feedId = feedId;
+ if (after) query.after = after;
- let connection: {
- articles: ArticleType[];
- pageInfo: { hasNextPage: boolean; endCursor?: string | null };
- } | null = null;
+ const endpoint = isReadView
+ ? "/api/articles/read"
+ : "/api/articles/unread";
- if (isReadView) {
- const result = await client
- .query(GetReadArticlesDocument, variables, {
- additionalTypenames: ["Article"],
- })
- .toPromise();
- if (result.error) {
- setError(new Error(result.error.message));
- return;
- }
- connection = result.data?.readArticles ?? null;
- } else {
- const result = await client
- .query(GetUnreadArticlesDocument, variables, {
- additionalTypenames: ["Article"],
- })
- .toPromise();
- if (result.error) {
- setError(new Error(result.error.message));
- return;
- }
- connection = result.data?.unreadArticles ?? null;
+ const { data } = await api.GET(endpoint, {
+ params: { query },
+ });
+
+ if (!data) {
+ setError(new Error("Failed to fetch articles"));
+ return;
}
- if (connection) {
+ if (data) {
setArticles((prev) =>
- append ? [...prev, ...connection.articles] : connection.articles,
+ append ? [...prev, ...data.articles] : data.articles,
);
- setHasNextPage(connection.pageInfo.hasNextPage);
- setEndCursor(connection.pageInfo.endCursor ?? null);
+ setHasNextPage(data.pageInfo.hasNextPage);
+ setEndCursor(data.pageInfo.endCursor ?? null);
setError(null);
}
},
- [client, isReadView, feedId],
+ [isReadView, feedId],
);
// Reset and fetch on feedId or view change
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index b96c76e..d1dd4d5 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,18 +1,14 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
-import { Provider as UrqlProvider } from "urql";
import "./index.css";
import App from "./App.tsx";
import { AuthProvider } from "./contexts/AuthContext";
-import { client } from "./services/graphql-client";
// biome-ignore lint/style/noNonNullAssertion: root element is guaranteed to exist
createRoot(document.getElementById("root")!).render(
<StrictMode>
- <UrqlProvider value={client}>
- <AuthProvider>
- <App />
- </AuthProvider>
- </UrqlProvider>
+ <AuthProvider>
+ <App />
+ </AuthProvider>
</StrictMode>,
);
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx
index 9b1e04c..c179fab 100644
--- a/frontend/src/pages/Settings.tsx
+++ b/frontend/src/pages/Settings.tsx
@@ -1,31 +1,24 @@
-import { useQuery } from "urql";
+import { useCallback, useState } from "react";
import { AddFeedForm, FeedList } from "../components";
-import { GetFeedsDocument } from "../graphql/generated/graphql";
export function Settings() {
- const [, refetchFeeds] = useQuery({
- query: GetFeedsDocument,
- });
+ const [refreshKey, setRefreshKey] = useState(0);
- const handleFeedAdded = () => {
- refetchFeeds();
- };
-
- const handleFeedUnsubscribed = () => {
- refetchFeeds();
- };
+ const handleChange = useCallback(() => {
+ setRefreshKey((k) => k + 1);
+ }, []);
return (
<div className="mx-auto max-w-3xl space-y-10">
<section>
- <AddFeedForm onFeedAdded={handleFeedAdded} />
+ <AddFeedForm onFeedAdded={handleChange} />
</section>
<section>
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wide text-stone-900">
Your Feeds
</h2>
- <FeedList onFeedUnsubscribed={handleFeedUnsubscribed} />
+ <FeedList key={refreshKey} onFeedUnsubscribed={handleChange} />
</section>
</div>
);
diff --git a/frontend/src/services/api-client.ts b/frontend/src/services/api-client.ts
new file mode 100644
index 0000000..c1ee475
--- /dev/null
+++ b/frontend/src/services/api-client.ts
@@ -0,0 +1,6 @@
+import createClient from "openapi-fetch";
+import type { paths } from "../api/generated";
+
+export const api = createClient<paths>({
+ credentials: "include",
+});
diff --git a/frontend/src/services/graphql-client.ts b/frontend/src/services/graphql-client.ts
deleted file mode 100644
index 4b2532a..0000000
--- a/frontend/src/services/graphql-client.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Client, cacheExchange, fetchExchange } from "urql";
-
-export const client = new Client({
- url: "/graphql",
- exchanges: [cacheExchange, fetchExchange],
- fetchOptions: {
- // Include cookies for session management
- credentials: "include",
- },
-});