diff options
Diffstat (limited to 'FrontEnd/src/views/common')
31 files changed, 492 insertions, 428 deletions
diff --git a/FrontEnd/src/views/common/AppBar.css b/FrontEnd/src/views/common/AppBar.css new file mode 100644 index 00000000..16572817 --- /dev/null +++ b/FrontEnd/src/views/common/AppBar.css @@ -0,0 +1,95 @@ +.app-bar {
+ display: flex;
+ align-items: center;
+ height: 56px;
+ position: fixed;
+ z-index: 1030;
+ top: 0;
+ left: 0;
+ right: 0;
+ background-color: var(--tl-primary-color);
+ transition: background-color 1s;
+}
+
+.app-bar .cru-avatar {
+ background-color: white;
+}
+
+.app-bar a {
+ color: var(--tl-text-on-primary-r1-color);
+ text-decoration: none;
+ margin: 0 1em;
+ transition: color 1s;
+}
+.app-bar a:hover {
+ color: var(--tl-text-on-primary-color);
+}
+.app-bar a.active {
+ color: var(--tl-text-on-primary-color);
+}
+
+.app-bar-brand {
+ display: flex;
+ align-items: center;
+}
+
+.app-bar-brand-icon {
+ height: 2em;
+}
+
+.app-bar-main-area {
+ display: flex;
+ flex-grow: 1;
+}
+
+.app-bar-link-area {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+}
+
+.app-bar-user-area {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ margin-left: auto;
+}
+
+.small-screen .app-bar-main-area {
+ position: absolute;
+ top: 56px;
+ left: 0;
+ right: 0;
+ transform-origin: top;
+ transition: transform 0.6s, background-color 1s;
+ background-color: var(--tl-primary-color);
+ flex-direction: column;
+}
+.small-screen .app-bar-main-area.app-bar-collapse {
+ transform: scale(1, 0);
+}
+.small-screen .app-bar-main-area a {
+ text-align: left;
+ padding: 0.5em 0.5em;
+}
+.small-screen .app-bar-link-area {
+ flex-direction: column;
+ align-items: stretch;
+}
+.small-screen .app-bar-user-area {
+ flex-direction: column;
+ align-items: stretch;
+ margin-left: unset;
+}
+.small-screen .app-bar-avatar {
+ align-self: flex-end;
+}
+
+.app-bar-toggler {
+ margin-left: auto;
+ font-size: 2em;
+ margin-right: 1em;
+ color: var(--tl-text-on-primary-color);
+ cursor: pointer;
+ user-select: none;
+}
diff --git a/FrontEnd/src/views/common/AppBar.tsx b/FrontEnd/src/views/common/AppBar.tsx index ebc8bf0c..5d62a88d 100644 --- a/FrontEnd/src/views/common/AppBar.tsx +++ b/FrontEnd/src/views/common/AppBar.tsx @@ -9,7 +9,7 @@ import { useUser } from "@/services/user"; import TimelineLogo from "./TimelineLogo"; import UserAvatar from "./user/UserAvatar"; -import "./index.css"; +import "./AppBar.css"; const AppBar: React.FC = (_) => { const { t } = useTranslation(); @@ -68,7 +68,7 @@ const AppBar: React.FC = (_) => { "/", <UserAvatar username={user.username} - className="avatar small rounded-circle bg-white cursor-pointer ml-auto" + className="cru-avatar small cru-round cursor-pointer ml-auto" />, "app-bar-avatar" ) diff --git a/FrontEnd/src/views/common/ConfirmDialog.tsx b/FrontEnd/src/views/common/ConfirmDialog.tsx deleted file mode 100644 index 72940c51..00000000 --- a/FrontEnd/src/views/common/ConfirmDialog.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { convertI18nText, I18nText } from "@/common"; -import React from "react"; -import { Modal, Button } from "react-bootstrap"; -import { useTranslation } from "react-i18next"; - -const ConfirmDialog: React.FC<{ - onClose: () => void; - onConfirm: () => void; - title: I18nText; - body: I18nText; -}> = ({ onClose, onConfirm, title, body }) => { - const { t } = useTranslation(); - - return ( - <Modal onHide={onClose} show centered> - <Modal.Header> - <Modal.Title className="text-danger"> - {convertI18nText(title, t)} - </Modal.Title> - </Modal.Header> - <Modal.Body>{convertI18nText(body, t)}</Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={onClose}> - {t("operationDialog.cancel")} - </Button> - <Button - variant="danger" - onClick={() => { - onConfirm(); - onClose(); - }} - > - {t("operationDialog.confirm")} - </Button> - </Modal.Footer> - </Modal> - ); -}; - -export default ConfirmDialog; diff --git a/FrontEnd/src/views/common/ImageCropper.css b/FrontEnd/src/views/common/ImageCropper.css new file mode 100644 index 00000000..2c4d0a8c --- /dev/null +++ b/FrontEnd/src/views/common/ImageCropper.css @@ -0,0 +1,38 @@ +.image-cropper-container {
+ position: relative;
+ box-sizing: border-box;
+ user-select: none;
+}
+
+.image-cropper-container img {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.image-cropper-mask-container {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ overflow: hidden;
+}
+
+.image-cropper-mask {
+ position: absolute;
+ box-shadow: 0 0 0 10000px rgba(255, 255, 255, 0.8);
+ touch-action: none;
+}
+
+.image-cropper-handler {
+ position: absolute;
+ width: 26px;
+ height: 26px;
+ border: black solid 2px;
+ border-radius: 50%;
+ background: white;
+ touch-action: none;
+}
diff --git a/FrontEnd/src/views/common/ImageCropper.tsx b/FrontEnd/src/views/common/ImageCropper.tsx index 2ef5b7ed..be44200a 100644 --- a/FrontEnd/src/views/common/ImageCropper.tsx +++ b/FrontEnd/src/views/common/ImageCropper.tsx @@ -3,6 +3,8 @@ import classnames from "classnames"; import { UiLogicError } from "@/common"; +import "./ImageCropper.css"; + export interface Clip { left: number; top: number; diff --git a/FrontEnd/src/views/common/LoadingButton.tsx b/FrontEnd/src/views/common/LoadingButton.tsx deleted file mode 100644 index cd9f1adc..00000000 --- a/FrontEnd/src/views/common/LoadingButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; -import { Button, ButtonProps, Spinner } from "react-bootstrap"; - -const LoadingButton: React.FC<{ loading?: boolean } & ButtonProps> = ({ - loading, - variant, - disabled, - ...otherProps -}) => { - return ( - <Button - variant={variant != null ? `outline-${variant}` : "outline-primary"} - disabled={disabled || loading} - {...otherProps} - > - {otherProps.children} - {loading ? ( - <Spinner - className="ms-1" - variant={variant} - animation="grow" - size="sm" - /> - ) : null} - </Button> - ); -}; - -export default LoadingButton; diff --git a/FrontEnd/src/views/common/LoadingPage.tsx b/FrontEnd/src/views/common/LoadingPage.tsx index 590fafa0..8c1e681a 100644 --- a/FrontEnd/src/views/common/LoadingPage.tsx +++ b/FrontEnd/src/views/common/LoadingPage.tsx @@ -1,10 +1,11 @@ import React from "react"; -import { Spinner } from "react-bootstrap"; + +import Spinner from "./Spinner"; const LoadingPage: React.FC = () => { return ( <div className="position-fixed w-100 h-100 d-flex justify-content-center align-items-center"> - <Spinner variant="primary" animation="border" /> + <Spinner /> </div> ); }; diff --git a/FrontEnd/src/views/common/Menu.tsx b/FrontEnd/src/views/common/Menu.tsx index ae73a331..a5d2ec2c 100644 --- a/FrontEnd/src/views/common/Menu.tsx +++ b/FrontEnd/src/views/common/Menu.tsx @@ -1,9 +1,9 @@ import React from "react"; import classnames from "classnames"; -import { OverlayTrigger, OverlayTriggerProps, Popover } from "react-bootstrap"; import { useTranslation } from "react-i18next"; -import { BootstrapThemeColor, convertI18nText, I18nText } from "@/common"; +import { convertI18nText, I18nText } from "@/common"; +import { PaletteColorType } from "@/palette"; export type MenuItem = | { @@ -13,7 +13,7 @@ export type MenuItem = type: "button"; text: I18nText; iconClassName?: string; - color?: BootstrapThemeColor; + color?: PaletteColorType; onClick: () => void; }; @@ -67,26 +67,14 @@ export default Menu; export interface PopupMenuProps { items: MenuItems; - children: OverlayTriggerProps["children"]; + children: React.ReactElement; } export const PopupMenu: React.FC<PopupMenuProps> = ({ items, children }) => { const [show, setShow] = React.useState<boolean>(false); const toggle = (): void => setShow(!show); - return ( - <OverlayTrigger - trigger="click" - rootClose - overlay={ - <Popover id="menu-popover"> - <Menu items={items} onItemClicked={() => setShow(false)} /> - </Popover> - } - show={show} - onToggle={toggle} - > - {children} - </OverlayTrigger> - ); + // TODO: + + return <Menu items={items} onItemClicked={() => setShow(false)} />; }; diff --git a/FrontEnd/src/views/common/SearchInput.css b/FrontEnd/src/views/common/SearchInput.css new file mode 100644 index 00000000..2943b3a2 --- /dev/null +++ b/FrontEnd/src/views/common/SearchInput.css @@ -0,0 +1,4 @@ +.cru-search-input {
+ display: flex;
+ flex-wrap: wrap;
+}
diff --git a/FrontEnd/src/views/common/SearchInput.tsx b/FrontEnd/src/views/common/SearchInput.tsx index ccb6dad6..da3f1c19 100644 --- a/FrontEnd/src/views/common/SearchInput.tsx +++ b/FrontEnd/src/views/common/SearchInput.tsx @@ -1,7 +1,10 @@ import React, { useCallback } from "react"; import classnames from "classnames"; import { useTranslation } from "react-i18next"; -import { Spinner, Form, Button } from "react-bootstrap"; + +import LoadingButton from "./button/LoadingButton"; + +import "./SearchInput.css"; export interface SearchInputProps { value: string; @@ -38,14 +41,15 @@ const SearchInput: React.FC<SearchInputProps> = (props) => { ); return ( - <Form + <div className={classnames( "cru-search-input", alwaysOneline ? "flex-nowrap" : "flex-sm-nowrap", props.className )} > - <Form.Control + <input + type="text" className="me-sm-2 flex-grow-1" value={props.value} onChange={onInputChange} @@ -63,15 +67,11 @@ const SearchInput: React.FC<SearchInputProps> = (props) => { "flex-shrink-0" )} > - {props.loading ? ( - <Spinner variant="primary" animation="border" /> - ) : ( - <Button variant="outline-primary" onClick={props.onButtonClick}> - {props.buttonText ?? t("search")} - </Button> - )} + <LoadingButton loading={props.loading} onClick={props.onButtonClick}> + {props.buttonText ?? t("search")} + </LoadingButton> </div> - </Form> + </div> ); }; diff --git a/FrontEnd/src/views/common/Spinner.tsx b/FrontEnd/src/views/common/Spinner.tsx new file mode 100644 index 00000000..783c9be2 --- /dev/null +++ b/FrontEnd/src/views/common/Spinner.tsx @@ -0,0 +1,13 @@ +import { PaletteColorType } from "@/palette"; +import React from "react"; + +export interface SpinnerProps { + size?: "sm" | "md" | "lg" | number; + color?: PaletteColorType; +} + +export default function Spinner( + props: SpinnerProps +): React.ReactElement | null { + return <span />; +} diff --git a/FrontEnd/src/views/common/ToggleIconButton.tsx b/FrontEnd/src/views/common/ToggleIconButton.tsx deleted file mode 100644 index c4d2d132..00000000 --- a/FrontEnd/src/views/common/ToggleIconButton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import classnames from "classnames"; - -export interface ToggleIconButtonProps - extends React.HTMLAttributes<HTMLElement> { - state: boolean; - trueIconClassName: string; - falseIconClassName: string; -} - -const ToggleIconButton: React.FC<ToggleIconButtonProps> = ({ - state, - className, - trueIconClassName, - falseIconClassName, - ...otherProps -}) => { - return ( - <i - className={classnames( - state ? trueIconClassName : falseIconClassName, - "icon-button", - className - )} - {...otherProps} - /> - ); -}; - -export default ToggleIconButton; diff --git a/FrontEnd/src/views/common/alert/AlertHost.tsx b/FrontEnd/src/views/common/alert/AlertHost.tsx index 949be7ed..21b9882d 100644 --- a/FrontEnd/src/views/common/alert/AlertHost.tsx +++ b/FrontEnd/src/views/common/alert/AlertHost.tsx @@ -1,7 +1,6 @@ import React from "react"; import without from "lodash/without"; import { useTranslation } from "react-i18next"; -import { Alert } from "react-bootstrap"; import { alertService, @@ -52,13 +51,7 @@ export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => { }; return ( - <Alert - className="m-3" - variant={alert.type ?? "primary"} - onClick={cancelTimer} - onClose={close} - dismissible - > + <div className="m-3" onClick={cancelTimer}> {(() => { const { message } = alert; if (typeof message === "function") { @@ -66,7 +59,7 @@ export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => { return <Message />; } else return convertI18nText(message, t); })()} - </Alert> + </div> ); }; diff --git a/FrontEnd/src/views/common/alert/alert.css b/FrontEnd/src/views/common/alert/alert.css new file mode 100644 index 00000000..12ce294e --- /dev/null +++ b/FrontEnd/src/views/common/alert/alert.css @@ -0,0 +1,4 @@ +.alert-container {
+ position: fixed;
+ z-index: 1040;
+}
diff --git a/FrontEnd/src/views/common/alert/alert.sass b/FrontEnd/src/views/common/alert/alert.sass deleted file mode 100644 index c3560b87..00000000 --- a/FrontEnd/src/views/common/alert/alert.sass +++ /dev/null @@ -1,15 +0,0 @@ -.alert-container
- position: fixed
- z-index: $zindex-popover
-
-@include media-breakpoint-up(sm)
- .alert-container
- bottom: 0
- right: 0
-
-@include media-breakpoint-down(sm)
- .alert-container
- bottom: 0
- right: 0
- left: 0
- text-align: center
diff --git a/FrontEnd/src/views/common/button/Button.css b/FrontEnd/src/views/common/button/Button.css new file mode 100644 index 00000000..b6df222f --- /dev/null +++ b/FrontEnd/src/views/common/button/Button.css @@ -0,0 +1,72 @@ +.cru-button {
+ color: white;
+ cursor: pointer;
+ padding: 0.2em 0.5em;
+ border-radius: 0.2em;
+ border: none;
+ transition: all 0.6s;
+}
+
+.cru-button.primary {
+ background-color: var(--tl-primary-color);
+}
+
+.cru-button.primary:hover {
+ background-color: var(--tl-primary-f1-color);
+}
+
+.cru-button.primary:active {
+ background-color: var(--tl-primary-f2-color);
+}
+
+.cru-button.primary.disabled {
+ background-color: var(--tl-primary-f3-color);
+}
+
+.cru-button.secondary {
+ background-color: var(--tl-secondary-color);
+}
+
+.cru-button.secondary:hover {
+ background-color: var(--tl-secondary-f1-color);
+}
+
+.cru-button.secondary:active {
+ background-color: var(--tl-secondary-f2-color);
+}
+
+.cru-button.secondary.disabled {
+ background-color: var(--tl-secondary-f3-color);
+}
+
+.cru-button.success {
+ background-color: var(--tl-success-color);
+}
+
+.cru-button.success:hover {
+ background-color: var(--tl-success-f1-color);
+}
+
+.cru-button.success:active {
+ background-color: var(--tl-success-f2-color);
+}
+
+.cru-button.success.disabled {
+ background-color: var(--tl-success-f3-color);
+}
+
+.cru-button.danger {
+ background-color: var(--tl-danger-color);
+}
+
+.cru-button.danger:hover {
+ background-color: var(--tl-danger-f1-color);
+}
+
+.cru-button.danger:active {
+ background-color: var(--tl-danger-f2-color);
+}
+
+.cru-button.danger.disabled {
+ background-color: var(--tl-danger-f3-color);
+}
diff --git a/FrontEnd/src/views/common/button/Button.tsx b/FrontEnd/src/views/common/button/Button.tsx new file mode 100644 index 00000000..a39ef8a7 --- /dev/null +++ b/FrontEnd/src/views/common/button/Button.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; + +import { calculateProps, CommonButtonProps } from "./common"; + +import "./Button.css"; + +function _Button( + props: CommonButtonProps & { + outline?: boolean; + customButtonClassName?: string; + }, + ref: React.ForwardedRef<HTMLButtonElement> +): React.ReactElement | null { + const { t } = useTranslation(); + + const { customButtonClassName, outline, ...otherProps } = props; + + const { newProps, children } = calculateProps( + otherProps, + customButtonClassName ?? "cru-button" + (outline ? " outline" : ""), + t + ); + + return ( + <button ref={ref} {...newProps}> + {children} + </button> + ); +} + +const Button = React.forwardRef(_Button); +export default Button; diff --git a/FrontEnd/src/views/common/button/FlatButton.css b/FrontEnd/src/views/common/button/FlatButton.css index 522563b9..c3c0dbb3 100644 --- a/FrontEnd/src/views/common/button/FlatButton.css +++ b/FrontEnd/src/views/common/button/FlatButton.css @@ -20,7 +20,7 @@ }
.cru-flat-button.primary.disabled {
- color: var(--tl-primary-lighter-color);
+ color: var(--tl-primary-l1-color);
}
.cru-flat-button.secondary {
@@ -28,7 +28,7 @@ }
.cru-flat-button.secondary.disabled {
- color: var(--tl-secondary-lighter-color);
+ color: var(--tl-secondary-l1-color);
}
.cru-flat-button.success {
@@ -36,7 +36,7 @@ }
.cru-flat-button.success.disabled {
- color: var(--tl-success-lighter-color);
+ color: var(--tl-success-l1-color);
}
.cru-flat-button.danger {
@@ -44,5 +44,5 @@ }
.cru-flat-button.danger.disabled {
- color: var(--tl-danger-ligher-color);
+ color: var(--tl-danger-l1-color);
}
diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx index 6351971a..266ea908 100644 --- a/FrontEnd/src/views/common/button/FlatButton.tsx +++ b/FrontEnd/src/views/common/button/FlatButton.tsx @@ -1,39 +1,16 @@ import React from "react"; -import { useTranslation } from "react-i18next"; -import classNames from "classnames"; -import { convertI18nText, I18nText } from "@/common"; -import { PaletteColorType } from "@/palette"; +import { CommonButtonProps } from "./common"; +import Button from "./Button"; import "./FlatButton.css"; function _FlatButton( - { - text, - color, - onClick, - className, - style, - }: { - text: I18nText; - color?: PaletteColorType; - onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void; - className?: string; - style?: React.CSSProperties; - }, + props: CommonButtonProps, ref: React.ForwardedRef<HTMLButtonElement> ): React.ReactElement | null { - const { t } = useTranslation(); - return ( - <button - ref={ref} - className={classNames("cru-flat-button", color ?? "primary", className)} - onClick={onClick} - style={style} - > - {convertI18nText(text, t)} - </button> + <Button ref={ref} customButtonClassName="cru-flat-button" {...props} /> ); } diff --git a/FrontEnd/src/views/common/button/LoadingButton.tsx b/FrontEnd/src/views/common/button/LoadingButton.tsx new file mode 100644 index 00000000..aee83aa2 --- /dev/null +++ b/FrontEnd/src/views/common/button/LoadingButton.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +import { CommonButtonProps } from "./common"; +import Button from "./Button"; +import Spinner from "../Spinner"; + +const LoadingButton: React.FC<{ loading?: boolean } & CommonButtonProps> = ({ + loading, + disabled, + color, + ...otherProps +}) => { + return ( + <Button color={color} disabled={disabled || loading} {...otherProps}> + {otherProps.children} + {loading ? <Spinner color={color} /> : null} + </Button> + ); +}; + +export default LoadingButton; diff --git a/FrontEnd/src/views/common/button/TextButton.css b/FrontEnd/src/views/common/button/TextButton.css index dc5abaaa..d267fb38 100644 --- a/FrontEnd/src/views/common/button/TextButton.css +++ b/FrontEnd/src/views/common/button/TextButton.css @@ -8,7 +8,7 @@ }
.cru-text-button.primary:hover {
- color: var(--tl-primary-lighter-color);
+ color: var(--tl-primary-l1-color);
}
.cru-text-button.secondary {
@@ -16,7 +16,7 @@ }
.cru-text-button.secondary:hover {
- color: var(--tl-secondary-lighter-color);
+ color: var(--tl-secondary-l1-color);
}
.cru-text-button.success {
@@ -24,7 +24,7 @@ }
.cru-text-button.success:hover {
- color: var(--tl-success-lighter-color);
+ color: var(--tl-success-l1-color);
}
.cru-text-button.danger {
@@ -32,5 +32,5 @@ }
.cru-text-button.danger:hover {
- color: var(--tl-danger-lighter-color);
+ color: var(--tl-danger-l1-color);
}
diff --git a/FrontEnd/src/views/common/button/TextButton.tsx b/FrontEnd/src/views/common/button/TextButton.tsx index 1a2bac94..1d8e7a4b 100644 --- a/FrontEnd/src/views/common/button/TextButton.tsx +++ b/FrontEnd/src/views/common/button/TextButton.tsx @@ -1,39 +1,16 @@ import React from "react"; -import { useTranslation } from "react-i18next"; -import classNames from "classnames"; -import { convertI18nText, I18nText } from "@/common"; -import { PaletteColorType } from "@/palette"; +import { CommonButtonProps } from "./common"; +import Button from "./Button"; import "./TextButton.css"; function _TextButton( - { - text, - color, - onClick, - className, - style, - }: { - text: I18nText; - color?: PaletteColorType; - onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void; - className?: string; - style?: React.CSSProperties; - }, + props: CommonButtonProps, ref: React.ForwardedRef<HTMLButtonElement> ): React.ReactElement | null { - const { t } = useTranslation(); - return ( - <button - ref={ref} - className={classNames("cru-text-button", color ?? "primary", className)} - onClick={onClick} - style={style} - > - {convertI18nText(text, t)} - </button> + <Button ref={ref} customButtonClassName="cru-flat-button" {...props} /> ); } diff --git a/FrontEnd/src/views/common/button/common.ts b/FrontEnd/src/views/common/button/common.ts new file mode 100644 index 00000000..a9db959e --- /dev/null +++ b/FrontEnd/src/views/common/button/common.ts @@ -0,0 +1,31 @@ +import React from "react"; +import classNames from "classnames"; +import { TFunction } from "i18next"; + +import { convertI18nText, I18nText } from "@/common"; +import { PaletteColorType } from "@/palette"; + +export type CommonButtonProps = { + text?: I18nText; + color?: PaletteColorType; +} & React.ButtonHTMLAttributes<HTMLButtonElement>; + +export function calculateProps( + props: CommonButtonProps, + buttonClassName: string, + t: TFunction +): { + children: React.ReactNode; + newProps: React.ButtonHTMLAttributes<HTMLButtonElement>; +} { + const { text, color, className, children, ...otherProps } = props; + const newProps = { + className: classNames(buttonClassName, color ?? "primary", className), + ...otherProps, + }; + + return { + children: text != null ? convertI18nText(text, t) : children, + newProps: newProps, + }; +} diff --git a/FrontEnd/src/views/common/dailog/ConfirmDialog.tsx b/FrontEnd/src/views/common/dailog/ConfirmDialog.tsx new file mode 100644 index 00000000..1ad52350 --- /dev/null +++ b/FrontEnd/src/views/common/dailog/ConfirmDialog.tsx @@ -0,0 +1,40 @@ +import { convertI18nText, I18nText } from "@/common"; +import React from "react"; +import { useTranslation } from "react-i18next"; + +import Button from "../button/Button"; +import Dialog from "./Dialog"; + +const ConfirmDialog: React.FC<{ + open?: boolean; + onClose: () => void; + onConfirm: () => void; + title: I18nText; + body: I18nText; +}> = ({ open, onClose, onConfirm, title, body }) => { + const { t } = useTranslation(); + + return ( + <Dialog onClose={onClose} open={open}> + <h3 className="text-danger">{convertI18nText(title, t)}</h3> + <p>{convertI18nText(body, t)}</p> + <div> + <Button + text="operationDialog.cancel" + color="secondary" + onClick={onClose} + /> + <Button + text="operationDialog.confirm" + color="danger" + onClick={() => { + onConfirm(); + onClose(); + }} + /> + </div> + </Dialog> + ); +}; + +export default ConfirmDialog; diff --git a/FrontEnd/src/views/common/dailog/Dialog.css b/FrontEnd/src/views/common/dailog/Dialog.css new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/FrontEnd/src/views/common/dailog/Dialog.css diff --git a/FrontEnd/src/views/common/dailog/Dialog.tsx b/FrontEnd/src/views/common/dailog/Dialog.tsx new file mode 100644 index 00000000..5a3902c4 --- /dev/null +++ b/FrontEnd/src/views/common/dailog/Dialog.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +export interface DialogProps { + onClose: () => void; + open?: boolean; + children?: React.ReactNode; +} + +export default function Dialog(props: DialogProps): React.ReactElement | null { + const { open, onClose, children } = props; + + return <div>{children}</div>; +} diff --git a/FrontEnd/src/views/common/FullPage.tsx b/FrontEnd/src/views/common/dailog/FullPageDialog.tsx index 1b59045a..88c90bbc 100644 --- a/FrontEnd/src/views/common/FullPage.tsx +++ b/FrontEnd/src/views/common/dailog/FullPageDialog.tsx @@ -1,13 +1,13 @@ import React from "react"; import classnames from "classnames"; -export interface FullPageProps { +export interface FullPageDialogProps { show: boolean; onBack: () => void; contentContainerClassName?: string; } -const FullPage: React.FC<FullPageProps> = ({ +const FullPageDialog: React.FC<FullPageDialogProps> = ({ show, onBack, children, @@ -36,4 +36,4 @@ const FullPage: React.FC<FullPageProps> = ({ ); }; -export default FullPage; +export default FullPageDialog; diff --git a/FrontEnd/src/views/common/OperationDialog.tsx b/FrontEnd/src/views/common/dailog/OperationDialog.tsx index ac4c51b9..1e765276 100644 --- a/FrontEnd/src/views/common/OperationDialog.tsx +++ b/FrontEnd/src/views/common/dailog/OperationDialog.tsx @@ -1,12 +1,15 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Form, Button, Modal } from "react-bootstrap"; import { TwitterPicker } from "react-color"; import moment from "moment"; import { convertI18nText, I18nText, UiLogicError } from "@/common"; -import LoadingButton from "./LoadingButton"; +import { PaletteColorType } from "@/palette"; + +import Button from "../button/Button"; +import LoadingButton from "../button/LoadingButton"; +import Dialog from "./Dialog"; interface DefaultErrorPromptProps { error?: string; @@ -141,9 +144,9 @@ export interface OperationDialogProps< OperationInputInfoList extends readonly OperationDialogInput[] > { open: boolean; - close: () => void; + onClose: () => void; title: I18nText | (() => React.ReactNode); - themeColor?: "danger" | "success" | string; + themeColor?: PaletteColorType; onProcess: ( inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList> ) => Promise<TData>; @@ -204,7 +207,7 @@ const OperationDialog = < const close = (): void => { if (step.type !== "process") { - props.close(); + props.onClose(); if (step.type === "success" && props.onSuccessAndClose) { props.onSuccessAndClose(step.data); } @@ -278,7 +281,7 @@ const OperationDialog = < body = ( <> - <Modal.Body> + <div> {inputPrompt} {inputScheme.map((item, index) => { const value = values[index]; @@ -289,50 +292,42 @@ const OperationDialog = < if (item.type === "text") { return ( - <Form.Group key={index}> + <div key={index}> {item.label && ( - <Form.Label>{convertI18nText(item.label, t)}</Form.Label> + <label>{convertI18nText(item.label, t)}</label> )} - <Form.Control + <input type={item.password === true ? "password" : "text"} value={value as string} onChange={(e) => { const v = e.target.value; updateValue(index, v); }} - isInvalid={error != null} disabled={process} /> - {error != null && ( - <Form.Control.Feedback type="invalid"> - {error} - </Form.Control.Feedback> - )} - {item.helperText && ( - <Form.Text>{t(item.helperText)}</Form.Text> - )} - </Form.Group> + {error != null && <div>{error}</div>} + {item.helperText && <div>{t(item.helperText)}</div>} + </div> ); } else if (item.type === "bool") { return ( - <Form.Group key={index}> - <Form.Check<"input"> + <div key={index}> + <input type="checkbox" checked={value as boolean} onChange={(event) => { updateValue(index, event.currentTarget.checked); }} - label={convertI18nText(item.label, t)} disabled={process} /> - </Form.Group> + <label>{convertI18nText(item.label, t)}</label> + </div> ); } else if (item.type === "select") { return ( - <Form.Group key={index}> - <Form.Label>{convertI18nText(item.label, t)}</Form.Label> - <Form.Control - as="select" + <div key={index}> + <label>{convertI18nText(item.label, t)}</label> + <select value={value as string} onChange={(event) => { updateValue(index, event.target.value); @@ -347,14 +342,14 @@ const OperationDialog = < </option> ); })} - </Form.Control> - </Form.Group> + </select> + </div> ); } else if (item.type === "color") { return ( - <Form.Group key={index}> + <div key={index}> {item.canBeNull ? ( - <Form.Check<"input"> + <input type="checkbox" checked={value !== null} onChange={(event) => { @@ -364,52 +359,47 @@ const OperationDialog = < updateValue(index, null); } }} - label={convertI18nText(item.label, t)} disabled={process} /> - ) : ( - <Form.Label>{convertI18nText(item.label, t)}</Form.Label> - )} + ) : null} + <label>{convertI18nText(item.label, t)}</label> {value !== null && ( <TwitterPicker color={value as string} onChange={(result) => updateValue(index, result.hex)} /> )} - </Form.Group> + </div> ); } else if (item.type === "datetime") { return ( - <Form.Group key={index}> + <div key={index}> {item.label && ( - <Form.Label>{convertI18nText(item.label, t)}</Form.Label> + <label>{convertI18nText(item.label, t)}</label> )} - <Form.Control + <input type="datetime-local" value={value as string} onChange={(e) => { const v = e.target.value; updateValue(index, v); }} - isInvalid={error != null} disabled={process} /> - {error != null && ( - <Form.Control.Feedback type="invalid"> - {error} - </Form.Control.Feedback> - )} - </Form.Group> + {error != null && <div>{error}</div>} + </div> ); } })} - </Modal.Body> - <Modal.Footer> - <Button variant="outline-secondary" onClick={close}> - {t("operationDialog.cancel")} - </Button> + </div> + <div> + <Button + text="operationDialog.cancel" + color="secondary" + onClick={close} + /> <LoadingButton - variant={props.themeColor} + color={props.themeColor} loading={process} disabled={!canProcess} onClick={() => { @@ -421,7 +411,7 @@ const OperationDialog = < > {t("operationDialog.confirm")} </LoadingButton> - </Modal.Footer> + </div> </> ); } else { @@ -439,12 +429,10 @@ const OperationDialog = < } body = ( <> - <Modal.Body>{content}</Modal.Body> - <Modal.Footer> - <Button variant="primary" onClick={close}> - {t("operationDialog.ok")} - </Button> - </Modal.Footer> + <div>{content}</div> + <div> + <Button text="operationDialog.ok" color="primary" onClick={close} /> + </div> </> ); } @@ -455,16 +443,16 @@ const OperationDialog = < : convertI18nText(props.title, t); return ( - <Modal show={props.open} onHide={close}> - <Modal.Header + <Dialog open={props.open} onClose={close}> + <h3 className={ props.themeColor != null ? "text-" + props.themeColor : undefined } > {title} - </Modal.Header> + </h3> {body} - </Modal> + </Dialog> ); }; diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css index bfd82b58..aaaaad58 100644 --- a/FrontEnd/src/views/common/index.css +++ b/FrontEnd/src/views/common/index.css @@ -1,130 +1,20 @@ -.image-cropper-container {
- position: relative;
- box-sizing: border-box;
- user-select: none;
+.cru-avatar {
+ width: 60px;
+ height: 60px;
}
-.image-cropper-container img {
- position: absolute;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
+.cru-avatar.large {
+ width: 100px;
+ height: 100px;
}
-.image-cropper-mask-container {
- position: absolute;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
- overflow: hidden;
+.cru-avatar.small {
+ width: 40px;
+ height: 40px;
}
-.image-cropper-mask {
- position: absolute;
- box-shadow: 0 0 0 10000px rgba(255, 255, 255, 0.8);
- touch-action: none;
-}
-
-.image-cropper-handler {
- position: absolute;
- width: 26px;
- height: 26px;
- border: black solid 2px;
+.cru-round {
border-radius: 50%;
- background: white;
- touch-action: none;
-}
-
-.app-bar {
- display: flex;
- align-items: center;
- height: 56px;
- position: fixed;
- z-index: 1030;
- top: 0;
- left: 0;
- right: 0;
- background-color: var(--tl-primary-color);
- transition: background-color 1s;
-}
-.app-bar a {
- color: var(--tl-text-on-primary-inactive-color);
- text-decoration: none;
- margin: 0 1em;
-}
-.app-bar a:hover {
- color: var(--tl-text-on-primary-color);
-}
-.app-bar a.active {
- color: var(--tl-text-on-primary-color);
-}
-
-.app-bar-brand {
- display: flex;
- align-items: center;
-}
-
-.app-bar-brand-icon {
- height: 2em;
-}
-
-.app-bar-main-area {
- display: flex;
- flex-grow: 1;
-}
-
-.app-bar-link-area {
- display: flex;
- align-items: center;
- flex-shrink: 0;
-}
-
-.app-bar-user-area {
- display: flex;
- align-items: center;
- flex-shrink: 0;
- margin-left: auto;
-}
-
-.small-screen .app-bar-main-area {
- position: absolute;
- top: 56px;
- left: 0;
- right: 0;
- transform-origin: top;
- transition: transform 0.6s, background-color 1s;
- background-color: var(--tl-primary-color);
- flex-direction: column;
-}
-.small-screen .app-bar-main-area.app-bar-collapse {
- transform: scale(1, 0);
-}
-.small-screen .app-bar-main-area a {
- text-align: left;
- padding: 0.5em 0.5em;
-}
-.small-screen .app-bar-link-area {
- flex-direction: column;
- align-items: stretch;
-}
-.small-screen .app-bar-user-area {
- flex-direction: column;
- align-items: stretch;
- margin-left: unset;
-}
-.small-screen .app-bar-avatar {
- align-self: flex-end;
-}
-
-.app-bar-toggler {
- margin-left: auto;
- font-size: 2em;
- margin-right: 1em;
- color: var(--tl-text-on-primary-color);
- cursor: pointer;
- user-select: none;
}
.cru-skeleton {
@@ -247,11 +137,6 @@ align-items: center;
}
-.cru-search-input {
- display: flex;
- flex-wrap: wrap;
-}
-
.alert-container {
position: fixed;
z-index: 1070;
diff --git a/FrontEnd/src/views/common/TabPages.tsx b/FrontEnd/src/views/common/tab/TabPages.tsx index 2b1d91cb..b7a9fb36 100644 --- a/FrontEnd/src/views/common/TabPages.tsx +++ b/FrontEnd/src/views/common/tab/TabPages.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Nav } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { convertI18nText, I18nText, UiLogicError } from "@/common"; @@ -31,6 +30,8 @@ const TabPages: React.FC<TabPagesProps> = ({ pageContainerClassName, pageContainerStyle, }) => { + // TODO: + if (pages.length === 0) { throw new UiLogicError("Page list can't be empty."); } @@ -47,23 +48,6 @@ const TabPages: React.FC<TabPagesProps> = ({ return ( <div className={className} style={style}> - <Nav variant="tabs" className={navClassName} style={navStyle}> - {pages.map((page) => ( - <Nav.Item key={page.id}> - <Nav.Link - active={tab === page.id} - onClick={() => { - setTab(page.id); - }} - > - {convertI18nText(page.tabText, t)} - </Nav.Link> - </Nav.Item> - ))} - {actions != null && ( - <div className="ms-auto cru-tab-pages-action-area">{actions}</div> - )} - </Nav> <div className={pageContainerClassName} style={pageContainerStyle}> {currentPage.page} </div> diff --git a/FrontEnd/src/views/common/tab/Tabs.tsx b/FrontEnd/src/views/common/tab/Tabs.tsx new file mode 100644 index 00000000..29ebcbd8 --- /dev/null +++ b/FrontEnd/src/views/common/tab/Tabs.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import { I18nText } from "@/common"; + +export interface Tab { + name: string; + text: I18nText; + link?: string; + onClick?: () => void; +} + +export interface TabsProps { + activeTabName?: string; + tabs: Tab[]; +} + +export default function Tabs(props: TabsProps): React.ReactElement | null { + return <div></div>; +} |