diff options
Diffstat (limited to 'Timeline/ClientApp/src/app/views/user')
6 files changed, 0 insertions, 507 deletions
diff --git a/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx b/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx deleted file mode 100644 index ffa2218b..00000000 --- a/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { AxiosError } from "axios"; -import { Modal, Row, Button } from "react-bootstrap"; - -import { UiLogicError } from "@/common"; - -import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper"; - -export interface ChangeAvatarDialogProps { - open: boolean; - close: () => void; - process: (blob: Blob) => Promise<void>; -} - -const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => { - const { t } = useTranslation(); - - const [file, setFile] = React.useState<File | null>(null); - const [fileUrl, setFileUrl] = React.useState<string | null>(null); - const [clip, setClip] = React.useState<Clip | null>(null); - const [ - cropImgElement, - setCropImgElement, - ] = React.useState<HTMLImageElement | null>(null); - const [resultBlob, setResultBlob] = React.useState<Blob | null>(null); - const [resultUrl, setResultUrl] = React.useState<string | null>(null); - - const [state, setState] = React.useState< - | "select" - | "crop" - | "processcrop" - | "preview" - | "uploading" - | "success" - | "error" - >("select"); - - const [message, setMessage] = useState< - string | { type: "custom"; text: string } | null - >("userPage.dialogChangeAvatar.prompt.select"); - - const trueMessage = - message == null - ? null - : typeof message === "string" - ? t(message) - : message.text; - - const closeDialog = props.close; - - const close = React.useCallback((): void => { - if (!(state === "uploading")) { - closeDialog(); - } - }, [state, closeDialog]); - - 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]); - - React.useEffect(() => { - if (resultBlob != null) { - const url = URL.createObjectURL(resultBlob); - setResultUrl(url); - setState("preview"); - return () => { - URL.revokeObjectURL(url); - }; - } else { - setResultUrl(null); - } - }, [resultBlob]); - - const onSelectFile = React.useCallback( - (e: React.ChangeEvent<HTMLInputElement>): void => { - const files = e.target.files; - if (files == null || files.length === 0) { - setFile(null); - } else { - setFile(files[0]); - } - }, - [] - ); - - const onCropNext = React.useCallback(() => { - if ( - cropImgElement == null || - clip == null || - clip.width === 0 || - file == null - ) { - throw new UiLogicError(); - } - - setState("processcrop"); - void applyClipToImage(cropImgElement, clip, file.type).then((b) => { - setResultBlob(b); - }); - }, [cropImgElement, clip, file]); - - const onCropPrevious = React.useCallback(() => { - setFile(null); - setState("select"); - }, []); - - const onPreviewPrevious = React.useCallback(() => { - setResultBlob(null); - setState("crop"); - }, []); - - const process = props.process; - - const upload = React.useCallback(() => { - if (resultBlob == null) { - throw new UiLogicError(); - } - - setState("uploading"); - process(resultBlob).then( - () => { - setState("success"); - }, - (e: unknown) => { - setState("error"); - setMessage({ type: "custom", text: (e as AxiosError).message }); - } - ); - }, [resultBlob, process]); - - const createPreviewRow = (): React.ReactElement => { - if (resultUrl == null) { - throw new UiLogicError(); - } - return ( - <Row className="justify-content-center"> - <img - className="change-avatar-img" - src={resultUrl} - alt={t("userPage.dialogChangeAvatar.previewImgAlt")} - /> - </Row> - ); - }; - - return ( - <Modal show={props.open} onHide={close}> - <Modal.Header> - <Modal.Title> {t("userPage.dialogChangeAvatar.title")}</Modal.Title> - </Modal.Header> - {(() => { - if (state === "select") { - return ( - <> - <Modal.Body className="container"> - <Row>{t("userPage.dialogChangeAvatar.prompt.select")}</Row> - <Row> - <input type="file" accept="image/*" onChange={onSelectFile} /> - </Row> - </Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={close}> - {t("operationDialog.cancel")} - </Button> - </Modal.Footer> - </> - ); - } else if (state === "crop") { - if (fileUrl == null) { - throw new UiLogicError(); - } - return ( - <> - <Modal.Body className="container"> - <Row className="justify-content-center"> - <ImageCropper - clip={clip} - onChange={setClip} - imageUrl={fileUrl} - imageElementCallback={setCropImgElement} - /> - </Row> - <Row>{t("userPage.dialogChangeAvatar.prompt.crop")}</Row> - </Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={close}> - {t("operationDialog.cancel")} - </Button> - <Button variant="secondary" onClick={onCropPrevious}> - {t("operationDialog.previousStep")} - </Button> - <Button - color="primary" - onClick={onCropNext} - disabled={ - cropImgElement == null || clip == null || clip.width === 0 - } - > - {t("operationDialog.nextStep")} - </Button> - </Modal.Footer> - </> - ); - } else if (state === "processcrop") { - return ( - <> - <Modal.Body className="container"> - <Row> - {t("userPage.dialogChangeAvatar.prompt.processingCrop")} - </Row> - </Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={close}> - {t("operationDialog.cancel")} - </Button> - <Button variant="secondary" onClick={onPreviewPrevious}> - {t("operationDialog.previousStep")} - </Button> - </Modal.Footer> - </> - ); - } else if (state === "preview") { - return ( - <> - <Modal.Body className="container"> - {createPreviewRow()} - <Row>{t("userPage.dialogChangeAvatar.prompt.preview")}</Row> - </Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={close}> - {t("operationDialog.cancel")} - </Button> - <Button variant="secondary" onClick={onPreviewPrevious}> - {t("operationDialog.previousStep")} - </Button> - <Button variant="primary" onClick={upload}> - {t("userPage.dialogChangeAvatar.upload")} - </Button> - </Modal.Footer> - </> - ); - } else if (state === "uploading") { - return ( - <> - <Modal.Body className="container"> - {createPreviewRow()} - <Row>{t("userPage.dialogChangeAvatar.prompt.uploading")}</Row> - </Modal.Body> - <Modal.Footer></Modal.Footer> - </> - ); - } else if (state === "success") { - return ( - <> - <Modal.Body className="container"> - <Row className="p-4 text-success"> - {t("operationDialog.success")} - </Row> - </Modal.Body> - <Modal.Footer> - <Button variant="success" onClick={close}> - {t("operationDialog.ok")} - </Button> - </Modal.Footer> - </> - ); - } else { - return ( - <> - <Modal.Body className="container"> - {createPreviewRow()} - <Row className="text-danger">{trueMessage}</Row> - </Modal.Body> - <Modal.Footer> - <Button variant="secondary" onClick={close}> - {t("operationDialog.cancel")} - </Button> - <Button variant="primary" onClick={upload}> - {t("operationDialog.retry")} - </Button> - </Modal.Footer> - </> - ); - } - })()} - </Modal> - ); -}; - -export default ChangeAvatarDialog; diff --git a/Timeline/ClientApp/src/app/views/user/ChangeNicknameDialog.tsx b/Timeline/ClientApp/src/app/views/user/ChangeNicknameDialog.tsx deleted file mode 100644 index 251b18c5..00000000 --- a/Timeline/ClientApp/src/app/views/user/ChangeNicknameDialog.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; - -import OperationDialog from "../common/OperationDialog"; - -export interface ChangeNicknameDialogProps { - open: boolean; - close: () => void; - onProcess: (newNickname: string) => Promise<void>; -} - -const ChangeNicknameDialog: React.FC<ChangeNicknameDialogProps> = (props) => { - return ( - <OperationDialog - open={props.open} - title="userPage.dialogChangeNickname.title" - titleColor="default" - inputScheme={[ - { type: "text", label: "userPage.dialogChangeNickname.inputLabel" }, - ]} - onProcess={([newNickname]) => { - return props.onProcess(newNickname as string); - }} - close={props.close} - /> - ); -}; - -export default ChangeNicknameDialog; diff --git a/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx b/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx deleted file mode 100644 index 888fb18a..00000000 --- a/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { Dropdown, Button } from "react-bootstrap"; - -import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; -import { useAvatar } from "@/services/user"; - -import BlobImage from "../common/BlobImage"; -import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; -import InfoCardTemplate from "../timeline-common/InfoCardTemplate"; - -export type PersonalTimelineManageItem = "avatar" | "nickname"; - -export type UserInfoCardProps = TimelineCardComponentProps< - PersonalTimelineManageItem ->; - -const UserInfoCard: React.FC<UserInfoCardProps> = (props) => { - const { - timeline, - collapse, - onMember, - onManage, - syncStatus, - toggleCollapse, - } = props; - const { t } = useTranslation(); - - const avatar = useAvatar(timeline?.owner?.username); - - return ( - <InfoCardTemplate - className={props.className} - syncStatus={syncStatus} - collapse={collapse} - toggleCollapse={toggleCollapse} - > - <div> - <BlobImage blob={avatar} className="avatar" /> - {timeline.owner.nickname} - <small className="ml-3 text-secondary"> - @{timeline.owner.username} - </small> - </div> - <p className="mb-0">{timeline.description}</p> - <small className="mt-1 d-block"> - {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} - </small> - <div className="text-right mt-2"> - {onManage != null ? ( - <Dropdown> - <Dropdown.Toggle variant="outline-primary"> - {t("timeline.manage")} - </Dropdown.Toggle> - <Dropdown.Menu> - <Dropdown.Item onClick={() => onManage("nickname")}> - {t("timeline.manageItem.nickname")} - </Dropdown.Item> - <Dropdown.Item onClick={() => onManage("avatar")}> - {t("timeline.manageItem.avatar")} - </Dropdown.Item> - <Dropdown.Item onClick={() => onManage("property")}> - {t("timeline.manageItem.property")} - </Dropdown.Item> - <Dropdown.Item onClick={onMember}> - {t("timeline.manageItem.member")} - </Dropdown.Item> - </Dropdown.Menu> - </Dropdown> - ) : ( - <Button variant="outline-primary" onClick={onMember}> - {t("timeline.memberButton")} - </Button> - )} - </div> - </InfoCardTemplate> - ); -}; - -export default UserInfoCard; diff --git a/Timeline/ClientApp/src/app/views/user/UserPageUI.tsx b/Timeline/ClientApp/src/app/views/user/UserPageUI.tsx deleted file mode 100644 index d405399c..00000000 --- a/Timeline/ClientApp/src/app/views/user/UserPageUI.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; - -import TimelinePageTemplateUI, { - TimelinePageTemplateUIProps, -} from "../timeline-common/TimelinePageTemplateUI"; - -import UserInfoCard, { PersonalTimelineManageItem } from "./UserInfoCard"; - -export type UserPageUIProps = Omit< - TimelinePageTemplateUIProps<PersonalTimelineManageItem>, - "CardComponent" ->; - -const UserPageUI: React.FC<UserPageUIProps> = (props) => { - return <TimelinePageTemplateUI {...props} CardComponent={UserInfoCard} />; -}; - -export default UserPageUI; diff --git a/Timeline/ClientApp/src/app/views/user/index.tsx b/Timeline/ClientApp/src/app/views/user/index.tsx deleted file mode 100644 index 7c0b1563..00000000 --- a/Timeline/ClientApp/src/app/views/user/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useState } from "react"; -import { useParams } from "react-router"; - -import { UiLogicError } from "@/common"; -import { useUser, userInfoService } from "@/services/user"; - -import TimelinePageTemplate from "../timeline-common/TimelinePageTemplate"; - -import UserPageUI from "./UserPageUI"; -import { PersonalTimelineManageItem } from "./UserInfoCard"; -import ChangeNicknameDialog from "./ChangeNicknameDialog"; -import ChangeAvatarDialog from "./ChangeAvatarDialog"; - -const UserPage: React.FC = (_) => { - const { username } = useParams<{ username: string }>(); - - const user = useUser(); - - const [dialog, setDialog] = useState<null | PersonalTimelineManageItem>(null); - - let dialogElement: React.ReactElement | undefined; - - const closeDialogHandler = (): void => { - setDialog(null); - }; - - if (dialog === "nickname") { - if (user == null) { - throw new UiLogicError("Change nickname without login."); - } - - dialogElement = ( - <ChangeNicknameDialog - open - close={closeDialogHandler} - onProcess={(newNickname) => - userInfoService.setNickname(username, newNickname) - } - /> - ); - } else if (dialog === "avatar") { - if (user == null) { - throw new UiLogicError("Change avatar without login."); - } - - dialogElement = ( - <ChangeAvatarDialog - open - close={closeDialogHandler} - process={(file) => userInfoService.setAvatar(username, file)} - /> - ); - } - - const onManage = React.useCallback((item: PersonalTimelineManageItem) => { - setDialog(item); - }, []); - - return ( - <> - <TimelinePageTemplate - name={`@${username}`} - UiComponent={UserPageUI} - onManage={onManage} - notFoundI18nKey="timeline.userNotExist" - /> - {dialogElement} - </> - ); -}; - -export default UserPage; diff --git a/Timeline/ClientApp/src/app/views/user/user.sass b/Timeline/ClientApp/src/app/views/user/user.sass deleted file mode 100644 index 5b7fcae7..00000000 --- a/Timeline/ClientApp/src/app/views/user/user.sass +++ /dev/null @@ -1,7 +0,0 @@ -.change-avatar-cropper-row - max-height: 400px - -.change-avatar-img - min-width: 50% - max-width: 100% - max-height: 400px |
