diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-14 11:52:56 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-14 11:53:08 +0900 |
| commit | 2889b562e64993482bd13fd806af8ed0865bab8b (patch) | |
| tree | 39400ac4d994fb33d2c544e7d4b9d98f8ecbd86a /frontend/src | |
| parent | e216c3bc97994b4172d15d52b46d5f6b75f35ea4 (diff) | |
| download | feedaka-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.ts | 666 | ||||
| -rw-r--r-- | frontend/src/components/AddFeedForm.tsx | 20 | ||||
| -rw-r--r-- | frontend/src/components/ArticleItem.tsx | 33 | ||||
| -rw-r--r-- | frontend/src/components/ArticleList.tsx | 13 | ||||
| -rw-r--r-- | frontend/src/components/FeedItem.tsx | 34 | ||||
| -rw-r--r-- | frontend/src/components/FeedList.tsx | 50 | ||||
| -rw-r--r-- | frontend/src/components/FeedSidebar.tsx | 30 | ||||
| -rw-r--r-- | frontend/src/contexts/AuthContext.tsx | 75 | ||||
| -rw-r--r-- | frontend/src/graphql/generated/fragment-masking.ts | 87 | ||||
| -rw-r--r-- | frontend/src/graphql/generated/gql.ts | 52 | ||||
| -rw-r--r-- | frontend/src/graphql/generated/graphql.ts | 304 | ||||
| -rw-r--r-- | frontend/src/graphql/generated/index.ts | 2 | ||||
| -rw-r--r-- | frontend/src/graphql/mutations.graphql | 65 | ||||
| -rw-r--r-- | frontend/src/graphql/queries.graphql | 94 | ||||
| l--------- | frontend/src/graphql/schema.graphql | 1 | ||||
| -rw-r--r-- | frontend/src/hooks/usePaginatedArticles.ts | 68 | ||||
| -rw-r--r-- | frontend/src/main.tsx | 10 | ||||
| -rw-r--r-- | frontend/src/pages/Settings.tsx | 21 | ||||
| -rw-r--r-- | frontend/src/services/api-client.ts | 6 | ||||
| -rw-r--r-- | frontend/src/services/graphql-client.ts | 10 |
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", - }, -}); |
