gallery infinite pagination

This commit is contained in:
monoid 2024-04-07 17:41:56 +09:00
parent ef77706c56
commit 4cf1381faa
11 changed files with 230 additions and 178 deletions

View File

@ -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 (
<TooltipProvider>
<Layout>

View File

@ -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 <span className={props.className}>{chars[index]}</span>;
}

View File

@ -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),
};
});
}

View File

@ -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);
}

View File

@ -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";

View File

@ -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 (
<Paper /*className={classes.paper}*/>
<Box /*className={classes.contentTitle}*/>
<Typography variant="h3">{x.type}</Typography>
<Button
variant="contained"
key={x.type}
onClick={() => {
set_disable(true);
prop.onCommitAll(x.type);
set_disable(false);
}}
>
Commit all
</Button>
</Box>
{x.value.map((y) => (
<Box sx={{ display: "flex" }} key={y.path}>
<Button
variant="contained"
onClick={() => {
set_disable(true);
prop.onCommit(y);
set_disable(false);
}}
disabled={button_disable}
>
Commit
</Button>
<Typography variant="h5">{y.path}</Typography>
</Box>
))}
</Paper>
);
}
// return (
// <Paper /*className={classes.paper}*/>
// <Box /*className={classes.contentTitle}*/>
// <Typography variant="h3">{x.type}</Typography>
// <Button
// variant="contained"
// key={x.type}
// onClick={() => {
// set_disable(true);
// prop.onCommitAll(x.type);
// set_disable(false);
// }}
// >
// Commit all
// </Button>
// </Box>
// {x.value.map((y) => (
// <Box sx={{ display: "flex" }} key={y.path}>
// <Button
// variant="contained"
// onClick={() => {
// set_disable(true);
// prop.onCommit(y);
// set_disable(false);
// }}
// disabled={button_disable}
// >
// Commit
// </Button>
// <Typography variant="h5">{y.path}</Typography>
// </Box>
// ))}
// </Paper>
// );
// }
export function DifferencePage() {
const ctx = useContext(UserContext);
// const classes = useStyles();
const [diffList, setDiffList] = useState<FileDifference[]>([]);
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 (
<Headline menu={menu}>
<PagePad>
{ctx.username == "admin" ? (
<div>
{diffList.map((x) => (
<TypeDifference key={x.type} content={x} onCommit={Commit} onCommitAll={CommitAll} />
))}
</div>
) : (
<Typography variant="h2">Not Allowed : please login as an admin</Typography>
)}
</PagePad>
</Headline>
);
}
// export function DifferencePage() {
// const ctx = useContext(UserContext);
// // const classes = useStyles();
// const [diffList, setDiffList] = useState<FileDifference[]>([]);
// 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 (
// <Headline menu={menu}>
// <PagePad>
// {ctx.username == "admin" ? (
// <div>
// {diffList.map((x) => (
// <TypeDifference key={x.type} content={x} onCommit={Commit} onCommitAll={CommitAll} />
// ))}
// </div>
// ) : (
// <Typography variant="h2">Not Allowed : please login as an admin</Typography>
// )}
// </PagePad>
// </Headline>
// );
// }

View File

@ -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 <div className="p-4">Error: {String(error)}</div>
}
const isLoadingMore = data && size > 0 && (data[size - 1] === undefined);
const isReachingEnd = data && data[size - 1]?.hasMore === false;
return (<div className="p-4 grid gap-2 overflow-auto h-screen items-start content-start">
<div className="flex space-x-2">
<Input className="flex-1"/>
<Input className="flex-1" />
<Button className="flex-none">Search</Button>
</div>
{(word || tags) &&
<div className="bg-primary rounded-full p-1 sticky top-0 shadow-md">
{word && <span className="text-primary-foreground ml-4">Search: {word}</span>}
{tags && <span className="text-primary-foreground ml-4">Tags: <ul className="inline-flex">{tags.split(",").map(x=> <TagBadge tagname={x} key={x}/>)}</ul></span>}
{tags && <span className="text-primary-foreground ml-4">Tags: <ul className="inline-flex">{tags.split(",").map(x => <TagBadge tagname={x} key={x} />)}</ul></span>}
</div>
}
{
data?.length === 0 && <div className="p-4 text-3xl">No results</div>
}
{
// TODO: implement infinite scroll
data?.map((x) => {
return (
<GalleryCard doc={x} key={x.id} />
);
// TODO: date based grouping
data?.map((docs) => {
return docs.data.map((x) => {
return (
<GalleryCard doc={x} key={x.id} />
);
});
})
}
{
<Button className="w-full" onClick={() => setSize(size + 1)}
disabled={isReachingEnd || isLoadingMore}
> {isLoadingMore && <Spinner className="mr-1" />}{size + 1} Load more</Button>
}
</div>
);
}

View File

@ -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";

View File

@ -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 (
<div className="p-4">
<Card>