From 2889b562e64993482bd13fd806af8ed0865bab8b Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 14 Feb 2026 11:52:56 +0900 Subject: refactor: migrate API from GraphQL to REST (TypeSpec/OpenAPI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/api/generated.d.ts | 666 +++++++++++++++++++++ frontend/src/components/AddFeedForm.tsx | 20 +- frontend/src/components/ArticleItem.tsx | 33 +- frontend/src/components/ArticleList.tsx | 13 +- frontend/src/components/FeedItem.tsx | 34 +- frontend/src/components/FeedList.tsx | 50 +- frontend/src/components/FeedSidebar.tsx | 30 +- frontend/src/contexts/AuthContext.tsx | 75 +-- frontend/src/graphql/generated/fragment-masking.ts | 87 --- frontend/src/graphql/generated/gql.ts | 52 -- frontend/src/graphql/generated/graphql.ts | 304 ---------- frontend/src/graphql/generated/index.ts | 2 - frontend/src/graphql/mutations.graphql | 65 -- frontend/src/graphql/queries.graphql | 94 --- frontend/src/graphql/schema.graphql | 1 - frontend/src/hooks/usePaginatedArticles.ts | 68 +-- frontend/src/main.tsx | 10 +- frontend/src/pages/Settings.tsx | 21 +- frontend/src/services/api-client.ts | 6 + frontend/src/services/graphql-client.ts | 10 - 20 files changed, 837 insertions(+), 804 deletions(-) create mode 100644 frontend/src/api/generated.d.ts delete mode 100644 frontend/src/graphql/generated/fragment-masking.ts delete mode 100644 frontend/src/graphql/generated/gql.ts delete mode 100644 frontend/src/graphql/generated/graphql.ts delete mode 100644 frontend/src/graphql/generated/index.ts delete mode 100644 frontend/src/graphql/mutations.graphql delete mode 100644 frontend/src/graphql/queries.graphql delete mode 120000 frontend/src/graphql/schema.graphql create mode 100644 frontend/src/services/api-client.ts delete mode 100644 frontend/src/services/graphql-client.ts (limited to 'frontend/src') 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; +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; +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(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[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([]); + const [fetching, setFetching] = useState(true); + const [error, setError] = useState(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 (
- Error: {error.message} + Error: {error}
); } - if (!data?.feeds || data.feeds.length === 0) { + if (feeds.length === 0) { return (

No feeds added yet.

@@ -38,11 +63,12 @@ export function FeedList({ onFeedUnsubscribed }: Props) { return (
- {data.feeds.map((feed) => ( + {feeds.map((feed) => ( ))}
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([]); + 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 && (
  • Loading...
  • )} - {data?.feeds.map((feed) => ( + {feeds.map((feed) => (