diff options
author | crupest <crupest@outlook.com> | 2023-08-19 02:13:26 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2023-08-19 02:13:26 +0800 |
commit | d6c1c9f2c9eddd7d6e4e91b2a9de71cfd9db6b73 (patch) | |
tree | 4b546fe67049a8211b3265a5d3316ae3947ac6e7 /FrontEnd/src | |
parent | eec2e74a928f6448a0503e003d8afa693730b365 (diff) | |
download | timeline-d6c1c9f2c9eddd7d6e4e91b2a9de71cfd9db6b73.tar.gz timeline-d6c1c9f2c9eddd7d6e4e91b2a9de71cfd9db6b73.tar.bz2 timeline-d6c1c9f2c9eddd7d6e4e91b2a9de71cfd9db6b73.zip |
...
Diffstat (limited to 'FrontEnd/src')
-rw-r--r-- | FrontEnd/src/pages/setting/ChangeAvatarDialog.css | 21 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx | 334 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/index.css | 10 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/index.tsx | 63 | ||||
-rw-r--r-- | FrontEnd/src/views/common/BlobImage.tsx | 33 | ||||
-rw-r--r-- | FrontEnd/src/views/common/ImageCropper.tsx | 30 | ||||
-rw-r--r-- | FrontEnd/src/views/common/button/ButtonRowV2.tsx | 143 | ||||
-rw-r--r-- | FrontEnd/src/views/common/button/IconButton.tsx | 1 | ||||
-rw-r--r-- | FrontEnd/src/views/common/button/index.tsx | 10 | ||||
-rw-r--r-- | FrontEnd/src/views/common/dialog/DialogContainer.tsx | 71 |
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> ); } |