aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--frontend/biome.json4
-rw-r--r--frontend/graphql-codegen.ts12
-rw-r--r--frontend/package-lock.json46
-rw-r--r--frontend/package.json1
-rw-r--r--frontend/src/graphql/generated/fragment-masking.ts87
-rw-r--r--frontend/src/graphql/generated/gql.ts52
-rw-r--r--frontend/src/graphql/generated/graphql.ts212
-rw-r--r--frontend/src/graphql/generated/index.ts2
-rw-r--r--frontend/src/graphql/mutations.graphql52
-rw-r--r--frontend/src/graphql/queries.graphql73
10 files changed, 539 insertions, 2 deletions
diff --git a/frontend/biome.json b/frontend/biome.json
index 5500f2c..d589e28 100644
--- a/frontend/biome.json
+++ b/frontend/biome.json
@@ -4,5 +4,9 @@
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
+ },
+ "files": {
+ "includes": ["**", "!src/graphql/generated/*.ts"],
+ "ignoreUnknown": true
}
}
diff --git a/frontend/graphql-codegen.ts b/frontend/graphql-codegen.ts
index ddba3b1..af62625 100644
--- a/frontend/graphql-codegen.ts
+++ b/frontend/graphql-codegen.ts
@@ -3,11 +3,19 @@ import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
overwrite: true,
schema: "../common/graphql/schema.graphql",
- documents: "src/**/*.tsx",
+ documents: ["src/**/*.tsx", "src/**/*.ts", "src/**/*.graphql"],
generates: {
- "src/graphql": {
+ "src/graphql/generated/": {
preset: "client",
plugins: [],
+ config: {
+ enumsAsTypes: true,
+ skipTypename: true,
+ useTypeImports: true,
+ scalars: {
+ DateTime: "string",
+ },
+ },
},
},
};
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index cbf5ced..bcc0bdc 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,11 +13,13 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.11",
+ "urql": "^4.2.2",
"wouter": "^3.7.1"
},
"devDependencies": {
"@biomejs/biome": "^2.1.1",
"@graphql-codegen/cli": "^5.0.7",
+ "@graphql-codegen/client-preset": "^4.8.3",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
@@ -26,6 +28,20 @@
"vite": "^7.0.3"
}
},
+ "node_modules/@0no-co/graphql.web": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.1.2.tgz",
+ "integrity": "sha512-N2NGsU5FLBhT8NZ+3l2YrzZSHITjNXNuDhC4iDiikv0IujaJ0Xc6xIxQZ/Ek3Cb+rgPjnLHYyJm11tInuJn+cw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
+ },
+ "peerDependenciesMeta": {
+ "graphql": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@@ -2605,6 +2621,16 @@
"@types/node": "*"
}
},
+ "node_modules/@urql/core": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz",
+ "integrity": "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A==",
+ "license": "MIT",
+ "dependencies": {
+ "@0no-co/graphql.web": "^1.0.13",
+ "wonka": "^6.3.2"
+ }
+ },
"node_modules/@vitejs/plugin-react": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz",
@@ -5970,6 +5996,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/urql": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/urql/-/urql-4.2.2.tgz",
+ "integrity": "sha512-3GgqNa6iF7bC4hY/ImJKN4REQILcSU9VKcKL8gfELZM8mM5BnLH1BsCc8kBdnVGD1LIFOs4W3O2idNHhON1r0w==",
+ "license": "MIT",
+ "dependencies": {
+ "@urql/core": "^5.1.1",
+ "wonka": "^6.3.2"
+ },
+ "peerDependencies": {
+ "@urql/core": "^5.0.0",
+ "react": ">= 16.8.0"
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
@@ -6134,6 +6174,12 @@
"webidl-conversions": "^3.0.0"
}
},
+ "node_modules/wonka": {
+ "version": "6.3.5",
+ "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz",
+ "integrity": "sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw==",
+ "license": "MIT"
+ },
"node_modules/wouter": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/wouter/-/wouter-3.7.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 9d809f0..6e8d3ad 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,6 +17,7 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.11",
+ "urql": "^4.2.2",
"wouter": "^3.7.1"
},
"devDependencies": {
diff --git a/frontend/src/graphql/generated/fragment-masking.ts b/frontend/src/graphql/generated/fragment-masking.ts
new file mode 100644
index 0000000..743a364
--- /dev/null
+++ b/frontend/src/graphql/generated/fragment-masking.ts
@@ -0,0 +1,87 @@
+/* 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
new file mode 100644
index 0000000..ae8c1e1
--- /dev/null
+++ b/frontend/src/graphql/generated/gql.ts
@@ -0,0 +1,52 @@
+/* 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 RemoveFeed($id: ID!) {\n removeFeed(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}": typeof types.AddFeedDocument,
+ "query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n articles {\n id\n isRead\n }\n }\n}\n\nquery GetUnreadArticles {\n unreadArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetReadArticles {\n readArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\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 }\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 RemoveFeed($id: ID!) {\n removeFeed(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}": types.AddFeedDocument,
+ "query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n articles {\n id\n isRead\n }\n }\n}\n\nquery GetUnreadArticles {\n unreadArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetReadArticles {\n readArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\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 }\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 RemoveFeed($id: ID!) {\n removeFeed(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}"): (typeof documents)["mutation AddFeed($url: String!) {\n addFeed(url: $url) {\n id\n url\n title\n fetchedAt\n }\n}\n\nmutation RemoveFeed($id: ID!) {\n removeFeed(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}"];
+/**
+ * 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 articles {\n id\n isRead\n }\n }\n}\n\nquery GetUnreadArticles {\n unreadArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetReadArticles {\n readArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\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 }\n }\n}"): (typeof documents)["query GetFeeds {\n feeds {\n id\n url\n title\n fetchedAt\n articles {\n id\n isRead\n }\n }\n}\n\nquery GetUnreadArticles {\n unreadArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetReadArticles {\n readArticles {\n id\n feedId\n guid\n title\n url\n isRead\n feed {\n id\n title\n }\n }\n}\n\nquery GetFeed($id: ID!) {\n feed(id: $id) {\n id\n url\n title\n fetchedAt\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 }\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
new file mode 100644
index 0000000..a54f1b6
--- /dev/null
+++ b/frontend/src/graphql/generated/graphql.ts
@@ -0,0 +1,212 @@
+/* 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'];
+};
+
+/** 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'];
+ /** Title of the feed (extracted from feed metadata) */
+ title: Scalars['String']['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;
+ /** 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;
+ /** Remove a feed subscription and all its articles */
+ removeFeed: Scalars['Boolean']['output'];
+};
+
+
+/** Root mutation type for modifying data */
+export type MutationAddFeedArgs = {
+ url: 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 MutationRemoveFeedArgs = {
+ id: Scalars['ID']['input'];
+};
+
+/** Root query type for reading data */
+export type Query = {
+ /** Get a specific article by ID */
+ article?: Maybe<Article>;
+ /** Get a specific feed by ID */
+ feed?: Maybe<Feed>;
+ /** Get all feeds with their metadata */
+ feeds: Array<Feed>;
+ /** Get all read articles across all feeds */
+ readArticles: Array<Article>;
+ /** Get all unread articles across all feeds */
+ unreadArticles: Array<Article>;
+};
+
+
+/** 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'];
+};
+
+export type AddFeedMutationVariables = Exact<{
+ url: Scalars['String']['input'];
+}>;
+
+
+export type AddFeedMutation = { addFeed: { id: string, url: string, title: string, fetchedAt: string } };
+
+export type RemoveFeedMutationVariables = Exact<{
+ id: Scalars['ID']['input'];
+}>;
+
+
+export type RemoveFeedMutation = { removeFeed: 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 GetFeedsQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type GetFeedsQuery = { feeds: Array<{ id: string, url: string, title: string, fetchedAt: string, articles: Array<{ id: string, isRead: boolean }> }> };
+
+export type GetUnreadArticlesQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type GetUnreadArticlesQuery = { unreadArticles: Array<{ id: string, feedId: string, guid: string, title: string, url: string, isRead: boolean, feed: { id: string, title: string } }> };
+
+export type GetReadArticlesQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type GetReadArticlesQuery = { readArticles: Array<{ id: string, feedId: string, guid: string, title: string, url: string, isRead: boolean, feed: { id: string, title: string } }> };
+
+export type GetFeedQueryVariables = Exact<{
+ id: Scalars['ID']['input'];
+}>;
+
+
+export type GetFeedQuery = { feed?: { id: string, url: string, title: string, fetchedAt: string, 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 } } | 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 RemoveFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveFeed"},"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":"removeFeed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode<RemoveFeedMutation, RemoveFeedMutationVariables>;
+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 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":"articles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isRead"}}]}}]}}]}}]} as unknown as DocumentNode<GetFeedsQuery, GetFeedsQueryVariables>;
+export const GetUnreadArticlesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUnreadArticles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unreadArticles"},"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"}}]}}]}}]}}]} as unknown as DocumentNode<GetUnreadArticlesQuery, GetUnreadArticlesQueryVariables>;
+export const GetReadArticlesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetReadArticles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readArticles"},"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"}}]}}]}}]}}]} 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":"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"}}]}}]}}]}}]} as unknown as DocumentNode<GetArticleQuery, GetArticleQueryVariables>; \ No newline at end of file
diff --git a/frontend/src/graphql/generated/index.ts b/frontend/src/graphql/generated/index.ts
new file mode 100644
index 0000000..f515991
--- /dev/null
+++ b/frontend/src/graphql/generated/index.ts
@@ -0,0 +1,2 @@
+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
new file mode 100644
index 0000000..8bcbcec
--- /dev/null
+++ b/frontend/src/graphql/mutations.graphql
@@ -0,0 +1,52 @@
+mutation AddFeed($url: String!) {
+ addFeed(url: $url) {
+ id
+ url
+ title
+ fetchedAt
+ }
+}
+
+mutation RemoveFeed($id: ID!) {
+ removeFeed(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
+ }
+}
diff --git a/frontend/src/graphql/queries.graphql b/frontend/src/graphql/queries.graphql
new file mode 100644
index 0000000..af4ac01
--- /dev/null
+++ b/frontend/src/graphql/queries.graphql
@@ -0,0 +1,73 @@
+query GetFeeds {
+ feeds {
+ id
+ url
+ title
+ fetchedAt
+ articles {
+ id
+ isRead
+ }
+ }
+}
+
+query GetUnreadArticles {
+ unreadArticles {
+ id
+ feedId
+ guid
+ title
+ url
+ isRead
+ feed {
+ id
+ title
+ }
+ }
+}
+
+query GetReadArticles {
+ readArticles {
+ id
+ feedId
+ guid
+ title
+ url
+ isRead
+ feed {
+ id
+ title
+ }
+ }
+}
+
+query GetFeed($id: ID!) {
+ feed(id: $id) {
+ id
+ url
+ title
+ fetchedAt
+ articles {
+ id
+ guid
+ title
+ url
+ isRead
+ }
+ }
+}
+
+query GetArticle($id: ID!) {
+ article(id: $id) {
+ id
+ feedId
+ guid
+ title
+ url
+ isRead
+ feed {
+ id
+ title
+ }
+ }
+}