Rework #6
@ -13,17 +13,20 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"dbtype": "workspace:*",
|
"dbtype": "workspace:*",
|
||||||
|
"jotai": "^2.7.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-resizable-panels": "^2.0.16",
|
"react-resizable-panels": "^2.0.16",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
"tailwind-merge": "^2.2.2",
|
"tailwind-merge": "^2.2.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"usehooks-ts": "^3.1.0",
|
||||||
"wouter": "^3.1.0"
|
"wouter": "^3.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -22,6 +22,7 @@ import NotFoundPage from "./page/404.tsx";
|
|||||||
import LoginPage from "./page/loginPage.tsx";
|
import LoginPage from "./page/loginPage.tsx";
|
||||||
import ProfilePage from "./page/profilesPage.tsx";
|
import ProfilePage from "./page/profilesPage.tsx";
|
||||||
import ContentInfoPage from "./page/contentInfoPage.tsx";
|
import ContentInfoPage from "./page/contentInfoPage.tsx";
|
||||||
|
import SettingPage from "./page/settingPage.tsx";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
@ -33,10 +34,10 @@ const App = () => {
|
|||||||
<Route path="/login" component={LoginPage} />
|
<Route path="/login" component={LoginPage} />
|
||||||
<Route path="/profile" component={ProfilePage}/>
|
<Route path="/profile" component={ProfilePage}/>
|
||||||
<Route path="/doc/:id" component={ContentInfoPage}/>
|
<Route path="/doc/:id" component={ContentInfoPage}/>
|
||||||
|
<Route path="/setting" component={SettingPage} />
|
||||||
{/*
|
{/*
|
||||||
<Route path="/doc/:id/reader" component={<ReaderPage />}></Route>
|
<Route path="/doc/:id/reader" component={<ReaderPage />}></Route>
|
||||||
<Route path="/difference" component={<DifferencePage />}></Route>
|
<Route path="/difference" component={<DifferencePage />}></Route>
|
||||||
<Route path="/setting" component={<SettingPage />}></Route>
|
|
||||||
<Route path="/tags" component={<TagsPage />}></Route>*/}
|
<Route path="/tags" component={<TagsPage />}></Route>*/}
|
||||||
<Route component={NotFoundPage} />
|
<Route component={NotFoundPage} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
@ -1,99 +0,0 @@
|
|||||||
import { Document, DocumentAccessor, DocumentBody, QueryListOption } from "../../model/doc";
|
|
||||||
import { toQueryString } from "./util";
|
|
||||||
const baseurl = "/api/doc";
|
|
||||||
|
|
||||||
export * from "../../model/doc";
|
|
||||||
|
|
||||||
export class FetchFailError extends Error {}
|
|
||||||
|
|
||||||
export class ClientDocumentAccessor implements DocumentAccessor {
|
|
||||||
search: (search_word: string) => Promise<Document[]>;
|
|
||||||
addList: (content_list: DocumentBody[]) => Promise<number[]>;
|
|
||||||
async findByPath(basepath: string, filename?: string): Promise<Document[]> {
|
|
||||||
throw new Error("not allowed");
|
|
||||||
}
|
|
||||||
async findDeleted(content_type: string): Promise<Document[]> {
|
|
||||||
throw new Error("not allowed");
|
|
||||||
}
|
|
||||||
async findList(option?: QueryListOption | undefined): Promise<Document[]> {
|
|
||||||
let res = await fetch(`${baseurl}/search?${option !== undefined ? toQueryString(option) : ""}`);
|
|
||||||
if (res.status == 401) throw new FetchFailError("Unauthorized");
|
|
||||||
if (res.status !== 200) throw new FetchFailError("findList Failed");
|
|
||||||
let ret = await res.json();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
async findById(id: number, tagload?: boolean | undefined): Promise<Document | undefined> {
|
|
||||||
let res = await fetch(`${baseurl}/${id}`);
|
|
||||||
if (res.status !== 200) throw new FetchFailError("findById Failed");
|
|
||||||
let ret = await res.json();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* not implement
|
|
||||||
*/
|
|
||||||
async findListByBasePath(basepath: string): Promise<Document[]> {
|
|
||||||
throw new Error("not implement");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
async update(c: Partial<Document> & { id: number }): Promise<boolean> {
|
|
||||||
const { id, ...rest } = c;
|
|
||||||
const res = await fetch(`${baseurl}/${id}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(rest),
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const ret = await res.json();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
async add(c: DocumentBody): Promise<number> {
|
|
||||||
throw new Error("not allow");
|
|
||||||
const res = await fetch(`${baseurl}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(c),
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const ret = await res.json();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
async del(id: number): Promise<boolean> {
|
|
||||||
const res = await fetch(`${baseurl}/${id}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
const ret = await res.json();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
async addTag(c: Document, tag_name: string): Promise<boolean> {
|
|
||||||
const { id, ...rest } = c;
|
|
||||||
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(rest),
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const ret = await res.json();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
async delTag(c: Document, tag_name: string): Promise<boolean> {
|
|
||||||
const { id, ...rest } = c;
|
|
||||||
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
body: JSON.stringify(rest),
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const ret = await res.json();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const CDocumentAccessor = new ClientDocumentAccessor();
|
|
||||||
export const makeThumbnailUrl = (x: Document) => {
|
|
||||||
return `${baseurl}/${x.id}/${x.content_type}/thumbnail`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CDocumentAccessor;
|
|
@ -1,28 +0,0 @@
|
|||||||
type Representable = string | number | boolean;
|
|
||||||
|
|
||||||
type ToQueryStringA = {
|
|
||||||
[name: string]: Representable | Representable[] | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toQueryString = (obj: ToQueryStringA) => {
|
|
||||||
return Object.entries(obj)
|
|
||||||
.filter((e): e is [string, Representable | Representable[]] => e[1] !== undefined)
|
|
||||||
.map((e) => (e[1] instanceof Array ? e[1].map((f) => `${e[0]}=${f}`).join("&") : `${e[0]}=${e[1]}`))
|
|
||||||
.join("&");
|
|
||||||
};
|
|
||||||
export const QueryStringToMap = (query: string) => {
|
|
||||||
const keyValue = query.slice(query.indexOf("?") + 1).split("&");
|
|
||||||
const param: { [k: string]: string | string[] } = {};
|
|
||||||
keyValue.forEach((p) => {
|
|
||||||
const [k, v] = p.split("=");
|
|
||||||
const pv = param[k];
|
|
||||||
if (pv === undefined) {
|
|
||||||
param[k] = v;
|
|
||||||
} else if (typeof pv === "string") {
|
|
||||||
param[k] = [pv, v];
|
|
||||||
} else {
|
|
||||||
pv.push(v);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return param;
|
|
||||||
};
|
|
@ -1,9 +1,10 @@
|
|||||||
import type { Document } from "dbtype/api";
|
import type { Document } from "dbtype/api";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx";
|
||||||
import TagBadge from "@/components/gallery/TagBadge.tsx";
|
import TagBadge from "@/components/gallery/TagBadge.tsx";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||||
import { Link, useLocation } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import { LazyImage } from "./LazyImage.tsx";
|
import { LazyImage } from "./LazyImage.tsx";
|
||||||
|
import StyledLink from "./StyledLink.tsx";
|
||||||
|
|
||||||
function clipTagsWhenOverflow(tags: string[], limit: number) {
|
function clipTagsWhenOverflow(tags: string[], limit: number) {
|
||||||
let l = 0;
|
let l = 0;
|
||||||
@ -22,12 +23,11 @@ export function GalleryCard({
|
|||||||
}: { doc: Document; }) {
|
}: { doc: Document; }) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [clipCharCount, setClipCharCount] = useState(200);
|
const [clipCharCount, setClipCharCount] = useState(200);
|
||||||
const [location] = useLocation();
|
|
||||||
|
|
||||||
const artists = x.tags.filter(x => x.startsWith("artist:")).map(x => x.replace("artist:", ""));
|
const artists = x.tags.filter(x => x.startsWith("artist:")).map(x => x.replace("artist:", ""));
|
||||||
const groups = x.tags.filter(x => x.startsWith("group:")).map(x => x.replace("group:", ""));
|
const groups = x.tags.filter(x => x.startsWith("group:")).map(x => x.replace("group:", ""));
|
||||||
|
|
||||||
const originalTags = x.tags.map(x => x.replace("artist:", "").replace("group:", ""));
|
const originalTags = x.tags.filter(x => !x.startsWith("artist:") && !x.startsWith("group:"));
|
||||||
const clippedTags = clipTagsWhenOverflow(originalTags, clipCharCount);
|
const clippedTags = clipTagsWhenOverflow(originalTags, clipCharCount);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -44,7 +44,7 @@ export function GalleryCard({
|
|||||||
window.removeEventListener("resize", listener);
|
window.removeEventListener("resize", listener);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <Card className="flex h-[200px]">
|
return <Card className="flex h-[200px]">
|
||||||
<div className="rounded-xl overflow-hidden h-[200px] w-[142px] flex-none bg-[#272733] flex items-center justify-center">
|
<div className="rounded-xl overflow-hidden h-[200px] w-[142px] flex-none bg-[#272733] flex items-center justify-center">
|
||||||
<LazyImage src={`/api/doc/${x.id}/comic/thumbnail`}
|
<LazyImage src={`/api/doc/${x.id}/comic/thumbnail`}
|
||||||
@ -55,16 +55,27 @@ export function GalleryCard({
|
|||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<CardHeader className="flex-none">
|
<CardHeader className="flex-none">
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
<Link href={`/doc/${x.id}`} state={{fromUrl: location}}>{x.title}</Link>
|
<StyledLink className="" to={`/doc/${x.id}`}>
|
||||||
|
{x.title}
|
||||||
|
</StyledLink>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{artists.join(", ")} {groups.length > 0 && "|"} {groups.join(", ")}
|
{artists.map((x, i) => <Fragment key={`artist:${x}`}>
|
||||||
|
<StyledLink to={`/search?allow_tag=artist:${x}`}>{x}</StyledLink>
|
||||||
|
{i + 1 < artists.length && <span className="opacity-50">, </span>}
|
||||||
|
</Fragment>)}
|
||||||
|
{groups.length > 0 && <span key={"sep"}>{" | "}</span>}
|
||||||
|
{groups.map((x, i) => <Fragment key={`group:${x}`}>
|
||||||
|
<StyledLink to={`/search?allow_tag=group:${x}`}>{x}</StyledLink>
|
||||||
|
{i + 1 < groups.length && <span className="opacity-50">, </span>}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-1" ref={ref}>
|
<CardContent className="flex-1" ref={ref}>
|
||||||
<ul className="flex flex-wrap gap-2 items-baseline content-start">
|
<ul className="flex flex-wrap gap-2 items-baseline content-start">
|
||||||
{clippedTags.map(tag => <TagBadge key={tag} tagname={tag} className="" />)}
|
{clippedTags.map(tag => <TagBadge key={tag} tagname={tag} className="" />)}
|
||||||
{clippedTags.length < originalTags.length && <TagBadge tagname="..." className="" disabled />}
|
{clippedTags.length < originalTags.length && <TagBadge key={"..."} tagname="..." className="" disabled />}
|
||||||
</ul>
|
</ul>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</div>
|
</div>
|
||||||
|
14
packages/client/src/components/gallery/StyledLink.tsx
Normal file
14
packages/client/src/components/gallery/StyledLink.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Link } from "wouter";
|
||||||
|
|
||||||
|
type StyledLinkProps = {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
to: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function StyledLink({ children, className, ...rest }: StyledLinkProps) {
|
||||||
|
return <Link {...rest}
|
||||||
|
className={cn("hover:underline underline-offset-1 rounded-sm focus-visible:ring-1 focus-visible:ring-ring", className)}
|
||||||
|
>{children}</Link>
|
||||||
|
}
|
@ -2,28 +2,35 @@ import { badgeVariants } from "@/components/ui/badge.tsx";
|
|||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
import { cn } from "@/lib/utils.ts";
|
import { cn } from "@/lib/utils.ts";
|
||||||
|
|
||||||
const femaleTagPrefix = "female:";
|
|
||||||
const maleTagPrefix = "male:";
|
|
||||||
|
|
||||||
function getTagKind(tagname: string) {
|
function getTagKind(tagname: string) {
|
||||||
if (tagname.startsWith(femaleTagPrefix)) {
|
if (tagname.match(":") === null) {
|
||||||
return "female";
|
return "default";
|
||||||
}
|
}
|
||||||
if (tagname.startsWith(maleTagPrefix)){
|
const prefix = tagname.split(":")[0];
|
||||||
return "male";
|
return prefix;
|
||||||
}
|
|
||||||
return "default";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPrettyTagname(tagname: string): string {
|
function toPrettyTagname(tagname: string): string {
|
||||||
const kind = getTagKind(tagname);
|
const kind = getTagKind(tagname);
|
||||||
|
const name = tagname.slice(kind.length + 1);
|
||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case "male":
|
case "male":
|
||||||
return `♂ ${tagname.slice(maleTagPrefix.length)}`;
|
return `♂ ${name}`;
|
||||||
case "female":
|
case "female":
|
||||||
return `♀ ${tagname.slice(femaleTagPrefix.length)}`;
|
return `♀ ${name}`;
|
||||||
default:
|
case "artist":
|
||||||
|
return `🎨 ${name}`;
|
||||||
|
case "group":
|
||||||
|
return `🖿 ${name}`;
|
||||||
|
case "series":
|
||||||
|
return `📚 ${name}`
|
||||||
|
case "character":
|
||||||
|
return `👤 ${name}`;
|
||||||
|
case "default":
|
||||||
return tagname;
|
return tagname;
|
||||||
|
default:
|
||||||
|
return name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,6 +42,11 @@ export default function TagBadge(props: { tagname: string, className?: string; d
|
|||||||
"px-1",
|
"px-1",
|
||||||
kind === "male" && "bg-[#2a7bbf] hover:bg-[#3091e7]",
|
kind === "male" && "bg-[#2a7bbf] hover:bg-[#3091e7]",
|
||||||
kind === "female" && "bg-[#c21f58] hover:bg-[#db2d67]",
|
kind === "female" && "bg-[#c21f58] hover:bg-[#db2d67]",
|
||||||
|
kind === "artist" && "bg-[#319795] hover:bg-[#38a89d]",
|
||||||
|
kind === "group" && "bg-[#805ad5] hover:bg-[#8b5cd6]",
|
||||||
|
kind === "series" && "bg-[#dc8f09] hover:bg-[#e69d17]",
|
||||||
|
kind === "character" && "bg-[#52952c] hover:bg-[#6cc24a]",
|
||||||
|
kind === "type" && "bg-[#d53f8c] hover:bg-[#e24996]",
|
||||||
kind === "default" && "bg-[#4a5568] hover:bg-[#718096]",
|
kind === "default" && "bg-[#4a5568] hover:bg-[#718096]",
|
||||||
props.disabled && "opacity-50",
|
props.disabled && "opacity-50",
|
||||||
props.className,
|
props.className,
|
||||||
|
42
packages/client/src/components/ui/radio-group.tsx
Normal file
42
packages/client/src/components/ui/radio-group.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { CheckIcon } from "@radix-ui/react-icons"
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const RadioGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const RadioGroupItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||||
|
<CheckIcon className="h-3.5 w-3.5 fill-primary" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem }
|
4
packages/client/src/hook/fetcher.tsx
Normal file
4
packages/client/src/hook/fetcher.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export async function fetcher(url: string) {
|
||||||
|
const res = await fetch(url);
|
||||||
|
return res.json();
|
||||||
|
}
|
7
packages/client/src/hook/useGalleryDoc.tsx
Normal file
7
packages/client/src/hook/useGalleryDoc.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import useSWR from "swr";
|
||||||
|
import type { Document } from "dbtype/api";
|
||||||
|
import { fetcher } from "./fetcher";
|
||||||
|
|
||||||
|
export function useGalleryDoc(id: string) {
|
||||||
|
return useSWR<Document>(`/api/doc/${id}`, fetcher);
|
||||||
|
}
|
23
packages/client/src/hook/useSearchGallery.tsx
Normal file
23
packages/client/src/hook/useSearchGallery.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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,20 +1,133 @@
|
|||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { useGalleryDoc } from "../hook/useGalleryDoc";
|
||||||
|
import TagBadge from "@/components/gallery/TagBadge";
|
||||||
|
import StyledLink from "@/components/gallery/StyledLink";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface ContentInfoPageProps {
|
export interface ContentInfoPageProps {
|
||||||
params: {
|
params: {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TagClassifyResult {
|
||||||
|
artist: string[];
|
||||||
|
group: string[];
|
||||||
|
series: string[];
|
||||||
|
type: string[];
|
||||||
|
character: string[];
|
||||||
|
rest: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function classifyTags(tags: string[]): TagClassifyResult {
|
||||||
|
const result = {
|
||||||
|
artist: [],
|
||||||
|
group: [],
|
||||||
|
series: [],
|
||||||
|
type: [],
|
||||||
|
character: [],
|
||||||
|
rest: [],
|
||||||
|
} as TagClassifyResult;
|
||||||
|
const tagKind = new Set(["artist", "group", "series", "type", "character"]);
|
||||||
|
for (const tag of tags) {
|
||||||
|
const split = tag.split(":");
|
||||||
|
if (split.length !== 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [prefix, name] = split;
|
||||||
|
if (tagKind.has(prefix)) {
|
||||||
|
result[prefix as keyof TagClassifyResult].push(name);
|
||||||
|
} else {
|
||||||
|
result.rest.push(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export function ContentInfoPage({params}: ContentInfoPageProps) {
|
export function ContentInfoPage({ params }: ContentInfoPageProps) {
|
||||||
|
const { data, error, isLoading } = useGalleryDoc(params.id);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div className="p-4">Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="p-4">Error: {String(error)}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <div className="p-4">Not found</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = data?.tags ?? [];
|
||||||
|
const classifiedTags = classifyTags(tags);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<h1>ContentInfoPage</h1>
|
<div className="m-auto h-[400px] mb-4 flex justify-center items-center flex-none bg-[#272733]
|
||||||
{params.id}
|
rounded-xl shadow-lg overflow-hidden">
|
||||||
<p>Find me in packages/client/src/page/contentInfoPage.tsx</p>
|
<img
|
||||||
|
className="max-w-full max-h-full object-cover object-center"
|
||||||
|
src={`/api/doc/${data.id}/comic/thumbnail`}
|
||||||
|
alt={data.title} />
|
||||||
|
</div>
|
||||||
|
<Card className="flex-1">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{data.title}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
<StyledLink to={`/search?allow_tag=type:${classifiedTags.type[0] ?? ""}`} className="text-sm">
|
||||||
|
{classifiedTags.type[0] ?? "N/A"}
|
||||||
|
</StyledLink>
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid gap-y-4 gap-x-3 lg:grid-cols-2">
|
||||||
|
<DescTagItem name="artist" items={classifiedTags.artist} />
|
||||||
|
<DescTagItem name="group" items={classifiedTags.group} />
|
||||||
|
<DescTagItem name="series" items={classifiedTags.series} />
|
||||||
|
<DescTagItem name="character" items={classifiedTags.character} />
|
||||||
|
<DescItem name="Created At">{new Date(data.created_at).toLocaleString()}</DescItem>
|
||||||
|
<DescItem name="Modified At">{new Date(data.modified_at).toLocaleString()}</DescItem>
|
||||||
|
<DescItem name="Path">{`${data.basepath}/${data.filename}`}</DescItem>
|
||||||
|
<DescItem name="Page Count">{JSON.stringify(data.additional)}</DescItem>
|
||||||
|
</div>
|
||||||
|
<div className="grid mt-4">
|
||||||
|
<span className="text-muted-foreground text-sm">Tags</span>
|
||||||
|
<ul className="mt-2 flex flex-wrap gap-1">
|
||||||
|
{classifiedTags.rest.map((tag) => <TagBadge key={tag} tagname={tag} />)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ContentInfoPage;
|
export default ContentInfoPage;
|
||||||
|
|
||||||
|
function DescItem({ name, children, className }: {
|
||||||
|
name: string,
|
||||||
|
className?: string,
|
||||||
|
children?: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return <div className={cn("grid content-start", className)}>
|
||||||
|
<span className="text-muted-foreground text-sm">{name}</span>
|
||||||
|
<span className="text-primary leading-4 font-medium">{children}</span>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DescTagItem({
|
||||||
|
items,
|
||||||
|
name,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
items: string[];
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return <DescItem name={name} className={className}>
|
||||||
|
{items.length === 0 ? "N/A" : items.map(
|
||||||
|
(x) => <StyledLink key={x} to={`/search?allow_tag=${name}:${x}`}>{x}</StyledLink>
|
||||||
|
)}
|
||||||
|
</DescItem>
|
||||||
|
}
|
||||||
|
@ -1,136 +0,0 @@
|
|||||||
import { IconButton, Theme, Typography } from "@mui/material";
|
|
||||||
import FullscreenIcon from "@mui/icons-material/Fullscreen";
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import { Route, Routes, useLocation, useParams } from "react-router-dom";
|
|
||||||
import DocumentAccessor, { Document } from "../accessor/document";
|
|
||||||
import { LoadingCircle } from "../component/loading";
|
|
||||||
import { CommonMenuList, ContentInfo, Headline } from "../component/mod";
|
|
||||||
import { NotFoundPage } from "./404";
|
|
||||||
import { getPresenter } from "./reader/reader";
|
|
||||||
import { PagePad } from "../component/pagepad";
|
|
||||||
|
|
||||||
export const makeContentInfoUrl = (id: number) => `/doc/${id}`;
|
|
||||||
export const makeComicReaderUrl = (id: number) => `/doc/${id}/reader`;
|
|
||||||
|
|
||||||
type DocumentState = {
|
|
||||||
doc: Document | undefined;
|
|
||||||
notfound: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ReaderPage(props?: {}) {
|
|
||||||
const location = useLocation();
|
|
||||||
const match = useParams<{ id: string }>();
|
|
||||||
if (match == null) {
|
|
||||||
throw new Error("unreachable");
|
|
||||||
}
|
|
||||||
const id = Number.parseInt(match.id ?? "NaN");
|
|
||||||
const [info, setInfo] = useState<DocumentState>({ doc: undefined, notfound: false });
|
|
||||||
const menu_list = (link?: string) => <CommonMenuList url={link}></CommonMenuList>;
|
|
||||||
const fullScreenTargetRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
if (!isNaN(id)) {
|
|
||||||
const c = await DocumentAccessor.findById(id);
|
|
||||||
setInfo({ doc: c, notfound: c === undefined });
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return (
|
|
||||||
<Headline menu={menu_list()}>
|
|
||||||
<Typography variant="h2">Oops. Invalid ID</Typography>
|
|
||||||
</Headline>
|
|
||||||
);
|
|
||||||
} else if (info.notfound) {
|
|
||||||
return (
|
|
||||||
<Headline menu={menu_list()}>
|
|
||||||
<Typography variant="h2">Content has been removed.</Typography>
|
|
||||||
</Headline>
|
|
||||||
);
|
|
||||||
} else if (info.doc === undefined) {
|
|
||||||
return (
|
|
||||||
<Headline menu={menu_list()}>
|
|
||||||
<LoadingCircle />
|
|
||||||
</Headline>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const ReaderPage = getPresenter(info.doc);
|
|
||||||
return (
|
|
||||||
<Headline
|
|
||||||
menu={menu_list(location.pathname)}
|
|
||||||
rightAppbar={
|
|
||||||
<IconButton
|
|
||||||
edge="start"
|
|
||||||
aria-label="account of current user"
|
|
||||||
aria-haspopup="true"
|
|
||||||
onClick={() => {
|
|
||||||
if (fullScreenTargetRef.current != null && document.fullscreenEnabled) {
|
|
||||||
fullScreenTargetRef.current.requestFullscreen();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
<FullscreenIcon />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ReaderPage doc={info.doc} fullScreenTarget={fullScreenTargetRef}></ReaderPage>
|
|
||||||
</Headline>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DocumentAbout = (prop?: {}) => {
|
|
||||||
const match = useParams<{ id: string }>();
|
|
||||||
if (match == null) {
|
|
||||||
throw new Error("unreachable");
|
|
||||||
}
|
|
||||||
const id = Number.parseInt(match.id ?? "NaN");
|
|
||||||
const [info, setInfo] = useState<DocumentState>({ doc: undefined, notfound: false });
|
|
||||||
const menu_list = (link?: string) => <CommonMenuList url={link}></CommonMenuList>;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
if (!isNaN(id)) {
|
|
||||||
const c = await DocumentAccessor.findById(id);
|
|
||||||
setInfo({ doc: c, notfound: c === undefined });
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return (
|
|
||||||
<Headline menu={menu_list()}>
|
|
||||||
<PagePad>
|
|
||||||
<Typography variant="h2">Oops. Invalid ID</Typography>
|
|
||||||
</PagePad>
|
|
||||||
</Headline>
|
|
||||||
);
|
|
||||||
} else if (info.notfound) {
|
|
||||||
return (
|
|
||||||
<Headline menu={menu_list()}>
|
|
||||||
<PagePad>
|
|
||||||
<Typography variant="h2">Content has been removed.</Typography>
|
|
||||||
</PagePad>
|
|
||||||
</Headline>
|
|
||||||
);
|
|
||||||
} else if (info.doc === undefined) {
|
|
||||||
return (
|
|
||||||
<Headline menu={menu_list()}>
|
|
||||||
<PagePad>
|
|
||||||
<LoadingCircle />
|
|
||||||
</PagePad>
|
|
||||||
</Headline>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Headline menu={menu_list()}>
|
|
||||||
<PagePad>
|
|
||||||
<ContentInfo document={info.doc}></ContentInfo>
|
|
||||||
</PagePad>
|
|
||||||
</Headline>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,38 +1,9 @@
|
|||||||
import useSWR from "swr";
|
|
||||||
import { useSearch } from "wouter";
|
import { useSearch } from "wouter";
|
||||||
import type { Document } from "dbtype/api";
|
|
||||||
import { Input } from "@/components/ui/input.tsx";
|
import { Input } from "@/components/ui/input.tsx";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
import { GalleryCard } from "@/components/gallery/GalleryCard.tsx";
|
import { GalleryCard } from "@/components/gallery/GalleryCard.tsx";
|
||||||
import TagBadge from "@/components/gallery/TagBadge.tsx";
|
import TagBadge from "@/components/gallery/TagBadge.tsx";
|
||||||
|
import { useSearchGallery } from "../hook/useSearchGallery";
|
||||||
async function fetcher(url: string) {
|
|
||||||
const res = await fetch(url);
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchParams {
|
|
||||||
word?: string;
|
|
||||||
tags?: string;
|
|
||||||
limit?: number;
|
|
||||||
cursor?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Gallery() {
|
export default function Gallery() {
|
||||||
const search = useSearch();
|
const search = useSearch();
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogContentText,
|
|
||||||
DialogTitle,
|
|
||||||
MenuList,
|
|
||||||
Paper,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
useTheme,
|
|
||||||
} from "@mui/material";
|
|
||||||
import React, { useContext, useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { CommonMenuList, Headline } from "../component/mod";
|
|
||||||
import { UserContext } from "../state";
|
|
||||||
import { doLogin as doSessionLogin } from "../state";
|
|
||||||
import { PagePad } from "../component/pagepad";
|
|
||||||
|
|
||||||
export const LoginPage = () => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const [userLoginInfo, setUserLoginInfo] = useState({ username: "", password: "" });
|
|
||||||
const [openDialog, setOpenDialog] = useState({ open: false, message: "" });
|
|
||||||
const { setUsername, setPermission } = useContext(UserContext);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const handleDialogClose = () => {
|
|
||||||
setOpenDialog({ ...openDialog, open: false });
|
|
||||||
};
|
|
||||||
const doLogin = async () => {
|
|
||||||
try {
|
|
||||||
const b = await doSessionLogin(userLoginInfo);
|
|
||||||
if (typeof b === "string") {
|
|
||||||
setOpenDialog({ open: true, message: b });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(`login as ${b.username}`);
|
|
||||||
setUsername(b.username);
|
|
||||||
setPermission(b.permission);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) {
|
|
||||||
console.error(e);
|
|
||||||
setOpenDialog({ open: true, message: e.message });
|
|
||||||
} else console.error(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigate("/");
|
|
||||||
};
|
|
||||||
const menu = CommonMenuList();
|
|
||||||
return (
|
|
||||||
<Headline menu={menu}>
|
|
||||||
<PagePad>
|
|
||||||
<Paper style={{ width: theme.spacing(40), padding: theme.spacing(4), alignSelf: "center" }}>
|
|
||||||
<Typography variant="h4">Login</Typography>
|
|
||||||
<div style={{ minHeight: theme.spacing(2) }}></div>
|
|
||||||
<form style={{ display: "flex", flexFlow: "column", alignItems: "stretch" }}>
|
|
||||||
<TextField
|
|
||||||
label="username"
|
|
||||||
onChange={(e) => setUserLoginInfo({ ...userLoginInfo, username: e.target.value ?? "" })}
|
|
||||||
></TextField>
|
|
||||||
<TextField
|
|
||||||
label="password"
|
|
||||||
type="password"
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") doLogin();
|
|
||||||
}}
|
|
||||||
onChange={(e) => setUserLoginInfo({ ...userLoginInfo, password: e.target.value ?? "" })}
|
|
||||||
/>
|
|
||||||
<div style={{ minHeight: theme.spacing(2) }}></div>
|
|
||||||
<div style={{ display: "flex" }}>
|
|
||||||
<Button onClick={doLogin}>login</Button>
|
|
||||||
<Button>signin</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Paper>
|
|
||||||
<Dialog open={openDialog.open} onClose={handleDialogClose}>
|
|
||||||
<DialogTitle>Login Failed</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText>detail : {openDialog.message}</DialogContentText>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={handleDialogClose} color="primary" autoFocus>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</PagePad>
|
|
||||||
</Headline>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,149 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Chip,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogContentText,
|
|
||||||
DialogTitle,
|
|
||||||
Divider,
|
|
||||||
Grid,
|
|
||||||
Paper,
|
|
||||||
TextField,
|
|
||||||
Theme,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/material";
|
|
||||||
import React, { useContext, useState } from "react";
|
|
||||||
import { CommonMenuList, Headline } from "../component/mod";
|
|
||||||
import { UserContext } from "../state";
|
|
||||||
import { PagePad } from "../component/pagepad";
|
|
||||||
|
|
||||||
const useStyles = (theme: Theme) => ({
|
|
||||||
paper: {
|
|
||||||
alignSelf: "center",
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
},
|
|
||||||
formfield: {
|
|
||||||
display: "flex",
|
|
||||||
flexFlow: "column",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export function ProfilePage() {
|
|
||||||
const userctx = useContext(UserContext);
|
|
||||||
// const classes = useStyles();
|
|
||||||
const menu = CommonMenuList();
|
|
||||||
const [pw_open, set_pw_open] = useState(false);
|
|
||||||
const [oldpw, setOldpw] = useState("");
|
|
||||||
const [newpw, setNewpw] = useState("");
|
|
||||||
const [newpwch, setNewpwch] = useState("");
|
|
||||||
const [msg_dialog, set_msg_dialog] = useState({ opened: false, msg: "" });
|
|
||||||
const permission_list = userctx.permission.map((p) => <Chip key={p} label={p}></Chip>);
|
|
||||||
const isElectronContent = ((window["electron"] as any) !== undefined) as boolean;
|
|
||||||
const handle_open = () => set_pw_open(true);
|
|
||||||
const handle_close = () => {
|
|
||||||
set_pw_open(false);
|
|
||||||
setNewpw("");
|
|
||||||
setNewpwch("");
|
|
||||||
};
|
|
||||||
const handle_ok = async () => {
|
|
||||||
if (newpw != newpwch) {
|
|
||||||
set_msg_dialog({ opened: true, msg: "password and password check is not equal." });
|
|
||||||
handle_close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isElectronContent) {
|
|
||||||
const elec = window["electron"] as any;
|
|
||||||
const success = elec.passwordReset(userctx.username, newpw);
|
|
||||||
if (!success) {
|
|
||||||
set_msg_dialog({ opened: true, msg: "user not exist." });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const res = await fetch("/user/reset", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: userctx.username,
|
|
||||||
oldpassword: oldpw,
|
|
||||||
newpassword: newpw,
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (res.status != 200) {
|
|
||||||
set_msg_dialog({ opened: true, msg: "failed to change password." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handle_close();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Headline menu={menu}>
|
|
||||||
<PagePad>
|
|
||||||
<Paper /*className={classes.paper}*/>
|
|
||||||
<Grid container direction="column" alignItems="center">
|
|
||||||
<Grid item>
|
|
||||||
<Typography variant="h4">{userctx.username}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Divider></Divider>
|
|
||||||
<Grid item>Permission</Grid>
|
|
||||||
<Grid item>{permission_list.length == 0 ? "-" : permission_list}</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Button onClick={handle_open}>Password Reset</Button>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Paper>
|
|
||||||
<Dialog open={pw_open} onClose={handle_close}>
|
|
||||||
<DialogTitle>Password Reset</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<Typography>type the old and new password</Typography>
|
|
||||||
<div /*className={classes.formfield}*/>
|
|
||||||
{!isElectronContent && (
|
|
||||||
<TextField
|
|
||||||
autoFocus
|
|
||||||
margin="dense"
|
|
||||||
type="password"
|
|
||||||
label="old password"
|
|
||||||
value={oldpw}
|
|
||||||
onChange={(e) => setOldpw(e.target.value)}
|
|
||||||
></TextField>
|
|
||||||
)}
|
|
||||||
<TextField
|
|
||||||
margin="dense"
|
|
||||||
type="password"
|
|
||||||
label="new password"
|
|
||||||
value={newpw}
|
|
||||||
onChange={(e) => setNewpw(e.target.value)}
|
|
||||||
></TextField>
|
|
||||||
<TextField
|
|
||||||
margin="dense"
|
|
||||||
type="password"
|
|
||||||
label="new password check"
|
|
||||||
value={newpwch}
|
|
||||||
onChange={(e) => setNewpwch(e.target.value)}
|
|
||||||
></TextField>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={handle_close} color="primary">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handle_ok} color="primary">
|
|
||||||
Ok
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
<Dialog open={msg_dialog.opened} onClose={() => set_msg_dialog({ opened: false, msg: "" })}>
|
|
||||||
<DialogTitle>Alert!</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText>{msg_dialog.msg}</DialogContentText>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={() => set_msg_dialog({ opened: false, msg: "" })} color="primary">
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</PagePad>
|
|
||||||
</Headline>
|
|
||||||
);
|
|
||||||
}
|
|
@ -8,6 +8,8 @@ export function ProfilePage() {
|
|||||||
console.error("User session expired. Redirecting to login page.");
|
console.error("User session expired. Redirecting to login page.");
|
||||||
return <Redirect to="/login" />;
|
return <Redirect to="/login" />;
|
||||||
}
|
}
|
||||||
|
// TODO: Add a logout button
|
||||||
|
// TODO: Add a change password button
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<Card>
|
<Card>
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import { Paper, Typography } from "@mui/material";
|
|
||||||
import React from "react";
|
|
||||||
import { CommonMenuList, Headline } from "../component/mod";
|
|
||||||
import { PagePad } from "../component/pagepad";
|
|
||||||
|
|
||||||
export const SettingPage = () => {
|
|
||||||
const menu = CommonMenuList();
|
|
||||||
return (
|
|
||||||
<Headline menu={menu}>
|
|
||||||
<PagePad>
|
|
||||||
<Paper>
|
|
||||||
<Typography variant="h2">Setting</Typography>
|
|
||||||
</Paper>
|
|
||||||
</PagePad>
|
|
||||||
</Headline>
|
|
||||||
);
|
|
||||||
};
|
|
102
packages/client/src/page/settingPage.tsx
Normal file
102
packages/client/src/page/settingPage.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { type TernaryDarkMode, useTernaryDarkMode, useMediaQuery } from "usehooks-ts";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
function LightModeView() {
|
||||||
|
return <div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
|
||||||
|
<div className="space-y-2 rounded-sm bg-[#ecedef] p-2">
|
||||||
|
<div className="space-y-2 rounded-md bg-white p-2 shadow-sm">
|
||||||
|
<div className="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
|
||||||
|
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
|
||||||
|
<div className="h-4 w-4 rounded-full bg-[#ecedef]" />
|
||||||
|
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
|
||||||
|
<div className="h-4 w-4 rounded-full bg-[#ecedef]" />
|
||||||
|
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DarkModeView() {
|
||||||
|
return <div className="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
|
||||||
|
<div className="space-y-2 rounded-sm bg-slate-950 p-2">
|
||||||
|
<div className="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||||
|
<div className="h-2 w-[80px] rounded-lg bg-slate-400" />
|
||||||
|
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||||
|
<div className="h-4 w-4 rounded-full bg-slate-400" />
|
||||||
|
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||||
|
<div className="h-4 w-4 rounded-full bg-slate-400" />
|
||||||
|
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingPage() {
|
||||||
|
const { setTernaryDarkMode, ternaryDarkMode, isDarkMode } = 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>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl">Settings</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg">Appearance</h3>
|
||||||
|
<span className="text-muted-foreground text-sm">Dark mode</span>
|
||||||
|
</div>
|
||||||
|
<RadioGroup value={ternaryDarkMode} onValueChange={(v) => setTernaryDarkMode(v as TernaryDarkMode)}
|
||||||
|
className="flex space-x-2 items-center"
|
||||||
|
>
|
||||||
|
<RadioGroupItem id="dark" value="dark" className="sr-only" />
|
||||||
|
<Label htmlFor="dark">
|
||||||
|
<div className="grid place-items-center">
|
||||||
|
<DarkModeView />
|
||||||
|
<span>Dark Mode</span>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
<RadioGroupItem id="light" value="light" className="sr-only" />
|
||||||
|
<Label htmlFor="light">
|
||||||
|
<div className="grid place-items-center">
|
||||||
|
<LightModeView />
|
||||||
|
<span>Light Mode</span>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
<RadioGroupItem id="system" value="system" className="sr-only" />
|
||||||
|
<Label htmlFor="system">
|
||||||
|
<div className="grid place-items-center">
|
||||||
|
{isSystemDarkMode ? <DarkModeView /> : <LightModeView />}
|
||||||
|
<span>System Mode</span>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingPage;
|
150
pnpm-lock.yaml
150
pnpm-lock.yaml
@ -20,6 +20,9 @@ importers:
|
|||||||
'@radix-ui/react-label':
|
'@radix-ui/react-label':
|
||||||
specifier: ^2.0.2
|
specifier: ^2.0.2
|
||||||
version: 2.0.2(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
version: 2.0.2(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-radio-group':
|
||||||
|
specifier: ^1.1.3
|
||||||
|
version: 1.1.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-slot':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.0.2
|
specifier: ^1.0.2
|
||||||
version: 1.0.2(@types/react@18.2.71)(react@18.2.0)
|
version: 1.0.2(@types/react@18.2.71)(react@18.2.0)
|
||||||
@ -35,6 +38,9 @@ importers:
|
|||||||
dbtype:
|
dbtype:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../dbtype
|
version: link:../dbtype
|
||||||
|
jotai:
|
||||||
|
specifier: ^2.7.2
|
||||||
|
version: 2.7.2(@types/react@18.2.71)(react@18.2.0)
|
||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
@ -53,6 +59,9 @@ importers:
|
|||||||
tailwindcss-animate:
|
tailwindcss-animate:
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(tailwindcss@3.4.3)
|
version: 1.0.7(tailwindcss@3.4.3)
|
||||||
|
usehooks-ts:
|
||||||
|
specifier: ^3.1.0
|
||||||
|
version: 3.1.0(react@18.2.0)
|
||||||
wouter:
|
wouter:
|
||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.0(react@18.2.0)
|
version: 3.1.0(react@18.2.0)
|
||||||
@ -987,6 +996,30 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.24.1
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.71
|
||||||
|
'@types/react-dom': 18.2.22
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.71)(react@18.2.0):
|
/@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.71)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
|
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1015,6 +1048,20 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-direction@1.0.1(@types/react@18.2.71)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.24.1
|
||||||
|
'@types/react': 18.2.71
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==}
|
resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1178,6 +1225,65 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-radio-group@1.1.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.24.1
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.71
|
||||||
|
'@types/react-dom': 18.2.22
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.24.1
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@radix-ui/react-id': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.71)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.71
|
||||||
|
'@types/react-dom': 18.2.22
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-slot@1.0.2(@types/react@18.2.71)(react@18.2.0):
|
/@radix-ui/react-slot@1.0.2(@types/react@18.2.71)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1283,6 +1389,20 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-use-previous@1.0.1(@types/react@18.2.71)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.24.1
|
||||||
|
'@types/react': 18.2.71
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-use-rect@1.0.1(@types/react@18.2.71)(react@18.2.0):
|
/@radix-ui/react-use-rect@1.0.1(@types/react@18.2.71)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==}
|
resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -3443,6 +3563,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==}
|
resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
/jotai@2.7.2(@types/react@18.2.71)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-6Ft5kpNu8p93Ssf1Faoza3hYQZRIYp7rioK8MwTTFnbQKwUyZElwquPwl1h6U0uo9hC0jr+ghO3gcSjc6P35/Q==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=17.0.0'
|
||||||
|
react: '>=17.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.2.71
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/js-tokens@4.0.0:
|
/js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@ -3678,6 +3814,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==}
|
resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash.debounce@4.0.8:
|
||||||
|
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.includes@4.3.0:
|
/lodash.includes@4.3.0:
|
||||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -5069,6 +5209,16 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/usehooks-ts@3.1.0(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==}
|
||||||
|
engines: {node: '>=16.15.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17 || ^18
|
||||||
|
dependencies:
|
||||||
|
lodash.debounce: 4.0.8
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/util-deprecate@1.0.2:
|
/util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user