Rework #6
@ -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>
|
||||
|
25
packages/client/src/components/Spinner.tsx
Normal file
25
packages/client/src/components/Spinner.tsx
Normal 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>;
|
||||
}
|
46
packages/client/src/hook/useSearchGallery.ts
Normal file
46
packages/client/src/hook/useSearchGallery.ts
Normal 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),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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";
|
||||
|
@ -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>
|
||||
// );
|
||||
// }
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user