Infinite scrolling setup

This commit is contained in:
Max 2025-02-05 18:08:10 +03:00
parent facaa96955
commit 3bb21f67e9
7 changed files with 231 additions and 104 deletions

View File

@ -33,6 +33,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.0.1",
"react-intersection-observer": "^9.15.1",
"react-quill": "^2.0.0",
"react-router-dom": "^6.26.2"
},
@ -6486,6 +6487,21 @@
}
}
},
"node_modules/react-intersection-observer": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.15.1.tgz",
"integrity": "sha512-vGrqYEVWXfH+AGu241uzfUpNK4HAdhCkSAyFdkMb9VWWXs6mxzBLpWCxEy9YcnDNY2g9eO6z7qUtTBdA9hc8pA==",
"license": "MIT",
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View File

@ -36,6 +36,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.0.1",
"react-intersection-observer": "^9.15.1",
"react-quill": "^2.0.0",
"react-router-dom": "^6.26.2"
},

View File

@ -4,10 +4,24 @@ export type GetRandomPostsRow = {
user_id: string;
title: string;
// created_at: Date;
}
};
export type TPostData = {
title: string;
content: string;
};
export type Post = {
blog_id: string | null;
created_at: string; // ISO 8601 date string
post_id: string;
title: string;
user_id: string;
};
export type SelectedPostsResponse = {
selected_posts: Post[];
has_next_page: boolean;
next_page_index: number;
prev_page_index: number;
};

View File

