mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-04 23:14:27 +01:00
feat: added infinite scroll to feed page
This commit is contained in:
@@ -1,19 +1,14 @@
|
||||
import { useCallback } from "react"
|
||||
import { Post } from "src/features/Posts/types"
|
||||
import { useReachedBottom } from "src/utils/hooks/useReachedBottom"
|
||||
import { ListProps } from "src/utils/interfaces"
|
||||
import { ListComponentProps } from "src/utils/interfaces"
|
||||
import PostCard, { PostCardSkeleton } from "../PostCard"
|
||||
|
||||
type Props = ListProps<Post>
|
||||
type Props = ListComponentProps<Post>
|
||||
|
||||
export default function PostsList(props: Props) {
|
||||
|
||||
|
||||
const reachedBottom = useCallback(() => {
|
||||
console.log("NEW FETCH")
|
||||
}, [])
|
||||
|
||||
const { ref } = useReachedBottom<HTMLDivElement>(reachedBottom)
|
||||
const { ref } = useReachedBottom<HTMLDivElement>(props.onReachedBottom)
|
||||
|
||||
if (props.isLoading)
|
||||
return <div className="flex flex-col gap-24">
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
|
||||
import { useFeedQuery } from 'src/graphql'
|
||||
import { MOCK_DATA } from 'src/mocks/data'
|
||||
import { useInfiniteQuery } from 'src/utils/hooks'
|
||||
import PostsList from '../../Components/PostsList/PostsList'
|
||||
import TrendingCard from '../../Components/TrendingCard/TrendingCard'
|
||||
import PopularCategories from './PopularCategories/PopularCategories'
|
||||
import SortBy from './SortBy/SortBy'
|
||||
import styles from './styles.module.css'
|
||||
|
||||
|
||||
export default function FeedPage() {
|
||||
|
||||
const feedQuery = useFeedQuery()
|
||||
const feedQuery = useFeedQuery({
|
||||
variables: {
|
||||
take: 10,
|
||||
skip: 0
|
||||
}
|
||||
})
|
||||
const { fetchMore, isFetchingMore } = useInfiniteQuery(feedQuery, 'getFeed')
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -22,7 +29,12 @@ export default function FeedPage() {
|
||||
<PopularCategories />
|
||||
</div>
|
||||
</aside>
|
||||
<PostsList isLoading={feedQuery.loading} items={feedQuery.data?.getFeed} />
|
||||
<PostsList
|
||||
isLoading={feedQuery.loading}
|
||||
items={feedQuery.data?.getFeed}
|
||||
isFetching={isFetchingMore}
|
||||
onReachedBottom={fetchMore}
|
||||
/>
|
||||
<aside>
|
||||
<div className="sticky top-16">
|
||||
<TrendingCard />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
query Feed {
|
||||
getFeed {
|
||||
query Feed($skip: Int, $take: Int) {
|
||||
getFeed(skip: $skip, take: $take) {
|
||||
... on Story {
|
||||
id
|
||||
title
|
||||
|
||||
@@ -254,7 +254,10 @@ export type SearchProjectsQueryVariables = Exact<{
|
||||
|
||||
export type SearchProjectsQuery = { __typename?: 'Query', searchProjects: Array<{ __typename?: 'Project', id: number, thumbnail_image: string, title: string, category: { __typename?: 'Category', title: string, id: number } }> };
|
||||
|
||||
export type FeedQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
export type FeedQueryVariables = Exact<{
|
||||
skip: InputMaybe<Scalars['Int']>;
|
||||
take: InputMaybe<Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type FeedQuery = { __typename?: 'Query', getFeed: Array<{ __typename?: 'Bounty', id: number, title: string, date: string, excerpt: string, votes_count: number, type: string, cover_image: string, deadline: string, reward_amount: number, applicants_count: number, author: { __typename?: 'User', id: number, name: string, image: string }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Question', id: number, title: string, date: string, excerpt: string, votes_count: number, type: string, cover_image: string, deadline: string, reward_amount: number, answers_count: number, author: { __typename?: 'User', id: number, name: string, image: string }, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, comments: Array<{ __typename?: 'PostComment', id: number, date: string, body: string, author: { __typename?: 'User', id: number, name: string, image: string } }> } | { __typename?: 'Story', id: number, title: string, date: string, excerpt: string, votes_count: number, type: string, cover_image: string, comments_count: number, author: { __typename?: 'User', id: number, name: string, image: string }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> }> };
|
||||
@@ -391,8 +394,8 @@ export type SearchProjectsQueryHookResult = ReturnType<typeof useSearchProjectsQ
|
||||
export type SearchProjectsLazyQueryHookResult = ReturnType<typeof useSearchProjectsLazyQuery>;
|
||||
export type SearchProjectsQueryResult = Apollo.QueryResult<SearchProjectsQuery, SearchProjectsQueryVariables>;
|
||||
export const FeedDocument = gql`
|
||||
query Feed {
|
||||
getFeed {
|
||||
query Feed($skip: Int, $take: Int) {
|
||||
getFeed(skip: $skip, take: $take) {
|
||||
... on Story {
|
||||
id
|
||||
title
|
||||
@@ -480,6 +483,8 @@ export const FeedDocument = gql`
|
||||
* @example
|
||||
* const { data, loading, error } = useFeedQuery({
|
||||
* variables: {
|
||||
* skip: // value for 'skip'
|
||||
* take: // value for 'take'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -87,9 +87,7 @@ posts.bounties = posts.bounties.map(b => ({ ...b, __typename: "Bounty" }))
|
||||
posts.questions = posts.questions.map(b => ({ ...b, __typename: "Question" }))
|
||||
posts.stories = posts.stories.map(b => ({ ...b, __typename: "Story" }))
|
||||
|
||||
export const feed: Post[] = [
|
||||
...posts.stories,
|
||||
...posts.bounties,
|
||||
...posts.questions,
|
||||
]
|
||||
export const feed: Post[] = Array(30).fill(0).map((_, idx) => {
|
||||
return { ...posts.stories[0], id: idx + 1, title: `Post Title ${idx + 1}` }
|
||||
})
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
FeedQuery,
|
||||
PostDetailsQuery,
|
||||
PostDetailsQueryVariables,
|
||||
FeedQueryVariables,
|
||||
} from 'src/graphql'
|
||||
|
||||
const delay = (ms = 1000) => new Promise((res) => setTimeout(res, ms))
|
||||
@@ -99,12 +100,12 @@ export const handlers = [
|
||||
)
|
||||
}),
|
||||
|
||||
graphql.query<FeedQuery>('Feed', async (req, res, ctx) => {
|
||||
graphql.query<FeedQuery, FeedQueryVariables>('Feed', async (req, res, ctx) => {
|
||||
await delay()
|
||||
|
||||
const { take, skip } = req.variables;
|
||||
return res(
|
||||
ctx.data({
|
||||
getFeed: getFeed()
|
||||
getFeed: getFeed({ take, skip })
|
||||
})
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ASSETS from "src/assets";
|
||||
import { MOCK_DATA } from "./data";
|
||||
import { Query } from 'src/graphql'
|
||||
import { Query, QueryGetFeedArgs } from 'src/graphql'
|
||||
|
||||
export function getCategory(id: number) {
|
||||
|
||||
@@ -42,8 +42,10 @@ export function hottestProjects() {
|
||||
return MOCK_DATA.projects.sort((p1, p2) => p2.votes_count - p1.votes_count).slice(0, 20)
|
||||
}
|
||||
|
||||
export function getFeed(): Query['getFeed'] {
|
||||
return MOCK_DATA.feed as any;
|
||||
export function getFeed(config: QueryGetFeedArgs): Query['getFeed'] {
|
||||
const take = config.take ?? 10
|
||||
const skip = config.skip ?? 0
|
||||
return MOCK_DATA.feed.slice(skip, skip + take) as any;
|
||||
}
|
||||
|
||||
export function getPostById(postId: number): Query['getPostById'] {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApolloClient, HttpLink, InMemoryCache, from } from "@apollo/client";
|
||||
import { ApolloClient, HttpLink, InMemoryCache, from, Reference, FieldPolicy } from "@apollo/client";
|
||||
import { onError } from "@apollo/client/link/error";
|
||||
import { RetryLink } from "@apollo/client/link/retry";
|
||||
|
||||
@@ -45,5 +45,40 @@ export const apolloClient = new ApolloClient({
|
||||
errorLink,
|
||||
httpLink
|
||||
]),
|
||||
cache: new InMemoryCache()
|
||||
cache: new InMemoryCache({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
getFeed: offsetLimitPagination()
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
type KeyArgs = FieldPolicy<any>["keyArgs"];
|
||||
|
||||
function offsetLimitPagination<T = Reference>(
|
||||
keyArgs: KeyArgs = false,
|
||||
): FieldPolicy<T[]> {
|
||||
return {
|
||||
keyArgs,
|
||||
merge(existing, incoming, { args }) {
|
||||
const merged = existing ? existing.slice(0) : [];
|
||||
if (args) {
|
||||
// Assume an skip of 0 if args.skip omitted.
|
||||
const { skip = 0 } = args;
|
||||
for (let i = 0; i < incoming.length; ++i) {
|
||||
merged[skip + i] = incoming[i];
|
||||
}
|
||||
} else {
|
||||
// It's unusual (probably a mistake) for a paginated field not
|
||||
// to receive any arguments, so you might prefer to throw an
|
||||
// exception here, instead of recovering by appending incoming
|
||||
// onto the existing array.
|
||||
merged.push.apply(merged, [...incoming]);
|
||||
}
|
||||
return merged;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from "./storeHooks";
|
||||
export * from "./useResizeListener";
|
||||
export * from "./usePressHolder";
|
||||
export * from "./useInfiniteQuery";
|
||||
export * from "./useReachedBottom";
|
||||
|
||||
34
src/utils/hooks/useInfiniteQuery.ts
Normal file
34
src/utils/hooks/useInfiniteQuery.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { QueryResult } from "@apollo/client";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export const useInfiniteQuery = (query: QueryResult, dataField: string) => {
|
||||
const [fetchingMore, setFetchingMore] = useState(false)
|
||||
const [reachedLastPage, setReachedLastPage] = useState(false)
|
||||
|
||||
|
||||
const fetchMore = useCallback(
|
||||
() => {
|
||||
if (!fetchingMore && !reachedLastPage) {
|
||||
setFetchingMore(true);
|
||||
// console.log(feedQuery.variables?.skip);
|
||||
|
||||
query.fetchMore({
|
||||
variables: {
|
||||
skip: query.data?.[dataField].length,
|
||||
}
|
||||
}).then((d) => {
|
||||
if (d.data?.[dataField].length === 0)
|
||||
setReachedLastPage(true)
|
||||
|
||||
setFetchingMore(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
, [dataField, fetchingMore, query, reachedLastPage]
|
||||
)
|
||||
|
||||
return {
|
||||
isFetchingMore: fetchingMore,
|
||||
fetchMore: fetchMore
|
||||
}
|
||||
}
|
||||
@@ -6,25 +6,30 @@ export const useReachedBottom = <T extends HTMLElement>(cb?: () => void, options
|
||||
const { offset = window.innerHeight, throttle = 600 } = options
|
||||
|
||||
const ref = useRef<T>(null);
|
||||
const callbackHandler = useRef<Function>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!cb)
|
||||
callbackHandler.current = undefined;
|
||||
else {
|
||||
callbackHandler.current = _debounce(cb, throttle)
|
||||
}
|
||||
}, [cb, throttle])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!cb) return;
|
||||
|
||||
const cbDebounced = _debounce(cb, throttle)
|
||||
const listener = () => {
|
||||
if (!ref.current) return;
|
||||
const curWindowPosition = window.scrollY + window.innerHeight;
|
||||
const elTriggerPosition = ref.current.offsetTop + ref.current.scrollHeight - offset;
|
||||
if (curWindowPosition > elTriggerPosition) cbDebounced();
|
||||
if (curWindowPosition > elTriggerPosition) callbackHandler.current?.();
|
||||
}
|
||||
|
||||
document.addEventListener('scroll', listener)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('scroll', listener)
|
||||
}
|
||||
}, [cb, offset, throttle])
|
||||
}, [offset, throttle])
|
||||
|
||||
return { ref }
|
||||
}
|
||||
@@ -5,7 +5,7 @@ export type Tag = {
|
||||
}
|
||||
|
||||
|
||||
export type ListProps<T> = {
|
||||
export type ListComponentProps<T> = {
|
||||
items?: T[]
|
||||
isLoading?: boolean;
|
||||
isFetching?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user