Add infinite scroll and modify fetch api to isolate the main object from list

This commit is contained in:
jasinco 2025-06-10 23:38:19 +08:00
parent 868b5e1c36
commit a592828feb
6 changed files with 73 additions and 36 deletions

View file

@ -5,6 +5,7 @@
"name": "nimfront", "name": "nimfront",
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.8", "@tailwindcss/vite": "^4.1.8",
"@tanstack/react-query": "^5.80.6",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
@ -233,6 +234,10 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.8", "", { "dependencies": { "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "tailwindcss": "4.1.8" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.1.8", "", { "dependencies": { "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "tailwindcss": "4.1.8" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A=="],
"@tanstack/query-core": ["@tanstack/query-core@5.80.6", "", {}, "sha512-nl7YxT/TAU+VTf+e2zTkObGTyY8YZBMnbgeA1ee66lIVqzKlYursAII6z5t0e6rXgwUMJSV4dshBTNacNpZHbQ=="],
"@tanstack/react-query": ["@tanstack/react-query@5.80.6", "", { "dependencies": { "@tanstack/query-core": "5.80.6" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-izX+5CnkpON3NQGcEm3/d7LfFQNo9ZpFtX2QsINgCYK9LT2VCIdi8D3bMaMSNhrAJCznRoAkFic76uvLroALBw=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],

View file

@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.8", "@tailwindcss/vite": "^4.1.8",
"@tanstack/react-query": "^5.80.6",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",

View file

@ -4,6 +4,9 @@ import PostForm from './Post'
import { Zone } from './Zone' import { Zone } from './Zone'
import { Delete } from './Delete' import { Delete } from './Delete'
import { Fetch } from './Fetch' import { Fetch } from './Fetch'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
function App() { function App() {
const [zone, setZone] = useState("post") const [zone, setZone] = useState("post")
@ -12,7 +15,7 @@ function App() {
<> <>
{zone == "post" && <PostForm />} {zone == "post" && <PostForm />}
{zone == "delete" && <Delete />} {zone == "delete" && <Delete />}
{zone == "view" && <Fetch />} {zone == "view" && <QueryClientProvider client={queryClient}><Fetch /></QueryClientProvider>}
<Zone setZone={setZone} /> <Zone setZone={setZone} />
</> </>

View file

@ -1,31 +1,23 @@
import { useEffect, useState } from "react" import React from "react"
import { FetchPost, FetchPost_t, MediaPathTrc } from "./api" import { FetchPost, MediaPathTrc, SinglePost_t } from "./api"
import { z } from 'zod/v4' import { z } from 'zod/v4'
import { useInfiniteQuery } from "@tanstack/react-query"
type fetched = z.infer<typeof FetchPost_t> // type fetched = z.infer<typeof FetchPost_t>
export const Fetch = () => { const Single = ({ post }: { post: z.infer<typeof SinglePost_t> }) => {
let [fetched, setFetched] = useState<fetched>([]) return (
useEffect(() => { <div className="w-full max-w-2xl bg-gray-200 rounded-lg shadow-md p-6 flex flex-col gap-4">
FetchPost().then(e => {
if (e) {
setFetched(e)
}
}
)
}, [])
return (<div className="min-h-dvh w-dvw flex flex-col items-center gap-6 py-8 px-4">
{fetched.map(post => <div className="w-full max-w-2xl bg-gray-200 rounded-lg shadow-md p-6 flex flex-col gap-4">
<p className="text-gray-800 text-base leading-relaxed">{post.content}</p> <p className="text-gray-800 text-base leading-relaxed">{post.content}</p>
{post.enclosure.length > 0 && post.enclosure[0] != null && <div className="flex flex-row overflow-x-auto gap-3 py-2 -mx-2 px-2 border-t border-gray-200 pt-4"> {post.enclosure.length > 0 && post.enclosure[0] != null && <div className="flex flex-row overflow-x-auto gap-3 py-2 -mx-2 px-2 border-t border-gray-200 pt-4">
{post.enclosure.map((enc, index) => { {post.enclosure.map((enc, index) => {
if (enc) { if (enc) {
return <img return <img
key={index} key={index}
src={MediaPathTrc(enc)} src={MediaPathTrc(enc)}
alt={`Post media ${post.id}-${index}`} alt={`Post media ${post.id}-${index}`}
className="rounded-md h-32 object-cover aspect-video flex-shrink-0" className="rounded-md h-32 object-cover aspect-video flex-shrink-0"
/> />
} else { } else {
return "" return ""
} }
@ -36,6 +28,43 @@ export const Fetch = () => {
<p className="self-start">PID: {post.id}</p> <p className="self-start">PID: {post.id}</p>
<p className="self-end">: {post.post_at.toLocaleTimeString()}</p> <p className="self-end">: {post.post_at.toLocaleTimeString()}</p>
</div> </div>
</div>)} </div>
</div>) )
}
export const Fetch = () => {
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
status,
} = useInfiniteQuery({
queryKey: ['id'],
queryFn: async ({ pageParam }) => {
return await FetchPost(pageParam == 0 ? undefined : pageParam)
},
initialPageParam: 0,
getNextPageParam: (lastPage, _) => {
return lastPage !== null && lastPage.length > 0 && (lastPage[lastPage.length - 1].id - 1) > 0 ? lastPage[lastPage.length - 1].id - 1 : undefined
},
})
return (
<div className="h-dvh w-dvw flex flex-col items-center gap-6 py-8 px-4 overflow-scroll" onScrollEnd={() => hasNextPage && !isFetching && fetchNextPage()}>
{status === 'pending' && <p>Loading</p>}
{status === 'error' && <p>Error: {error.message}</p>}
{data && data.pages.map((group, i) => (
<React.Fragment key={i}>
{group && group.map(post => (
<Single post={post} key={post.id} />
))}
</React.Fragment>
))}
{hasNextPage && <div className="w-full max-w-2xl bg-gray-200 rounded-lg shadow-md p-6 flex flex-col gap-4 animate-pulse h-[20dvh]" >
</div>}
{!hasNextPage && <p className="mb-15"></p>}
</div>
)
} }

View file

@ -113,7 +113,7 @@ export default function PostForm() {
<div className="ml-auto flex items-center gap-3"> {/* Aligned items, added items-center */} <div className="ml-auto flex items-center gap-3"> {/* Aligned items, added items-center */}
<label className="w-fit text-lg font-medium text-gray-700 break-keep"></label> <label className="w-fit text-lg font-medium text-gray-700 break-keep"></label>
<input <input
className="border-b-2 border-gray-400 w-full text-right outline-none focus:border-gray-600 transition-colors duration-200 p-1" // Modernized signing input className="border-b-2 border-gray-400 w-full text-right outline-none focus:border-gray-600 transition-colors duration-200 p-1 text-gray-700" // Modernized signing input
{...register('signing', { {...register('signing', {
maxLength: 20, maxLength: 20,
})} })}
@ -131,4 +131,4 @@ export default function PostForm() {
</form> </form>
</> </>
) )
} }

View file

@ -6,18 +6,17 @@ type Inputs = {
signing?: string signing?: string
files?: FileList files?: FileList
} }
export const SinglePost_t = z.object({
id: z.number(),
content: z.string(),
signing: z.nullable(z.string()),
post_at: z.coerce.date(),
heart: z.number(),
igid: z.nullable(z.string()),
enclosure: z.array(z.nullable(z.string())),
})
export const FetchPost_t = z.array( export const FetchPost_t = z.array(SinglePost_t)
z.object({
id: z.number(),
content: z.string(),
signing: z.nullable(z.string()),
post_at: z.coerce.date(),
heart: z.number(),
igid: z.nullable(z.string()),
enclosure: z.array(z.nullable(z.string())),
})
)
export const Post = async (post_form: Inputs) => { export const Post = async (post_form: Inputs) => {
// Delete Blank Item // Delete Blank Item