diff --git a/packages/client/package.json b/packages/client/package.json index f6f8f3c..36d89b2 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -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": { diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 9e9088f..b8892e2 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -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 = () => { + {/* }> }> - }> }>*/} diff --git a/packages/client/src/accessor/document.ts b/packages/client/src/accessor/document.ts deleted file mode 100644 index ac95feb..0000000 --- a/packages/client/src/accessor/document.ts +++ /dev/null @@ -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; - addList: (content_list: DocumentBody[]) => Promise; - async findByPath(basepath: string, filename?: string): Promise { - throw new Error("not allowed"); - } - async findDeleted(content_type: string): Promise { - throw new Error("not allowed"); - } - async findList(option?: QueryListOption | undefined): Promise { - 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 { - 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 { - throw new Error("not implement"); - return []; - } - async update(c: Partial & { id: number }): Promise { - 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 { - 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 { - const res = await fetch(`${baseurl}/${id}`, { - method: "DELETE", - }); - const ret = await res.json(); - return ret; - } - async addTag(c: Document, tag_name: string): Promise { - 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 { - 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; diff --git a/packages/client/src/accessor/util.ts b/packages/client/src/accessor/util.ts deleted file mode 100644 index 8af0f61..0000000 --- a/packages/client/src/accessor/util.ts +++ /dev/null @@ -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; -}; diff --git a/packages/client/src/components/gallery/GalleryCard.tsx b/packages/client/src/components/gallery/GalleryCard.tsx index 97eafcb..064b32f 100644 --- a/packages/client/src/components/gallery/GalleryCard.tsx +++ b/packages/client/src/components/gallery/GalleryCard.tsx @@ -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(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(() => { @@ -44,7 +44,7 @@ export function GalleryCard({ window.removeEventListener("resize", listener); }; }, []); - + return
- {x.title} + + {x.title} + - {artists.join(", ")} {groups.length > 0 && "|"} {groups.join(", ")} + {artists.map((x, i) => + {x} + {i + 1 < artists.length && , } + )} + {groups.length > 0 && {" | "}} + {groups.map((x, i) => + {x} + {i + 1 < groups.length && , } + + )}
    {clippedTags.map(tag => )} - {clippedTags.length < originalTags.length && } + {clippedTags.length < originalTags.length && }
