aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2023-08-19 02:13:26 +0800
committercrupest <crupest@outlook.com>2023-08-19 02:13:26 +0800
commitd6c1c9f2c9eddd7d6e4e91b2a9de71cfd9db6b73 (patch)
tree4b546fe67049a8211b3265a5d3316ae3947ac6e7 /FrontEnd/src
parenteec2e74a928f6448a0503e003d8afa693730b365 (diff)
downloadtimeline-d6c1c9f2c9eddd7d6e4e91b2a9de71cfd9db6b73.tar.gz
timeline-d6c1c9f2c9eddd7d6e4e91b2a9de71cfd9db6b73.tar.bz2
timeline-d6c1c9f2c9eddd7d6e4e91b2a9de71cfd9db6b73.zip
...
Diffstat (limited to 'FrontEnd/src')
-rw-r--r--FrontEnd/src/pages/setting/ChangeAvatarDialog.css21
-rw-r--r--FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx334
-rw-r--r--FrontEnd/src/pages/setting/index.css10
-rw-r--r--FrontEnd/src/pages/setting/index.tsx63
-rw-r--r--FrontEnd/src/views/common/BlobImage.tsx33
-rw-r--r--FrontEnd/src/views/common/ImageCropper.tsx30
-rw-r--r--FrontEnd/src/views/common/button/ButtonRowV2.tsx143
-rw-r--r--FrontEnd/src/views/common/button/IconButton.tsx1
-rw-r--r--FrontEnd/src/views/common/button/index.tsx10
-rw-r--r--FrontEnd/src/views/common/dialog/DialogContainer.tsx71
10 files changed, 413 insertions, 303 deletions
diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.css b/FrontEnd/src/pages/setting/ChangeAvatarDialog.css
new file mode 100644
index 00000000..2aa0bb54
--- /dev/null
+++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.css
@@ -0,0 +1,21 @@
+.change-avatar-dialog-prompt {
+ margin: 0.5em 0;
+}
+
+.change-avatar-dialog-prompt.success {
+ color: var(--cru-create-color);
+}
+
+.change-avatar-dialog-prompt.error {
+ color: var(--cru-danger-color);
+}
+
+.change-avatar-cropper {
+ max-height: 400px;
+}
+
+.change-avatar-preview-image {
+ min-width: 50%;
+ max-width: 100%;
+ max-height: 300px;
+} \ No newline at end of file
diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
index 7ac7dcad..f35fc5a7 100644
--- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
+++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx
@@ -1,10 +1,4 @@
-import {
- useState,
- useEffect,
- ChangeEvent,
- ComponentPropsWithoutRef,
-} from "react";
-import { AxiosError } from "axios";
+import { useState, ChangeEvent, ComponentPropsWithoutRef } from "react";
import { useC, Text, UiLogicError } from "@/common";
@@ -16,11 +10,13 @@ import ImageCropper, {
Clip,
applyClipToImage,
} from "@/views/common/ImageCropper";
-import Button from "@/views/common/button/Button";
-import ButtonRow from "@/views/common/button/ButtonRow";
+import BlobImage from "@/views/common/BlobImage";
+import ButtonRowV2 from "@/views/common/button/ButtonRowV2";
import Dialog from "@/views/common/dialog/Dialog";
import DialogContainer from "@/views/common/dialog/DialogContainer";
+import "./ChangeAvatarDialog.css";
+
interface ChangeAvatarDialogProps {
open: boolean;
onClose: () => void;
@@ -34,15 +30,6 @@ export default function ChangeAvatarDialog({
const user = useUser();
- const [file, setFile] = useState<File | null>(null);
- const [fileUrl, setFileUrl] = useState<string | null>(null);
- const [clip, setClip] = useState<Clip | null>(null);
- const [cropImgElement, setCropImgElement] = useState<HTMLImageElement | null>(
- null,
- );
- const [resultBlob, setResultBlob] = useState<Blob | null>(null);
- const [resultUrl, setResultUrl] = useState<string | null>(null);
-
type State =
| "select"
| "crop"
@@ -53,46 +40,22 @@ export default function ChangeAvatarDialog({
| "error";
const [state, setState] = useState<State>("select");
+ const [file, setFile] = useState<File | null>(null);
+ const [clip, setClip] = useState<Clip | null>(null);
+ const [cropImgElement, setCropImgElement] = useState<HTMLImageElement | null>(
+ null,
+ );
+ const [resultBlob, setResultBlob] = useState<Blob | null>(null);
const [message, setMessage] = useState<Text>(
"settings.dialogChangeAvatar.prompt.select",
);
- const trueMessage = c(message);
-
const close = (): void => {
- if (!(state === "uploading")) {
+ if (state !== "uploading") {
onClose();
}
};
- useEffect(() => {
- if (file != null) {
- const url = URL.createObjectURL(file);
- setClip(null);
- setFileUrl(url);
- setState("crop");
- return () => {
- URL.revokeObjectURL(url);
- };
- } else {
- setFileUrl(null);
- setState("select");
- }
- }, [file]);
-
- useEffect(() => {
- if (resultBlob != null) {
- const url = URL.createObjectURL(resultBlob);
- setResultUrl(url);
- setState("preview");
- return () => {
- URL.revokeObjectURL(url);
- };
- } else {
- setResultUrl(null);
- }
- }, [resultBlob]);
-
const onSelectFile = (e: ChangeEvent<HTMLInputElement>): void => {
const files = e.target.files;
if (files == null || files.length === 0) {
@@ -113,7 +76,9 @@ export default function ChangeAvatarDialog({
}
setState("process-crop");
+
void applyClipToImage(cropImgElement, clip, file.type).then((b) => {
+ setState("preview");
setResultBlob(b);
});
};
@@ -124,7 +89,6 @@ export default function ChangeAvatarDialog({
};
const onPreviewPrevious = () => {
- setResultBlob(null);
setState("crop");
};
@@ -144,77 +108,80 @@ export default function ChangeAvatarDialog({
() => {
setState("success");
},
- (e: unknown) => {
+ () => {
setState("error");
- setMessage({ type: "custom", value: (e as AxiosError).message });
+ setMessage("operationDialog.error");
},
);
};
- const createPreviewRow = (): React.ReactElement => {
- if (resultUrl == null) {
- throw new UiLogicError();
- }
- return (
- <div className="row justify-content-center">
- <div className="col col-auto">
- <img
- className="change-avatar-img"
- src={resultUrl}
- alt={c("settings.dialogChangeAvatar.previewImgAlt") ?? undefined}
- alt={c("settings.dialogChangeAvatar.previewImgAlt") ?? undefined}
- />
- </div>
- </div>
- );
- };
+ const cancelButton = {
+ key: "cancel",
+ action: "secondary",
+ text: "operationDialog.cancel",
+ onClick: close,
+ } as const;
+
+ const createPreviousButton = (onClick: () => void) =>
+ ({
+ key: "previous",
+ action: "secondary",
+ text: "operationDialog.previousStep",
+ onClick,
+ }) as const;
const buttonsMap: Record<
State,
- ComponentPropsWithoutRef<typeof ButtonRow>["buttons"]
+ ComponentPropsWithoutRef<typeof ButtonRowV2>["buttons"]
> = {
select: [
+ cancelButton,
{
- key: "cancel",
- type: "normal",
- props: {
- outline: true,
- color: "secondary",
- text: "operationDialog.cancel",
- onClick: close,
- },
+ key: "next",
+ action: "primary",
+ text: "operationDialog.nextStep",
+ onClick: () => setState("crop"),
+ disabled: file == null,
},
],
crop: [
+ cancelButton,
+ createPreviousButton(onCropPrevious),
{
- key: "cancel",
- type: "normal",
- props: {
- outline: true,
- color: "secondary",
- text: "operationDialog.cancel",
- onClick: close,
- },
+ key: "next",
+ action: "primary",
+ text: "operationDialog.nextStep",
+ onClick: onCropNext,
+ disabled: cropImgElement == null || clip == null || clip.width === 0,
},
+ ],
+ "process-crop": [cancelButton, createPreviousButton(onPreviewPrevious)],
+ preview: [
+ cancelButton,
+ createPreviousButton(onPreviewPrevious),
{
- key: "previous",
- type: "normal",
- props: {
- outline: true,
- color: "secondary",
- text: "operationDialog.previousStep",
- onClick: onCropPrevious,
- },
+ key: "upload",
+ action: "primary",
+ text: "settings.dialogChangeAvatar.upload",
+ onClick: upload,
},
+ ],
+ uploading: [],
+ success: [
{
- key: "next",
- type: "normal",
- props: {
- color: "primary",
- text: "operationDialog.nextStep",
- onClick: onCropNext,
- disabled: cropImgElement == null || clip == null || clip.width === 0,
- },
+ key: "ok",
+ text: "operationDialog.ok",
+ color: "create",
+ onClick: close,
+ },
+ ],
+ error: [
+ cancelButton,
+ {
+ key: "retry",
+ action: "primary",
+ text: "operationDialog.retry",
+ onClick: upload,
},
],
};
@@ -224,150 +191,95 @@ export default function ChangeAvatarDialog({
<DialogContainer
title="settings.dialogChangeAvatar.title"
titleColor="primary"
- buttons={buttonsMap[state]}
+ buttonsV2={buttonsMap[state]}
>
{(() => {
if (state === "select") {
return (
- <div className="">
- <div className="row">
+ <div className="change-avatar-dialog-container">
+ <div className="change-avatar-dialog-prompt">
{c("settings.dialogChangeAvatar.prompt.select")}
</div>
- <div className="row">
- <input
- className="px-0"
- type="file"
- accept="image/*"
- onChange={onSelectFile}
- />
- </div>
+ <input
+ className="change-avatar-select-input"
+ type="file"
+ accept="image/*"
+ onChange={onSelectFile}
+ />
</div>
);
} else if (state === "crop") {
- if (fileUrl == null) {
+ if (file == null) {
throw new UiLogicError();
}
return (
- <div className="container">
- <div className="row justify-content-center">
- <ImageCropper
- clip={clip}
- onChange={setClip}
- imageUrl={fileUrl}
- imageElementCallback={setCropImgElement}
- />
- </div>
- <div className="row">
+ <div className="change-avatar-dialog-container">
+ <ImageCropper
+ className="change-avatar-cropper"
+ clip={clip}
+ onChange={setClip}
+ image={file}
+ imageElementCallback={setCropImgElement}
+ />
+ <div className="change-avatar-dialog-prompt">
{c("settings.dialogChangeAvatar.prompt.crop")}
</div>
</div>
);
} else if (state === "process-crop") {
return (
- <>
- <div className="container">
- <div className="row">
- {c("settings.dialogChangeAvatar.prompt.processingCrop")}
- </div>
+ <div className="change-avatar-dialog-container">
+ <div className="change-avatar-dialog-prompt">
+ {c("settings.dialogChangeAvatar.prompt.processingCrop")}
</div>
- <hr />
- <div className="cru-dialog-bottom-area">
- <Button
- text="operationDialog.cancel"
- color="secondary"
- onClick={close}
- outline
- />
- <Button
- text="operationDialog.previousStep"
- color="secondary"
- onClick={onPreviewPrevious}
- outline
- />
- </div>
- </>
+ </div>
);
} else if (state === "preview") {
return (
- <>
- <div className="container">
- {createPreviewRow()}
- <div className="row">
- {t("settings.dialogChangeAvatar.prompt.preview")}
- </div>
- </div>
- <hr />
- <div className="cru-dialog-bottom-area">
- <Button
- text="operationDialog.cancel"
- color="secondary"
- outline
- onClick={close}
- />
- <Button
- text="operationDialog.previousStep"
- color="secondary"
- outline
- onClick={onPreviewPrevious}
- />
- <Button
- text="settings.dialogChangeAvatar.upload"
- color="primary"
- onClick={upload}
- />
+ <div className="change-avatar-dialog-container">
+ <BlobImage
+ className="change-avatar-preview-image"
+ src={resultBlob}
+ alt={
+ c("settings.dialogChangeAvatar.previewImgAlt") ?? undefined
+ }
+ />
+ <div className="change-avatar-dialog-prompt">
+ {c("settings.dialogChangeAvatar.prompt.preview")}
</div>
- </>
+ </div>
);
} else if (state === "uploading") {
return (
- <>
- <div className="container">
- {createPreviewRow()}
- <div className="row">
- {t("settings.dialogChangeAvatar.prompt.uploading")}
- </div>
+ <div className="change-avatar-dialog-container">
+ <BlobImage
+ className="change-avatar-preview-image"
+ src={resultBlob}
+ />
+ <div className="change-avatar-dialog-prompt">
+ {c("settings.dialogChangeAvatar.prompt.uploading")}
</div>
- </>
+ </div>
);
} else if (state === "success") {
return (
- <>
- <div className="container">
- <div className="row p-4 text-success">
- {t("operationDialog.success")}
- </div>
- </div>
- <hr />
- <div className="cru-dialog-bottom-area">
- <Button
- text="operationDialog.ok"
- color="success"
- onClick={close}
- />
+ <div className="change-avatar-dialog-container">
+ <div className="change-avatar-dialog-prompt success">
+ {c("operationDialog.success")}
</div>
- </>
+ </div>
);
} else {
return (
- <>
- <div className="container">
- {createPreviewRow()}
- <div className="row text-danger">{trueMessage}</div>
- </div>
- <hr />
- <div>
- <Button
- text="operationDialog.cancel"
- color="secondary"
- onClick={close}
- />
- <Button
- text="operationDialog.retry"
- color="primary"
- onClick={upload}
- />
+ <div className="change-avatar-dialog-container">
+ <BlobImage
+ className="change-avatar-preview-image"
+ src={resultBlob}
+ />
+ <div className="change-avatar-dialog-prompt error">
+ {c(message)}
</div>
- </>
+ </div>
);
}
})()}
diff --git a/FrontEnd/src/pages/setting/index.css b/FrontEnd/src/pages/setting/index.css
index 8af65e93..86ccf706 100644
--- a/FrontEnd/src/pages/setting/index.css
+++ b/FrontEnd/src/pages/setting/index.css
@@ -1,15 +1,5 @@
/* TODO: Make item prettier. */
-.change-avatar-cropper-row {
- max-height: 400px;
-}
-
-.change-avatar-img {
- min-width: 50%;
- max-width: 100%;
- max-height: 400px;
-}
-
.setting-section {
padding: 1em 0;
margin: 1em 0;
diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx
index 8673d55a..4e0bf27e 100644
--- a/FrontEnd/src/pages/setting/index.tsx
+++ b/FrontEnd/src/pages/setting/index.tsx
@@ -156,33 +156,34 @@ function RegisterCodeSettingItem() {
}, [user, registerCode]);
return (
- <SettingItemContainer
- title="settings.myRegisterCode"
- description="settings.myRegisterCodeDesc"
- className="register-code-setting-item"
- onClick={() => setDialogOpen(true)}
- >
- {registerCode === undefined ? (
- <Spinner />
- ) : registerCode === null ? (
- <span>Noop</span>
- ) : (
- <code
- className="register-code"
- onClick={(event) => {
- void navigator.clipboard.writeText(registerCode).then(() => {
- pushAlert({
- type: "create",
- message: "settings.myRegisterCodeCopied",
+ <>
+ <SettingItemContainer
+ title="settings.myRegisterCode"
+ description="settings.myRegisterCodeDesc"
+ className="register-code-setting-item"
+ onClick={() => setDialogOpen(true)}
+ >
+ {registerCode === undefined ? (
+ <Spinner />
+ ) : registerCode === null ? (
+ <span>Noop</span>
+ ) : (
+ <code
+ className="register-code"
+ onClick={(event) => {
+ void navigator.clipboard.writeText(registerCode).then(() => {
+ pushAlert({
+ type: "create",
+ message: "settings.myRegisterCodeCopied",
+ });
});
- });
- event.stopPropagation();
- }}
- >
- {registerCode}
- </code>
- )}
-
+ event.stopPropagation();
+ }}
+ >
+ {registerCode}
+ </code>
+ )}
+ </SettingItemContainer>
<ConfirmDialog
title="settings.renewRegisterCode"
body="settings.renewRegisterCodeDesc"
@@ -196,8 +197,8 @@ function RegisterCodeSettingItem() {
setRegisterCode(undefined);
});
}}
- />
- </SettingItemContainer>
+ />{" "}
+ </>
);
}
@@ -243,7 +244,6 @@ export default function SettingPage() {
"change-avatar",
"change-nickname",
"logout",
- "renew-register-code",
]);
return (
@@ -271,7 +271,10 @@ export default function SettingPage() {
/>
</SettingSection>
) : null}
- <SettingSection title="settings.subheader.customization" color="secondary">
+ <SettingSection
+ title="settings.subheader.customization"
+ color="secondary"
+ >
<LanguageChangeSettingItem />
</SettingSection>
<ChangePasswordDialog {...dialogPropsMap["change-password"]} />
diff --git a/FrontEnd/src/views/common/BlobImage.tsx b/FrontEnd/src/views/common/BlobImage.tsx
index 5e050ebe..259c2210 100644
--- a/FrontEnd/src/views/common/BlobImage.tsx
+++ b/FrontEnd/src/views/common/BlobImage.tsx
@@ -1,27 +1,26 @@
-import * as React from "react";
+import { ComponentPropsWithoutRef, useState, useEffect } from "react";
-const BlobImage: React.FC<
- Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src"> & {
- blob?: Blob | unknown;
- }
-> = (props) => {
- const { blob, ...otherProps } = props;
+type BlobImageProps = Omit<ComponentPropsWithoutRef<"img">, "src"> & {
+ imgRef?: React.Ref<HTMLImageElement>;
+ src?: Blob | string | null;
+};
+
+export default function BlobImage(props: BlobImageProps) {
+ const { imgRef, src, ...otherProps } = props;
- const [url, setUrl] = React.useState<string | undefined>(undefined);
+ const [url, setUrl] = useState<string | null | undefined>(undefined);
- React.useEffect(() => {
- if (blob instanceof Blob) {
- const url = URL.createObjectURL(blob);
+ useEffect(() => {
+ if (src instanceof Blob) {
+ const url = URL.createObjectURL(src);
setUrl(url);
return () => {
URL.revokeObjectURL(url);
};
} else {
- setUrl(undefined);
+ setUrl(src);
}
- }, [blob]);
-
- return <img {...otherProps} src={url} />;
-};
+ }, [src]);
-export default BlobImage;
+ return <img ref={imgRef} {...otherProps} src={url ?? undefined} />;
+}
diff --git a/FrontEnd/src/views/common/ImageCropper.tsx b/FrontEnd/src/views/common/ImageCropper.tsx
index 04e17415..fcab74b0 100644
--- a/FrontEnd/src/views/common/ImageCropper.tsx
+++ b/FrontEnd/src/views/common/ImageCropper.tsx
@@ -4,6 +4,7 @@ import classnames from "classnames";
import { UiLogicError } from "@/common";
import "./ImageCropper.css";
+import BlobImage from "./BlobImage";
export interface Clip {
left: number;
@@ -33,17 +34,17 @@ interface ImageCropperSavedState {
export interface ImageCropperProps {
clip: Clip | null;
- imageUrl: string;
+ image: string | Blob;
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 { clip, image, onChange, imageElementCallback, className } = props;
const [oldState, setOldState] = React.useState<ImageCropperSavedState | null>(
- null
+ null,
);
const [imageInfo, setImageInfo] = React.useState<ImageInfo | null>(null);
@@ -71,7 +72,7 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => {
imageElementCallback(null);
}
},
- [imageElementCallback]
+ [imageElementCallback],
);
const onImageLoad = React.useCallback(
@@ -93,7 +94,7 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => {
imageElementCallback(img);
}
},
- [onChange, imageElementCallback]
+ [onChange, imageElementCallback],
);
const onPointerDown = React.useCallback(
@@ -107,7 +108,7 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => {
pointerId: e.pointerId,
});
},
- [oldState, c]
+ [oldState, c],
);
const onPointerUp = React.useCallback(
@@ -116,7 +117,7 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => {
e.currentTarget.releasePointerCapture(e.pointerId);
setOldState(null);
},
- [oldState]
+ [oldState],
);
const onPointerMove = React.useCallback(
@@ -153,7 +154,7 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => {
onChange({ left: newRatio.x, top: newRatio.y, width: oldClip.width });
},
- [oldState, onChange]
+ [oldState, onChange],
);
const onHandlerPointerMove = React.useCallback(
@@ -198,7 +199,7 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => {
onChange({ left: oldClip.left, top: oldClip.top, width: newWidth });
},
- [imageInfo, oldState, onChange]
+ [imageInfo, oldState, onChange],
);
const toPercentage = (n: number): string => `${n}%`;
@@ -229,7 +230,12 @@ const ImageCropper = (props: ImageCropperProps): React.ReactElement => {
className={classnames("image-cropper-container", className)}
style={containerStyle}
>
- <img ref={onImageRef} src={imageUrl} onLoad={onImageLoad} alt="to crop" />
+ <BlobImage
+ imgRef={onImageRef}
+ src={image}
+ onLoad={onImageLoad}
+ alt="to crop"
+ />
<div className="image-cropper-mask-container">
<div
className="image-cropper-mask"
@@ -263,7 +269,7 @@ export default ImageCropper;
export function applyClipToImage(
image: HTMLImageElement,
clip: Clip,
- mimeType: string
+ mimeType: string,
): Promise<Blob> {
return new Promise((resolve, reject) => {
const naturalSize = {
@@ -292,7 +298,7 @@ export function applyClipToImage(
0,
0,
clipArea.length,
- clipArea.length
+ clipArea.length,
);
canvas.toBlob((blob) => {
diff --git a/FrontEnd/src/views/common/button/ButtonRowV2.tsx b/FrontEnd/src/views/common/button/ButtonRowV2.tsx
new file mode 100644
index 00000000..3467ad52
--- /dev/null
+++ b/FrontEnd/src/views/common/button/ButtonRowV2.tsx
@@ -0,0 +1,143 @@
+import { ComponentPropsWithoutRef, Ref } from "react";
+import classNames from "classnames";
+
+import Button from "./Button";
+import FlatButton from "./FlatButton";
+import IconButton from "./IconButton";
+import LoadingButton from "./LoadingButton";
+
+import "./ButtonRow.css";
+import { Text, ThemeColor } from "../common";
+
+interface ButtonRowV2ButtonBase {
+ key: string | number;
+ action?: "primary" | "secondary";
+ color?: ThemeColor;
+ disabled?: boolean;
+ onClick?: () => void;
+}
+
+interface ButtonRowV2ButtonWithNoType extends ButtonRowV2ButtonBase {
+ type?: undefined | null;
+ text: Text;
+ outline?: boolean;
+ props?: ComponentPropsWithoutRef<typeof Button>;
+}
+
+interface ButtonRowV2NormalButton extends ButtonRowV2ButtonBase {
+ type: "normal";
+ text: Text;
+ outline?: boolean;
+ props?: ComponentPropsWithoutRef<typeof Button>;
+}
+
+interface ButtonRowV2FlatButton extends ButtonRowV2ButtonBase {
+ type: "flat";
+ text: Text;
+ props?: ComponentPropsWithoutRef<typeof FlatButton>;
+}
+
+interface ButtonRowV2IconButton extends ButtonRowV2ButtonBase {
+ type: "icon";
+ icon: string;
+ props?: ComponentPropsWithoutRef<typeof IconButton>;
+}
+
+interface ButtonRowV2LoadingButton extends ButtonRowV2ButtonBase {
+ type: "loading";
+ text: Text;
+ loading?: boolean;
+ props?: ComponentPropsWithoutRef<typeof LoadingButton>;
+}
+
+type ButtonRowV2Button =
+ | ButtonRowV2ButtonWithNoType
+ | ButtonRowV2NormalButton
+ | ButtonRowV2FlatButton
+ | ButtonRowV2IconButton
+ | ButtonRowV2LoadingButton;
+
+interface ButtonRowV2Props {
+ className?: string;
+ containerRef?: Ref<HTMLDivElement>;
+ buttons: ButtonRowV2Button[];
+ buttonsClassName?: string;
+}
+
+export default function ButtonRowV2({
+ className,
+ containerRef,
+ buttons,
+ buttonsClassName,
+}: ButtonRowV2Props) {
+ return (
+ <div ref={containerRef} className={classNames("cru-button-row", className)}>
+ {buttons.map((button) => {
+ const { key, action, color, disabled, onClick } = button;
+
+ const realAction = action ?? "primary";
+ const realColor =
+ color ?? (realAction === "primary" ? "primary" : "secondary");
+
+ const commonProps = { key, color: realColor, disabled, onClick };
+ const newClassName = classNames(
+ button.props?.className,
+ buttonsClassName,
+ );
+
+ switch (button.type) {
+ case null:
+ case undefined:
+ case "normal": {
+ const { text, outline, props } = button;
+ return (
+ <Button
+ {...commonProps}
+ text={text}
+ outline={outline ?? realAction !== "primary"}
+ {...props}
+ className={newClassName}
+ />
+ );
+ }
+ case "flat": {
+ const { text, props } = button;
+ return (
+ <FlatButton
+ {...commonProps}
+ text={text}
+ {...props}
+ className={newClassName}
+ />
+ );
+ }
+ case "icon": {
+ const { icon, props } = button;
+ return (
+ <IconButton
+ {...commonProps}
+ icon={icon}
+ {...props}
+ className={newClassName}
+ />
+ );
+ }
+ case "loading": {
+ const { text, loading, props } = button;
+ return (
+ <LoadingButton
+ {...commonProps}
+ text={text}
+ loading={loading}
+ {...props}
+ className={newClassName}
+ />
+ );
+ }
+ default:
+ throw new Error();
+ }
+ })}
+ </div>
+ );
+}
diff --git a/FrontEnd/src/views/common/button/IconButton.tsx b/FrontEnd/src/views/common/button/IconButton.tsx
index 60050e0d..126f4263 100644
--- a/FrontEnd/src/views/common/button/IconButton.tsx
+++ b/FrontEnd/src/views/common/button/IconButton.tsx
@@ -9,6 +9,7 @@ interface IconButtonProps extends ComponentPropsWithoutRef<"i"> {
icon: string;
color?: ThemeColor;
large?: boolean;
+ disabled?: boolean; // TODO: Not implemented
}
export default function IconButton(props: IconButtonProps) {
diff --git a/FrontEnd/src/views/common/button/index.tsx b/FrontEnd/src/views/common/button/index.tsx
index 73038849..b5aa5470 100644
--- a/FrontEnd/src/views/common/button/index.tsx
+++ b/FrontEnd/src/views/common/button/index.tsx
@@ -3,5 +3,13 @@ import FlatButton from "./FlatButton";
import IconButton from "./IconButton";
import LoadingButton from "./LoadingButton";
import ButtonRow from "./ButtonRow";
+import ButtonRowV2 from "./ButtonRowV2";
-export { Button, FlatButton, IconButton, LoadingButton, ButtonRow };
+export {
+ Button,
+ FlatButton,
+ IconButton,
+ LoadingButton,
+ ButtonRow,
+ ButtonRowV2,
+};
diff --git a/FrontEnd/src/views/common/dialog/DialogContainer.tsx b/FrontEnd/src/views/common/dialog/DialogContainer.tsx
index b0a87ea5..afee2669 100644
--- a/FrontEnd/src/views/common/dialog/DialogContainer.tsx
+++ b/FrontEnd/src/views/common/dialog/DialogContainer.tsx
@@ -2,11 +2,11 @@ import { ComponentProps, Ref, ReactNode } from "react";
import classNames from "classnames";
import { ThemeColor, Text, useC } from "../common";
-import { ButtonRow } from "../button";
+import { ButtonRow, ButtonRowV2 } from "../button";
import "./DialogContainer.css";
-interface DialogContainerProps {
+interface DialogContainerBaseProps {
className?: string;
title: Text;
titleColor?: ThemeColor;
@@ -14,25 +14,37 @@ interface DialogContainerProps {
titleRef?: Ref<HTMLDivElement>;
bodyContainerClassName?: string;
bodyContainerRef?: Ref<HTMLDivElement>;
- buttons: ComponentProps<typeof ButtonRow>["buttons"];
buttonsClassName?: string;
buttonsContainerRef?: ComponentProps<typeof ButtonRow>["containerRef"];
children: ReactNode;
}
-export default function DialogContainer({
- className,
- title,
- titleColor,
- titleClassName,
- titleRef,
- bodyContainerClassName,
- bodyContainerRef,
- buttons,
- buttonsClassName,
- buttonsContainerRef,
- children,
-}: DialogContainerProps) {
+interface DialogContainerWithButtonsProps extends DialogContainerBaseProps {
+ buttons: ComponentProps<typeof ButtonRow>["buttons"];
+}
+
+interface DialogContainerWithButtonsV2Props extends DialogContainerBaseProps {
+ buttonsV2: ComponentProps<typeof ButtonRowV2>["buttons"];
+}
+
+type DialogContainerProps =
+ | DialogContainerWithButtonsProps
+ | DialogContainerWithButtonsV2Props;
+
+export default function DialogContainer(props: DialogContainerProps) {
+ const {
+ className,
+ title,
+ titleColor,
+ titleClassName,
+ titleRef,
+ bodyContainerClassName,
+ bodyContainerRef,
+ buttonsClassName,
+ buttonsContainerRef,
+ children,
+ } = props;
+
const c = useC();
return (
@@ -57,12 +69,27 @@ export default function DialogContainer({
{children}
</div>
<hr className="cru-dialog-container-hr" />
- <ButtonRow
- containerRef={buttonsContainerRef}
- className={classNames("cru-dialog-container-button-row", buttonsClassName)}
- buttons={buttons}
- buttonsClassName="cru-dialog-container-button"
- />
+ {"buttons" in props ? (
+ <ButtonRow
+ containerRef={buttonsContainerRef}
+ className={classNames(
+ "cru-dialog-container-button-row",
+ buttonsClassName,
+ )}
+ buttons={props.buttons}
+ buttonsClassName="cru-dialog-container-button"
+ />
+ ) : (
+ <ButtonRowV2
+ containerRef={buttonsContainerRef}
+ className={classNames(
+ "cru-dialog-container-button-row",
+ buttonsClassName,
+ )}
+ buttons={props.buttonsV2}
+ buttonsClassName="cru-dialog-container-button"
+ />
+ )}
</div>
);
}