diff --git a/.eslintignore b/.eslintignore index 5cd10be..380f2ef 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ +libs/graphql/types/src/api.ts libs/graphql/types/src/index.ts apps/front/.next .dockerignore \ No newline at end of file diff --git a/.eslintrc.front.json b/.eslintrc.front.json index 7a64c07..126bf5e 100644 --- a/.eslintrc.front.json +++ b/.eslintrc.front.json @@ -1,9 +1,10 @@ { - "plugins": ["jest-dom"], + "plugins": ["jest-dom", "@tanstack/query"], "extends": [ "./.eslintrc.json", "plugin:react/recommended", - "plugin:jest-dom/recommended" + "plugin:jest-dom/recommended", + "plugin:@tanstack/eslint-plugin-query/recommended" ], "env": { "jest": true }, "settings": { "react": { "version": "detect" } }, diff --git a/apps/api/src/graphql/schema.graphql b/apps/api/src/graphql/schema.graphql index e08f2fa..ecfd97e 100644 --- a/apps/api/src/graphql/schema.graphql +++ b/apps/api/src/graphql/schema.graphql @@ -60,6 +60,12 @@ type GqlCategory { products: [GqlProduct!]! } +type GqlPaginatedProducts { + id: Int! + data: [GqlProduct!]! + hasMoreData: Boolean! +} + type GqlOrderedItem { id: ID! quantity: Int! @@ -101,6 +107,7 @@ type GqlUserOrder { type Query { products: [GqlProduct!]! + productsByPage(pagination: GqlPaginationArgs!): GqlPaginatedProducts! productsWithIds(ids: [Int!]!): [GqlProduct!]! product(id: Int!): GqlProduct! categories: [GqlCategory!]! @@ -111,6 +118,11 @@ type Query { myAddresses: [GqlAddress!]! } +input GqlPaginationArgs { + offset: Int = 0 + limit: Int = 25 +} + type Mutation { signup(email: String!, lastName: String!, firstName: String!, password: String!): GqlAuthOutput! login(username: String!, password: String!): GqlAuthOutput! diff --git a/apps/api/src/modules/dtos/pagination-args.dto.ts b/apps/api/src/modules/dtos/pagination-args.dto.ts new file mode 100644 index 0000000..4eacd2a --- /dev/null +++ b/apps/api/src/modules/dtos/pagination-args.dto.ts @@ -0,0 +1,10 @@ +import { Field, InputType, Int } from '@nestjs/graphql'; + +@InputType() +export class GqlPaginationArgs { + @Field(() => Int) + offset = 0; + + @Field(() => Int) + limit = 25; +} diff --git a/apps/api/src/modules/products/closures/get-paginated.closure.ts b/apps/api/src/modules/products/closures/get-paginated.closure.ts new file mode 100644 index 0000000..4789077 --- /dev/null +++ b/apps/api/src/modules/products/closures/get-paginated.closure.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; + +import { DatabaseService, selectProduct } from '@backend/database'; + +import { GqlPaginationArgs } from '../../dtos/pagination-args.dto'; +import { GetAllSelectType } from './get-all.closure'; + +@Injectable() +export class GetPaginatedClosure { + public static Include = selectProduct({ + Category: true, + }); + + constructor(private readonly db: DatabaseService) {} + + async fetch({ + limit, + offset, + }: GqlPaginationArgs): Promise<[GetAllSelectType[], number]> { + return this.db.$transaction([ + this.db.product.findMany({ + include: GetPaginatedClosure.Include, + skip: offset, + take: limit, + }), + this.db.product.count(), + ]); + } +} diff --git a/apps/api/src/modules/products/dtos/gql.paginated-products.dto.ts b/apps/api/src/modules/products/dtos/gql.paginated-products.dto.ts new file mode 100644 index 0000000..a938f4d --- /dev/null +++ b/apps/api/src/modules/products/dtos/gql.paginated-products.dto.ts @@ -0,0 +1,16 @@ +import 'reflect-metadata'; +import { ObjectType, Field, Int } from '@nestjs/graphql'; + +import { GqlProduct } from './gql.product.dto'; + +@ObjectType() +export class GqlPaginatedProducts { + @Field(() => Int) + id: number; + + @Field(() => [GqlProduct]) + data: Array; + + @Field(() => Boolean) + hasMoreData: boolean; +} diff --git a/apps/api/src/modules/products/products.module.ts b/apps/api/src/modules/products/products.module.ts index 0a49ceb..014451b 100644 --- a/apps/api/src/modules/products/products.module.ts +++ b/apps/api/src/modules/products/products.module.ts @@ -5,12 +5,19 @@ import { DatabaseModule } from '@backend/database'; import { CategoriesModule } from '../categories/categories.module'; import { GetAllClosure } from './closures/get-all.closure'; import { GetByClosure } from './closures/get-by.closure'; +import { GetPaginatedClosure } from './closures/get-paginated.closure'; import { ProductsResolver } from './products.resolver'; import { ProductsService } from './products.service'; @Module({ imports: [DatabaseModule, forwardRef(() => CategoriesModule)], - providers: [ProductsService, ProductsResolver, GetByClosure, GetAllClosure], + providers: [ + ProductsService, + ProductsResolver, + GetByClosure, + GetAllClosure, + GetPaginatedClosure, + ], exports: [ProductsService, ProductsResolver], }) export class ProductsModule {} diff --git a/apps/api/src/modules/products/products.resolver.ts b/apps/api/src/modules/products/products.resolver.ts index 2e33e50..4397d0f 100644 --- a/apps/api/src/modules/products/products.resolver.ts +++ b/apps/api/src/modules/products/products.resolver.ts @@ -10,8 +10,11 @@ import { Product, Category } from '@prisma/client'; import { CategoriesService } from '../categories/categories.service'; import { GqlCategory } from '../categories/dtos/gql.category.dto'; +import { GqlPaginationArgs } from '../dtos/pagination-args.dto'; import { GqlProduct } from '../products/dtos/gql.product.dto'; import { ProductsService } from '../products/products.service'; +import { GetAllSelectType } from './closures/get-all.closure'; +import { GqlPaginatedProducts } from './dtos/gql.paginated-products.dto'; @Resolver(GqlProduct) export class ProductsResolver { @@ -21,10 +24,18 @@ export class ProductsResolver { ) {} @Query(() => [GqlProduct], { name: 'products' }) - async getAll(): Promise> { + async getAll(): Promise> { return this.products.getAll(); } + @Query(() => GqlPaginatedProducts, { name: 'productsByPage' }) + async getPaginatedProducts( + @Args({ name: 'pagination', type: () => GqlPaginationArgs }) + pagination: GqlPaginationArgs + ): Promise { + return this.products.getPaginated(pagination); + } + @Query(() => [GqlProduct], { name: 'productsWithIds' }) async getProductsWithIds( @Args('ids', { type: () => [Int] }) ids: Array diff --git a/apps/api/src/modules/products/products.service.ts b/apps/api/src/modules/products/products.service.ts index f1fe543..b621c9e 100644 --- a/apps/api/src/modules/products/products.service.ts +++ b/apps/api/src/modules/products/products.service.ts @@ -3,14 +3,18 @@ import { Product } from '@prisma/client'; import { DatabaseService } from '@backend/database'; +import { GqlPaginationArgs } from '../dtos/pagination-args.dto'; import { GetAllClosure, GetAllSelectType } from './closures/get-all.closure'; import { GetByClosure, GetBySelectType } from './closures/get-by.closure'; +import { GetPaginatedClosure } from './closures/get-paginated.closure'; +import { GqlPaginatedProducts } from './dtos/gql.paginated-products.dto'; @Injectable() export class ProductsService { constructor( private readonly db: DatabaseService, private readonly getAllClosure: GetAllClosure, + private readonly getPaginatedClosure: GetPaginatedClosure, private readonly getByClosure: GetByClosure ) {} @@ -18,6 +22,20 @@ export class ProductsService { return this.getAllClosure.fetch(); } + async getPaginated(input: GqlPaginationArgs): Promise { + const [data, count] = await this.getPaginatedClosure.fetch(input); + + return { + id: data.length > 0 ? data.at(0).id : null, + data: data.map((product) => { + const { price, ...data } = product; + + return { ...data, price: Number(price) }; + }), + hasMoreData: data.length + input.offset < count, + }; + } + async getByIds(ids: Array): Promise> { return this.db.product.findMany({ where: { diff --git a/apps/front/src/components/specialized/shop/Shop.tsx b/apps/front/src/components/specialized/shop/Shop.tsx index 951c4f2..e1d9ba0 100644 --- a/apps/front/src/components/specialized/shop/Shop.tsx +++ b/apps/front/src/components/specialized/shop/Shop.tsx @@ -1,4 +1,4 @@ -import { useProductsQuery } from '@front/api'; +import { useInfiniteProductsByPageQuery } from '@front/api'; import ErrorCircle from '@front/assets/icons/error-circle.svg'; import { PageTitle, @@ -9,7 +9,17 @@ import { import { ArticlesList } from './articles-list/ArticlesList'; export const ShopRoot = () => { - const { status, data } = useProductsQuery(); + const { status, data, fetchNextPage, hasNextPage, isFetchingNextPage } = + useInfiniteProductsByPageQuery( + { + offset: 0, + limit: 20, + }, + { + getNextPageParam: (lastPage) => + lastPage.productsByPage.hasMoreData === true ? true : undefined, + } + ); return (
@@ -18,7 +28,15 @@ export const ShopRoot = () => { { { loading: Loading, - success: , + success: ( + + ), error: ( An error occured while fetching articles diff --git a/apps/front/src/components/specialized/shop/articles-list/ArticlesList.tsx b/apps/front/src/components/specialized/shop/articles-list/ArticlesList.tsx index 73633b0..c9455ed 100644 --- a/apps/front/src/components/specialized/shop/articles-list/ArticlesList.tsx +++ b/apps/front/src/components/specialized/shop/articles-list/ArticlesList.tsx @@ -1,21 +1,46 @@ -import { ProductsQueryData } from '@front/api'; +import { Fragment } from 'react'; +import { ProductsByPageQuery } from '@front/api'; + +import { + LoadMoreProducts, + LoadMoreProductsProps, +} from '../load-more-products/LoadMoreProducts'; import { Article } from './article/Article'; -type ArticlesListProps = { - products?: ProductsQueryData; -}; +interface ArticlesListProps extends Omit { + pages?: ProductsByPageQuery[]; +} -export const ArticlesList = ({ products }: ArticlesListProps) => { - if (!products) { +export const ArticlesList = ({ + pages, + pageParams, + fetchNextPage, + hasNextPage, + isLoading, +}: ArticlesListProps) => { + if (!pages) { return null; } return ( -
- {products.map((p) => ( -
- ))} -
+ <> +
+ {pages.map(({ productsByPage }) => ( + + {productsByPage.data.map((p) => ( +
+ ))} + + ))} +
+ + ); }; diff --git a/apps/front/src/components/specialized/shop/graphql/get-products-by-page.query.graphql b/apps/front/src/components/specialized/shop/graphql/get-products-by-page.query.graphql new file mode 100644 index 0000000..90699db --- /dev/null +++ b/apps/front/src/components/specialized/shop/graphql/get-products-by-page.query.graphql @@ -0,0 +1,18 @@ +query ProductsByPage($offset: Int!, $limit: Int!) { + productsByPage(pagination: { offset: $offset, limit: $limit }) { + id + data { + id + name + description + image + price + stock + category { + id + name + } + } + hasMoreData + } +} diff --git a/apps/front/src/components/specialized/shop/graphql/get-product.query.graphql b/apps/front/src/components/specialized/shop/graphql/get-products.query.graphql similarity index 67% rename from apps/front/src/components/specialized/shop/graphql/get-product.query.graphql rename to apps/front/src/components/specialized/shop/graphql/get-products.query.graphql index 0a67d38..12125f9 100644 --- a/apps/front/src/components/specialized/shop/graphql/get-product.query.graphql +++ b/apps/front/src/components/specialized/shop/graphql/get-products.query.graphql @@ -1,10 +1,11 @@ -query Product($id: Int!) { - product(id: $id) { +query Products { + products { id name description image price + stock category { id name diff --git a/apps/front/src/components/specialized/shop/load-more-products/LoadMoreProducts.tsx b/apps/front/src/components/specialized/shop/load-more-products/LoadMoreProducts.tsx new file mode 100644 index 0000000..17df114 --- /dev/null +++ b/apps/front/src/components/specialized/shop/load-more-products/LoadMoreProducts.tsx @@ -0,0 +1,83 @@ +import { + FetchNextPageOptions, + InfiniteQueryObserverResult, +} from '@tanstack/react-query'; +import { useCallback, useEffect } from 'react'; +import { useInView } from 'react-intersection-observer'; + +import { ProductsByPageQuery, ProductsByPageQueryVariables } from '@front/api'; +import ProductIcon from '@front/assets/icons/product-2.svg'; +import { Button } from '@front/components'; + +export interface LoadMoreProductsProps { + fetchNextPage: ( + options?: FetchNextPageOptions | undefined + ) => Promise>; + pageParams: unknown[] | undefined; + isLoading: boolean; + hasNextPage: boolean | undefined; + hasMoreData: boolean | undefined; +} + +export const LoadMoreProducts = ({ + fetchNextPage, + pageParams, + isLoading, + hasNextPage, + hasMoreData, +}: LoadMoreProductsProps) => { + const { ref, inView } = useInView(); + + const loadMore = useCallback(() => { + if (hasMoreData) { + const lastPageParam = pageParams?.at(-1) as + | ProductsByPageQueryVariables + | undefined; + + void fetchNextPage({ + pageParam: lastPageParam + ? { + offset: lastPageParam.offset + 20, + limit: lastPageParam.limit, + } + : { + offset: 20, + limit: 20, + }, + }); + } + }, [fetchNextPage, pageParams, hasMoreData]); + + const handleNext = () => { + loadMore(); + }; + + useEffect(() => { + if (inView) { + loadMore(); + } + }, [inView, loadMore]); + + return { + true: ( +
+ +
+ ), + false: ( +
+ +
Nothing more to display
+
+ ), + undefined: null, + }[`${hasNextPage}`]; +}; diff --git a/libs/frontend/assets/files/icons/product-2.svg b/libs/frontend/assets/files/icons/product-2.svg new file mode 100644 index 0000000..ec3839f --- /dev/null +++ b/libs/frontend/assets/files/icons/product-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/frontend/assets/files/icons/sad.svg b/libs/frontend/assets/files/icons/sad.svg new file mode 100644 index 0000000..79a124f --- /dev/null +++ b/libs/frontend/assets/files/icons/sad.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/frontend/components/src/button/Button.tsx b/libs/frontend/components/src/button/Button.tsx index 021e53f..0a3fd5a 100644 --- a/libs/frontend/components/src/button/Button.tsx +++ b/libs/frontend/components/src/button/Button.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren } from 'react'; +import { forwardRef, LegacyRef, PropsWithChildren } from 'react'; import Spinner from '@front/assets/icons/spinner.svg'; @@ -13,31 +13,37 @@ type ButtonProps = { loadingText?: string; }; -export const Button = ({ - onClick, - variant, - className = '', - isLoading = false, - loadingText, - children, -}: PropsWithChildren) => { - const variantStyles = getVariantClasses(variant); +export const Button = forwardRef( + ( + { + onClick, + variant, + className = '', + isLoading = false, + loadingText, + children, + }: PropsWithChildren, + ref: LegacyRef | undefined + ) => { + const variantStyles = getVariantClasses(variant); - return ( - - ); -}; + return ( + + ); + } +); diff --git a/libs/graphql/codegen/src/codegen-fetch.ts b/libs/graphql/codegen/src/codegen-fetch.ts index d87601e..5d1cc57 100644 --- a/libs/graphql/codegen/src/codegen-fetch.ts +++ b/libs/graphql/codegen/src/codegen-fetch.ts @@ -3,7 +3,10 @@ import { CodegenConfig } from '@graphql-codegen/cli'; const config: CodegenConfig = { overwrite: true, schema: 'apps/api/src/graphql/schema.graphql', - documents: ['apps/front/src/**/*.graphql'], + documents: [ + 'apps/front/src/**/*.graphql', + 'libs/frontend/components/src/**/*.graphql', + ], generates: { 'libs/graphql/types/src/api.ts': { plugins: [ @@ -12,6 +15,7 @@ const config: CodegenConfig = { 'typescript-react-query', ], config: { + addInfiniteQuery: true, pureMagicComment: true, // enforce tree-shaking fetcher: { func: './fetcher#useFetchData', diff --git a/libs/graphql/types/src/api.ts b/libs/graphql/types/src/api.ts index 2ac8eb2..4db7b43 100644 --- a/libs/graphql/types/src/api.ts +++ b/libs/graphql/types/src/api.ts @@ -1,10 +1,23 @@ -import { useMutation, useQuery, UseMutationOptions, UseQueryOptions } from '@tanstack/react-query'; +import { + useMutation, + useQuery, + useInfiniteQuery, + UseMutationOptions, + UseQueryOptions, + UseInfiniteQueryOptions, +} from '@tanstack/react-query'; import { useFetchData } from './fetcher'; export type Maybe = T | null; export type InputMaybe = Maybe; -export type Exact = { [K in keyof T]: T[K] }; -export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; -export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type Exact = { + [K in keyof T]: T[K]; +}; +export type MakeOptional = Omit & { + [SubKey in K]?: Maybe; +}; +export type MakeMaybe = Omit & { + [SubKey in K]: Maybe; +}; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -87,6 +100,18 @@ export type GqlOrderedItem = { quantity: Scalars['Int']; }; +export type GqlPaginatedProducts = { + __typename?: 'GqlPaginatedProducts'; + data: Array; + hasMoreData: Scalars['Boolean']; + id: Scalars['Int']; +}; + +export type GqlPaginationArgs = { + limit?: InputMaybe; + offset?: InputMaybe; +}; + export type GqlPartialCreditCard = { __typename?: 'GqlPartialCreditCard'; expires: Scalars['String']; @@ -140,7 +165,6 @@ export type Mutation = { signup: GqlAuthOutput; }; - export type MutationCreateAddressArgs = { city: Scalars['String']; country: Scalars['String']; @@ -148,19 +172,16 @@ export type MutationCreateAddressArgs = { zipCode: Scalars['String']; }; - export type MutationLoginArgs = { password: Scalars['String']; username: Scalars['String']; }; - export type MutationPlaceOrderArgs = { creditCard: GqlPlaceOrderInput; orderedItems: Array; }; - export type MutationSignupArgs = { email: Scalars['String']; firstName: Scalars['String']; @@ -178,37 +199,30 @@ export type Query = { myOrders: Array; product: GqlProduct; products: Array; + productsByPage: GqlPaginatedProducts; productsWithIds: Array; }; - export type QueryCategoryArgs = { id: Scalars['Int']; }; - export type QueryGetOrderArgs = { id: Scalars['Int']; }; - export type QueryProductArgs = { id: Scalars['Int']; }; +export type QueryProductsByPageArgs = { + pagination: GqlPaginationArgs; +}; export type QueryProductsWithIdsArgs = { ids: Array; }; -export type LoginMutationVariables = Exact<{ - username: Scalars['String']; - password: Scalars['String']; -}>; - - -export type LoginMutation = { __typename?: 'Mutation', login: { __typename?: 'GqlAuthOutput', id: string, token: string, email: string, firstName: string, lastName: string } }; - export type NewAddressMutationVariables = Exact<{ street: Scalars['String']; zipCode: Scalars['String']; @@ -216,35 +230,105 @@ export type NewAddressMutationVariables = Exact<{ country: Scalars['String']; }>; - -export type NewAddressMutation = { __typename?: 'Mutation', createAddress: { __typename?: 'GqlNewAddressOutput', id: string, street: string, zipCode: string, city: string, country: string } }; +export type NewAddressMutation = { + __typename?: 'Mutation'; + createAddress: { + __typename?: 'GqlNewAddressOutput'; + id: string; + street: string; + zipCode: string; + city: string; + country: string; + }; +}; export type GetOrderQueryVariables = Exact<{ id: Scalars['Int']; }>; - -export type GetOrderQuery = { __typename?: 'Query', getOrder: { __typename?: 'GqlUserOrder', createdAt: any, creditCard: { __typename?: 'GqlPartialCreditCard', number: string, expires: string }, items: Array<{ __typename?: 'GqlPartialOrderedItem', id: string, name: string, quantity: number, price: number }> } }; +export type GetOrderQuery = { + __typename?: 'Query'; + getOrder: { + __typename?: 'GqlUserOrder'; + createdAt: any; + creditCard: { + __typename?: 'GqlPartialCreditCard'; + number: string; + expires: string; + }; + items: Array<{ + __typename?: 'GqlPartialOrderedItem'; + id: string; + name: string; + quantity: number; + price: number; + }>; + }; +}; export type PlaceOrderMutationVariables = Exact<{ creditCard: GqlPlaceOrderInput; orderedItems: Array | GqlNewOrderedItem; }>; +export type PlaceOrderMutation = { + __typename?: 'Mutation'; + placeOrder: { __typename?: 'GqlPlaceOrderOutput'; orderId: number }; +}; -export type PlaceOrderMutation = { __typename?: 'Mutation', placeOrder: { __typename?: 'GqlPlaceOrderOutput', orderId: number } }; - -export type MyAddressesQueryVariables = Exact<{ [key: string]: never; }>; - +export type MyAddressesQueryVariables = Exact<{ [key: string]: never }>; -export type MyAddressesQuery = { __typename?: 'Query', myAddresses: Array<{ __typename?: 'GqlAddress', id: string, street: string, zipCode: string, city: string, country: string }> }; +export type MyAddressesQuery = { + __typename?: 'Query'; + myAddresses: Array<{ + __typename?: 'GqlAddress'; + id: string; + street: string; + zipCode: string; + city: string; + country: string; + }>; +}; -export type ProductQueryVariables = Exact<{ - id: Scalars['Int']; +export type ProductsByPageQueryVariables = Exact<{ + offset: Scalars['Int']; + limit: Scalars['Int']; }>; +export type ProductsByPageQuery = { + __typename?: 'Query'; + productsByPage: { + __typename?: 'GqlPaginatedProducts'; + id: number; + hasMoreData: boolean; + data: Array<{ + __typename?: 'GqlProduct'; + id: string; + name: string; + description: string; + image: string; + price: number; + stock: number; + category: { __typename?: 'GqlCategory'; id: string; name: string }; + }>; + }; +}; -export type ProductQuery = { __typename?: 'Query', product: { __typename?: 'GqlProduct', id: string, name: string, description: string, image: string, price: number, category: { __typename?: 'GqlCategory', id: string, name: string } } }; +export type ProductsQueryVariables = Exact<{ [key: string]: never }>; + +export type ProductsQuery = { + __typename?: 'Query'; + products: Array<{ + __typename?: 'GqlProduct'; + id: string; + name: string; + description: string; + image: string; + price: number; + stock: number; + category: { __typename?: 'GqlCategory'; id: string; name: string }; + }>; +}; export type SignupMutationVariables = Exact<{ email: Scalars['String']; @@ -253,54 +337,82 @@ export type SignupMutationVariables = Exact<{ password: Scalars['String']; }>; +export type SignupMutation = { + __typename?: 'Mutation'; + signup: { __typename?: 'GqlAuthOutput'; id: string; token: string }; +}; -export type SignupMutation = { __typename?: 'Mutation', signup: { __typename?: 'GqlAuthOutput', id: string, token: string } }; - -export type CategoriesQueryVariables = Exact<{ [key: string]: never; }>; - +export type CategoriesQueryVariables = Exact<{ [key: string]: never }>; -export type CategoriesQuery = { __typename?: 'Query', categories: Array<{ __typename?: 'GqlCategory', id: string, name: string, products: Array<{ __typename?: 'GqlProduct', id: string, name: string, description: string, price: number }> }> }; +export type CategoriesQuery = { + __typename?: 'Query'; + categories: Array<{ + __typename?: 'GqlCategory'; + id: string; + name: string; + products: Array<{ + __typename?: 'GqlProduct'; + id: string; + name: string; + description: string; + price: number; + }>; + }>; +}; export type CategoryQueryVariables = Exact<{ id: Scalars['Int']; }>; - -export type CategoryQuery = { __typename?: 'Query', category: { __typename?: 'GqlCategory', id: string, name: string, products: Array<{ __typename?: 'GqlProduct', id: string, name: string, description: string, price: number }> } }; +export type CategoryQuery = { + __typename?: 'Query'; + category: { + __typename?: 'GqlCategory'; + id: string; + name: string; + products: Array<{ + __typename?: 'GqlProduct'; + id: string; + name: string; + description: string; + price: number; + }>; + }; +}; export type ProductsWithIdsQueryVariables = Exact<{ ids: Array | Scalars['Int']; }>; +export type ProductsWithIdsQuery = { + __typename?: 'Query'; + productsWithIds: Array<{ + __typename?: 'GqlProduct'; + id: string; + name: string; + description: string; + image: string; + price: number; + }>; +}; -export type ProductsWithIdsQuery = { __typename?: 'Query', productsWithIds: Array<{ __typename?: 'GqlProduct', id: string, name: string, description: string, image: string, price: number }> }; - -export type ProductsQueryVariables = Exact<{ [key: string]: never; }>; - - -export type ProductsQuery = { __typename?: 'Query', products: Array<{ __typename?: 'GqlProduct', id: string, name: string, description: string, image: string, price: number, stock: number, category: { __typename?: 'GqlCategory', id: string, name: string } }> }; +export type LoginMutationVariables = Exact<{ + username: Scalars['String']; + password: Scalars['String']; +}>; +export type LoginMutation = { + __typename?: 'Mutation'; + login: { + __typename?: 'GqlAuthOutput'; + id: string; + token: string; + email: string; + firstName: string; + lastName: string; + }; +}; -export const LoginDocument = /*#__PURE__*/ ` - mutation Login($username: String!, $password: String!) { - login(username: $username, password: $password) { - id - token - email - firstName - lastName - } -} - `; -export const useLoginMutation = < - TError = unknown, - TContext = unknown - >(options?: UseMutationOptions) => - useMutation( - ['Login'], - useFetchData(LoginDocument), - options - ); export const NewAddressDocument = /*#__PURE__*/ ` mutation NewAddress($street: String!, $zipCode: String!, $city: String!, $country: String!) { createAddress( @@ -317,15 +429,26 @@ export const NewAddressDocument = /*#__PURE__*/ ` } } `; -export const useNewAddressMutation = < - TError = unknown, - TContext = unknown - >(options?: UseMutationOptions) => - useMutation( - ['NewAddress'], - useFetchData(NewAddressDocument), - options - ); +export const useNewAddressMutation = ( + options?: UseMutationOptions< + NewAddressMutation, + TError, + NewAddressMutationVariables, + TContext + > +) => + useMutation< + NewAddressMutation, + TError, + NewAddressMutationVariables, + TContext + >( + ['NewAddress'], + useFetchData( + NewAddressDocument + ), + options + ); export const GetOrderDocument = /*#__PURE__*/ ` query GetOrder($id: Int!) { getOrder(id: $id) { @@ -343,18 +466,19 @@ export const GetOrderDocument = /*#__PURE__*/ ` } } `; -export const useGetOrderQuery = < - TData = GetOrderQuery, - TError = unknown - >( - variables: GetOrderQueryVariables, - options?: UseQueryOptions - ) => - useQuery( - ['GetOrder', variables], - useFetchData(GetOrderDocument).bind(null, variables), - options - ); +export const useGetOrderQuery = ( + variables: GetOrderQueryVariables, + options?: UseQueryOptions +) => + useQuery( + ['GetOrder', variables], + useFetchData(GetOrderDocument).bind( + null, + variables + ), + options + ); + export const PlaceOrderDocument = /*#__PURE__*/ ` mutation PlaceOrder($creditCard: GqlPlaceOrderInput!, $orderedItems: [GqlNewOrderedItem!]!) { placeOrder(creditCard: $creditCard, orderedItems: $orderedItems) { @@ -362,15 +486,26 @@ export const PlaceOrderDocument = /*#__PURE__*/ ` } } `; -export const usePlaceOrderMutation = < - TError = unknown, - TContext = unknown - >(options?: UseMutationOptions) => - useMutation( - ['PlaceOrder'], - useFetchData(PlaceOrderDocument), - options - ); +export const usePlaceOrderMutation = ( + options?: UseMutationOptions< + PlaceOrderMutation, + TError, + PlaceOrderMutationVariables, + TContext + > +) => + useMutation< + PlaceOrderMutation, + TError, + PlaceOrderMutationVariables, + TContext + >( + ['PlaceOrder'], + useFetchData( + PlaceOrderDocument + ), + options + ); export const MyAddressesDocument = /*#__PURE__*/ ` query MyAddresses { myAddresses { @@ -382,26 +517,78 @@ export const MyAddressesDocument = /*#__PURE__*/ ` } } `; -export const useMyAddressesQuery = < - TData = MyAddressesQuery, - TError = unknown - >( - variables?: MyAddressesQueryVariables, - options?: UseQueryOptions - ) => - useQuery( - variables === undefined ? ['MyAddresses'] : ['MyAddresses', variables], - useFetchData(MyAddressesDocument).bind(null, variables), - options - ); -export const ProductDocument = /*#__PURE__*/ ` - query Product($id: Int!) { - product(id: $id) { +export const useMyAddressesQuery = ( + variables?: MyAddressesQueryVariables, + options?: UseQueryOptions +) => + useQuery( + variables === undefined ? ['MyAddresses'] : ['MyAddresses', variables], + useFetchData( + MyAddressesDocument + ).bind(null, variables), + options + ); + +export const ProductsByPageDocument = /*#__PURE__*/ ` + query ProductsByPage($offset: Int!, $limit: Int!) { + productsByPage(pagination: {offset: $offset, limit: $limit}) { + id + data { + id + name + description + image + price + stock + category { + id + name + } + } + hasMoreData + } +} + `; +export const useProductsByPageQuery = < + TData = ProductsByPageQuery, + TError = unknown +>( + variables: ProductsByPageQueryVariables, + options?: UseQueryOptions +) => + useQuery( + ['ProductsByPage', variables], + useFetchData( + ProductsByPageDocument + ).bind(null, variables), + options + ); +export const useInfiniteProductsByPageQuery = < + TData = ProductsByPageQuery, + TError = unknown +>( + variables: ProductsByPageQueryVariables, + options?: UseInfiniteQueryOptions +) => { + const query = useFetchData( + ProductsByPageDocument + ); + return useInfiniteQuery( + ['ProductsByPage.infinite', variables], + (metaData) => query({ ...variables, ...(metaData.pageParam ?? {}) }), + options + ); +}; + +export const ProductsDocument = /*#__PURE__*/ ` + query Products { + products { id name description image price + stock category { id name @@ -409,18 +596,19 @@ export const ProductDocument = /*#__PURE__*/ ` } } `; -export const useProductQuery = < - TData = ProductQuery, - TError = unknown - >( - variables: ProductQueryVariables, - options?: UseQueryOptions - ) => - useQuery( - ['Product', variables], - useFetchData(ProductDocument).bind(null, variables), - options - ); +export const useProductsQuery = ( + variables?: ProductsQueryVariables, + options?: UseQueryOptions +) => + useQuery( + variables === undefined ? ['Products'] : ['Products', variables], + useFetchData(ProductsDocument).bind( + null, + variables + ), + options + ); + export const SignupDocument = /*#__PURE__*/ ` mutation Signup($email: String!, $lastName: String!, $firstName: String!, $password: String!) { signup( @@ -434,15 +622,19 @@ export const SignupDocument = /*#__PURE__*/ ` } } `; -export const useSignupMutation = < - TError = unknown, - TContext = unknown - >(options?: UseMutationOptions) => - useMutation( - ['Signup'], - useFetchData(SignupDocument), - options - ); +export const useSignupMutation = ( + options?: UseMutationOptions< + SignupMutation, + TError, + SignupMutationVariables, + TContext + > +) => + useMutation( + ['Signup'], + useFetchData(SignupDocument), + options + ); export const CategoriesDocument = /*#__PURE__*/ ` query Categories { categories { @@ -457,18 +649,18 @@ export const CategoriesDocument = /*#__PURE__*/ ` } } `; -export const useCategoriesQuery = < - TData = CategoriesQuery, - TError = unknown - >( - variables?: CategoriesQueryVariables, - options?: UseQueryOptions - ) => - useQuery( - variables === undefined ? ['Categories'] : ['Categories', variables], - useFetchData(CategoriesDocument).bind(null, variables), - options - ); +export const useCategoriesQuery = ( + variables?: CategoriesQueryVariables, + options?: UseQueryOptions +) => + useQuery( + variables === undefined ? ['Categories'] : ['Categories', variables], + useFetchData( + CategoriesDocument + ).bind(null, variables), + options + ); + export const CategoryDocument = /*#__PURE__*/ ` query Category($id: Int!) { category(id: $id) { @@ -483,18 +675,40 @@ export const CategoryDocument = /*#__PURE__*/ ` } } `; -export const useCategoryQuery = < - TData = CategoryQuery, - TError = unknown - >( - variables: CategoryQueryVariables, - options?: UseQueryOptions - ) => - useQuery( - ['Category', variables], - useFetchData(CategoryDocument).bind(null, variables), - options - ); +export const useCategoryQuery = ( + variables: CategoryQueryVariables, + options?: UseQueryOptions +) => + useQuery( + ['Category', variables], + useFetchData(CategoryDocument).bind( + null, + variables + ), + options + ); +export const useInfiniteCategoryQuery = < + TData = CategoryQuery, + TError = unknown +>( + pageParamKey: keyof CategoryQueryVariables, + variables: CategoryQueryVariables, + options?: UseInfiniteQueryOptions +) => { + const query = useFetchData( + CategoryDocument + ); + return useInfiniteQuery( + ['Category.infinite', variables], + (metaData) => + query({ + ...variables, + ...(metaData.pageParam ? { [pageParamKey]: metaData.pageParam } : {}), + }), + options + ); +}; + export const ProductsWithIdsDocument = /*#__PURE__*/ ` query ProductsWithIds($ids: [Int!]!) { productsWithIds(ids: $ids) { @@ -507,42 +721,63 @@ export const ProductsWithIdsDocument = /*#__PURE__*/ ` } `; export const useProductsWithIdsQuery = < - TData = ProductsWithIdsQuery, - TError = unknown - >( - variables: ProductsWithIdsQueryVariables, - options?: UseQueryOptions - ) => - useQuery( - ['ProductsWithIds', variables], - useFetchData(ProductsWithIdsDocument).bind(null, variables), - options - ); -export const ProductsDocument = /*#__PURE__*/ ` - query Products { - products { + TData = ProductsWithIdsQuery, + TError = unknown +>( + variables: ProductsWithIdsQueryVariables, + options?: UseQueryOptions +) => + useQuery( + ['ProductsWithIds', variables], + useFetchData( + ProductsWithIdsDocument + ).bind(null, variables), + options + ); +export const useInfiniteProductsWithIdsQuery = < + TData = ProductsWithIdsQuery, + TError = unknown +>( + pageParamKey: keyof ProductsWithIdsQueryVariables, + variables: ProductsWithIdsQueryVariables, + options?: UseInfiniteQueryOptions +) => { + const query = useFetchData< + ProductsWithIdsQuery, + ProductsWithIdsQueryVariables + >(ProductsWithIdsDocument); + return useInfiniteQuery( + ['ProductsWithIds.infinite', variables], + (metaData) => + query({ + ...variables, + ...(metaData.pageParam ? { [pageParamKey]: metaData.pageParam } : {}), + }), + options + ); +}; + +export const LoginDocument = /*#__PURE__*/ ` + mutation Login($username: String!, $password: String!) { + login(username: $username, password: $password) { id - name - description - image - price - stock - category { - id - name - } + token + email + firstName + lastName } } `; -export const useProductsQuery = < - TData = ProductsQuery, - TError = unknown - >( - variables?: ProductsQueryVariables, - options?: UseQueryOptions - ) => - useQuery( - variables === undefined ? ['Products'] : ['Products', variables], - useFetchData(ProductsDocument).bind(null, variables), - options - ); \ No newline at end of file +export const useLoginMutation = ( + options?: UseMutationOptions< + LoginMutation, + TError, + LoginMutationVariables, + TContext + > +) => + useMutation( + ['Login'], + useFetchData(LoginDocument), + options + ); diff --git a/package.json b/package.json index 60807c7..d962b80 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.39.4", + "react-intersection-observer": "^9.4.1", "reflect-metadata": "^0.1.13", "regenerator-runtime": "0.13.11", "rxjs": "^7.0.0", @@ -81,6 +82,7 @@ "@nrwl/web": "15.1.1", "@nrwl/workspace": "15.1.1", "@svgr/webpack": "^6.5.1", + "@tanstack/eslint-plugin-query": "^4.15.1", "@testing-library/react": "13.4.0", "@types/bcrypt": "^5.0.0", "@types/jest": "29.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25e9dc4..6d4afb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,7 @@ specifiers: '@nrwl/workspace': 15.1.1 '@prisma/client': ^4.6.1 '@svgr/webpack': ^6.5.1 + '@tanstack/eslint-plugin-query': ^4.15.1 '@tanstack/react-query': ^4.16.1 '@testing-library/react': 13.4.0 '@types/bcrypt': ^5.0.0 @@ -91,6 +92,7 @@ specifiers: react: 18.2.0 react-dom: 18.2.0 react-hook-form: ^7.39.4 + react-intersection-observer: ^9.4.1 react-test-renderer: 18.2.0 readme-package-icons: ^1.1.7 reflect-metadata: ^0.1.13 @@ -135,6 +137,7 @@ dependencies: react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-hook-form: 7.39.4_react@18.2.0 + react-intersection-observer: 9.4.1_react@18.2.0 reflect-metadata: 0.1.13 regenerator-runtime: 0.13.11 rxjs: 7.5.7 @@ -168,6 +171,7 @@ devDependencies: '@nrwl/web': 15.1.1_2s3yboin3pfvf7uzmuiabz7bia '@nrwl/workspace': 15.1.1_gayucdlmwoehp4xp2baonpzyea '@svgr/webpack': 6.5.1 + '@tanstack/eslint-plugin-query': 4.15.1 '@testing-library/react': 13.4.0_biqbaboplfbrettd7655fr4n2y '@types/bcrypt': 5.0.0 '@types/jest': 29.2.3 @@ -4601,6 +4605,10 @@ packages: dependencies: tslib: 2.4.1 + /@tanstack/eslint-plugin-query/4.15.1: + resolution: {integrity: sha512-SMj7OsOkutuJQ9LQLIoXxW5u0kx84V7RICmJEcjpKNkHsBd31QttJvtG1hLx9GXb3ZuFLZ0Y1dOD/D9b/GIzTg==} + dev: true + /@tanstack/query-core/4.15.1: resolution: {integrity: sha512-+UfqJsNbPIVo0a9ANW0ZxtjiMfGLaaoIaL9vZeVycvmBuWywJGtSi7fgPVMCPdZQFOzMsaXaOsDtSKQD5xLRVQ==} dev: false @@ -12683,6 +12691,14 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false + /react-intersection-observer/9.4.1_react@18.2.0: + resolution: {integrity: sha512-IXpIsPe6BleFOEHKzKh5UjwRUaz/JYS0lT/HPsupWEQou2hDqjhLMStc5zyE3eQVT4Fk3FufM8Fw33qW1uyeiw==} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /react-is/16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}