diff --git a/packages/client/src/components/gallery/StyledLink.tsx b/packages/client/src/components/gallery/StyledLink.tsx new file mode 100644 index 0000000..b2e4458 --- /dev/null +++ b/packages/client/src/components/gallery/StyledLink.tsx @@ -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 {children} +} \ No newline at end of file diff --git a/packages/client/src/components/gallery/TagBadge.tsx b/packages/client/src/components/gallery/TagBadge.tsx index 4a46a7a..4b82c69 100644 --- a/packages/client/src/components/gallery/TagBadge.tsx +++ b/packages/client/src/components/gallery/TagBadge.tsx @@ -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, diff --git a/packages/client/src/components/ui/radio-group.tsx b/packages/client/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..c939b06 --- /dev/null +++ b/packages/client/src/components/ui/radio-group.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/packages/client/src/hook/fetcher.tsx b/packages/client/src/hook/fetcher.tsx new file mode 100644 index 0000000..f5e5b0f --- /dev/null +++ b/packages/client/src/hook/fetcher.tsx @@ -0,0 +1,4 @@ +export async function fetcher(url: string) { + const res = await fetch(url); + return res.json(); +} diff --git a/packages/client/src/hook/useGalleryDoc.tsx b/packages/client/src/hook/useGalleryDoc.tsx new file mode 100644 index 0000000..54a7dce --- /dev/null +++ b/packages/client/src/hook/useGalleryDoc.tsx @@ -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(`/api/doc/${id}`, fetcher); +} \ No newline at end of file diff --git a/packages/client/src/hook/useSearchGallery.tsx b/packages/client/src/hook/useSearchGallery.tsx new file mode 100644 index 0000000..800124c --- /dev/null +++ b/packages/client/src/hook/useSearchGallery.tsx @@ -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); +} diff --git a/packages/client/src/page/contentInfoPage.tsx b/packages/client/src/page/contentInfoPage.tsx index abd4f91..b45a949 100644 --- a/packages/client/src/page/contentInfoPage.tsx +++ b/packages/client/src/page/contentInfoPage.tsx @@ -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
Loading...
+ } + + if (error) { + return
Error: {String(error)}
+ } + + if (!data) { + return
Not found
+ } + + const tags = data?.tags ?? []; + const classifiedTags = classifyTags(tags); return (
-

ContentInfoPage

- {params.id} -

Find me in packages/client/src/page/contentInfoPage.tsx

+
+ {data.title} +
+ + + {data.title} + + + {classifiedTags.type[0] ?? "N/A"} + + + + +
+ + + + + {new Date(data.created_at).toLocaleString()} + {new Date(data.modified_at).toLocaleString()} + {`${data.basepath}/${data.filename}`} + {JSON.stringify(data.additional)} +
+
+ Tags +
    + {classifiedTags.rest.map((tag) => )} +
+
+
+
); } -export default ContentInfoPage; \ No newline at end of file +export default ContentInfoPage; + +function DescItem({ name, children, className }: { + name: string, + className?: string, + children?: React.ReactNode +}) { + return
+ {name} + {children} +
; +} + +function DescTagItem({ + items, + name, + className, +}: { + name: string; + items: string[]; + className?: string; +}) { + return + {items.length === 0 ? "N/A" : items.map( + (x) => {x} + )} + +} diff --git a/packages/client/src/page/contentinfo.tsx b/packages/client/src/page/contentinfo.tsx deleted file mode 100644 index 655585c..0000000 --- a/packages/client/src/page/contentinfo.tsx +++ /dev/null @@ -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({ doc: undefined, notfound: false }); - const menu_list = (link?: string) => ; - const fullScreenTargetRef = useRef(null); - - useEffect(() => { - (async () => { - if (!isNaN(id)) { - const c = await DocumentAccessor.findById(id); - setInfo({ doc: c, notfound: c === undefined }); - } - })(); - }, []); - - if (isNaN(id)) { - return ( - - Oops. Invalid ID - - ); - } else if (info.notfound) { - return ( - - Content has been removed. - - ); - } else if (info.doc === undefined) { - return ( - - - - ); - } else { - const ReaderPage = getPresenter(info.doc); - return ( - { - if (fullScreenTargetRef.current != null && document.fullscreenEnabled) { - fullScreenTargetRef.current.requestFullscreen(); - } - }} - color="inherit" - > - - - } - > - - - ); - } -} - -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({ doc: undefined, notfound: false }); - const menu_list = (link?: string) => ; - - useEffect(() => { - (async () => { - if (!isNaN(id)) { - const c = await DocumentAccessor.findById(id); - setInfo({ doc: c, notfound: c === undefined }); - } - })(); - }, []); - - if (isNaN(id)) { - return ( - - - Oops. Invalid ID - - - ); - } else if (info.notfound) { - return ( - - - Content has been removed. - - - ); - } else if (info.doc === undefined) { - return ( - - - - - - ); - } else { - return ( - - - - - - ); - } -}; diff --git a/packages/client/src/page/galleryPage.tsx b/packages/client/src/page/galleryPage.tsx index 0648bbe..81699f5 100644 --- a/packages/client/src/page/galleryPage.tsx +++ b/packages/client/src/page/galleryPage.tsx @@ -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(); diff --git a/packages/client/src/page/login.tsx b/packages/client/src/page/login.tsx deleted file mode 100644 index ba94ec0..0000000 --- a/packages/client/src/page/login.tsx +++ /dev/null @@ -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 ( - - - - Login -
-
- setUserLoginInfo({ ...userLoginInfo, username: e.target.value ?? "" })} - > - { - if (e.key === "Enter") doLogin(); - }} - onChange={(e) => setUserLoginInfo({ ...userLoginInfo, password: e.target.value ?? "" })} - /> -
-
- - -
- -
- - Login Failed - - detail : {openDialog.message} - - - - - -
-
- ); -}; diff --git a/packages/client/src/page/profile.tsx b/packages/client/src/page/profile.tsx deleted file mode 100644 index 1ad0e15..0000000 --- a/packages/client/src/page/profile.tsx +++ /dev/null @@ -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) => ); - 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 ( - - - - - - {userctx.username} - - - Permission - {permission_list.length == 0 ? "-" : permission_list} - - - - - - - Password Reset - - type the old and new password -
- {!isElectronContent && ( - setOldpw(e.target.value)} - > - )} - setNewpw(e.target.value)} - > - setNewpwch(e.target.value)} - > -
-
- - - - -
- set_msg_dialog({ opened: false, msg: "" })}> - Alert! - - {msg_dialog.msg} - - - - - -
-
- ); -} diff --git a/packages/client/src/page/profilesPage.tsx b/packages/client/src/page/profilesPage.tsx index 852bd42..8039a06 100644 --- a/packages/client/src/page/profilesPage.tsx +++ b/packages/client/src/page/profilesPage.tsx @@ -8,6 +8,8 @@ export function ProfilePage() { console.error("User session expired. Redirecting to login page."); return ; } + // TODO: Add a logout button + // TODO: Add a change password button return (
diff --git a/packages/client/src/page/setting.tsx b/packages/client/src/page/setting.tsx deleted file mode 100644 index 19a3b39..0000000 --- a/packages/client/src/page/setting.tsx +++ /dev/null @@ -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 ( - - - - Setting - - - - ); -}; diff --git a/packages/client/src/page/settingPage.tsx b/packages/client/src/page/settingPage.tsx new file mode 100644 index 0000000..3f8f901 --- /dev/null +++ b/packages/client/src/page/settingPage.tsx @@ -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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
; +} + +function DarkModeView() { + return
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+} + +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 ( +
+ + + Settings + + +
+
+

Appearance

+ Dark mode +
+ setTernaryDarkMode(v as TernaryDarkMode)} + className="flex space-x-2 items-center" + > + + + + + + + +
+
+
+
+ ) +} + +export default SettingPage; \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c57ff2..13ee424 100644 --- a/pnpm-lock.yaml +++ b/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==}