From f5dfd52f6efece2f4cad227044ecf4dd66301bbc Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 26 Aug 2023 21:36:58 +0800 Subject: ... --- FrontEnd/src/components/menu/PopupMenu.tsx | 73 ++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 FrontEnd/src/components/menu/PopupMenu.tsx (limited to 'FrontEnd/src/components/menu/PopupMenu.tsx') diff --git a/FrontEnd/src/components/menu/PopupMenu.tsx b/FrontEnd/src/components/menu/PopupMenu.tsx new file mode 100644 index 00000000..23a67f79 --- /dev/null +++ b/FrontEnd/src/components/menu/PopupMenu.tsx @@ -0,0 +1,73 @@ +import { useState, CSSProperties, ReactNode } from "react"; +import classNames from "classnames"; +import { createPortal } from "react-dom"; +import { usePopper } from "react-popper"; + +import { useClickOutside } from "~src/utilities/hooks"; + +import Menu, { MenuItems } from "./Menu"; + +import { ThemeColor } from "../common"; + +import "./PopupMenu.css"; + +export interface PopupMenuProps { + color?: ThemeColor; + items: MenuItems; + children?: ReactNode; + containerClassName?: string; + containerStyle?: CSSProperties; +} + +export default function PopupMenu({ + color, + items, + children, + containerClassName, + containerStyle, +}: PopupMenuProps) { + const [show, setShow] = useState(false); + + const [referenceElement, setReferenceElement] = + useState(null); + const [popperElement, setPopperElement] = useState( + null, + ); + const { styles, attributes } = usePopper(referenceElement, popperElement); + + useClickOutside(popperElement, () => setShow(false), true); + + return ( +
setShow(true)} + > + {children} + {show && + createPortal( +
+ { + setShow(false); + }} + /> +
, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + document.getElementById("portal")!, + )} +
+ ); +} -- cgit v1.2.3 From 256cc9592a3f31fc392e1ccdb699aa206b7b47ce Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 26 Aug 2023 23:49:28 +0800 Subject: ... --- FrontEnd/src/common.ts | 2 - FrontEnd/src/components/AppBar.tsx | 3 +- FrontEnd/src/components/Card.tsx | 1 + FrontEnd/src/components/LoadFailReload.tsx | 37 ------------------ FrontEnd/src/components/LoadingPage.tsx | 13 ------- FrontEnd/src/components/SearchInput.tsx | 2 +- FrontEnd/src/components/button/ButtonRowV2.tsx | 3 +- FrontEnd/src/components/button/LoadingButton.tsx | 1 - FrontEnd/src/components/common.ts | 1 - FrontEnd/src/components/dialog/ConfirmDialog.css | 0 FrontEnd/src/components/dialog/ConfirmDialog.tsx | 1 - FrontEnd/src/components/dialog/OperationDialog.tsx | 7 +--- FrontEnd/src/components/dialog/index.ts | 1 + FrontEnd/src/components/hooks.ts | 14 ------- FrontEnd/src/components/hooks/index.ts | 3 ++ FrontEnd/src/components/hooks/responsive.ts | 7 ++++ FrontEnd/src/components/hooks/useClickOutside.ts | 38 ++++++++++++++++++ FrontEnd/src/components/hooks/useScrollToBottom.ts | 44 +++++++++++++++++++++ FrontEnd/src/components/input/InputGroup.tsx | 16 ++++---- FrontEnd/src/components/menu/Menu.tsx | 2 +- FrontEnd/src/components/menu/PopupMenu.tsx | 6 +-- FrontEnd/src/pages/login/index.tsx | 14 ++----- FrontEnd/src/pages/register/index.tsx | 26 +++++-------- FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx | 5 +-- .../src/pages/setting/ChangePasswordDialog.tsx | 17 ++++---- FrontEnd/src/pages/setting/index.tsx | 11 +++--- FrontEnd/src/pages/timeline/Timeline.tsx | 3 +- .../src/pages/timeline/TimelineDeleteDialog.tsx | 4 +- FrontEnd/src/pages/timeline/TimelinePostView.tsx | 8 ++-- FrontEnd/src/utilities/hooks.ts | 5 --- FrontEnd/src/utilities/hooks/mediaQuery.ts | 5 --- FrontEnd/src/utilities/hooks/useClickOutside.ts | 38 ------------------ FrontEnd/src/utilities/hooks/useScrollToBottom.ts | 45 ---------------------- 33 files changed, 149 insertions(+), 234 deletions(-) delete mode 100644 FrontEnd/src/components/LoadFailReload.tsx delete mode 100644 FrontEnd/src/components/LoadingPage.tsx delete mode 100644 FrontEnd/src/components/dialog/ConfirmDialog.css delete mode 100644 FrontEnd/src/components/hooks.ts create mode 100644 FrontEnd/src/components/hooks/index.ts create mode 100644 FrontEnd/src/components/hooks/responsive.ts create mode 100644 FrontEnd/src/components/hooks/useClickOutside.ts create mode 100644 FrontEnd/src/components/hooks/useScrollToBottom.ts delete mode 100644 FrontEnd/src/utilities/hooks.ts delete mode 100644 FrontEnd/src/utilities/hooks/mediaQuery.ts delete mode 100644 FrontEnd/src/utilities/hooks/useClickOutside.ts delete mode 100644 FrontEnd/src/utilities/hooks/useScrollToBottom.ts (limited to 'FrontEnd/src/components/menu/PopupMenu.tsx') diff --git a/FrontEnd/src/common.ts b/FrontEnd/src/common.ts index 7c053140..1ca796c3 100644 --- a/FrontEnd/src/common.ts +++ b/FrontEnd/src/common.ts @@ -3,8 +3,6 @@ // This error should never occur. If it does, it indicates there is some logic bug in codes. export class UiLogicError extends Error {} -export const highlightTimelineUsername = "crupest"; - export type { I18nText } from "./i18n"; export type { I18nText as Text } from "./i18n"; export { c, convertI18nText } from "./i18n"; diff --git a/FrontEnd/src/components/AppBar.tsx b/FrontEnd/src/components/AppBar.tsx index da3a946f..1a5c1941 100644 --- a/FrontEnd/src/components/AppBar.tsx +++ b/FrontEnd/src/components/AppBar.tsx @@ -2,9 +2,10 @@ import { useState } from "react"; import classnames from "classnames"; import { Link, NavLink } from "react-router-dom"; -import { I18nText, useC, useMobile } from "./common"; import { useUser } from "~src/services/user"; +import { I18nText, useC } from "./common"; +import { useMobile } from "./hooks"; import TimelineLogo from "./TimelineLogo"; import { IconButton } from "./button"; import UserAvatar from "./user/UserAvatar"; diff --git a/FrontEnd/src/components/Card.tsx b/FrontEnd/src/components/Card.tsx index a8f0d3cc..5d3ef630 100644 --- a/FrontEnd/src/components/Card.tsx +++ b/FrontEnd/src/components/Card.tsx @@ -2,6 +2,7 @@ import { ComponentPropsWithoutRef, Ref } from "react"; import classNames from "classnames"; import { ThemeColor } from "./common"; + import "./Card.css"; interface CardProps extends ComponentPropsWithoutRef<"div"> { diff --git a/FrontEnd/src/components/LoadFailReload.tsx b/FrontEnd/src/components/LoadFailReload.tsx deleted file mode 100644 index 81ba1f67..00000000 --- a/FrontEnd/src/components/LoadFailReload.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as React from "react"; -import { Trans } from "react-i18next"; - -export interface LoadFailReloadProps { - className?: string; - style?: React.CSSProperties; - onReload: () => void; -} - -const LoadFailReload: React.FC = ({ - onReload, - className, - style, -}) => { - return ( - - 0 - { - onReload(); - e.preventDefault(); - }} - > - 1 - - 2 - - ); -}; - -export default LoadFailReload; diff --git a/FrontEnd/src/components/LoadingPage.tsx b/FrontEnd/src/components/LoadingPage.tsx deleted file mode 100644 index 35ee1aa8..00000000 --- a/FrontEnd/src/components/LoadingPage.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from "react"; - -import Spinner from "./Spinner"; - -const LoadingPage: React.FC = () => { - return ( -
- -
- ); -}; - -export default LoadingPage; diff --git a/FrontEnd/src/components/SearchInput.tsx b/FrontEnd/src/components/SearchInput.tsx index e3216b86..71820bfa 100644 --- a/FrontEnd/src/components/SearchInput.tsx +++ b/FrontEnd/src/components/SearchInput.tsx @@ -1,7 +1,7 @@ import classNames from "classnames"; import { useC, Text } from "./common"; -import LoadingButton from "./button/LoadingButton"; +import { LoadingButton } from "./button"; import "./SearchInput.css"; diff --git a/FrontEnd/src/components/button/ButtonRowV2.tsx b/FrontEnd/src/components/button/ButtonRowV2.tsx index 3467ad52..5129e7f1 100644 --- a/FrontEnd/src/components/button/ButtonRowV2.tsx +++ b/FrontEnd/src/components/button/ButtonRowV2.tsx @@ -1,13 +1,14 @@ import { ComponentPropsWithoutRef, Ref } from "react"; import classNames from "classnames"; +import { Text, ThemeColor } from "../common"; + import Button from "./Button"; import FlatButton from "./FlatButton"; import IconButton from "./IconButton"; import LoadingButton from "./LoadingButton"; import "./ButtonRow.css"; -import { Text, ThemeColor } from "../common"; interface ButtonRowV2ButtonBase { key: string | number; diff --git a/FrontEnd/src/components/button/LoadingButton.tsx b/FrontEnd/src/components/button/LoadingButton.tsx index 7e7d08e6..d9d41ddb 100644 --- a/FrontEnd/src/components/button/LoadingButton.tsx +++ b/FrontEnd/src/components/button/LoadingButton.tsx @@ -1,7 +1,6 @@ import classNames from "classnames"; import { I18nText, ThemeColor, useC } from "../common"; - import Spinner from "../Spinner"; import "./LoadingButton.css"; diff --git a/FrontEnd/src/components/common.ts b/FrontEnd/src/components/common.ts index e6f7319f..b96388ab 100644 --- a/FrontEnd/src/components/common.ts +++ b/FrontEnd/src/components/common.ts @@ -11,4 +11,3 @@ export const themeColors = [ export type ThemeColor = (typeof themeColors)[number]; export { breakpoints } from "./breakpoints"; -export { useMobile } from "./hooks"; diff --git a/FrontEnd/src/components/dialog/ConfirmDialog.css b/FrontEnd/src/components/dialog/ConfirmDialog.css deleted file mode 100644 index e69de29b..00000000 diff --git a/FrontEnd/src/components/dialog/ConfirmDialog.tsx b/FrontEnd/src/components/dialog/ConfirmDialog.tsx index 26939c9b..1d997305 100644 --- a/FrontEnd/src/components/dialog/ConfirmDialog.tsx +++ b/FrontEnd/src/components/dialog/ConfirmDialog.tsx @@ -1,5 +1,4 @@ import { useC, Text, ThemeColor } from "../common"; - import Dialog from "./Dialog"; import DialogContainer from "./DialogContainer"; diff --git a/FrontEnd/src/components/dialog/OperationDialog.tsx b/FrontEnd/src/components/dialog/OperationDialog.tsx index e5db7f4f..96766825 100644 --- a/FrontEnd/src/components/dialog/OperationDialog.tsx +++ b/FrontEnd/src/components/dialog/OperationDialog.tsx @@ -2,23 +2,18 @@ import { useState, ReactNode, ComponentProps } from "react"; import classNames from "classnames"; import { useC, Text, ThemeColor } from "../common"; - import { useInputs, InputGroup, Initializer as InputInitializer, - InputValueDict, - InputErrorDict, InputConfirmValueDict, } from "../input"; +import { ButtonRow } from "../button"; import Dialog from "./Dialog"; import DialogContainer from "./DialogContainer"; -import { ButtonRow } from "../button"; import "./OperationDialog.css"; -export type { InputInitializer, InputValueDict, InputErrorDict }; - interface OperationDialogPromptProps { message?: Text; customMessage?: Text; diff --git a/FrontEnd/src/components/dialog/index.ts b/FrontEnd/src/components/dialog/index.ts index 59f15791..17db8fd0 100644 --- a/FrontEnd/src/components/dialog/index.ts +++ b/FrontEnd/src/components/dialog/index.ts @@ -4,6 +4,7 @@ export { default as Dialog } from "./Dialog"; export { default as FullPageDialog } from "./FullPageDialog"; export { default as OperationDialog } from "./OperationDialog"; export { default as ConfirmDialog } from "./ConfirmDialog"; +export { default as DialogContainer } from "./DialogContainer"; type DialogMap = { [K in D]: V; diff --git a/FrontEnd/src/components/hooks.ts b/FrontEnd/src/components/hooks.ts deleted file mode 100644 index 523a4538..00000000 --- a/FrontEnd/src/components/hooks.ts +++ /dev/null @@ -1,14 +0,0 @@ -// TODO: Migrate hooks - -export { - useIsSmallScreen, - useClickOutside, - useScrollToBottom, -} from "~src/utilities/hooks"; - -import { useMediaQuery } from "react-responsive"; -import { breakpoints } from "./breakpoints"; - -export function useMobile(): boolean { - return useMediaQuery({ maxWidth: breakpoints.sm }); -} diff --git a/FrontEnd/src/components/hooks/index.ts b/FrontEnd/src/components/hooks/index.ts new file mode 100644 index 00000000..3c9859bc --- /dev/null +++ b/FrontEnd/src/components/hooks/index.ts @@ -0,0 +1,3 @@ +export { useMobile } from "./responsive"; +export { default as useClickOutside } from "./useClickOutside"; +export { default as useScrollToBottom } from "./useScrollToBottom"; diff --git a/FrontEnd/src/components/hooks/responsive.ts b/FrontEnd/src/components/hooks/responsive.ts new file mode 100644 index 00000000..6bcce96c --- /dev/null +++ b/FrontEnd/src/components/hooks/responsive.ts @@ -0,0 +1,7 @@ +import { useMediaQuery } from "react-responsive"; + +import { breakpoints } from "../breakpoints"; + +export function useMobile(): boolean { + return useMediaQuery({ maxWidth: breakpoints.sm }); +} diff --git a/FrontEnd/src/components/hooks/useClickOutside.ts b/FrontEnd/src/components/hooks/useClickOutside.ts new file mode 100644 index 00000000..828ce7e3 --- /dev/null +++ b/FrontEnd/src/components/hooks/useClickOutside.ts @@ -0,0 +1,38 @@ +import { useRef, useEffect } from "react"; + +export default function useClickOutside( + element: HTMLElement | null | undefined, + onClickOutside: () => void, + nextTick?: boolean, +): void { + const onClickOutsideRef = useRef<() => void>(onClickOutside); + + useEffect(() => { + onClickOutsideRef.current = onClickOutside; + }, [onClickOutside]); + + useEffect(() => { + if (element != null) { + const handler = (event: MouseEvent): void => { + let e: HTMLElement | null = event.target as HTMLElement; + while (e) { + if (e == element) { + return; + } + e = e.parentElement; + } + onClickOutsideRef.current(); + }; + if (nextTick) { + setTimeout(() => { + document.addEventListener("click", handler); + }); + } else { + document.addEventListener("click", handler); + } + return () => { + document.removeEventListener("click", handler); + }; + } + }, [element, nextTick]); +} diff --git a/FrontEnd/src/components/hooks/useScrollToBottom.ts b/FrontEnd/src/components/hooks/useScrollToBottom.ts new file mode 100644 index 00000000..79fcda16 --- /dev/null +++ b/FrontEnd/src/components/hooks/useScrollToBottom.ts @@ -0,0 +1,44 @@ +import { useRef, useEffect } from "react"; +import { fromEvent, filter, throttleTime } from "rxjs"; + +function useScrollToBottom( + handler: () => void, + enable = true, + option = { + maxOffset: 5, + throttle: 1000, + }, +): void { + const handlerRef = useRef<(() => void) | null>(null); + + useEffect(() => { + handlerRef.current = handler; + + return () => { + handlerRef.current = null; + }; + }, [handler]); + + useEffect(() => { + const subscription = fromEvent(window, "scroll") + .pipe( + filter( + () => + window.scrollY >= + document.body.scrollHeight - window.innerHeight - option.maxOffset, + ), + throttleTime(option.throttle), + ) + .subscribe(() => { + if (enable) { + handlerRef.current?.(); + } + }); + + return () => { + subscription.unsubscribe(); + }; + }, [enable, option.maxOffset, option.throttle]); +} + +export default useScrollToBottom; diff --git a/FrontEnd/src/components/input/InputGroup.tsx b/FrontEnd/src/components/input/InputGroup.tsx index 4f487344..47a43b38 100644 --- a/FrontEnd/src/components/input/InputGroup.tsx +++ b/FrontEnd/src/components/input/InputGroup.tsx @@ -72,12 +72,9 @@ export type InputDirtyDict = Record; // use never so you don't have to cast everywhere export type InputConfirmValueDict = Record; -export type GeneralInputErrorDict = - | { - [key: string]: Text | null | undefined; - } - | null - | undefined; +export type GeneralInputErrorDict = { + [key: string]: Text | null | undefined; +}; type MakeInputInfo = Omit; @@ -87,8 +84,9 @@ export type InputInfo = { export type Validator = ( values: InputValueDict, + errors: GeneralInputErrorDict, inputs: InputInfo[], -) => GeneralInputErrorDict; +) => void; export type InputScheme = { inputs: InputInfo[]; @@ -157,7 +155,9 @@ function validate( values: InputValueDict, inputs: InputInfo[], ): InputErrorDict { - return cleanObject(validator?.(values, inputs) ?? {}); + const errors: GeneralInputErrorDict = {}; + validator?.(values, errors, inputs); + return cleanObject(errors); } export function useInputs(options: { init: Initializer }): { diff --git a/FrontEnd/src/components/menu/Menu.tsx b/FrontEnd/src/components/menu/Menu.tsx index e8099c76..c01c6cfb 100644 --- a/FrontEnd/src/components/menu/Menu.tsx +++ b/FrontEnd/src/components/menu/Menu.tsx @@ -2,9 +2,9 @@ import { CSSProperties } from "react"; import classNames from "classnames"; import { useC, Text, ThemeColor } from "../common"; +import Icon from "../Icon"; import "./Menu.css"; -import Icon from "../Icon"; export type MenuItem = | { diff --git a/FrontEnd/src/components/menu/PopupMenu.tsx b/FrontEnd/src/components/menu/PopupMenu.tsx index 23a67f79..9d90799d 100644 --- a/FrontEnd/src/components/menu/PopupMenu.tsx +++ b/FrontEnd/src/components/menu/PopupMenu.tsx @@ -3,11 +3,9 @@ import classNames from "classnames"; import { createPortal } from "react-dom"; import { usePopper } from "react-popper"; -import { useClickOutside } from "~src/utilities/hooks"; - -import Menu, { MenuItems } from "./Menu"; - import { ThemeColor } from "../common"; +import { useClickOutside } from "../hooks"; +import Menu, { MenuItems } from "./Menu"; import "./PopupMenu.css"; diff --git a/FrontEnd/src/pages/login/index.tsx b/FrontEnd/src/pages/login/index.tsx index 582ebd0f..39ea3831 100644 --- a/FrontEnd/src/pages/login/index.tsx +++ b/FrontEnd/src/pages/login/index.tsx @@ -6,11 +6,7 @@ import { useUser, userService } from "~src/services/user"; import { useC } from "~src/components/common"; import LoadingButton from "~src/components/button/LoadingButton"; -import { - InputErrorDict, - InputGroup, - useInputs, -} from "~src/components/input/InputGroup"; +import { InputGroup, useInputs } from "~src/components/input/InputGroup"; import Page from "~src/components/Page"; import "./index.css"; @@ -47,15 +43,13 @@ export default function LoginPage() { label: "user.rememberMe", }, ], - validator: ({ username, password }) => { - const result: InputErrorDict = {}; + validator: ({ username, password }, errors) => { if (username === "") { - result["username"] = "login.emptyUsername"; + errors["username"] = "login.emptyUsername"; } if (password === "") { - result["password"] = "login.emptyPassword"; + errors["password"] = "login.emptyPassword"; } - return result; }, }, dataInit: {}, diff --git a/FrontEnd/src/pages/register/index.tsx b/FrontEnd/src/pages/register/index.tsx index 9e478612..fa25c2c2 100644 --- a/FrontEnd/src/pages/register/index.tsx +++ b/FrontEnd/src/pages/register/index.tsx @@ -7,11 +7,7 @@ import { getHttpTokenClient } from "~src/http/token"; import { userService, useUser } from "~src/services/user"; import { LoadingButton } from "~src/components/button"; -import { - useInputs, - InputErrorDict, - InputGroup, -} from "~src/components/input/InputGroup"; +import { useInputs, InputGroup } from "~src/components/input/InputGroup"; import "./index.css"; @@ -51,26 +47,22 @@ export default function RegisterPage() { label: "register.registerCode", }, ], - validator: ({ - username, - password, - confirmPassword, - registerCode, - }) => { - const result: InputErrorDict = {}; + validator: ( + { username, password, confirmPassword, registerCode }, + errors, + ) => { if (username === "") { - result["username"] = "register.error.usernameEmpty"; + errors["username"] = "register.error.usernameEmpty"; } if (password === "") { - result["password"] = "register.error.passwordEmpty"; + errors["password"] = "register.error.passwordEmpty"; } if (confirmPassword !== password) { - result["confirmPassword"] = "register.error.confirmPasswordWrong"; + errors["confirmPassword"] = "register.error.confirmPasswordWrong"; } if (registerCode === "") { - result["registerCode"] = "register.error.registerCodeEmpty"; + errors["registerCode"] = "register.error.registerCodeEmpty"; } - return result; }, }, dataInit: {}, diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx index c34bcf4f..011c5059 100644 --- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx @@ -11,9 +11,8 @@ import ImageCropper, { applyClipToImage, } from "~src/components/ImageCropper"; import BlobImage from "~src/components/BlobImage"; -import ButtonRowV2 from "~src/components/button/ButtonRowV2"; -import Dialog from "~src/components/dialog/Dialog"; -import DialogContainer from "~src/components/dialog/DialogContainer"; +import { ButtonRowV2 } from "~src/components/button"; +import { Dialog, DialogContainer } from "~src/components/dialog"; import "./ChangeAvatarDialog.css"; diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx index bfcea92d..946b9fbe 100644 --- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx @@ -3,9 +3,7 @@ import { useNavigate } from "react-router-dom"; import { userService } from "~src/services/user"; -import OperationDialog, { - InputErrorDict, -} from "~src/components/dialog/OperationDialog"; +import { OperationDialog } from "~src/components/dialog"; interface ChangePasswordDialogProps { open: boolean; @@ -47,21 +45,22 @@ export function ChangePasswordDialog(props: ChangePasswordDialogProps) { password: true, }, ], - validator: ({ oldPassword, newPassword, retypedNewPassword }) => { - const result: InputErrorDict = {}; + validator: ( + { oldPassword, newPassword, retypedNewPassword }, + errors, + ) => { if (oldPassword === "") { - result["oldPassword"] = + errors["oldPassword"] = "settings.dialogChangePassword.errorEmptyOldPassword"; } if (newPassword === "") { - result["newPassword"] = + errors["newPassword"] = "settings.dialogChangePassword.errorEmptyNewPassword"; } if (retypedNewPassword !== newPassword) { - result["retypedNewPassword"] = + errors["retypedNewPassword"] = "settings.dialogChangePassword.errorRetypeNotMatch"; } - return result; }, }} onProcess={async ({ oldPassword, newPassword }) => { diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx index 67416a08..918a77b5 100644 --- a/FrontEnd/src/pages/setting/index.tsx +++ b/FrontEnd/src/pages/setting/index.tsx @@ -4,25 +4,26 @@ import { ReactNode, ComponentPropsWithoutRef, } from "react"; -import { useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; // For change language. import { useNavigate } from "react-router-dom"; import classNames from "classnames"; -import { useC, Text } from "~src/common"; import { useUser, userService } from "~src/services/user"; import { getHttpUserClient } from "~src/http/user"; +import { pushAlert } from "~src/services/alert"; + +import { useC, Text } from "~src/common"; -import { useDialog } from "~src/components/dialog"; -import ConfirmDialog from "~src/components/dialog/ConfirmDialog"; +import { useDialog, ConfirmDialog } from "~src/components/dialog"; import Card from "~src/components/Card"; import Spinner from "~src/components/Spinner"; import Page from "~src/components/Page"; + import ChangePasswordDialog from "./ChangePasswordDialog"; import ChangeAvatarDialog from "./ChangeAvatarDialog"; import ChangeNicknameDialog from "./ChangeNicknameDialog"; import "./index.css"; -import { pushAlert } from "~src/services/alert"; interface SettingSectionProps extends Omit, "title"> { diff --git a/FrontEnd/src/pages/timeline/Timeline.tsx b/FrontEnd/src/pages/timeline/Timeline.tsx index f266ec9d..caf4f502 100644 --- a/FrontEnd/src/pages/timeline/Timeline.tsx +++ b/FrontEnd/src/pages/timeline/Timeline.tsx @@ -1,6 +1,5 @@ import { useState, useEffect } from "react"; import classnames from "classnames"; -import { useScrollToBottom } from "~src/utilities/hooks"; import { HubConnectionState } from "@microsoft/signalr"; import { @@ -16,6 +15,8 @@ import { import { getTimelinePostUpdate$ } from "~src/services/timeline"; +import { useScrollToBottom } from "~src/components/hooks"; + import TimelinePostList from "./TimelinePostList"; import TimelinePostEdit from "./TimelinePostCreateView"; import TimelineCard from "./TimelineCard"; diff --git a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx index 7b7b8e8c..a7209e75 100644 --- a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx +++ b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx @@ -39,9 +39,9 @@ const TimelineDeleteDialog: React.FC = (props) => { label: "", }, ], - validator: ({ name }) => { + validator: ({ name }, errors) => { if (name !== timeline.nameV2) { - return { name: "timeline.deleteDialog.notMatch" }; + errors.name = "timeline.deleteDialog.notMatch"; } }, }} diff --git a/FrontEnd/src/pages/timeline/TimelinePostView.tsx b/FrontEnd/src/pages/timeline/TimelinePostView.tsx index 2a8c5947..6b87ef2a 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostView.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostView.tsx @@ -1,11 +1,13 @@ import { useState } from "react"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "~src/http/timeline"; +import { + getHttpTimelineClient, + HttpTimelinePostInfo, +} from "~src/http/timeline"; import { pushAlert } from "~src/services/alert"; -import { useClickOutside } from "~src/utilities/hooks"; - +import { useClickOutside } from "~src/components/hooks"; import UserAvatar from "~src/components/user/UserAvatar"; import { useDialog } from "~src/components/dialog"; import FlatButton from "~src/components/button/FlatButton"; diff --git a/FrontEnd/src/utilities/hooks.ts b/FrontEnd/src/utilities/hooks.ts deleted file mode 100644 index a59f7167..00000000 --- a/FrontEnd/src/utilities/hooks.ts +++ /dev/null @@ -1,5 +0,0 @@ -import useClickOutside from "./hooks/useClickOutside"; -import useScrollToBottom from "./hooks/useScrollToBottom"; -import { useIsSmallScreen } from "./hooks/mediaQuery"; - -export { useClickOutside, useScrollToBottom, useIsSmallScreen }; diff --git a/FrontEnd/src/utilities/hooks/mediaQuery.ts b/FrontEnd/src/utilities/hooks/mediaQuery.ts deleted file mode 100644 index ad55c3c0..00000000 --- a/FrontEnd/src/utilities/hooks/mediaQuery.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useMediaQuery } from "react-responsive"; - -export function useIsSmallScreen(): boolean { - return useMediaQuery({ maxWidth: 576 }); -} diff --git a/FrontEnd/src/utilities/hooks/useClickOutside.ts b/FrontEnd/src/utilities/hooks/useClickOutside.ts deleted file mode 100644 index 6dcbf7b3..00000000 --- a/FrontEnd/src/utilities/hooks/useClickOutside.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useRef, useEffect } from "react"; - -export default function useClickOutside( - element: HTMLElement | null | undefined, - onClickOutside: () => void, - nextTick?: boolean -): void { - const onClickOutsideRef = useRef<() => void>(onClickOutside); - - useEffect(() => { - onClickOutsideRef.current = onClickOutside; - }, [onClickOutside]); - - useEffect(() => { - if (element != null) { - const handler = (event: MouseEvent): void => { - let e: HTMLElement | null = event.target as HTMLElement; - while (e) { - if (e == element) { - return; - } - e = e.parentElement; - } - onClickOutsideRef.current(); - }; - if (nextTick) { - setTimeout(() => { - document.addEventListener("click", handler); - }); - } else { - document.addEventListener("click", handler); - } - return () => { - document.removeEventListener("click", handler); - }; - } - }, [element, nextTick]); -} diff --git a/FrontEnd/src/utilities/hooks/useScrollToBottom.ts b/FrontEnd/src/utilities/hooks/useScrollToBottom.ts deleted file mode 100644 index 216746f4..00000000 --- a/FrontEnd/src/utilities/hooks/useScrollToBottom.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useRef, useEffect } from "react"; -import { fromEvent } from "rxjs"; -import { filter, throttleTime } from "rxjs/operators"; - -function useScrollToBottom( - handler: () => void, - enable = true, - option = { - maxOffset: 5, - throttle: 1000, - } -): void { - const handlerRef = useRef<(() => void) | null>(null); - - useEffect(() => { - handlerRef.current = handler; - - return () => { - handlerRef.current = null; - }; - }, [handler]); - - useEffect(() => { - const subscription = fromEvent(window, "scroll") - .pipe( - filter( - () => - window.scrollY >= - document.body.scrollHeight - window.innerHeight - option.maxOffset - ), - throttleTime(option.throttle) - ) - .subscribe(() => { - if (enable) { - handlerRef.current?.(); - } - }); - - return () => { - subscription.unsubscribe(); - }; - }, [enable, option.maxOffset, option.throttle]); -} - -export default useScrollToBottom; -- cgit v1.2.3 From aef14be13c3bd9e93eeea598dbfbf707ba98d448 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 14 Sep 2023 23:47:16 +0800 Subject: ... --- FrontEnd/src/components/Page.css | 8 +++ FrontEnd/src/components/Page.tsx | 2 + FrontEnd/src/components/index.css | 66 +++------------------- FrontEnd/src/components/menu/Menu.tsx | 14 ++--- FrontEnd/src/components/menu/PopupMenu.tsx | 3 +- FrontEnd/src/components/tab/TabBar.css | 18 ++---- FrontEnd/src/components/tab/TabPages.css | 3 + FrontEnd/src/components/theme.css | 4 +- FrontEnd/src/pages/timeline/edit/ImagePostEdit.css | 3 +- FrontEnd/src/pages/timeline/edit/ImagePostEdit.tsx | 10 ++-- .../src/pages/timeline/edit/MarkdownPostEdit.css | 34 ++++------- .../src/pages/timeline/edit/MarkdownPostEdit.tsx | 3 +- .../src/pages/timeline/edit/PlainTextPostEdit.css | 19 ++----- .../pages/timeline/edit/TimelinePostCreateView.css | 8 ++- .../pages/timeline/edit/TimelinePostCreateView.tsx | 2 + 15 files changed, 71 insertions(+), 126 deletions(-) create mode 100644 FrontEnd/src/components/Page.css (limited to 'FrontEnd/src/components/menu/PopupMenu.tsx') diff --git a/FrontEnd/src/components/Page.css b/FrontEnd/src/components/Page.css new file mode 100644 index 00000000..b22d83af --- /dev/null +++ b/FrontEnd/src/components/Page.css @@ -0,0 +1,8 @@ +.cru-page { + padding: var(--cru-page-padding); +} + +.cru-page-no-top-padding { + padding-top: 0; +} + diff --git a/FrontEnd/src/components/Page.tsx b/FrontEnd/src/components/Page.tsx index 86fdb2f5..8c9febcc 100644 --- a/FrontEnd/src/components/Page.tsx +++ b/FrontEnd/src/components/Page.tsx @@ -1,6 +1,8 @@ import { ComponentPropsWithoutRef, Ref } from "react"; import classNames from "classnames"; +import "./Page.css"; + interface PageProps extends ComponentPropsWithoutRef<"div"> { noTopPadding?: boolean; pageRef?: Ref; diff --git a/FrontEnd/src/components/index.css b/FrontEnd/src/components/index.css index 32e762cb..de156e0e 100644 --- a/FrontEnd/src/components/index.css +++ b/FrontEnd/src/components/index.css @@ -13,69 +13,17 @@ body { line-height: 1.2; } -.cru-page { - padding: var(--cru-page-padding); +textarea { + transition: border-color 0.3s; + border-color: var(--cru-text-minor-color); } -.cru-page-no-top-padding { - padding-top: 0; +textarea:hover { + border-color: var(--cru-clickable-primary-hover-color); } -.cru-text-center { - text-align: center; -} - -.cru-text-end { - text-align: end; -} - -.cru-float-left { - float: left; -} - -.cru-float-right { - float: right; -} - -.cru-align-text-bottom { - vertical-align: text-bottom; -} - -.cru-align-middle { - vertical-align: middle; -} - -.cru-clearfix::after { - clear: both; -} - -.cru-fill-parent { - width: 100%; - height: 100%; -} - -.cru-avatar { - width: 60px; - height: 60px; -} - -.cru-avatar.large { - width: 100px; - height: 100px; -} - -.cru-avatar.small { - width: 40px; - height: 40px; -} - -.cru-round { - border-radius: 50%; -} - -.cru-tab-pages-action-area { - display: flex; - align-items: center; +textarea:focus { + border-color: var(--cru-clickable-primary-normal-color); } .alert-container { diff --git a/FrontEnd/src/components/menu/Menu.tsx b/FrontEnd/src/components/menu/Menu.tsx index c01c6cfb..1a196a69 100644 --- a/FrontEnd/src/components/menu/Menu.tsx +++ b/FrontEnd/src/components/menu/Menu.tsx @@ -1,4 +1,4 @@ -import { CSSProperties } from "react"; +import { MouseEvent, CSSProperties } from "react"; import classNames from "classnames"; import { useC, Text, ThemeColor } from "../common"; @@ -15,21 +15,21 @@ export type MenuItem = text: Text; icon?: string; color?: ThemeColor; - onClick: () => void; + onClick?: (e: MouseEvent) => void; }; export type MenuItems = MenuItem[]; export type MenuProps = { items: MenuItems; - onItemClicked?: () => void; + onItemClick?: (e: MouseEvent) => void; className?: string; style?: CSSProperties; }; export default function Menu({ items, - onItemClicked, + onItemClick, className, style, }: MenuProps) { @@ -46,9 +46,9 @@ export default function Menu({