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",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"@tanstack/react-query": "^5.80.6",
|
||||
"prettier": "^3.5.3",
|
||||
"react": "^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=="],
|
||||
|
||||
"@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__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"@tanstack/react-query": "^5.80.6",
|
||||
"prettier": "^3.5.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
|
|
|
@ -4,6 +4,9 @@ import PostForm from './Post'
|
|||
import { Zone } from './Zone'
|
||||
import { Delete } from './Delete'
|
||||
import { Fetch } from './Fetch'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
function App() {
|
||||
const [zone, setZone] = useState("post")
|
||||
|
@ -12,7 +15,7 @@ function App() {
|
|||
<>
|
||||
{zone == "post" && <PostForm />}
|
||||
{zone == "delete" && <Delete />}
|
||||
{zone == "view" && <Fetch />}
|
||||
{zone == "view" && <QueryClientProvider client={queryClient}><Fetch /></QueryClientProvider>}
|
||||
|
||||
<Zone setZone={setZone} />
|
||||
</>
|
||||
|
|
|
@ -1,31 +1,23 @@
|
|||
import { useEffect, useState } from "react"
|
||||
import { FetchPost, FetchPost_t, MediaPathTrc } from "./api"
|
||||
import React from "react"
|
||||
import { FetchPost, MediaPathTrc, SinglePost_t } from "./api"
|
||||
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 = () => {
|
||||
let [fetched, setFetched] = useState<fetched>([])
|
||||
useEffect(() => {
|
||||
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">
|
||||
const Single = ({ post }: { post: z.infer<typeof SinglePost_t> }) => {
|
||||
return (
|
||||
<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>
|
||||
{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) => {
|
||||
if (enc) {
|
||||
return <img
|
||||
key={index}
|
||||
src={MediaPathTrc(enc)}
|
||||
alt={`Post media ${post.id}-${index}`}
|
||||
className="rounded-md h-32 object-cover aspect-video flex-shrink-0"
|
||||
/>
|
||||
key={index}
|
||||
src={MediaPathTrc(enc)}
|
||||
alt={`Post media ${post.id}-${index}`}
|
||||
className="rounded-md h-32 object-cover aspect-video flex-shrink-0"
|
||||
/>
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
@ -36,6 +28,43 @@ export const Fetch = () => {
|
|||
<p className="self-start">PID: {post.id}</p>
|
||||
<p className="self-end">發文於: {post.post_at.toLocaleTimeString()}</p>
|
||||
</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 */}
|
||||
<label className="w-fit text-lg font-medium text-gray-700 break-keep">署名</label>
|
||||
<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', {
|
||||
maxLength: 20,
|
||||
})}
|
||||
|
@ -131,4 +131,4 @@ export default function PostForm() {
|
|||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
21
src/api.ts
21
src/api.ts
|
@ -6,18 +6,17 @@ type Inputs = {
|
|||
signing?: string
|
||||
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(
|
||||
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(SinglePost_t)
|
||||
|
||||
export const Post = async (post_form: Inputs) => {
|
||||
// Delete Blank Item
|
||||
|
|
Loading…
Add table
Reference in a new issue