aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/app/views/common
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/app/views/common')
-rw-r--r--FrontEnd/src/app/views/common/AppBar.tsx80
-rw-r--r--FrontEnd/src/app/views/common/BlobImage.tsx27
-rw-r--r--FrontEnd/src/app/views/common/ConfirmDialog.tsx40
-rw-r--r--FrontEnd/src/app/views/common/FlatButton.tsx36
-rw-r--r--FrontEnd/src/app/views/common/FullPage.tsx39
-rw-r--r--FrontEnd/src/app/views/common/ImageCropper.tsx306
-rw-r--r--FrontEnd/src/app/views/common/LoadFailReload.tsx37
-rw-r--r--FrontEnd/src/app/views/common/LoadingButton.tsx29
-rw-r--r--FrontEnd/src/app/views/common/LoadingPage.tsx12
-rw-r--r--FrontEnd/src/app/views/common/Menu.tsx92
-rw-r--r--FrontEnd/src/app/views/common/OperationDialog.tsx471
-rw-r--r--FrontEnd/src/app/views/common/SearchInput.tsx78
-rw-r--r--FrontEnd/src/app/views/common/Skeleton.tsx30
-rw-r--r--FrontEnd/src/app/views/common/TabPages.tsx74
-rw-r--r--FrontEnd/src/app/views/common/TimelineLogo.tsx26
-rw-r--r--FrontEnd/src/app/views/common/ToggleIconButton.tsx30
-rw-r--r--FrontEnd/src/app/views/common/UserTimelineLogo.tsx26
-rw-r--r--FrontEnd/src/app/views/common/alert/AlertHost.tsx106
-rw-r--r--FrontEnd/src/app/views/common/alert/alert.sass15
-rw-r--r--FrontEnd/src/app/views/common/common.sass191
-rw-r--r--FrontEnd/src/app/views/common/user/UserAvatar.tsx19
21 files changed, 0 insertions, 1764 deletions
diff --git a/FrontEnd/src/app/views/common/AppBar.tsx b/FrontEnd/src/app/views/common/AppBar.tsx
deleted file mode 100644
index 91dfbee9..00000000
--- a/FrontEnd/src/app/views/common/AppBar.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-import { Link, NavLink } from "react-router-dom";
-import classnames from "classnames";
-import { useMediaQuery } from "react-responsive";
-
-import { useUser } from "@/services/user";
-
-import TimelineLogo from "./TimelineLogo";
-import UserAvatar from "./user/UserAvatar";
-
-const AppBar: React.FC = (_) => {
- const { t } = useTranslation();
-
- const user = useUser();
- const hasAdministrationPermission = user && user.hasAdministrationPermission;
-
- const isSmallScreen = useMediaQuery({ maxWidth: 576 });
-
- const [expand, setExpand] = React.useState<boolean>(false);
- const collapse = (): void => setExpand(false);
- const toggleExpand = (): void => setExpand(!expand);
-
- const createLink = (
- link: string,
- label: React.ReactNode,
- className?: string
- ): React.ReactNode => (
- <NavLink
- to={link}
- activeClassName="active"
- onClick={collapse}
- className={className}
- >
- {label}
- </NavLink>
- );
-
- return (
- <nav className={classnames("app-bar", isSmallScreen && "small-screen")}>
- <Link to="/" className="app-bar-brand active">
- <TimelineLogo className="app-bar-brand-icon" />
- Timeline
- </Link>
-
- {isSmallScreen && (
- <i className="bi-list app-bar-toggler" onClick={toggleExpand} />
- )}
-
- <div
- className={classnames(
- "app-bar-main-area",
- !expand && "app-bar-collapse"
- )}
- >
- <div className="app-bar-link-area">
- {createLink("/settings", t("nav.settings"))}
- {createLink("/about", t("nav.about"))}
- {hasAdministrationPermission &&
- createLink("/admin", t("nav.administration"))}
- </div>
-
- <div className="app-bar-user-area">
- {user != null
- ? createLink(
- "/",
- <UserAvatar
- username={user.username}
- className="avatar small rounded-circle bg-white cursor-pointer ml-auto"
- />,
- "app-bar-avatar"
- )
- : createLink("/login", t("nav.login"))}
- </div>
- </div>
- </nav>
- );
-};
-
-export default AppBar;
diff --git a/FrontEnd/src/app/views/common/BlobImage.tsx b/FrontEnd/src/app/views/common/BlobImage.tsx
deleted file mode 100644
index 0dd25c52..00000000
--- a/FrontEnd/src/app/views/common/BlobImage.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from "react";
-
-const BlobImage: React.FC<
- Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src"> & {
- blob?: Blob | unknown;
- }
-> = (props) => {
- const { blob, ...otherProps } = props;
-
- const [url, setUrl] = React.useState<string | undefined>(undefined);
-
- React.useEffect(() => {
- if (blob instanceof Blob) {
- const url = URL.createObjectURL(blob);
- setUrl(url);
- return () => {
- URL.revokeObjectURL(url);
- };
- } else {
- setUrl(undefined);
- }
- }, [blob]);
-
- return <img {...otherProps} src={url} />;
-};
-
-export default BlobImage;
diff --git a/FrontEnd/src/app/views/common/ConfirmDialog.tsx b/FrontEnd/src/app/views/common/ConfirmDialog.tsx
deleted file mode 100644
index 72940c51..00000000
--- a/FrontEnd/src/app/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/app/views/common/FlatButton.tsx b/FrontEnd/src/app/views/common/FlatButton.tsx
deleted file mode 100644
index b1f7a051..00000000
--- a/FrontEnd/src/app/views/common/FlatButton.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from "react";
-import classnames from "classnames";
-
-import { BootstrapThemeColor } from "@/common";
-
-export interface FlatButtonProps {
- variant?: BootstrapThemeColor | string;
- disabled?: boolean;
- className?: string;
- style?: React.CSSProperties;
- onClick?: () => void;
-}
-
-const FlatButton: React.FC<FlatButtonProps> = (props) => {
- const { disabled, className, style } = props;
- const variant = props.variant ?? "primary";
-
- const onClick = disabled ? undefined : props.onClick;
-
- return (
- <div
- className={classnames(
- "flat-button",
- variant,
- disabled ? "disabled" : null,
- className
- )}
- style={style}
- onClick={onClick}
- >
- {props.children}
- </div>
- );
-};
-
-export default FlatButton;
diff --git a/FrontEnd/src/app/views/common/FullPage.tsx b/FrontEnd/src/app/views/common/FullPage.tsx
deleted file mode 100644
index 1b59045a..00000000
--- a/FrontEnd/src/app/views/common/FullPage.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from "react";
-import classnames from "classnames";
-
-export interface FullPageProps {
- show: boolean;
- onBack: () => void;
- contentContainerClassName?: string;
-}
-
-const FullPage: React.FC<FullPageProps> = ({
- show,
- onBack,
- children,
- contentContainerClassName,
-}) => {
- return (
- <div
- className="cru-full-page"
- style={{ display: show ? undefined : "none" }}
- >
- <div className="cru-full-page-top-bar">
- <i
- className="icon-button bi-arrow-left text-white ms-3"
- onClick={onBack}
- />
- </div>
- <div
- className={classnames(
- "cru-full-page-content-container",
- contentContainerClassName
- )}
- >
- {children}
- </div>
- </div>
- );
-};
-
-export default FullPage;
diff --git a/FrontEnd/src/app/views/common/ImageCropper.tsx b/FrontEnd/src/app/views/common/ImageCropper.tsx
deleted file mode 100644
index 2ef5b7ed..00000000
--- a/FrontEnd/src/app/views/common/ImageCropper.tsx
+++ /dev/null
@@ -1,306 +0,0 @@
-import React from "react";
-import classnames from "classnames";
-
-import { UiLogicError } from "@/common";
-
-export interface Clip {
- left: number;
- top: number;
- width: number;
-}
-
-interface NormailizedClip extends Clip {
- height: number;
-}
-
-interface ImageInfo {
- width: number;
- height: number;
- landscape: boolean;
- ratio: number;
- maxClipWidth: number;
- maxClipHeight: number;
-}
-
-interface ImageCropperSavedState {
- clip: NormailizedClip;
- x: number;
- y: number;
- pointerId: number;
-}
-
-export interface ImageCropperProps {
- clip: Clip | null;
- imageUrl: string;
- onChange: (clip: Clip) => void;
- imageElementCallback?: (element: HTMLImageElement | null) => void;
- className?: string;
-}
-
-const ImageCropper = (props: ImageCropperProps): React.ReactElement => {
- const { clip, imageUrl, onChange, imageElementCallback, className } = props;
-
- const [oldState, setOldState] = React.useState<ImageCropperSavedState | null>(
- null
- );
- const [imageInfo, setImageInfo] = React.useState<ImageInfo | null>(null);
-
- const normalizeClip = (c: Clip | null | undefined): NormailizedClip => {
- if (c == null) {
- return { left: 0, top: 0, width: 0, height: 0 };
- }
-
- return {
- left: c.left || 0,
- top: c.top || 0,
- width: c.width || 0,
- height: imageInfo != null ? (c.width || 0) / imageInfo.ratio : 0,
- };
- };
-
- const c = normalizeClip(clip);
-
- const imgElementRef = React.useRef<HTMLImageElement | null>(null);
-
- const onImageRef = React.useCallback(
- (e: HTMLImageElement | null) => {
- imgElementRef.current = e;
- if (imageElementCallback != null && e == null) {
- imageElementCallback(null);
- }
- },
- [imageElementCallback]
- );
-
- const onImageLoad = React.useCallback(
- (e: React.SyntheticEvent<HTMLImageElement>) => {
- const img = e.currentTarget;
- const landscape = img.naturalWidth >= img.naturalHeight;
-
- const info = {
- width: img.naturalWidth,
- height: img.naturalHeight,
- landscape,
- ratio: img.naturalHeight / img.naturalWidth,
- maxClipWidth: landscape ? img.naturalHeight / img.naturalWidth : 1,
- maxClipHeight: landscape ? 1 : img.naturalWidth / img.naturalHeight,
- };
- setImageInfo(info);
- onChange({ left: 0, top: 0, width: info.maxClipWidth });
- if (imageElementCallback != null) {
- imageElementCallback(img);
- }
- },
- [onChange, imageElementCallback]
- );
-
- const onPointerDown = React.useCallback(
- (e: React.PointerEvent) => {
- if (oldState != null) return;
- e.currentTarget.setPointerCapture(e.pointerId);
- setOldState({
- x: e.clientX,
- y: e.clientY,
- clip: c,
- pointerId: e.pointerId,
- });
- },
- [oldState, c]
- );
-
- const onPointerUp = React.useCallback(
- (e: React.PointerEvent) => {
- if (oldState == null || oldState.pointerId !== e.pointerId) return;
- e.currentTarget.releasePointerCapture(e.pointerId);
- setOldState(null);
- },
- [oldState]
- );
-
- const onPointerMove = React.useCallback(
- (e: React.PointerEvent) => {
- if (oldState == null) return;
-
- const oldClip = oldState.clip;
-
- const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y };
-
- const { current: imgElement } = imgElementRef;
-
- if (imgElement == null) throw new UiLogicError("Image element is null.");
-
- const moveRatio = {
- x: movement.x / imgElement.width,
- y: movement.y / imgElement.height,
- };
-
- const newRatio = {
- x: oldClip.left + moveRatio.x,
- y: oldClip.top + moveRatio.y,
- };
- if (newRatio.x < 0) {
- newRatio.x = 0;
- } else if (newRatio.x > 1 - oldClip.width) {
- newRatio.x = 1 - oldClip.width;
- }
- if (newRatio.y < 0) {
- newRatio.y = 0;
- } else if (newRatio.y > 1 - oldClip.height) {
- newRatio.y = 1 - oldClip.height;
- }
-
- onChange({ left: newRatio.x, top: newRatio.y, width: oldClip.width });
- },
- [oldState, onChange]
- );
-
- const onHandlerPointerMove = React.useCallback(
- (e: React.PointerEvent) => {
- if (oldState == null) return;
-
- const oldClip = oldState.clip;
-
- const movement = { x: e.clientX - oldState.x, y: e.clientY - oldState.y };
-
- const ratio = imageInfo == null ? 1 : imageInfo.ratio;
-
- const { current: imgElement } = imgElementRef;
-
- if (imgElement == null) throw new UiLogicError("Image element is null.");
-
- const moveRatio = {
- x: movement.x / imgElement.width,
- y: movement.x / imgElement.width / ratio,
- };
-
- const newRatio = {
- x: oldClip.width + moveRatio.x,
- y: oldClip.height + moveRatio.y,
- };
-
- const maxRatio = {
- x: Math.min(1 - oldClip.left, newRatio.x),
- y: Math.min(1 - oldClip.top, newRatio.y),
- };
-
- const maxWidthRatio = Math.min(maxRatio.x, maxRatio.y * ratio);
-
- let newWidth;
- if (newRatio.x < 0) {
- newWidth = 0;
- } else if (newRatio.x > maxWidthRatio) {
- newWidth = maxWidthRatio;
- } else {
- newWidth = newRatio.x;
- }
-
- onChange({ left: oldClip.left, top: oldClip.top, width: newWidth });
- },
- [imageInfo, oldState, onChange]
- );
-
- const toPercentage = (n: number): string => `${n}%`;
-
- // fuck!!! I just can't find a better way to implement this in pure css
- const containerStyle: React.CSSProperties = (() => {
- if (imageInfo == null) {
- return { width: "100%", paddingTop: "100%", height: 0 };
- } else {
- if (imageInfo.ratio > 1) {
- return {
- width: toPercentage(100 / imageInfo.ratio),
- paddingTop: "100%",
- height: 0,
- };
- } else {
- return {
- width: "100%",
- paddingTop: toPercentage(100 * imageInfo.ratio),
- height: 0,
- };
- }
- }
- })();
-
- return (
- <div
- className={classnames("image-cropper-container", className)}
- style={containerStyle}
- >
- <img ref={onImageRef} src={imageUrl} onLoad={onImageLoad} alt="to crop" />
- <div className="image-cropper-mask-container">
- <div
- className="image-cropper-mask"
- touch-action="none"
- style={{
- left: toPercentage(c.left * 100),
- top: toPercentage(c.top * 100),
- width: toPercentage(c.width * 100),
- height: toPercentage(c.height * 100),
- }}
- onPointerMove={onPointerMove}
- onPointerDown={onPointerDown}
- onPointerUp={onPointerUp}
- />
- </div>
- <div
- className="image-cropper-handler"
- touch-action="none"
- style={{
- left: `calc(${(c.left + c.width) * 100}% - 15px)`,
- top: `calc(${(c.top + c.height) * 100}% - 15px)`,
- }}
- onPointerMove={onHandlerPointerMove}
- onPointerDown={onPointerDown}
- onPointerUp={onPointerUp}
- />
- </div>
- );
-};
-
-export default ImageCropper;
-
-export function applyClipToImage(
- image: HTMLImageElement,
- clip: Clip,
- mimeType: string
-): Promise<Blob> {
- return new Promise((resolve, reject) => {
- const naturalSize = {
- width: image.naturalWidth,
- height: image.naturalHeight,
- };
- const clipArea = {
- x: naturalSize.width * clip.left,
- y: naturalSize.height * clip.top,
- length: naturalSize.width * clip.width,
- };
-
- const canvas = document.createElement("canvas");
- canvas.width = clipArea.length;
- canvas.height = clipArea.length;
- const context = canvas.getContext("2d");
-
- if (context == null) throw new Error("Failed to create context.");
-
- context.drawImage(
- image,
- clipArea.x,
- clipArea.y,
- clipArea.length,
- clipArea.length,
- 0,
- 0,
- clipArea.length,
- clipArea.length
- );
-
- canvas.toBlob((blob) => {
- if (blob == null) {
- reject(new Error("canvas.toBlob returns null"));
- } else {
- resolve(blob);
- }
- }, mimeType);
- });
-}
diff --git a/FrontEnd/src/app/views/common/LoadFailReload.tsx b/FrontEnd/src/app/views/common/LoadFailReload.tsx
deleted file mode 100644
index a80e7b76..00000000
--- a/FrontEnd/src/app/views/common/LoadFailReload.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from "react";
-import { Trans } from "react-i18next";
-
-export interface LoadFailReloadProps {
- className?: string;
- style?: React.CSSProperties;
- onReload: () => void;
-}
-
-const LoadFailReload: React.FC<LoadFailReloadProps> = ({
- onReload,
- className,
- style,
-}) => {
- return (
- <Trans
- i18nKey="loadFailReload"
- parent="div"
- className={className}
- style={style}
- >
- 0
- <a
- href="#"
- onClick={(e) => {
- onReload();
- e.preventDefault();
- }}
- >
- 1
- </a>
- 2
- </Trans>
- );
-};
-
-export default LoadFailReload;
diff --git a/FrontEnd/src/app/views/common/LoadingButton.tsx b/FrontEnd/src/app/views/common/LoadingButton.tsx
deleted file mode 100644
index cd9f1adc..00000000
--- a/FrontEnd/src/app/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/app/views/common/LoadingPage.tsx b/FrontEnd/src/app/views/common/LoadingPage.tsx
deleted file mode 100644
index 590fafa0..00000000
--- a/FrontEnd/src/app/views/common/LoadingPage.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from "react";
-import { Spinner } from "react-bootstrap";
-
-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" />
- </div>
- );
-};
-
-export default LoadingPage;
diff --git a/FrontEnd/src/app/views/common/Menu.tsx b/FrontEnd/src/app/views/common/Menu.tsx
deleted file mode 100644
index ae73a331..00000000
--- a/FrontEnd/src/app/views/common/Menu.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-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";
-
-export type MenuItem =
- | {
- type: "divider";
- }
- | {
- type: "button";
- text: I18nText;
- iconClassName?: string;
- color?: BootstrapThemeColor;
- onClick: () => void;
- };
-
-export type MenuItems = MenuItem[];
-
-export interface MenuProps {
- items: MenuItems;
- className?: string;
- onItemClicked?: () => void;
-}
-
-const Menu: React.FC<MenuProps> = ({ items, className, onItemClicked }) => {
- const { t } = useTranslation();
-
- return (
- <div className={classnames("cru-menu", className)}>
- {items.map((item, index) => {
- if (item.type === "divider") {
- return <div key={index} className="cru-menu-divider" />;
- } else {
- return (
- <div
- key={index}
- className={classnames(
- "cru-menu-item",
- `color-${item.color ?? "primary"}`
- )}
- onClick={() => {
- item.onClick();
- onItemClicked?.();
- }}
- >
- {item.iconClassName != null ? (
- <i
- className={classnames(
- item.iconClassName,
- "cru-menu-item-icon"
- )}
- />
- ) : null}
- {convertI18nText(item.text, t)}
- </div>
- );
- }
- })}
- </div>
- );
-};
-
-export default Menu;
-
-export interface PopupMenuProps {
- items: MenuItems;
- children: OverlayTriggerProps["children"];
-}
-
-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>
- );
-};
diff --git a/FrontEnd/src/app/views/common/OperationDialog.tsx b/FrontEnd/src/app/views/common/OperationDialog.tsx
deleted file mode 100644
index ac4c51b9..00000000
--- a/FrontEnd/src/app/views/common/OperationDialog.tsx
+++ /dev/null
@@ -1,471 +0,0 @@
-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";
-
-interface DefaultErrorPromptProps {
- error?: string;
-}
-
-const DefaultErrorPrompt: React.FC<DefaultErrorPromptProps> = (props) => {
- const { t } = useTranslation();
-
- let result = <p className="text-danger">{t("operationDialog.error")}</p>;
-
- if (props.error != null) {
- result = (
- <>
- {result}
- <p className="text-danger">{props.error}</p>
- </>
- );
- }
-
- return result;
-};
-
-export interface OperationDialogTextInput {
- type: "text";
- label?: I18nText;
- password?: boolean;
- initValue?: string;
- textFieldProps?: Omit<
- React.InputHTMLAttributes<HTMLInputElement>,
- "type" | "value" | "onChange" | "aria-relevant"
- >;
- helperText?: string;
-}
-
-export interface OperationDialogBoolInput {
- type: "bool";
- label: I18nText;
- initValue?: boolean;
-}
-
-export interface OperationDialogSelectInputOption {
- value: string;
- label: I18nText;
- icon?: React.ReactElement;
-}
-
-export interface OperationDialogSelectInput {
- type: "select";
- label: I18nText;
- options: OperationDialogSelectInputOption[];
- initValue?: string;
-}
-
-export interface OperationDialogColorInput {
- type: "color";
- label?: I18nText;
- initValue?: string | null;
- canBeNull?: boolean;
-}
-
-export interface OperationDialogDateTimeInput {
- type: "datetime";
- label?: I18nText;
- initValue?: string;
-}
-
-export type OperationDialogInput =
- | OperationDialogTextInput
- | OperationDialogBoolInput
- | OperationDialogSelectInput
- | OperationDialogColorInput
- | OperationDialogDateTimeInput;
-
-interface OperationInputTypeStringToValueTypeMap {
- text: string;
- bool: boolean;
- select: string;
- color: string | null;
- datetime: string;
-}
-
-type MapOperationInputTypeStringToValueType<Type> =
- Type extends keyof OperationInputTypeStringToValueTypeMap
- ? OperationInputTypeStringToValueTypeMap[Type]
- : never;
-
-type MapOperationInputInfoValueType<T> = T extends OperationDialogInput
- ? MapOperationInputTypeStringToValueType<T["type"]>
- : T;
-
-const initValueMapperMap: {
- [T in OperationDialogInput as T["type"]]: (
- item: T
- ) => MapOperationInputInfoValueType<T>;
-} = {
- bool: (item) => item.initValue ?? false,
- color: (item) => item.initValue ?? null,
- datetime: (item) => {
- if (item.initValue != null) {
- return moment(item.initValue).format("YYYY-MM-DDTHH:mm:ss");
- } else {
- return "";
- }
- },
- select: (item) => item.initValue ?? item.options[0].value,
- text: (item) => item.initValue ?? "",
-};
-
-type MapOperationInputInfoValueTypeList<
- Tuple extends readonly OperationDialogInput[]
-> = {
- [Index in keyof Tuple]: MapOperationInputInfoValueType<Tuple[Index]>;
-} & { length: Tuple["length"] };
-
-export type OperationInputError =
- | {
- [index: number]: I18nText | null | undefined;
- }
- | null
- | undefined;
-
-const isNoError = (error: OperationInputError): boolean => {
- if (error == null) return true;
- for (const key in error) {
- if (error[key] != null) return false;
- }
- return true;
-};
-
-export interface OperationDialogProps<
- TData,
- OperationInputInfoList extends readonly OperationDialogInput[]
-> {
- open: boolean;
- close: () => void;
- title: I18nText | (() => React.ReactNode);
- themeColor?: "danger" | "success" | string;
- onProcess: (
- inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList>
- ) => Promise<TData>;
- inputScheme?: OperationInputInfoList;
- inputValidator?: (
- inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList>
- ) => OperationInputError;
- inputPrompt?: I18nText | (() => React.ReactNode);
- processPrompt?: () => React.ReactNode;
- successPrompt?: (data: TData) => React.ReactNode;
- failurePrompt?: (error: unknown) => React.ReactNode;
- onSuccessAndClose?: (data: TData) => void;
-}
-
-const OperationDialog = <
- TData,
- OperationInputInfoList extends readonly OperationDialogInput[]
->(
- props: OperationDialogProps<TData, OperationInputInfoList>
-): React.ReactElement => {
- const inputScheme = (props.inputScheme ??
- []) as readonly OperationDialogInput[];
-
- const { t } = useTranslation();
-
- type Step =
- | { type: "input" }
- | { type: "process" }
- | {
- type: "success";
- data: TData;
- }
- | {
- type: "failure";
- data: unknown;
- };
- const [step, setStep] = useState<Step>({ type: "input" });
-
- type ValueType = boolean | string | null | undefined;
-
- const [values, setValues] = useState<ValueType[]>(
- inputScheme.map((item) => {
- if (item.type in initValueMapperMap) {
- return (
- initValueMapperMap[item.type] as (
- i: OperationDialogInput
- ) => ValueType
- )(item);
- } else {
- throw new UiLogicError("Unknown input scheme.");
- }
- })
- );
- const [dirtyList, setDirtyList] = useState<boolean[]>(() =>
- inputScheme.map(() => false)
- );
- const [inputError, setInputError] = useState<OperationInputError>();
-
- const close = (): void => {
- if (step.type !== "process") {
- props.close();
- if (step.type === "success" && props.onSuccessAndClose) {
- props.onSuccessAndClose(step.data);
- }
- } else {
- console.log("Attempt to close modal when processing.");
- }
- };
-
- const onConfirm = (): void => {
- setStep({ type: "process" });
- props
- .onProcess(
- values.map((v, index) => {
- if (inputScheme[index].type === "datetime" && v !== "")
- return new Date(v as string).toISOString();
- else return v;
- }) as unknown as MapOperationInputInfoValueTypeList<OperationInputInfoList>
- )
- .then(
- (d) => {
- setStep({
- type: "success",
- data: d,
- });
- },
- (e: unknown) => {
- setStep({
- type: "failure",
- data: e,
- });
- }
- );
- };
-
- let body: React.ReactNode;
- if (step.type === "input" || step.type === "process") {
- const process = step.type === "process";
-
- let inputPrompt =
- typeof props.inputPrompt === "function"
- ? props.inputPrompt()
- : convertI18nText(props.inputPrompt, t);
- inputPrompt = <h6>{inputPrompt}</h6>;
-
- const validate = (values: ValueType[]): boolean => {
- const { inputValidator } = props;
- if (inputValidator != null) {
- const result = inputValidator(
- values as unknown as MapOperationInputInfoValueTypeList<OperationInputInfoList>
- );
- setInputError(result);
- return isNoError(result);
- }
- return true;
- };
-
- const updateValue = (index: number, newValue: ValueType): void => {
- const oldValues = values;
- const newValues = oldValues.slice();
- newValues[index] = newValue;
- setValues(newValues);
- if (dirtyList[index] === false) {
- const newDirtyList = dirtyList.slice();
- newDirtyList[index] = true;
- setDirtyList(newDirtyList);
- }
- validate(newValues);
- };
-
- const canProcess = isNoError(inputError);
-
- body = (
- <>
- <Modal.Body>
- {inputPrompt}
- {inputScheme.map((item, index) => {
- const value = values[index];
- const error: string | null =
- dirtyList[index] && inputError != null
- ? convertI18nText(inputError[index], t)
- : null;
-
- if (item.type === "text") {
- return (
- <Form.Group key={index}>
- {item.label && (
- <Form.Label>{convertI18nText(item.label, t)}</Form.Label>
- )}
- <Form.Control
- 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>
- );
- } else if (item.type === "bool") {
- return (
- <Form.Group key={index}>
- <Form.Check<"input">
- type="checkbox"
- checked={value as boolean}
- onChange={(event) => {
- updateValue(index, event.currentTarget.checked);
- }}
- label={convertI18nText(item.label, t)}
- disabled={process}
- />
- </Form.Group>
- );
- } else if (item.type === "select") {
- return (
- <Form.Group key={index}>
- <Form.Label>{convertI18nText(item.label, t)}</Form.Label>
- <Form.Control
- as="select"
- value={value as string}
- onChange={(event) => {
- updateValue(index, event.target.value);
- }}
- disabled={process}
- >
- {item.options.map((option, i) => {
- return (
- <option value={option.value} key={i}>
- {option.icon}
- {convertI18nText(option.label, t)}
- </option>
- );
- })}
- </Form.Control>
- </Form.Group>
- );
- } else if (item.type === "color") {
- return (
- <Form.Group key={index}>
- {item.canBeNull ? (
- <Form.Check<"input">
- type="checkbox"
- checked={value !== null}
- onChange={(event) => {
- if (event.currentTarget.checked) {
- updateValue(index, "#007bff");
- } else {
- updateValue(index, null);
- }
- }}
- label={convertI18nText(item.label, t)}
- disabled={process}
- />
- ) : (
- <Form.Label>{convertI18nText(item.label, t)}</Form.Label>
- )}
- {value !== null && (
- <TwitterPicker
- color={value as string}
- onChange={(result) => updateValue(index, result.hex)}
- />
- )}
- </Form.Group>
- );
- } else if (item.type === "datetime") {
- return (
- <Form.Group key={index}>
- {item.label && (
- <Form.Label>{convertI18nText(item.label, t)}</Form.Label>
- )}
- <Form.Control
- 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>
- );
- }
- })}
- </Modal.Body>
- <Modal.Footer>
- <Button variant="outline-secondary" onClick={close}>
- {t("operationDialog.cancel")}
- </Button>
- <LoadingButton
- variant={props.themeColor}
- loading={process}
- disabled={!canProcess}
- onClick={() => {
- setDirtyList(inputScheme.map(() => true));
- if (validate(values)) {
- onConfirm();
- }
- }}
- >
- {t("operationDialog.confirm")}
- </LoadingButton>
- </Modal.Footer>
- </>
- );
- } else {
- let content: React.ReactNode;
- const result = step;
- if (result.type === "success") {
- content =
- props.successPrompt?.(result.data) ?? t("operationDialog.success");
- if (typeof content === "string")
- content = <p className="text-success">{content}</p>;
- } else {
- content = props.failurePrompt?.(result.data) ?? <DefaultErrorPrompt />;
- if (typeof content === "string")
- content = <DefaultErrorPrompt error={content} />;
- }
- body = (
- <>
- <Modal.Body>{content}</Modal.Body>
- <Modal.Footer>
- <Button variant="primary" onClick={close}>
- {t("operationDialog.ok")}
- </Button>
- </Modal.Footer>
- </>
- );
- }
-
- const title =
- typeof props.title === "function"
- ? props.title()
- : convertI18nText(props.title, t);
-
- return (
- <Modal show={props.open} onHide={close}>
- <Modal.Header
- className={
- props.themeColor != null ? "text-" + props.themeColor : undefined
- }
- >
- {title}
- </Modal.Header>
- {body}
- </Modal>
- );
-};
-
-export default OperationDialog;
diff --git a/FrontEnd/src/app/views/common/SearchInput.tsx b/FrontEnd/src/app/views/common/SearchInput.tsx
deleted file mode 100644
index ccb6dad6..00000000
--- a/FrontEnd/src/app/views/common/SearchInput.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import React, { useCallback } from "react";
-import classnames from "classnames";
-import { useTranslation } from "react-i18next";
-import { Spinner, Form, Button } from "react-bootstrap";
-
-export interface SearchInputProps {
- value: string;
- onChange: (value: string) => void;
- onButtonClick: () => void;
- className?: string;
- loading?: boolean;
- buttonText?: string;
- placeholder?: string;
- additionalButton?: React.ReactNode;
- alwaysOneline?: boolean;
-}
-
-const SearchInput: React.FC<SearchInputProps> = (props) => {
- const { onChange, onButtonClick, alwaysOneline } = props;
-
- const { t } = useTranslation();
-
- const onInputChange = useCallback(
- (event: React.ChangeEvent<HTMLInputElement>): void => {
- onChange(event.currentTarget.value);
- },
- [onChange]
- );
-
- const onInputKeyPress = useCallback(
- (event: React.KeyboardEvent<HTMLInputElement>): void => {
- if (event.key === "Enter") {
- onButtonClick();
- event.preventDefault();
- }
- },
- [onButtonClick]
- );
-
- return (
- <Form
- className={classnames(
- "cru-search-input",
- alwaysOneline ? "flex-nowrap" : "flex-sm-nowrap",
- props.className
- )}
- >
- <Form.Control
- className="me-sm-2 flex-grow-1"
- value={props.value}
- onChange={onInputChange}
- onKeyPress={onInputKeyPress}
- placeholder={props.placeholder}
- />
- {props.additionalButton ? (
- <div className="mt-2 mt-sm-0 flex-shrink-0 order-sm-last ms-sm-2">
- {props.additionalButton}
- </div>
- ) : null}
- <div
- className={classnames(
- alwaysOneline ? "mt-0 ms-2" : "mt-2 mt-sm-0 ms-auto ms-sm-0",
- "flex-shrink-0"
- )}
- >
- {props.loading ? (
- <Spinner variant="primary" animation="border" />
- ) : (
- <Button variant="outline-primary" onClick={props.onButtonClick}>
- {props.buttonText ?? t("search")}
- </Button>
- )}
- </div>
- </Form>
- );
-};
-
-export default SearchInput;
diff --git a/FrontEnd/src/app/views/common/Skeleton.tsx b/FrontEnd/src/app/views/common/Skeleton.tsx
deleted file mode 100644
index 14886c71..00000000
--- a/FrontEnd/src/app/views/common/Skeleton.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from "react";
-import classnames from "classnames";
-import { range } from "lodash";
-
-export interface SkeletonProps {
- lineNumber?: number;
- className?: string;
- style?: React.CSSProperties;
-}
-
-const Skeleton: React.FC<SkeletonProps> = (props) => {
- const { lineNumber: lineNumberProps, className, style } = props;
- const lineNumber = lineNumberProps ?? 3;
-
- return (
- <div className={classnames(className, "cru-skeleton")} style={style}>
- {range(lineNumber).map((i) => (
- <div
- key={i}
- className={classnames(
- "cru-skeleton-line",
- i === lineNumber - 1 && "last"
- )}
- />
- ))}
- </div>
- );
-};
-
-export default Skeleton;
diff --git a/FrontEnd/src/app/views/common/TabPages.tsx b/FrontEnd/src/app/views/common/TabPages.tsx
deleted file mode 100644
index 2b1d91cb..00000000
--- a/FrontEnd/src/app/views/common/TabPages.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import React from "react";
-import { Nav } from "react-bootstrap";
-import { useTranslation } from "react-i18next";
-
-import { convertI18nText, I18nText, UiLogicError } from "@/common";
-
-export interface TabPage {
- id: string;
- tabText: I18nText;
- page: React.ReactNode;
-}
-
-export interface TabPagesProps {
- pages: TabPage[];
- actions?: React.ReactNode;
- className?: string;
- style?: React.CSSProperties;
- navClassName?: string;
- navStyle?: React.CSSProperties;
- pageContainerClassName?: string;
- pageContainerStyle?: React.CSSProperties;
-}
-
-const TabPages: React.FC<TabPagesProps> = ({
- pages,
- actions,
- className,
- style,
- navClassName,
- navStyle,
- pageContainerClassName,
- pageContainerStyle,
-}) => {
- if (pages.length === 0) {
- throw new UiLogicError("Page list can't be empty.");
- }
-
- const { t } = useTranslation();
-
- const [tab, setTab] = React.useState<string>(pages[0].id);
-
- const currentPage = pages.find((p) => p.id === tab);
-
- if (currentPage == null) {
- throw new UiLogicError("Current tab value is bad.");
- }
-
- 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>
- </div>
- );
-};
-
-export default TabPages;
diff --git a/FrontEnd/src/app/views/common/TimelineLogo.tsx b/FrontEnd/src/app/views/common/TimelineLogo.tsx
deleted file mode 100644
index 27d188fc..00000000
--- a/FrontEnd/src/app/views/common/TimelineLogo.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React, { SVGAttributes } from "react";
-
-export interface TimelineLogoProps extends SVGAttributes<SVGElement> {
- color?: string;
-}
-
-const TimelineLogo: React.FC<TimelineLogoProps> = (props) => {
- const { color, ...forwardProps } = props;
- const coercedColor = color ?? "currentcolor";
- return (
- <svg
- className={props.className}
- viewBox="0 0 100 100"
- fill="none"
- strokeWidth="12"
- stroke={coercedColor}
- {...forwardProps}
- >
- <line x1="50" y1="0" x2="50" y2="25" />
- <circle cx="50" cy="50" r="22" />
- <line x1="50" y1="75" x2="50" y2="100" />
- </svg>
- );
-};
-
-export default TimelineLogo;
diff --git a/FrontEnd/src/app/views/common/ToggleIconButton.tsx b/FrontEnd/src/app/views/common/ToggleIconButton.tsx
deleted file mode 100644
index c4d2d132..00000000
--- a/FrontEnd/src/app/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/app/views/common/UserTimelineLogo.tsx b/FrontEnd/src/app/views/common/UserTimelineLogo.tsx
deleted file mode 100644
index 19b9fee5..00000000
--- a/FrontEnd/src/app/views/common/UserTimelineLogo.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React, { SVGAttributes } from "react";
-
-export interface UserTimelineLogoProps extends SVGAttributes<SVGElement> {
- color?: string;
-}
-
-const UserTimelineLogo: React.FC<UserTimelineLogoProps> = (props) => {
- const { color, ...forwardProps } = props;
- const coercedColor = color ?? "currentcolor";
-
- return (
- <svg viewBox="0 0 100 100" {...forwardProps}>
- <g fill="none" stroke={coercedColor} strokeWidth="12">
- <line x1="50" x2="50" y1="0" y2="25" />
- <circle cx="50" cy="50" r="22" />
- <line x1="50" x2="50" y1="75" y2="100" />
- </g>
- <g fill={coercedColor}>
- <circle cx="85" cy="75" r="10" />
- <path d="m70,100c0,0 15,-30 30,0.25" />
- </g>
- </svg>
- );
-};
-
-export default UserTimelineLogo;
diff --git a/FrontEnd/src/app/views/common/alert/AlertHost.tsx b/FrontEnd/src/app/views/common/alert/AlertHost.tsx
deleted file mode 100644
index 949be7ed..00000000
--- a/FrontEnd/src/app/views/common/alert/AlertHost.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import React from "react";
-import without from "lodash/without";
-import { useTranslation } from "react-i18next";
-import { Alert } from "react-bootstrap";
-
-import {
- alertService,
- AlertInfoEx,
- kAlertHostId,
- AlertInfo,
-} from "@/services/alert";
-import { convertI18nText } from "@/common";
-
-interface AutoCloseAlertProps {
- alert: AlertInfo;
- close: () => void;
-}
-
-export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => {
- const { alert, close } = props;
- const { dismissTime } = alert;
-
- const { t } = useTranslation();
-
- const timerTag = React.useRef<number | null>(null);
- const closeHandler = React.useRef<(() => void) | null>(null);
-
- React.useEffect(() => {
- closeHandler.current = close;
- }, [close]);
-
- React.useEffect(() => {
- const tag =
- dismissTime === "never"
- ? null
- : typeof dismissTime === "number"
- ? window.setTimeout(() => closeHandler.current?.(), dismissTime)
- : window.setTimeout(() => closeHandler.current?.(), 5000);
- timerTag.current = tag;
- return () => {
- if (tag != null) {
- window.clearTimeout(tag);
- }
- };
- }, [dismissTime]);
-
- const cancelTimer = (): void => {
- const { current: tag } = timerTag;
- if (tag != null) {
- window.clearTimeout(tag);
- }
- };
-
- return (
- <Alert
- className="m-3"
- variant={alert.type ?? "primary"}
- onClick={cancelTimer}
- onClose={close}
- dismissible
- >
- {(() => {
- const { message } = alert;
- if (typeof message === "function") {
- const Message = message;
- return <Message />;
- } else return convertI18nText(message, t);
- })()}
- </Alert>
- );
-};
-
-const AlertHost: React.FC = () => {
- const [alerts, setAlerts] = React.useState<AlertInfoEx[]>([]);
-
- // react guarantee that state setters are stable, so we don't need to add it to dependency list
-
- React.useEffect(() => {
- const consume = (alert: AlertInfoEx): void => {
- setAlerts((old) => [...old, alert]);
- };
-
- alertService.registerConsumer(consume);
- return () => {
- alertService.unregisterConsumer(consume);
- };
- }, []);
-
- return (
- <div id={kAlertHostId} className="alert-container">
- {alerts.map((alert) => {
- return (
- <AutoCloseAlert
- key={alert.id}
- alert={alert}
- close={() => {
- setAlerts((old) => without(old, alert));
- }}
- />
- );
- })}
- </div>
- );
-};
-
-export default AlertHost;
diff --git a/FrontEnd/src/app/views/common/alert/alert.sass b/FrontEnd/src/app/views/common/alert/alert.sass
deleted file mode 100644
index c3560b87..00000000
--- a/FrontEnd/src/app/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/app/views/common/common.sass b/FrontEnd/src/app/views/common/common.sass
deleted file mode 100644
index cbf7292e..00000000
--- a/FrontEnd/src/app/views/common/common.sass
+++ /dev/null
@@ -1,191 +0,0 @@
-.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, 80%)
- touch-action: none
-
-.image-cropper-handler
- position: absolute
- width: 26px
- height: 26px
- border: black solid 2px
- 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
-
- a
- color: var(--tl-text-on-primary-inactive-color)
- text-decoration: none
- margin: 0 1em
-
- &:hover
- color: var(--tl-text-on-primary-color)
-
- &.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
-
- &.app-bar-collapse
- transform: scale(1,0)
-
- a
- text-align: left
- padding: 0.5em 0.5em
-
- .app-bar-link-area
- flex-direction: column
- align-items: stretch
-
- .app-bar-user-area
- flex-direction: column
- align-items: stretch
- margin-left: unset
-
- .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
- padding: 0 1em
-
-.cru-skeleton-line
- height: 1em
- background-color: #e6e6e6
- margin: 0.7em 0
- border-radius: 0.2em
-
- &.last
- width: 50%
-
-.cru-full-page
- position: fixed
- z-index: 1031
- left: 0
- top: 0
- right: 0
- bottom: 0
- background-color: white
- padding-top: 56px
-
-.cru-full-page-top-bar
- height: 56px
-
- position: absolute
- top: 0
- left: 0
- right: 0
- z-index: 1
-
- background-color: var(--tl-primary-color)
-
- display: flex
- align-items: center
-
-.cru-full-page-content-container
- overflow: scroll
-
-.cru-menu
- min-width: 200px
-
-.cru-menu-item
- font-size: 1.2em
- padding: 0.5em 1.5em
- cursor: pointer
-
- @each $color, $value in $theme-colors
- &.color-#{$color}
- color: $value
-
- &:hover
- color: white
- background-color: $value
-
-.cru-menu-item-icon
- margin-right: 1em
-
-.cru-menu-divider
- border-top: 1px solid $gray-200
-
-.cru-tab-pages-action-area
- display: flex
- align-items: center
-
-.cru-search-input
- display: flex
- flex-wrap: wrap
diff --git a/FrontEnd/src/app/views/common/user/UserAvatar.tsx b/FrontEnd/src/app/views/common/user/UserAvatar.tsx
deleted file mode 100644
index 9e822528..00000000
--- a/FrontEnd/src/app/views/common/user/UserAvatar.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from "react";
-
-import { getHttpUserClient } from "@/http/user";
-
-export interface UserAvatarProps
- extends React.ImgHTMLAttributes<HTMLImageElement> {
- username: string;
-}
-
-const UserAvatar: React.FC<UserAvatarProps> = ({ username, ...otherProps }) => {
- return (
- <img
- src={getHttpUserClient().generateAvatarUrl(username)}
- {...otherProps}
- />
- );
-};
-
-export default UserAvatar;