Infinite scrolling setup
This commit is contained in:
parent
facaa96955
commit
3bb21f67e9
16
enshi/package-lock.json
generated
16
enshi/package-lock.json
generated
@ -33,6 +33,7 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-i18next": "^15.0.1",
|
"react-i18next": "^15.0.1",
|
||||||
|
"react-intersection-observer": "^9.15.1",
|
||||||
"react-quill": "^2.0.0",
|
"react-quill": "^2.0.0",
|
||||||
"react-router-dom": "^6.26.2"
|
"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": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-i18next": "^15.0.1",
|
"react-i18next": "^15.0.1",
|
||||||
|
"react-intersection-observer": "^9.15.1",
|
||||||
"react-quill": "^2.0.0",
|
"react-quill": "^2.0.0",
|
||||||
"react-router-dom": "^6.26.2"
|
"react-router-dom": "^6.26.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,10 +4,24 @@ export type GetRandomPostsRow = {
|
|||||||
user_id: string;
|
user_id: string;
|
||||||
title: string;
|
title: string;
|
||||||
// created_at: Date;
|
// created_at: Date;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export type TPostData = {
|
export type TPostData = {
|
||||||
title: string;
|
title: string;
|
||||||
content: 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;
|
||||||
|
};
|
||||||
|
|||||||
@ -1,67 +1,91 @@
|
|||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import {
|
||||||
import { Container, Flex, Heading, Separator } from "@radix-ui/themes";
|
Box,
|
||||||
import { useQuery } from "@tanstack/react-query";
|
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 { 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 { axiosLocalhost } from "../../api/axios/axios";
|
||||||
import PostCard from "./PostCard/PostCard";
|
import PostCard from "./PostCard/PostCard";
|
||||||
|
|
||||||
const LIMIT = 10;
|
const LIMIT = 3;
|
||||||
|
|
||||||
export default function RandomPostsPage() {
|
export default function RandomPostsPage() {
|
||||||
const {t} = useTranslation()
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data, refetch } = useQuery({
|
const [ref, inView] = useInView();
|
||||||
queryKey: ["random_posts_key"],
|
|
||||||
queryFn: async () => {
|
|
||||||
try {
|
|
||||||
const response = await axiosLocalhost.get(
|
|
||||||
`/posts/random?limit=${LIMIT}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data as GetRandomPostsRow[];
|
const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({
|
||||||
} catch (error) {
|
queryKey: [`random_post_inf`],
|
||||||
console.log(`Something went wrong`);
|
queryFn: async ({ pageParam }): Promise<SelectedPostsResponse> => {
|
||||||
}
|
const response = await axiosLocalhost.get(
|
||||||
|
`/posts/random?limit=${LIMIT}&offset=${pageParam}`
|
||||||
|
);
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex direction={"column"} className="mx-auto">
|
<Flex direction={"column"} className="mx-auto overflow-hidden">
|
||||||
<Heading size={"9"} weight={"regular"} className="text-center">
|
<Heading size={"9"} weight={"regular"} className="text-center">
|
||||||
{t("discover")}
|
{t("discover")}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Separator size={"4"} className="my-8" />
|
<Separator size={"4"} className="my-8" />
|
||||||
|
<ScrollArea>
|
||||||
<ScrollArea.Root className="w-full h-full overflow-hidden">
|
{data?.pages.map((post, i) => {
|
||||||
<ScrollArea.Viewport className="overflow-scroll rounded size-full">
|
return (
|
||||||
{data?.map((post, i) => {
|
<>
|
||||||
return (
|
{post.selected_posts.map((post) => {
|
||||||
<Container size={"3"} key={`post${i}`}>
|
return (
|
||||||
<PostCard post={post} />
|
<Container size={"3"} key={`post${i}`}>
|
||||||
</Container>
|
<PostCard post={post} />
|
||||||
);
|
</Container>
|
||||||
})}
|
);
|
||||||
</ScrollArea.Viewport>
|
})}
|
||||||
<ScrollArea.Scrollbar
|
</>
|
||||||
className="z-50 flex touch-none select-none p-0.5 w-2"
|
);
|
||||||
orientation="vertical"
|
})}
|
||||||
>
|
<Box ref={ref}>qqqqqqqqqqqq</Box>
|
||||||
<ScrollArea.Thumb className="relative flex-1 rounded-[10px] bg-slate-200"/>
|
</ScrollArea>
|
||||||
</ScrollArea.Scrollbar>
|
</Flex>
|
||||||
{/* <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>
|
|
||||||
</Flex>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,45 +146,70 @@ func (q *Queries) GetPostsByUserId(ctx context.Context, userID int64) ([]Post, e
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRandomPosts = `-- name: GetRandomPosts :many
|
const getRandomPosts = `-- name: GetRandomPosts :one
|
||||||
SELECT post_id, blog_id, user_id, title, created_at
|
WITH all_posts AS (
|
||||||
FROM public.posts
|
SELECT
|
||||||
ORDER BY RANDOM()
|
COUNT(*) AS post_count
|
||||||
LIMIT $1
|
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 {
|
type GetRandomPostsParams struct {
|
||||||
PostID int64 `json:"post_id"`
|
Column1 interface{} `json:"column_1"`
|
||||||
BlogID pgtype.Int8 `json:"blog_id"`
|
Column2 interface{} `json:"column_2"`
|
||||||
UserID int64 `json:"user_id"`
|
Offset int32 `json:"offset"`
|
||||||
Title pgtype.Text `json:"title"`
|
|
||||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetRandomPosts(ctx context.Context, limit int32) ([]GetRandomPostsRow, error) {
|
type GetRandomPostsRow struct {
|
||||||
rows, err := q.db.Query(ctx, getRandomPosts, limit)
|
SelectedPosts interface{} `json:"selected_posts"`
|
||||||
if err != nil {
|
HasNextPage bool `json:"has_next_page"`
|
||||||
return nil, err
|
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) {
|
||||||
var i GetRandomPostsRow
|
row := q.db.QueryRow(ctx, getRandomPosts, arg.Column1, arg.Column2, arg.Offset)
|
||||||
if err := rows.Scan(
|
var i GetRandomPostsRow
|
||||||
&i.PostID,
|
err := row.Scan(
|
||||||
&i.BlogID,
|
&i.SelectedPosts,
|
||||||
&i.UserID,
|
&i.HasNextPage,
|
||||||
&i.Title,
|
&i.NextPageIndex,
|
||||||
&i.CreatedAt,
|
&i.PrevPageIndex,
|
||||||
); err != nil {
|
)
|
||||||
return nil, err
|
return i, err
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePostBlogId = `-- name: UpdatePostBlogId :exec
|
const updatePostBlogId = `-- name: UpdatePostBlogId :exec
|
||||||
|
|||||||
@ -35,8 +35,42 @@ SET blog_id=$2, updated_at=CURRENT_TIMESTAMP
|
|||||||
WHERE post_id = $1
|
WHERE post_id = $1
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetRandomPosts :many
|
-- name: GetRandomPosts :one
|
||||||
SELECT post_id, blog_id, user_id, title, created_at
|
WITH all_posts AS (
|
||||||
FROM public.posts
|
SELECT
|
||||||
ORDER BY RANDOM()
|
COUNT(*) AS post_count
|
||||||
LIMIT $1;
|
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;
|
||||||
|
|||||||
@ -19,25 +19,38 @@ func GetRandomPost(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
postsData, err :=
|
offset, err := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||||
db_repo.New(db_connection.Dbx).
|
|
||||||
GetRandomPosts(context.Background(), int32(limit))
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rest_api_stuff.InternalErrorAnswer(c, err)
|
rest_api_stuff.InternalErrorAnswer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]any, 0)
|
params := db_repo.GetRandomPostsParams{
|
||||||
|
Column1: int32(limit),
|
||||||
for _, post := range postsData {
|
Column2: int32(offset),
|
||||||
result = append(result, gin.H{
|
Offset: int32(offset * limit),
|
||||||
"post_id": strconv.Itoa(int(post.PostID)),
|
|
||||||
"title": post.Title,
|
|
||||||
"user_id": strconv.Itoa(int(post.UserID)),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user