settings page
This commit is contained in:
parent
3813dbfeff
commit
2df64ac2fe
@ -13,17 +13,20 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@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-tooltip": "^1.0.7",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"dbtype": "workspace:*",
|
||||
"jotai": "^2.7.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-resizable-panels": "^2.0.16",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"wouter": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -22,6 +22,7 @@ import NotFoundPage from "./page/404.tsx";
|
||||
import LoginPage from "./page/loginPage.tsx";
|
||||
import ProfilePage from "./page/profilesPage.tsx";
|
||||
import ContentInfoPage from "./page/contentInfoPage.tsx";
|
||||
import SettingPage from "./page/settingPage.tsx";
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
@ -33,10 +34,10 @@ const App = () => {
|
||||
<Route path="/login" component={LoginPage} />
|
||||
<Route path="/profile" component={ProfilePage}/>
|
||||
<Route path="/doc/:id" component={ContentInfoPage}/>
|
||||
<Route path="/setting" component={SettingPage} />
|
||||
{/*
|
||||
<Route path="/doc/:id/reader" component={<ReaderPage />}></Route>
|
||||
<Route path="/difference" component={<DifferencePage />}></Route>
|
||||
<Route path="/setting" component={<SettingPage />}></Route>
|
||||
<Route path="/tags" component={<TagsPage />}></Route>*/}
|
||||
<Route component={NotFoundPage} />
|
||||
</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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.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 { LazyImage } from "./LazyImage.tsx";
|
||||
import StyledLink from "./StyledLink.tsx";
|
||||
|
||||
function clipTagsWhenOverflow(tags: string[], limit: number) {
|
||||
let l = 0;
|
||||
@ -22,12 +23,11 @@ export function GalleryCard({
|
||||
}: { doc: Document; }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [clipCharCount, setClipCharCount] = useState(200);
|
||||
const [location] = useLocation();
|
||||
|
||||
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 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);
|
||||
|
||||
useEffect(() => {
|
||||
@ -55,16 +55,27 @@ export function GalleryCard({
|
||||
<div className="flex-1 flex flex-col">
|
||||
<CardHeader className="flex-none">
|
||||
<CardTitle>
|
||||
<Link href={`/doc/${x.id}`} state={{fromUrl: location}}>{x.title}</Link>
|
||||
<StyledLink className="" to={`/doc/${x.id}`}>
|
||||
{x.title}
|
||||
</StyledLink>
|
||||
</CardTitle>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1" ref={ref}>
|
||||
<ul className="flex flex-wrap gap-2 items-baseline content-start">
|
||||
{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>
|
||||
</CardContent>
|
||||
</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 { cn } from "@/lib/utils.ts";
|
||||
|
||||
const femaleTagPrefix = "female:";
|
||||
const maleTagPrefix = "male:";
|
||||
|
||||
function getTagKind(tagname: string) {
|
||||
if (tagname.startsWith(femaleTagPrefix)) {
|
||||
return "female";
|
||||
if (tagname.match(":") === null) {
|
||||
return "default";
|
||||
}
|
||||
if (tagname.startsWith(maleTagPrefix)){
|
||||
return "male";
|
||||
}
|
||||
return "default";
|
||||
const prefix = tagname.split(":")[0];
|
||||
return prefix;
|
||||
}
|
||||
|
||||
function toPrettyTagname(tagname: string): string {
|
||||
const kind = getTagKind(tagname);
|
||||
const name = tagname.slice(kind.length + 1);
|
||||
|
||||
switch (kind) {
|
||||
case "male":
|
||||
return `♂ ${tagname.slice(maleTagPrefix.length)}`;
|
||||
return `♂ ${name}`;
|
||||
case "female":
|
||||
return `♀ ${tagname.slice(femaleTagPrefix.length)}`;
|
||||
default:
|
||||
return `♀ ${name}`;
|
||||
case "artist":
|
||||
return `🎨 ${name}`;
|
||||
case "group":
|
||||
return `🖿 ${name}`;
|
||||
case "series":
|
||||
return `📚 ${name}`
|
||||
case "character":
|
||||
return `👤 ${name}`;
|
||||
case "default":
|
||||
return tagname;
|
||||
default:
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,6 +42,11 @@ export default function TagBadge(props: { tagname: string, className?: string; d
|
||||
"px-1",
|
||||
kind === "male" && "bg-[#2a7bbf] hover:bg-[#3091e7]",
|
||||
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]",
|
||||
props.disabled && "opacity-50",
|
||||
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 {
|
||||
params: {
|
||||
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 (
|
||||
<div className="p-4">
|
||||
<h1>ContentInfoPage</h1>
|
||||
{params.id}
|
||||
<p>Find me in packages/client/src/page/contentInfoPage.tsx</p>
|
||||
<div className="m-auto h-[400px] mb-4 flex justify-center items-center flex-none bg-[#272733]
|
||||
rounded-xl shadow-lg overflow-hidden">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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 type { Document } from "dbtype/api";
|
||||
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";
|
||||
|
||||
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);
|
||||
}
|
||||
import { useSearchGallery } from "../hook/useSearchGallery";
|
||||
|
||||
export default function Gallery() {
|
||||
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.");
|
||||
return <Redirect to="/login" />;
|
||||
}
|
||||
// TODO: Add a logout button
|
||||
// TODO: Add a change password button
|
||||
return (
|
||||
<div className="p-4">
|
||||
<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':
|
||||
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)
|
||||
'@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':
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2(@types/react@18.2.71)(react@18.2.0)
|
||||
@ -35,6 +38,9 @@ importers:
|
||||
dbtype:
|
||||
specifier: workspace:*
|
||||
version: link:../dbtype
|
||||
jotai:
|
||||
specifier: ^2.7.2
|
||||
version: 2.7.2(@types/react@18.2.71)(react@18.2.0)
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
@ -53,6 +59,9 @@ importers:
|
||||
tailwindcss-animate:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(tailwindcss@3.4.3)
|
||||
usehooks-ts:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0(react@18.2.0)
|
||||
wouter:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0(react@18.2.0)
|
||||
@ -987,6 +996,30 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
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):
|
||||
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
|
||||
peerDependencies:
|
||||
@ -1015,6 +1048,20 @@ packages:
|
||||
react: 18.2.0
|
||||
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):
|
||||
resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==}
|
||||
peerDependencies:
|
||||
@ -1178,6 +1225,65 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
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):
|
||||
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
||||
peerDependencies:
|
||||
@ -1283,6 +1389,20 @@ packages:
|
||||
react: 18.2.0
|
||||
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):
|
||||
resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==}
|
||||
peerDependencies:
|
||||
@ -3443,6 +3563,22 @@ packages:
|
||||
resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==}
|
||||
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:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@ -3678,6 +3814,10 @@ packages:
|
||||
resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==}
|
||||
dev: true
|
||||
|
||||
/lodash.debounce@4.0.8:
|
||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||
dev: false
|
||||
|
||||
/lodash.includes@4.3.0:
|
||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||
dev: false
|
||||
@ -5069,6 +5209,16 @@ packages:
|
||||
react: 18.2.0
|
||||
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:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user