aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/views/common
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/views/common')
-rw-r--r--FrontEnd/src/views/common/AppBar.css95
-rw-r--r--FrontEnd/src/views/common/AppBar.tsx81
-rw-r--r--FrontEnd/src/views/common/BlobImage.tsx27
-rw-r--r--FrontEnd/src/views/common/Card.css15
-rw-r--r--FrontEnd/src/views/common/Card.tsx27
-rw-r--r--FrontEnd/src/views/common/ImageCropper.css38
-rw-r--r--FrontEnd/src/views/common/ImageCropper.tsx306
-rw-r--r--FrontEnd/src/views/common/LoadFailReload.tsx37
-rw-r--r--FrontEnd/src/views/common/LoadingPage.tsx13
-rw-r--r--FrontEnd/src/views/common/SearchInput.css8
-rw-r--r--FrontEnd/src/views/common/SearchInput.tsx79
-rw-r--r--FrontEnd/src/views/common/Skeleton.css14
-rw-r--r--FrontEnd/src/views/common/Skeleton.tsx32
-rw-r--r--FrontEnd/src/views/common/Spinner.css13
-rw-r--r--FrontEnd/src/views/common/Spinner.tsx43
-rw-r--r--FrontEnd/src/views/common/TimelineLogo.tsx27
-rw-r--r--FrontEnd/src/views/common/alert/AlertHost.tsx113
-rw-r--r--FrontEnd/src/views/common/alert/alert.css33
-rw-r--r--FrontEnd/src/views/common/button/Button.css51
-rw-r--r--FrontEnd/src/views/common/button/Button.tsx47
-rw-r--r--FrontEnd/src/views/common/button/FlatButton.css18
-rw-r--r--FrontEnd/src/views/common/button/FlatButton.tsx37
-rw-r--r--FrontEnd/src/views/common/button/IconButton.css10
-rw-r--r--FrontEnd/src/views/common/button/IconButton.tsx29
-rw-r--r--FrontEnd/src/views/common/button/LoadingButton.tsx40
-rw-r--r--FrontEnd/src/views/common/button/index.tsx6
-rw-r--r--FrontEnd/src/views/common/dialog/ConfirmDialog.tsx43
-rw-r--r--FrontEnd/src/views/common/dialog/Dialog.css55
-rw-r--r--FrontEnd/src/views/common/dialog/Dialog.tsx51
-rw-r--r--FrontEnd/src/views/common/dialog/FullPageDialog.css44
-rw-r--r--FrontEnd/src/views/common/dialog/FullPageDialog.tsx53
-rw-r--r--FrontEnd/src/views/common/dialog/OperationDialog.css25
-rw-r--r--FrontEnd/src/views/common/dialog/OperationDialog.tsx531
-rw-r--r--FrontEnd/src/views/common/index.css293
-rw-r--r--FrontEnd/src/views/common/input/InputPanel.css25
-rw-r--r--FrontEnd/src/views/common/input/InputPanel.tsx257
-rw-r--r--FrontEnd/src/views/common/menu/Menu.css24
-rw-r--r--FrontEnd/src/views/common/menu/Menu.tsx72
-rw-r--r--FrontEnd/src/views/common/menu/PopupMenu.css6
-rw-r--r--FrontEnd/src/views/common/menu/PopupMenu.tsx71
-rw-r--r--FrontEnd/src/views/common/tab/TabPages.tsx71
-rw-r--r--FrontEnd/src/views/common/tab/Tabs.css33
-rw-r--r--FrontEnd/src/views/common/tab/Tabs.tsx62
-rw-r--r--FrontEnd/src/views/common/user/UserAvatar.tsx19
44 files changed, 0 insertions, 2974 deletions
diff --git a/FrontEnd/src/views/common/AppBar.css b/FrontEnd/src/views/common/AppBar.css
deleted file mode 100644
index 3ec4fa36..00000000
--- a/FrontEnd/src/views/common/AppBar.css
+++ /dev/null
@@ -1,95 +0,0 @@
-.app-bar {
- display: flex;
- align-items: center;
- height: 56px;
- position: fixed;
- z-index: 1030;
- top: 0;
- left: 0;
- right: 0;
- background-color: var(--cru-primary-color);
- transition: background-color 1s;
-}
-
-.app-bar .cru-avatar {
- background-color: white;
-}
-
-.app-bar a {
- color: var(--cru-primary-t1-color);
- text-decoration: none;
- margin: 0 1em;
- transition: color 1s;
-}
-.app-bar a:hover {
- color: var(--cru-primary-t-color);
-}
-.app-bar a.active {
- color: var(--cru-primary-t-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(--cru-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(--cru-primary-t-color);
- cursor: pointer;
- user-select: none;
-}
diff --git a/FrontEnd/src/views/common/AppBar.tsx b/FrontEnd/src/views/common/AppBar.tsx
deleted file mode 100644
index 278c70fd..00000000
--- a/FrontEnd/src/views/common/AppBar.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-import { useTranslation } from "react-i18next";
-import { Link, NavLink } from "react-router-dom";
-import { useMediaQuery } from "react-responsive";
-
-import { useUser } from "@/services/user";
-
-import TimelineLogo from "./TimelineLogo";
-import UserAvatar from "./user/UserAvatar";
-
-import "./AppBar.css";
-
-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}
- onClick={collapse}
- className={({ isActive }) => classnames(className, isActive && "active")}
- >
- {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="cru-avatar small cru-round cursor-pointer ml-auto"
- />,
- "app-bar-avatar"
- )
- : createLink("/login", t("nav.login"))}
- </div>
- </div>
- </nav>
- );
-};
-
-export default AppBar;
diff --git a/FrontEnd/src/views/common/BlobImage.tsx b/FrontEnd/src/views/common/BlobImage.tsx
deleted file mode 100644
index 5e050ebe..00000000
--- a/FrontEnd/src/views/common/BlobImage.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import * as 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/views/common/Card.css b/FrontEnd/src/views/common/Card.css
deleted file mode 100644
index 6de0dd8e..00000000
--- a/FrontEnd/src/views/common/Card.css
+++ /dev/null
@@ -1,15 +0,0 @@
-:root {
- --cru-card-border-radius: 8px;
-}
-
-.cru-card {
- border: 1px solid;
- border-color: #e9ecef;
- border-radius: var(--cru-card-border-radius);
- background: #fefeff;
- transition: all 0.3s;
-}
-
-.cru-card:hover {
- border-color: var(--cru-primary-color);
-}
diff --git a/FrontEnd/src/views/common/Card.tsx b/FrontEnd/src/views/common/Card.tsx
deleted file mode 100644
index ebbce77e..00000000
--- a/FrontEnd/src/views/common/Card.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import classNames from "classnames";
-import * as React from "react";
-
-import "./Card.css";
-
-function _Card(
- {
- className,
- children,
- ...otherProps
- }: React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>,
- ref: React.ForwardedRef<HTMLDivElement>
-): React.ReactElement | null {
- return (
- <div
- ref={ref}
- className={classNames("cru-card", className)}
- {...otherProps}
- >
- {children}
- </div>
- );
-}
-
-const Card = React.forwardRef(_Card);
-
-export default Card;
diff --git a/FrontEnd/src/views/common/ImageCropper.css b/FrontEnd/src/views/common/ImageCropper.css
deleted file mode 100644
index 2c4d0a8c..00000000
--- a/FrontEnd/src/views/common/ImageCropper.css
+++ /dev/null
@@ -1,38 +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, 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
deleted file mode 100644
index 04e17415..00000000
--- a/FrontEnd/src/views/common/ImageCropper.tsx
+++ /dev/null
@@ -1,306 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-
-import { UiLogicError } from "@/common";
-
-import "./ImageCropper.css";
-
-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"
- 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"
- 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/views/common/LoadFailReload.tsx b/FrontEnd/src/views/common/LoadFailReload.tsx
deleted file mode 100644
index 81ba1f67..00000000
--- a/FrontEnd/src/views/common/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<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/views/common/LoadingPage.tsx b/FrontEnd/src/views/common/LoadingPage.tsx
deleted file mode 100644
index 35ee1aa8..00000000
--- a/FrontEnd/src/views/common/LoadingPage.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as React from "react";
-
-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 />
- </div>
- );
-};
-
-export default LoadingPage;
diff --git a/FrontEnd/src/views/common/SearchInput.css b/FrontEnd/src/views/common/SearchInput.css
deleted file mode 100644
index f0503016..00000000
--- a/FrontEnd/src/views/common/SearchInput.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.cru-search-input {
- display: flex;
- flex-wrap: wrap;
-}
-
-.cru-search-input-input {
- width: 100%;
-}
diff --git a/FrontEnd/src/views/common/SearchInput.tsx b/FrontEnd/src/views/common/SearchInput.tsx
deleted file mode 100644
index 9d644ab7..00000000
--- a/FrontEnd/src/views/common/SearchInput.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { useCallback } from "react";
-import * as React from "react";
-import classnames from "classnames";
-import { useTranslation } from "react-i18next";
-
-import LoadingButton from "./button/LoadingButton";
-
-import "./SearchInput.css";
-
-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 (
- <div
- className={classnames(
- "cru-search-input",
- alwaysOneline ? "flex-nowrap" : "flex-sm-nowrap",
- props.className
- )}
- >
- <input
- type="text"
- className="cru-search-input-input 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"
- )}
- >
- <LoadingButton loading={props.loading} onClick={props.onButtonClick}>
- {props.buttonText ?? t("search")}
- </LoadingButton>
- </div>
- </div>
- );
-};
-
-export default SearchInput;
diff --git a/FrontEnd/src/views/common/Skeleton.css b/FrontEnd/src/views/common/Skeleton.css
deleted file mode 100644
index db1a1c34..00000000
--- a/FrontEnd/src/views/common/Skeleton.css
+++ /dev/null
@@ -1,14 +0,0 @@
-.cru-skeleton {
- padding: 0 1em;
-}
-
-.cru-skeleton-line {
- height: 1em;
- background-color: #e6e6e6;
- margin: 0.7em 0;
- border-radius: 0.2em;
-}
-
-.cru-skeleton-line.last {
- width: 50%;
-}
diff --git a/FrontEnd/src/views/common/Skeleton.tsx b/FrontEnd/src/views/common/Skeleton.tsx
deleted file mode 100644
index 3b149db9..00000000
--- a/FrontEnd/src/views/common/Skeleton.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-import range from "lodash/range";
-
-import "./Skeleton.css";
-
-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/views/common/Spinner.css b/FrontEnd/src/views/common/Spinner.css
deleted file mode 100644
index a1de68d2..00000000
--- a/FrontEnd/src/views/common/Spinner.css
+++ /dev/null
@@ -1,13 +0,0 @@
-@keyframes cru-spinner-animation {
- from {
- transform: scale(0,0);
- }
-}
-
-.cru-spinner {
- display: inline-block;
- animation: cru-spinner-animation 0.5s infinite alternate;
- background-color: currentColor;
- border-radius: 50%;
- transform-origin: center;
-}
diff --git a/FrontEnd/src/views/common/Spinner.tsx b/FrontEnd/src/views/common/Spinner.tsx
deleted file mode 100644
index e99a9d1b..00000000
--- a/FrontEnd/src/views/common/Spinner.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-
-import { PaletteColorType } from "@/palette";
-
-import "./Spinner.css";
-
-export interface SpinnerProps {
- size?: "sm" | "md" | "lg" | number | string;
- color?: PaletteColorType;
- className?: string;
- style?: React.CSSProperties;
-}
-
-export default function Spinner(
- props: SpinnerProps
-): React.ReactElement | null {
- const { size, color, className, style } = props;
- const calculatedSize =
- size === "sm"
- ? "18px"
- : size === "md"
- ? "30px"
- : size === "lg"
- ? "42px"
- : typeof size === "number"
- ? size
- : size == null
- ? "20px"
- : size;
- const calculatedColor = color ?? "primary";
-
- return (
- <span
- className={classnames(
- "cru-spinner",
- `cru-color-${calculatedColor}`,
- className
- )}
- style={{ width: calculatedSize, height: calculatedSize, ...style }}
- />
- );
-}
diff --git a/FrontEnd/src/views/common/TimelineLogo.tsx b/FrontEnd/src/views/common/TimelineLogo.tsx
deleted file mode 100644
index e06ed0f5..00000000
--- a/FrontEnd/src/views/common/TimelineLogo.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { SVGAttributes } from "react";
-import * as React 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/views/common/alert/AlertHost.tsx b/FrontEnd/src/views/common/alert/AlertHost.tsx
deleted file mode 100644
index 42074781..00000000
--- a/FrontEnd/src/views/common/alert/AlertHost.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import * as React from "react";
-import without from "lodash/without";
-import { useTranslation } from "react-i18next";
-import classNames from "classnames";
-
-import { alertService, AlertInfoEx, AlertInfo } from "@/services/alert";
-import { convertI18nText } from "@/common";
-
-import IconButton from "../button/IconButton";
-
-import "./alert.css";
-
-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 (
- <div
- className={classNames(
- "m-3 cru-alert",
- "cru-" + (alert.type ?? "primary")
- )}
- onClick={cancelTimer}
- >
- <div className="cru-alert-content">
- {(() => {
- const { message, customMessage } = alert;
- if (customMessage != null) {
- return customMessage;
- } else {
- return convertI18nText(message, t);
- }
- })()}
- </div>
- <div className="cru-alert-close-button-container">
- <IconButton
- icon="x"
- className="cru-alert-close-button"
- onClick={close}
- />
- </div>
- </div>
- );
-};
-
-const AlertHost: React.FC = () => {
- const [alerts, setAlerts] = React.useState<AlertInfoEx[]>([]);
-
- React.useEffect(() => {
- const consume = (alert: AlertInfoEx): void => {
- setAlerts((old) => [...old, alert]);
- };
-
- alertService.registerConsumer(consume);
- return () => {
- alertService.unregisterConsumer(consume);
- };
- }, []);
-
- return (
- <div 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/views/common/alert/alert.css b/FrontEnd/src/views/common/alert/alert.css
deleted file mode 100644
index fc15e3cb..00000000
--- a/FrontEnd/src/views/common/alert/alert.css
+++ /dev/null
@@ -1,33 +0,0 @@
-.alert-container {
- position: fixed;
- z-index: 1040;
-}
-
-.cru-alert {
- border-radius: 5px;
- border: var(--cru-theme-color) 1px solid;
- color: var(--cru-theme-t-color);
- background-color: var(--cru-theme-r1-color);
-
- display: flex;
- overflow: hidden;
-}
-
-.cru-alert-content {
- padding: 0.5em 2em;
-}
-
-.cru-alert-close-button-container {
- flex-shrink: 0;
- margin-left: auto;
- width: 2em;
- text-align: center;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: var(--cru-theme-t-color);
-}
-
-.cru-alert-close-button {
- color: var(--cru-theme-color);
-}
diff --git a/FrontEnd/src/views/common/button/Button.css b/FrontEnd/src/views/common/button/Button.css
deleted file mode 100644
index c34176f6..00000000
--- a/FrontEnd/src/views/common/button/Button.css
+++ /dev/null
@@ -1,51 +0,0 @@
-.cru-button:not(.outline) {
- color: var(--cru-theme-t-color);
- cursor: pointer;
- padding: 0.2em 0.5em;
- border-radius: 0.2em;
- border: none;
- transition: all 0.5s;
- background-color: var(--cru-theme-color);
-}
-
-.cru-button:not(.outline):hover {
- background-color: var(--cru-theme-f1-color);
-}
-
-.cru-button:not(.outline):active {
- background-color: var(--cru-theme-f2-color);
-}
-
-.cru-button:not(.outline):disabled {
- background-color: var(--cru-disable-color);
- cursor: auto;
-}
-
-.cru-button.outline {
- color: var(--cru-theme-color);
- border: var(--cru-theme-color) 1px solid;
- cursor: pointer;
- padding: 0.2em 0.5em;
- border-radius: 0.2em;
- transition: all 0.6s;
- background-color: white;
-}
-
-.cru-button.outline:hover {
- color: var(--cru-theme-f1-color);
- border-color: var(--cru-theme-f1-color);
- background-color: var(--cru-background-color);
-}
-
-.cru-button.outline:active {
- color: var(--cru-theme-f2-color);
- border-color: var(--cru-theme-f2-color);
- background-color: var(--cru-background-1-color);
-}
-
-.cru-button.outline:disabled {
- color: var(--cru-disable-color);
- border-color: var(--cru-disable-color);
- background-color: white;
- cursor: auto;
-}
diff --git a/FrontEnd/src/views/common/button/Button.tsx b/FrontEnd/src/views/common/button/Button.tsx
deleted file mode 100644
index be605328..00000000
--- a/FrontEnd/src/views/common/button/Button.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { ComponentPropsWithoutRef, Ref } from "react";
-import classNames from "classnames";
-
-import { I18nText, useC } from "@/common";
-import { PaletteColorType } from "@/palette";
-
-import "./Button.css";
-
-interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
- color?: PaletteColorType;
- text?: I18nText;
- outline?: boolean;
- buttonRef?: Ref<HTMLButtonElement> | null;
-}
-
-export default function Button(props: ButtonProps) {
- const {
- buttonRef,
- color,
- text,
- outline,
- className,
- children,
- ...otherProps
- } = props;
-
- if (text != null && children != null) {
- console.warn("You can't set both text and children props.");
- }
-
- const c = useC();
-
- return (
- <button
- ref={buttonRef}
- className={classNames(
- "cru-" + (color ?? "primary"),
- "cru-button",
- outline && "outline",
- className,
- )}
- {...otherProps}
- >
- {text != null ? c(text) : children}
- </button>
- );
-}
diff --git a/FrontEnd/src/views/common/button/FlatButton.css b/FrontEnd/src/views/common/button/FlatButton.css
deleted file mode 100644
index f0d33153..00000000
--- a/FrontEnd/src/views/common/button/FlatButton.css
+++ /dev/null
@@ -1,18 +0,0 @@
-.cru-flat-button {
- cursor: pointer;
- padding: 0.2em 0.5em;
- border-radius: 0.2em;
- border: none;
- background-color: transparent;
- transition: all 0.6s;
- color: var(--cru-theme-color);
-}
-
-.cru-flat-button.disabled {
- color: var(--cru-theme-l1-color);
- cursor: default;
-}
-
-.cru-flat-button:hover:not(.disabled) {
- background-color: #e9ecef;
-}
diff --git a/FrontEnd/src/views/common/button/FlatButton.tsx b/FrontEnd/src/views/common/button/FlatButton.tsx
deleted file mode 100644
index 49912b68..00000000
--- a/FrontEnd/src/views/common/button/FlatButton.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { ComponentPropsWithoutRef, Ref } from "react";
-import classNames from "classnames";
-
-import { I18nText, useC } from "@/common";
-import { PaletteColorType } from "@/palette";
-
-import "./FlatButton.css";
-
-interface FlatButtonProps extends ComponentPropsWithoutRef<"button"> {
- color?: PaletteColorType;
- text?: I18nText;
- buttonRef?: Ref<HTMLButtonElement> | null;
-}
-
-export default function FlatButton(props: FlatButtonProps) {
- const { color, text, className, children, buttonRef, ...otherProps } = props;
-
- if (text != null && children != null) {
- console.warn("You can't set both text and children props.");
- }
-
- const c = useC();
-
- return (
- <button
- ref={buttonRef}
- className={classNames(
- "cru-" + (color ?? "primary"),
- "cru-flat-button",
- className,
- )}
- {...otherProps}
- >
- {text != null ? c(text) : children}
- </button>
- );
-}
diff --git a/FrontEnd/src/views/common/button/IconButton.css b/FrontEnd/src/views/common/button/IconButton.css
deleted file mode 100644
index 45fb103c..00000000
--- a/FrontEnd/src/views/common/button/IconButton.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.cru-icon-button {
- color: var(--cru-theme-color);
- font-size: 1.4rem;
- background: none;
- border: none;
-}
-
-.cru-icon-button.large {
- font-size: 1.6rem;
-}
diff --git a/FrontEnd/src/views/common/button/IconButton.tsx b/FrontEnd/src/views/common/button/IconButton.tsx
deleted file mode 100644
index 652a8b09..00000000
--- a/FrontEnd/src/views/common/button/IconButton.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { ComponentPropsWithoutRef } from "react";
-import classNames from "classnames";
-
-import { PaletteColorType } from "@/palette";
-
-import "./IconButton.css";
-
-interface IconButtonProps extends ComponentPropsWithoutRef<"i"> {
- icon: string;
- color?: PaletteColorType;
- large?: boolean;
-}
-
-export default function IconButton(props: IconButtonProps) {
- const { icon, color, className, large, ...otherProps } = props;
-
- return (
- <button
- className={classNames(
- "cru-icon-button",
- large && "large",
- "bi-" + icon,
- color ? "cru-" + color : "cru-primary",
- className,
- )}
- {...otherProps}
- />
- );
-}
diff --git a/FrontEnd/src/views/common/button/LoadingButton.tsx b/FrontEnd/src/views/common/button/LoadingButton.tsx
deleted file mode 100644
index fceaec27..00000000
--- a/FrontEnd/src/views/common/button/LoadingButton.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import * as React from "react";
-import classNames from "classnames";
-import { useTranslation } from "react-i18next";
-
-import { convertI18nText, I18nText } from "@/common";
-import { PaletteColorType } from "@/palette";
-
-import Spinner from "../Spinner";
-
-interface LoadingButtonProps extends React.ComponentPropsWithoutRef<"button"> {
- color?: PaletteColorType;
- text?: I18nText;
- loading?: boolean;
-}
-
-function LoadingButton(props: LoadingButtonProps): JSX.Element {
- const { t } = useTranslation();
-
- const { color, text, loading, className, children, ...otherProps } = props;
-
- if (text != null && children != null) {
- console.warn("You can't set both text and children props.");
- }
-
- return (
- <button
- className={classNames(
- "cru-" + (color ?? "primary"),
- "cru-button outline",
- className,
- )}
- {...otherProps}
- >
- {text != null ? convertI18nText(text, t) : children}
- {loading && <Spinner />}
- </button>
- );
-}
-
-export default LoadingButton;
diff --git a/FrontEnd/src/views/common/button/index.tsx b/FrontEnd/src/views/common/button/index.tsx
deleted file mode 100644
index cff5ba3f..00000000
--- a/FrontEnd/src/views/common/button/index.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import Button from "./Button";
-import FlatButton from "./FlatButton";
-import IconButton from "./IconButton";
-import LoadingButton from "./LoadingButton";
-
-export { Button, FlatButton, IconButton, LoadingButton };
diff --git a/FrontEnd/src/views/common/dialog/ConfirmDialog.tsx b/FrontEnd/src/views/common/dialog/ConfirmDialog.tsx
deleted file mode 100644
index 8c2cea5a..00000000
--- a/FrontEnd/src/views/common/dialog/ConfirmDialog.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { convertI18nText, I18nText } from "@/common";
-import * as 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="cru-color-danger">{convertI18nText(title, t)}</h3>
- <hr />
- <p>{convertI18nText(body, t)}</p>
- <hr />
- <div className="cru-dialog-bottom-area">
- <Button
- text="operationDialog.cancel"
- color="secondary"
- outline
- onClick={onClose}
- />
- <Button
- text="operationDialog.confirm"
- color="danger"
- onClick={() => {
- onConfirm();
- onClose();
- }}
- />
- </div>
- </Dialog>
- );
-};
-
-export default ConfirmDialog;
diff --git a/FrontEnd/src/views/common/dialog/Dialog.css b/FrontEnd/src/views/common/dialog/Dialog.css
deleted file mode 100644
index 21ea52fc..00000000
--- a/FrontEnd/src/views/common/dialog/Dialog.css
+++ /dev/null
@@ -1,55 +0,0 @@
-.cru-dialog-overlay {
- position: fixed;
- z-index: 1040;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(255, 255, 255, 0.92);
-
- display: flex;
- padding: 2em;
-
- overflow: auto;
-}
-
-.cru-dialog-container {
- max-width: 100%;
- min-width: 30vw;
-
- margin: auto;
-
- border: var(--cru-primary-color) 1px solid;
- border-radius: 5px;
- padding: 1.5em;
- background-color: white;
-}
-
-.cru-dialog-bottom-area {
- display: flex;
- justify-content: flex-end;
-}
-
-.cru-dialog-bottom-area > * {
- margin: 0 0.5em;
-}
-
-.cru-dialog-enter .cru-dialog-container {
- transform: scale(0, 0);
- opacity: 0;
- transform-origin: center;
-}
-
-.cru-dialog-enter-active .cru-dialog-container {
- transform: scale(1, 1);
- opacity: 1;
- transition: transform 0.3s, opacity 0.3s;
- transform-origin: center;
-}
-
-.cru-dialog-exit-active .cru-dialog-container {
- transition: transform 0.3s, opacity 0.3s;
- transform: scale(0, 0);
- opacity: 0;
- transform-origin: center;
-}
diff --git a/FrontEnd/src/views/common/dialog/Dialog.tsx b/FrontEnd/src/views/common/dialog/Dialog.tsx
deleted file mode 100644
index 923c636b..00000000
--- a/FrontEnd/src/views/common/dialog/Dialog.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { ReactNode } from "react";
-import ReactDOM from "react-dom";
-import { CSSTransition } from "react-transition-group";
-
-import "./Dialog.css";
-
-const optionalPortalElement = document.getElementById("portal");
-if (optionalPortalElement == null) {
- throw new Error("Portal element not found");
-}
-const portalElement = optionalPortalElement;
-
-interface DialogProps {
- onClose: () => void;
- open: boolean;
- children?: ReactNode;
- disableCloseOnClickOnOverlay?: boolean;
-}
-
-export default function Dialog(props: DialogProps) {
- const { open, onClose, children, disableCloseOnClickOnOverlay } = props;
-
- return ReactDOM.createPortal(
- <CSSTransition
- mountOnEnter
- unmountOnExit
- in={open}
- timeout={300}
- classNames="cru-dialog"
- >
- <div
- className="cru-dialog-overlay"
- onPointerDown={
- disableCloseOnClickOnOverlay
- ? undefined
- : () => {
- onClose();
- }
- }
- >
- <div
- className="cru-dialog-container"
- onPointerDown={(e) => e.stopPropagation()}
- >
- {children}
- </div>
- </div>
- </CSSTransition>,
- portalElement,
- );
-}
diff --git a/FrontEnd/src/views/common/dialog/FullPageDialog.css b/FrontEnd/src/views/common/dialog/FullPageDialog.css
deleted file mode 100644
index 2f1fc636..00000000
--- a/FrontEnd/src/views/common/dialog/FullPageDialog.css
+++ /dev/null
@@ -1,44 +0,0 @@
-.cru-full-page {
- position: fixed;
- z-index: 1030;
- 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(--cru-primary-color);
- display: flex;
- align-items: center;
-}
-
-.cru-full-page-content-container {
- overflow: scroll;
-}
-
-.cru-full-page-back-button {
- color: var(--cru-primary-t-color);
-}
-
-.cru-full-page-enter {
- transform: translate(100%, 0);
-}
-
-.cru-full-page-enter-active {
- transform: none;
- transition: transform 0.3s;
-}
-
-.cru-full-page-exit-active {
- transition: transform 0.3s;
- transform: translate(100%, 0);
-}
diff --git a/FrontEnd/src/views/common/dialog/FullPageDialog.tsx b/FrontEnd/src/views/common/dialog/FullPageDialog.tsx
deleted file mode 100644
index 6368fc0a..00000000
--- a/FrontEnd/src/views/common/dialog/FullPageDialog.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import * as React from "react";
-import { createPortal } from "react-dom";
-import classnames from "classnames";
-import { CSSTransition } from "react-transition-group";
-
-import "./FullPageDialog.css";
-import IconButton from "../button/IconButton";
-
-export interface FullPageDialogProps {
- show: boolean;
- onBack: () => void;
- contentContainerClassName?: string;
- children: React.ReactNode;
-}
-
-const FullPageDialog: React.FC<FullPageDialogProps> = ({
- show,
- onBack,
- children,
- contentContainerClassName,
-}) => {
- return createPortal(
- <CSSTransition
- mountOnEnter
- unmountOnExit
- in={show}
- timeout={300}
- classNames="cru-full-page"
- >
- <div className="cru-full-page">
- <div className="cru-full-page-top-bar">
- <IconButton
- icon="arrow-left"
- className="ms-3 cru-full-page-back-button"
- onClick={onBack}
- />
- </div>
- <div
- className={classnames(
- "cru-full-page-content-container",
- contentContainerClassName
- )}
- >
- {children}
- </div>
- </div>
- </CSSTransition>,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- document.getElementById("portal")!
- );
-};
-
-export default FullPageDialog;
diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.css b/FrontEnd/src/views/common/dialog/OperationDialog.css
deleted file mode 100644
index 2f7617d0..00000000
--- a/FrontEnd/src/views/common/dialog/OperationDialog.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.cru-operation-dialog-group {
- display: block;
- margin: 0.4em 0;
-}
-
-.cru-operation-dialog-label {
- display: block;
- color: var(--cru-primary-color);
-}
-
-.cru-operation-dialog-inline-label {
- margin-inline-start: 0.5em;
-}
-
-.cru-operation-dialog-error-text {
- display: block;
- font-size: 0.8em;
- color: var(--cru-danger-color);
-}
-
-.cru-operation-dialog-helper-text {
- display: block;
- font-size: 0.8em;
- color: var(--cru-primary-color);
-}
diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.tsx b/FrontEnd/src/views/common/dialog/OperationDialog.tsx
deleted file mode 100644
index 71be030a..00000000
--- a/FrontEnd/src/views/common/dialog/OperationDialog.tsx
+++ /dev/null
@@ -1,531 +0,0 @@
-import { useState } from "react";
-import * as React from "react";
-import { useTranslation } from "react-i18next";
-import { TwitterPicker } from "react-color";
-import classNames from "classnames";
-import moment from "moment";
-
-import { convertI18nText, I18nText, UiLogicError } from "@/common";
-
-import { PaletteColorType } from "@/palette";
-
-import Button from "../button/Button";
-import LoadingButton from "../button/LoadingButton";
-import Dialog from "./Dialog";
-
-import "./OperationDialog.css";
-
-interface DefaultErrorPromptProps {
- error?: string;
-}
-
-const DefaultErrorPrompt: React.FC<DefaultErrorPromptProps> = (props) => {
- const { t } = useTranslation();
-
- let result = <p className="cru-color-danger">{t("operationDialog.error")}</p>;
-
- if (props.error != null) {
- result = (
- <>
- {result}
- <p className="cru-color-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"
- >;
- helperText?: string;
-}
-
-export interface OperationDialogBoolInput {
- type: "bool";
- label: I18nText;
- initValue?: boolean;
- helperText?: string;
-}
-
-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;
- helperText?: 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;
- onClose: () => void;
- title: I18nText | (() => React.ReactNode);
- themeColor?: PaletteColorType;
- 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.onClose();
- 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 = (
- <>
- <div>
- {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 (
- <div
- key={index}
- className={classNames(
- "cru-operation-dialog-group",
- error != null ? "error" : null
- )}
- >
- {item.label && (
- <label className="cru-operation-dialog-label">
- {convertI18nText(item.label, t)}
- </label>
- )}
- <input
- type={item.password === true ? "password" : "text"}
- value={value as string}
- onChange={(e) => {
- const v = e.target.value;
- updateValue(index, v);
- }}
- disabled={process}
- />
- {error != null && (
- <div className="cru-operation-dialog-error-text">
- {error}
- </div>
- )}
- {item.helperText && (
- <div className="cru-operation-dialog-helper-text">
- {t(item.helperText)}
- </div>
- )}
- </div>
- );
- } else if (item.type === "bool") {
- return (
- <div
- key={index}
- className={classNames(
- "cru-operation-dialog-group",
- error != null ? "error" : null
- )}
- >
- <input
- type="checkbox"
- checked={value as boolean}
- onChange={(event) => {
- updateValue(index, event.currentTarget.checked);
- }}
- disabled={process}
- />
- <label className="cru-operation-dialog-inline-label">
- {convertI18nText(item.label, t)}
- </label>
- {error != null && (
- <div className="cru-operation-dialog-error-text">
- {error}
- </div>
- )}
- {item.helperText && (
- <div className="cru-operation-dialog-helper-text">
- {t(item.helperText)}
- </div>
- )}
- </div>
- );
- } else if (item.type === "select") {
- return (
- <div
- key={index}
- className={classNames(
- "cru-operation-dialog-group",
- error != null ? "error" : null
- )}
- >
- <label className="cru-operation-dialog-label">
- {convertI18nText(item.label, t)}
- </label>
- <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>
- );
- })}
- </select>
- </div>
- );
- } else if (item.type === "color") {
- return (
- <div
- key={index}
- className={classNames(
- "cru-operation-dialog-group",
- error != null ? "error" : null
- )}
- >
- {item.canBeNull ? (
- <input
- type="checkbox"
- checked={value !== null}
- onChange={(event) => {
- if (event.currentTarget.checked) {
- updateValue(index, "#007bff");
- } else {
- updateValue(index, null);
- }
- }}
- disabled={process}
- />
- ) : null}
- <label className="cru-operation-dialog-inline-label">
- {convertI18nText(item.label, t)}
- </label>
- {value !== null && (
- <TwitterPicker
- color={value as string}
- triangle="hide"
- onChange={(result) => updateValue(index, result.hex)}
- />
- )}
- </div>
- );
- } else if (item.type === "datetime") {
- return (
- <div
- key={index}
- className={classNames(
- "cru-operation-dialog-group",
- error != null ? "error" : null
- )}
- >
- {item.label && (
- <label className="cru-operation-dialog-label">
- {convertI18nText(item.label, t)}
- </label>
- )}
- <input
- type="datetime-local"
- value={value as string}
- onChange={(e) => {
- const v = e.target.value;
- updateValue(index, v);
- }}
- disabled={process}
- />
- {error != null && <div>{error}</div>}
- </div>
- );
- }
- })}
- </div>
- <hr />
- <div className="cru-dialog-bottom-area">
- <Button
- text="operationDialog.cancel"
- color="secondary"
- outline
- onClick={close}
- disabled={process}
- />
- <LoadingButton
- color={props.themeColor}
- loading={process}
- disabled={!canProcess}
- onClick={() => {
- setDirtyList(inputScheme.map(() => true));
- if (validate(values)) {
- onConfirm();
- }
- }}
- >
- {t("operationDialog.confirm")}
- </LoadingButton>
- </div>
- </>
- );
- } 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="cru-color-success">{content}</p>;
- } else {
- content = props.failurePrompt?.(result.data) ?? <DefaultErrorPrompt />;
- if (typeof content === "string")
- content = <DefaultErrorPrompt error={content} />;
- }
- body = (
- <>
- <div>{content}</div>
- <hr />
- <div className="cru-dialog-bottom-area">
- <Button text="operationDialog.ok" color="primary" onClick={close} />
- </div>
- </>
- );
- }
-
- const title =
- typeof props.title === "function"
- ? props.title()
- : convertI18nText(props.title, t);
-
- return (
- <Dialog open={props.open} onClose={close}>
- <h3
- className={
- props.themeColor != null
- ? "cru-color-" + props.themeColor
- : "cru-color-primary"
- }
- >
- {title}
- </h3>
- <hr />
- {body}
- </Dialog>
- );
-};
-
-export default OperationDialog;
diff --git a/FrontEnd/src/views/common/index.css b/FrontEnd/src/views/common/index.css
deleted file mode 100644
index 111a3ec0..00000000
--- a/FrontEnd/src/views/common/index.css
+++ /dev/null
@@ -1,293 +0,0 @@
-:root {
- --cru-background-color: #f8f9fa;
- --cru-background-1-color: #e9ecef;
- --cru-background-2-color: #dee2e6;
-
- --cru-disable-color: #ced4da;
-
- /*
- --cru-primary-color: rgb(0, 123, 255);
- --cru-primary-l1-color: rgb(26, 136, 255);
- --cru-primary-l2-color: rgb(51, 149, 255);
- --cru-primary-l3-color: rgb(77, 163, 255);
- --cru-primary-d1-color: rgb(0, 111, 230);
- --cru-primary-d2-color: rgb(0, 98, 204);
- --cru-primary-d3-color: rgb(0, 86, 179);
- --cru-primary-f1-color: rgb(0, 111, 230);
- --cru-primary-f2-color: rgb(0, 98, 204);
- --cru-primary-f3-color: rgb(0, 86, 179);
- --cru-primary-r1-color: rgb(26, 136, 255);
- --cru-primary-r2-color: rgb(51, 149, 255);
- --cru-primary-r3-color: rgb(77, 163, 255);
- --cru-primary-t-color: rgb(255, 255, 255);
- --cru-primary-t1-color: rgb(230, 230, 230);
- --cru-primary-t2-color: rgb(204, 204, 204);
- --cru-primary-t3-color: rgb(179, 179, 179);
- --cru-primary-enhance-color: rgb(77, 163, 255);
- --cru-primary-enhance-l1-color: rgb(94, 172, 255);
- --cru-primary-enhance-l2-color: rgb(112, 181, 255);
- --cru-primary-enhance-l3-color: rgb(130, 190, 255);
- --cru-primary-enhance-d1-color: rgb(43, 145, 255);
- --cru-primary-enhance-d2-color: rgb(10, 128, 255);
- --cru-primary-enhance-d3-color: rgb(0, 112, 232);
- --cru-primary-enhance-f1-color: rgb(94, 172, 255);
- --cru-primary-enhance-f2-color: rgb(112, 181, 255);
- --cru-primary-enhance-f3-color: rgb(130, 190, 255);
- --cru-primary-enhance-r1-color: rgb(43, 145, 255);
- --cru-primary-enhance-r2-color: rgb(10, 128, 255);
- --cru-primary-enhance-r3-color: rgb(0, 112, 232);
- --cru-primary-enhance-t-color: rgb(0, 0, 0);
- --cru-primary-enhance-t1-color: rgb(26, 26, 26);
- --cru-primary-enhance-t2-color: rgb(51, 51, 51);
- --cru-primary-enhance-t3-color: rgb(77, 77, 77);
- --cru-secondary-color: rgb(128, 128, 128);
- --cru-secondary-l1-color: rgb(141, 141, 141);
- --cru-secondary-l2-color: rgb(153, 153, 153);
- --cru-secondary-l3-color: rgb(166, 166, 166);
- --cru-secondary-d1-color: rgb(115, 115, 115);
- --cru-secondary-d2-color: rgb(102, 102, 102);
- --cru-secondary-d3-color: rgb(90, 90, 90);
- --cru-secondary-f1-color: rgb(115, 115, 115);
- --cru-secondary-f2-color: rgb(102, 102, 102);
- --cru-secondary-f3-color: rgb(90, 90, 90);
- --cru-secondary-r1-color: rgb(141, 141, 141);
- --cru-secondary-r2-color: rgb(153, 153, 153);
- --cru-secondary-r3-color: rgb(166, 166, 166);
- --cru-secondary-t-color: rgb(255, 255, 255);
- --cru-secondary-t1-color: rgb(230, 230, 230);
- --cru-secondary-t2-color: rgb(204, 204, 204);
- --cru-secondary-t3-color: rgb(179, 179, 179);
- --cru-danger-color: rgb(255, 0, 0);
- --cru-danger-l1-color: rgb(255, 26, 26);
- --cru-danger-l2-color: rgb(255, 51, 51);
- --cru-danger-l3-color: rgb(255, 77, 77);
- --cru-danger-d1-color: rgb(230, 0, 0);
- --cru-danger-d2-color: rgb(204, 0, 0);
- --cru-danger-d3-color: rgb(179, 0, 0);
- --cru-danger-f1-color: rgb(230, 0, 0);
- --cru-danger-f2-color: rgb(204, 0, 0);
- --cru-danger-f3-color: rgb(179, 0, 0);
- --cru-danger-r1-color: rgb(255, 26, 26);
- --cru-danger-r2-color: rgb(255, 51, 51);
- --cru-danger-r3-color: rgb(255, 77, 77);
- --cru-danger-t-color: rgb(255, 255, 255);
- --cru-danger-t1-color: rgb(230, 230, 230);
- --cru-danger-t2-color: rgb(204, 204, 204);
- --cru-danger-t3-color: rgb(179, 179, 179);
- --cru-success-color: rgb(0, 128, 0);
- --cru-success-l1-color: rgb(0, 166, 0);
- --cru-success-l2-color: rgb(0, 204, 0);
- --cru-success-l3-color: rgb(0, 243, 0);
- --cru-success-d1-color: rgb(0, 115, 0);
- --cru-success-d2-color: rgb(0, 102, 0);
- --cru-success-d3-color: rgb(0, 90, 0);
- --cru-success-f1-color: rgb(0, 115, 0);
- --cru-success-f2-color: rgb(0, 102, 0);
- --cru-success-f3-color: rgb(0, 90, 0);
- --cru-success-r1-color: rgb(0, 166, 0);
- --cru-success-r2-color: rgb(0, 204, 0);
- --cru-success-r3-color: rgb(0, 243, 0);
- --cru-success-t-color: rgb(255, 255, 255);
- --cru-success-t1-color: rgb(230, 230, 230);
- --cru-success-t2-color: rgb(204, 204, 204);
- --cru-success-t3-color: rgb(179, 179, 179);
- */
-}
-
-.cru-primary {
- --cru-theme-color: var(--cru-primary-color);
- --cru-theme-l1-color: var(--cru-primary-l1-color);
- --cru-theme-l2-color: var(--cru-primary-l2-color);
- --cru-theme-l3-color: var(--cru-primary-l3-color);
- --cru-theme-d1-color: var(--cru-primary-d1-color);
- --cru-theme-d2-color: var(--cru-primary-d2-color);
- --cru-theme-d3-color: var(--cru-primary-d3-color);
- --cru-theme-f1-color: var(--cru-primary-f1-color);
- --cru-theme-f2-color: var(--cru-primary-f2-color);
- --cru-theme-f3-color: var(--cru-primary-f3-color);
- --cru-theme-r1-color: var(--cru-primary-r1-color);
- --cru-theme-r2-color: var(--cru-primary-r2-color);
- --cru-theme-r3-color: var(--cru-primary-r3-color);
- --cru-theme-t-color: var(--cru-primary-t-color);
- --cru-theme-t1-color: var(--cru-primary-t1-color);
- --cru-theme-t2-color: var(--cru-primary-t2-color);
- --cru-theme-t3-color: var(--cru-primary-t3-color);
-}
-
-.cru-primary-enhance {
- --cru-theme-color: var(--cru-primary-enhance-color);
- --cru-theme-l1-color: var(--cru-primary-enhance-l1-color);
- --cru-theme-l2-color: var(--cru-primary-enhance-l2-color);
- --cru-theme-l3-color: var(--cru-primary-enhance-l3-color);
- --cru-theme-d1-color: var(--cru-primary-enhance-d1-color);
- --cru-theme-d2-color: var(--cru-primary-enhance-d2-color);
- --cru-theme-d3-color: var(--cru-primary-enhance-d3-color);
- --cru-theme-f1-color: var(--cru-primary-enhance-f1-color);
- --cru-theme-f2-color: var(--cru-primary-enhance-f2-color);
- --cru-theme-f3-color: var(--cru-primary-enhance-f3-color);
- --cru-theme-r1-color: var(--cru-primary-enhance-r1-color);
- --cru-theme-r2-color: var(--cru-primary-enhance-r2-color);
- --cru-theme-r3-color: var(--cru-primary-enhance-r3-color);
- --cru-theme-t-color: var(--cru-primary-enhance-t-color);
- --cru-theme-t1-color: var(--cru-primary-enhance-t1-color);
- --cru-theme-t2-color: var(--cru-primary-enhance-t2-color);
- --cru-theme-t3-color: var(--cru-primary-enhance-t3-color);
-}
-
-.cru-secondary {
- --cru-theme-color: var(--cru-secondary-color);
- --cru-theme-l1-color: var(--cru-secondary-l1-color);
- --cru-theme-l2-color: var(--cru-secondary-l2-color);
- --cru-theme-l3-color: var(--cru-secondary-l3-color);
- --cru-theme-d1-color: var(--cru-secondary-d1-color);
- --cru-theme-d2-color: var(--cru-secondary-d2-color);
- --cru-theme-d3-color: var(--cru-secondary-d3-color);
- --cru-theme-f1-color: var(--cru-secondary-f1-color);
- --cru-theme-f2-color: var(--cru-secondary-f2-color);
- --cru-theme-f3-color: var(--cru-secondary-f3-color);
- --cru-theme-r1-color: var(--cru-secondary-r1-color);
- --cru-theme-r2-color: var(--cru-secondary-r2-color);
- --cru-theme-r3-color: var(--cru-secondary-r3-color);
- --cru-theme-t-color: var(--cru-secondary-t-color);
- --cru-theme-t1-color: var(--cru-secondary-t1-color);
- --cru-theme-t2-color: var(--cru-secondary-t2-color);
- --cru-theme-t3-color: var(--cru-secondary-t3-color);
-}
-
-.cru-success {
- --cru-theme-color: var(--cru-success-color);
- --cru-theme-l1-color: var(--cru-success-l1-color);
- --cru-theme-l2-color: var(--cru-success-l2-color);
- --cru-theme-l3-color: var(--cru-success-l3-color);
- --cru-theme-d1-color: var(--cru-success-d1-color);
- --cru-theme-d2-color: var(--cru-success-d2-color);
- --cru-theme-d3-color: var(--cru-success-d3-color);
- --cru-theme-f1-color: var(--cru-success-f1-color);
- --cru-theme-f2-color: var(--cru-success-f2-color);
- --cru-theme-f3-color: var(--cru-success-f3-color);
- --cru-theme-r1-color: var(--cru-success-r1-color);
- --cru-theme-r2-color: var(--cru-success-r2-color);
- --cru-theme-r3-color: var(--cru-success-r3-color);
- --cru-theme-t-color: var(--cru-success-t-color);
- --cru-theme-t1-color: var(--cru-success-t1-color);
- --cru-theme-t2-color: var(--cru-success-t2-color);
- --cru-theme-t3-color: var(--cru-success-t3-color);
-}
-
-.cru-danger {
- --cru-theme-color: var(--cru-danger-color);
- --cru-theme-l1-color: var(--cru-danger-l1-color);
- --cru-theme-l2-color: var(--cru-danger-l2-color);
- --cru-theme-l3-color: var(--cru-danger-l3-color);
- --cru-theme-d1-color: var(--cru-danger-d1-color);
- --cru-theme-d2-color: var(--cru-danger-d2-color);
- --cru-theme-d3-color: var(--cru-danger-d3-color);
- --cru-theme-f1-color: var(--cru-danger-f1-color);
- --cru-theme-f2-color: var(--cru-danger-f2-color);
- --cru-theme-f3-color: var(--cru-danger-f3-color);
- --cru-theme-r1-color: var(--cru-danger-r1-color);
- --cru-theme-r2-color: var(--cru-danger-r2-color);
- --cru-theme-r3-color: var(--cru-danger-r3-color);
- --cru-theme-t-color: var(--cru-danger-t-color);
- --cru-theme-t1-color: var(--cru-danger-t1-color);
- --cru-theme-t2-color: var(--cru-danger-t2-color);
- --cru-theme-t3-color: var(--cru-danger-t3-color);
-}
-
-.cru-color-primary {
- color: var(--cru-primary-color);
-}
-
-.cru-color-primary-enhance {
- color: var(--cru-primary-enhance-color);
-}
-
-.cru-color-secondary {
- color: var(--cru-secondary-color);
-}
-
-.cru-color-success {
- color: var(--cru-success-color);
-}
-
-.cru-color-danger {
- color: var(--cru-danger-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;
-}
-
-.alert-container {
- position: fixed;
- z-index: 1070;
-}
-
-@media (min-width: 576px) {
- .alert-container {
- bottom: 0;
- right: 0;
- }
-}
-
-@media (max-width: 575.98px) {
- .alert-container {
- bottom: 0;
- right: 0;
- left: 0;
- text-align: center;
- }
-} \ No newline at end of file
diff --git a/FrontEnd/src/views/common/input/InputPanel.css b/FrontEnd/src/views/common/input/InputPanel.css
deleted file mode 100644
index f9d6ac8b..00000000
--- a/FrontEnd/src/views/common/input/InputPanel.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.cru-input-panel-group {
- display: block;
- margin: 0.4em 0;
-}
-
-.cru-input-panel-label {
- display: block;
- color: var(--cru-primary-color);
-}
-
-.cru-input-panel-inline-label {
- margin-inline-start: 0.5em;
-}
-
-.cru-input-panel-error-text {
- display: block;
- font-size: 0.8em;
- color: var(--cru-danger-color);
-}
-
-.cru-input-panel-helper-text {
- display: block;
- font-size: 0.8em;
- color: var(--cru-primary-color);
-}
diff --git a/FrontEnd/src/views/common/input/InputPanel.tsx b/FrontEnd/src/views/common/input/InputPanel.tsx
deleted file mode 100644
index 234ed267..00000000
--- a/FrontEnd/src/views/common/input/InputPanel.tsx
+++ /dev/null
@@ -1,257 +0,0 @@
-import * as React from "react";
-import classNames from "classnames";
-import { useTranslation } from "react-i18next";
-import { TwitterPicker } from "react-color";
-
-import { convertI18nText, I18nText } from "@/common";
-
-import "./InputPanel.css";
-
-export interface TextInput {
- type: "text";
- label?: I18nText;
- helper?: I18nText;
- password?: boolean;
-}
-
-export interface BoolInput {
- type: "bool";
- label: I18nText;
- helper?: I18nText;
-}
-
-export interface SelectInputOption {
- value: string;
- label: I18nText;
- icon?: React.ReactElement;
-}
-
-export interface SelectInput {
- type: "select";
- label: I18nText;
- options: SelectInputOption[];
-}
-
-export interface ColorInput {
- type: "color";
- label?: I18nText;
-}
-
-export interface DateTimeInput {
- type: "datetime";
- label?: I18nText;
- helper?: I18nText;
-}
-
-export type Input =
- | TextInput
- | BoolInput
- | SelectInput
- | ColorInput
- | DateTimeInput;
-
-interface InputTypeToValueTypeMap {
- text: string;
- bool: boolean;
- select: string;
- color: string;
- datetime: string;
-}
-
-type ValueTypes = InputTypeToValueTypeMap[keyof InputTypeToValueTypeMap];
-
-type MapInputTypeToValueType<Type> = Type extends keyof InputTypeToValueTypeMap
- ? InputTypeToValueTypeMap[Type]
- : never;
-
-type MapInputToValueType<T> = T extends Input
- ? MapInputTypeToValueType<T["type"]>
- : T;
-
-type MapInputListToValueTypeList<Tuple extends readonly Input[]> = {
- [Index in keyof Tuple]: MapInputToValueType<Tuple[Index]>;
-} & { length: Tuple["length"] };
-
-export type InputPanelError = {
- [index: number]: I18nText | null | undefined;
-};
-
-export function hasError(e: InputPanelError | null | undefined): boolean {
- if (e == null) return false;
- for (const key of Object.keys(e)) {
- if (e[key as unknown as number] != null) return true;
- }
- return false;
-}
-
-export interface InputPanelProps<InputList extends readonly Input[]> {
- scheme: InputList;
- values: MapInputListToValueTypeList<InputList>;
- onChange: (
- values: MapInputListToValueTypeList<InputList>,
- index: number
- ) => void;
- error?: InputPanelError;
- disable?: boolean;
-}
-
-const InputPanel = <InputList extends readonly Input[]>(
- props: InputPanelProps<InputList>
-): React.ReactElement => {
- const { values, onChange, scheme, error, disable } = props;
-
- const { t } = useTranslation();
-
- const updateValue = (index: number, newValue: ValueTypes): void => {
- const oldValues = values;
- const newValues = oldValues.slice();
- newValues[index] = newValue;
- onChange(
- newValues as unknown as MapInputListToValueTypeList<InputList>,
- index
- );
- };
-
- return (
- <div>
- {scheme.map((item, index) => {
- const v = values[index];
- const e: string | null = convertI18nText(error?.[index], t);
-
- if (item.type === "text") {
- return (
- <div
- key={index}
- className={classNames("cru-input-panel-group", e && "error")}
- >
- {item.label && (
- <label className="cru-input-panel-label">
- {convertI18nText(item.label, t)}
- </label>
- )}
- <input
- type={item.password === true ? "password" : "text"}
- value={v as string}
- onChange={(e) => {
- const v = e.target.value;
- updateValue(index, v);
- }}
- disabled={disable}
- />
- {e && <div className="cru-input-panel-error-text">{e}</div>}
- {item.helper && (
- <div className="cru-input-panel-helper-text">
- {convertI18nText(item.helper, t)}
- </div>
- )}
- </div>
- );
- } else if (item.type === "bool") {
- return (
- <div
- key={index}
- className={classNames("cru-input-panel-group", e && "error")}
- >
- <input
- type="checkbox"
- checked={v as boolean}
- onChange={(event) => {
- const value = event.currentTarget.checked;
- updateValue(index, value);
- }}
- disabled={disable}
- />
- <label className="cru-input-panel-inline-label">
- {convertI18nText(item.label, t)}
- </label>
- {e != null && (
- <div className="cru-input-panel-error-text">{e}</div>
- )}
- {item.helper && (
- <div className="cru-input-panel-helper-text">
- {convertI18nText(item.helper, t)}
- </div>
- )}
- </div>
- );
- } else if (item.type === "select") {
- return (
- <div
- key={index}
- className={classNames("cru-input-panel-group", e && "error")}
- >
- <label className="cru-input-panel-label">
- {convertI18nText(item.label, t)}
- </label>
- <select
- value={v as string}
- onChange={(event) => {
- const value = event.target.value;
- updateValue(index, value);
- }}
- disabled={disable}
- >
- {item.options.map((option, i) => {
- return (
- <option value={option.value} key={i}>
- {option.icon}
- {convertI18nText(option.label, t)}
- </option>
- );
- })}
- </select>
- </div>
- );
- } else if (item.type === "color") {
- return (
- <div
- key={index}
- className={classNames("cru-input-panel-group", e && "error")}
- >
- <label className="cru-input-panel-inline-label">
- {convertI18nText(item.label, t)}
- </label>
- <TwitterPicker
- color={v as string}
- triangle="hide"
- onChange={(result) => updateValue(index, result.hex)}
- />
- </div>
- );
- } else if (item.type === "datetime") {
- return (
- <div
- key={index}
- className={classNames("cru-input-panel-group", e && "error")}
- >
- {item.label && (
- <label className="cru-input-panel-label">
- {convertI18nText(item.label, t)}
- </label>
- )}
- <input
- type="datetime-local"
- value={v as string}
- onChange={(e) => {
- const v = e.target.value;
- updateValue(index, v);
- }}
- disabled={disable}
- />
- {e != null && (
- <div className="cru-input-panel-error-text">{e}</div>
- )}
- {item.helper && (
- <div className="cru-input-panel-helper-text">
- {convertI18nText(item.helper, t)}
- </div>
- )}
- </div>
- );
- }
- })}
- </div>
- );
-};
-
-export default InputPanel;
diff --git a/FrontEnd/src/views/common/menu/Menu.css b/FrontEnd/src/views/common/menu/Menu.css
deleted file mode 100644
index c3fa82c4..00000000
--- a/FrontEnd/src/views/common/menu/Menu.css
+++ /dev/null
@@ -1,24 +0,0 @@
-.cru-menu {
- min-width: 200px;
-}
-
-.cru-menu-item {
- font-size: 1em;
- padding: 0.5em 1.5em;
- cursor: pointer;
- transition: all 0.5s;
- color: var(--cru-theme-color);
-}
-
-.cru-menu-item:hover {
- color: var(--cru-theme-t-color);
- background-color: var(--cru-theme-color);
-}
-
-.cru-menu-item-icon {
- margin-right: 1em;
-}
-
-.cru-menu-divider {
- border-top: 1px solid #e9ecef;
-}
diff --git a/FrontEnd/src/views/common/menu/Menu.tsx b/FrontEnd/src/views/common/menu/Menu.tsx
deleted file mode 100644
index de3b1664..00000000
--- a/FrontEnd/src/views/common/menu/Menu.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import * as React from "react";
-import classnames from "classnames";
-import { useTranslation } from "react-i18next";
-
-import { convertI18nText, I18nText } from "@/common";
-import { PaletteColorType } from "@/palette";
-
-import "./Menu.css";
-
-export type MenuItem =
- | {
- type: "divider";
- }
- | {
- type: "button";
- text: I18nText;
- iconClassName?: string;
- color?: PaletteColorType;
- onClick: () => void;
- };
-
-export type MenuItems = MenuItem[];
-
-export type MenuProps = {
- items: MenuItems;
- onItemClicked?: () => void;
- className?: string;
- style?: React.CSSProperties;
-};
-
-export default function _Menu({
- items,
- onItemClicked,
- className,
- style,
-}: MenuProps): React.ReactElement | null {
- const { t } = useTranslation();
-
- return (
- <div className={classnames("cru-menu", className)} style={style}>
- {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",
- `cru-${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>
- );
-}
diff --git a/FrontEnd/src/views/common/menu/PopupMenu.css b/FrontEnd/src/views/common/menu/PopupMenu.css
deleted file mode 100644
index f6654f68..00000000
--- a/FrontEnd/src/views/common/menu/PopupMenu.css
+++ /dev/null
@@ -1,6 +0,0 @@
-.cru-popup-menu-menu-container {
- z-index: 1040;
- border-radius: 5px;
- border: var(--cru-primary-color) 1px solid;
- background-color: white;
-}
diff --git a/FrontEnd/src/views/common/menu/PopupMenu.tsx b/FrontEnd/src/views/common/menu/PopupMenu.tsx
deleted file mode 100644
index 74ca7aba..00000000
--- a/FrontEnd/src/views/common/menu/PopupMenu.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import classNames from "classnames";
-import * as React from "react";
-import { createPortal } from "react-dom";
-import { usePopper } from "react-popper";
-
-import { useClickOutside } from "@/utilities/hooks";
-
-import Menu, { MenuItems } from "./Menu";
-
-import "./PopupMenu.css";
-
-export interface PopupMenuProps {
- items: MenuItems;
- children?: React.ReactNode;
- containerClassName?: string;
- containerStyle?: React.CSSProperties;
-}
-
-const PopupMenu: React.FC<PopupMenuProps> = ({
- items,
- children,
- containerClassName,
- containerStyle,
-}) => {
- const [show, setShow] = React.useState<boolean>(false);
-
- const [referenceElement, setReferenceElement] =
- React.useState<HTMLDivElement | null>(null);
- const [popperElement, setPopperElement] =
- React.useState<HTMLDivElement | null>(null);
- const { styles, attributes } = usePopper(referenceElement, popperElement);
-
- useClickOutside(popperElement, () => setShow(false), true);
-
- return (
- <>
- <div
- ref={setReferenceElement}
- className={classNames(
- "cru-popup-menu-trigger-container",
- containerClassName
- )}
- style={containerStyle}
- onClick={() => setShow(true)}
- >
- {children}
- </div>
- {show
- ? createPortal(
- <div
- ref={setPopperElement}
- className="cru-popup-menu-menu-container"
- style={styles.popper}
- {...attributes.popper}
- >
- <Menu
- items={items}
- onItemClicked={() => {
- setShow(false);
- }}
- />
- </div>,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- document.getElementById("portal")!
- )
- : null}
- </>
- );
-};
-
-export default PopupMenu;
diff --git a/FrontEnd/src/views/common/tab/TabPages.tsx b/FrontEnd/src/views/common/tab/TabPages.tsx
deleted file mode 100644
index cdb988e0..00000000
--- a/FrontEnd/src/views/common/tab/TabPages.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import * as React from "react";
-
-import { I18nText, UiLogicError } from "@/common";
-
-import Tabs from "./Tabs";
-
-export interface TabPage {
- name: string;
- text: I18nText;
- page: React.ReactNode;
-}
-
-export interface TabPagesProps {
- pages: TabPage[];
- actions?: React.ReactNode;
- dense?: boolean;
- className?: string;
- style?: React.CSSProperties;
- navClassName?: string;
- navStyle?: React.CSSProperties;
- pageContainerClassName?: string;
- pageContainerStyle?: React.CSSProperties;
-}
-
-const TabPages: React.FC<TabPagesProps> = ({
- pages,
- actions,
- dense,
- className,
- style,
- navClassName,
- navStyle,
- pageContainerClassName,
- pageContainerStyle,
-}) => {
- if (pages.length === 0) {
- throw new UiLogicError("Page list can't be empty.");
- }
-
- const [tab, setTab] = React.useState<string>(pages[0].name);
-
- const currentPage = pages.find((p) => p.name === tab);
-
- if (currentPage == null) {
- throw new UiLogicError("Current tab value is bad.");
- }
-
- return (
- <div className={className} style={style}>
- <Tabs
- tabs={pages.map((page) => ({
- name: page.name,
- text: page.text,
- onClick: () => {
- setTab(page.name);
- },
- }))}
- dense={dense}
- activeTabName={tab}
- className={navClassName}
- style={navStyle}
- actions={actions}
- />
- <div className={pageContainerClassName} style={pageContainerStyle}>
- {currentPage.page}
- </div>
- </div>
- );
-};
-
-export default TabPages;
diff --git a/FrontEnd/src/views/common/tab/Tabs.css b/FrontEnd/src/views/common/tab/Tabs.css
deleted file mode 100644
index 395d16a7..00000000
--- a/FrontEnd/src/views/common/tab/Tabs.css
+++ /dev/null
@@ -1,33 +0,0 @@
-.cru-nav {
- border-bottom: var(--cru-primary-color) 1px solid;
- display: flex;
-}
-
-.cru-nav-item {
- color: var(--cru-primary-color);
- border: var(--cru-background-2-color) 0.5px solid;
- border-bottom: none;
- padding: 0.5em 1.5em;
- border-top-left-radius: 5px;
- border-top-right-radius: 5px;
- transition: all 0.5s;
- cursor: pointer;
-}
-
-.cru-nav.dense .cru-nav-item {
- padding: 0.2em 1em;
-}
-
-.cru-nav-item:hover {
- background-color: var(--cru-background-1-color);
-}
-
-.cru-nav-item.active {
- color: var(--cru-primary-t-color);
- background-color: var(--cru-primary-color);
- border-color: var(--cru-primary-color);
-}
-
-.cru-nav-action-area {
- margin-left: auto;
-}
diff --git a/FrontEnd/src/views/common/tab/Tabs.tsx b/FrontEnd/src/views/common/tab/Tabs.tsx
deleted file mode 100644
index 3e3ef6fa..00000000
--- a/FrontEnd/src/views/common/tab/Tabs.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import * as React from "react";
-import { Link } from "react-router-dom";
-import { useTranslation } from "react-i18next";
-import classnames from "classnames";
-
-import { convertI18nText, I18nText } from "@/common";
-
-import "./Tabs.css";
-
-export interface Tab {
- name: string;
- text: I18nText;
- link?: string;
- onClick?: () => void;
-}
-
-export interface TabsProps {
- activeTabName?: string;
- actions?: React.ReactNode;
- dense?: boolean;
- tabs: Tab[];
- className?: string;
- style?: React.CSSProperties;
-}
-
-export default function Tabs(props: TabsProps): React.ReactElement | null {
- const { tabs, activeTabName, className, style, dense, actions } = props;
-
- const { t } = useTranslation();
-
- return (
- <div
- className={classnames("cru-nav", dense && "dense", className)}
- style={style}
- >
- {tabs.map((tab) => {
- const active = activeTabName === tab.name;
- const className = classnames("cru-nav-item", active && "active");
-
- if (tab.link != null) {
- return (
- <Link
- key={tab.name}
- to={tab.link}
- onClick={tab.onClick}
- className={className}
- >
- {convertI18nText(tab.text, t)}
- </Link>
- );
- } else {
- return (
- <span key={tab.name} onClick={tab.onClick} className={className}>
- {convertI18nText(tab.text, t)}
- </span>
- );
- }
- })}
- <div className="cru-nav-action-area">{actions}</div>
- </div>
- );
-}
diff --git a/FrontEnd/src/views/common/user/UserAvatar.tsx b/FrontEnd/src/views/common/user/UserAvatar.tsx
deleted file mode 100644
index fcff8c69..00000000
--- a/FrontEnd/src/views/common/user/UserAvatar.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as 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;