From 4cf1381faa0d98bb18fe72a4a529eff63e35ff48 Mon Sep 17 00:00:00 2001 From: monoid Date: Sun, 7 Apr 2024 17:41:56 +0900 Subject: [PATCH] gallery infinite pagination --- packages/client/src/App.tsx | 25 +- packages/client/src/components/Spinner.tsx | 25 ++ .../src/hook/{fetcher.tsx => fetcher.ts} | 0 .../{useGalleryDoc.tsx => useGalleryDoc.ts} | 0 packages/client/src/hook/useSearchGallery.ts | 46 ++++ packages/client/src/hook/useSearchGallery.tsx | 23 -- packages/client/src/page/contentInfoPage.tsx | 2 +- packages/client/src/page/difference.tsx | 244 +++++++++--------- packages/client/src/page/galleryPage.tsx | 30 ++- packages/client/src/page/reader/comicPage.tsx | 2 +- packages/client/src/page/settingPage.tsx | 11 +- 11 files changed, 230 insertions(+), 178 deletions(-) create mode 100644 packages/client/src/components/Spinner.tsx rename packages/client/src/hook/{fetcher.tsx => fetcher.ts} (100%) rename packages/client/src/hook/{useGalleryDoc.tsx => useGalleryDoc.ts} (100%) create mode 100644 packages/client/src/hook/useSearchGallery.ts delete mode 100644 packages/client/src/hook/useSearchGallery.tsx diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index a51f7e4..9faa003 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -1,19 +1,9 @@ import { Route, Switch, Redirect } from "wouter"; +import { useTernaryDarkMode } from "usehooks-ts"; +import { useEffect } from "react"; import './App.css' -// import { -// // DifferencePage, -// // DocumentAbout, -// // Gallery, -// // LoginPage, -// // NotFoundPage, -// // ProfilePage, -// // ReaderPage, -// // SettingPage, -// // TagsPage, -// } from "./page/mod"; - import { TooltipProvider } from "./components/ui/tooltip.tsx"; import Layout from "./components/layout/layout.tsx"; @@ -26,6 +16,17 @@ import SettingPage from "@/page/settingPage.tsx"; import ComicPage from "@/page/reader/comicPage.tsx"; const App = () => { + const { isDarkMode } = useTernaryDarkMode(); + + useEffect(() => { + if (isDarkMode) { + document.body.classList.add("dark"); + } + else { + document.body.classList.remove("dark"); + } + }, [isDarkMode]); + return ( diff --git a/packages/client/src/components/Spinner.tsx b/packages/client/src/components/Spinner.tsx new file mode 100644 index 0000000..4c3d960 --- /dev/null +++ b/packages/client/src/components/Spinner.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +export function Spinner(props: { className?: string; }) { + const chars = ["⠋", + "⠙", + "⠹", + "⠸", + "⠼", + "⠴", + "⠦", + "⠧", + "⠇", + "⠏" + ]; + const [index, setIndex] = React.useState(0); + React.useEffect(() => { + const interval = setInterval(() => { + setIndex((index + 1) % chars.length); + }, 80); + return () => clearInterval(interval); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [index]); + + return {chars[index]}; +} diff --git a/packages/client/src/hook/fetcher.tsx b/packages/client/src/hook/fetcher.ts similarity index 100% rename from packages/client/src/hook/fetcher.tsx rename to packages/client/src/hook/fetcher.ts diff --git a/packages/client/src/hook/useGalleryDoc.tsx b/packages/client/src/hook/useGalleryDoc.ts similarity index 100% rename from packages/client/src/hook/useGalleryDoc.tsx rename to packages/client/src/hook/useGalleryDoc.ts diff --git a/packages/client/src/hook/useSearchGallery.ts b/packages/client/src/hook/useSearchGallery.ts new file mode 100644 index 0000000..2a313fb --- /dev/null +++ b/packages/client/src/hook/useSearchGallery.ts @@ -0,0 +1,46 @@ +import useSWRInifinite from "swr/infinite"; +import type { Document } from "dbtype/api"; +import { fetcher } from "./fetcher"; + +interface SearchParams { + word?: string; + tags?: string; + limit?: number; + cursor?: number; +} + +export function useSearchGallery({ + word, tags, limit, cursor, +}: SearchParams) { + + return useSWRInifinite< + { + data: Document[]; + nextCursor: number | null; + hasMore: boolean; + } + >((index, previous) => { + if (!previous && index > 0) return null; + if (previous && !previous.hasMore) return null; + const search = new URLSearchParams(); + if (word) search.set("word", word); + if (tags) search.set("allow_tag", tags); + if (limit) search.set("limit", limit.toString()); + if (cursor) search.set("cursor", cursor.toString()); + if (index === 0) { + return `/api/doc/search?${search.toString()}`; + } + if (!previous || !previous.data) return null; + const last = previous.data[previous.data.length - 1]; + search.set("cursor", last.id.toString()); + return `/api/doc/search?${search.toString()}`; + }, async (url) => { + const res = await fetcher(url); + return { + data: res, + nextCursor: res.length === 0 ? null : res[res.length - 1].id, + hasMore: limit ? res.length === limit : (res.length === 20), + }; + }); +} + diff --git a/packages/client/src/hook/useSearchGallery.tsx b/packages/client/src/hook/useSearchGallery.tsx deleted file mode 100644 index 800124c..0000000 --- a/packages/client/src/hook/useSearchGallery.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import useSWR from "swr"; -import type { Document } from "dbtype/api"; -import { fetcher } from "./fetcher"; - -interface SearchParams { - word?: string; - tags?: string; - limit?: number; - cursor?: number; -} - -export function useSearchGallery({ - word, tags, limit, cursor, -}: SearchParams) { - const search = new URLSearchParams(); - if (word) search.set("word", word); - if (tags) search.set("allow_tag", tags); - if (limit) search.set("limit", limit.toString()); - if (cursor) search.set("cursor", cursor.toString()); - return useSWR< - Document[] - >(`/api/doc/search?${search.toString()}`, fetcher); -} diff --git a/packages/client/src/page/contentInfoPage.tsx b/packages/client/src/page/contentInfoPage.tsx index 39092a2..3c1b94e 100644 --- a/packages/client/src/page/contentInfoPage.tsx +++ b/packages/client/src/page/contentInfoPage.tsx @@ -1,5 +1,5 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { useGalleryDoc } from "../hook/useGalleryDoc"; +import { useGalleryDoc } from "../hook/useGalleryDoc.ts"; import TagBadge from "@/components/gallery/TagBadge"; import StyledLink from "@/components/gallery/StyledLink"; import { cn } from "@/lib/utils"; diff --git a/packages/client/src/page/difference.tsx b/packages/client/src/page/difference.tsx index 2dabe32..a5ae699 100644 --- a/packages/client/src/page/difference.tsx +++ b/packages/client/src/page/difference.tsx @@ -1,126 +1,126 @@ -import { Box, Button, Paper, Typography } from "@mui/material"; -import React, { useContext, useEffect, useState } from "react"; -import { CommonMenuList, Headline } from "../component/mod"; -import { UserContext } from "../state"; -import { PagePad } from "../component/pagepad"; +// import { Box, Button, Paper, Typography } from "@mui/material"; +// import React, { useContext, useEffect, useState } from "react"; +// import { CommonMenuList, Headline } from "../component/mod"; +// import { UserContext } from "../state"; +// import { PagePad } from "../component/pagepad"; -type FileDifference = { - type: string; - value: { - type: string; - path: string; - }[]; -}; +// type FileDifference = { +// type: string; +// value: { +// type: string; +// path: string; +// }[]; +// }; -function TypeDifference(prop: { - content: FileDifference; - onCommit: (v: { type: string; path: string }) => void; - onCommitAll: (type: string) => void; -}) { - // const classes = useStyles(); - const x = prop.content; - const [button_disable, set_disable] = useState(false); +// function TypeDifference(prop: { +// content: FileDifference; +// onCommit: (v: { type: string; path: string }) => void; +// onCommitAll: (type: string) => void; +// }) { +// // const classes = useStyles(); +// const x = prop.content; +// const [button_disable, set_disable] = useState(false); - return ( - - - {x.type} - - - {x.value.map((y) => ( - - - {y.path} - - ))} - - ); -} +// return ( +// +// +// {x.type} +// +// +// {x.value.map((y) => ( +// +// +// {y.path} +// +// ))} +// +// ); +// } -export function DifferencePage() { - const ctx = useContext(UserContext); - // const classes = useStyles(); - const [diffList, setDiffList] = useState([]); - const doLoad = async () => { - const list = await fetch("/api/diff/list"); - if (list.ok) { - const inner = await list.json(); - setDiffList(inner); - } else { - // setDiffList([]); - } - }; - const Commit = async (x: { type: string; path: string }) => { - const res = await fetch("/api/diff/commit", { - method: "POST", - body: JSON.stringify([{ ...x }]), - headers: { - "content-type": "application/json", - }, - }); - const bb = await res.json(); - if (bb.ok) { - doLoad(); - } else { - console.error("fail to add document"); - } - }; - const CommitAll = async (type: string) => { - const res = await fetch("/api/diff/commitall", { - method: "POST", - body: JSON.stringify({ type: type }), - headers: { - "content-type": "application/json", - }, - }); - const bb = await res.json(); - if (bb.ok) { - doLoad(); - } else { - console.error("fail to add document"); - } - }; - useEffect(() => { - doLoad(); - const i = setInterval(doLoad, 5000); - return () => { - clearInterval(i); - }; - }, []); - const menu = CommonMenuList(); - return ( - - - {ctx.username == "admin" ? ( -
- {diffList.map((x) => ( - - ))} -
- ) : ( - Not Allowed : please login as an admin - )} -
-
- ); -} +// export function DifferencePage() { +// const ctx = useContext(UserContext); +// // const classes = useStyles(); +// const [diffList, setDiffList] = useState([]); +// const doLoad = async () => { +// const list = await fetch("/api/diff/list"); +// if (list.ok) { +// const inner = await list.json(); +// setDiffList(inner); +// } else { +// // setDiffList([]); +// } +// }; +// const Commit = async (x: { type: string; path: string }) => { +// const res = await fetch("/api/diff/commit", { +// method: "POST", +// body: JSON.stringify([{ ...x }]), +// headers: { +// "content-type": "application/json", +// }, +// }); +// const bb = await res.json(); +// if (bb.ok) { +// doLoad(); +// } else { +// console.error("fail to add document"); +// } +// }; +// const CommitAll = async (type: string) => { +// const res = await fetch("/api/diff/commitall", { +// method: "POST", +// body: JSON.stringify({ type: type }), +// headers: { +// "content-type": "application/json", +// }, +// }); +// const bb = await res.json(); +// if (bb.ok) { +// doLoad(); +// } else { +// console.error("fail to add document"); +// } +// }; +// useEffect(() => { +// doLoad(); +// const i = setInterval(doLoad, 5000); +// return () => { +// clearInterval(i); +// }; +// }, []); +// const menu = CommonMenuList(); +// return ( +// +// +// {ctx.username == "admin" ? ( +//
+// {diffList.map((x) => ( +// +// ))} +//
+// ) : ( +// Not Allowed : please login as an admin +// )} +//
+//
+// ); +// } diff --git a/packages/client/src/page/galleryPage.tsx b/packages/client/src/page/galleryPage.tsx index 4ed66bc..20d15f7 100644 --- a/packages/client/src/page/galleryPage.tsx +++ b/packages/client/src/page/galleryPage.tsx @@ -3,7 +3,8 @@ import { Input } from "@/components/ui/input.tsx"; import { Button } from "@/components/ui/button.tsx"; import { GalleryCard } from "@/components/gallery/GalleryCard.tsx"; import TagBadge from "@/components/gallery/TagBadge.tsx"; -import { useSearchGallery } from "../hook/useSearchGallery"; +import { useSearchGallery } from "../hook/useSearchGallery.ts"; +import { Spinner } from "../components/Spinner.tsx"; export default function Gallery() { const search = useSearch(); @@ -12,7 +13,7 @@ export default function Gallery() { const tags = searchParams.get("allow_tag") ?? undefined; const limit = searchParams.get("limit"); const cursor = searchParams.get("cursor"); - const { data, error, isLoading } = useSearchGallery({ + const { data, error, isLoading, size, setSize } = useSearchGallery({ word, tags, limit: limit ? Number.parseInt(limit) : undefined, cursor: cursor ? Number.parseInt(cursor) : undefined @@ -25,28 +26,39 @@ export default function Gallery() { return
Error: {String(error)}
} + const isLoadingMore = data && size > 0 && (data[size - 1] === undefined); + const isReachingEnd = data && data[size - 1]?.hasMore === false; + return (
- +
{(word || tags) &&
{word && Search: {word}} - {tags && Tags:
    {tags.split(",").map(x=> )}
} + {tags && Tags:
    {tags.split(",").map(x => )}
}
} { data?.length === 0 &&
No results
} { - // TODO: implement infinite scroll - data?.map((x) => { - return ( - - ); + // TODO: date based grouping + data?.map((docs) => { + return docs.data.map((x) => { + return ( + + ); + }); }) } + { + + }
); } + diff --git a/packages/client/src/page/reader/comicPage.tsx b/packages/client/src/page/reader/comicPage.tsx index 9be24bc..5ae10ef 100644 --- a/packages/client/src/page/reader/comicPage.tsx +++ b/packages/client/src/page/reader/comicPage.tsx @@ -1,4 +1,4 @@ -import { useGalleryDoc } from "@/hook/useGalleryDoc"; +import { useGalleryDoc } from "@/hook/useGalleryDoc.ts"; import { cn } from "@/lib/utils"; import type { Document } from "dbtype/api"; import { useCallback, useEffect, useRef, useState } from "react"; diff --git a/packages/client/src/page/settingPage.tsx b/packages/client/src/page/settingPage.tsx index 3f8f901..3e73907 100644 --- a/packages/client/src/page/settingPage.tsx +++ b/packages/client/src/page/settingPage.tsx @@ -43,18 +43,9 @@ function DarkModeView() { } export function SettingPage() { - const { setTernaryDarkMode, ternaryDarkMode, isDarkMode } = useTernaryDarkMode(); + const { setTernaryDarkMode, ternaryDarkMode } = useTernaryDarkMode(); const isSystemDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); - useEffect(() => { - if (isDarkMode) { - document.body.classList.add("dark"); - } - else { - document.body.classList.remove("dark"); - } - }, [isDarkMode]); - return (