@ -1,66 +1,90 @@
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Container, Flex, Heading, Separator } from "@radix-ui/themes";
import { useQuery } from "@tanstack/react-query";
import {
Box,
Container,
Flex,
Heading,
ScrollArea,
Separator,
} from "@radix-ui/themes";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { GetRandomPostsRow } from "../../@types/PostTypes";
import { useInView } from "react-intersection-observer";
import { SelectedPostsResponse } from "../../@types/PostTypes";
import { axiosLocalhost } from "../../api/axios/axios";
import PostCard from "./PostCard/PostCard";
const LIMIT = 10;
const LIMIT = 3;
export default function RandomPostsPage() {
const {t} = useTranslation()
const { t } = useTranslation();
const { data, refetch } = useQuery({
queryKey: ["random_posts_key"],
queryFn: async () => {
try {
const [ref, inView] = useInView();
const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: [`random_post_inf`],
queryFn: async ({ pageParam }): Promise<SelectedPostsResponse> => {
const response = await axiosLocalhost.get(
`/posts/random?limit=${LIMIT}`
`/posts/random?limit=${LIMIT}&offset=${pageParam}`
);
return response.data as GetRandomPostsRow[];
} catch (error) {
console.log(`Something went wrong`);
}
return [];
return response.data as SelectedPostsResponse;
},
initialPageParam: 0,
getPreviousPageParam: (lastPage) =>
lastPage.prev_page_index < 0 ? undefined : lastPage.prev_page_index,
getNextPageParam: (lastPage) =>
lastPage.next_page_index < 0 ? undefined : lastPage.next_page_index,
});
// const { data, refetch } = useQuery({
// queryKey: ["random_posts_key"],
// queryFn: async () => {
// try {
// const response = await axiosLocalhost.get(
// `/posts/random?limit=${LIMIT}&offset=0`
// );
// return response.data as GetRandomPostsRow[];
// } catch (error) {
// console.log(`Something went wrong`);
// }
// return [];
// },
// });
useEffect(() => {
if (inView) {
console.log(`Loading more posts...`);
if (hasNextPage) fetchNextPage();
}
}, [inView]);
return (
<>
<Flex direction={"column"} className="mx-auto">
<Flex direction={"column"} className="mx-auto overflow-hidden">
<Heading size={"9"} weight={"regular"} className="text-center">
{t("discover")}
</Heading>
<Separator size={"4"} className="my-8" />
<ScrollArea.Root className="w-full h-full overflow-hidden">
<ScrollArea.Viewport className="overflow-scroll rounded size-full">
{data?.map((post, i) => {
<ScrollArea>
{data?.pages.map((post, i) => {
return (
<>
{post.selected_posts.map((post) => {
return (
<Container size={"3"} key={`post${i}`}>
<PostCard post={post} />
</Container>
);
})}
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="z-50 flex touch-none select-none p-0.5 w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="relative flex-1 rounded-[10px] bg-slate-200"/>
</ScrollArea.Scrollbar>
{/* <ScrollArea.Scrollbar
className="flex touch-none select-none bg-blackA3 p-0.5 transition-colors duration-[160ms] ease-out hover:bg-blackA5 data-[orientation=horizontal]:h-2.5 data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col"
orientation="horizontal"
>
<ScrollArea.Thumb className="relative flex-1 rounded-[10px] bg-mauve10 before:absolute before:left-1/2 before:top-1/2 before:size-full before:min-h-[44px] before:min-w-[44px] before:-translate-x-1/2 before:-translate-y-1/2" />
</ScrollArea.Scrollbar> */}
{/* <ScrollArea.Corner className="bg-blackA5" /> */}
</ScrollArea.Root>
</>
);
})}
<Box ref={ref}>qqqqqqqqqqqq</Box>
</ScrollArea>
</Flex>
</>
);

View File

@ -146,45 +146,70 @@ func (q *Queries) GetPostsByUserId(ctx context.Context, userID int64) ([]Post, e
return items, nil
}
const getRandomPosts = `-- name: GetRandomPosts :many
SELECT post_id, blog_id, user_id, title, created_at
FROM public.posts
ORDER BY RANDOM()
LIMIT $1
const getRandomPosts = `-- name: GetRandomPosts :one
WITH all_posts AS (
SELECT
COUNT(*) AS post_count
FROM
public.posts
),
filtered_posts AS (
SELECT
ARRAY(
SELECT
json_build_object(
'post_id', post_id::text, 'blog_id', blog_id::text,
'user_id', user_id::text, 'title', title,
'created_at', created_at
)
FROM
public.posts
ORDER BY
created_at DESC
LIMIT $1 OFFSET $3
) as selected_posts
)
SELECT
fp.selected_posts,
(ap.post_count - ($2 + 1) * $1)::int > 0 as has_next_page,
case
when (ap.post_count - ($2 + 1) * $1)::int > 0
then $2 + 1
else -1
end as next_page_index,
case
when (ap.post_count - ( $2 + 1 ) * $1 + 1 * $1)::int <= ap.post_count
then $2 - 1
else -1
end as prev_page_index
FROM
filtered_posts fp,
all_posts ap
`
type GetRandomPostsRow struct {
PostID int64 `json:"post_id"`
BlogID pgtype.Int8 `json:"blog_id"`
UserID int64 `json:"user_id"`
Title pgtype.Text `json:"title"`
CreatedAt pgtype.Timestamp `json:"created_at"`
type GetRandomPostsParams struct {
Column1 interface{} `json:"column_1"`
Column2 interface{} `json:"column_2"`
Offset int32 `json:"offset"`
}
func (q *Queries) GetRandomPosts(ctx context.Context, limit int32) ([]GetRandomPostsRow, error) {
rows, err := q.db.Query(ctx, getRandomPosts, limit)
if err != nil {
return nil, err
type GetRandomPostsRow struct {
SelectedPosts interface{} `json:"selected_posts"`
HasNextPage bool `json:"has_next_page"`
NextPageIndex int32 `json:"next_page_index"`
PrevPageIndex int32 `json:"prev_page_index"`
}
defer rows.Close()
var items []GetRandomPostsRow
for rows.Next() {
func (q *Queries) GetRandomPosts(ctx context.Context, arg GetRandomPostsParams) (GetRandomPostsRow, error) {
row := q.db.QueryRow(ctx, getRandomPosts, arg.Column1, arg.Column2, arg.Offset)
var i GetRandomPostsRow
if err := rows.Scan(
&i.PostID,
&i.BlogID,
&i.UserID,
&i.Title,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
err := row.Scan(
&i.SelectedPosts,
&i.HasNextPage,
&i.NextPageIndex,
&i.PrevPageIndex,
)
return i, err
}
const updatePostBlogId = `-- name: UpdatePostBlogId :exec

View File

@ -35,8 +35,42 @@ SET blog_id=$2, updated_at=CURRENT_TIMESTAMP
WHERE post_id = $1
RETURNING *;
-- name: GetRandomPosts :many
SELECT post_id, blog_id, user_id, title, created_at
FROM public.posts
ORDER BY RANDOM()
LIMIT $1;
-- name: GetRandomPosts :one
WITH all_posts AS (
SELECT
COUNT(*) AS post_count
FROM
public.posts
),
filtered_posts AS (
SELECT
ARRAY(
SELECT
json_build_object(
'post_id', post_id::text, 'blog_id', blog_id::text,
'user_id', user_id::text, 'title', title,
'created_at', created_at
)
FROM
public.posts
ORDER BY
created_at DESC
LIMIT $1 OFFSET $3
) as selected_posts
)
SELECT
fp.selected_posts,
(ap.post_count - ($2 + 1) * $1)::int > 0 as has_next_page,
case
when (ap.post_count - ($2 + 1) * $1)::int > 0
then $2 + 1
else -1
end as next_page_index,
case
when (ap.post_count - ( $2 + 1 ) * $1 + 1 * $1)::int <= ap.post_count
then $2 - 1
else -1
end as prev_page_index
FROM
filtered_posts fp,
all_posts ap;

View File

@ -19,25 +19,38 @@ func GetRandomPost(c *gin.Context) {
return
}
postsData, err :=
db_repo.New(db_connection.Dbx).
GetRandomPosts(context.Background(), int32(limit))
offset, err := strconv.Atoi(c.DefaultQuery("offset", "0"))
if err != nil {
rest_api_stuff.InternalErrorAnswer(c, err)
return
}
result := make([]any, 0)
for _, post := range postsData {
result = append(result, gin.H{
"post_id": strconv.Itoa(int(post.PostID)),
"title": post.Title,
"user_id": strconv.Itoa(int(post.UserID)),
})
params := db_repo.GetRandomPostsParams{
Column1: int32(limit),
Column2: int32(offset),
Offset: int32(offset * limit),
}
c.IndentedJSON(http.StatusOK, result)
postsData, err :=
db_repo.New(db_connection.Dbx).
GetRandomPosts(context.Background(), params)
if err != nil {
rest_api_stuff.InternalErrorAnswer(c, err)
return
}
// result := make([]any, 0)
// for _, post := range postsData {
// result = append(result, gin.H{
// "post_id": strconv.Itoa(int(post.PostID)),
// "title": post.Title,
// "user_id": strconv.Itoa(int(post.UserID)),
// })
// }
c.IndentedJSON(http.StatusOK, postsData)
}