Add infinite scroll and modify fetch api to isolate the main object from list
This commit is contained in:
parent
868b5e1c36
commit
a592828feb
6 changed files with 73 additions and 36 deletions
5
bun.lock
5
bun.lock
|
@ -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=="],
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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} />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,21 +1,13 @@
|
||||||
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) => {
|
||||||
|
@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -6,9 +6,7 @@ type Inputs = {
|
||||||
signing?: string
|
signing?: string
|
||||||
files?: FileList
|
files?: FileList
|
||||||
}
|
}
|
||||||
|
export const SinglePost_t = z.object({
|
||||||
export const FetchPost_t = z.array(
|
|
||||||
z.object({
|
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
signing: z.nullable(z.string()),
|
signing: z.nullable(z.string()),
|
||||||
|
@ -17,7 +15,8 @@ export const FetchPost_t = z.array(
|
||||||
igid: z.nullable(z.string()),
|
igid: z.nullable(z.string()),
|
||||||
enclosure: z.array(z.nullable(z.string())),
|
enclosure: z.array(z.nullable(z.string())),
|
||||||
})
|
})
|
||||||
)
|
|
||||||
|
export const FetchPost_t = z.array(SinglePost_t)
|
||||||
|
|
||||||
export const Post = async (post_form: Inputs) => {
|
export const Post = async (post_form: Inputs) => {
|
||||||
// Delete Blank Item
|
// Delete Blank Item
|
||||||
|
|
Loading…
Add table
Reference in a new issue